summaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-06-01fix(api): wake-schedule — accept skewed toggles, atomic persist, ↵Adam Malczewski
boot-recovery reason Three review-finding fixes in models.ts + regression tests: 1. POST /wake-schedule/toggle no longer rejects 'past' timestamps (Gemini #1, High). Client-server clock skew + request latency meant a freshly-computed nextOccurrenceAt(HH:MM) for an imminent slot could land in the past by the time the server validated it, silently failing the UI toggle. The scheduler's recoverScheduleEntry already fires within MISSED_WAKE_GRACE_MS and rolls forward by 24h × N, so the strict <= now check was actively harmful. Kept Number.isFinite + slot-present validation. 2. persistSchedule is now transactional (Gemini #3, Medium). The old DELETE-then-N-INSERTs path, when an INSERT failed mid-loop, left the table empty (DELETE had committed) and silently wiped the user's schedule on next boot — the catch swallowed the error. Wrapped both in db.transaction(...): on failure everything rolls back, in-memory state is untouched, and the previously persisted snapshot stays intact. 3. Boot-recovery reason no longer masked when boot recovery + due slots coincide (Gemini #5, Nit). Capture bootFireRequested before clearing the flag and append ' (boot recovery)' to the reason so the lastWake/pendingRetry surface tells the truth. Tests: - Replaced 'POST toggle rejects past timestamp' (the bug-as-feature test) with 'POST toggle ACCEPTS a slightly-past timestamp (clock skew / latency)' regression guard. - Added 'POST toggle rejects NaN / Infinity / non-number slot values' to lock the malformed-input path. - Added 'snapshot remains consistent across toggle round-trips (persistSchedule atomicity)' — exercises GET/POST cycles to ensure the transactional impl agrees with itself across add/remove. All 427 tests pass; biome clean.
2026-06-01docs: update HANDOFF.md with notifySubagents flag + bumped test countAdam Malczewski
2026-06-01feat(notifications): add notifySubagents toggle to suppress subagent turn pingsAdam Malczewski
A parent agent that spawns 8 subagents was producing 9 "Turn complete" notifications per round — almost always noise. New `notifySubagents` config flag (defaults to false) gates `turn-completed` and `turn-error` from any tab with a `parentTabId`. The flag is intentionally NOT applied to `permission-required` — a subagent's permission prompt still needs a human tap to proceed, so suppressing it would silently hang the subagent. `agent-spawned` is already top-level-only by construction. Wiring: - core/notifications/types.ts: NtfyConfig.notifySubagents: boolean - core/notifications/config.ts: defaults to false; normalize() tolerates missing / wrong-typed values and falls back to false - core/notifications/dispatcher.ts: new optional TabParentLookup option (getTabParentId). When notifySubagents=false AND the lookup returns a non-empty parent id string, turn-completed/turn-error are dropped. Lookup failures (no lookup configured, throws, returns undefined) fall back to "treat as top-level" so legitimate top-level events are never silently dropped when the DB is briefly unreadable. - api/app.ts: wires getTabParentId via core's getTab(id)?.parentTabId - frontend SettingsPanel.svelte: "Include subagent tabs" checkbox with an explanatory hint that permission prompts still fire Tests (+9): - 3 in config.test.ts: default-false, explicit-true, wrong-typed fallback - 6 in dispatcher.test.ts: suppression of turn-completed/turn-error from subagents, no suppression when flag is true, permission-required not gated, graceful fallback when lookup is missing/throws/returns undefined Live ntfy.sh round-trip re-verified (status: 200).
2026-06-01docs: update HANDOFF for 4-slot probing + DB migrationAdam Malczewski
2026-06-01feat(wake): probe 4 times per marked hour (:00 :15 :30 :45), coalesce ↵Adam Malczewski
same-tick fires Marking an hour on the Claude Wake Schedule panel now schedules FOUR probes within that hour instead of one. Rate-window edges are unforgiving — a single probe at :15 can miss the actual reset moment by up to 14 minutes; hitting :00 / :15 / :30 / :45 puts us within ~7 minutes of any reset that happens during that hour. When multiple slots come due in the same 30s scheduler tick (or recover together at boot), they coalesce into a SINGLE upstream wake call — no point hitting Anthropic 4× in the same window. DB schema - wake_schedule is now (hour, slot_minute, next_wake_at) PK (hour, slot_minute). Destructive migration: detect old single-row-per-hour schema by absence of the slot_minute column and DROP TABLE. No other table is touched. Per user direction: no back-compat for old rows. API - POST /models/wake-schedule/toggle add: { hour, timestamps: { '0': ms, '15': ms, '30': ms, '45': ms } } — all 4 slots required, all must be future Unix ms. Delete shape unchanged ({ hour }). - GET /models/wake-schedule shape: schedule: { '9': { '0': ts, '15': ts, '30': ts, '45': ts }, ... } probeSlotMinutes: [0, 15, 30, 45] resetOffsetHours, lastWake, pendingRetry (unchanged from prior commit) Frontend - Computes 4 timestamps client-side (next occurrence of HH:MM in local TZ) and sends them in one request. - markedHours summary now says 'Probes :00 :15 :30 :45 → reset by ~Xh later'. - Same in-flight tracking / current-hour ring / status row as before. Tests - wake-scheduler.test.ts unchanged (pure helpers still correct; added PROBE_SLOT_MINUTES + isProbeSlotMinute exports). - routes.test.ts rewritten for the new payload shape: 12 wake-schedule tests covering snapshot shape, add/remove (full 4-slot round-trip), validation (range, integer, past-slot, missing slot, non-object, missing timestamps), independent multi-hour scheduling, and re-toggle replacement. 417 tests total (was 414).
2026-06-01docs: update HANDOFF.md with Gemini review triage + post-fix stateAdam Malczewski
2026-06-01fix(notifications): address Gemini review — tighten validation, sanitize ↵Adam Malczewski
Click, support Basic auth, non-optimistic UI clear Acted on 4 of 6 findings from the gemini-3-flash-preview second-opinion review (the other 2 were verified-wrong or judged not worth the complexity — see HANDOFF.md). core/src/notifications/ntfy.ts: - validateTopicUrl now enforces ntfy's actual topic-name constraints: exactly one path segment, 1–64 chars, charset [A-Za-z0-9_-]. Prevents users from saving topic URLs that look fine but silently 404 at publish time (cf. binwiederhier/ntfy#1451 for the 64-char limit and binwiederhier/ntfy's topic-name regex for the charset). - Click header now passes through sanitizeHeader, closing the same CRLF-injection vector that Title/Tags already had. - Authorization header construction now factors through a small buildAuthHeaderValue helper: a value that already starts with a scheme token ("Bearer xyz", "Basic dXNlcjpwYXNz") is used verbatim, so users of private ntfy servers that want Basic auth can paste the full header value. Bare tokens still get the "Bearer " prefix automatically. frontend/SettingsPanel.svelte: - clearNtfyAuthToken() was optimistic: it flipped hasAuthToken=false locally before awaiting the network call. If the request failed the UI lied about server state, and worse — a subsequent Save() with authToken:undefined would silently re-arm the original token. Now awaits the response, surfaces failures via the existing ntfySaveError banner, and only mutates local state on success. Adds a ntfyClearingToken loading flag so the button disables + spins during the request. Tests: +6 in ntfy.test.ts (multi-segment rejection, charset rejection, length boundary, 64-char acceptance, Basic auth pass-through, Click sanitization). All 442 tests pass; biome clean; svelte-check clean; manual ntfy.sh end-to-end re-verified.
2026-06-01docs: update HANDOFF.md with post-review fixes and triageAdam Malczewski
- Document the Gemini Flash 3 Preview review pass. - Record triage table of all 4 findings (1 fixed Major, 1 deferred Minor, 1 subsumed by the fix, 1 fixed nit). - Update verification numbers (404 tests, 142 files checked, 167 modules built). - Update manual smoke-test instructions to cover the fresh-install theme bug regression (was the headline finding). - Note the open recommendations (System theme, storage-key audit, ordering) that were intentionally deferred.
2026-06-01a11y(sidebar): label the remove-panel button for screen readersAdam Malczewski
Gemini review nit. The ✕ button on each sidebar slot (idx > 0) was read by screen readers as "multiplication sign" or "cross". Adds aria-label="Remove panel" so the action is announced clearly. Also gitignore claude-report.md (Gemini review artifact, not source).
2026-06-01fix(theme): consolidate boot apply and Settings picker into shared moduleAdam Malczewski
Gemini review surfaced that App.svelte (onMount theme apply) and SettingsPanel.svelte (theme <select>) hand-rolled their own defaults and could disagree: - App.svelte only set data-theme if localStorage had a value, so on a fresh install daisyUI fell back to the first theme in app.css (light). - SettingsPanel.svelte hardcoded a UI default of "dark". Result: a first-time user saw a light app but a Settings panel that claimed "dark" was selected. Picking *any* value in the dropdown was the only way to reconcile reality with the UI. This commit: - Adds packages/frontend/src/lib/theme.ts as the single source of truth: THEMES list, Theme type, THEME_STORAGE_KEY, DEFAULT_THEME, plus loadStoredTheme() and applyTheme() that handle SSR / private-mode / bad-value cases. - Rewires App.svelte's onMount to call applyTheme(loadStoredTheme()), so the boot apply always writes a known good theme to the DOM (even on fresh installs), matching what Settings will show. - Rewires SettingsPanel.svelte's picker to use the shared module, dropping its duplicate THEMES const, duplicate storage key, duplicate apply/persist logic, and the conflicting "dark" fallback. - Adds 11 unit tests in tests/theme.test.ts covering the default-fallback, known/unknown stored values, SecurityError-on-read, SSR (no localStorage), DOM-attribute write, persistence round-trip, and the "DOM still updates if storage write throws" contract. The daisyUI plugin block in app.css still lists themes — that's a CSS-time concern and can't be imported from TS, so it's kept in sync by convention (noted in the new module's doc comment).
2026-06-01docs: HANDOFF.md for claude-reset-fix branchAdam Malczewski
2026-06-01fix(frontend): ClaudeReset — live currentHour, real Set derived, in-flight ↵Adam Malczewski
tracking, status row Bugs fixed - fadedHours was $derived((): Set => {...}) — returned a *function*, not a Set. blockClass() then called fadedHours() once per of the 24 buttons, defeating Svelte's memoization. Now uses $derived.by(() => Set), passed in to blockClass as a value. - currentHour was $derived(new Date().getHours()) which is computed once on mount and never updates. After midnight (or any hour boundary) the 'now' ring stayed on the wrong block. Now driven by a nowMs $state bumped by a 30s setInterval, cleaned up on destroy. - Rapid double-clicks could land out of order ('last response wins, not last click'). Now tracks an in-flight Set + per-hour sequence counter; stale responses are dropped and pending buttons are disabled. - No feedback on wake success/failure. Snapshot now includes lastWake + pendingRetry, surfaced as a colored status row. Cleanups - resetOffsetHours pulled from the server snapshot (was hardcoded +5). - fadedHours window is now resetOffsetHours - 1 (was hardcoded 4). - onclick handler short-circuits when the hour is already pending.
2026-06-01fix(api): wake scheduler — missed-wake recovery, retry consolidation, ↵Adam Malczewski
status surface Bugs fixed - Missed wakes silently lost. The old loadScheduleFromDB just pushed any past next_wake_at to its 'next occurrence' in *server* local time, so a wake that fired while the API was down never ran — defeating the whole point of the panel (overnight task picks up after a 5h rate-window reset). Now: if missed by <= 2h we fire it on the next tick; either way the entry is rolled forward by 24h-multiple steps. - Server-TZ drift. nextOccurrenceAt15 used the server's local TZ, so on a UTC Docker host running for a user in PST the reschedule slowly migrated the fire time. Now we advance by 24h * N from the original client-supplied timestamp, preserving the user's wall-clock intent. - Retry storm. Every failed wake pushed a new entry into a retries[] array, all converging at the same +5min instant. Replaced with a single shared pending-retry slot whose budget resets on subsequent failures. - Retry race with fresh fires. If a tick fired AND a retry was due in the same iteration we'd double-hit the upstream. Now retries only run on ticks where no fresh wake fired. New behavior surfaced on /wake-schedule: { schedule, resetOffsetHours, lastWake, pendingRetry } POST /wake-schedule/toggle now also rejects non-integer hours (4.5, etc.) and returns the same snapshot shape so the client can stay in sync. Tests: 9 new HTTP route tests covering snapshot shape, add/remove, validation (range, integer, past timestamp, missing timestamp), and independent multi-hour scheduling.
2026-06-01feat(api): extract pure wake-scheduler helpers (nextDailyAfter, ↵Adam Malczewski
recoverScheduleEntry) Side-effect-free module so missed-wake recovery and rescheduling can be unit-tested without booting Hono or touching SQLite. - nextDailyAfter: advances by 24h increments until strictly > now (handles multi-day gaps in a single step instead of looping a day at a time). - recoverScheduleEntry: classifies a past next_wake_at into 'fire now, then advance' vs 'silently advance' based on MISSED_WAKE_GRACE_MS (2h). - CLAUDE_RESET_OFFSET_HOURS / resetHourFor: single source of truth for the '+5h reset' display, previously hardcoded in three places. Includes 12 unit tests covering grace boundaries, multi-day skip, custom grace windows, and midnight wraparound.
2026-06-01docs: add HANDOFF.md for ntfy notifications featureAdam Malczewski
2026-06-01feat(frontend): ntfy.sh settings block in SettingsPanelAdam Malczewski
Adds a 'Notifications (ntfy.sh)' section below 'Backend URL' with: - Enable toggle (master switch) - Topic URL field (with security hint: anyone with the URL can read) - Optional auth token (password input; placeholder reflects whether one is already stored, and a 'Clear stored token' button surfaces only when hasAuthToken=true) - Per-event-type checkboxes driven by the eventTypes catalog returned from GET /notifications (so adding a new event type in core doesn't require a frontend change) - Save + Send test buttons, with inline success/error feedback The component hand-mirrors the NtfyConfig shape rather than importing it from @dispatch/core — matching the existing pattern (lib/types.ts mirrors a few core types) to keep node-only barrels out of the browser bundle.
2026-06-01feat(api): wire notification dispatcher into app + /notifications routesAdam Malczewski
PermissionManager: add onPromptAdded(listener) callback. Fires exactly once per unique pending prompt id, even when broadcastPending is called repeatedly for unrelated mutations (e.g. another prompt resolving while this one is still pending). app.ts: instantiate NotificationDispatcher, attach to both AgentManager and PermissionManager. Tab-title lookup via core's getTab so the notifications carry human-readable context instead of raw UUIDs. routes/notifications.ts: - GET /notifications — current config (auth token redacted) plus the event-type catalog and defaults - PUT /notifications — partial update; auth token semantics are undefined=keep, ''=clear, otherwise replace - POST /notifications/test — sends a test notification with the current config (rejects if disabled or topic invalid) Tests: - new permission-manager.test.ts covers the onPromptAdded contract (one-fire-per-prompt, dedup across rebroadcasts, unsubscribe, listener throws don't break siblings) - existing routes.test.ts gets stubs for the new core notification exports so the @dispatch/core mock stays complete
2026-06-01feat(core): ntfy.sh notification dispatcher moduleAdam Malczewski
Adds a transport-agnostic NotificationDispatcher and a fire-and-forget ntfy.sh transport (no SDK; just fetch). Configuration is persisted as a single global JSON blob under the 'ntfy_config' settings key. Event taxonomy (per-event toggles): - turn-completed — assistant turn finished cleanly - turn-error — final turn error (after all fallbacks) - permission-required — new permission prompt was created - agent-spawned — top-level user-agent tab spawned via 'summon' Design: - Single internal notify(event) interface so a future transport (email, webhook) plugs in without changing call sites. - attachToAgentManager + attachToPermissionManager subscribe to the existing event streams via narrow listener interfaces (no @dispatch/api dependency back into core). - 5s in-memory dedupe window on dedupeKey suppresses permission re-emits. - 10s per-request abort timeout so a hung ntfy server can't pin a worker. - All sends are fire-and-forget: void Promise.resolve(...).catch(warn). Tests (39 new): - ntfy transport: URL/headers/body/auth/click, header sanitization, per-event-type defaults, error paths. - config: defaults, normalization tolerance, round-trip, redaction. - dispatcher: master switch, per-event toggle, dedupe, agent/permission hookups, top-level-only filtering for agent-spawned, dispose.
2026-06-01docs: add HANDOFF.md for h3 header declutterAdam Malczewski
2026-06-01feat(settings): inline theme picker into Settings panelAdam Malczewski
The Theme button + ThemeSwitcher modal were a header-triggered modal. That doesn't belong in a sidebar-panel architecture, and theme picking is a UI preference that belongs alongside the other Settings entries. - Add a "Theme" section as the first block in SettingsPanel with the same theme list as ThemeSwitcher. - The localStorage key (`dispatch-theme`) and apply-on-change behavior are unchanged, so the boot-time theme apply in App.svelte's onMount keeps working without modification. - Delete the now-unused ThemeSwitcher.svelte component; no remaining importers.
2026-06-01feat(sidebar): add Debug panel with copy-conversation actionAdam Malczewski
New "Debug" panel option in the sidebar, grouping dev-facing actions. Currently exposes the Copy-conversation button (ported from the old header). Leaves room for additional debug actions without re-cluttering the header. The Copy action wraps `tabStore.copyConversation()` and shows a "Copied"/"Failed" affordance for 1.5s, matching the previous header behavior.
2026-06-01feat(header): remove copy + theme buttons; keep title, status, sidebar toggleAdam Malczewski
These move to dedicated sidebar panels (Debug panel and Settings panel respectively) in follow-up commits. Header is now visibly cleaner: only the Dispatch title (left), connection status indicator, and the Sidebar toggle (right) remain.
2026-06-01test(queue): cover multi-message continuation collapseAdam Malczewski
Add a frontend store test (flagged by a Gemini review) that queues TWO messages mid-turn and asserts they collapse into a single untagged initiator row joined with "\n---\n" — matching the backend's joined user turn — and that the next turn-start tags that single row. The prior test only covered the single-message case, leaving the join logic structurally correct but untested.
2026-06-01fix(queue): consume queued messages after a turn ends (start a new turn)Adam Malczewski
A message queued while the agent was mid-turn was only handled if it arrived DURING a tool batch (injected as a [USER INTERRUPT]). If it landed after the last tool call — or the turn had no tools — the agent silently appended it to history and ended the turn with no response, so it sat there unanswered. This affected both user-queued messages and agent-queued ones (send_to_tab). - agent.ts: stop the end-of-turn drain that swallowed trailing queued messages into history. They now stay on the queue. - agent-manager: after a CLEAN turn settles, continueFromQueue() drains the queue and starts a fresh turn to answer it. Skipped on a user-stopped or errored turn (queue preserved for the next send). - Loop safety: continuation draws from the existing autoWakeBudget, so a runaway agent<->agent chain is bounded; human sends refill it, so human conversations are never throttled. - dequeueMessages now tags message-consumed with reason "interrupt" | "continuation"; the frontend collapses continuation- consumed queued bubbles into the next turn's initiator row (avoids the linger/dup traps documented in queue-interrupt-reconcile-edge-cases.md). - Tests: agent (no-swallow + interrupt regression), agent-manager (continuation, no-op when empty, user-stop preserves queue, bounded loop), frontend (continuation bubble becomes next initiator). - wishlist: remove the now-fixed item.
2026-06-01feat(tabs): tab-to-tab agent communication via short handlesAdam Malczewski
Add send_to_tab / read_tab tools so an agent can message or read another tab by a git-style short handle (shortest unique prefix of the tab UUID, min 4 chars), shown in the tab bar. - core/db/tabs: resolveTabPrefix + shortestUniquePrefix (open tabs only, LIKE-sanitized prefix matching) - new tools read-tab.ts / send-to-tab.ts (+ tests) decoupled from the DB TabRow via a minimal ResolvedTabRef projection - agent-manager: unified deliverMessage routing (busy -> queue, idle -> new turn) shared by POST /chat and send_to_tab; agent->agent auto-wake budget (MAX_AGENT_AUTO_WAKES) to bound ping-pong loops - summon/loader: send_to_tab + read_tab as grantable tools - frontend: shortHandleFor + handle badge in TabBar; perm toggles - notes: tab-comm / user-agents / todo-redesign plans - chore: biome format fixes (debug-logger, summon.test) Refs notes/plan-tab-comm.md
2026-05-31feat(debug): wire LLM debug logger end-to-endAdam Malczewski
The debug-logger.ts module existed but was completely orphaned — none of its functions had any callsites, so DISPATCH_DEBUG_LLM=1 did nothing. Wires it in across the stack: - llm/debug-logger.ts: add wrapFetchWithLogging() that tees SSE bodies via TransformStream + response.clone() so we capture every chunk without draining the body the AI SDK consumes. Redacts authorization / x-api-key / cookie headers in logs. Also exports nextDebugSeq() so requests and log files share an id. - llm/provider.ts: all 3 factories (Claude OAuth, plain-API-key Anthropic, OpenAI-compatible) now pass fetch: wrapFetchWithLogging(globalThis.fetch). For Claude OAuth the wrap goes on the inner base fetch so logged bodies reflect the post-transform shape + Claude-Code session headers. Added tabId to ProviderConfig for log labelling. - agent/agent.ts: threads tabId through createProvider and emits logAgentLoop / logStepLifecycle / logStreamEvent at every meaningful point in the run loop — step start/end, tool count, every fullStream event. All are no-ops when DISPATCH_DEBUG_LLM is unset. - core/index.ts: re-exports the debug helpers. - tests/llm/provider.test.ts: switch one full-object equality assertion to property assertions so the test survives the new fetch: wrapper. Plumbing the env var into the container required three more fixes: - bin/up: re-export DISPATCH_DEBUG_LLM* so docker compose forwards them (compose only forwards vars referenced in the environment: block). Also pre-creates /tmp/dispatch/llm-debug and chowns it on first run so the container's UID-1000 bun process can write into it without EACCES. - docker-compose.yml: declare the debug vars on api.environment and bind-mount /tmp/dispatch/llm-debug:/tmp/dispatch/llm-debug so logs are inspectable from the host without docker exec. - docker/entrypoint.dev.sh: explicitly forward DISPATCH_DEBUG_* through the 'su -' login-shell barrier — su - resets the environment to TERM/ PATH/HOME/SHELL/USER/LOGNAME only, silently stripping everything else. This is why the vars appeared via 'docker exec env' (which spawns a new process inheriting the container env) but were absent from the actual bun process's /proc/<pid>/environ. bin/build: drop stray sudo for consistency with bin/up and bin/down.
2026-05-31feat: implement user agents (top-level tabs via summon)Adam Malczewski
- agent parameter is now required on summon tool - new top_level param spawns independent fire-and-forget user agent tabs - gated by perm_user_agent permission (UI checkbox added) - agent definition type validation (subagent vs user-agent slug mismatch) - context-aware error messages when agent slug not found - read_file_slice added to summon tool's allowed tools enum - updated and expanded summon tests
2026-05-31docs(notes): expand wishlist with layout-restore and edit-history itemsAdam Malczewski
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30fix(agent): stream thinking for all adaptive Claude models, not just Opus 4.7Adam Malczewski
Extended thinking was gated on a hardcoded `model === "claude-opus-4-7"` check, so newer/other adaptive models (Opus 4.8, Opus/Sonnet 4.6) fell into the classic `thinking: { type: "enabled" }` branch. Adaptive models default thinking display to "omitted", so no thinking was streamed — the UI showed nothing for Claude while DeepSeek (a separate openai-compatible path) worked. Replace the string check with a pure helper `anthropicThinkingProviderOptions` that mirrors opencode's transform.ts detection: - adaptive (`type: "adaptive"`) for Opus 4.7+ (version-parsed) and Opus/Sonnet 4.6 (id substring; handles dash and dot forms); - `display: "summarized"` ONLY for Opus 4.7+ (they default to omitted and must be forced); Opus/Sonnet 4.6 stream thinking without it; - all other Claude models keep classic `enabled` + budgetTokens. Pure function (no provider/streamText/network), unit-tested directly: Opus 4.8 (the reported bug), Opus 4.7, Sonnet/Opus 4.6, Opus 4.5 + dated Sonnet (enabled), a future Opus 4.9 (proves version-parse), and effort->budget mapping.
2026-05-30fix(frontend): sidebar fill panels overflow with 3+ views openAdam Malczewski
Key Usage (plus Tasks and Cache Rate) used flex-1 + min-h-0, letting flex shrink the panel below its content's natural height. The content wrapper was a plain block, so inner scroll regions never got a bounded height and their bars/lists spilled into neighbouring panels or past the window edge. Drop the flex-1 fill entirely: every panel now sizes to its content and the sidebar's own overflow-y-auto handles scrolling.
2026-05-30chore: gitignore local scratch/sensitive files (cyberdeck/, .old_tabs.svelte.ts)Adam Malczewski
2026-05-30chore(notes): collect loose root docs into notes/; add reconcile edge-cases noteAdam Malczewski
Move all loose root-level .md files (plans, reports, gemini reviews, incident notes) into a single notes/ directory, and update the doc-reference breadcrumbs in code comments/test labels to the notes/ path. Add notes/queue-interrupt-reconcile-edge-cases.md: documents why the queue/interrupt/turn-sealed reconcile path keeps surfacing edge cases (a catalog of the four review-pass bugs, the no-loss/no-duplicate invariants, the recommended membership-based reconcile refactor, and interleaving-test guidance).
2026-05-30feat(chunks): chunk-native frontend store with turn-sealed reconcile + ↵Adam Malczewski
per-chunk eviction Replace the stored ChatMessage[] with a chunk-native model: tab.chunks (sealed ChunkRow[]) + tab.live (transient in-flight turn buffer) + derived tab.renderGroups. This enables per-chunk eviction (trimming WITHIN a large turn) and raw-chunk pagination (loadOlderChunks), removing the whole-message eviction limitation. Backend: - Emit turn-start/turn-sealed around each turn; expose currentTurnId in the status snapshot. turn-sealed fires after the durable write (status:idle fires before it). - New GET /tabs/:id/chunks raw paginated endpoint (limit/before). - Wrap appendChunks in a single SQLite transaction. Frontend: - turn-sealed drives a turn-aware reconcile that folds the sealed turn into chunks while preserving a concurrent newer in-flight turn and pending queued messages; deferred while the user is scrolled up. - Stable turn-scoped render keys (${turnId}:${role}:${n}) avoid remount/flash. Reconcile correctness (three review passes): - preserve a concurrent newer turn when an earlier deferred reconcile flushes; - keep optimistic queued user messages (no loss); - turn-start backfill skips pending queued rows and tags only the turn initiator; - bind consumed interrupt messages to the in-flight turn so they collapse on seal (no lingering/duplicated bubble). Tests: chat-store reconcile/eviction/pagination suite; api chunks endpoint + events.
2026-05-30docs(chunks): eviction limitation + gemini review of the chunk-log refactorAdam Malczewski
- eviction-limitation.md: frontend eviction is whole-message, not per-chunk; options to fully fix later. - gemini-chunk-log-review.md: read-only review of the refactor (cache fix, flat storage, pagination).
2026-05-30refactor(chunks): append-only chunk log with per-step cache-stable wireAdam Malczewski
Replace the message-as-container model with a flat, append-only chunk log. - chunks table (id, tab_id, seq, turn_id, step, role, type, data_json): one row per chunk; tool_call (assistant) and tool_result (tool) are SEPARATE rows linked by callId. Message/turn are derived groupings, not stored. - chunks/transform.ts: DB-free explode (Chunk[] -> rows) / group (rows -> messages), shared by backend and the browser frontend. - Cache fix: toModelMessages segments each turn at tool-batch boundaries into stable [assistant, tool] pairs per step, so earlier steps serialize byte-identically across requests (kills the prompt-cache churn). - agent-manager persists a turn's chunks on seal (once), discarding a failed fallback attempt's partial chunks; rebuilds agent history from the log. - GET /messages windows the log by chunk seq then groups; loadMoreMessages merges a turn split across the window boundary by turnId. - One-shot migration drops the legacy messages table and clears tabs; settings/credentials/keys/usage preserved. Full suite green (317 tests); biome, tsc, and svelte-check clean.
2026-05-30docs(cache): cache-miss investigation report + append-only chunk-log ↵Adam Malczewski
refactor plan - cache-miss-report.md: root-causes the prompt-cache churn (multi-step turns reshuffle their own wire prefix every step) with evidence and file refs - plan-chunk-log.md: executable plan to move to a flat append-only chunk log, fixing both cache stability and per-chunk frontend pagination
2026-05-30feat(cache): Anthropic prompt caching, usage telemetry, and Cache Rate viewAdam Malczewski
- send prompt-caching + oauth anthropic-beta headers on the Claude OAuth provider - restructure the OAuth request body (billing header, identity split, relocate third-party system prompt to the first user message) to match Claude Code - apply rolling cache_control breakpoints and group a turn's tool results into a single role:tool message for correct breakpoint placement - emit per-step usage events (cache read/write split) and add the Cache Rate sidebar panel - dedup byte-identical tool calls within a single batch
2026-05-29fix(claude): eliminate /home mount race that blanks Claude credentials at bootAdam Malczewski
On hosts where /home is a separate filesystem, the dispatch-api service could start before /home was mounted. The API's first DB access then failed (EACCES: mkdir '/home/tradam'), Claude account discovery silently caught the error and left claudeAccounts empty, and -- because discovery only ran in the constructor -- it stayed empty for the whole process lifetime. Every Claude message then fell back to the deepseek-v4-flash / empty-key defaults, producing a 401 'Missing API key' from OpenCode Zen. Fixes: - s6 run script waits (capped ~30s) for /home/tradam before exec'ing bun; passes instantly where /home is on the root filesystem. - systemd unit gains RequiresMountsFor=/home and After=...home.mount. - agent-manager re-runs _refreshClaudeAccounts() on config hot-reload and lazily on an empty cache in the Anthropic path, so a process that lost the boot race self-heals on the next request instead of staying broken.
2026-05-29Make chat input an auto-growing textareaAdam Malczewski
Replace the single-line input with a textarea that starts at one line, grows vertically as text wraps (up to 7 lines), then scrolls. Override daisyUI's .textarea min-height:5rem so the resting state is one line. Enter sends, Shift+Enter inserts a newline.
2026-05-29docs(wishlist): add token usage tracking, queue-not-consumed bug, compaction ↵Adam Malczewski
tool
2026-05-29fix: preserve per-tab working directory on sendAdam Malczewski
refreshAgentConfig (added to pick up model/key edits on send) also overwrote the tab's workingDirectory with the agent's default cwd every time, discarding the directory the user set via setWorkingDirectory. Remove that line so refreshing agent config no longer clobbers the per-tab working directory. Also merge top-level wishlist.md into dispatch/wishlist.md and drop the items completed today (subagent summon, stop button).
2026-05-29fix(packaging): add notification-fd to dispatch-s6 loggersAdam Malczewski
The dispatch-{api,frontend}-log run scripts invoke `s6-log -d3` (send a readiness notification on fd 3), but the service dirs lacked a notification-fd file, so s6-supervise never opened fd 3. s6-log aborted with "invalid notification fd: Bad file descriptor" (exit 100) and crash-looped, so nothing drained the producer's stdout pipe. The API then filled its 64KB stdout pipe and blocked in write() before reaching listen(), so port 18390 never opened and the frontend could not reach the backend (the frontend survived only because it logs almost nothing). Add notification-fd=3 to both logger service dirs and install them via PKGBUILD, matching every other logger on the system. This also makes s6-rc bring the logger up ready-first, preventing the pipe-fill race.
2026-05-29fix: override pointer-events on stop button spinner for iOS touch supportAdam Malczewski
daisyUI's .loading class sets pointer-events:none on the spinner. On iOS Safari, when a child inside a <button> has pointer-events:none, the synthesized click event from touch sequences can fail to dispatch to the parent button entirely - a well-known WebKit quirk. Since the spinner covers ~20x20px of the button, mobile taps that land on it are silently dropped. Setting pointer-events:auto on the spinner lets touch events pass through to the parent <button> correctly.
2026-05-29feat: stop generation button with abort signal plumbingAdam Malczewski
- Add POST /chat/stop endpoint on API - Thread abortSignal from agent-manager through Agent.run() to streamText - Thread abortSignal option through the Agent.run() signature - Emit status:idle on stopTab() so frontend WS gets the update - Add stopGeneration() store method on frontend tabStore - Add stop button in ChatInput (btn-sm lg:btn-xs for mobile tap target) - Add tests for /chat/stop endpoint - Refactor processMessage to pass abortSignal to agent.run
2026-05-29fix: handle unavailable tool calls via native v6 tool-error event, not ↵Adam Malczewski
synthetic invalid tool - Removed __invalid__ tool definition, experimental_repairToolCall, and v4-era NoSuchToolError catch block — AI SDK v6 already emits a native tool-error stream event with the original tool name - Added synthesizeResidualToolResults() helper to fill orphaned tool-call IDs with isError: true results for abort/error terminal paths - tool-error handler now break's instead of return's — lets sibling tools execute normally via the manual executor loop - Added final safety net after execution loop to catch any genuinely orphaned tool-call IDs before round-tripping to the LLM - Propagated isError through toModelMessages so error results are properly flagged in conversation history - Updated tests: tool-error event now continues to idle (not error), added sibling-orphan prevention test
2026-05-29fix: include agent_id in foreground summon results for Open Tab button; ↵Adam Malczewski
preserve slider selection on agent config refresh
2026-05-29feat: subagent summon — catalog filter, error hints, system prompt, ↵Adam Malczewski
AgentBuilder default, SubAgent mode display - Filter summon tool catalog to is_subagent-flagged agents only - Return fresh subagent list in error when slug not found - Add subagent hint to system prompt when summon tool available - Default is_subagent checkbox to true in AgentBuilder - Fix tab-created event to include agentSlug and agentModels - Add SubAgent read-only mode to ModelSelector with model slider
2026-05-29fix: refresh agent config on send; widen fallback retry detectionAdam Malczewski
- Refresh agent config from API before sending a message so edits in AgentBuilder (changed keyId/modelId/agentModels) take effect immediately on existing tabs instead of using stale snapshots - Broaden isRetryable check to also match 'usage limit' and 'exhausted' so fallback keys are actually tried on quota errors
2026-05-29feat: disappearing chat history — chunk-limited frontend window with ↵Adam Malczewski
backend pagination Frontend keeps only a bounded window of chunks in memory (configurable via settings slider, default 100). Older messages are evicted when at the bottom and re-fetched from the backend on scroll-up. - Backend: paginated GET /tabs/:id/messages with ?limit=N&before=seq - Store: evictMessages trims oldest messages until total chunks ≤ limit - Store: loadMoreMessages fetches next page and prepends with dedup - ChatPanel: smart scroll hooks trigger eviction on return-to-bottom - ChatPanel: onNearTop loads older history with scroll-position maintenance - Settings: chunk limit slider in Memory section - Fix: oldestLoadedSeq recalculated after eviction (pagination cursor stays valid) - Fix: seq preserved on ChatMessage for cursor tracking - Fix: scrolledUpTabs cleaned up on tab switch (no memory leak) - Fix: evictMessages reads appSettings.chunkLimit directly (live updates)
2026-05-29fix(packaging): convert dispatch-systemd from user units to system template ↵Adam Malczewski
units (User=%i) The per-user systemd manager ([email protected]) fails to start on WSL (kernel 6.6.87.2, microsoft/WSL#13186 — 'Failed to spawn executor: Device or resource busy'), which breaks pacman's 30-systemd-daemon-reload-user.hook on install. Changes: - New [email protected] + [email protected] system template units with User=%i (run as the named instance user) - Remove old user-scope dispatch-api.service / dispatch-frontend.service - Install to /usr/lib/systemd/system/ instead of .../systemd/user/ - Update PKGBUILD, .install hints, and bin/service to use sudo systemctl dispatch-api@<user>