From c37ced6e6ea654156e8d2a5cea77a174a10bfd9e Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 16 Aug 2026 18:52:19 +0200 Subject: [PATCH] CB-582 + CB-604: the ask nudge, and kind: is now validated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kind: entry documented the unvalidated-typo gotcha as live. CB-604 fixed it, so the entry now carries the refusal message instead, and points at CB-606 for the three fields that still have the same shape. New entry for CB-582: a worker paused on bridge_ask nudges the lead's pane, and bridge_status and REST both show the open question. The gotcha is the part that matters — the ~55s window is closed, not removed, so 'do not brief a worker to ask me' still stands. --- 11-Features.md | 64 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/11-Features.md b/11-Features.md index 5850a48..a059c12 100644 --- a/11-Features.md +++ b/11-Features.md @@ -1166,6 +1166,43 @@ durable artefact; the PR is.** If you orchestrate over REST, poll on a timer. --- +## Get told when a worker is waiting on your answer + +**What.** A worker on an async (`wait:false`) delegation that pauses mid-turn in `bridge_ask` now +nudges the lead's pane by itself, naming the exact call that resumes it: + +``` +Worker term_a asked a question (ticket task-3) — answer it with +bridge_send(turnId="term_a#1", content=...) to resume its turn: +which config file? +``` + +`bridge_status{sessionId}` shows the same open question, and so does REST +`GET /sessions/{id}/status` (as `question`, `turnId`, `ticket`). + +**On.** Automatic, on the same terms as the ticket nudge above — it is a **third source in the same +per-lead schedule**, not a new push path, so CB-590's one-schedule-per-lead guarantee still holds and +it spends from its own `push_reminders` budget. + +**Why.** `bridge_ask` opens a reverse-rendezvous window of about **55 seconds**. A lead polling on its +normal cadence of minutes never saw it, so the worker timed out and carried on without an answer — +the ask was, in practice, unusable on the delegation mode the charter tells leads to prefer. Two +smaller holes closed with it: REST `GET /tasks/{ticket}` dropped `turnId` on an `ASKING` phase, so a +REST caller could read the question and had no way to answer it, and `bridge_status` said nothing +about an open question at all. + +**Gotcha.** **This closes the window; it does not remove it.** The worker still gets ~55 seconds, and +a nudge only helps a lead that is injectable right now — a lead mid-turn for a minute still misses it. +So the standing advice is unchanged: **do not brief a worker to "ask me."** Decide the question before +you delegate, or give the worker an explicit default to use. + +Why the window is not simply widened: `DEFAULT_ASK_TIMEOUT_MS = 55_000` sits just under the *worker's +own* MCP client cap of about 60 seconds, so the daemon can return a clean typed timeout before the +client severs the call. Raising the server constant buys nothing — the worker's client kills the call +regardless. + +--- + ## Members cannot use the operator's admin forge token **What.** Every member launch overwrites `GITEA_ACCESS_TOKEN` with a non-blank blocked sentinel, and @@ -1671,14 +1708,25 @@ adapter. existing *Pin an opencode endpoint* entry). The partition-by-kind design itself reads as the natural consequence: profiles fully own their backend, so routing is a lookup, not a branch. -**Gotcha.** `kind:` is lower-cased but **never validated against the two known values.** A typo — -`kind: opencod`, say — is silently accepted, normalized, and (because it doesn't equal -`"opencode"`) routed into the **claude-code** adapter bucket. If `argv:` was also left unset, the -launch command defaults to `List.of(k)` — literally the misspelled string itself — rather than -`claude`, because the argv-defaulting logic only special-cases the exact string -`"claude-code"`. There is no config-load check anywhere that would catch this before spawn. -Separately, the composite constructor does refuse two adapters claiming the same profile name -("worker profile '…' is claimed by two peer adapters"), so that failure mode is caught loud. +**Validated at config load since CB-604 (2026-08-16).** An unrecognized `kind:` now refuses to +start, naming the profile, the bad value, the accepted set, and what would otherwise happen: + +``` +refusing to start: profile(s) [gemini=opencod] set an unrecognized kind — accepted values are +claude-code, opencode (case-insensitive); an unrecognized kind would otherwise fall back to the +claude-code adapter and try to launch a program named after the typo. +``` + +Before that fix, `kind: opencod` was silently accepted, normalized, and — because it did not equal +`"opencode"` — routed into the **claude-code** adapter bucket. With `argv:` also unset, the launch +command defaulted to `List.of(kind)`, literally the misspelled string, because the argv default only +special-cases the exact string `"claude-code"`. Nothing caught it before the spawn failed. + +**Gotcha.** The composite constructor separately refuses two adapters claiming the same profile name +("worker profile '…' is claimed by two peer adapters"), so that failure mode has always been loud. +Checking for the same shape elsewhere found three more unvalidated fields — `auth.mode`, the +per-profile `placement:`, and the top-level `placement:` policy, which is validated but only lazily +at first spawn. That is CB-606; until it lands, a typo in those three is still accepted silently. ---