Skip to content

fix(external-agent): keep awaiting_review proposals reviewable when the run record is gone - #95

Open
Alphaxiaoteng wants to merge 1 commit into
0xsline:mainfrom
Alphaxiaoteng:fix/external-proposal-run-recovery
Open

fix(external-agent): keep awaiting_review proposals reviewable when the run record is gone#95
Alphaxiaoteng wants to merge 1 commit into
0xsline:mainfrom
Alphaxiaoteng:fix/external-proposal-run-recovery

Conversation

@Alphaxiaoteng

Copy link
Copy Markdown

Problem

A persisted awaiting_review external edit session whose agent run record no longer exists (e.g. the editor/server restarted before the run was persisted) currently fails hydration with External proposal is active in another editor or no longer resumable. This orphans the proposal permanently: it can never be applied, rejected, or discarded, and the project's external-edit surface is blocked (the UI also surfaces Agent run for edit session ... is unavailable).

Root cause

hydrateStoredExternalBridge treats two different situations identically when ExternalSessionRunLedger.resume() returns null:

  1. the run still exists but is claimed by another active editor — must not steal ownership;
  2. the run record is gone entirely — the awaiting_review proposal is complete on disk and only needs the run ledger for durable artifact recording, not for review/apply/reject.

Case 2 should degrade gracefully instead of throwing, but the current code throws for both.

Fix

  • external-bridge-hydration.ts: when the run cannot be resumed, check whether the run record actually still exists via loadAgentRuntimeSidecar. Only throw when the record exists (owned elsewhere). When the record is gone, install the restored session with run: null and keep the awaiting_review proposal reviewable.
  • external-bridge-runtime.ts: apply() and reject() no longer hard-require the run ledger (requireRun + confirmOwnership); they confirm ownership only when a run is present. discard()/markTerminal were already null-safe.
  • external-edit-session-runtime.verify.ts: new check that an awaiting_review proposal with a missing run record hydrates as reviewable and can be rejected.

Verification

  • npx tsc -b --force
  • npm run lint ✅ (0 errors)
  • npm run build (tsc -b && vite build) ✅
  • npm test ✅ (full suite)

…he run record is gone

A persisted awaiting_review external edit session whose agent run record no
longer exists (e.g. the editor/server restarted before the run was persisted)
currently throws 'External proposal is active in another editor or no longer
resumable' during hydration, orphaning the proposal: it can never be applied,
rejected, or discarded.

When the run record is genuinely gone (not merely claimed by another active
editor), degrade gracefully instead of throwing: install the restored session
without a run ledger, publish the awaiting_review proposal for review, and let
apply/reject/discard proceed without requiring the missing run. When the run
still exists and is owned elsewhere, the original safety guard is kept.

@0xsline 0xsline left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking ownership cases remain:

  1. external-bridge-hydration.ts:121-132 treats a run missing from loadAgentRuntimeSidecar() as definitively gone. That read uses kvGet(), which falls back to IndexedDB after a project-store failure (sharedKv.ts:405-420). A stale/empty local cache can therefore report “gone” while the authoritative store still has a lease owned by another editor, allowing the run-less path to bypass ownership.

  2. Once installed without a ledger, external-bridge-runtime.ts:234-299 allows apply/reject without any claim. saveExternalProposal() is an unconditional kvSet (externalProposalStore.ts:139-144), so two tabs can hydrate the same orphan and publish conflicting terminal states. Please add an authoritative, atomic proposal/run recovery claim (or equivalent CAS) and cover concurrent recovery plus offline-cache ambiguity. Also split hydrateStoredExternalBridge; it is now 52 lines, over the repo <50 function limit.

The submitted typecheck, changed-file lint, and runtime verify pass, but the new test covers only a single runtime and reject, so it does not exercise these races.

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