Skip to content

Background recovery, bridge workspace cleanup, bridge health in the UI - #18

Merged
christopherjnelson merged 3 commits into
mainfrom
claude/optimistic-shannon-4dh7gs
Sep 28, 2026
Merged

christopherjnelson merged 3 commits into
mainfrom
claude/optimistic-shannon-4dh7gs

Conversation

@christopherjnelson

Copy link
Copy Markdown
Member

This PR covers the four follow-ups left from the audit fixes, in three commits.

Recover unfinished work in the background and share the reviewer check (654ca87)

Startup and project open used to wait for recovery to finish. Recovery can replay a submission to an external UHP server, so a slow or hung server kept Foreman from listening and made opening a project hang.

  • Recovery now starts in the background. Concurrent calls share one in-flight recovery per controller.
  • A failure is recorded as a recovery.failed event and logged to stderr with credentials masked. It never surfaces as an unhandled rejection.
  • Recovery claims the assignments it is working on, and the 5 s reconcile loop skips only those. The two can no longer process the same assignment at once.
  • Recovery only stops runs that were already active when it began.
  • A late "still running" observation can no longer overwrite an assignment that has already finished.
  • recovery state is reported in service status and in workspace-setup: idle / running / failed, with timestamps and the last error.
  • Shutdown does not wait for recovery, and its final flush is bounded to 5 s.
  • The rule deciding whether a Reviewer response proves the complete read-only evidence package was duplicated in the live and reconcile paths. It is now one method, and each site keeps its own error text and events.
    • Tests pin one pass case and eight rejection cases at each site, and they pass both before and after the refactor.
    • The reconcile site's separate harness-id comparison is now covered by reviewerModelEvidence. That check requires selectedHarnessId, which comes from the reported harness, to equal the requested harness.

Seed bridge workspaces in one git process and clean them up (6d16080)

  • Seeding: workspace seeding streams every blob through a single git cat-file --batch process. All existing limits and messages are kept.
    • On a 3000-file repository, seeding went from a median of 24.2 s to 2.7 s. Some of the later runs were helped by a warm cache.
  • DELETE endpoint: DELETE /extensions/foreman-workspace/v1/workspaces/:id returns 204, 404 for an unknown id, or 409 while a response is running. It requires the bridge token like every other route.
  • Startup sweep: at startup the bridge removes orphaned bridge directories that no persisted state references.
  • Idle sweep: idle workspaces are swept after LOCAL_CLI_UHP_WORKSPACE_TTL_MS (default 24 h; 0 disables). A workspace with a running response is never removed.
  • Symlinks: removal never follows a symlink out of the work root.
  • State writes: these are now serialized, so concurrent persists cannot race on the temp file.

Show the local bridge's health in the UI (d2998bb)

  • Header badge: a "Bridge · Restarting" (warning) or "Bridge · Unavailable" (danger) badge appears in the header. Nothing is shown while the bridge is ready.
  • Repository details: the bridge's message, restart count, time until the next restart, and the log path (with a Copy button). When the bridge has given up, it also offers "Reopen repository", which starts a fresh bridge.
  • Polling: each workspace-setup request runs git status on the repository. The UI therefore polls it every 5 s while the bridge needs attention, every 30 s otherwise, and not at all while the tab is hidden.

Verification

  • Typecheck is clean.
  • vitest: 595/595 passing. New files are recovery, reviewer-evidence and bridge-status.
  • pnpm test:bridge: 68/68 passing on two consecutive runs. New tests cover batch seeding (binary, symlink, executable, 250+ files, and failure cases), DELETE, symlink safety, the startup sweep, and the TTL sweep.
  • Build succeeds.
  • The bridge health UI was screenshot-checked at 1440 and 1024 px against a stubbed API.

Worth knowing

  • Shared work root: the startup sweep assumes one bridge per work root and state file. Foreman's per-project bridges each get their own, but two hand-started bridges sharing a work root would sweep each other's workspaces.
  • Discovery still blocks startup: startup discovery (refreshDiscovery) is still awaited. It has its own timeout, and recovery needs its result.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP


Generated by Claude Code

The backend already reported whether a project's bridge was ready,
restarting or given up, but the UI never showed it.

- A "Bridge · Restarting" (warning) or "Bridge · Unavailable" (danger)
  badge sits next to Repository details in the header. Nothing is shown
  while the bridge is ready.
- Repository details then shows a notice with the bridge's message,
  restart count, time to the next restart and the log path (with Copy).
  When the bridge has given up it explains that reopening the repository
  starts a fresh bridge and offers a Reopen repository button.
- workspace-setup is polled every 5 s while the bridge needs attention
  and every 30 s otherwise, and not at all while the tab is hidden. Each
  poll runs git status on the repository, so this keeps the cost bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
Startup and project open awaited Controller.recover(), which can replay
a submission against an external UHP server. A slow or hung server kept
Foreman from listening and made opening a project hang.

- Startup and project open now start recovery without waiting for it.
  Concurrent calls share one in-flight recovery per controller.
- A failure is recorded as a recovery.failed event and written to stderr
  with credentials masked, never left as an unhandled rejection.
- Recovery claims the assignments it is handling, and the 5 s reconcile
  loop skips only those, so the two never process one assignment at
  once. Recovery only stops runs that were active when it started, so a
  run started meanwhile is left alone. A late "still running"
  observation can no longer overwrite a terminal assignment.
- Recovery state (idle, running or failed, with timestamps and the last
  error) is reported in service status and workspace-setup.
- Shutdown does not wait for recovery, and its final flush is bounded.

The rule that decides whether a Reviewer response proves the complete
read-only evidence package was written out twice, in the live submit
path and in reconcile. It is now one method used by both, with each
site's error text and events unchanged. Tests pinning both sites (a pass
case and eight rejection cases each) pass before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
Seeding ran two git cat-file processes per file, and bridge workspaces
were never deleted.

- seedWorkspace streams every blob through one `git cat-file --batch`
  process, parsed incrementally from raw buffers with backpressure. All
  existing limits and error messages are kept; blob size is checked from
  the record header before the body is buffered, and the child has a
  timeout. On a 3000-file repository seeding took a median 2.7 s instead
  of 24.2 s.
- DELETE /extensions/foreman-workspace/v1/workspaces/:id removes a
  workspace and its sibling dirs: 204, 404 for an unknown id, 409 while
  a response is running in it.
- At startup the bridge removes bridge-named directories under its work
  root that no persisted state references.
- Idle workspaces are swept after LOCAL_CLI_UHP_WORKSPACE_TTL_MS
  (default 24 h, 0 disables); one with a running response is never
  removed.
- Removal resolves paths and never follows a symlink out of the work
  root. State writes are serialized so concurrent persists cannot race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
@christopherjnelson
christopherjnelson merged commit 9ef4829 into main Sep 28, 2026
1 check passed
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.

2 participants