Skip to content

[AI-1526][AI-1528] Address Qodo findings from #376/#377 - #378

Merged
realtonyyoung merged 6 commits into
mainfrom
tonyyoung/qodo-followup-1526-1528
Jul 28, 2026
Merged

realtonyyoung merged 6 commits into
mainfrom
tonyyoung/qodo-followup-1526-1528

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Follow-up to #376 and #377, both of which merged with unaddressed Qodo review findings. No behavior change to the settlement protocol itself.

Finding 1 — liveness read inside the sequenced-command lock (real bug/perf, from #377)

SequencedCommandProcessor.BuildProcessedAckLocked() invoked the readLiveness delegate while holding _lock. In production that delegate is AgentOrchestrator.ReadLiveness(), which called AgentKillQuarantine.Snapshot() and LINQ'd over the projected list — allocating inside the critical section. #377's proactive terminal ack moved that from "only on a duplicate replay" to every settled command, so lock-hold time grew and could delay concurrent SubmitAsync / AckPrefix callers.

Both halves fixed:

  1. AgentKillQuarantine.IsQuarantined(agentId) — an allocation-free ConcurrentDictionary.ContainsKey membership test. AgentOrchestrator.ReadLiveness now uses it instead of Snapshot().Any(q => q.Id == agentId).
  2. _readLiveness is no longer invoked under _lock on either ack path:
    • Proactive (lane) path — the lock block records the outcome in the cache and advances the watermark; the ack is built and sent after the lock is released.
    • Duplicate-replay path — HandleDuplicateLocked now only captures the cached CommandOutcome (a struct copy) into an out parameter; SubmitAsync builds and sends the ack after releasing the lock. SubmitAsync's body moved into SubmitLocked so the lock scope stays a single, obvious block.

BuildProcessedAckLocked is renamed BuildProcessedAck and its doc now states the "must not be called under _lock" contract.

The deliberate ordering invariant is preserved: both callers record the outcome in the cache before the ack is built, so an ack can never advertise an outcome the cache has not recorded. Only the ack construction moved out of the lock, not the cache write.

Finding 2 — README described the old settlement-retry behavior (from #377)

The README said settlement-layer coded 409s were retried "a few times" and were "normally invisible to the caller." That stopped being true when the flows MCP server moved to a 3-minute elapsed deadline with an explicit coded timeout as a tool error. The section now documents:

  • which two codes retry (flow_settlement_busy, reviewer_launch_incarnation_superseded) and that everything else surfaces on the first attempt;
  • the backoff shape (250–500 ms first retry, 5–10 s steady state);
  • that the budget is an elapsed-time deadline of 3 minutes from the first attempt, including each request's own duration (not an attempt count);
  • that exhausting it returns an explicit isError: true tool result in the Error (code): message shape, carrying the last coded error plus attempt count and elapsed time — with an example — and that it is retryable;
  • that the round poll loop has its own separate budget and message.

Finding 3 — comment verbosity (both PRs, "review recommended")

Trimmed restated mechanics and repeated rationale at the three flagged sites, to roughly the density of the surrounding untouched code:

  • FlowRetryTiming.cs — FlowRetryClock and SettlementBackoff class docs condensed. Kept the pinned backoff formula: it is the assertable contract the tests pin, not a restatement of the code.
  • SequencedCommandProcessor.cs — dropped the "Task 13 / Task 15" plan-ticket call-site annotations and the duplicated §3.2 F rationale that appeared in three places; the ordering invariant is now stated once, on BuildProcessedAck.
  • AgentOrchestratorBorrowLaunchTests.cs — dropped the historical "this path had never executed in production" paragraph. Kept the two-checkout requirement (a future simplification to one repo would silently make the test vacuous), the decoy-content note, and the non-vacuity guards ("the droppings must genuinely exist first", "re-basing the baseline cannot launder a corruption") — those record a non-obvious why and prevent regressions. Also kept the ContentBaseline / GitState helper docs explaining why .git is excluded and why SnapshotTree is not used.

Tests

New regression coverage in SequencedCommandProcessorTests:

  • Liveness_is_never_read_while_the_processor_lock_is_held — a test seam (LockHeldByCurrentThreadForTest, Monitor.IsEntered) lets the injected readLiveness delegate assert directly that it is not holding the lock. No sleeps, no thread choreography. Mutation-verified: reverting either ack path to build under the lock fails this test with readsUnderLock == 2.
  • Proactive_ack_is_built_only_after_the_outcome_is_recorded_in_the_cache — guards the ordering invariant the lock move had to preserve.
  • AgentKillQuarantineTests.IsQuarantined_answers_by_id — including the ordinal-comparison behavior of the backing dictionary.

Suite: Capacitor.Cli.Tests.Unit on macOS — baseline origin/main 4059 total / 44 failed (the known local tmp-symlink + parallel-load flakes: config-toml, MCP-registration, uninstall). Branch result recorded in a follow-up comment; failing sets diffed to confirm no new failures.

scripts/check-linear-ids.sh passes (no Linear IDs in .cs).

Keep the liveness read off the sequenced-command lock, correct the README's
settlement-retry description, and trim design-doc-grade comments.

- SequencedCommandProcessor no longer calls the readLiveness delegate while
  holding _lock. The proactive terminal ack introduced by #377 put that read on
  every settled command, not just a duplicate replay, so lock-hold time grew and
  could delay concurrent SubmitAsync/AckPrefix callers. Both ack paths now
  record the outcome in the cache under the lock and build the ack after
  releasing it, preserving the ordering invariant that an ack can never
  advertise an outcome the cache has not recorded.

- AgentKillQuarantine gains an allocation-free IsQuarantined(agentId);
  AgentOrchestrator.ReadLiveness uses it instead of Snapshot().Any(...), which
  allocated a projected list on every call.

- README: settlement-layer coded 409s are no longer described as retried "a few
  times" and normally invisible. Documents the real 3-minute elapsed deadline,
  the backoff shape, which codes retry, and the explicit isError tool result the
  flows MCP server returns when the deadline is exhausted.

- Trimmed restated-mechanics comments in FlowRetryTiming, SequencedCommandProcessor
  and AgentOrchestratorBorrowLaunchTests. Comments recording a non-obvious why
  (the pinned backoff formula, the two-checkout requirement, the non-vacuity
  guards) are kept.

Tests: new regression coverage that the liveness delegate is never invoked under
the processor lock (mutation-verified), that the cache records the outcome
before the ack is built, and for IsQuarantined.
@linear-code

linear-code Bot commented Jul 28, 2026

Copy link
Copy Markdown

AI-1526

AI-1528

@realtonyyoung
realtonyyoung marked this pull request as ready for review July 28, 2026 11:43
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Local test results (macOS, Capacitor.Cli.Tests.Unit)

total failed skipped
baseline origin/main (4e7ee2c) 4059 44 8
this branch 4062 42 8

+3 total = the three new regression tests, all passing.

Failing-set diff: zero new failures. All 42 branch failures are a subset of the 44 baseline failures — the known local tmp-symlink / parallel-load flakes (RegisterKcapMcpServers_*, UnregisterKcapMcpServers_*, InstallCodex_*, User_level_uninstall_*, EnableNetworkAccess_*, …). Two baseline failures (Dispose_flushes_the_partial_buffer_…, Transcript_channel_at_capacity_…) did not reproduce on the branch — both are unrelated to these changes and are themselves flaky.

Mutation check on the new guard (so it is not vacuous): reverting both ack paths to build the CommandAck inside _lock makes Liveness_is_never_read_while_the_processor_lock_is_held fail with readsUnderLock == 2 (one per path). Reverted before commit.

scripts/check-linear-ids.sh → exit 0.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Move settlement liveness reads outside the command lock

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Moves terminal-ack liveness reads outside the processor lock while preserving cache-before-ack
 ordering.
• Adds allocation-free quarantine membership checks for frequent liveness reads.
• Documents actual settlement retries and adds concurrency regression coverage.
Diagram

sequenceDiagram
    actor C as Command Caller
    participant P as Seq Processor
    participant K as Outcome Cache
    participant O as Orchestrator
    participant Q as Quarantine
    participant A as Ack Sink
    C->>P: Submit command
    P->>K: Record outcome under lock
    K-->>P: Outcome committed
    P->>O: Read liveness outside lock
    O->>Q: Check agent ID
    Q-->>O: Membership result
    O-->>P: Current state
    P->>A: Send processed ack
Loading
High-Level Assessment

The chosen approach is appropriate: retain atomic cache and watermark mutations under the existing lock, copy the cached value-type outcome, and defer only liveness-dependent acknowledgement construction until after lock release. Optimizing the snapshot alone would reduce allocation but leave an external delegate inside the critical section, while caching liveness would weaken the acknowledgement's live-state semantics.

Files changed (8) +145 / -70

Bug fix (3) +70 / -41
AgentKillQuarantine.csAdd allocation-free quarantine membership lookup +5/-0

Add allocation-free quarantine membership lookup

• Adds an ordinal agent-ID membership API backed directly by ConcurrentDictionary.ContainsKey. This avoids constructing and scanning status snapshots during acknowledgement liveness reads.

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

AgentOrchestrator.csUse direct quarantine lookup for liveness +1/-1

Use direct quarantine lookup for liveness

• Replaces Snapshot().Any(...) with IsQuarantined(agentId), eliminating projected-list allocation from the frequent liveness path.

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

SequencedCommandProcessor.csBuild processed acknowledgements outside the processor lock +64/-40

Build processed acknowledgements outside the processor lock

• Refactors submission and duplicate handling so processed outcomes are captured under the lock but liveness reads and acknowledgement construction occur afterward. The proactive lane path likewise records outcomes and advances the watermark before releasing the lock, preserving cache-before-ack ordering. Adds a test seam for directly detecting lock ownership during liveness reads.

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

Tests (2) +58 / -0
AgentKillQuarantineTests.csTest direct quarantine membership semantics +13/-0

Test direct quarantine membership semantics

• Verifies present and absent agent-ID lookups and confirms ordinal, case-sensitive matching against the backing dictionary.

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

SequencedCommandProcessorTests.csCover lock-free liveness reads and ack ordering +45/-0

Cover lock-free liveness reads and ack ordering

• Adds regression tests proving both proactive and duplicate-replay liveness reads occur without the processor lock. Also verifies that the processed watermark and cached outcome are committed before proactive acknowledgement construction.

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

Documentation (3) +17 / -29
README.mdCorrect settlement retry behavior documentation +3/-1

Correct settlement retry behavior documentation

• Documents the two retryable settlement error codes, jittered exponential backoff, and three-minute elapsed-time deadline. Clarifies that deadline exhaustion returns an explicit retryable tool error and that unrelated failures surface immediately.

README.md

FlowRetryTiming.csCondense retry timing documentation +8/-15

Condense retry timing documentation

• Shortens comments around the injectable retry clock and shared backoff schedule while retaining the timing contract and design reference. Runtime behavior is unchanged.

src/Capacitor.Cli/Commands/FlowRetryTiming.cs

AgentOrchestratorBorrowLaunchTests.csClarify borrowed-checkout regression comments +6/-13

Clarify borrowed-checkout regression comments

• Condenses explanatory comments while preserving the rationale for separate daemon and requester checkouts and per-round cleanup assertions. Test behavior is unchanged.

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

@qodo-code-review

qodo-code-review Bot commented Jul 28, 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


Action required

1. Concurrency comments remain verbose ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added BuildProcessedAck documentation uses eleven lines to explain implementation ordering and
lock behavior, while other added comments repeat the same rationale. This conflicts with the
requirement that source comments remain brief and rely on self-explanatory code.
Code

src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[R120-130]

+    /// duplicate-replay path and the proactive settle ack at the end of <see cref="RunLaneAsync"/>, so a
+    /// retransmission can never disagree with the proactive send about outcome/agent/session/rejection-reason.
+    /// The CACHED rejection reason rides along so a rejected launch stays distinguishable as daemon_capacity
+    /// (requeue) vs semantic (fail) — the exact lost-rejection case the identity cache answers.
+    ///
+    /// <para>MUST NOT be called under <c>_lock</c>. <c>CurrentState</c> is read LIVE at ack time through the
+    /// readLiveness delegate, which in production walks the orchestrator's lifecycle collections; every
+    /// settled command reaches this, so holding <c>_lock</c> across it would put that read on the critical
+    /// path of concurrent <see cref="SubmitAsync"/>/<see cref="AckPrefix"/> callers. Both callers record the
+    /// outcome in the cache first and build the ack after releasing the lock, which preserves the ordering
+    /// that matters: an ack can never advertise an outcome the cache has not recorded.</para></summary>
Evidence
PR Compliance ID 3 requires brief, necessary comments. The cited additions repeatedly narrate why
liveness is read outside _lock and why the cache is updated first, including an eleven-line method
comment and additional caller comments expressing the same invariant.

CLAUDE.md: Keep Source Comments Concise and Free of Linear Issue Numbers
src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[120-130]
src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[64-65]
src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[234-236]
src/Capacitor.Cli.Daemon/Services/AgentKillQuarantine.cs[32-34]

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

## Issue description
The newly added comments repeatedly explain the same cache-ordering and lock-release behavior in excessive detail.

## Issue Context
Keep a concise statement of the essential invariant and the requirement not to invoke liveness reads under `_lock`; remove implementation narration duplicated around callers and simple methods.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[120-130]
- src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[64-65]
- src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs[234-236]
- src/Capacitor.Cli.Daemon/Services/AgentKillQuarantine.cs[32-34]

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


2. ai1526 remains in comments ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Two added XML comments embed the Linear identifier ai1526 through a design-document path. Source
comments must not contain Linear issue numbers.
Code

src/Capacitor.Cli/Commands/FlowRetryTiming.cs[9]

+/// docs/superpowers/specs/2026-07-25-ai1526-concurrent-launch-settlement-admission-design.md (3.2 G).
Evidence
PR Compliance ID 3 prohibits Linear issue numbers in source comments. The added XML comments
reference 2026-07-25-ai1526-concurrent-launch-settlement-admission-design.md in two locations.

CLAUDE.md: Keep Source Comments Concise and Free of Linear Issue Numbers
src/Capacitor.Cli/Commands/FlowRetryTiming.cs[9-9]
src/Capacitor.Cli/Commands/FlowRetryTiming.cs[70-70]

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

## Issue description
The added XML comments contain the Linear identifier `ai1526`, which is prohibited in source comments.

## Issue Context
Retain any useful API explanation, but remove the internal issue-number references. If traceability is essential, use an acceptable GitHub issue reference instead.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/FlowRetryTiming.cs[9-9]
- src/Capacitor.Cli/Commands/FlowRetryTiming.cs[70-70]

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



Informational

3. Error provenance documented incorrectly ✓ Resolved 🐞 Bug ≡ Correctness
Description
The README says deadline exhaustion carries the last coded server error, but the first request can
time out before any response is parsed. In that case LastCode is null and the formatter invents
the fallback flow_settlement_busy, so callers may incorrectly believe that code came from the
server.
Code

README.md[425]

+If the deadline is exhausted the call does **not** hang or silently succeed: the tool returns an explicit error result (`isError: true`) in the usual `Error (code): message` shape, carrying the last coded error it kept hitting plus how many attempts were made over how long — for example `Error (flow_settlement_busy): gave up after 14 attempts over 3m — the daemon is still settling a prior launch and could not admit this one in time.` That outcome is retryable: try again in a minute, or check whether another flow is already running against the same daemon. The round **poll** loop has its own separate budget and its own graceful-cap message.
Evidence
The retry helper initializes lastCode to null and may return from the in-flight cancellation path
before parsing a response. The formatter explicitly replaces that null with flow_settlement_busy,
contradicting the unconditional claim that the result carries the last coded error encountered.

src/Capacitor.Cli/Commands/McpFlowsServer.cs[393-414]
src/Capacitor.Cli/Commands/McpFlowsServer.cs[860-868]

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

## Issue description
Correct the settlement-deadline documentation: when the deadline cancels an in-flight request before any coded response is received, the displayed `flow_settlement_busy` code is a fallback rather than the last server error.

## Issue Context
`SendWithSettlementRetryAsync` can return `DeadlineExhausted` with a null `LastCode`, and `FormatSettlementDeadlineError` then substitutes `flow_settlement_busy`.

## Fix Focus Areas
- README.md[425-425]
- src/Capacitor.Cli/Commands/McpFlowsServer.cs[393-414]
- src/Capacitor.Cli/Commands/McpFlowsServer.cs[860-868]

ⓘ 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/Commands/FlowRetryTiming.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/SequencedCommandProcessor.cs Outdated
Comment thread README.md Outdated
Codex review of #378. SendProactiveAck(BuildProcessedAck(..)) evaluates its
argument before entering the try, so a throwing readLiveness -- a delegate that
walks the orchestrator's live lifecycle collections -- escaped the containment
entirely.

From the lane that faults RunLaneAsync and leaves the item's Done unresolved:
the submitter waits forever and every later command is stranded, which the
server reads as a permanently held daemon capacity slot. From SubmitAsync it
escapes into the hub handler, and that is the RECOVERY path -- a duplicate
replay is what the server sends when it never received the terminal ack.

SendProactiveAck becomes SendSettledAck(item, outcome) and builds inside the
try. The replay path now goes through it too, replacing a bare
`_ = _sendAck(...)` that contained neither a synchronous throw nor a faulted
task.

Three tests. The lane one is bounded on purpose: reverting the fix makes it
HANG rather than fail (verified -- an unbounded await pinned the suite until
the harness timeout), and a timed-out CI job is a much worse signal than a
named assertion.

Also corrected the comment on Proactive_ack_is_built_only_after_the_outcome_
is_recorded_in_the_cache: the review pointed out it is not a regression net for
the lock placement, since Monitor is reentrant and LastProcessedSeq would read
the same updated value from inside the lock. It pins the ordering, which is
worth pinning; lock placement is covered by the test above it.

20/20 unit tests, check-linear-ids clean.
Codex review round 2 of #378. Both findings predate this PR -- they are in the
unreviewed #376/#377 surface -- but I asked whether any send was left
uncontained, so they belong here.

P1: the lane's rejection sends. Both arms called _sendRejected BEFORE the cache
entry was marked Processed and Done resolved. A synchronous throw escaped
RunLaneAsync, permanently killing the single serial consumer: the command stayed
nonterminal, its submitter waited forever, and every queued command was stranded
-- the same held-capacity-slot class the ack fix addressed. Now the lane settles
FIRST, then notifies, with both sends contained; relative order (rejection then
ack) is unchanged. The whole body is wrapped in try/finally so Done is released
even if something unforeseen throws.

P2: the in-progress duplicate's Accepted ack was still a bare send under _lock.
A synchronous throw escaped into the hub, a faulted task went unobserved, and
any synchronous transport work ran inside the processor's critical section --
the section this PR exists to keep narrow. HandleDuplicateLocked now only
CAPTURES which answer is owed; SubmitAsync sends after releasing the lock.

Consolidated into one SendContained(Func<Task>, seq, what) helper that invokes
the delegate inside the try, so argument construction is covered at every site.
Every _sendRejected/_sendAck in the file now routes through it.

Two more tests: a throwing rejection send must not fault the lane or lose the
settlement, and a throwing Accepted-ack send must not escape an in-progress
duplicate. Both bounded, for the reason recorded on the previous test.

22/22 unit tests, check-linear-ids clean.
…of the lock

Codex review round 3 of #378.

The try/finally comment I wrote last round was wrong, and the review said so.
finally releases the submitter but the exception still propagates out of the
await foreach and faults the lane -- and it reports SUCCESS to a submitter whose
command may never have been marked terminal. The realistic trigger is
diagnostics: an ILogger provider throwing from the execute-fault LogWarning, or
from SendContained's own catch. There is now a real per-item catch that
synthesizes the terminal state when the normal path did not reach it and lets
the loop continue, with every diagnostic inside it individually guarded --
because the most likely way to arrive there is diagnostics throwing.

Second: SendContained catches exceptions but still invokes the delegate
immediately, so it does nothing about lock scope. RejectLocked, the
duplicate-collision branch and SynthesizeErrorLocked were all still running
transport delegates inside _lock -- the same objection that moved the Accepted
ack out. They now only record WHICH rejection is owed; SubmitAsync sends it
after releasing the lock. Containment and lock-scope were two problems and I had
only fixed one.

Two tests, both aimed at the gaps the review identified in the previous ones:
a throwing ILogger (NullLogger cannot reach that path) asserting the command is
still terminal and the lane survives, and a direct assertion that no rejection
send runs with the lock held, covering the two locked reject paths the
lane-based test could not.

24/24 unit tests, check-linear-ids clean.
…the failure

Codex review round 4 of #378.

P1: `settled` was set AFTER the rejection notification, so a throw escaping
SendRejectedContained reached the outer catch with the real terminal outcome
already committed -- and the catch overwrote it with InternalError. A
LaunchRejected(daemon_capacity) could be cached and announced as one result and
replayed later as a contradictory one. It is now set the instant the
cache+watermark commit succeeds, before any notification: the cache is
authoritative from that point and announcing it must never be able to rewrite
it.

P2: SendContained's own diagnostics were unguarded. On the hub-side paths
(stale epoch, gap, duplicate collision, accepted/processed replay) there is no
outer per-item catch, so a transport throw followed by a logging throw escaped
straight into the hub; and the async branch's unguarded LogDebug faulted its own
DISCARDED continuation, recreating the unobserved task failure the wrapper
exists to prevent. Both now route through LogQuietly.

One new test, mutation-verified: un-guarding the synchronous diagnostic makes it
fail.

Deliberately NO test for the `settled` ordering. I wrote one, then
mutation-tested it and it passed against the un-fixed code -- with the
diagnostics guarded, no post-commit path can escape into the outer catch, so the
overwrite is currently unreachable. The ordering stays as defence-in-depth (free,
and it removes a latent contradiction) but a test that passes either way would
claim coverage that is not there, which is the exact criticism this review made
of an earlier ordering test. The reasoning is recorded in a comment where the
test would have gone.

25/25 unit tests, check-linear-ids clean.
1. Removed the design-doc paths from FlowRetryTiming's XML comments. They
   embedded an internal issue slug AND pointed at a file in a private repo, so
   a public reader could not follow them anyway. The API explanation stays.

3. The deadline message asserted a cause the client had not observed.
   LastCode is null when the deadline cancels an in-flight request before any
   coded response is parsed -- the very first attempt can time out -- and the
   message still said "the daemon is still settling a prior launch" while
   printing the client's own default code in the Error(...) position, which
   reads as a server verdict. The code token stays stable (agents match on it)
   but the narrative now says where it came from. README corrected to match.
   Two tests pin both arms of the discriminator.

2. Trimmed the duplicated concurrency narration: the caller-side comments now
   point at SendSettledAck instead of restating its rationale, and
   BuildProcessedAck's summary is shorter. The invariants themselves stay --
   "never under _lock" and "the cache is committed before the ack is built"
   are exactly what five rounds of review found, and losing them would invite
   the same defects back.

38/38 settlement-retry tests, 25/25 processor tests, check-linear-ids clean.
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

All three addressed at b3a35df.

1 — ai1526 in comments. Removed. Both were design-doc paths, and they were doubly wrong for a public repo: they embed an internal issue slug and point at a file in the private kcap-server repo, so a public reader could never follow them. The API explanation stays; only the pointers went. (check-linear-ids.sh did pass on them — the script matches AI-####, not a lowercase ai1526 inside a filename — so this was a real gap in the check, not just a lint miss.)

3 — Error provenance. Correct, and it was worth more than a README edit, so I fixed the message too. LastCode is null when the deadline cancels an in-flight request before any coded response is parsed — the very first attempt can time out — and the message still asserted "the daemon is still settling a prior launch" while printing the client's own default code in the Error(...) position, which reads as a server verdict. The code token stays stable, because agents match on it, but the narrative now names its source:

Error (flow_settlement_busy): gave up after 1 attempt over 3m — no coded response arrived before the deadline, so the code above is this client's default rather than something the server reported.

Two tests pin both arms: no coded response must not say "still settling", and a real coded response must still say it. README updated to match.

2 — Verbose concurrency comments. Partly taken. I trimmed the duplication you pointed at: the two caller-side comments now point at SendSettledAck instead of restating its rationale, and BuildProcessedAck's summary is shorter.

I did not remove the invariants themselves, and I want to be explicit about why rather than quietly declining. "Never invoke the liveness read under _lock" and "the cache is committed before the ack is built" are precisely what a five-round adversarial review kept finding violations of on this file — the second one caught a case where a committed LaunchRejected(daemon_capacity) could be replayed later as a contradictory InternalError. The code cannot express either constraint on its own; nothing stops the next edit from calling Build(...) at the call site again. That's the case where a comment earns its lines. Happy to cut further if you can point at a specific one that isn't load-bearing.

A codex review flow also ran on this PR to clean over five rounds; the fixes it produced are in the commits above 29c42be.

@realtonyyoung
realtonyyoung merged commit 7a4975a into main Jul 28, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/qodo-followup-1526-1528 branch July 28, 2026 14:03
alexeyzimarev added a commit that referenced this pull request Jul 30, 2026
* docs: design for flow-participant-aware agent commands

Protects review and review-flow agents from accidental attach-injection and
stop, enforced daemon-side. Read-only attach, stop refuses without --force,
stop --all skips and says so.

Refs AI-1557, #379.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: implementation plan for flow-participant-aware agent commands

Also corrects the spec's Attached mechanism: its payload ends with an
unbounded snapshot, so a trailing flag would be painted onto the terminal
rather than parsed. Uses a separate AttachedReadOnly frame instead, which
also fails closed against an older client.

Refs AI-1557, #379.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(agent): carry and show each agent's launch kind

* feat(ipc): add StopV2 and AttachedReadOnly frames

* feat(daemon): attach to review and flow agents read-only

* test(daemon): prove the read-only attach test discriminates on stdin delivery

Attaching_to_a_flow_participant_is_read_only previously only asserted the
frame type and empty ClientDims — neither observes the Stdin arm, so
NoopPtyProcess silently swallowed writes and the test stayed green even
with the readOnly guard deleted. Seed the agent with a recording PTY
double instead and assert zero writes; add the mirror assertion to the
plain-agent case so the pair brackets the behaviour.

* feat(cli): print the read-only banner and stop sending input

* feat(daemon): refuse to stop review and flow agents without force

* fix(daemon): stop-all skip uses predicate negation, not record Except

Except hashed AgentInstance's mutable teardown fields (Status, LastOutputAt,
...) rather than identity, and was enumerated after the concurrent stops had
already started mutating them — a stopped agent could be misreported as
skipped too, with a duplicate row in the ack. Also corrects the refusal
message for a plain (non-flow) review agent, which previously claimed a
flow round it doesn't have.

* feat(cli): add `agent stop --force` and report skipped agents

Switches the client onto the StopV2 frame the daemon has enforced protection
against since #378: `stop --all` now partitions review/review-flow agents out
of the confirmation prompt and reports them as skipped (exit 0) rather than
stopping everything unconditionally, and `--force` opts back in. Also pins
LocalControlServer's StopV2 decode-and-dispatch over a real socket, the one
hop the codec round-trip and handler tests don't individually cover.

* docs: document read-only attach and stop --force for review agents

* docs: fix version-skew inaccuracies in the agent-protection caveat

The claim that daemon-side enforcement "holds regardless of client version"
was wrong: an old kcap sends the legacy Stop request (no --force concept),
and the daemon treats that as --force, so a stale client can silently
force-stop a review/review-flow agent against an up-to-date daemon. The
old-daemon sentence also conflated stop with ls/attach: those degrade
silently, but stop always sends the newer request format an old daemon
can't decode, so it hard-fails and tells the user to restart the daemon
instead of running unprotected. Also filled in two gaps in help-agent.txt's
parallel text: attach's read-only view also ignores terminal resize, and
--force lifts the single-id refusal, not just the --all skip.

* fix: address whole-branch review findings on flow-participant agent commands

- stop --all --force now groups protected agents under their own labelled
  heading, matching the spec (the plan's snippet had it backwards; fixed
  the plan too so re-execution won't replay the defect)
- the PID-record stop path (a prior-incarnation survivor) now honours
  Kind-based protection instead of reaping unconditionally; the decision
  reads the record via a new FindPidRecord accessor, keeping
  TryStopByPidRecordAsync itself policy-free for its server-origin caller
- KindText/IsProtectedKind/ProtectionReason fail safe on an unrecognised
  LaunchKind instead of defaulting to unprotected
- FrameCodec.StopV2 guards against a zero-length payload
- ParseAgentRow and the ls fetch path restore the 3-column floor a stray
  tab in a repo path could otherwise defeat
- doc/help/test fixes: help-agent.txt synopsis gets [--force], stale
  ls/AgentList column docs updated, and the tautological --force
  dispatch test is supplemented with one that can actually fail

Closes #379, AI-1557

* docs: correct the FindPidRecord summary and the ls row shape in top-level help

The FindPidRecord extraction left the original XML summary attached to the new
method, so it claimed to "reap it by identity and delete the record on confirmed
death" — which it does not; it only reads. Moved back onto
TryStopByPidRecordAsync, which does do that and was left undocumented.

help-usage.txt still described `agent ls` as (id, status, repo); it grew a KIND
column. This was the fourth instance of that stale text, the other three having
been corrected already.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Neutralise table delimiters in the agent list and refuse malformed rows

`AgentList` is a tab/newline-delimited table, and RepoPath, FlowRunId and
FlowRole are free-form — a repo path may legally contain a tab or newline. Emitted
raw, one shifts the reader's columns or splits the row.

That is not merely cosmetic. The CLI keys `stop --all`'s confirmation off the kind
column, so a shifted row makes a plain agent read as protected: the prompt says
"Skipping 1 review agent", the user confirms N, and the daemon — which computes
eligibility itself and is authoritative — stops N+1. The confirmation understates
the blast radius, which is the property the prompt exists to convey.

Closed at both layers. The daemon replaces tab/CR/LF with a space in the three
free-form cells. The CLI accepts only exactly 3 columns (older daemon) or exactly
6 (current) and refuses the whole table otherwise, rather than acting on a guess.

Found by Qodo on #408. Two earlier reviews rated this class Minor on the grounds
that the daemon stays authoritative for the stop — true, but it is the prompt that
the user consents to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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