Skip to content

Add interrupt_agent / followup_task (CL-6997) - #619

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-6997-interrupt_agent-and-followup_task-the-second-half-of
Aug 24, 2026
Merged

Add interrupt_agent / followup_task (CL-6997)#619
TheGreatAxios merged 2 commits into
mainfrom
cl-6997-interrupt_agent-and-followup_task-the-second-half-of

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • interrupt_agent({ target }) stops a retained worker's current turn while keeping the session and its context reusable (distinct from the permanent close_agent).
  • followup_task({ target, message }) sends new work into a retained session's existing agent, reusing its prior context and tool outputs rather than starting fresh.
  • Both gated to orchestrator tiers via the existing FLEET_VERBS / assertTierMayMountFleetVerb mechanism, denied to leaves.

Safety note on interrupt_agent / CL-6984

The vendored Agent object exposes no interrupt()/abort() primitive separate from close() — the reactor's own internal abort() (vendor/intx-inference/src/reactor.ts) is not exposed on the public Agent interface; only close() reaches it, and close() is the codepath with the known close()-ordering workdir-lock risk (CL-6984, still open).

interrupt_agent therefore does not call close() at all. It fires a dedicated AbortController signal scoped only to the in-flight agent.send() call (the same signal option Agent.send already documents), combined via AbortSignal.any alongside the run's own controller only for that one send. This rejects the caller's wait on that turn without touching close(), disposeSubAgentSession, or the workdir lock — so it cannot wedge the lock the way a stuck close() can.

The tradeoff, stated plainly: this is an approximation, not a true hard-stop. Per the vendored send-queue's own documentation, aborting a signal mid-cycle rejects the caller's promise immediately but the reactor cycle keeps running in the background until it finishes naturally; there is no lower-level primitive in the vendored code to stop that compute without closing the session. followup_task's new agent.send() call queues behind that cycle via the vendored send-queue's own FIFO ordering — no second continuation mechanism was built.

Regression coverage for real agent reuse

lifecycle-tools.test.ts only proves interrupt/followup behave correctly against fake registered closures at the tool/store layer — it doesn't touch run.ts's real onAgentReady wiring where followup calls agent!.send() on the same live agent object. Added src/subagent/followup-live-agent.test.ts, using option 1 (stub live agent through the real run.ts flow) rather than the call-count fallback: it replaces createAgentWithLiveToolDispatch with a stub Agent via withMockedModuleDuring, drives the real runSubAgent end to end (real tool assembly, real dispatch brief, real onAgentReady/interrupt/followup closures), interrupts the in-flight send, then calls the captured followup handle directly, and asserts on the stub's own shared message log and a construction counter — proving one agent instance was built and reused, not rebuilt. Chose option 1 because it exercises the actual code path the regression would break, not just an indirect proxy for it; the construction-count check from option 2 is folded in as an extra assertion rather than the whole test.

Test plan

  • bun test src/subagent/lifecycle-tools.test.ts src/subagent/authority.test.ts src/subagent/followup-live-agent.test.ts
  • bun run check (lint, typecheck, build, full test suite) — 5340 pass, 0 fail

@linear-code

linear-code Bot commented Aug 24, 2026

Copy link
Copy Markdown

CL-6997

Second half of reusable worker sessions: interrupt_agent stops a
retained worker's current turn while keeping the session and its
context reusable, and followup_task sends new work into a retained
session's existing agent, reusing its prior context and tool outputs.

interrupt_agent fires a signal scoped only to the in-flight
agent.send() call, never close() — it cannot hit the close()-ordering
workdir-lock issue tracked separately (CL-6984). There is no lower-
level stop primitive in the vendored agent for the reactor cycle
itself, so this is an approximation: it stops the caller from waiting,
not the worker's compute, which keeps running in the background until
it finishes naturally.

Both verbs are gated to orchestrator tiers via the existing
FLEET_VERBS / assertTierMayMountFleetVerb mechanism, denied to leaves.
lifecycle-tools.test.ts only exercised interrupt_agent/followup_task
against fake registered closures at the tool/store layer, not run.ts's
real onAgentReady wiring where followup calls agent!.send() on the
same live agent object. Add a test that drives runSubAgent end to end
with createAgentWithLiveToolDispatch replaced by a stub Agent, proving
the followup send after an interrupt lands on the same agent instance
(one construction, one shared message log) rather than a rebuilt one.
@TheGreatAxios
TheGreatAxios force-pushed the cl-6997-interrupt_agent-and-followup_task-the-second-half-of branch from 6dc9513 to efefe8e Compare August 24, 2026 15:05
@TheGreatAxios
TheGreatAxios enabled auto-merge (squash) August 24, 2026 15:05
@TheGreatAxios
TheGreatAxios merged commit 1438da2 into main Aug 24, 2026
5 checks passed
TheGreatAxios added a commit that referenced this pull request Aug 27, 2026
* Add interrupt_agent / followup_task (CL-6997)

Second half of reusable worker sessions: interrupt_agent stops a
retained worker's current turn while keeping the session and its
context reusable, and followup_task sends new work into a retained
session's existing agent, reusing its prior context and tool outputs.

interrupt_agent fires a signal scoped only to the in-flight
agent.send() call, never close() — it cannot hit the close()-ordering
workdir-lock issue tracked separately (CL-6984). There is no lower-
level stop primitive in the vendored agent for the reactor cycle
itself, so this is an approximation: it stops the caller from waiting,
not the worker's compute, which keeps running in the background until
it finishes naturally.

Both verbs are gated to orchestrator tiers via the existing
FLEET_VERBS / assertTierMayMountFleetVerb mechanism, denied to leaves.

* Add live-agent regression guard for followup_task (CL-6997)

lifecycle-tools.test.ts only exercised interrupt_agent/followup_task
against fake registered closures at the tool/store layer, not run.ts's
real onAgentReady wiring where followup calls agent!.send() on the
same live agent object. Add a test that drives runSubAgent end to end
with createAgentWithLiveToolDispatch replaced by a stub Agent, proving
the followup send after an interrupt lands on the same agent instance
(one construction, one shared message log) rather than a rebuilt one.
TheGreatAxios added a commit that referenced this pull request Aug 27, 2026
* Add interrupt_agent / followup_task (CL-6997)

Second half of reusable worker sessions: interrupt_agent stops a
retained worker's current turn while keeping the session and its
context reusable, and followup_task sends new work into a retained
session's existing agent, reusing its prior context and tool outputs.

interrupt_agent fires a signal scoped only to the in-flight
agent.send() call, never close() — it cannot hit the close()-ordering
workdir-lock issue tracked separately (CL-6984). There is no lower-
level stop primitive in the vendored agent for the reactor cycle
itself, so this is an approximation: it stops the caller from waiting,
not the worker's compute, which keeps running in the background until
it finishes naturally.

Both verbs are gated to orchestrator tiers via the existing
FLEET_VERBS / assertTierMayMountFleetVerb mechanism, denied to leaves.

* Add live-agent regression guard for followup_task (CL-6997)

lifecycle-tools.test.ts only exercised interrupt_agent/followup_task
against fake registered closures at the tool/store layer, not run.ts's
real onAgentReady wiring where followup calls agent!.send() on the
same live agent object. Add a test that drives runSubAgent end to end
with createAgentWithLiveToolDispatch replaced by a stub Agent, proving
the followup send after an interrupt lands on the same agent instance
(one construction, one shared message log) rather than a rebuilt one.
@TheGreatAxios
TheGreatAxios deleted the cl-6997-interrupt_agent-and-followup_task-the-second-half-of branch August 28, 2026 00:09
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