feat(agent-manager): add OpenCode adapter for agent control#82
Conversation
Detects running opencode processes, reads sessions from OpenCode's SQLite DB (~/.local/share/opencode/opencode.db, XDG-aware), and exposes them through the existing AgentAdapter interface so all agent commands work unchanged. Status logic distinguishes running tools from finished turns to avoid mislabeling mid-turn work as WAITING. Adapted from maquinista-labs/maquinista's source_opencode.go (Go, Postgres-backed) — credits to @otaviocarvalho in the PR body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the time-based freshness heuristic with OpenCode's own turn-done flag (assistant message's time.completed JSON field). Resolves two issues: - Mid-turn pauses (LLM thinking between steps, long reasoning) no longer flip status to WAITING — time.completed stays null until the turn ends. - Ordering messages by time_created (not time_updated) prevents a stale user message — touched by OpenCode appending summary diffs after the turn — from masking the latest assistant message and falling through to RUNNING when the agent had actually finished. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the contribution, @nothingrotf, really clean adapter. I just have a few minor comments. The rest LGTM. |
| if (ref.dbPath === this.dbPath) { | ||
| db = this.openDb(); | ||
| } else { | ||
| db = this.openDbAt(ref.dbPath); |
There was a problem hiding this comment.
This seems not likely to happen.
There was a problem hiding this comment.
Good catch — sessionFilePath is always our own encoding, so the else branch is dead code. Dropped it in 838597a.
| if (!fs.existsSync(dbPath)) return null; | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any | ||
| const Ctor = require('better-sqlite3') as any; |
There was a problem hiding this comment.
Use import instead of require('better-sqlite3')
There was a problem hiding this comment.
jest.mock('better-sqlite3', …) works the same with an ES import
There was a problem hiding this comment.
Done in 838597a — switched to import Database from 'better-sqlite3', tests still pass.
There was a problem hiding this comment.
Confirmed — kept the jest.mock setup as-is and it intercepts the ES import the same way.
|
|
||
| private resetDb(): void { | ||
| if (this.db) { | ||
| try { this.db.close(); } catch { /* ignore */ } |
There was a problem hiding this comment.
This is only called when we have a query exception. How about the user terminates the process or the command finish running?
There was a problem hiding this comment.
Good point — the handle was leaking on normal command completion. Renamed resetDb to a public close() and registered hooks on exit, SIGINT, and SIGTERM in the constructor (838597a). Now the DB is released on graceful exit, Ctrl+C, and signal-driven shutdown — not only on query exceptions.
- Use static `import Database from 'better-sqlite3'` instead of dynamic require — jest.mock works the same with an ES import. - Drop the `dbPath === this.dbPath` branch in getConversation; the else path is unreachable since sessionFilePath is always our own encoding. - Replace private resetDb with public close(), and register process exit / SIGINT / SIGTERM hooks so the SQLite handle is released on normal command completion and signal-driven shutdown, not only on query exceptions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review @codeaholicguy! Addressed all four comments in 838597a:
All 24 OpenCodeAdapter tests + the wider 288-test suite still green. Ready for another look whenever you have a sec. |
|
Thanks for quickly addressing the comments. LGTM. |
feat(agent-manager): add OpenCode adapter for agent control
Summary
Adds an
OpenCodeAdapterto@ai-devkit/agent-managerso OpenCode sessions can be detected, listed, opened, and controlled through the sameai-devkit agentcommands that already work for Claude Code, Codex, and Gemini CLI.OpenCode stores its sessions in a SQLite database (
~/.local/share/opencode/opencode.db, XDG-aware) rather than JSONL files like the other agents. The adapter handles that difference internally — the rest of the agent-control surface (process detection, terminal focus,agent list/sessions/send/detail/open) is unchanged.Credits / inspiration
The SQLite schema reading approach (tables
session,message,partand the JSON-in-data extraction) was adapted frommaquinista-labs/maquinista, specificallyinternal/monitor/source_opencode.go. The Go implementation is monitor-oriented and depends on a Postgres-backed session map; this PR adapts the same data-model knowledge to ai-devkit'sAgentAdapterinterface (TypeScript, file/process-driven, no external services).Special thanks to @otaviocarvalho, maintainer of maquinista-labs/maquinista, for the reference implementation and for pointing me at the relevant source file.
What changed
packages/agent-manager/src/adapters/OpenCodeAdapter.tsdetectAgents,canHandle,getConversation,listSessionsbetter-sqlite3$XDG_DATA_HOME(fallback~/.local/share)(dbPath, sessionId)intoAgentInfo.sessionFilePathas<dbPath>::<sessionId>so the existing interface stays untouchedAgentTypegains'opencode'(AgentAdapter.ts)RUNNINGwhile a tool part hasstate.status='running',RUNNINGfor parts updated in the last 5 s (mid-turn),WAITINGfor an assistant turn that has been quiet beyond that,IDLEpast 5 minadapters/index.ts+index.ts, registration inpackages/cli/src/commands/agent.ts,TYPE_LABELSentry__tests__/adapters/OpenCodeAdapter.test.tscovering process matching, status transitions (tool-running, mid-turn, finished-turn, idle), conversation reconstruction, and session listingbetter-sqlite3added topackages/agent-manager/package.json(already in the monorepo via@ai-devkit/memory)❌ Not Readyto🚧 TestingStatus determination
Decided in this order (first match wins):
lastActiveolder than 5 minIDLEstate.status='running'RUNNINGRUNNING(mid-turn)assistant(turn finished, past freshness)WAITINGRUNNINGThe tool-running and freshness checks fix the case where a long-running build/tool would otherwise be misreported as
WAITINGjust because its owning message hasrole='assistant'.Test plan
npm testinpackages/agent-managerpasses (288 tests, 23 new)npx tsc --noEmitclean in bothagent-managerandcliai-devkit agent listagainst a project with a live OpenCode session shows the agent with correct CWD, summary (first user message), and statusRUNNING→WAITINGcorrectly when a turn finishesai-devkit agent sendto a waiting OpenCode session delivers the message via the existing tmux/iTerm2/Terminal.app pathai-devkit agent detailreconstructs the conversation from SQLite partsNotes for reviewers
sessionFilePathencoding<dbPath>::<sessionId>is internal to this adapter — other adapters and the CLI treat it as an opaque string. Chose::over a custom URI to avoid touching the shared interface.better-sqlite3is required dynamically (require()) so test environments can mock it cleanly; production code always exercises the same path.TerminalFocusManagerorTtyWriter— OpenCode runs in a normal TTY, soagent open/agent sendreuse the existing tmux/AppleScript path with no special-casing.