Skip to content

CLI-parity sessions: permission-mode, engine lifecycle, streaming, queueing, CI#2

Merged
Neverdecel merged 8 commits into
mainfrom
claude/session-cli-parity-lifecycle
Jul 9, 2026
Merged

CLI-parity sessions: permission-mode, engine lifecycle, streaming, queueing, CI#2
Neverdecel merged 8 commits into
mainfrom
claude/session-cli-parity-lifecycle

Conversation

@Neverdecel

Copy link
Copy Markdown
Owner

Summary

Continues the handoff1.md next-steps. The through-line: a hub session should behave like a native claude session started in the same directory — it inherits ~/.claude by default and is controlled via the CLI's own flags, not a parallel system the hub invents. Six focused, independently-reviewable commits plus a docs refresh.

Every change was driven end-to-end against the real Claude CLI 2.1.205, not just unit-tested.

What's in here

Commit Change
ci GitHub Actions: Node 20 + pinned pnpm via corepack → install/build/typecheck/test (all keyless; the live test self-skips).
feat(sessions) Config parity + permission unification. Per-session permissionMode--permission-mode, model--model, settings--settings, each inheriting ~/.claude when unset. The legacy 3-way approvalMode becomes a preset alias (approve_all→manual, approve_writes→acceptEdits, full_auto→bypassPermissions). Permission policy is delegated to the engine — the ApprovalBroker/READ_ONLY_TOOLS tool-name policy (and its spoofing sharp edge from the handoff) is retired; the hub just relays what the CLI surfaces. DB migration 2.
feat(sessions) Engine liveness + stop control. SessionDto gains engineState (new/live/hibernated/dead) + enginePid, a second state axis independent of the interaction status. New stop_engine WS control; a mid-turn crash closes the turn with done{error} instead of hanging. DB migration 3. UI: per-session liveness dots, an engine indicator + "stop engine" button, permission dropdown showing the CLI modes.
perf(web) Bundle trim. highlight.js/lib/core + a language subset: web bundle 1.11 MB → 229 KB (362 → 78 KB gzip); size warning gone.
feat(sessions) Queue mid-turn messages. A user_message sent while busy is buffered (bounded at 100) and drained on done instead of throwing session_busy (409). Web composer offers a "Queue" button.
feat(claude) Streaming. --include-partial-messages; stream_event text deltas map to onDelta (key ${messageId}:${textBlockIndex}) so assistant text streams and the final message reconciles the buffer.
docs Refresh README + CLAUDE.md for the new model; add handoff2.md. (CLAUDE.md was previously untracked; the now-accurate architecture guide is added to the repo.)

Notable outcomes

  • Retiring the hub-side tool-name policy eliminates the READ_ONLY_TOOLS spoofing sharp edge for free — the CLI does the write/read distinction natively via acceptEdits.
  • A local-run finding (a host PreToolUse hook auto-allowing a tool so the hub never saw it) is now intended, documented CLI-parity behavior rather than a bug.
  • "Process is dead" and "session is gone" are finally distinct states.

Verification

  • pnpm install/build/typecheck/test green: 19 passing (+2 live gated), typecheck clean, web bundle rebuilt.
  • Live drives: manual asks for Write while bypassPermissions runs it silently; --model sonnet reaches the CLI; migration 2/3 applied cleanly to existing sessions; engine live→dead on external SIGKILL (idle and mid-turn), dead→live respawn, live→hibernated on stop_engine; streaming deltas concatenate exactly to the final message text under the same messageKey.

Not in scope

Approval affordances (permission_suggestions), roadmap items (Copilot/Codex adapters, session fork, scheduling), event-replay pagination. See handoff2.md.

Wit, Floris De (178) added 8 commits July 9, 2026 12:59
Node 20 + pinned pnpm via corepack. Runs install --frozen-lockfile,
build (shared first), typecheck, and the keyless test suite (the live
claude CLI test self-skips). Protects the session refactor that follows.
…sion-mode

A hub session now behaves like a native `claude` session started in the same
directory. Per-session config passes through to the CLI and inherits ~/.claude
when unset:
  - permissionMode -> --permission-mode (canonical control)
  - model          -> --model
  - settings       -> --settings

The legacy 3-way approvalMode dial becomes a preset alias that maps onto the
CLI's native modes (approve_all->manual, approve_writes->acceptEdits,
full_auto->bypassPermissions), and the UI exposes the CLI modes directly
(incl. plan/dontAsk/auto) with an 'inherit' default.

Permission policy is delegated to the engine: the CLI applies --permission-mode
and only routes tools it wants asked to --permission-prompt-tool; the hub relays
those to the user. This retires the hub's READ_ONLY_TOOLS tool-name policy
(and its spoofing sharp edge) and the ApprovalBroker. The echo stub now applies
the mode itself (permissionModeDisposition) to stay a faithful template.

DB migration 2 adds permission_mode/requested_model/settings and backfills
permission_mode from the legacy approval_mode preset.

Verified live against claude 2.1.205: manual asks for Write, bypassPermissions
runs it silently, --model sonnet reaches the CLI; migration applied cleanly to
existing sessions.
Add a second state axis to SessionDto, independent of the interaction status:
  - engineState: new | live | hibernated | dead
  - enginePid: the live engine's OS pid (or null)

SessionRuntime tracks transitions: live on spawn, hibernated on idle-dispose,
stop, or boot reconcile, dead on an unexpected exit. A mid-turn crash also closes
the open turn with a done{error} instead of hanging in 'running'. New stop_engine
WS control tears the process down now while keeping the session resumable
(restart happens on the next message via --resume). Migration 3 adds engine_state.

UI: a per-session liveness dot in the sidebar, an engine indicator + 'stop engine'
button in the status bar, and the permission dial shows the CLI modes.

Verified live against claude 2.1.205: live->dead on external SIGKILL (idle and
mid-turn), dead->live on respawn, live->hibernated on stop_engine.
Import highlight.js/lib/core and register ~19 languages instead of the full
bundle. Cuts the web bundle from 1.11 MB to 229 KB (362 KB -> 78 KB gzip) and
clears the chunk-size warning. Unknown languages fall back to plaintext.
A user_message sent while a turn is running is now buffered (bounded at 100) and
drained one at a time on each 'done', instead of throwing session_busy (409).
interrupt/stop/dispose clear the queue (a deliberate stop discards pending work).
The web composer now lets you queue a follow-up while busy (button reads 'Queue').

Covered by a new e2e test: two back-to-back messages run in order with two dones
and no busy rejection.
Add --include-partial-messages and map the CLI's stream_event text_delta events
to onDelta, so assistant text streams token-by-token instead of arriving as a
whole block. Reconstructs the delta messageKey as ${messageId}:${textBlockIndex}
(mapping the absolute content-block index to normalize.ts's text-only ordinal) so
the final persisted message replaces the streaming buffer without duplication.

Verified live against claude 2.1.205: deltas arrive and concatenate exactly to
the final message text under the same messageKey.
Refresh README + CLAUDE.md: the approval broker / READ_ONLY_TOOLS 'safety dial'
is replaced by per-session --permission-mode delegated to the engine; document
the engineState/enginePid lifecycle axis, stop_engine, mid-turn queueing, and
Claude partial-message streaming. Add handoff2.md. (CLAUDE.md was previously
untracked; committing the now-accurate architecture guide.)
@Neverdecel Neverdecel merged commit f2e81e9 into main Jul 9, 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