Repository navigation
Control IPC versioned hello + consent hardening + one execution domain - #430
Conversation
…vision surface Slice-2 pre-work of the desktop supervisor umbrella: versioned hello frame, subscriber grace for the no-UI consent deny, pump-only receive-loop unparking, and the StatusSubscribe/DaemonStatus supervision surface. Two PRs, one spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pre-hello daemon's codec throws on frame byte 15 and drops the connection without a reply, so the app detects a down-level daemon by the absent HelloReply, not by an Error frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…subscribe Review findings on the subscriber-arrival waiter state machine: - Unsubscribe re-armed _subscriberArrival whenever the map was left empty, including a 0->0 no-op remove (unknown/duplicate id). That orphans any waiter holding the pre-existing generation's source until its full wait budget expires. Gate the re-arm on TryRemove actually succeeding. - Document why PromptAsync's CancelAfter still runs on the system clock (the TimeProvider parameter is threaded but not yet wired) so a future fake-time test can't silently pass while the real timeout never fires. - Explain why _subscriberArrival needs RunContinuationsAsynchronously (Subscribe holds _deliveryGate when it completes the source). - Rename the LaunchConsentIpcTests polling helper off the production API name it shadowed (WaitForSubscriberAsync -> SpinUntilSubscribedAsync). Added a regression test pinning the 0->0 no-op case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Threads the TimeProvider injected in Task 3 end-to-end: the gate anchors one monotonic deadline at prompt-path entry, derives every wait (grace, PromptAsync timeout) from Remaining() computed immediately before waiting, and lets external CancellationToken firing propagate uncaught (no fabricated decision, no decision-log record). The broker's PromptAsync timeout now runs on the same injected clock (WaitAsync(timeout, time, ct) replacing the CancelAfter linked-CTS), with cancellation claiming the entry like a timeout but always rethrowing instead of ever returning a resolver's verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…time format latch
The latch's premise was false: the shipped server mixes sequenced and un-sequenced commands by design (sequenced rides only the review-flow settlement lane; ordinary launches and every stop are un-sequenced), so a daemon-lifetime latch would break normal launches and silently discard stops after the first review flow. All server-origin launch/stop execution now routes through the one existing serial lane in arrival order; nothing is refused; queue bounds come from the server's capacity-gated dispatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handler classification for every unparked handler, synchronous enqueue contract on SubmitUnsequenced (both cross-format directions pinned), per-agent stop coalescing as the count bound, processor publication rule with the bounded transition residual, shutdown-only token provenance, and lane fault isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane start gate drains the one inline legacy item before any lane execution (single-assignment processor, per-boot epoch); launch-aware stop coalescing; stop admission bounded to known targets (drop-at-enqueue is observably identical to the eventual no-op); bool commit-or-refuse SubmitUnsequenced; pinned lane shutdown order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lets; settle accepted sequenced items on shutdown Typed SubmitUnsequenced with all predicates under the processor lock, active-launch tracking so an executing consent-parked launch stays an admissible stop target, transition lock with placeholder-TCS reservation, corrected payload-class bound formula, shutdown settlement of accepted sequenced items, and restoration of the processor-null and internal-bypass bullets a prior edit script accidentally deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ration; honest bound formula Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… stop-queue cap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bmission outcomes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ edge-triggered alarm Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng proof; alarm hysteresis Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t residual Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts task 5's daemon-lifetime `mixed_command_formats` latch, whose premise was false: the shipped server mixes command formats permanently by design — the sequenced tuple rides only the review-flow settlement lane, while ordinary launches and every stop are un-sequenced. The latch would have bricked every dashboard launch and silently discarded every stop after the first review flow. Nothing is refused for its format any more. All server-origin launch/stop EXECUTION — sequenced and un-sequenced alike — runs in arrival order on the one existing serial lane, so the pump's serialization is relocated rather than changed and cross-format ordering holds by construction: - SequencedCommandProcessor gains a typed SubmitUnsequenced(UnsequencedItem) -> Committed | Coalesced | Refused | DroppedUnknownTarget, with every predicate and mutation in ONE critical section before it returns: stop admissibility against the injected target probe union the in-flight-launch set, instance-counted active-launch tracking retired by ONE terminal-finalization path, launch-aware pending-stop key clearing, (agent, payload-class) coalescing with identity-guarded retirement at dequeue, and an edge-triggered queued-stop alarm with 128/60s hysteresis plus current/high-water metrics. - The sequenced mutations live in SubmitLocked's ACCEPT branch only; duplicate replays and every rejection class mutate nothing. - No handler awaits execution (launch or stop); a null processor keeps the shipped inline await, reserving an inline slot under the one lock that also guards publication, which the lane awaits before its first item. - Shutdown closes the lane before teardown, synthesizes terminal answers for accepted queued sequenced items (completing their done-tasks exactly once), discards queued un-sequenced ones, retires every active token and returns the queued-stop counter to zero. Internal reaping and local-socket stops keep bypassing the lane deliberately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l in production Both corrections ground the spec in verified code reality found during the rework: the registry-independent physical stop targets prior-incarnation survivors that exist only in PID records, and the processor is built in the orchestrator constructor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…docs Review round on the one-execution-domain change: - Every `Submit*ForTest` call in the orchestrator-level pins is now wrapped in WaitBoundedAsync. Those tests park the lane and release it later in the same test, so a regression re-adding an execution await inside a handler would block before the release ran — and with no [Timeout] on them that is a suite hang rather than a named failure. - Spec §3.3 no longer claims the queued-stop depth counters are exported metrics: they are carried in the alarm message and exposed as accessors for a future status surface. DaemonStatusReport is deliberately untouched (wire change). - Spec §3.3 also gains the half of the PID-record admission correction its ratifying commit left out: admissible stop targets are the registry, durable PID records and active launch instances, because the stop path reaps a prior incarnation's survivor by record for an id this incarnation never registered — a real action, not the no-op that justifies dropping. - The plan no longer asserts the retracted cross-format latch outside its SUPERSEDED note: the goal line, the latch trip-condition constraint, the "no queue/worker for legacy commands" constraint and Task 6's CLAUDE.md instruction all describe the one serial execution domain instead. - StopAcceptingForShutdown's two stacked <summary> blocks are split, with the lane shutdown-order paragraph moved onto DisposeAsync. - AgentPidRecordStore.Exists documents its failure asymmetry: a filesystem fault answers false and drops that one survivor stop, safe only because the server's retry-until-gone lane re-sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ect index Documents AI-1648's versioned hello frame pair + LocalControlCapabilities discovery, the consent subscriber-grace/TimeProvider deadline discipline, and the one-execution-domain routing for server launch/stop commands, pointing to the slice-2 pre-work spec.
PR Summary by QodoControl IPC Hello/HelloReply, consent prompt hardening, and one command execution lane
AI Description
Diagram
High-Level Assessment
Files changed (27)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb415ac25c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // never-published one in a test, or a stuck inline item) still drains instead of hanging here. | ||
| await _laneShutdown.CancelAsync(); | ||
| try { await _laneTask; } catch { /* best-effort */ } | ||
| _laneShutdown.Dispose(); |
There was a problem hiding this comment.
Make processor disposal idempotent
DaemonRunner explicitly calls orchestrator.DisposeAsync() and then disposes the host, whose service provider invokes AgentOrchestrator.DisposeAsync() again for the registered singleton. That second invocation calls SequencedCommandProcessor.DisposeAsync() again, but this line disposed _laneShutdown during the first invocation, so the subsequent _laneShutdown.CancelAsync() throws ObjectDisposedException. Consequently, every normal daemon shutdown can fault during host cleanup; retain the cancellation source or otherwise guard repeated disposal.
Useful? React with 👍 / 👎.
Code Review by Qodo
1.
|
…capability list Bot-review findings on the PR: a second DisposeAsync (DI container after the orchestrator's explicit dispose) would CancelAsync a disposed CTS and throw; the grace-wait expression read the monotonic clock twice; the capability list was a mutable shared List handed straight to the reply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A UTC step must not stretch or shrink the 60s window between emitted alarms; compare TimeProvider timestamps instead of wall-clock instants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…isposal A launch already past consent keeps running to registration after the shutdown token fires; settling the lane after the child enumeration let that late-registered child survive graceful shutdown. Drain first — the supersession semantics are unchanged because the lane was closed to new work before anything else. Also: null (not timestamp zero) as the never-emitted alarm sentinel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice-2 pre-work of the desktop supervisor umbrella (AI-1622), first of the two merge-ordered PRs (AI-1648; AI-1649 follows). Spec, review-hardened across two flows (6 + 11 rounds to clean):
docs/superpowers/specs/2026-08-01-slice2-prework-control-ipc-design.md.What this delivers
1. Versioned hello on the control socket — optional one-shot
Hello = 15→HelloReply = 75(protocol_version,daemon_version,daemon_name,capabilities: ["consent/1"]). Capability strings are the discovery surface for the future desktop app; the list is assembled next to the routing (LocalControlCapabilities) so a capability can't be advertised without its handler. Down-level discovery is hello-then-EOF (an old daemon's codec can't decode byte 15). Forward compat pinned: unmapped JSON members skipped, omittedcapabilitieshandled null-safe.2. Consent hardening — the
prompt_no_uiinstant deny gains a subscriber grace (min(5s, timeout), burned from one monotonicTimeProviderdeadline — total wall time never exceeds the policy timeout; zero-budget arrivals settle asprompt_timeout). Broker gains a generational subscriber-arrival waiter (arrival wins ties; 1→0 re-arm only on real removals). External cancellation now aborts without fabricating a consent decision or a log record.3. One execution domain for server launch/stop commands — the SignalR receive pump no longer awaits launch/stop execution; both command formats execute in arrival order on the processor's single serial lane (typed
SubmitUnsequenced→Committed|Coalesced|Refused|DroppedUnknownTarget, every predicate under one lock). Instance-counted active-launch tracking keeps a dequeued, consent-parked launch a valid stop target; admissibility includes PID-record survivors so the server's retry-until-gone physical stop keeps working; launch-aware per-(target, payload) stop coalescing with an edge-triggered depth alarm; shutdown settles accepted sequenced items via synthesized terminal answers and discards un-seq'd items (daemon teardown supersedes them). Nothing is ever refused for its command format — the shipped server mixes formats by design.An earlier daemon-lifetime "format latch" design was implemented, then reverted mid-branch after review verified against kcap-server that the server legitimately mixes formats (the latch would have broken launches and silently discarded stops after the first review flow); the commit history deliberately preserves that correction trail.
Testing
Full unit suite baseline-exact vs main (5084 tests; all failures byte-identical to main's known
~/.codex-area baseline). New coverage: FrameCodec/hello wire pins, real-socket hello tests, the grace/deadline matrix onFakeTimeProvider, broker waiter state machine, and the one-domain matrix (cross-format ordering both directions, instance-count pins, transition-lock pin, coalescing/saturation/alarm-hysteresis pins, shutdown settlement incl. done-task exactly-once, teardown-reap + next-boot handoff seam). Every parkable test wait is bounded — a regression fails fast instead of hanging the suite.Refs: AI-1648 (this PR), AI-1622 (umbrella), AI-1649 (next: supervision IPC, rebases on this).
🤖 Generated with Claude Code