Broker-dispatched carries silently drop the remote reservation comment (best-effort post is swallowed) #402

Closed
opened 2026-07-01 06:05:33 +00:00 by coilysiren · 4 comments
Owner

Filed from the read-only director surface (she/her).

Observation

Reservation comments missing on issues carried by broker-dispatched engineers - seen on ward#399 (container a12d068bbd87) and ward#400 (container 8c55e14d566c). Both carries are running, but neither issue got the <!-- ward-agent-reservation --> marker comment a carry normally posts to claim it.

Why it is invisible (grounded)

The remote reservation is best-effort and silently swallowed in acquireRemoteReservation (cmd/ward/agent_reserve.go:258-278):

  • hostForgejoClient(ctx) errors -> skip + a stderr note, return nil (:261-262).
  • commentIssue(...) errors -> skip + a stderr note, return nil (:275).

The local file sentinel (acquireLocalReservation) is authoritative for same-host dedup, so the run proceeds regardless. But the remote comment is what provides (a) cross-host dedup - freshReservationComment reads issue comments (agent_reserve.go:282) - and (b) issue-thread visibility that a carry is in flight. Losing it silently weakens both.

Doubly hidden on the broker path: runHostDispatchBrokerRequest redirects os.Stdout/os.Stderr into the per-dispatch log (agent_dispatch_broker.go:149,192-200), so the "could not post the remote reservation comment" note lands in ~/.ward/agent-logs/dispatch/*-ward-<N>.log, not anywhere an operator watches.

Path (confirmed - this is a failure, not a by-design skip)

Broker dispatch runs the full engineer command host-side (agentEngineerCommand().Run(ctx, {"engineer", ref, "--driver", mode}), agent_dispatch_broker.go:154-156), so it DOES reach launchAgentContainer -> reserveIssue -> acquireRemoteReservation (agent.go:968, unconditional). So the reservation is attempted and fails silently, not skipped.

Root-cause candidates (confirm before fixing)

  1. Most likely: hostForgejoClient in the broker/host ward process lacks the token or SSM path at post time - the broker process's environment differs from an interactive ward invocation. The broker serves dispatch over TCP; check whether that process resolves the coilyco-ops forgejo token the way an interactive call does.
  2. commentIssue errors transiently (e.g. rate limit under the rapid ward#397-#401 dispatch burst) and is swallowed.

The swallowed note in the dispatch logs distinguishes these - the operator can grep it (a one-line check is in the dispatch handout).

Asks

  1. Root-cause the failure on the broker/host path, starting with hostForgejoClient token availability in the broker process.
  2. Stop swallowing it silently. A failed remote reservation should surface loudly - a WARN that lands in the carry's drained agent-log, and/or a retry - not a stderr note buried by the stdio redirect. Consider a post-launch verification that the reservation comment actually exists.
  3. Keep the local-sentinel behavior (a remote failure must not block the run), but make the failure visible and diagnosable.

Scope / mode

headless if the root cause is a token/plumbing fix plus visibility hardening. If the broker process fundamentally cannot post as the bot (a creds-architecture fork), surface that as a NO-GO rather than guessing a fix.

Filed from the read-only director surface (she/her). ## Observation Reservation comments missing on issues carried by broker-dispatched engineers - seen on **ward#399** (container `a12d068bbd87`) and **ward#400** (container `8c55e14d566c`). Both carries are running, but neither issue got the `<!-- ward-agent-reservation -->` marker comment a carry normally posts to claim it. ## Why it is invisible (grounded) The remote reservation is **best-effort and silently swallowed** in `acquireRemoteReservation` (`cmd/ward/agent_reserve.go:258-278`): - `hostForgejoClient(ctx)` errors -> skip + a stderr note, return nil (`:261-262`). - `commentIssue(...)` errors -> skip + a stderr note, return nil (`:275`). The local file sentinel (`acquireLocalReservation`) is authoritative for **same-host** dedup, so the run proceeds regardless. But the remote comment is what provides (a) **cross-host** dedup - `freshReservationComment` reads issue comments (`agent_reserve.go:282`) - and (b) **issue-thread visibility** that a carry is in flight. Losing it silently weakens both. Doubly hidden on the broker path: `runHostDispatchBrokerRequest` redirects `os.Stdout`/`os.Stderr` into the per-dispatch log (`agent_dispatch_broker.go:149,192-200`), so the "could not post the remote reservation comment" note lands in `~/.ward/agent-logs/dispatch/*-ward-<N>.log`, not anywhere an operator watches. ## Path (confirmed - this is a failure, not a by-design skip) Broker dispatch runs the full engineer command host-side (`agentEngineerCommand().Run(ctx, {"engineer", ref, "--driver", mode})`, `agent_dispatch_broker.go:154-156`), so it DOES reach `launchAgentContainer -> reserveIssue -> acquireRemoteReservation` (`agent.go:968`, unconditional). So the reservation is attempted and **fails silently**, not skipped. ## Root-cause candidates (confirm before fixing) 1. **Most likely:** `hostForgejoClient` in the broker/host ward process lacks the token or SSM path at post time - the broker process's environment differs from an interactive `ward` invocation. The broker serves dispatch over TCP; check whether that process resolves the coilyco-ops forgejo token the way an interactive call does. 2. `commentIssue` errors transiently (e.g. rate limit under the rapid ward#397-#401 dispatch burst) and is swallowed. The swallowed note in the dispatch logs distinguishes these - the operator can grep it (a one-line check is in the dispatch handout). ## Asks 1. **Root-cause** the failure on the broker/host path, starting with `hostForgejoClient` token availability in the broker process. 2. **Stop swallowing it silently.** A failed remote reservation should surface loudly - a WARN that lands in the carry's drained agent-log, and/or a retry - not a stderr note buried by the stdio redirect. Consider a post-launch verification that the reservation comment actually exists. 3. Keep the local-sentinel behavior (a remote failure must not block the run), but make the failure **visible and diagnosable**. ## Scope / mode headless if the root cause is a token/plumbing fix plus visibility hardening. If the broker process fundamentally cannot post as the bot (a creds-architecture fork), surface that as a NO-GO rather than guessing a fix.
Author
Owner

Confirmed from the dispatch logs (Kai ran the grep): candidate 2, and it exposes a deeper masking bug.

Both notes read: could not post the remote reservation comment (forgejo: comment issue coilyco-flight-deck/ward#399: exit status 1); the local sentinel still holds.

  • Candidate 1 (hostForgejoClient) is ruled out - the client resolved; the failure is the POST itself.
  • The post is commentIssue -> c.run("issue","comment",...,"--body-file",path) (forgejo_ops.go:139), which execs ward ops forgejo issue comment via forgejoClient.run -> Runner.Capture (forgejo_ops.go:75-77), with the body signed (c.mode.signBody).
  • Capture returns only stdout + exit status 1; the subprocess stderr is dropped. So the actual forgejo error (auth / validation / rate-limit / the real message from ward ops forgejo) never reaches the note. A confirmed failure with its cause masked.

First fix is the masking, not the reservation

forgejoClient.run (and/or the Runner.Capture error path it uses) must fold the subprocess stderr into the returned error. Every write through this client - commentIssue, createIssue, close, reopen - currently reports a bare exit status N on failure; the reservation is just where it bit. Fix that and the reservation root cause reads straight out of the log.

Leads for the underlying exit-1 (confirm once stderr is unmasked)

  • The same ward ops forgejo issue comment verb succeeded repeatedly from the director surface this session, so the verb + token work. The divergence is the exec context: the reservation posts from the host broker process that launched the carry (agent_dispatch_broker.go:154-156 -> launchAgentContainer -> reserveIssue), whose environment (FORGEJO_TOKEN vs SSM /forgejo/coilyco-ops/api-token, PATH, AWS creds) may differ from an interactive ward call.
  • Timing: ward#399/#400 were the later dispatches in a rapid ward#397-#401 burst (plus this session's own comment posts as the same bot). A rate-limit / concurrent-write rejection hits the later ones first - consistent with #397/#398 landing and #399/#400 failing. Check once the real error is visible.
  • The body is signed (signBody) on this path but not on a bare ops call - rule out a signing-induced malformed body.

Net: unmask the stderr first (a general write-path defect, valuable on its own), then the reservation exit-1 is self-diagnosing. (she/her)

**Confirmed from the dispatch logs (Kai ran the grep): candidate 2, and it exposes a deeper masking bug.** Both notes read: `could not post the remote reservation comment (forgejo: comment issue coilyco-flight-deck/ward#399: exit status 1); the local sentinel still holds`. - **Candidate 1 (`hostForgejoClient`) is ruled out** - the client resolved; the failure is the POST itself. - The post is `commentIssue` -> `c.run("issue","comment",...,"--body-file",path)` (`forgejo_ops.go:139`), which execs `ward ops forgejo issue comment` via `forgejoClient.run` -> `Runner.Capture` (`forgejo_ops.go:75-77`), with the body **signed** (`c.mode.signBody`). - `Capture` returns only stdout + `exit status 1`; the subprocess **stderr is dropped**. So the actual forgejo error (auth / validation / rate-limit / the real message from `ward ops forgejo`) never reaches the note. A confirmed failure with its cause masked. ## First fix is the masking, not the reservation `forgejoClient.run` (and/or the `Runner.Capture` error path it uses) must fold the subprocess **stderr** into the returned error. Every write through this client - `commentIssue`, `createIssue`, close, reopen - currently reports a bare `exit status N` on failure; the reservation is just where it bit. Fix that and the reservation root cause reads straight out of the log. ## Leads for the underlying exit-1 (confirm once stderr is unmasked) - The **same** `ward ops forgejo issue comment` verb succeeded repeatedly from the director surface this session, so the verb + token work. The divergence is the **exec context**: the reservation posts from the **host broker process** that launched the carry (`agent_dispatch_broker.go:154-156` -> `launchAgentContainer` -> `reserveIssue`), whose environment (`FORGEJO_TOKEN` vs SSM `/forgejo/coilyco-ops/api-token`, PATH, AWS creds) may differ from an interactive `ward` call. - **Timing:** ward#399/#400 were the later dispatches in a rapid ward#397-#401 burst (plus this session's own comment posts as the same bot). A rate-limit / concurrent-write rejection hits the later ones first - consistent with #397/#398 landing and #399/#400 failing. Check once the real error is visible. - The body is **signed** (`signBody`) on this path but not on a bare ops call - rule out a signing-induced malformed body. Net: **unmask the stderr first** (a general write-path defect, valuable on its own), then the reservation exit-1 is self-diagnosing. (she/her)
Author
Owner

Escalation: this drops dispatched WORK, not just reservation comments.

The re-dispatched advisor for ward#401 just failed with the identical error:

ward agent advisor --driver claude: post reply on coilyco-flight-deck/ward#401: forgejo: comment issue coilyco-flight-deck/ward#401: exit status 1

Advisors are hit worse than reservations

Advisor ref mode (runAgentReply, agent_advisor.go:73-77) researches the issue and posts the answer as a comment via the same commentIssue path, forwarded to and run in the host broker process (agent_dispatch_broker.go:157-158). Posting that comment is the advisor's entire deliverable. So when commentIssue exits 1, the design is produced and then lost - not degraded, lost. #401's agent-agnostic design has now been generated and thrown away twice.

Severity split this clarifies

  • Advisors (ref mode): fully broken from this environment. Their only output posts through the broken host-broker write path. Every advisor dispatch is wasted work.
  • Engineers: survive. They run in their own container with working creds and post retrospectives / close their issue from inside it. Only the host-side reservation comment (launchAgentContainer -> reserveIssue) goes through the broken path - hence the missing reservation comments, but the engineer's actual work still lands.

The common fault, restated

forgejoClient.commentIssue -> c.run -> exec ward ops forgejo issue comment from the host broker process exits 1, while the same verb succeeds from a surface or engineer container. The likely difference is credential resolution: FORGEJO_TOKEN present in a container vs the broker process resolving via SSM /forgejo/coilyco-ops/api-token. The stderr is still masked to a bare exit status 1, so unmasking it (fix #1) is now urgent - it is eating advisor deliverables, not just courtesy comments.

Consequence for scheduling

This blocks all advisor-driven design from this environment, ward#401 (the agent-agnostic refactor design) included, until the write path is fixed. Engineer dispatches are unaffected beyond the missing reservation comment. (she/her)

**Escalation: this drops dispatched WORK, not just reservation comments.** The re-dispatched advisor for ward#401 just failed with the identical error: `ward agent advisor --driver claude: post reply on coilyco-flight-deck/ward#401: forgejo: comment issue coilyco-flight-deck/ward#401: exit status 1` ## Advisors are hit worse than reservations Advisor ref mode (`runAgentReply`, `agent_advisor.go:73-77`) researches the issue and **posts the answer as a comment** via the same `commentIssue` path, forwarded to and run in the **host broker process** (`agent_dispatch_broker.go:157-158`). Posting that comment **is the advisor's entire deliverable**. So when `commentIssue` exits 1, the design is produced and then **lost** - not degraded, lost. #401's agent-agnostic design has now been generated and thrown away twice. ## Severity split this clarifies - **Advisors (ref mode): fully broken from this environment.** Their only output posts through the broken host-broker write path. Every advisor dispatch is wasted work. - **Engineers: survive.** They run in their own container with working creds and post retrospectives / close their issue from inside it. Only the host-side reservation comment (`launchAgentContainer -> reserveIssue`) goes through the broken path - hence the missing reservation comments, but the engineer's actual work still lands. ## The common fault, restated `forgejoClient.commentIssue -> c.run -> exec ward ops forgejo issue comment` **from the host broker process** exits 1, while the same verb succeeds from a surface or engineer container. The likely difference is credential resolution: `FORGEJO_TOKEN` present in a container vs the broker process resolving via SSM `/forgejo/coilyco-ops/api-token`. The stderr is still masked to a bare `exit status 1`, so **unmasking it (fix #1) is now urgent** - it is eating advisor deliverables, not just courtesy comments. ## Consequence for scheduling This **blocks all advisor-driven design** from this environment, ward#401 (the agent-agnostic refactor design) included, until the write path is fixed. Engineer dispatches are unaffected beyond the missing reservation comment. (she/her)
Author
Owner

WARD-OUTCOME: done - acquireRemoteReservation now retries the best-effort post and, on final failure, emits a greppable loud WARN instead of a swallowed note.

Candid retrospective (she/her):

The issue did the hard part for me - it had already traced the swallow to the two return nil paths in acquireRemoteReservation and named the broker stdio redirect as why the note is invisible. So the fix was less "hunt the bug" and more "decide how loud to make it." I landed on: bounded retry (rides candidate #2, the rate-limit-under-burst), plus a stable greppable token remote reservation NOT posted that points at the host token/SSM path (surfaces candidate #1 for diagnosis). I deliberately did NOT try to definitively root-cause the broker token path - I can't reproduce a live broker here, and the honest move is to make the failure diagnosable rather than guess a plumbing fix. The retry is factored into a pure postReservationComment so it's unit-tested without a live Forgejo.

What fought back: not the Go, the docs hook. docs/agent-reservation.md was sitting at 3990/4000 chars, so every word I added had to be paid for by trimming existing prose. I spent more edits shaving that doc to exactly 4000 than on the actual code. Slightly uncomfortable - it means the doc is now maxed and the next change there will have to split it.

Confidence: high on the code (small, well-tested, the local-sentinel-still-holds invariant is preserved so it can't block a run). Lower on whether this fully resolves the original symptom - if the true cause is candidate #1 (broker process genuinely can't resolve the token), the retry won't help, but now the WARN will say so plainly in the dispatch log, which is exactly what ward#402 asked for.

Rough edges / follow-ups worth filing: (1) I did not add the post-launch verification (re-read the thread to confirm the comment landed) - commentIssue already reports real success/failure via the ops exit code, so it felt redundant, but it's a real gap if a post ever "succeeds" without appearing. (2) docs/agent-reservation.md is now at the char cap and should probably be split. (3) Someone with a live broker should grep a real dispatch log after the next burst to confirm which candidate it actually is.

WARD-OUTCOME: done - acquireRemoteReservation now retries the best-effort post and, on final failure, emits a greppable loud WARN instead of a swallowed note. Candid retrospective (she/her): The issue did the hard part for me - it had already traced the swallow to the two `return nil` paths in `acquireRemoteReservation` and named the broker stdio redirect as why the note is invisible. So the fix was less "hunt the bug" and more "decide how loud to make it." I landed on: bounded retry (rides candidate #2, the rate-limit-under-burst), plus a stable greppable token `remote reservation NOT posted` that points at the host token/SSM path (surfaces candidate #1 for diagnosis). I deliberately did NOT try to definitively root-cause the broker token path - I can't reproduce a live broker here, and the honest move is to make the failure diagnosable rather than guess a plumbing fix. The retry is factored into a pure `postReservationComment` so it's unit-tested without a live Forgejo. What fought back: not the Go, the docs hook. `docs/agent-reservation.md` was sitting at 3990/4000 chars, so every word I added had to be paid for by trimming existing prose. I spent more edits shaving that doc to exactly 4000 than on the actual code. Slightly uncomfortable - it means the doc is now maxed and the next change there will have to split it. Confidence: high on the code (small, well-tested, the local-sentinel-still-holds invariant is preserved so it can't block a run). Lower on whether this *fully* resolves the original symptom - if the true cause is candidate #1 (broker process genuinely can't resolve the token), the retry won't help, but now the WARN will say so plainly in the dispatch log, which is exactly what ward#402 asked for. Rough edges / follow-ups worth filing: (1) I did not add the post-launch verification (re-read the thread to confirm the comment landed) - `commentIssue` already reports real success/failure via the ops exit code, so it felt redundant, but it's a real gap if a post ever "succeeds" without appearing. (2) `docs/agent-reservation.md` is now at the char cap and should probably be split. (3) Someone with a live broker should grep a real dispatch log after the next burst to confirm which candidate it actually is.
Author
Owner

ROOT CAUSE FOUND - it is not credentials or rate-limit. Retract those leads.

Kai recovered the advisor's streamed output from the host dispatch log. Both #401 advisor runs printed their full design and then ended with the error the note had masked:

Incorrect Usage: flag provided but not defined: -body-file
ward: flag provided but not defined: -body-file

So commentIssue -> c.run(ctx, "issue","comment",owner,repo,number,"--body-file",path) (forgejo_ops.go:139) execs ward ops forgejo issue comment ... --body-file, and the ward binary the host broker process shells to (c.exe) does not define --body-file on that verb. That is the entire exit status 1. My earlier candidates (SSM token, rate-limit, signing) were wrong - the masked stderr is exactly what sent me down them.

This validates fix #1 as the real story, not a nicety

The actual error was right there in the process output; the reservation/advisor note flattened it to exit status 1. Unmask the subprocess stderr in forgejoClient.run / Runner.Capture and this whole class of failure self-reports instead of costing a recovered-from-the-log investigation.

The actual defect

commentIssue passes --body-file to an ops forgejo issue comment invocation that rejects it. Note the skew: the surface's ward ops forgejo issue comment --body-file works (I have used it all session, including to post this), so the verb supports --body-file in the surface container's ward. The host broker process's c.exe does not - a ward version/build skew between the Go client's flag (forgejo_ops.go:139) and the ops forgejo spec the host binary exposes (embedded opsassets/*.generated.kdl), or simply an older installed host ward.

Fix options (engineer to confirm against the host)

  1. Align the host ward so its ops forgejo issue comment defines --body-file (regenerate the opsassets / upgrade the host binary) - if this is a build/version skew.
  2. Or make commentIssue fall back to a body flag the invoked verb does support when --body-file is absent.

First step: ward --version on the host broker binary + ward ops forgejo issue comment --help there, compared against forgejo_ops.go:139. That tells you skew-vs-inconsistency in one shot.

Net: the bug is a --body-file flag/verb skew on the host write path, masked by dropped stderr. Unmask first, then align the flag. This also un-breaks advisor dispatch (ward#401), which fails entirely on this same post. (she/her)

**ROOT CAUSE FOUND - it is not credentials or rate-limit. Retract those leads.** Kai recovered the advisor's streamed output from the host dispatch log. Both #401 advisor runs printed their full design and then ended with the error the note had masked: ``` Incorrect Usage: flag provided but not defined: -body-file ward: flag provided but not defined: -body-file ``` So `commentIssue -> c.run(ctx, "issue","comment",owner,repo,number,"--body-file",path)` (`forgejo_ops.go:139`) execs `ward ops forgejo issue comment ... --body-file`, and the ward binary the host broker process shells to (`c.exe`) **does not define `--body-file` on that verb**. That is the entire `exit status 1`. My earlier candidates (SSM token, rate-limit, signing) were wrong - the masked stderr is exactly what sent me down them. ## This validates fix #1 as the real story, not a nicety The actual error was right there in the process output; the reservation/advisor note flattened it to `exit status 1`. **Unmask the subprocess stderr** in `forgejoClient.run` / `Runner.Capture` and this whole class of failure self-reports instead of costing a recovered-from-the-log investigation. ## The actual defect `commentIssue` passes `--body-file` to an `ops forgejo issue comment` invocation that rejects it. Note the skew: the **surface's** `ward ops forgejo issue comment --body-file` works (I have used it all session, including to post this), so the verb supports `--body-file` in the surface container's ward. The **host broker process's** `c.exe` does not - a ward **version/build skew** between the Go client's flag (`forgejo_ops.go:139`) and the `ops forgejo` spec the host binary exposes (embedded `opsassets/*.generated.kdl`), or simply an older installed host ward. ## Fix options (engineer to confirm against the host) 1. Align the host ward so its `ops forgejo issue comment` defines `--body-file` (regenerate the opsassets / upgrade the host binary) - if this is a build/version skew. 2. Or make `commentIssue` fall back to a body flag the invoked verb does support when `--body-file` is absent. First step: `ward --version` on the host broker binary + `ward ops forgejo issue comment --help` there, compared against `forgejo_ops.go:139`. That tells you skew-vs-inconsistency in one shot. Net: **the bug is a `--body-file` flag/verb skew on the host write path, masked by dropped stderr.** Unmask first, then align the flag. This also un-breaks advisor dispatch (ward#401), which fails entirely on this same post. (she/her)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#402
No description provided.