Skip to content

feat: cancel runs that time out, and send the prompt on stdin - #16

Closed
matt-wright-dev wants to merge 1 commit into
mainfrom
feat/cancel-runs-and-stdin-prompts
Closed

feat: cancel runs that time out, and send the prompt on stdin#16
matt-wright-dev wants to merge 1 commit into
mainfrom
feat/cancel-runs-and-stdin-prompts

Conversation

@matt-wright-dev

Copy link
Copy Markdown
Collaborator

Closes #11.

What changes

Runs can be cancelled, and cancelled runs leave nothing behind.

  • Every docker run gets a unique --name.
  • runDocker accepts an AbortSignal; on abort it force-removes the container, kills the client, and rejects with the reason.
  • agent({ timeoutMs }) sets a deadline; executing_the_agent cancels the run when it passes and throws agent run exceeded <n> ms.
  • Under Vitest, executing_the_agent also cancels when the test finishes first (its own timeout, or a failure elsewhere), so the agent stops working and billing. Vitest is loaded dynamically since it is an optional peer; other runners skip this part.
  • Exit, SIGTERM and SIGINT handlers force-remove any container still tracked (then re-raise the signal). Removal on abort is synchronous on purpose: the first version removed asynchronously and was cut short by worker teardown when the timed-out test was the last one in its worker, so the container ran on. The signal handlers assume Vitest's default forks pool.

The prompt goes to the container on stdin, not argv.

  • Linux caps a single argument at 128 KB; a pasted file or transcript in the prompt used to fail with argument list too long. stdin has no limit, and the prompt no longer appears in ps on the host.
  • Both CLIs read the prompt from stdin in print mode (checked with real runs).
  • buildClaudeDockerArgs / buildDockerArgs now take an optional containerName instead of prompt, since they only build argv. Both are exported but undocumented; flagging in case anyone uses them directly.

Checks

  • pnpm run lint && pnpm run test && pnpm run build: clean; 87 tests across 19 files.
  • New unit specs: _runProcess (stdin, abort kills and rejects, already-aborted), both bindings (name, -i, no prompt on argv, runner receives stdin/name/signal), executing_the_agent (timeoutMs cancels and reports), agent (timeoutMs on the context).
  • e2e/cancel.spec.ts against real Docker: an aborted run rejects and docker ps -a no longer lists it; a 300 KB stdin reaches the container intact. The existing agent e2e passes for both agents on the stdin path.
  • Consumer check, both agents, from a separate project running the packed tarball: a long task with timeoutMs: 8000 is cancelled with the expected message and no agent-gwt-* container remains; the leak itself (only Vitest's test timeout, no timeoutMs) leaves no container behind either, checked immediately and 5 s after the run; prompts of 150 to 240 KB are delivered in full and answered correctly (Cursor in 32 s; Claude sonnet took 8 minutes and about $1.5 on 150 KB, which is the model's doing, not the transport).

Notes

  • Minor bump: new timeoutMs option and a new signal field on RunAgentOptions.
  • The cancel-on-test-finish hook relies on Vitest's onTestFinished; it is wrapped so a missing or non-Vitest runtime is silently ignored.

Closes #11.

Cancellation
- Every `docker run` gets a unique `--name` (agent-gwt-<agent>-<hex>).
- runDocker accepts `signal`, `stdin`, and `containerName`; on abort it
  force-removes the container synchronously (an async removal was cut
  short by worker teardown when the timed-out test was the last in its
  worker), kills the client, and rejects with the abort reason. Exit,
  SIGTERM and SIGINT handlers remove any still-tracked container, then
  re-raise.
- RunAgentOptions gains `signal`; agent({ timeoutMs }) puts a deadline
  on the context and executing_the_agent aborts the run when it passes,
  throwing `agent run exceeded <n> ms`.
- Under vitest, executing_the_agent also aborts when the test finishes
  first (its own timeout, a failure elsewhere), so the agent stops
  working and billing. vitest is loaded dynamically because it is an
  optional peer; other runners skip this.

Prompt on stdin
- Both bindings pass the prompt through `docker run -i` on stdin instead
  of argv. Linux caps one argument at 128 KB; stdin has no limit, and
  the prompt no longer shows in `ps` on the host. Verified against both
  CLIs.
- buildClaudeDockerArgs / buildDockerArgs take an optional `containerName`
  instead of `prompt` (they build argv only).

Tests
- _runProcess.spec: stdin delivery, abort kills the child and rejects,
  already-aborted signal rejects at once (uses node as the child).
- Specs for both bindings assert the name, `-i`, no prompt on argv, and
  that the runner receives stdin, containerName, and the signal.
- executing_the_agent.spec: timeoutMs cancels and reports.
- e2e/cancel.spec: against real docker, an aborted run rejects and its
  container is gone; a 300 KB stdin reaches the container intact.

README: cancellation and timeouts section; stdin noted where the run
commands are described.
@matt-wright-dev matt-wright-dev added the minor Next release bumps the minor version label Aug 30, 2026
@matt-wright-dev

Copy link
Copy Markdown
Collaborator Author

Closing in favor of a fresh implementation on top of #17. Every file this PR touched was moved or rewritten there, so a rebase would have been a rewrite anyway. Same feature, redesigned for the AgentBinding / runBoundAgent split. #11 stays the tracking issue, the replacement PR links back here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Next release bumps the minor version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Robustness: cancel runs that time out, and accept large prompts over stdin

1 participant