Skip to content

refactor(runtime): remove the opencode adapter and in-sandbox serve - #73

Open
FreshlyBrewedCode wants to merge 1 commit into
acp/usage-uifrom
acp/remove-opencode
Open

FreshlyBrewedCode wants to merge 1 commit into
acp/usage-uifrom
acp/remove-opencode

Conversation

@FreshlyBrewedCode

@FreshlyBrewedCode FreshlyBrewedCode commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Removes the opencode-specific runtime, so the ACP adapter is the only runtime (ADR 0013 §1, §4, §6).

What changed

Removed

  • src/runtime/opencode-adapter.ts and its test: opencode serve in a localProcessSandbox, the freePort probe, and extractOpencodeSignal.
  • src/lib/sandbox-config.ts and its test (writeHeadlessPermissions, the opencode.json write).
  • AgentAdapter.prepareWorkspace, StartRunOptions.prepareWorkspace and startTrackedRun's prepareWorkspace. Every caller goes with them: the CLI, the legacy POST /api/runs {dir, clone} path, the no-op implementations in the ACP, replay and fake adapters, and the three tests that exercised preparation.
  • Dependencies @tanstack/ai-opencode and @tanstack/ai-sandbox-local-process.
  • Write-back's D16 stray-artifact cleanup. Its patterns were .tanstack-projected- and ^data(/|$), so it also deleted any top-level data/ the agent changed. WriteBackFinished.cleanedArtifacts becomes optional so old logs still decode. New runs don't write it, and the SPA shows the "cleaned" row only when the list is non-empty. The .tanstack-projected-* gitignore entry is removed too. There was no data/ entry in .gitignore.

Kept: @tanstack/ai-sandbox. @tanstack/ai-acp declares it as a non-optional peer and needs it at runtime. Its root index.js imports permissions.js, which imports @tanstack/ai-sandbox, and the package exports nothing but .. Bun would auto-install the peer anyway, but it stays a direct dependency so that is visible.

Replay corpus re-recorded through the ACP adapter

  • scripts/record-corpus.ts <agent> <model> <out> runs e2e/implement-issue.ts (implement, fix, structured pr-metadata, write-back) for a fake issue ci(release): add GitHub Actions, semantic-release, and npm packaging #1, "add slugify". It runs against a throwaway clone of a local bare origin, with a fake gh on PATH. Each item the adapter yields goes through a new recordingAdapter.
  • The corpus format extends {step, chunk} to {step, chunk, signal?}. Replay yields the recorded signal with its chunk, so it knows no agent's event names. Legacy lines carry no signal and replay as chunks only.
  • test/corpus/acp-claude-implement-issue.ndjson: claude / haiku, 385 lines, 195 KB. Signals: session id per step, usage, and pr-metadata's structured output (branch factory/issue-1-add-slugify).
  • test/corpus/acp-opencode-implement-issue.ndjson: opencode / opencode/big-pickle, 2113 lines, 765 KB. It is large mostly because of 1309 reasoning deltas. Branch factory/issue-1-add-slugify-helper.
  • Migrated to the new corpora:
    • replay/adapter.test.ts: block grouping, signal replay, and a record→load round trip.
    • agent-step.usage.test.ts: the usage split, plus context/cost from a recorded step.
    • e2e/implement-issue.test.ts: now runs once per agent.
    • The e2e transcript leg: a new seeded run, run-static-acp-replay, from the opencode recording, which has reasoning.
    • The e2e echo run.
  • Kept on the old opencode corpora, because these tests are about historical logs:
    • events.test.ts: D3's evidence about opencode chunk shapes. Its verbatim-AgentChunk check now also covers the ACP files.
    • web/lib/transcript.test.ts.
    • agentStepContextTokens' legacy fallback test.
    • The e2e run-static-corpus, used for the old-log fallback and runs ordering.

agent-step.ts

  • The module comment now describes the ACP cancel: abort → session/cancel, then a kill after a grace period and in finally.
  • abortableIterable stays. The problem it solves is generic: an async generator's return() queues behind a pending next(). The ACP adapter's generator waits on its incoming queue, so without the wrapper an interrupt would block until the agent's next session update. The abort that would release it fires only after that. The shutdown tests that use a never-yielding adapter also need it.
  • AGENT_TEARDOWN_GRACE_MS (1 s) stays, unchanged. It bounds settled for adapters that never tear down, which the shutdown tests use. Its comment is rewritten for ACP: teardown was measured at 13–70 ms, and an agent that ignores the cancel is killed by the adapter's own 2 s timer.

Fix exposed by the removal. The daemon cancels a run that was still a reserved slot right after startRun returns. The prepareWorkspace await used to give that cancel a moment to land before the workflow's first step. Without it, shutdown.test.ts saw an ExecStarted before RunCancelled. startRun now yields once after RunStarted and checks cancelled before calling workflow.run.

Also: the sessionId doc comment in src/events.ts no longer mentions opencode.session-id.

Validation

  • bun run check: green, 451 tests.
  • bun run test:e2e (via nix develop): 17/17 passed.
  • Leftover grep, outside docs/: no matches for opencode serve, freePort, ai-opencode, localProcessSandbox, writeHeadlessPermissions, opencode.json or prepareWorkspace. .tanstack-projected- remains only in the historical event comment and fixture.
  • Live smoke through factory serve on port 3066. It used a scratch project, a test db, and a local bare origin with HEAD on main, and no write-back. Port 3005 was not touched.
    • smoke workflow, one run per agent, started concurrently with factory start --agent … --watch. Both completed and edited math.ts.
      • Claude/haiku: 7.4 s, context 21.1k/200k, $0.026.
      • opencode/big-pickle: 15.6 s, context 8.2k/200k, $0.
    • Both workspaces show only M math.ts. There is no opencode.json and no .git/info/exclude entry.
    • Live cancel per agent, mid-tool-call (python3 -c 'time.sleep(90)'):
      • Claude: cancel returned in 28 ms. The step is cancelled and the run RunCancelled. 0 of 4 processes were alive 3 s later (claude-agent-acp, the claude binary, its shell, python).
      • opencode: cancel returned in 20 ms. 0 of 2 processes were alive (opencode acp, python).

Deviations

  • Write-back's stray-artifact cleanup was removed along with the ignore entry. The data/ pattern was the "data/ ignore", and it deleted real changes under a top-level data/.
  • The startRun yield-before-workflow fix described above.

Closes #66
Part of #62

🤖 Generated with Claude Code

The ACP adapter is the only runtime now (ADR 0013 §1, §4, §6):

- delete `opencode-adapter.ts` (`opencode serve` in a local-process
  sandbox, the free-port probe, `extractOpencodeSignal`) and
  `lib/sandbox-config.ts` (the `opencode.json` write)
- drop `AgentAdapter.prepareWorkspace` and `StartRunOptions` /
  `startTrackedRun`'s `prepareWorkspace`, with every caller
- drop `@tanstack/ai-opencode` and `-sandbox-local-process`; keep
  `@tanstack/ai-sandbox`, which `@tanstack/ai-acp` imports at load time
- drop write-back's D16 stray-artifact cleanup (it also deleted any
  top-level `data/`); `WriteBackFinished.cleanedArtifacts` becomes
  optional for old logs, and the `.tanstack-projected-*` ignore goes
- the replay corpus is re-recorded through the ACP adapter for Claude
  (haiku) and opencode (big-pickle) by `scripts/record-corpus.ts`;
  lines are `{step, chunk, signal?}`, so replay yields the recorded
  signals and knows no agent's event names
- `agent-step.ts`'s commentary describes the ACP cancel; it keeps
  `abortableIterable`, which `return()` behind a pending `next()` still
  needs
- a run yields once before its workflow, so the daemon's deferred
  cancel of a reserved run lands before the first step; the
  `prepareWorkspace` await used to provide that gap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

refactor(runtime): remove the opencode adapter and in-sandbox serve

1 participant