Skip to content

Fix recovery-copy UX and guard agent writes to session data / 修复冲突副本体验并防护 agent 直写会话数据#6123

Merged
SivanCola merged 5 commits into
esengine:main-v2from
SivanCola:fix/recovery-copy-ux-and-session-data-guard
Jul 6, 2026
Merged

Fix recovery-copy UX and guard agent writes to session data / 修复冲突副本体验并防护 agent 直写会话数据#6123
SivanCola merged 5 commits into
esengine:main-v2from
SivanCola:fix/recovery-copy-ux-and-session-data-guard

Conversation

@SivanCola

@SivanCola SivanCola commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two user-reported problem clusters from v1.17 screenshots, fixed together because they share the same failure surface (save-conflict recovery):

Recovery-copy UX

  • agent:ready racing the initial hydrate could leave a stale ready=false tab meta: the composer stayed locked on a recovered session even though the backend was ready, until an unrelated settings reload happened to re-emit ready. The ready handler now re-syncs the active tab metadata from ListTabs first (cached history preserved), so the send gate opens as soon as the backend is ready.
  • Recovery notices no longer leak internal recovery-branch ids (...saved as recovery branch 20260706-...-recovery-...). The backend emits neutral wording, and localizedBackendNoticeText maps both the new and the legacy English wording to localized user-facing text, so already-persisted transcripts render friendly too (zh/zh-TW/en).
  • The "conflict copy" banner no longer builds a tooltip out of digest/parent id internals; the now-unused recovery.reason/digest/parent locale keys are removed.
  • The two new regressions are added to the frontend test/test:all explicit lists so CI actually runs them.

Session-data write guard (root cause of a token-burn loop)

  • A user in Yolo mode asked the agent to "fix Reasonix" from inside Reasonix; the agent bulk-edited the app's own session JSONL/index files via bash/Python. The runtime is the only writer of those stores (CAS ledger + autosave), so external edits race its saves: conflict copies fork repeatedly, and each app save makes the agent's write look like it "did not take", so the model retries in a loop (observed: 3.5M+ tokens in one turn).
  • New SessionDataGuard: file-writer built-ins (write_file/edit_file/multi_edit/move_file/notebook_edit/delete_range/delete_symbol) refuse targets under <state root>/sessions and <state root>/projects/*/sessions even when workspace write roots cover them (e.g. a home-directory workspace). Explicit [sandbox] allow_write entries remain a sanctioned escape hatch.
  • bash appends a warning to the output of commands that reference those trees — it never blocks (diagnostic reads are legitimate; a static read/write distinction over shell text is not decidable), but the warning names the conflict-copy mechanism so the model stops treating app overwrites as its own failed writes.
  • Wired at both composition roots (boot addBuiltins for CLI/desktop, acpBuiltinTools for ACP). The zero-value guard is unconfined, so init-registered template tools behave exactly as before.
  • Same-shape follow-up (second commit): deny-side checks fold case on Windows/macOS — EvalSymlinks keeps the caller's spelling while default NTFS/APFS volumes are case-insensitive, so ~/.reasonix/SESSIONS/... reached the real store past the guard (repro'd; same shape as the Fix Windows desktop session lease self-lock on model switch / 修复 Windows 桌面端切换模型会话租约自锁 #6023 lease-key case split). forbid_read (confineRead) gets the same fold; allow-side confine keeps exact matching so a case-sensitive filesystem cannot be waved through. State-root-direct runtime ledgers (desktop-*.json incl. the fixed .tmp sibling, metrics-pending.json, crash-pending.json) join the guard — the app rewrites them wholesale, so agent edits vanish the same way (config.toml/credentials/skills stay writable on request). reasonix review's template-built bash now carries the guard too.
  • Review follow-up (fourth commit): rebased onto latest main-v2 (Move the persisted-rewrite baseline into agent.Session / 将 rewrite 持久化基线内聚进 Session #6125/Migrate diagnostic sidecars through overlong-name reconciliation / 超长名整理时迁移诊断 sidecar #6126 refactored recoverSnapshotConflict into outcome enums; the sanitized notice wording now rides on the new flow), and heartbeat-tasks.json is back to agent-editable — it is a documented human/AI-editable contract (desktop/heartbeat.go header, heartbeat panel tip), so the guard dropping it broke an advertised flow; a positive regression now pins that contract.

Validation

  • go test ./... (root, all green) and go vet on touched packages
  • cd desktop && go test ./...
  • ./scripts/cache-guard.sh (tail_avg 95 >= threshold 90, pass)
  • Focused: go test ./internal/tool/builtin -run 'TestSessionDataGuard|TestWriteToolsRejectSessionData|TestBashAppendsSessionDataHint|TestConfineRead' (incl. case-variant bypass regressions, verified on a case-insensitive volume)
  • Frontend: pnpm typecheck, pnpm test:typecheck, full pnpm test (new ready-event-meta-sync and recovery-banner-privacy pass in the wired list)

Cache-impact: low - no tool schema/name/description/order bytes change (guard state lives in unexported struct fields; bash warnings ride in tool results, not the stable prefix); backend notice wording changes are event text, not provider-visible prompt.
Cache-guard: scripts/cache-guard.sh pass (tail_avg 95/threshold 90) plus scripts/check-cache-impact.sh with this body.
System-prompt-review: internal/config/config.go only gains the AllowWriteRoots helper; DefaultSystemPrompt/LanguagePolicy text untouched; boot.go changes are tool assembly only, sysPrompt construction unchanged.

@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) tui Terminal UI / CLI (internal/cli, internal/control) skills Skill system (internal/skill, internal/tool) agent Core agent loop (internal/agent, internal/control) config Configuration & setup (internal/config) labels Jul 6, 2026
merge-order-check added 4 commits July 7, 2026 03:35
Recovery-copy fixes (user-reported screenshots):
- agent:ready could reuse an in-flight hydrate and keep a stale
  ready=false tab meta, locking the composer until an unrelated
  settings reload re-emitted ready. The ready handler now refreshes
  the active tab metadata from ListTabs first (preserving cached
  history), so the send gate opens as soon as the backend is ready.
- Recovery notices no longer expose internal recovery-branch ids;
  the frontend maps both the new and the legacy English notice
  wording to localized, user-facing text so old transcripts render
  friendly too. The recovery banner no longer builds a tooltip from
  digest/parent-id internals, and the now-dead locale keys are gone.

Session-data write guard (root cause of the token-burn loop):
- An agent asked to "fix Reasonix" from inside Reasonix would write
  the app's own session JSONL/meta files via bash or file tools. The
  runtime is the only writer of those stores; external edits race its
  autosaves, fork endless conflict copies, and look to the agent like
  failed writes, so it retries in a loop.
- New SessionDataGuard rejects writer-tool targets under
  <state root>/sessions and <state root>/projects/*/sessions even when
  workspace write roots cover them (e.g. a home-directory workspace);
  explicit [sandbox] allow_write entries remain a sanctioned escape
  hatch. bash appends a warning when a command references those trees
  (never blocks: diagnostic reads are legitimate) so the retry loop is
  broken at the model level.
- Wired through ConfineWriters/ConfineBash/Workspace at the boot and
  ACP composition roots; zero-value guard keeps init-registered tools
  unconfined as before.
Same-shape sweep after the initial guard landed:

- Case-variant bypass (confirmed by repro): EvalSymlinks does not
  normalize case, and default macOS/Windows volumes are
  case-insensitive, so ~/.reasonix/SESSIONS/x.jsonl reached the real
  store while sliding past the deny check — the same failure shape as
  the Windows lease-key case split (esengine#6023). Deny-side checks (the
  guard and forbid_read's confineRead) now fold case on those
  platforms; allow-side confine keeps exact matching, since folding an
  allow rule on a case-sensitive filesystem would wave a genuinely
  different directory through.
- State-root-direct runtime ledgers (desktop-tabs.json and friends,
  heartbeat-tasks.json, metrics/crash-pending.json) are rewritten
  wholesale by the app, so agent edits vanish the same way session
  edits do — the "index files" from the user report. They join the
  guard; config.toml/credentials/skills stay writable on request.
- reasonix review built its subagent bash from the init template,
  skipping the guard; it now carries the same session-data warning.
Follow-up sweep on the remaining lexical gaps of the session-data
bash warning (write tools were never affected — relative paths get
absolutized before the hard deny):

- $HOME/${HOME} and %APPDATA%/$env:APPDATA spellings of the guarded
  trees now match, alongside the existing absolute and ~/ forms.
- The hint is workDir-aware: the desktop Global workspace lives at
  <state root>/global-workspace, so ../sessions reached the stores
  without an absolute path in the command text. Relative forms are
  matched from a state-root workDir, and a workDir already inside a
  guarded store warns on every command.
- workDir goes through realPath so a symlinked cwd (or /var vs
  /private/var on macOS) cannot dodge the workDir checks.

Remaining lexical limits (paths hidden in script files, runtime
variable concatenation) are inherent to text matching; the writer-tool
hard deny and the conflict-copy CAS remain the backstops.
Review finding: heartbeat-tasks.json is a documented human/AI-editable
contract (desktop/heartbeat.go header; the heartbeat panel tip says
"AI agents can also edit heartbeat-tasks.json"), so the runtime-ledger
guard must not hard-refuse it — that broke the advertised flow of
asking the agent to create or update heartbeat tasks. Drop it from the
deny list and the bash hint needles, and pin the contract with a
positive regression so the guard can never swallow it again.
@SivanCola SivanCola force-pushed the fix/recovery-copy-ux-and-session-data-guard branch from 32e4abe to 4b00b17 Compare July 6, 2026 19:46
@SivanCola SivanCola marked this pull request as ready for review July 6, 2026 23:02
@SivanCola SivanCola requested a review from esengine as a code owner July 6, 2026 23:02
@SivanCola SivanCola merged commit c92b7b5 into esengine:main-v2 Jul 6, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) config Configuration & setup (internal/config) desktop Wails desktop app (desktop/**) skills Skill system (internal/skill, internal/tool) tui Terminal UI / CLI (internal/cli, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant