#2115

[BUG] Quarantined workspace mutation intent has no disposal path — every write tool stays blocked across tasks and restarts

Openllanozeroopened 14d ago

What version of eigent are you using?

Desktop 1.0.6 (Windows installer Eigent.Setup.1.0.6.exe); backend source == release tag v1.0.6 (verified byte-identical for every file cited below)

System information

  • OS: Windows 11 (10.0.26300) x64
  • Space: single-agent session, Git enabled, checkout_mode = primary_checkout (the User Worktree is checked out directly)
  • Workspace: an existing git repository on branch master, ~2.8k untracked paths at the time of the incident (mostly vendored third-party projects)
  • Runtime state dir: ~/.eigent (journal: ~/.eigent/run-journal.sqlite3; git state: ~/.eigent/workspace-git)

Problem description

Provenance & confidence (please read first)

This report was produced by an AI-assisted investigation (LLM-driven source reading + journal queries), not by reading a hand-maintained design doc. Treat it accordingly:

  • Empirically observed: the refusals themselves, the git_status/read-only asymmetry, the Run row with terminal_detail = "Run background Terminal sessions did not … Git finalization: t6", and the two orphan rows in the journal. These are quoted from our actual state.
  • Verified by code reading, not individually re-executed: the five dead ends below, and the claim that nothing ever produces discarded (our grep for state="discarded" across backend/app returned 0 hits). Line numbers are cited so you can confirm each one quickly; if any is wrong in current main, that is my error and the conclusion may need re-derivation.
  • What we did NOT do: we did not build a minimal reproduction from a clean clone. Our environment is a production desktop install that we then hand-repaired (see "Severity"), so the recovery path we describe is a workaround, not a validated product flow. The precise internal trigger sequence — in particular whether the cancel-during-background-command step is the only way to reach this state — needs your confirmation.

What happened

Once a Run terminates while a workspace mutation intent is still open, the intent is quarantined (git_mutation_intents.status = 'prepared' or 'needs_attention'). From that moment every mutating tool call in the Space is refused — in that task, in new tasks, and after a full app restart — while read-only tools keep working.

Symptom set as observed by the agent:

  • write_to_file, shell_exec, shell_write_content_to_file all fail with Workspace operation is still active or needs attention
  • shell_exec additionally gets the hint "Wait for or stop the active background Terminal session, then retry." — but the referenced session id no longer exists: shell_kill_process('t6') returns No active session found with ID 't6'.
  • git_status reports state: ready, pending_managed_paths: [], healthy diagnostics.
  • The Run that caused it carries terminal_detail = "Run background Terminal sessions did not stop before Git finalization: t6".

Expected: an operator can abandon / reconcile the abandoned operation (there is a declared transition for it), or at minimum a new task can start writing. Actual: total write lockout. The only escape we found was editing our own copy of the journal DB.

Steps to reproduce (deterministic)

  1. Create/enable a Git Space with checkout_mode = primary_checkout and session_mode = single-agent.
  2. In a task, call shell_exec with block=false on a long-running command (e.g. python -m http.server). A broad_process mutation intent is created with status prepared.
  3. Before that command exits, stop/cancel the task from the desktop UI (or let the turn fail). The Run is terminalized with terminal_detail = "Run background Terminal sessions did not stop before Git finalization: <id>", and mark_broad_write_needs_attention() quarantines the intent (intent → needs_attention, change set → needs_attention).
  4. Start a brand new task in the same Space and call any mutating tool. → Refused: Workspace operation is still active or needs attention.
  5. Quit and restart the desktop app entirely. → Still refused.

Variant that leaves prepared instead of needs_attention (same effect): make an exact_path write fail after the intent row is created — e.g. change the worktree from a second process between prepare and checkpoint, which raises RepositoryStateChangedError (content.py:423-432). No code path collects the intent afterwards; it simply stays prepared.

Why it cannot be recovered (five dead ends)

AttemptWhy it failsCode
Restart the appstartup reconciliation deliberately scans only statuses=("prepared",) and additionally requires the change set to be open; the docstring states quarantine is intentional ("One ambiguous ChangeSet is quarantined without blocking reconciliation of others")workspace_git/mutation.py:1519-1525
Finalize / end the offending Runfinalization returns deferred_mutation as soon as it sees a prepared/needs_attention intent, and it only advances open change sets; it never touches intent statusworkspace_git/lifecycle.py:183-209
Reviewed re-capture (retry_broad_write_checkpoint)it is the only code that flips needs_attention back, but its first statement requires a path-budget overflow receipt under <state_root>/path-budget/<digest>.json. Such a receipt is written only by the post-dispatch budget-overflow branch, so a non-budget failure never produces one → ContentRepositoryError("No path-budget overflow receipt is available"). It also requires a live in-process PreparedWorkspaceExecution, which a new task cannot obtainmutation.py:766-782, mutation.py:829-846, agent/toolkit/terminal_toolkit.py:278
Discard itthe journal state machine allows needs_attention → {open, discarded}, but no code in the application ever produces discarded (grep for state="discarded" → 0 hits)run_journal/store.py:7584-7587
Start a new taskthe fence matches on (repository_id, worktree_ref), and worktree_ref is the binding's target_ref — identical for every task in the Space. The comment says it explicitly: "A Run lease admits a task, not every operation within it. This transaction also fences separate service instances."run_journal/store.py:7807-7824; mutation.py:1331; mutation.py:1123, :1185 (exclusive_worktree=True for all writes)

There is also no UI affordance: the renderer never calls POST /spaces/{space_id}/git/checkpoints (the only route accepting an explicit paths list), and POST /git/save-point commits only pending_managed_paths, which is empty here, so it raises NoCheckpointChangesError. Yet the machine-readable refusal tells the agent to Review existing content and checkpoint explicitly selected batches of at most 500 paths (workspace_git/backend.py:91-97) — an instruction that cannot be carried out.

log

Refusal raised by the fence (run_journal/store.py:7810-7824):

SELECT intents.intent_id FROM git_mutation_intents intents
JOIN git_change_sets active USING (change_set_id)
JOIN git_change_sets target ON target.change_set_id = ?
WHERE active.repository_id = target.repository_id
  AND active.worktree_ref  = target.worktree_ref
  AND intents.status IN ('prepared', 'needs_attention')
LIMIT 1
→ raise OutboxLeaseLostError("Workspace operation is still active or needs attention")

Run row of the Run that created the orphan (values from our journal):

run_id           1793076977815-7897
status           failed
terminal_reason  error
terminal_detail  Run background Terminal sessions did not stop before Git finalization: t6

Change set / intent left behind:

changeset_fc1583809…  state=needs_attention  base_commit=f6623d4…  run=1793076977815-7897
gitintent_d90b06bb…   status=needs_attention  trigger=terminal.execute  scope=broad_process

The tool call that never got captured:

tool_calls: shell_exec  status=outcome_unknown  safety_class=unsafe_write

Backend log lines (desktop main.log, timestamps local):

16:27:22  shell_write_content_to_file → Repository changed before checkpoint; refresh status
16:13:59  write_to_file               → Terminal/ file write was not started: Workspace operation is
          still active or needs attention

(The log excerpts that matter are quoted above. The full desktop log is available on request; we are not attaching it by default because it contains local paths and model endpoints, and we will not upload the journal file at all.)

Additional context

Suspected cause and suggested modifications

The state machine has an entry transition with no exit. Three possible fixes, in order of invasiveness:

  1. Implement the declared disposal. Expose an explicit "abandon this workspace operation" action (a controller route + a button in the Space's version-history panel) that performs the already-allowed transition needs_attention → discarded on the change set and closes the intent, without ever touching the user's files. This is the missing producer of a state the schema already admits.

  2. Let startup reconciliation handle quarantined intents. reconcile_startup() currently scans only statuses=("prepared",) (mutation.py:1519-1525). Consider a reviewed path for needs_attention rows — reusing retry_broad_write_checkpoint's semantics but without the receipt precondition, since a receipt is only produced by the post-dispatch budget branch.

  3. Don't let the fence be cross-task without an exit. If cross-service fencing is required (store.py:7808-7809), pair it with a bounded lease/expiry for prepared intents, so a dead Run cannot block the checkout forever.

Diagnostics should name the blocker

The refusal text is currently identical to an unrelated condition and carries an actionable-but-false hint:

  • OutboxLeaseLostError is reused both for lost terminal/outbox leases and for this intent fence; terminal_toolkit.py:1138-1141 appends "stop the active background Terminal session" to any instance of that error class, even when no session exists. Suggest a dedicated error class for the fence and attaching the blocking intent_id (+ its Run id and status) to the message, e.g. blocked_by_intent=gitintent_d90b06bb… (status=needs_attention, run=…, trigger=terminal.execute) — we only found it by opening the SQLite journal with a read-only client.

Severity

Total loss of the write surface for a Git Space, not recoverable by the user through any documented action, and it silently affects every new task in the Space. We had to hand-edit ~/.eigent/run-journal.sqlite3 (a guarded single-row UPDATE git_mutation_intents SET status='completed' WHERE intent_id=? AND status='needs_attention') after verifying via git status that the abandoned operation had left nothing uncaptured. That is not a workaround we would recommend to anyone, and the data-loss risk of alternative workarounds (deleting and re-initialising the Space's repository binding) is worse.

Disclosure

Our installed copy has exactly two modified files relative to tag v1.0.6, neither of which is cited above: backend/app/tool_validation.py (a pre-dispatch quoted-scalar coercion patch) and backend/app/agent/listen_chat_agent.py. All files cited in this report were verified byte-identical to tag v1.0.6 before filing.

Activity

0

No activity yet.