Skip to content

Bug dock: composer wedges when the server can't arm report-a-bug (staged-skill drift) #296

Description

@aarontrowbridge

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:

  1. bugReportSkillStaged() (and the app's amicode_bug_report boot param) evaluate the extension's skill set — the VSIX ships skills/report-a-bug, so the button renders.
  2. BugReportManager.armSession() POSTs /session/:id/command with command: "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 predated report-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, when report-a-bug is 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)

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions