Skip to content

feat: detached send, server-owned queue, steering, push-on-append - #17

Merged
thatswiftdev merged 2 commits into
mainfrom
wt/chat-detach
Oct 23, 2026
Merged

thatswiftdev merged 2 commits into
mainfrom
wt/chat-detach

Conversation

@thatswiftdev

Copy link
Copy Markdown
Contributor

Stage 1 of the Lionidas agent-chat parity plan (pentools LioClaw behaviour): plans/20261022T171041Z-agent-chat-pentools-parity-plan.md in Lionidas, B1 + B2 + B5 + the closing gate. The Claude Agent SDK provider (B1b) is a separate PR.

What changes

  • Detached send, opt-in. POST /api/agent/sessions/:id/messages with detach: true answers 202 at once: {startedTurnId} when idle, {queued: {queueId, disposition}} when busy (mode: 'steer' | 'queue'). It requires an execution authority (400 DETACH_UNSUPPORTED otherwise). Without detach the route behaves exactly as before: all existing suites pass unchanged.
  • Server-owned queue. Queue rows carry identity (queueId, body, request id, principal, fingerprint). Every removal writes its own queue.removed {reason: claimed | deleted | delivered} row. Adds DELETE /api/agent/sessions/:id/queue/:queueId.
  • Claim-all drain. After any admitted turn settles (held or detached), and once at boot, all pending items run as one turn (pentools' finishTurn). The claim, the user row (the items' own text + items[]) and turn.started are committed together inside admit(). The composed prompt goes to the model only.
    • Queue rows from before this change (no id) are never run automatically. The Lionidas dev DB holds two of them.
    • A runaway guard refuses to claim the same id twice in one process.
  • Steering at tool boundaries. Adds LoopDeps.takeSteering. Delivery commits queue.removed (delivered), steer.delivered and a user_message.recorded {via:'steer'} row in one transaction, so later turns keep the steer in model history.
  • Duplicate-submit guard. Before reserving, a detached send's clientRequestId is looked up across admitted turns and every queued item, pending or gone. Same content returns the existing id; different content returns 409 REQUEST_ID_REUSE.
  • EventSink.onAppend. Rows are pushed after the outermost commit only. Rolled-back rows are never pushed, and listener faults are isolated.
  • ExecutionAuthority.beginShutdown(). A closing gate the host calls before aborting turns, so a turn settling during shutdown can't start queued work.
  • session.notice rows. A detached turn retries a failed finalization with bounded waits, which stop at shutdown. If every retry fails, it writes a notice and leaves the attempt for recovery.
  • B5. Tool events with no invocation id get per-turn counter ids instead of the tool name, in the transport and in workflow runs.

Verification

  • tsc --noEmit is clean. vitest: 50 files, 310 passed (277 existing + 33 new), 5 skipped (as on main).
  • New tests: test/conversation-detach.test.ts (17, over real HTTP), test/event-sink-onappend.test.ts (7), test/turn-engine-queue.test.ts (9). The detach file passed 3 runs in a row.
  • Mutation-checked: 16 targeted breaks, each turning a test red. The breaks covered:
    • the closing gate, the steer memory row and the claim passthrough;
    • the duplicate-submit pre-check, the held-turn drain and the counter ids;
    • the flush guard and rollback truncation;
    • the legacy filter and the finalize retry, including stopping at shutdown;
    • the steer seam and the loop call;
    • engine dedupe, the claim-no-match guard and the display text.
  • Not covered yet: a live turn against a real model. That happens in the Lionidas UI stage with the operator watching.

…ing, push-on-append

- POST /messages {detach:true, mode:'steer'|'queue'} answers 202 at once
  (startedTurnId | queued); the held route is unchanged
- identity-bearing queue rows, one queue.removed row per removal,
  DELETE /api/agent/sessions/:id/queue/:queueId
- claim-all drain after any settled turn and at boot: all pending items
  run as ONE turn, claimed atomically inside admit(); user row keeps the
  items' own text + items, the composed prompt goes to the model only;
  legacy id-less rows never auto-run; runaway guard on repeat claims
- steer delivery at tool boundaries (LoopDeps.takeSteering) with a
  via:'steer' user row so later turns keep it in model history
- duplicate-submit guard across admitted turns and queued items
- EventSink.onAppend: push after the outermost commit, never rollbacks
- ExecutionAuthority.beginShutdown closing gate; session.notice rows;
  bounded finalize retry for detached turns (stops at shutdown)
- tool ids without an invocation id use a per-turn counter (B5)

Plan: lionidas plans/20261022T171041Z-agent-chat-pentools-parity-plan.md (B1/B2/B5)
@thatswiftdev
thatswiftdev merged commit 6281491 into main Oct 23, 2026
1 check passed
@thatswiftdev
thatswiftdev deleted the wt/chat-detach branch October 23, 2026 01:32
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