Make OpenCode Runs Observable and Recoverable

Work directly in:
/home/kernel-kain/Documents/Github/unfinished-business
Execution rules:
- Work only on the main branch.
- Begin with:
- git status
- git switch main
- git pull --ff-only origin main
- Preserve all unrelated changes, untracked files, and existing stashes. Do not apply, delete, or commit .backboard/, .unfinished-business/, .env files, credentials, or the existing stash unless this phase explicitly requires it.
- Read the relevant source, tests, AGENTS.md files, and documentation before editing.
- Implement only the phase described below. Do not begin the next phase.
- Maintain backward compatibility with existing saved projects through explicit migrations.
- Do not expose provider secrets, prompts containing secrets, or private model output in logs, telemetry, screenshots, or error messages.
- Add or update tests for every behavior changed.
- After implementation, update the relevant project documentation, including:
- README.md when user-facing behavior changes
- docs/ARCHITECTURE.md
- docs/DECISIONS.md
- docs/IMPLEMENTATION_PLAN.md
- docs/EVIDENCE.md
- docs/RELEASE_NOTES.md
- any phase-specific setup or integration documentation
- Mark the phase complete in the implementation plan only after its acceptance criteria pass.
- Run all relevant verification. At minimum:
- npm run typecheck
- npm test
- npm run build Run visual, security, budget, contract, migration, or integration tests when affected.
- Review the final git diff for accidental files, secrets, debugging output, and unrelated changes.
- Commit with the commit message specified by this phase.
- Push the commit to origin/main. Never force-push.
- If the push is rejected because origin/main advanced, fetch and rebase safely, resolve only scoped conflicts, rerun verification, and push normally.
- Finish with:
- implementation summary
- changed files
- documentation updated
- tests and results
- known limitations
- commit hash
- push result
Do not stop for ordinary implementation decisions. Stop only if there is a destructive conflict, missing required credential, irreconcilable remote conflict, or a decision that would materially change the product’s intended behavior.
I've completed the preparatory step, but I have to stop before implementation: no phase description was included in your message.
What I did
git status— clean onmain(only untracked.backboard/, left untouched as required).git switch main/git pull --ff-only origin main— already up to date; head isd14b7bc feat(models): support all accessible OpenCode models.- Read
docs/IMPLEMENTATION_PLAN.md— its phase history ends at:- Upgrade Phase 0 — product-upgrade preparation: complete and verified
- Upgrade Phase 1 — model access and provider recovery: complete and verified, committed and pushed
- Searched the repo for the next phase spec — no "Upgrade Phase 2" definition exists anywhere: not in
docs/IMPLEMENTATION_PLAN.md,docs/DECISIONS.md,docs/RELEASE_NOTES.md,docs/prompts/,AGENTS.md(none present), or the idea packet. There is no plan document describing what comes after Phase 1.
Why I'm not proceeding
Your instructions say to "implement only the phase described below," but no phase was described. Everything Phase 1 required — including the committed/pushed precondition for the next phase — is satisfied, so I can't infer the intended scope from the repo itself.
Choosing my own "Phase 2" (e.g., guessing whether it should be multi-user isolation, repository connectors, Backboard integration, public deployment, or bundle splitting from the deferred-work list) would be a decision that materially changes the product's intended behavior — one of the explicit stop conditions. The project's own governing docs (docs/prompts/README.md) also require each implementation phase to come from an explicitly supplied prompt, and a blocked or unspecified phase must stay at its gate rather than silently continue.
Current state (nothing changed)
- Branch:
mainatd14b7bc, in sync withorigin/main— clean except untracked.backboard/ - Stash:
stash@{0}: codex-pre-pull-2026-10-31— preserved untouched - Files modified: none · Commits made: none · Pushed: n/a
What I need from you
The phase specification: its name/title and scope, work units or acceptance criteria, the required commit message if it's pinned, and which docs are phase-specific for this one. Once you paste that in, I'll pick up exactly where I stopped — the tree is already verified at the correct starting state.
Implement Phase 2: make OpenCode operations observable, cancellable, and recoverable.
Objectives:
- Replace frozen requests with meaningful live progress.
- Prevent timed-out native sessions from permanently blocking the workflow.
- Reconcile late responses and diffs exactly once.
Implementation requirements:
- Inspect the installed OpenCode client’s event and session APIs before designing the implementation.
- Convert Oracle, Architect, Maker, and Witness runs into persisted asynchronous operations where appropriate.
- Consume OpenCode events or safe polling to report:
- connecting;
- session created;
- waiting for model;
- receiving output;
- validating result;
- waiting for permission;
- applying result;
- running checks;
- completed or failed.
- Do not display private chain-of-thought or stream unvalidated raw JSON.
- If partial output is displayed, label it as incomplete and do not save it as the section.
- Add elapsed time, cancel, retry, and recovery controls.
- Persist enough operation metadata to recover:
- intended action;
- target section;
- starting revision;
- model;
- native session/message IDs;
- idempotency key;
- expected output type.
- Reconciliation must retrieve and validate the actual late response, diff, or check result.
- Apply a recovered result exactly once.
- Never mark a run successful only because a native session is idle.
- Support:
- Continue waiting
- Reconcile now
- Open in OpenCode
- Cancel
- Retry after confirmed failure
- Mark abandoned
- Make old unknown operations recoverable through migration.
- Add tests for:
- late Oracle response;
- late Architect response;
- late Maker diff;
- duplicate reconciliation;
- cancellation;
- permission waiting;
- reconnect after server restart;
- section 06 timeout recovery.
Acceptance criteria:
- A slow run shows useful progress promptly.
- A timed-out run does not permanently block the project.
- Reconciliation applies a valid late result once.
- Retrying cannot accidentally duplicate a previous native run.
- Cancelled or abandoned operations remain understandable in history.
Commit message:
fix(runtime): make OpenCode runs observable and recoverable
User requested that active blocking work be moved to the background.
Backgrounded work:
- shell: for group in "tests/experience.test.ts" "tests/experience-hardening.test.ts" "tests/migration.test.ts tests/contracts.test.ts tests/prompts.test.ts" "tests/runs.test.ts tests/sessions.test.ts tests/recovery.test.ts tests/store-hardening.test.ts tests/workflow.test.ts" "tests/security.test.ts tests/http-hardening.test.ts tests/http-workflow.test.ts tests/model-access.test.ts tests/onboarding.test.ts tests/provider-errors.test.ts tests/phase-zero-fixtures.test.ts tests/connection.test.ts tests/native-permissions.test.ts tests/budgets.test.ts"; do echo "== $group"; timeout 240 npx tsx --test $group 2>&1 | tail -4; done
The backgrounded work is still unfinished. Move on to other work if you can. If there is nothing else useful to do, finish your response. Do not wait, sleep, poll, or report the backgrounded work as complete until a later completion notification is added to the conversation.
Clean. Now write the new phase test file tests/observable-runs.test.ts covering all eight required scenarios:
Phase 2 implementation summary — run observability and recovery
Implementation summary
OpenCode runs (Oracle, Architect, Maker, Witness) are now persisted asynchronous operations with honest live progress, and timed-out/late runs reconcile their real results exactly once.
- Pre-flight checks (requirement 1): I fully mapped the installed
@opencode/client@2.0.32API surface —event.subscribe(SSE),session.{create,prompt,generate,wait,get,diff,interrupt},message.list,permission.*— and chose safe polling of the bounded endpoints (session.get,message.list,permission.request.list) because the app's network policy enforcesmaxResponseBytes/timeouts against an unbounded SSE stream. Recorded as decision U2-01 indocs/DECISIONS.md. - Async persisted runs (req 2):
clarify,draft,witness, and Maker now queue an operation (202 Accepted) and run throughGuideRunCoordinator/MakerRunCoordinator; thequestionadapter stays synchronous by design (changes no saved state). - Progress phases (req 3): connectors emit and persist
CONNECTING → SESSION_CREATED → WAITING_FOR_MODEL → RECEIVING_OUTPUT → VALIDATING_RESULT → WAITING_PERMISSION → APPLYING_RESULT → RUNNING_CHECKSplus terminal states; a browser refresh shows where the run actually is. - No unsafe display/saves (reqs 4, 5): phases are fixed safe summaries; no raw model text, no partial output; sections/proposals/drafts are stored only from validate-replies through the same schemas as live completions.
- Controls (req 6, 11): Run Inspector gains elapsed time plus Continue waiting / Reconcile now / Open in OpenCode / Cancel / Retry after failure / Mark abandoned; new routes
POST /api/runs/:id/retryandPOST /api/runs/:id/abandon. - Recovery metadata (req 7): schema v5 persists
intent,sectionId,startingRevision,idempotencyKey,expectedOutput,retryOf, andprogresson every operation. - Exact-once reconciliation (reqs 8–10): reconcile observes the outcome, retrieves the actual late reply/diff, validates it, runs the node check, and applies via
appliedAt+ the idempotent action record; an idle session alone is never success; retry creates a new operation withretryOflineage after a confirmed failure only, so no native run can be duplicated; unretrievable results stay honest inCOMPLETED_UNAPPLIED. - Migration (req 12): explicit v1/v2/v3/v4 → v5 migration preserves all history and stamps old unresolved operations with a deterministic
AWAITING_RECONCILIATIONmarker, keeping them recoverable. - Extra hardening found during testing: bounded retries for
recoverAfterRestartandwriteProgressno-op matching (so background observation can't rattle revisions after a terminal write), plus latereply-vs-cancel merge rules.
Changed files
- Source:
src/shared/workflow.ts,src/shared/demo.ts,src/server/workflow/model.ts,src/server/workflow/service.ts,src/server/experience/{service,guide,runs,progress,failures,guide-runs}.ts(3 new server modules),src/server/app.ts,src/web/{api.ts, styles.css, opencode-panel.tsx},src/web/components/RunInspector.tsx,src/web/workshop/WorkshopPage.tsx - Tests: new
tests/observable-runs.test.ts; updatedexperience.test.ts,experience-hardening.test.ts,http-hardening.test.ts,migration.test.ts,model-access.test.ts,store-hardening.test.ts
Documentation updated
README.md, docs/{ARCHITECTURE,DECISIONS,IMPLEMENTATION_PLAN,EVIDENCE,RELEASE_NOTES,CHANGELOG,OPENCODE_INTEGRATION,OPERATIONS_AND_RECOVERY,TOOL_CATALOG,DEMO_AND_SUBMISSION,PRODUCT_BRIEF}.md
Tests and results
| Check | Result |
|---|---|
npm run typecheck | PASS |
npm test | PASS — 150 tests, 0 failures (10 consecutive full-suite runs stable; was 139 before the phase) |
| Coverage required by the phase | late Oracle response; late Architect response; late Maker diff; duplicate reconciliation; cancellation; permission waiting; reconnect after restart; section 06 timeout recovery — all in tests/observable-runs.test.ts |
npm run build | PASS (Vite large-chunk warning remains, per earlier phases) |
npm run budget | PASS — 194.9 / 267.8 / 14.6 KiB gzip; hero 570.9 KiB |
npm run test:visual | PASS — 18/18, snapshots unchanged (demo renders stay byte-identical) |
npm audit --omit=dev --audit-level=high | PASS (exit 0; 6 moderate advisories unchanged) |
| Diff/security review | PASS — no secrets, debug output, or unrelated changes; .backboard/, stash, and .env untouched |
Known limitations
- Live progress uses bounded polling; the SSE event stream is the documented next upgrade (U2-01), since it doesn't fit the capped-response network policy yet.
- Retry is Maker-only (the persisted prompt for guide runs is never stored, by design); guide retries are re-sent as new stage requests.
- "Open in OpenCode" focuses this app's OpenCode panel; it doesn't drive a remote OpenCode UI.
- Maker
ABANDONED→ success requires OpenCode to report a succeeded outcome and a readable diff; an unreadable late diff staysCOMPLETED_UNAPPLIEDfor later reconciliation. - No live provider inference was used in tests; all provider fixtures remain synthetic.
Commit hash
53fa12cf7c7fb7e9431be7b8d0d7f3c68f47b66d — fix(runtime): make OpenCode runs observable and recoverable
Push result
Success — d14b7bc..53fa12c main -> main via normal push (no force-push). origin/main did not advance during the phase, so no rebase was needed. Working tree is clean except the pre-existing untracked .backboard/, and stash@{0}: codex-pre-pull-2026-10-31 is preserved untouched.