Skip to content

Supervision IPC: daemon state + live agent list over the control socket - #442

Merged
alexeyzimarev merged 13 commits into
mainfrom
alexeyzimarev/ai-1649-supervision-ipc-daemon-state-live-agent-list-stop-agent
Aug 3, 2026
Merged

Supervision IPC: daemon state + live agent list over the control socket#442
alexeyzimarev merged 13 commits into
mainfrom
alexeyzimarev/ai-1649-supervision-ipc-daemon-state-live-agent-list-stop-agent

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Implements the supervision surface from the slice-2 pre-work spec (§4, docs/superpowers/specs/2026-08-01-slice2-prework-control-ipc-design.md): StatusSubscribe = 16 opens a long-lived connection that receives full DaemonStatus = 76 snapshots — immediately on subscribe, then debounced re-pushes driven by a monotonic change generation (DaemonStatusNotifier, mutation-first/pulse-second via centralized orchestrator helpers). AgentInstance gains RequesterUserId stamped from the launch command. The hello capability list now advertises status/1 alongside consent/1. Stop reuses the existing StopV2 frame — no new stop machinery.

No CLI surface changes — this is app-facing IPC only, so no README update is needed.

Notes for the desktop-app consumer (AI-1650): active_agents counts Starting/Running agents (display semantics), not the daemon's admission gate (EffectiveCount, which includes kill-quarantine); and a client that routinely disconnects mid-push currently logs a Warning per vanish on the daemon side.

AI-1649

🤖 Generated with Claude Code

alexeyzimarev and others added 10 commits August 3, 2026 17:05
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…apshot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e status/1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stress

Extends DaemonStatusIpcTests with the real-socket behavior matrix: every
mutation re-pushes, a pulse burst coalesces into one trailing snapshot,
two subscribers converge independently via their own cursors, a mutation
landing exactly at the snapshot/cursor boundary still converges (via the
self-clearing AfterSnapshotForTest hook), subscriber EOF reaps the handler
promptly, concurrent mutations never produce an internally-inconsistent
payload, and a shutting-down daemon just closes the subscription.

Test-only: adds the ReadOrNullAsync helper, makes the harness's server
stop idempotent (StopServerOnceAsync) since the shutdown test and
RunAsync's finally both stop it, and fixes the harness leaking a
stateDir temp directory per test. No production changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 3, 2026

Copy link
Copy Markdown

AI-1649

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Supervision IPC: daemon status + live agent list over the control socket

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add StatusSubscribe→DaemonStatus streaming snapshots to the local control socket (status/1).
• Drive re-pushes via a shared generation notifier and mutate-then-pulse orchestrator helpers.
• Stamp RequesterUserId on agents and pin the wire/behavior with unit + real-socket tests.
Diagram

graph TD
  A([Desktop app]) -->|"StatusSubscribe (16)"| B["LocalControlServer"] -->|"route"| C["DaemonStatusIpc"]
  C -->|"DaemonStatus (76)"| A
  C -->|"snapshot agents"| D["AgentOrchestrator"]
  D -->|"Pulse() on mutate"| E["DaemonStatusNotifier"]
  F["ServerConnection"] -->|"Pulse() on hub state"| E
  C -->|"WaitBeyondAsync()"| E
  C -->|"serialize snake_case"| G["StatusIpc DTOs/JSON"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Incremental status-diff frames
  • ➕ Less bandwidth for large agent lists
  • ➕ Clients can update UI without re-rendering full snapshots
  • ➖ More complex correctness story (ordering, missed diffs, resync protocol)
  • ➖ Harder forward-compat and test matrix; likely needs explicit resync framing
2. Channel-based notifier (e.g., Channel)
  • ➕ Avoids explicit TaskCompletionSource rearm logic
  • ➕ Potentially simpler cancellation semantics
  • ➖ Easy to accidentally make notifications consumable (not broadcast)
  • ➖ Still needs a generation/cursor concept to avoid missed updates

Recommendation: The current approach (monotonic generation + full snapshot re-push + debounce) is a strong fit for correctness: subscribers converge even if they miss pulses, and the wire contract stays simple (single snapshot DTO). Keep the generation-based broadcast notifier, but continue to watch daemon-side logging for noisy disconnects (the PR notes a Warning per mid-push vanish).

Files changed (23) +2308 / -36

Enhancement (12) +274 / -27
FrameCodec.csTeach IPC codec about StatusSubscribe and DaemonStatus payload shapes +4/-4

Teach IPC codec about StatusSubscribe and DaemonStatus payload shapes

• Extends frame encoding/decoding so StatusSubscribe carries an empty payload (like List/Detach) and DaemonStatus carries UTF-8 JSON in Text (like consent frames).

src/Capacitor.Cli.Core/LocalIpc/FrameCodec.cs

FrameType.csPin new supervision frame types (16, 76) +2/-0

Pin new supervision frame types (16, 76)

• Adds StatusSubscribe=16 (client→daemon) and DaemonStatus=76 (daemon→client) while preserving the append-only frame numbering contract.

src/Capacitor.Cli.Core/LocalIpc/FrameType.cs

LocalFrame.csAdd StatusJson helper for JSON-carrying status frames +4/-0

Add StatusJson helper for JSON-carrying status frames

• Introduces LocalFrame.StatusJson(...) to construct DaemonStatus frames with JSON stored in the Text field, mirroring the existing HelloJson helper.

src/Capacitor.Cli.Core/LocalIpc/LocalFrame.cs

StatusIpc.csAdd DaemonStatus wire DTOs + snake_case source-gen JSON context +34/-0

Add DaemonStatus wire DTOs + snake_case source-gen JSON context

• Defines DaemonStatusDto/DaemonInfoDto/AgentStatusDto and a StatusIpcJsonContext configured for snake_case serialization. Documents the wire contract expectations: nulls are always emitted, and deserialization must ignore unknown members for forward compatibility.

src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs

DaemonRunner.csCache ResolveDaemonVersion and register supervision IPC services +16/-1

Cache ResolveDaemonVersion and register supervision IPC services

• Caches the daemon informational version to avoid repeated reflection on every status snapshot. Registers DaemonStatusNotifier and DaemonStatusIpc as singletons and documents the DI requirement that optional notifier parameters must be satisfied by bare DI registrations.

src/Capacitor.Cli.Daemon/DaemonRunner.cs

AgentOrchestrator.LocalIpc.csExpose ordered AgentStatus snapshot and pulse on local-spawn publish +15/-1

Expose ordered AgentStatus snapshot and pulse on local-spawn publish

• Adds SnapshotAgentsForStatus() that deterministically orders agents (created_at then id ordinal) and maps internal state to AgentStatusDto, including RequesterUserId. Switches local spawn publishing to use PublishAgent() so it pulses the status generation.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs

AgentOrchestrator.csAdd RequesterUserId and centralize mutate-then-pulse helpers +53/-13

Add RequesterUserId and centralize mutate-then-pulse helpers

• Extends AgentInstance with RequesterUserId stamped from LaunchAgentCommand. Introduces a shared DaemonStatusNotifier (optional ctor param) plus SetAgentStatus/PublishAgent/UnpublishAgent helpers, and rewires key mutation sites (launch, read-loop, failure, stop, cleanup) to enforce mutation-first then Pulse().

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

DaemonStatusIpc.csImplement StatusSubscribe handler with debounced snapshot pushes +76/-0

Implement StatusSubscribe handler with debounced snapshot pushes

• Adds a long-lived subscription handler that immediately pushes a full DaemonStatus snapshot, then re-pushes on notifier generation changes with debounce coalescing. Includes an EOF watcher to cancel promptly on client disconnect and internal test seams (AfterSnapshotForTest, ActiveSubscribersForTest, Debounce).

src/Capacitor.Cli.Daemon/Services/DaemonStatusIpc.cs

DaemonStatusNotifier.csAdd broadcast-safe generation notifier for status pushes +42/-0

Add broadcast-safe generation notifier for status pushes

• Introduces a monotonic Version counter with Pulse() and WaitBeyondAsync(seen, ct). Ensures atomic version/source capture under one lock, broadcast wakeups, and per-waiter cancellation without consuming the shared signal.

src/Capacitor.Cli.Daemon/Services/DaemonStatusNotifier.cs

LocalControlCapabilities.csAdvertise status/1 alongside consent/1 +4/-3

Advertise status/1 alongside consent/1

• Updates the hello capability list to include status/1 and updates documentation to assert the invariant that advertised capabilities must be routed by LocalControlServer.

src/Capacitor.Cli.Daemon/Services/LocalControlCapabilities.cs

LocalControlServer.csRoute StatusSubscribe frames to DaemonStatusIpc +3/-2

Route StatusSubscribe frames to DaemonStatusIpc

• Extends the control socket routing switch to handle StatusSubscribe by invoking DaemonStatusIpc, and updates the default error message to include the new expected frame.

src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs

ServerConnection.csPulse status notifier on hub connection state transitions +21/-3

Pulse status notifier on hub connection state transitions

• Adds an optional DaemonStatusNotifier parameter and pulses it on connect success, reconnecting, reconnected, and closed events. Exposes StatusNotifierForTest to support DI wiring verification tests.

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs

Tests (10) +948 / -9
AgentOrchestratorLocalAttachTests.csUpdate LocalControlServer test wiring to include DaemonStatusIpc +13/-4

Update LocalControlServer test wiring to include DaemonStatusIpc

• Adjusts existing real-socket attach/list tests to construct LocalControlServer with a DaemonStatusIpc instance. Avoids creating a second ServerConnection by reusing the test-owned one for correct disposal.

test/Capacitor.Cli.Tests.Unit/AgentOrchestratorLocalAttachTests.cs

AgentOrchestratorRequesterTests.csAdd tests for stamping RequesterUserId into AgentInstance +96/-0

Add tests for stamping RequesterUserId into AgentInstance

• Adds vendor-harness tests that verify LaunchAgentCommand.RequesterUserId is captured on AgentInstance creation, and that SeedAgentForTest stamps requester values correctly (including null default).

test/Capacitor.Cli.Tests.Unit/AgentOrchestratorRequesterTests.cs

AgentStatusSnapshotTests.csPin status snapshot ordering/mapping and notifier generation pulses +137/-0

Pin status snapshot ordering/mapping and notifier generation pulses

• Adds unit tests for SnapshotAgentsForStatus ordering (created_at then id) and field mapping (kind spellings, requester, nullables). Verifies Publish/SetStatus/Unpublish each advance the shared notifier generation.

test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs

DaemonStatusIpcTests.csAdd real Unix-socket end-to-end behavior matrix for supervision IPC +421/-0

Add real Unix-socket end-to-end behavior matrix for supervision IPC

• Introduces a full real-socket test harness for StatusSubscribe/DaemonStatus and pins debounced convergence semantics: immediate snapshot, mutation-triggered re-push, burst coalescing, two-subscriber convergence, snapshot-boundary mutation convergence, EOF reaping, stress consistency invariant, and clean shutdown close behavior.

test/Capacitor.Cli.Tests.Unit/Daemon/DaemonStatusIpcTests.cs

DaemonStatusNotifierTests.csAdd unit tests for DaemonStatusNotifier broadcast semantics +64/-0

Add unit tests for DaemonStatusNotifier broadcast semantics

• Covers synchronous completion for stale cursors, waiter wakeup on pulse, broadcast behavior for multiple waiters, blocking until next pulse after a pulse, and per-waiter cancellation isolation.

test/Capacitor.Cli.Tests.Unit/Daemon/DaemonStatusNotifierTests.cs

DaemonStatusWiringTests.csPin DI wiring so notifier singleton is actually shared +127/-0

Pin DI wiring so notifier singleton is actually shared

• Adds DI-focused tests ensuring ServerConnection and AgentOrchestrator resolved via DI share the one registered DaemonStatusNotifier (preventing silent regressions if registrations switch to factory delegates that omit the notifier).

test/Capacitor.Cli.Tests.Unit/Daemon/DaemonStatusWiringTests.cs

LaunchConsentIpcTests.csUpdate consent IPC tests to pass DaemonStatusIpc to LocalControlServer +2/-1

Update consent IPC tests to pass DaemonStatusIpc to LocalControlServer

• Extends the existing local control server harness in consent IPC tests to construct and pass a DaemonStatusIpc instance to the LocalControlServer constructor.

test/Capacitor.Cli.Tests.Unit/Daemon/LaunchConsentIpcTests.cs

LocalControlHelloTests.csUpdate hello tests for new capability list and server wiring +5/-4

Update hello tests for new capability list and server wiring

• Updates LocalControlHelloTests to build LocalControlServer with DaemonStatusIpc and asserts hello capabilities now include both consent/1 and status/1.

test/Capacitor.Cli.Tests.Unit/Daemon/LocalControlHelloTests.cs

FrameCodecStatusTests.csAdd frame codec round-trip tests for supervision frames +38/-0

Add frame codec round-trip tests for supervision frames

• Adds unit tests asserting StatusSubscribe encodes/decodes as empty payload and DaemonStatus round-trips JSON text payload, and pins the assigned byte values (16 and 76).

test/Capacitor.Cli.Tests.Unit/FrameCodecStatusTests.cs

StatusIpcJsonTests.csPin DaemonStatus JSON contract (snake_case, null emission, field order) +45/-0

Pin DaemonStatus JSON contract (snake_case, null emission, field order)

• Adds exact JSON serialization assertions for DaemonStatusDto and verifies forward compatibility by deserializing payloads with unknown members without error.

test/Capacitor.Cli.Tests.Unit/StatusIpcJsonTests.cs

Documentation (1) +1086 / -0
2026-08-03-ai1649-supervision-ipc.mdAdd detailed implementation plan for supervision IPC (AI-1649) +1086/-0

Add detailed implementation plan for supervision IPC (AI-1649)

• Adds a step-by-step plan covering wire frames, DTO contracts, notifier semantics, daemon handler wiring, and a full behavior test matrix. Documents key invariants like append-only frame values, snake_case JSON with null emission, and mutate-then-pulse ordering.

docs/superpowers/plans/2026-08-03-ai1649-supervision-ipc.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e6eb61ea4

ℹ️ 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".

await RegisterDaemonAsync();
_connectedTimestamp = Stopwatch.GetTimestamp();
LogConnected(_config.Name);
_statusNotifier.Pulse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish initial connection attempt transitions

When a status client subscribes while the initial StartHubAsync attempt is in Connecting, a failed attempt returns the hub to Disconnected and enters the retry delay without reaching this success-only pulse; initial start failures do not invoke the reconnect/closed handlers either. Because DaemonRunner starts LocalControlServer before calling ConnectAsync, this race is reachable and the client can continue displaying connecting throughout an arbitrarily long outage. Pulse the notifier when the initial attempt enters Connecting and again after a failed attempt has returned to Disconnected, rather than only after successful registration.

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Disconnect logs warnings ✓ Resolved 🐞 Bug ◔ Observability
Description
DaemonStatusIpc.HandleSubscribeAsync only treats OperationCanceledException as a normal termination,
so a client disconnect during a status push can throw IOException/SocketException and get logged as
a warning by LocalControlServer for routine disconnects.
Code

src/Capacitor.Cli.Daemon/Services/DaemonStatusIpc.cs[R47-50]

+        } catch (OperationCanceledException) {
+            // subscriber EOF or daemon shutdown — either way the connection just closes
+        } finally {
+            Interlocked.Decrement(ref _subscribers);
Evidence
The subscription loop writes frames and only catches OperationCanceledException; any IO-layer
exception will escape. LocalControlServer logs any non-cancellation exception as a warning, so those
disconnect exceptions become warning spam.

src/Capacitor.Cli.Daemon/Services/DaemonStatusIpc.cs[27-51]
src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs[37-66]
src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs[107-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`DaemonStatusIpc.HandleSubscribeAsync` only catches `OperationCanceledException`. If the subscriber disconnects while the daemon is in `FrameCodec.WriteAsync`, the write commonly throws `IOException`/`SocketException` (or similar), which bubbles up to `LocalControlServer.HandleConnectionAsync` and is logged as a warning. This makes normal client lifecycle (disconnect/reconnect) look like faults.

### Issue Context
This is a long-lived subscription endpoint; disconnects are expected and should be treated as clean termination rather than “faulted connection”.

### Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/DaemonStatusIpc.cs[27-52]
- src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs[37-66]

### What to change
1. In `DaemonStatusIpc.HandleSubscribeAsync`, broaden the “normal termination” catch to include expected transport exceptions from `ReadAsync`/`WriteAsync` on disconnect (e.g. `IOException`, `SocketException`, `EndOfStreamException`, `ObjectDisposedException`).
2. Optionally (if you want the server boundary to handle this generically), adjust `LocalControlServer.HandleConnectionAsync` to not warn on these expected disconnect exceptions for long-lived handlers, but the cleaner fix is to absorb them inside `DaemonStatusIpc`.
3. Keep the existing `finally` decrement logic unchanged so `ActiveSubscribersForTest` stays correct.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Temp state dir leaked ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
AgentStatusSnapshotTests.Build creates a temporary state directory via
Directory.CreateTempSubdirectory but never deletes it, so repeated unit test runs will accumulate
leftover directories under the system temp path.
Code

test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs[R38-41]

+    static (AgentOrchestrator Orchestrator, DaemonStatusNotifier Notifier) Build() {
+        var stateDir = Directory.CreateTempSubdirectory("kcap-status-snapshot-state-").FullName;
+        var store       = new LaunchConsentStore(stateDir, NullLogger.Instance);
+        var broker      = new LaunchConsentBroker();
Evidence
The test helper creates a temp directory and the tests do not delete it, only disposing the
orchestrator. A nearby new test file in this PR demonstrates the intended cleanup pattern by
deleting its temp state directory.

test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs[38-50]
test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs[67-85]
test/Capacitor.Cli.Tests.Unit/Daemon/DaemonStatusIpcTests.cs[108-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AgentStatusSnapshotTests.Build()` creates a per-test `stateDir` with `Directory.CreateTempSubdirectory(...)` and uses it as `DaemonConfig.StateDir`, but the tests only dispose the orchestrator and never delete `stateDir`. This leaks directories on every test run.

### Issue Context
Other new socket/status tests in this PR explicitly delete their temp state directory during cleanup, suggesting that cleanup is expected.

### Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs[38-86]

### What to change
1. Return `stateDir` (and any other temp roots you intentionally create) from `Build()` so each test can delete it in its `finally` after disposing the orchestrator.
2. Alternatively, refactor `Build()` into a small disposable/async-disposable harness type that owns `stateDir` and deletes it when disposed.
3. If you decide to also clean `config.WorktreeRoot`, only delete it if it was actually created.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/Capacitor.Cli.Daemon/Services/DaemonStatusIpc.cs
Comment thread test/Capacitor.Cli.Tests.Unit/Daemon/AgentStatusSnapshotTests.cs Outdated
alexeyzimarev and others added 3 commits August 3, 2026 20:45
…r disconnects, clean test temp dirs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local spawns store "" for AgentInstance.Model as an internal "no model"
sentinel, and a server-driven launch with a blank requested model can
retain "" too (ModelSelectionLaunchPolicy.Evaluate treats blank as
Honor). SnapshotAgentsForStatus copied Model verbatim, so the supervision
wire emitted two representations ("" and null) for the same absent state
even though the wire contract pins absent = null. Normalize only at the
wire-mapping boundary; AgentInstance itself keeps storing "".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit b02e5f0 into main Aug 3, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-1649-supervision-ipc-daemon-state-live-agent-list-stop-agent branch August 3, 2026 19:53
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