Skip to content

fix(execution): grouped sub-agent edited args validated, abort ends wedged tool waits, re-pause keeps approvalIds (Eve2-F4/F5/F6) - #618

Merged
LinuxDevil merged 3 commits into
mainfrom
fix/eve2-f456-execution
Oct 12, 2026
Merged

LinuxDevil merged 3 commits into
mainfrom
fix/eve2-f456-execution

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Fixes three findings from the second adversarial audit (audit/eve2), all in the approval/abort resume path. All repros verified against this branch.

Eve2-F4 — grouped sub-agent edited args skip schema validation (High)

groupDecision() stored decision.args raw for snapshot.subagent records (the lead's registry holds none of the sub-agent's tools), so an invalid edit like { a: 'not-a-number' } reached execute() unchecked. decideGroupMember() now runs validateGroupArgs() once every call of the step is decided — inside the sub-agent's own resume, against the registry that owns the tools — and replaces each decision's args with the parsed output, like withEditedArgs() does for a single call. An invalid edit throws LOUSHO_TOOL_ARGS_INVALID and the claimed record goes back, so the approval stays pending.

  • Repro: npx tsx audit/eve2/repro/subagentGroupEditedArgs.ts → "OK: edited args were validated/rejected"
  • Regression tests: src/parallelApprovals.test.ts (invalid args rejected, valid args run)

Eve2-F5 — abort during tool execution never settles the run (High)

Two tool waits were not raced against the run's signal the way the model call already is (Eve CORE-F15): Promise.all(this.running) in ToolBatch and executeApprovedTool() in resume.ts. A signal-ignoring/wedged tool left send(), stream() and approvals.resolve() pending forever.

  • ToolBatch.settled() races the running calls against the run signal; when the abort side wins after a one-turn drain (so a settlement already landing still records — a tool released just as the run aborted keeps its result), still-running calls are reported unrecorded and get the same 'cancelled' result as calls never reached. Late settlements record nothing on top.
  • executeApprovedTool() waits on abortableToolCall(), the same drain-aware race, so a tool that aborts the signal inside its own execute() still returns its real result.
  • The underlying tool keeps running (documented for ExecuteOptions.signal); the run ends finishReason: 'aborted' with abortReason, through the existing abortRun() path.
  • Repros: abortDuringToolExecution.ts → resolved finish=aborted in ~173ms; abortDuringResumedTool.ts → resolved finish=aborted in ~167ms.
  • Regression tests: src/execution/abortEdges.test.ts (wedged tool in a batch; wedged approved tool on resume)
  • docs/runs.md updated: a signal-ignoring tool is no longer "waited for".

Eve2-F6 — re-paused run's checkpoint drops approvalIds (Medium-High)

markAwaitingApproval() wrote only approvalId, so a run re-pausing on several of a step's calls (e.g. a resumed sub-agent pausing again) showed durable readers 1 of N pending approvals. It now carries approvalIds like markGroupPending() does.

  • Repro: rePausedSubagentCheckpointIds.ts → "OK: checkpoint names every pending approval"
  • Regression test: src/execution/approvalResumeDurability.test.ts

Verification

  • npx vitest run src/execution: 73 files / 1064 tests pass (incl. parallelApprovals 14, abortEdges 9, approvalResumeDurability 4)
  • npx vitest run on resume/approval consumers outside src/execution (cloudflare checkpoint, kvCheckpointStore, publicSurface, SqliteStore): 178 tests pass
  • npx tsc --noEmit -p tsconfig.json: clean
  • npm run lint: clean
  • CHANGELOG: one line per fix tagged (Eve2-F4/F5/F6)

LinuxDevil and others added 3 commits October 12, 2026 18:03
…edged tool waits, re-pause keeps approvalIds (Eve2-F4/F5/F6)

- Eve2-F4: approve-with-edits on a sub-agent's grouped approval stored
  decision.args raw, so schema-invalid args ran the tool; the sub-agent's
  resume now validates every decided group's edited args before the step
  runs (LOUSHO_TOOL_ARGS_INVALID leaves the approval pending).
- Eve2-F5: a tool that ignores its abortSignal held send() / stream() /
  approvals.resolve() open forever. The tool batch's settle wait and the
  resume's approved-tool wait are raced against the run's signal like the
  model call already was (Eve CORE-F15): the run ends 'aborted' with a
  'cancelled' result for calls still running, a settlement already landing
  as the signal fires is still kept, and the tool keeps running.
- Eve2-F6: markAwaitingApproval() wrote only approvalId on a re-pause, so
  durable discovery saw 1 of N pending approvals; it now carries the
  group's approvalIds like markGroupPending() does.
…ve2-F5)

The settle-on-abort race yielded a single event-loop turn, which dropped
the result of any tool still running when the signal fired - a 20ms tool
aborted mid-flight lost its B4 'waited' transcript entry. The abort side
now drains for TOOL_ABORT_GRACE_MS (500ms), so a tool released while the
run unwinds keeps its real result (B4 contract restored) while a truly
wedged tool still abandons inside ~500ms, well under any caller-visible
latency (F5 contract kept).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@LinuxDevil
LinuxDevil merged commit edcf1ac into main Oct 12, 2026
7 checks passed
LinuxDevil pushed a commit that referenced this pull request Oct 12, 2026
…sumed tool calls

The resumed-tool race added in #618 duplicated generateStep's abortable();
both now share it, with an optional grace period for the tool path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n
LinuxDevil pushed a commit that referenced this pull request Oct 12, 2026
…sumed tool calls

The resumed-tool race added in #618 duplicated generateStep's abortable();
both now share it, with an optional grace period for the tool path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n
LinuxDevil added a commit that referenced this pull request Oct 12, 2026
…opy errors, usable templates, eval/dev accuracy, slimmer image (#613)

* fix(cli): kits load under plain node, dev reloads local helpers, import hints

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* fix(providers): pass OPENAI_BASE_URL / ANTHROPIC_BASE_URL on ai 4 provider packages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* fix(execution): PropagatingToolError, McpToolError, WorkspaceError instanceof across SDK copies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* feat(cli): build agent modules, usable minimal/tools scaffolds, leaner docker image

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* fix(cli): eval timeouts and empty runs, command help, small message and exit-code fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* chore(package): stop shipping the CJS CLI build; changelog and upgrading notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* refactor(cli): split failLateCases so it stays under the cognitive-complexity limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* refactor(execution): one abortable() helper for provider calls and resumed tool calls

The resumed-tool race added in #618 duplicated generateStep's abortable();
both now share it, with an optional grace period for the tool path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* test(mcp): let the run start before the client timeout fires

A 200 ms timeout could fire before a cold runner started the run, so the
cancel matched nothing and the run was never aborted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

* ci: retrigger checks

* test(mcp): give serveMcp's run-start and cancel waits room on busy runners

vi.waitFor's 1 s default failed on a CI runner whose suite took 300 s,
before the server had started the run. The late-cancel test's runs now
last long enough for the cancel to land first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BG6n8mwoFq1jmay9zCPS6n

---------

Co-authored-by: Claude <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