Repository navigation
Bug dock: composer wedges when the server can't arm report-a-bug (staged-skill drift) #296
Description
Symptom
With 0.2.1, the Report a Bug composer button renders and is clickable, but clicking it never opens a working bug dock — from the user's side, the button "won't send messages." Observed 2026-08-08 on a fleet install where the canonical opencode server runs outside the extension host (launchd, amicode-server.sh).
Root cause
The button and the arm step gate on different skill sets:
bugReportSkillStaged()(and the app'samicode_bug_reportboot param) evaluate the extension's skill set — the VSIX shipsskills/report-a-bug, so the button renders.BugReportManager.armSession()POSTs/session/:id/commandwithcommand: "report-a-bug"against the server's staged skills. When the server is the launchd canonical server, that set is the wrapper's static staging snapshot — which predatedreport-a-bug, so the arm 404s.
What follows is per spec but reads terribly: open() catches, deletes the orphaned session, and shows an error toast. The composer, however, appears to stay in bug mode wired to the now-deleted session — subsequent sends go nowhere. (App-side recovery gap; the composer should exit bug mode when the open fails, or never enter it until open-bug-report lands.)
Evidence: server GET /command listed 41 commands, none named report-a-bug; the chat DB contained zero sessions with bug_report metadata despite multiple button clicks; the fix was staging the skill server-side.
Suggested fixes
- Pre-flight the arm: the extension can GET
/command(or the boot-time equivalent) and, whenreport-a-bugis absent server-side, fail with an actionable error — "the opencode server's staged skills are stale (missing report-a-bug); restart/re-stage the server" — instead of a generic arm failure. - App-side recovery: on a failed/never-completed open, the composer exits bug mode (or never enters it); a failed click must not mute the normal composer.
- Docs: note in ADR-0004's lifecycle that the arm target is server-side state, so any non-extension-hosted server must re-stage skills on extension updates. (Fleet-side, the server wrapper now rsyncs the newest VSIX's
skills/into its staging dir at boot — additive, no deletes — which closes the drift class for that deployment.)
Refs: #251 (bug-session orchestration), ADR-0004, #295 (related server-misconfiguration guard, draft).
Implementation Plan
Layer 1: Pre-flight the arm (extension-side)
File: packages/extension/src/bug_report.ts
In open(), before createSession(), GET /command from the server and verify report-a-bug is in the response. If absent, show an actionable error and return without creating a session.
New private method:
private async serverHasCommand(server: BugReportServer, name: string): Promise<boolean> {
try {
const res = await this.fetch(new URL("/command", server.url), server, { method: "GET" });
if (!res.ok) return true; // optimistic on probe failure — fall through to arm
const commands: { name: string }[] = await res.json();
return commands.some((c) => c.name === name);
} catch {
return true; // network failure: let the arm path handle it
}
}Call site (in open(), before createSession):
if (!(await this.serverHasCommand(server, REPORT_A_BUG_SKILL))) {
this.deps.showError(
"Amicode: the opencode server's staged skills are stale (missing report-a-bug). " +
"Restart or re-stage the server.",
);
return;
}Fail-open: if the GET itself fails (network, server busy), return true and let the existing arm-failure path handle it. Never block the happy path on a probe failure.
Layer 2: Reactive liveness effect (app-side)
File: packages/app/src/pages/session/composer/session-bug-dock.tsx
Add a createEffect that watches the session map when the dock is in "chat" phase. If the dock's current sessionID vanishes from the synced session-info (because the extension deleted it), auto-dismiss silently via controller.dismiss(). No new HTTP calls — rides the existing session-info subscription. No toast (the extension already showed one).
// Liveness watch: if the dock's session vanishes externally (arm-failure
// cleanup race, or any unexpected deletion), auto-dismiss silently.
createEffect(() => {
if (bugDockController.phase() !== "chat") return;
const id = bugDockController.sessionID();
if (!id) return;
const info = serverSync().session.data.info ?? {};
if (!(id in info)) {
bugDockController.dismiss();
}
});Layer 3: ADR-0004 update
Append to "Accepted costs": the arm target (/session/:id/command) resolves against the server's command set, not the extension's staged skill paths. A non-extension-hosted server must re-stage skills on extension updates; the extension pre-flights the arm and fails actionably when the skill is absent server-side. The fleet wrapper's additive rsync-at-boot closes this drift class.
Acceptance Criteria
- Clicking "Report Bug" when the server lacks the skill shows: "Amicode: the opencode server's staged skills are stale (missing report-a-bug). Restart or re-stage the server." — no session created.
- If the dock adopts a session via sync-watch and that session is then deleted, the dock auto-dismisses within one reactive tick — no user action required, no wedged composer.
- The pre-flight is fail-open: a network error on GET /command does NOT block the button; the existing arm-failure path handles it.
- ADR-0004 documents the server-side arm constraint.
- Test: extension pre-flight — mock GET /command without report-a-bug; assert showError + no createSession.
- Test: app liveness — adopt session, remove from session map, assert dismiss.
Scope
- Extension:
bug_report.ts(~15 lines new) - App:
session-bug-dock.tsx(~8 lines new) - Docs:
docs/adr/0004-bug-session-lifecycle.md(~3 lines appended) - Tests:
test/bug_report.test.ts(1 new describe block), app dock test (1 new case)
Activity
docs(adr-0004): note server-side arm constraint (staged-skill drift, #…
fix(extension): pre-flight bug-report arm — fail actionably on stale …