fix(subagent): stop the injection notice contradicting its own reason - #6002
fix(subagent): stop the injection notice contradicting its own reason#6002leonlaiyc wants to merge 1 commit into
Conversation
|
🤖 Kiro Crew [operator: bolichen97#66809557]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author. Assessment: The only real red is the black gate — If you'd prefer I don't touch this PR, add the |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
|
@leonlaiyc Thanks for this. It is still valid: on current Two notes before review. Overlap with #8003 (@welikoiwanenko, subagent credit usage). It edits the same two files and the same Rebase needed. The branch is 1837 commits behind main and the notice builder moved to Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
`_injection_notice_outcome`'s completed branch asserts "result delivery timed
out", but only two of the six `notify_injection_failed` call sites pass a
timeout. `slack/gateway.py` sends "provider dead after prompt-busy retries",
"ACP process died", a raw `str(exception)` from a failed injection turn, and
the last injection-failure reason after the attempt cap — and the notice prints
that reason on the line directly above the outcome line, so the message
contradicts itself:
Agent `a1` ❌ ACP process died
Task: ...
The agent finished but result delivery timed out.
The reader is an LLM deciding whether to retry, so the wrong mechanism is not
cosmetic.
State only the outcome and leave the cause to `reason`, which already carries
it. A real timeout loses nothing: its callers say so in `reason`, and the old
copy said "timed out" twice.
Residual named by the reviewer on merged kirodotdev#5900. Spec updated in the same commit
per AGENTS.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3614f8a to
f756fb6
Compare
|
Rebased onto main Clean rebase, no conflicts. The only edit on top of your commit is Gates run locally on the changed files only: Please review the resolution. A maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply if anything looks wrong. |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All claims verified against the base tree: the six call sites and their verbatim reasons check out (gateway.py:7467, 7488, 8261, 8533, 8588 plus terminal.py:239), the completed branch at First-Principles-Verdict: PASS One stale sibling: Grepped What this change shipsIntent: stop the injection-failure notice telling the LLM "timed out" when its own Inventory (5 items)
The rewritten pins carry their evidence in-tree: the base code itself shows four callers passing non-timeout reasons into a line asserting a timeout, so the old pins recorded the contradiction, not a decision. [FIRST-PRINCIPLES-REVIEWED] f756fb6 |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of The frontend parser reads outcome from the glyph, not the English payload text, so the old string there is inert fixture data. Everything checks out: four of the six Design-Verdict: PASS A one-line copy fix that removes a false mechanism claim contradicted by four of six call sites; cause stays in [DESIGN-REVIEWED] f756fb6 |
Problem / Motivation
When a sub-agent reaches a terminal state but its report cannot be injected into
the parent session,
notify_injection_failedbuilds a notice whose second lineis the caller's
reasonand whose fourth is_injection_notice_outcome(info).For a run that actually completed, that outcome line is:
Only two of the six call sites pass a timeout. The other four pass something
else entirely —
slack/gateway.py:reason"provider dead after prompt-busy retries""ACP process died"str(t.exception())from the failed injection turn_last_failure_reasonafter the attempt capSo the message contradicts itself:
This is the residual the reviewer named on merged #5900, which introduced the
outcome-aware line:
(There are four, counting
str(exception)at 7066.)Why it matters
failure_msgis not only rendered in the dashboard slot — it is queued intoslot._pending_subagent_failuresand drained into the LLM's context on thenext
_run_chatturn. So the reader deciding what to do next is a model, andthe two mechanisms call for different responses: a delivery timeout invites a
retry of the injection, while "the ACP process died" or "provider dead after
prompt-busy retries" means the transport is gone and the sensible move is to
read the result off disk. Handing the model a confident, specific, wrong
mechanism is worse than handing it none.
docs/system-specs/common/injected-messages.mddocuments the string, so thewrong copy is also the specified copy.
What changed (motivation → approach → change)
Root cause is that one branch of the helper describes the mechanism of the
delivery failure while the notice already prints that mechanism, from a value
the helper cannot see. The two can only agree by accident.
The completed branch now states the outcome and nothing else:
reason(
"injection timed out after 300s","delivery timed out"), so the specificcause still reaches the reader — and the old copy said "timed out" twice.
stoppedandfailedbranches are untouched. They describe the run'sown terminal state, which the record does know, and they are not in tension
with
reason.mechanism back: the cause belongs to
reason, this line states the outcome.docs/system-specs/common/injected-messages.mdis updated in the samecommit, per
AGENTS.md, including a note on why the line names nomechanism.
Tests
New
TestInjectionNoticeDoesNotContradictItsReasonintest/test_subagent_coverage.py, driving the realnotify_injection_failedand reading the queuedfailure_msg— the sameend-to-end shape the neighbouring
TestNotifyInjectionFailedOutcomeCopyuses:test_a_non_timeout_reason_is_not_overridden_by_the_outcome_line, parametrizedover the four reasons the non-timeout call sites actually pass. Asserts the
caller's reason is still shown, that
"timed out"does not appear anywhere inthe notice, and that the new line is present.
test_a_real_timeout_still_reads_as_one— a control: withreason="injection timed out after 300s"the reader still learns it was atimeout, so the neutral copy costs nothing.
test_the_result_recovery_hint_still_agrees_with_the_line— a controlagainst the obvious way this could go wrong: "could not be delivered" must not
read as "there is nothing to read", so with a
result_pathpresent the noticestill points at the file and still says
Use the read tool.Two existing tests asserted the old wording and are updated, not deleted:
test_completed_keeps_the_finished_copybecomestest_completed_states_the_outcome_without_a_mechanism(and now also asserts"timed out" not in line), andtest_post_run_delivery_timeout_keeps_existing_copy_and_hintbecomestest_post_run_delivery_failure_keeps_the_completed_copy_and_hint.Red-before, measured against pristine
origin/mainproduction code(
10c422628) with the new tests in place — 8 failed / 262 passed. The fourparametrized failures state the defect in one line each:
Green-after: 270 passed in that module. Blast radius: 1303 passed / 26
skipped across the whole
-k "subagent or injected"selection. That run alsoreports 32 collection errors in
test_bench_download_fd.pyandtest_discord_doctor.py(KeyError: 'file'), pre-existing on this machine andunrelated — verified earlier today by stashing a change and re-running those two
files on pristine main for the same result.
flake8,isortandmypyare clean.On
scripts/docs-lint.sh: I do not have a pass to report from this host. Itswrapper execs
python3, which here resolves to the Windows Store shim (exit 49before the lint runs), and the real 3.10 install ships
python.exewith nopython3.exe, so putting it first onPATHdoes not shadow the shim. What thelint enforces is that every doc is indexed and every link resolves; this change
edits one line of prose inside an existing section of an existing doc and
creates, moves, renames and deletes nothing, so it cannot affect either
property. CI runs the real gate.
Manual verification
N/A — unit coverage sufficient: the defect is the text of a generated message,
and the tests read that message end to end out of the queued event rather than
asserting on the helper alone.
Related Issues
Residual named by the reviewer on merged #5900 (
fix: derive injection-timeout notice copy from the run's outcome (#5882)). Independent of the openrun-coordinator stack — #5281 adds
notify_injection_failedcall sites buttouches neither
_injection_notice_outcomenor this wording, so there is nooverlapping hunk. No separate issue was filed.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement