Fix recovery-copy UX and guard agent writes to session data / 修复冲突副本体验并防护 agent 直写会话数据#6123
Merged
SivanCola merged 5 commits intoJul 6, 2026
Conversation
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.
32e4abe to
4b00b17
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:readyracing the initial hydrate could leave a staleready=falsetab 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 fromListTabsfirst (cached history preserved), so the send gate opens as soon as the backend is ready....saved as recovery branch 20260706-...-recovery-...). The backend emits neutral wording, andlocalizedBackendNoticeTextmaps both the new and the legacy English wording to localized user-facing text, so already-persisted transcripts render friendly too (zh/zh-TW/en).digest/parent idinternals; the now-unusedrecovery.reason/digest/parentlocale keys are removed.test/test:allexplicit lists so CI actually runs them.Session-data write guard (root cause of a token-burn loop)
SessionDataGuard: file-writer built-ins (write_file/edit_file/multi_edit/move_file/notebook_edit/delete_range/delete_symbol) refuse targets under<state root>/sessionsand<state root>/projects/*/sessionseven when workspace write roots cover them (e.g. a home-directory workspace). Explicit[sandbox] allow_writeentries remain a sanctioned escape hatch.bashappends 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.addBuiltinsfor CLI/desktop,acpBuiltinToolsfor ACP). The zero-value guard is unconfined, so init-registered template tools behave exactly as before.EvalSymlinkskeeps 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-sideconfinekeeps exact matching so a case-sensitive filesystem cannot be waved through. State-root-direct runtime ledgers (desktop-*.jsonincl. the fixed.tmpsibling,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.main-v2(Move the persisted-rewrite baseline into agent.Session / 将 rewrite 持久化基线内聚进 Session #6125/Migrate diagnostic sidecars through overlong-name reconciliation / 超长名整理时迁移诊断 sidecar #6126 refactoredrecoverSnapshotConflictinto outcome enums; the sanitized notice wording now rides on the new flow), andheartbeat-tasks.jsonis 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) andgo veton touched packagescd desktop && go test ./..../scripts/cache-guard.sh(tail_avg 95 >= threshold 90, pass)go test ./internal/tool/builtin -run 'TestSessionDataGuard|TestWriteToolsRejectSessionData|TestBashAppendsSessionDataHint|TestConfineRead'(incl. case-variant bypass regressions, verified on a case-insensitive volume)pnpm typecheck,pnpm test:typecheck, fullpnpm test(newready-event-meta-syncandrecovery-banner-privacypass 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.