Skip to content

fix(dashboard): stop rendering an approval-parked subagent as running - #7477

Merged
bolichen97 merged 1 commit into
mainfrom
fix/parked-subagent-render
Sep 1, 2026
Merged

fix(dashboard): stop rendering an approval-parked subagent as running#7477
bolichen97 merged 1 commit into
mainfrom
fix/parked-subagent-render

Conversation

@chenmingwei23

@chenmingwei23 chenmingwei23 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

1. What is the problem?

A sub-agent parked on an unanswered spawn-approval prompt was rendered by the
dashboard as running.

Two surfaces folded status 'pending' into their running tally:

  • SubagentProgressBar (the wave chip above the composer) counted the parked
    run behind the spinning Loader2, and its per-agent row rendered a bare task
    label with a ticking elapsed timer -- pixel-identical to an agent that had
    launched a process and was working.
  • SubagentRunCard (the inline launch card in the transcript) printed
    1 agent running for a wave that had started nothing.

The run really had launched nothing: it is registered in _agents, counted by the
manager, and blocked on a prompt the user has not answered. turns == 0,
_pid is None, _exec_started is None.

One correction to the issue's stated mechanism. The issue attributes this to
the dashboard not consuming the backend awaiting_approval field from #7299, and
says the chip reads it off /api/spawn. Neither is how the chip gets its state:
the chip is fed by WS frames and polls /api/spawn only to reconcile phantom
rows. The dashboard already had the equivalent state, and had it before #7299:
sseSubagentPending writes status: 'pending' plus an approval_id from the WS
approval frame whose id is spawn:<agent_id>, and the Subagents side panel
already renders that as "Awaiting approval" with inline Approve/Reject.

So the defect is not a missing wire field. It is that the two tally surfaces
never asked the question, and the predicate that answers it -- status === 'pending' && !!approval_id -- was a module-private const in chatSlice with
three call sites, none of them a renderer.

2. Why this issue matters to the user

The running count is the one number these two surfaces exist to publish, and for
a parked run it asserted the opposite of the truth: the user was told work was in
progress while the wave was in fact waiting on them. There is no timeout on the
far side, so the run sits there until it is answered.

The launch card makes it worse than a transient glitch. The chip disappears when
the wave ends and only ever covers the slot you are viewing; the card stays
anchored in scrollback. So the false "running" claim is the one that persists.

3. How our fix solves it

Chain from the symptom back to the cause:

row/count says "running" -> the tally treats 'pending' as running -> the
renderers never consulted the predicate -> the predicate was private to the
store, so each renderer re-derived the question from status alone.

The fix cuts at the last link, then repairs the two tallies:

  • chatSlice.ts -- export isAwaitingSpawnApproval so the renderers share the
    ONE definition. approval_id is the load-bearing half: sseSubagentPending is
    the only writer of 'pending' and always sets it, so its absence means a card
    built some other way and must not be claimed as blocked on the user.
  • SubagentProgressBar.tsx -- parked runs leave the spinning running count
    and are reported under their own count with a distinct glyph; the per-agent row
    names the approval instead of rendering blank. Two details that are not
    cosmetic:
    • the mount predicate gains the awaiting term. Excluding parked runs from
      running means a wave whose only member is parked now has running === 0,
      and the old running > 0 || queued > 0 would have unmounted the one surface
      naming what the wave is blocked on. There is a test for exactly this.
    • the approval branch is checked before retrying/stalled. A run that
      never executed produces the watchdog's silence trivially, so a stall badge on
      it is describing an absence this row can already explain exactly; the
      approval is strictly more specific, and it is the state the user can act on.
  • SubagentRunCard.tsx -- the same split in tally(), plus an awaiting chip
    and a leading glyph ranked above failed: a failure is history, an unanswered
    approval is still actionable. The chip needs no settled === 0 guard (unlike
    the queued chip, which is slot-keyed and can report another wave's queue) --
    awaiting is derived from this launch's own ids.
  • i18n -- one new key per namespace, translated across all 12 authored
    locales, en-XA regenerated.

Deliberately NOT in this PR: the backend hop. #7318's comments ask for
awaiting_approval on the subagent WS payload in dashboard/ws.py, gated on
_awaiting_approval is True AND _exec_started is None. That is unbuildable on
main today and would be dead code if merged: main never sets
_awaiting_approval on the spawn path at all (its three writers in run.py are
all in-run TOOL approvals), so the gate is always false for a spawn-parked run.
The writer, and the _awaiting_spawn_approval helper the comment says to reuse,
both live in PR #7299, which is still open. Landing that hop here would
duplicate an open PR's diff to ship an always-false field.

The same dependency bounds the other half of the issue: an unowned spawn (from
the CLI, slot="") gets no pending card at all -- useWebSocket requires
data.slot before dispatching, deliberately, so an approval cannot be
misattributed to whatever chat the user happens to be viewing. Giving that run a
dashboard representation, and the click-through the issue asks for, needs #7299's
HTTP field. It is a clean follow-up once #7299 merges.

4. What tests we did

  • New: website/src/test/SubagentProgressBar.parkedApproval.test.tsx, 6
    assertions -- parked run excluded from running and reported as awaiting; the
    row names the approval; the chip stays mounted when the parked run is the whole
    wave; no awaiting count when nothing is parked; the approval wins over a stall
    verdict on the same run; a 'pending' entry with no approval_id keeps its
    place in the running count.
  • Extended: SubagentRunCard.test.tsx +4 -- a wholly parked wave does not
    claim to be running, a mixed wave counts only the members that started, no chip
    when nothing is parked, and the same no-approval_id negative control.
  • Mutation-verified red on base. Reverting SubagentProgressBar.tsx alone
    reds 4 of the 6 new assertions; reverting SubagentRunCard.tsx alone reds 2 of
    the 4. The cases that stay green in both are the negative controls, which is
    what they are for.
  • No regressions: the 11 neighbouring subagent suites pass, 110 tests
    (SubagentProgressBar.* x4, SubagentProgress/Scale/Resilience.reducers,
    ChatSidebar.subagentApproval, ChatSidebar.subagentRunning,
    selectSlotSubagents, subagentActivity). SubagentRunCard.test.tsx is 28/28
    including its 24 pre-existing cases.
  • i18n gate chain: npm run i18n:check exits 0, all 19 checks PASS, with
    [source-strings] 2 new English key(s) / 0 badly shaped and [pseudolocale] en-XA matches en. The 11 catalog data suites pass, 233 tests.
  • Lint / types: eslint on the touched files reports the one no-console
    warning that is already on base (verified by linting the stashed base file --
    line 157 before, 165 after); zero new warnings. tsc -b produces a
    byte-identical error set before and after my diff (16 lines, all downstream of
    one missing devDependency in this sandbox -- see below).

Screenshots

Captured from the REAL built SPA by website/scripts/capture-parked-subagent-chip.mjs
(committed here), which pushes the same WS frames the gateway sends -- a
subagent_spawn for the member that started, and an approval whose id is
spawn:<agent_id> for the one parked on the user. The scenario is a two-member
wave: one executing, one parked.

The harness ASSERTS the rendered strings before it photographs, so a stale bundle
reds rather than quietly capturing the old copy, and --expect-before inverts the
verdict so the BEFORE frame has to actually REPRODUCE the defect (a blank page
would otherwise pass as convincing evidence).

The wave chip above the composer. Before, the parked run is a bare label with a
ticking timer, indistinguishable from the row below it that is doing work, and the
spinner claims 2:

wave chip before

After -- 1 running, 1 awaiting, and the parked row says what it is waiting for:

wave chip after

The launch card in scrollback, which is the one that persists after the chip
drops. Before, then after:

launch card before

launch card after

Whole surface after the fix, showing every surface finally agreeing -- the sidebar
row ("1 sub-agent needs approval"), the launch card, the wave chip, and the
composer's pre-existing approval banner:

chat surface after

Harness output on the two trees:

AFTER  { running: '1', awaiting: '1', cardAwaiting: '1', parkedRow: true,
         cardRunning: true, cardClaimsTwoRunning: false }   -> pass
BEFORE { running: '2', awaiting: null, cardAwaiting: null, parkedRow: false,
         cardRunning: false, cardClaimsTwoRunning: true }   -> defect reproduced

5. Any other suggestions on the work

  • This PR was red for a while on lanes it did not cause; both are resolved
    upstream now.
    Frontend Lint & Type Check runs eslint src/ --max-warnings 659 and main had drifted to 660, so it failed on every PR whose changed surface
    reached that lane. Backend Tests failed on two main-owned tests in turn: the
    dashboard/handlers/files.py log-site census, and
    test_irq.py::test_an_entry_joining_after_a_partial_fire_serves_its_own_floor.
    None of them are reachable from this diff, which contains zero Python files. The
    branch is now rebased onto main at c412c2ff9, which carries fix(ci): re-measure the files.py log-site census after #7293 #7492, fix(ci): repair two cross-merge breakages reding main's own tip #7508 and
    fix(ci): bring two drifted ratchet gates back in line with the code #7512; measured on that tree, eslint src/ reports 653 warnings, 0 errors
    (six under the ceiling), test_irq.py is 60/60, and the census test passes.
    Attribution for the one warning this PR's files do carry: it is the pre-existing
    no-console in SubagentProgressBar.tsx, already on base at line 157 and moved
    to 165 by my diff -- CI's own per-file warning list named that file for that
    warning only, and none of my other five files appeared at all.
  • Building this locally needed a workaround worth knowing about. main added
    @radix-ui/react-tabs to website/package.json; this host's registry rejects
    auth (E401, and it is not in the npm cache), so tsc -b and vite build both
    fail on that one unresolved import. I confirmed it is entirely upstream of this
    diff -- tsc -b produces a byte-identical 16-line error set before and after my
    change, every line downstream of src/components/ui/tabs.tsx -- and produced the
    screenshots against a local, uncommitted shim for that package alone. CI installs
    it properly and its tsc -b step passed.
  • The composer banner next door has two real defects, left alone on purpose.
    ChatInput.tsx (~line 2787) hardcodes '1 sub-agent is awaiting your approval to run' / `${n} sub-agents are awaiting...` -- an untranslated English
    literal with the plural chosen in JS, which is precisely the
    [plurals-hardcoded] class website/docs/i18n-catalog.md documents. It is
    visible in the whole-surface screenshot above. Fixing it needs a plural key
    across 12 catalogs and would ratchet the ceiling; it is unrelated to the tally
    bug and belongs in its own PR.
  • The issue's open API question is untouched, deliberately. Whether a parked
    run should leave the backend's /api/spawn count is a semantics change with
    external consumers (kirocrew spawn list). Nothing here changes that field --
    the split is presentational and dashboard-local.

Pattern harvest

Rule candidate: a status predicate with more than one consumer must not be private
to its module.

This defect class is "N surfaces answer the same question, and the ones that
re-derive it get it wrong". The predicate existed, was correct, and was three lines
above two counters that duplicated its subject badly -- isAwaitingSpawnApproval
was const, so both renderers reached for status === 'pending' instead. #7318's
own comment counts five separate serializers of per-run subagent state for the
same reason.

Mechanically catchable, cheapest first:

  1. An eslint no-restricted-syntax rule banning a bare status === 'pending'
    comparison on a SubagentActivity outside store/chatSlice.ts, the way the
    i18n gate bans toLocaleDateString() with no locale. Naming the exported
    predicate is the opt-out, so there is no allowlist to maintain.
  2. A store-level test asserting that every SubagentActivity status predicate is
    exported, i.e. that no const is<X> = (a: SubagentActivity) exists unexported
    in chatSlice.ts. That is a convention detector rather than a per-site pin, so
    it also covers the next predicate someone adds.

Not a one-off: countActiveSubagents, selectSlotSubagentsActive, tally() and
the chip's activeList are four independent spellings of "is this run active",
and this PR only reconciled the two that render a count.

Closes #7318

@chenmingwei23
chenmingwei23 requested a review from a team September 1, 2026 04:10
@chenmingwei23
chenmingwei23 requested a review from a team as a code owner September 1, 2026 04:10
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of d4353355d70a4bb81929eb3816ece8d02aa3fb70 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

A real UI lie fixed at its actual root — one exported predicate replacing per-renderer re-derivation — with the backend dependency correctly deferred to the open PR that owns it.

Watch

[DESIGN-REVIEWED] d435335

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of d4353355d70a4bb81929eb3816ece8d02aa3fb70 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified: the predicate now has 5 real consumers across the two renderers plus the slice's own selectors; sseSubagentPending (chatSlice.ts:3544, fed only from useWebSocket.ts:1163) is indeed the sole writer of 'pending' and always carries approval_id; the capture script and committed screenshots match a 364-script / 512-file repo convention; and the remaining 'pending'-folding sites (SessionActionsMenu reload gate, ChatPage tip suppression, the Sessions-rail activity counters) are "in flight" signals where a parked run legitimately counts — not "running" labels, so no unfixed siblings.

First-Principles-Verdict: PASS

Two tally surfaces asserted "running" for a run blocked on the user; this exports the one existing predicate and repairs both, deferring the backend hop that can't build yet.

What this change ships

Intent: stop the dashboard telling the user work is in progress when a sub-agent wave is actually waiting on their approval — a FIX.

  1. Wave chip no longer counts a parked run behind the spinner — justified (the reported defect)
  2. Launch card no longer says "N agents running" for parked members — justified (same defect, persistent surface)
  3. Chip header gains a hand-glyph "awaiting" count — justified (excluded runs must land somewhere honest)
  4. Parked row names the approval instead of a blank label with ticking timer — justified
  5. Card gains awaiting chip; hand glyph ranked above failed — declared, justified (only actionable state)
  6. Chip stays mounted when the whole wave is parked — justified consequence of exclusion
  7. Approval label outranks stall/retry verdict on the same row — declared, justified (strictly more specific)
  8. isAwaitingSpawnApproval exported from chatSlice — 5 counted consumers (2 renderers, 2 in-slice)
  9. One i18n key × 2 namespaces × 12 locales — mandated by the i18n gate
  10. Capture harness + 5 screenshots in temp-screenshots/ — matches convention (364 sibling capture-*.mjs, 512 committed files)

The fix sits at cause level for what the frontend can reach (the predicate was store-private, so renderers re-derived it wrong), and the deferred backend field is a deletion this lane would have demanded anyway: main has no writer for _awaiting_approval on the spawn path, so shipping the field now would be always-false surface.

[FIRST-PRINCIPLES-REVIEWED] d435335

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of d4353355d70a4bb81929eb3816ece8d02aa3fb70 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Parked runs now tell the truth — the awaiting state is named, colored distinctly, and the Approve/Reject banner sits right below it.

Suggestions

  • SubagentRunCard awaiting chip (<Hand size={10}/> {counts.awaiting}) is icon+count only, while its sibling queued chip carries visible text ("{n} waiting"); on the persistent scrollback card a hand glyph alone can read as blocked/error — add a short visible word ("{n} awaiting") like the queued chip does, since the title tooltip never reaches touch users.
  • The new key "Waiting for your approval to start" is a fourth phrasing of one state (side panel "Awaiting approval", session list "needs approval", banner "awaiting your approval to run") — align it to the "Awaiting your approval" family so the user learns one term.

[UX-REVIEWED] d435335

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed d4353355d70a4bb81929eb3816ece8d02aa3fb70 — this comment is updated in place on each push.

Review details

I've traced the candidate to its root. The claimed defect requires a run to simultaneously hold status:'pending', a set approval_id, and stalled:true. The only client writer of stalled is sseSubagentStalled/sseSubagentBatchUpdate/sseSubagentSnapshot, all fed by the backend reaper. In subagent_manager/monitoring.py:621 the stall path returns early unless info.turns > 0 or info._pid is not None, and a spawn parked on approval (_spawn_with_approval at admission.py:713, before _run) has neither — no turns executed and no PID. So the backend never emits subagent_stalled for a parked-on-spawn-approval run; the pending+stalled co-occurrence exists only in the synthetic unit test's direct dispatch, not in practice. The snapshot path forces status to running/tool, so it never satisfies isAwaitingSpawnApproval either. Candidate 1 fails condition (a): no concrete input that occurs in practice. Dropped.

No new groundable findings emerged from tracing the changed lines.

No findings.

[OPUS-REVIEWED] d435335

Verdict parsed from the review's SHA-scoped output markers for commit d4353355d70a4bb81929eb3816ece8d02aa3fb70.

False positive or not applicable? A repository writer can comment:
/ai-review override fable d4353355d70a4bb81929eb3816ece8d02aa3fb70: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of d4353355d70a4bb81929eb3816ece8d02aa3fb70 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] d435335

False positive or not applicable? A repository writer can comment:
/ai-review override gpt d4353355d70a4bb81929eb3816ece8d02aa3fb70: <one-sentence reason>

@chenmingwei23
chenmingwei23 force-pushed the fix/parked-subagent-render branch from d9fb50a to 0353413 Compare September 1, 2026 04:35
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 1, 2026
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Round 1 triage. Three lanes were red on the first head; two were mine and are
fixed, one is not mine.

Fixed -- PR Hygiene. The gate matches ^ *(Rule candidate|Not generalizable) *: and my Pattern harvest line began with **, so the bold
marker put the phrase off the line start. Unbolded, marker now first on its line.

Fixed -- Screenshot Evidence. This was a legitimate red and I have not
waived it. Rather than take the no-screenshots label for a change that does
have a visual delta, the PR now carries before/after frames of both surfaces,
captured from the real built SPA by a committed harness
(website/scripts/capture-parked-subagent-chip.mjs) that pushes the same WS
frames the gateway sends. The harness asserts the rendered strings before it
photographs -- so a stale bundle reds instead of quietly capturing the old copy --
and --expect-before inverts the verdict, so the BEFORE frame has to actually
reproduce the defect rather than merely fail to show the fix:

AFTER  running '1', awaiting '1', card '1 agent running' + awaiting chip  -> pass
BEFORE running '2', awaiting absent, card '2 agents running'              -> defect reproduced

Getting there needed a workaround worth flagging for anyone else on this host:
main added @radix-ui/react-tabs, this box's registry rejects auth (E401, not
cached), and vite build will not link with that import unresolved. I built
against a local uncommitted shim for that one package. Nothing in the diff depends
on it, and CI's own tsc -b step passed.

Not mine -- Frontend Lint & Type Check. The lane runs npx eslint src/ --max-warnings 659 and main measures 660 warnings, 0 errors, so it reds on any PR
whose changed surface reaches it. #7480 is the open fix for that exact drift.
Attribution for this PR: CI's own warning list names SubagentProgressBar.tsx
once, for the no-console warning already on base (line 157 before my diff, 165
after), and none of my other five files appear at all. I have not folded #7480's
one-line fix in here -- that would duplicate an open PR. Happy to rebase onto it
once it merges; say the word if you would rather I carry the fix instead.

All four AI review lanes that had reported (Design Review, First Principles, Opus
4.8, UX Review) were green on the previous head; they are re-running on this one.

@chenmingwei23
chenmingwei23 force-pushed the fix/parked-subagent-render branch from 0353413 to 0723d35 Compare September 1, 2026 04:46
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Round 2. PR Hygiene and Screenshot Evidence are green. One new red was real
and mine; the other is still main's.

Fixed -- Frontend Tests (3). Not a flake: src/i18n/style/hiStyle.test.ts
("hi tone", style/hi.md section 4) is a ceiling that fails on growth, baseline 118,
and my two Hindi values took it to 120. The rule is that Hindi copy addresses the
user in the informal register rather than the formal honorific pronoun, and both of
my values used the formal one. Rewritten to the informal possessive; the shard's
other 431 files and 6818 tests were already passing.

The gap that let it through is worth naming, since it is the reusable lesson here:
before pushing I ran the catalog gates (parity, duplicate keys, changed-value QA,
dead keys, English identity, key refs, pseudolocale, glossary, ratchets) and the
npm run i18n:check chain -- all green -- but NOT the eleven per-language
src/i18n/style/*.test.ts suites, which is where per-locale register and
punctuation rules live. Those are pure-data tests and cheap. I have now run all
eleven: 83 tests, green, so no sibling locale has the same problem (the de value
already used the informal address its own guide requires). Anyone adding a catalog
key should run src/i18n/style/ alongside i18n:check -- the gate chain does not
cover it.

Still not mine -- Frontend Lint & Type Check. Unchanged from my earlier
comment: the lane's ceiling is 659 and main measures 660, with #7480 open to fix
exactly that. My six files contribute only the pre-existing no-console warning.

Screenshots in the description are re-pinned to the new head so the evidence tracks
the current commit rather than a superseded one.

@chenmingwei23
chenmingwei23 force-pushed the fix/parked-subagent-render branch from 0723d35 to 18f320d Compare September 1, 2026 05:16
A sub-agent parked on an unanswered spawn-approval prompt was reported by
the dashboard as running. Two surfaces folded status 'pending' into their
running tally: the wave chip above the composer (SubagentProgressBar) put
the parked run behind a spinning loader and rendered its row as a bare task
label with a ticking elapsed timer, and the inline launch card in the
transcript (SubagentRunCard) printed "1 agent running". The run had in fact
launched no process at all -- it was registered, counted, and blocked on a
prompt the user had not answered.

The state was already in the store. sseSubagentPending writes status
'pending' plus an approval_id from the WS approval frame, and the Subagents
side panel already renders it as "Awaiting approval" with Approve/Reject.
The defect was that the two tally surfaces never asked the question, and the
predicate that answers it (status 'pending' AND an approval_id) was a private
const in chatSlice with three call sites, none of them a renderer.

- Export isAwaitingSpawnApproval from chatSlice so the renderers share the
  one definition instead of re-deriving it from status alone.
- SubagentProgressBar: parked runs leave the running count and get their own
  count with a distinct glyph; the per-agent row names the approval. The
  mount predicate gains the awaiting term, because a wave whose only member
  is parked now has running === 0 and would otherwise unmount the one
  surface naming what it is blocked on. The row's approval branch is checked
  before retrying/stalled: a run that never executed produces the watchdog's
  silence trivially, and the approval is the more specific explanation.
- SubagentRunCard: same split in tally(), plus an awaiting chip and a
  leading glyph ranked above failed -- a failure is history, an unanswered
  approval is still actionable.
- One new catalog key per namespace, translated across all 12 authored
  locales, en-XA regenerated.

Tests: 6 new assertions in SubagentProgressBar.parkedApproval.test.tsx and 4
added to SubagentRunCard.test.tsx, including negative controls that a
'pending' entry with no approval_id keeps its previous treatment. Both
suites were verified red against the base tree.

Visual evidence: website/scripts/capture-parked-subagent-chip.mjs drives the real
built SPA, pushes the same WS frames the gateway sends (subagent_spawn plus an
`approval` whose id is `spawn:<agent_id>`), and ASSERTS the rendered strings
before it photographs, so a stale bundle reds instead of quietly capturing the
old copy. Before/after frames under temp-screenshots/parked-subagent-approval/;
the before run reproduces the defect (2 running claimed, no awaiting count).

The hi translation uses the informal second-person possessive, not the formal
honorific pronoun: style/hi.md section 4 requires the informal register, and
src/i18n/style/hiStyle.test.ts is a ceiling that fails on growth, so two formal
values would have raised it from 118 to 120.

Closes #7318
@chenmingwei23
chenmingwei23 force-pushed the fix/parked-subagent-render branch from 18f320d to d435335 Compare September 1, 2026 05:46
@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 1, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) September 1, 2026 06:44

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CI green, no blocking bot findings, diff matches description. Approved.

@bolichen97
bolichen97 merged commit afdeed8 into main Sep 1, 2026
72 of 78 checks passed
@bolichen97
bolichen97 deleted the fix/parked-subagent-render branch September 1, 2026 06:48
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 1, 2026
chenmingwei23 added a commit that referenced this pull request Sep 2, 2026
A default install has no YOLO override, no auto_approve_subagent_spawn and no
session trust, so every spawn_run is gated behind the interactive spawn
approval. While that prompt is unanswered the run is registered in _agents and
counted by the manager's running count, so every reader that goes through
/api/spawn reports it exactly like an agent that is executing: no child ACP
process, subagents_spawned still 0, and nothing in the payload, the CLI spawn
list, the MCP roster or the log naming the gate. An unowned spawn (the CLI
posts no parent_session) raises its prompt with slot="", so it is surfaced
only on the global approvals feed and appears in no chat tab either.

Two adjacent halves of #6484 have already landed and are not redone here.
#7325 stopped the reap of such a run blaming a deadline it never reached, and
in doing so put info._awaiting_approval on the spawn gate. #7477 stopped a
chat tab rendering an owned parked run as executing, deriving its cue from the
WS approval event (status 'pending' + approval_id), not from this payload --
so it is scoped to a slot, and the unowned spawn still reaches no tab.

What is left is the wait's NAME, on every path that reads a run:

* _spawn_with_approval logs at INFO under the run id, with the parent (or
  "<unowned>"). #7325 marked the wait in machine state for the reaper; a mark
  is not a message, and nothing was written at all -- which is exactly how
  #6484 was reported, the reporter's only lead being that no log record
  mentioned the affected run id.
* BOTH /api/spawn read paths carry awaiting_approval while parked, through ONE
  shared predicate _awaiting_spawn_approval(), present only then so the default
  payload is unchanged. The list endpoint feeds `kirocrew spawn list`; the
  single-run status endpoint is what a BLOCKING `kirocrew spawn run` polls
  every 2s, so reporting it on the list alone would have left the CLI
  reproduction exactly as silent as before.
* That predicate requires _exec_started is None as well as the flag, because
  the flag is SHARED: run.py sets it at three in-run tool-approval sites, so a
  bare read would render a run at turn 5 waiting on a tool prompt as "waiting
  for spawn approval" and tell a still-polling caller to approve it "to start
  this run" that already started. _exec_started is stamped once when execution
  begins (_run_inner_impl), so None means the run never entered execution.
  terminal.py picks the reap message off the same pair, arrived at
  independently; the predicate is not extracted onto SubagentInfo because this
  read must survive the info doubles the handlers are tested with, and
  unifying would mean editing a reap path this change does not touch.
  One predicate rather than two inlined conditions: the handlers build their
  payloads independently, and a drift between them is invisible to a
  behavioural test, so a source ratchet pins both call sites.
* MCP `spawn_list` reports [awaiting-approval] rather than [running] -- the
  surface an LLM reads, and the one spawn.py itself points a failing caller at
  ("Check spawn_list").
* `kirocrew spawn list` renders the wait instead of the bare hourglass it
  shared with a running agent, and the blocking poll announces it once rather
  than on every poll.

* Prunes src/kiro_crew/mcp_tools/spawn.py from .github/black-baseline.txt: the
  file was listed as known-unformatted and this change makes it black-clean,
  and that baseline is shrink-only, so the gate requires the graduated entry
  be removed.

Fixes #6484
bolichen97 pushed a commit that referenced this pull request Sep 2, 2026
…al (#7299)

A default install has no YOLO override, no auto_approve_subagent_spawn and no
session trust, so every spawn_run is gated behind the interactive spawn
approval. While that prompt is unanswered the run is registered in _agents and
counted by the manager's running count, so every reader that goes through
/api/spawn reports it exactly like an agent that is executing: no child ACP
process, subagents_spawned still 0, and nothing in the payload, the CLI spawn
list, the MCP roster or the log naming the gate. An unowned spawn (the CLI
posts no parent_session) raises its prompt with slot="", so it is surfaced
only on the global approvals feed and appears in no chat tab either.

Two adjacent halves of #6484 have already landed and are not redone here.
#7325 stopped the reap of such a run blaming a deadline it never reached, and
in doing so put info._awaiting_approval on the spawn gate. #7477 stopped a
chat tab rendering an owned parked run as executing, deriving its cue from the
WS approval event (status 'pending' + approval_id), not from this payload --
so it is scoped to a slot, and the unowned spawn still reaches no tab.

What is left is the wait's NAME, on every path that reads a run:

* _spawn_with_approval logs at INFO under the run id, with the parent (or
  "<unowned>"). #7325 marked the wait in machine state for the reaper; a mark
  is not a message, and nothing was written at all -- which is exactly how
  #6484 was reported, the reporter's only lead being that no log record
  mentioned the affected run id.
* BOTH /api/spawn read paths carry awaiting_approval while parked, through ONE
  shared predicate _awaiting_spawn_approval(), present only then so the default
  payload is unchanged. The list endpoint feeds `kirocrew spawn list`; the
  single-run status endpoint is what a BLOCKING `kirocrew spawn run` polls
  every 2s, so reporting it on the list alone would have left the CLI
  reproduction exactly as silent as before.
* That predicate requires _exec_started is None as well as the flag, because
  the flag is SHARED: run.py sets it at three in-run tool-approval sites, so a
  bare read would render a run at turn 5 waiting on a tool prompt as "waiting
  for spawn approval" and tell a still-polling caller to approve it "to start
  this run" that already started. _exec_started is stamped once when execution
  begins (_run_inner_impl), so None means the run never entered execution.
  terminal.py picks the reap message off the same pair, arrived at
  independently; the predicate is not extracted onto SubagentInfo because this
  read must survive the info doubles the handlers are tested with, and
  unifying would mean editing a reap path this change does not touch.
  One predicate rather than two inlined conditions: the handlers build their
  payloads independently, and a drift between them is invisible to a
  behavioural test, so a source ratchet pins both call sites.
* MCP `spawn_list` reports [awaiting-approval] rather than [running] -- the
  surface an LLM reads, and the one spawn.py itself points a failing caller at
  ("Check spawn_list").
* `kirocrew spawn list` renders the wait instead of the bare hourglass it
  shared with a running agent, and the blocking poll announces it once rather
  than on every poll.

* Prunes src/kiro_crew/mcp_tools/spawn.py from .github/black-baseline.txt: the
  file was listed as known-unformatted and this change makes it black-clean,
  and that baseline is shrink-only, so the gate requires the graduated entry
  be removed.

Fixes #6484

Co-authored-by: gh-autofix#2887 <chenmingwei23@users.noreply.github.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.

dashboard: a subagent parked on an unanswered spawn approval still renders as running (awaiting_approval has no frontend consumer)

2 participants