Skip to content

v0.72: auto-spawn tickets, in-place updates, rewind fix#14

Merged
AsteroidHunter merged 15 commits into
mainfrom
premain
May 26, 2026
Merged

v0.72: auto-spawn tickets, in-place updates, rewind fix#14
AsteroidHunter merged 15 commits into
mainfrom
premain

Conversation

@AsteroidHunter
Copy link
Copy Markdown
Owner

v0.72

New

  • Auto-spawned tickets / full dock coverage. Sessions already running before the daemon starts now appear in the dock. A boot-time pane scan seeds them, a SessionStart hook plus ~/.claude/sessions/<pid>.json persistence keep them identified, and placeholders reconcile to the real ticket on the first hook event.
  • expediter update / update.sh. Update in place instead of uninstall + reinstall. Pulls the latest (fast-forward only), rebuilds, and re-syncs the shims, helpers, and hook entries. Pass --dev / --no-pull to skip the pull (for contributors on a branch). New README "Update" section documents it.
  • "COOKING" label on a ticket while its session is actively working.

Fixed

  • Ghost tickets and the Idle-stuck working state.
  • Tickets no longer get stuck idle after a conversation rewind. Pane-ticket reconciliation now re-keys a pane's ticket to the live session_id (generalized from the old placeholder-only path) and clears any stale duplicate bound to the same pane.

Known issues

  • A state-change glitch that may appear only for chats linked with agent-view.
  • An edge case still under investigation. Fixes will ship via expediter update.

… reconciliation

Extended the ticketStore EventType union with 'Idle' and slotted it into EVENT_PRIORITY at -1 so any real event (Notification=0, Stop=1, PermissionRequest=2) supersedes it through the existing upsert priority guard with no branch changes. Added findByPane(tmux_pane) — linear scan over the store — that hook handlers will call to locate and remove a pending:<pane_id> placeholder before upserting the authoritative ticket keyed by session_id.

On the page side, mirrored the union into the local EventType alias, added Idle branches to typeClass ('type-idle') and typeLabel ('IDLE'), short-circuited staleClass for Idle (the type-idle CSS already applies filter: saturate(0), so stacking stale-N tiers would be redundant), and added the .ticket.type-idle CSS rule alongside the other .type-* rules.

Phase 1 of the boot-time-session-enumeration plan; no behavior change yet because nothing upserts Idle tickets until Phase 2 wires SessionStart and Phase 4 wires the boot scan.
…e scan for complete dock coverage

Phases 2-6 of the boot-time-session-enumeration plan land together because Phase 2's hook handler imports from the module Phase 3 creates; per-phase commits would leave broken builds.

install.sh: restructured EVENTS into (event_name, matcher) tuples and registered SessionStart three times (matcher = startup / resume / clear) since the matcher field accepts only single exact strings. Reworked the Python settings.json merge so the dedupe key is (matcher, hook_script-in-command) instead of bare command — without it, the three SessionStart blocks would collapse to one.

src/lib/server/sessionsStore.ts (new): loadSessions/recordSession/forgetSession/pruneStaleSessions backing ~/.expediter/sessions.json. Atomic temp+rename writes; the temp filename includes pid + monotonic counter + random suffix so concurrent writers don't share a temp path. Defensive per-entry shape validation drops malformed rows without losing the rest of the file. EXPEDITER_SESSIONS_FILE env var overrides the path for tests.

src/lib/server/bootScan.ts (new): runBootScan walks `tmux list-panes -a`, filters to claude/claude.exe processes, prunes orphans from sessions.json, then for each live claude pane either upserts from a persisted entry, resolves a --name argv flag against ~/.claude/projects/<slug>/*.jsonl via latestCustomTitle (named-session path), or seeds a `pending:<pane_id>` placeholder ticket. parseName handles both --name=foo and --name foo, single and double quoted. All shell calls go through execFile (no shell). Wired into hooks.server.ts as `void runBootScan().catch(...)`, gated on NODE_ENV !== 'test' so test imports don't shell out.

src/routes/api/hooks/event/+server.ts: new SessionStart branch (await recordSession, upsert Idle, fire-and-forget title pre-fill via latestCustomTitle). Extracted reconcilePlaceholder helper that removes any `pending:<pane>` ticket before upserting an authoritative one; called from SessionStart and from the existing Stop/PR/Notification path so pre-existing unnamed sessions reconcile on their first interaction regardless of event type. SessionEnd now awaits forgetSession. recordSession/forgetSession are awaited rather than fire-and-forget so the on-disk state is observable to the next request and can't race a subsequent write.

ticketStore.ts (Phase 1, already committed): exposes findByPane used by reconcilePlaceholder; Idle event_type slots into EVENT_PRIORITY at -1.

Tests: 197 pass, 0 fail across 13 files; svelte-check 0 errors. New tests cover Idle priority cases, findByPane, SessionStart upserts + recordSession side effect, placeholder reconciliation across all event branches, SessionEnd forgetSession, concurrent recordSession (parallel writers must not corrupt the file — fixed a real bug here where shared .tmp filenames stomped each other), pruneStaleSessions, slugify/parseName/parsePaneRows/isClaudePane/findSessionIdByName/upsertPlaceholder.
Detached panes were producing dead tickets — select-window / select-pane succeed but there's no Terminal.app window to bring forward. Added #{session_attached} to the tmux -F format, threaded session_attached through PaneRow, and gated the claude-pane filter on it. parsePaneRows now requires 5 columns; tests updated.
upsertPlaceholder gained an optional title parameter; runBootScan now passes the parseName result through the fall-through call so an explicitly-named session no longer renders a whimsical stub when findSessionIdByName misses. Synthetic pending:<pane> key preserved. Added two regression tests.
The pgrep+argv+jsonl-scan path missed claude --resume (no --name on CLI) and was racy at boot. runBootScan now reads claude's own per-pid metadata files via readSessionMetas, walks each up with ps -o ppid= to find the owning tmux shell, and uses the metadata's sessionId and name directly. Removed claudeArgvFor, parseName, findSessionIdByName and reverted the upsertPlaceholder title param; added parseSessionMeta with unit tests.
runBootScan now consults the live ~/.claude/sessions/<pid>.json metadata before the persisted ~/.expediter/sessions.json entry. The old order let a stale persisted entry (left behind when a claude exited without firing SessionEnd in a still-live pane) mask the live session, so the dock keyed the ticket by the dead session_id and markWorking calls from the live hook payloads silently no-op'd.

markWorking now lifts event_type from Idle to Stop on the way in. The .ticket.type-idle saturate(0) parent filter was desaturating the green working palette and the typeLabel kept reading "IDLE" while claude was actively processing.
Made runBootScan take injectable deps (listPanes/readSessionMetas/parentPid) so the metadata-vs-persisted ordering is testable. The detached-skip work rides along: listPanes now reads #{session_attached} and the seed loop skips detached panes, while keeping them in livePaneIds so persisted records aren't pruned.
…tickets

update.sh replaces the uninstall+install cycle: rebuilds the app, rewrites the
shims/config, re-copies the cc-clock/cc-dates helpers, and re-merges settings.json
hooks (idempotent, backed up), skipping first-install checks and warning rather
than aborting when the daemon is live. The ticket stub now shows COOKING instead
of the event-type label while a ticket is working.
…r rewinds

Replaced placeholder-only reconcilePlaceholder with dropPaneTicketsExcept (drops
any ticket on a pane not keyed by the live session_id) and rebindPaneTicket
(re-keys a pane's ticket to the live session_id before markWorking, so a rewind
that diverged the session_id no longer leaves the ticket stuck idle). Cancels
decline watchers for the displaced ids.
…mand

update.sh now fast-forward pulls the current branch before rebuilding (new Sync
phase), skipped by --dev/--no-pull, a dirty tree, missing .git, or non-ff
history — it never forces or merges, falling back to building the checkout
as-is. expediter.mjs gains an `update` subcommand that runs
$EXPEDITER_HOME/update.sh with extra args passed through, dispatched before the
daemon starts.
Updated the brand-version span in +page.svelte (v0.7 -> v0.72) and package.json
(0.0.1 -> 0.72.0) for the v0.72 release.
Added an Update section (mirroring Install/Uninstall) covering `expediter update`
and `./update.sh`, the fast-forward-only pull with the --dev/--no-pull escape
hatch, and the post-update daemon restart.
Swapped em dashes for hyphens, commas, and parentheses across update.sh (all new
this release) and the three lines added for the `expediter update` subcommand in
expediter.mjs. Pre-existing author comments elsewhere in expediter.mjs were left
untouched.
@AsteroidHunter AsteroidHunter merged commit ecc0353 into main May 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant