Commit Graph
80 Commits
Author SHA1 Message Date
YHH 4fbbca6a99 refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop

Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.

Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.

CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.

* build(lint): gate the modern-idiom linters

Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.

omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
2026-08-07 15:30:33 +08:00
YHH 93243ab14b feat(agent): store-P2 write-authority probe for unleased session saves (#7532)
Observe-first increment toward lease-holder-is-single-writer: one deduped structured warning per unleased save path; no behavior change. Store-P2 groundwork for the snapshot-conflict cluster.
2026-08-05 01:03:56 +00:00
YHH 0f5a900c45 fix(session): drop leading @file refs from the session preview (#7464)
Prompts that open with @path references produced a preview — and so a
sidebar title — made entirely of paths, with the actual request pushed
past the 80-rune clamp. Skip the leading reference run; a prompt that is
nothing but references keeps them.
2026-08-04 19:59:01 +08:00
SivanCola 41cfc15fe2 Harden CLI recovery trash publication
Problem:
CLI recovery GC exposed a Desktop trash entry before its authoritative event log and sidecars had moved. A crash, restore, or purge in that window could strand a split artifact set. Numeric /resume also ran GC after completion had assigned an index, allowing the index to shift before execution.

Root cause:
The cleanup protocol moved the primary transcript before publishing its durable operation marker, and the numbered resume path mutated the same ordered list it was about to resolve.

Fix:
Stage the transcript and all owned artifacts in a hidden, durably marked directory, reconcile interrupted stages on startup, and publish the complete trash directory with one same-filesystem rename. Retain compatibility for the earlier recovery-trash marker protocol. Keep numbered resume resolution read-only and update its ordering documentation.

Verification:
- go test ./internal/agent ./internal/cli
- go test -race ./internal/agent ./internal/cli
- go vet ./...
- go test ./...
- cd desktop && go test ./...
2026-08-04 14:55:18 +08:00
SivanCola 0d41d616b3 Fix CLI recovery session hygiene
Problem:
CLI conflict-recovery copies can accumulate in resume lists, where they are indistinguishable from the original conversation and may be selected arbitrarily.

Root cause:
The CLI flattened session metadata, ignored recovery lineage, and never invoked the conservative recovery cleanup used by Desktop.

Fix:
Group recovery families in CLI resume surfaces, label copies with their parent, prefer the newest visible leaf, and move only covered, untouched, idle, unleased copies into the compatible session trash. Hold parent and branch removal guards across final revalidation and make interrupted trash moves durable.

Verification:
- go test ./...
- go vet ./...
- focused go test -race for agent recovery and CLI resume ordering
- ./scripts/cache-guard.sh
- Desktop recovery/session focused tests
2026-08-03 23:46:52 +08:00
SivanCola 1e4e8b5d94 Merge main-v2 into PR 6971 hardening branch
Problem:
- PR 6971 had fallen behind main-v2 and conflicted with the new Kimi K3 preset helper.

Root cause:
- Both branches added provider override helpers at the same insertion point.

Fix:
- Merge current main-v2 and retain both the Kimi K3 and Qwen per-model overrides without rewriting contributor history.

Verification:
- go test ./internal/agent ./internal/cli ./internal/config
- ./scripts/cache-guard.sh
2026-07-31 00:04:14 +08:00
SivanCola 952200ac1d fix(session): harden PR 6971 recovery contracts
Problem:
- Same-revision snapshot recovery could overwrite an interrupted foreign write.
- Paste recovery mutated loaded history, reused labels after resume, and trusted assistant text.
- Mixed Qwen catalogs inherited a provider-wide 1M context budget.

Root cause:
- Revision equality was treated as ownership without checking the persisted digest.
- Recovery lived in the persistence layer while paste IDs were process-local.
- Qwen context metadata was configured only at provider scope.

Fix:
- Require persisted-state ownership for non-prefix snapshots and preserve foreign bytes.
- Keep loaded history byte-stable, recover labels only on explicit resubmission, and continue IDs from history.
- Add per-model context overrides for smaller Qwen catalog models.

Verification:
- go test ./internal/agent ./internal/cli ./internal/config
- go test -race ./internal/agent -run snapshot recovery cases
- ./scripts/cache-guard.sh
- full repository suite passed except an opt-in live probe using an expired DEEPSEEK_API_KEY; the package passed with that variable unset

Co-authored-by: SivanCola <[email protected]>
2026-07-31 00:01:46 +08:00
32721fc0e1 feat(context): introduce Context Engine v2 / 引入上下文引擎 v2 (#7001)
* feat(memory): separate standing instructions from scoped facts

* fix(memory): preserve scoped memory compatibility

Problem:
Omitted-scope updates could move legacy global memories into the current project, and global preferences or feedback were unavailable on the first turn. Mixed-version clients could also route independently scoped facts through the wrong directory.

Root cause:
Save normalized an empty scope before consulting the active file, the stable prompt only contained the memory index, and previous releases treated metadata.type as the routing signal.

Fix:
Preserve the effective scope of same-name updates, snapshot global user and feedback bodies into deterministic lower-priority guidance, retain a previous-release routing type alongside authoritative fact_type metadata, and queue complete save/revocation notes for the current session.

Verification:
./scripts/cache-guard.sh
go vet ./...
env -u DEEPSEEK_API_KEY go test ./...
(cd desktop) go test ./...
(cd desktop/frontend) pnpm test
(cd desktop/frontend) pnpm build
gofmt -l .
git diff --check

* refactor(context): separate raw and provider user input

* refactor(instructions): resolve scoped context with provenance

* refactor(memory): add versioned fact storage

Context: Memory facts were addressed by mutable slugs, trusted a separately maintained index, and had no durable update history or conflict protection.

Change: Add stable IDs, monotonic revisions, atomic writes, derived indexes, deterministic legacy migration, scoped identity handling, revision restore, and optimistic update checks. Surface the metadata through tools and Desktop while preserving legacy frontmatter routing and Safe Mode behavior.

Compatibility: Existing files remain readable, gain deterministic runtime metadata before migration, and keep metadata.type/fact_type/scope values that previous releases understand. Added Desktop JSON fields are optional.

Verification: go test ./internal/memory; go test -race ./internal/memory; go test ./internal/boot -run Test.*Memory; go test ./... with provider credentials isolated; desktop go test ./...; frontend pnpm test and typecheck; go vet ./...; ./scripts/cache-guard.sh.

* feat(memory): recall relevant facts per user turn

Problem:
Saved facts were only visible through a stable index and required the model or user to invoke memory search manually.

Design:
Add conservative host-side BM25 recall for real user turns with project-over-global deduplication, freshness labels, explainable scoring, strict character limits, and local-home redaction. Keep the system prompt and tool schemas stable by appending recall only to the provider user-turn tail, and exclude synthetic turns and already-loaded global guidance.

Compatibility:
Preserve Store.List behavior and add ListAll for scope-aware consumers. No persisted memory or session fields change; display fallbacks strip the new transient suffix.

Verification:
go test -count=1 ./internal/memory ./internal/agent ./internal/control
go test -race -count=1 ./internal/memory
go test -race -count=1 ./internal/control with focused recall tests
go test -count=1 ./internal/tool/builtin/ ./internal/boot/
go vet ./...
./scripts/cache-guard.sh

* feat(memory): auto-save low-risk project facts

Problem:
Every remember call required a fresh approval, including bounded project facts that were new, non-sensitive, and low impact. This made durable context expensive enough that agents and users often skipped it.

Design:
Classify interactive remember requests against the scoped store. Automatically allow only explicit project/reference creates that are bounded, non-sensitive, and not semantic duplicates. Keep global, user/feedback, sensitive, and overwrite requests on the existing fresh-human path; keep headless and sub-agent mutation fail closed.

Safety:
Issue one-shot hashed grants and enforce create-only again inside the serialized Store write, preventing an approval-to-write race from becoming an overwrite. Clear abandoned grants at turn completion. Expose revision history and audited restore through the controller.

Compatibility:
The remember description and schema are unchanged. RequireCreate is a runtime-only SaveOptions field; persisted memory and session formats are unchanged.

Verification:
DEEPSEEK_API_KEY= go test -count=1 ./...
go test -count=1 ./... in desktop
go test -race -count=1 ./internal/memory ./internal/control with memory/approval tests
go vet ./...
./scripts/cache-guard.sh

* feat(memory): recover archived facts safely

Problem: Archived facts remained inspectable on disk but users could not recover them through Reasonix, and ad hoc restoration could overwrite active memory or lose revision provenance.

Fix: Add store-validated archive recovery with collision checks, exclusive active-file creation, monotonic revisions, revision snapshots, controller turn-tail refresh, and a Desktop restore action.

Compatibility: Existing active and archived Markdown files remain readable; recovery writes the established v2 frontmatter and hidden revision layout, while older binaries continue to read the restored active fact.

Verification: go test ./internal/memory ./internal/control; go test ./... in desktop; pnpm test; pnpm exec tsc --noEmit; ./scripts/cache-guard.sh.

* feat(context): expose memory recall diagnostics

Expose scoped fact freshness, project-over-global conflict decisions, and the last automatic recall trace through the desktop contract. Add revision history and restore APIs while preserving non-null collection responses and remote-workspace isolation.

This gives Context Center enough provenance to explain what was available, selected, suppressed, or restored without changing the stable prompt prefix.

* feat(context): add provenance center to desktop

Make scoped memories explainable in the desktop settings surface. Show stable identity, freshness, revision history, project-over-global resolution, automatic recall traces, instruction precedence, imports, and diagnostics.

The suggestions workflow remains compatible while the Context Center gains zero-impact read paths and explicit restore actions for user-controlled recovery.

* feat(memory): make suggestions zero-config

Open the suggestions surface with automatic local-history scanning by default, removing the hidden localStorage setup switch. Keep every candidate behind explicit confirmation.

Deduplicate against all scoped active facts and resolved instruction bodies so project and global memories cannot produce repeated guidance.

* feat(memory): add CLI diagnostics and recovery

Context:
Context Engine v2 exposed revision, recall, and instruction provenance in the backend and desktop, but the CLI only listed legacy merged facts and archives.

Changes:
- add shared /memory diagnostics for scoped facts, recall traces, instruction precedence, revisions, and archives
- add explicit revision restore and archive recovery commands with structured completion across CLI, desktop, and remote workspaces
- preserve same-name facts across scopes and keep narrow TUI output bounded

Verification:
- go test ./internal/control ./internal/cli ./internal/remote/workbench/runtime ./internal/memory -count=1
- go test -race ./internal/control ./internal/memory
- cd desktop && go test . -count=1
- ./scripts/cache-guard.sh

Cache-impact: management diagnostics only; provider-visible prompt and tool schemas are unchanged.

* docs(context): document instructions and memory v2

Context:
Context Engine v2 changed instruction precedence, automatic recall, safe memory writes, revision recovery, and desktop suggestions, while the user docs still described the legacy approval-only retrieval model.

Changes:
- rewrite the context and memory contract around standing instructions versus background facts
- document recall freshness, project-over-global resolution, zero-configuration writes and suggestions, revision/archive recovery, cache stability, privacy, and remote fail-closed behavior
- add a complete Chinese companion and update Guide, CLI, migration, spec, approval, and README navigation in both languages

Verification:
- validated relative Markdown links, code fences, and final newlines across 14 changed files
- scanned for stale approval/suggestion claims
- git diff --cached --check
- scanned the staged diff for secrets and machine-local paths

* fix(context): preserve safe headless memory writes

Allow controller-scoped low-risk project/reference creates to pass the non-interactive approval gate while keeping deny rules and all higher-risk memory operations fail-closed. Reject archive recovery through symlinked files or archive directories, with end-to-end regressions for both boundaries.

* fix(memory): reject non-directory migration roots

* fix(context): make instruction and memory references safe

Problem:
Provider-visible instruction paths exposed machine-local roots, memory results exposed store paths, and listed scoped references could not be used directly for read, update, revision, or archive operations.

Root cause:
Instruction rendering used absolute source paths and memory lookup treated display names as unqualified slugs. Symlink reads also crossed boundaries through a path-resolution gap, while scope-aware listing deduplicated same-name facts.

Fix:
Render stable workspace/user provenance, confine instruction and import reads to their owning roots, reject external symlinks, and support project/<name>.md and global/<name>.md references end to end. Preserve unqualified compatibility, stable-ID renames, revision snapshots, exact-scope archive behavior, low-risk create-only approval boundaries, and bilingual documentation.

Verification:
Focused instruction/memory tests and race tests; go test -count=1 ./...; go test -race ./...; go vet ./...; desktop go test/race/vet; pnpm test:all; pnpm build; go run ./cmd/remote-protocol-gen --check; ./scripts/cache-guard.sh (8/8 pass, long tool loop tail average 95%); staged privacy scan clean.

* docs(contributors): attribute adopted context work

Attribution:
Record the authors whose prior pull request mechanisms were adapted by the Context Engine v2 integration.

- cyq1017: feedback injection from #5710 and Markdown input normalization from #5603
- salarkhannn: Markdown input normalization from #5604

Scope:
This metadata-only commit adds repository-level co-author attribution without changing runtime behavior.

Verification:
Confirmed source PR ownership and both public GitHub noreply identities.

Co-authored-by: cyq <[email protected]>
Co-authored-by: Salar <[email protected]>

* test(desktop): allow slow Windows git metadata probe

Problem:
The Windows Desktop CI job failed when MetaForTab's asynchronous first git probe did not finish within the test's one-second deadline.

Root cause:
The production path intentionally launches the cold git branch lookup in the background so tab metadata never blocks. Windows runner process startup can exceed one second under normal CI load, making the test timing assumption platform-fragile.

Fix:
Keep the non-blocking metadata implementation unchanged and extend the cross-platform test observation window to five seconds, with a comment documenting the Windows process-startup variance.

Verification:
- GOCACHE=/tmp/reasonix-desktop-go-cache go test -count=1 -run 'TestMetaForTabIncludesWorkspaceContext|TestWorkspaceGitBranch' ./desktop
- git diff --check

---------

Co-authored-by: cyq <[email protected]>
Co-authored-by: Salar <[email protected]>
2026-07-29 02:27:29 +08:00
Yan Li 7ee229e0d8 fix(agent): repair orphaned paste labels on session load
When a session is resumed or its history replayed, orphaned
[Pasted text #N] labels that were never expanded (because
pastedBlocks was lost during a prior reload) are stored raw in
the transcript.  The model receives an opaque token it cannot
interpret.

repairOrphanedPasteLabels runs during loadSessionUnlocked:
  1. Scans the same transcript for prior expansions (--- Begin/End ---)
  2. Inlines recovered content into orphaned labels
  3. Replaces unrecoverable labels with [pasted content unavailable]

This cleans historical pollution at load time, complementing the
TUI-layer fix (chat_tui_paste.go) that prevents new orphaned
labels on submit.

Ref: #6948
2026-07-28 09:10:35 +08:00
Yan Li 9f801723d9 fix(agent): skip byte-level prefix comparison when revision CAS proves same-runtime ownership
When baseState.revision == currentRevision the revision ledger proves
no external writer has touched the transcript.  In that case the
byte-level messagesHavePrefix comparison in checkSnapshotWrite is
redundant — the revision CAS already vouches for consistency — and its
false positives (normalisation / JSON round-trip / local-only metadata)
drive spurious diverged conflicts that cascade into recovery-branch
chains and force-saves that break the prefix-cache.

Future-tense (Delta): anchor on the revision CAS match, not the
byte-level disk snapshot (Gamma).  Same insight as Agda PR #8611
(makeTau's nTarget = nOld + nctel - 1).

Ref: #6027 Phase 3, #6948
2026-07-26 12:49:11 +08:00
SivanColaanderphenheimer a6e1459621 Fix Desktop session runtime ownership / 修复桌面会话运行时所有权 (#6897)
* fix(desktop): canonicalize runtime process identity

Problem: executable-based single-instance IDs let stable, canary, and portable builds concurrently write the same Reasonix home, while path aliases could split session lease identity.

Root cause: process ownership was keyed by binary location and fresh paths resolved only their immediate parent.

Fix: hash the canonical REASONIX_HOME and resolve every missing path component through the nearest existing symlink or junction ancestor, preserving isolated homes and dev-mode parallelism.

Verification: Go agent tests, Desktop tests, Windows cross-compilation, and canonical-path race tests pass.

* fix(desktop): unify writable session runtime ownership

Problem: Goal and Delivery mode rebuilds, duplicate tab restore, background reattachment, and saved-session navigation could build a second controller for one session or tear down the live source before a target was proven usable.

Root cause: controller startup, lease ownership, deferred retries, detached-session attachment, and path switching were coordinated per WorkspaceTab without a canonical process-wide admission record or one atomic controller/lease/path commit.

Fix: add a SessionRuntime registry keyed by runtime ID and canonical session key; attach or wait on existing local runtimes; safely reclaim only orphaned current-process leases; and make saved-session switches build, restore, validate, and lease a candidate before atomically publishing it. Target failures preserve the source controller, lease, path, mode profile, and epoch.

Verification: full Desktop and repository tests, Desktop/Agent/Controller/lease race suites, all 18 collaboration-token-mode orderings, deterministic A/B/A navigation fencing, frontend tests/build, Wails bindings, and a production Desktop build pass.

Co-authored-by: erphenheimer <[email protected]>

* fix(frontend): fence sends and events by runtime state

Problem: the composer could append an optimistic user bubble while a workspace was starting or lease-blocked, and late events from a replaced controller could mutate the new transcript.

Root cause: frontend readiness used only legacy booleans and controller events had no process-runtime generation fence.

Fix: expose structured runtime phase and issue metadata, gate every submit path before optimistic dispatch, seed and advance per-tab runtime epochs, and ignore events from obsolete epochs while retaining legacy event compatibility.

Verification: the full frontend suite, focused send-failure tests, TypeScript checking, and the production Vite build pass.

* docs: record desktop session ownership fix

Problem: the user-visible impact and compatibility boundary of the Desktop self-lock fix were not documented.

Fix: describe single-runtime session ownership, epoch-fenced events, readiness-gated sends, and REASONIX_HOME-scoped single-instance behavior in the Unreleased changelog.

Verification: Markdown diff inspection and repository privacy scan pass.

* test(desktop): align CI fixtures with runtime ownership

Problem: CI found an unused private runtime helper and a Linux timing path where a Heartbeat test injected Ctrl plus Ready without publishing the matching SessionRuntime state.

Fix: remove the unused helper and advance the injected test runtime exactly as the production controller publication path does; production readiness checks remain strict.

Verification: the Heartbeat submission and pending-prompt tests pass for 20 repetitions and for 5 race-enabled repetitions, and go vet passes.

* fix(desktop): wait for starting session runtimes

Problem: two persisted tabs restoring the same session could attach the first tab's starting placeholder before its controller was published, retire both candidate builds, and leave the visible session permanently starting.

Root cause: startup called the general attach path before claimSessionRuntime classified the registered phase, and applyRuntimeTab could create a second registry record when adopting a legacy runtime into a target that already owned a placeholder.

Fix: make claimSessionRuntime the startup admission entry, refuse attachments from non-ready runtime owners, reuse the target placeholder when adopting compatible visible or detached runtimes, and cover the starting-to-ready duplicate-tab handoff.

Verification: focused runtime tests passed for 10 repetitions; the runtime/mode/navigation race matrix passed for 3 repetitions; Desktop go test ./..., go test -race ., go vet ./..., go build ./..., and git diff --check passed.

---------

Co-authored-by: erphenheimer <[email protected]>
2026-07-24 16:13:54 +08:00
SivanCola 5480019c4f feat(agent): evolve Auto mode with Auto Guard / 以 Auto Guard 强化自动模式 (#6732)
* feat(agent): add Auto-mode failure recovery checkpoint / 为自动模式增加失败恢复检查点

Problem:
In Auto mode, after a tool or verification failure the agent could silently
change strategy, expand write scope, or raise risk without a user decision.

Root cause:
There was only the Ask/Auto/YOLO permission axis and plan confirmation. No
host-side checkpoint blocked unconfirmed recovery mutations after failures.

Fix:
- Add a shared per-task recovery state machine (idle/diagnosing/awaiting/
  approved_once) consulted before permission and write-lock acquisition.
- Deterministic rules auto-continue same-strategy verification retries and
  force confirm for high-risk, scope, strategy, or consecutive failures.
- Independent recovery reviewer with isolated policy/session; fail closed.
- Fresh recovery approval cards (continue/revise/stop); never session-drain.
- Config/session defaults: new sessions on, pre-upgrade sessions off.
- Desktop composer toggle + settings default; Bot structured recovery UX;
  content-free recovery metrics; reviewer/gate sidecars for cold restart.
- Docs: English + Chinese tool-approval modes.

Verification:
- go test ./internal/recovery/ ./internal/control/ ./internal/bot/ -run
  'TestRecovery|TestCache|Approval' -count=1
- Cache-shape test: success-path system/tools hashes unchanged with gate on
- desktop package compiles (go test -c)

Cache-impact: low — success path keeps stable system prompt and tool schemas
byte-stable; only failure-path dynamic user-tail and an isolated reviewer
session add tokens.
Cache-guard: internal/recovery TestRecoveryWiringPreservesSuccessPathCacheShape
System-prompt-review: N/A for main agent (reviewer uses a separate fixed policy)

* fix(agent): harden auto recovery checkpoints

Problem:
Auto recovery could start a concurrent Agent.Run, wait forever in headless mode, duplicate revise feedback, and persist incomplete or orphaned recovery state.

Root cause:
Failure guidance was routed through Controller.Steer, headless detection relied on approval timeout, reviewer transcripts were treated as session sidecars, and recovery state lacked ordered atomic persistence and task-scoped replay semantics.

Fix:
Deliver guidance to the exact Agent.Run, use explicit headless mode, isolate root and subagent recovery identities, make recovery state atomic and session-owned, support cold-start decisions, and complete the Wails/mock bridge contract.

Verification:
go test ./...; go test -race ./...; root and desktop vet/lint; desktop tests; frontend typecheck/tests/build; real-browser Continue/Revise/Stop checks.

* style: format recovery checkpoint fields

Problem:
CI stopped before compilation because four recovery-related Go files from the original PR were not gofmt-clean.

Root cause:
Aligned struct fields were added without running the repository-wide formatter gate.

Fix:
Apply gofmt to the affected root and desktop files without changing behavior.

Verification:
The exact root and desktop gofmt checks from CI now produce no output.

* test(recovery): respect Windows file modes

Problem:
The Windows CI leg failed even though the recovery snapshot was written atomically and remained valid JSON.

Root cause:
The test expected POSIX 0600 permission bits from FileMode.Perm on Windows, where NTFS permissions are represented by inherited ACLs and Go reports synthetic 0666 bits.

Fix:
Keep the atomic persistence assertions on every platform and limit the numeric 0600 assertion to platforms where POSIX mode bits are meaningful.

Verification:
Focused recovery tests pass, the Windows test package cross-compiles, and the exact root gofmt gate is clean.

* fix: harden recovery checkpoint session isolation

Problem:
Recovery state could cross session boundaries, delayed snapshots could target a new session, destructive delete tools could bypass human confirmation, synchronous approval replies could be lost, and legacy sessions could resume enabled.

Root cause:
The gate resolved persistence paths asynchronously, session rotations did not consistently replace gate state, approval IDs were bound after event emission, delete tools were absent from deterministic risk rules, and resume ignored branch metadata.

Fix:
Capture and drain persistence per session, reset or carry recovery state explicitly across every rotation, bind approval IDs before emission, classify delete tools as high risk, and restore the persisted per-session preference with a legacy-off default.

Verification:
go test ./...
(cd desktop && go test ./...)
go test -race ./internal/recovery ./internal/control -run 'Recovery|FreshSessionRotations|SetSessionPath'
go vet ./...
./scripts/cache-guard.sh

* fix(recovery): preserve defaults across session resets

Problem:
Recovery settings were omitted from rendered configuration, and fresh sessions could inherit a resumed session preference instead of the configured default.

Root cause:
The serializers did not emit the new fields, while the controller kept one mutable recovery flag for both current-session state and future-session defaults. Desktop metadata snapshots also did not consistently mirror the effective preference.

Fix:
Render and round-trip the recovery settings, separate current and default controller state, route known-new paths through an explicit fresh-session binding, and synchronize Desktop tabs and branch metadata after rotation.

Verification:
go test ./...
(cd desktop && go test .)
go test -race ./internal/control -run recovery rotation cases
(cd desktop && go test -race . -run recovery session cases)

* feat(agent): evolve recovery checkpoint into Auto Guard

Problem:
Auto only applied the recovery checkpoint after failures, duplicated UI and config controls, and escalated every uncertain recovery directly to a human.

Root cause:
High-risk classification was scoped to active failure state, while persisted one-shot grants and recovery-specific Stop and replay branches made the flow stateful and fragmented.

Fix:
Add a deterministic pre-action Auto Guard, return reviewer denials to agents before three-strike human escalation, remove stale grant replay and recovery Stop, consolidate config and UI, and preserve legacy wire and config decoding.

Verification:
- go test ./...
- go test -race ./internal/recovery
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm test:typecheck && pnpm test && pnpm build)

* test(recovery): synchronize prompt resolution

Problem:
Linux CI could schedule prompt-resolution goroutines before the gate re-keyed its provisional waiter, leaving two new tests blocked until the package timeout.

Root cause:
The tests resolved real approval IDs without first binding them, so they depended on scheduler timing even though production prompt emitters use BindApprovalID.

Fix:
Bind each approval ID synchronously inside the prompt callback before resolving the decision.

Verification:
- go test ./internal/recovery -run TestHighRiskMutationPromptsBeforeAnyFailure\|TestReviewerBlockReturnsReasonThenEscalates -count=100
- go test -race ./internal/recovery -run TestHighRiskMutationPromptsBeforeAnyFailure\|TestReviewerBlockReturnsReasonThenEscalates -count=100

* ci: restart stuck Windows runner

Problem:
The Windows test job stayed in progress well past its configured 10-minute step timeout and ignored both normal and force-cancel requests.

Root cause:
The hosted runner stopped reporting progress after entering the test step, preventing the workflow from reaching a rerunnable conclusion.

Fix:
Advance the PR head without file changes so the workflow concurrency policy replaces the stuck run with a fresh runner.

Verification:
The same head already passed Ubuntu, macOS, race, lint, coverage, CodeQL, cache-impact, and desktop Linux checks.

* fix(desktop): keep Yolo visible in composer

Problem:
The Auto Guard refinement hid Yolo from the composer unless it was already active, which made a supported approval mode less discoverable and left the three-column control partially empty.

Root cause:
The composer rendered the Yolo button conditionally and narrowed the approval control for a two-option default surface.

Fix:
Always render Ask, Auto, and Yolo, restore the three-option width, and align English/Chinese documentation and regression coverage with the direct selection flow.

Verification:
- pnpm exec tsx src/__tests__/composer-run-strip.test.tsx
- pnpm test:typecheck
- pnpm check:css
- pnpm test
- pnpm build
- Browser DOM/click verification for all three visible options

* fix(recovery): close Auto Guard evidence gaps

Problem:
Auto Guard could miss dependency/config deletes, moves, and common install or publish commands, while the recovery reviewer never received the read-only evidence gathered after a failure.

Root cause:
High-risk classification covered only a subset of file mutation tools and broad command names, and successful diagnostic observations were discarded before populating the task-local diagnosis state.

Fix:
Classify every built-in file mutation path and risky package-manager subcommand, keep ordinary verification commands on the fast path, and persist bounded, deduplicated, task-local diagnostic excerpts for the isolated reviewer. Mark dynamic reviewer evidence as untrusted.

Verification:
- go test -race ./internal/recovery -count=1
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- go vet ./...
- cd desktop && go test ./... -count=1
- go test ./internal/recovery -run TestRecoveryWiringPreservesSuccessPathCacheShape -count=1
- ./scripts/cache-guard.sh

* fix(recovery): remove obsolete path helper

Problem:
The PR lint job rejected an unused pathFromArgs helper left behind after high-risk classification moved to multi-path extraction.

Root cause:
The compatibility wrapper had no remaining callers, while go vet does not report unused package-level functions.

Fix:
Remove the obsolete wrapper and retain pathsFromArgs as the single path extraction implementation.

Verification:
- go test ./internal/recovery -count=1
- go test -race ./internal/recovery -count=1
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m

* test(recovery): freeze Auto Guard behavior matrix

Problem:
Auto Guard routing is embedded in the Gate coordinator, so later
refactors risk changing product semantics without a focused oracle.

Fix:
Add a pure Decision engine skeleton and table-driven golden coverage for
the product matrix: ordinary Auto paths never call the reviewer, high
risk and scope/strategy/second-failure ask immediately, ambiguous
recovery reviews once, reject budget escalates on the third block, and
reviewer errors fail closed without burning reject credits.

Verification:
go test ./internal/recovery/

* refactor(recovery): pure decision core, collapsed state, provider reviewer

Problem:
Auto Guard mixed classification, routing, agent-based review, and multi-source
runtime state in one Gate, making later product simplification risky.

Root cause:
Phase/ConsecutiveFails/SafeRetryLeft/Pending lived as parallel truth, and the
reviewer spun a full agent.Agent with tools and compaction.

Fix:
- Route through a pure Decide engine (Classifier facts → allow/review/ask).
- Keep one taskRuntime + activeFailure; derive Phase and wire compatibility
  fields on Snapshot; Restore still drops Pending/ApprovalID grants.
- Replace the agent reviewer with a bounded provider.Stream call
  (temp 0, max 256 tokens, 8 KiB total, 4 KiB output cap, recovery-reviewer
  usage source).

Verification:
go test ./internal/recovery/ ./internal/control/ -run 'Recovery|AutoGuard|recovery'

* refactor(desktop): one-click Auto recovery and remove Guard toggles

Problem:
Recovery cards still used select-then-confirm, and unreleased per-tab /
Settings Auto Guard toggles competed with Ask/Auto/YOLO as a fourth control.

Fix:
- Recovery card is one click: Continue this step / Try another approach,
  optional guidance, details collapsed, localized reason by change_kind.
- Empty revise feedback is accepted; host injects a fixed agent guidance.
- Remove desktop Settings and Wails per-tab Auto Guard toggles; kill switch
  remains [agent].auto_recovery_checkpoint only.
- Stop persisting RecoveryCheckpointEnabled on BranchMeta.
- Document guarded Auto semantics in EN/ZH mode docs.

Verification:
go test ./internal/recovery ./internal/control -run Recovery
desktop go test focused recovery/session cases
pnpm exec tsx src/__tests__/decision-surface.test.tsx

* fix(desktop): keep legacy Auto Guard Wails no-ops

Problem:
Removing the Wails methods broke frontend typecheck against generated
bindings that still list the old surfaces.

Fix:
Restore thin no-op / config-read Wails methods so older frontends and
generated bindings keep compiling, without reviving per-tab or Settings
Auto Guard toggles as product controls.

Verification:
pnpm typecheck
go test . -run AutoRecovery|RecoveryCheckpoint|NewSessionUsesFresh (desktop)

* fix(recovery): unblock legacy Approve, honor project kill switch, keep evidence JSON valid

Problem:
1. Pre-action high-risk recovery cards parked a live waiter without taskRuntime,
   so Approve looked them up via Snapshot and never called ResolveRecovery —
   old clients could hang until context cancel.
2. Desktop re-applied Auto Guard from user-only config after boot, ignoring
   project [agent].auto_recovery_checkpoint = "off".
3. Reviewer evidence over 6 KiB was clipped mid-JSON, producing invalid payload.

Root cause:
Live waiter state was inferred from a persistence-shaped snapshot; desktop
overwrote the controller kill switch; field budgeting happened after marshal.

Fix:
- Gate.HasApproval(id) inspects the live waiter table; Approve routes through it.
- Remove desktop secondary SetRecoveryCheckpointEnabled / DefaultEnabled overrides.
- Budget optional evidence fields, then marshal; assert json.Valid and size.
- UTF-8-safe feedback clip at the 4 KiB host cap.

Verification:
go test -race ./internal/recovery ./internal/control
go test . -run AutoRecovery|NewSessionUsesFresh (desktop)

* fix(ci): clear Auto Guard lint failures and freeze project kill switch

Problem:
CI golangci-lint v2.12.2 failed on three issues introduced by the Auto Guard
refactor: an empty if (SA9003), a redundant explicit type (ST1023), and an
unused desktop helper (U1000).

Fix:
- Assert the real recovery approval id is cleared after legacy Approve.
- Use r := reviewer in the decision matrix helper.
- Remove unused currentTabRecoveryCheckpointEnabled.
- Add boot regression: global on + project off disables Auto Guard at Build
  and after SetFreshSessionPath.

Verification:
go test ./internal/recovery ./internal/control ./internal/boot -run LegacyApprove|BehaviorMatrix|BuildHonorsProject|HasApproval
staticcheck -checks SA9003,ST1023,U1000 on touched packages

* test(desktop): freeze project Auto Guard kill switch

Problem:
The existing regression only exercised boot.Build and did not pass through the desktop runtime wiring that previously reapplied the user-global Auto Guard setting.

Root cause:
The test used a legacy-compatible config path and treated a same-controller session rotation as proof of desktop rebuild behavior, so the secondary desktop overrides were outside its coverage.

Fix:
Use the primary user config path and add a desktop integration regression that builds a project tab with global on/project off, then verifies initial configuration and a fresh-session rotation both remain disabled.

Verification:
go test ./...
go test -race ./internal/recovery ./internal/control ./internal/boot
(cd desktop && go test ./...)
golangci-lint v2.12.2 in root and desktop
./scripts/cache-guard.sh

* refactor(recovery): make Auto Guard intrinsic to Auto

Problem:
The short-lived auto_recovery_checkpoint kill switch duplicated Auto mode state across config, recovery decisions, controller sessions, and desktop tabs, increasing user and maintenance cost.

Root cause:
Auto Guard was introduced as an optional feature even though its decision matrix is part of Auto mode's product contract.

Fix:
Remove the config key and the full runtime enablement chain. Ignore retired TOML and session metadata fields, clean them on the next save, and retain only no-op Wails compatibility methods for older generated frontends. Update docs and regressions, and format files that blocked the prior CI run.

Verification:
- go test -race ./internal/recovery ./internal/control
- go test ./internal/agent ./internal/config ./internal/boot
- cd desktop && go test ./...
- cd desktop/frontend && pnpm typecheck && pnpm test:typecheck
- decision-surface and composer-run-strip tests
- ./scripts/cache-guard.sh

* test(boot): isolate Windows config directories

Problem:
The Windows root-module CI repeatedly loaded a test provider from an earlier case, retried example.invalid for 92 seconds, and then timed out.

Root cause:
isolateConfigHome redirected HOME and XDG_CONFIG_HOME but not the AppData and LocalAppData directories used by os.UserConfigDir and os.UserCacheDir on Windows.

Fix:
Redirect USERPROFILE, AppData, and LocalAppData into the same per-test temporary directory.

Verification:
- go test ./internal/boot
- repeated the affected Boot tests three times
- confirmed the Go standard library reads AppData and LocalAppData on Windows

* fix(desktop): prevent approval control overlap

Problem:
The three-option approval bar overlapped the adjacent model selector at compact composer widths.

Root cause:
The 760px container rule shrank the approval wrapper to 164-178px while its child mode bar retained a fixed 200-232px width.

Fix:
Allow the approval mode bar to shrink with its compact parent and add a CSS contract regression for the responsive geometry.

Verification:
- pnpm test
- pnpm build
- pnpm typecheck
- pnpm test:typecheck
- pnpm check:css
- ./scripts/cache-guard.sh
- in-app browser geometry at 1280px and 1182px viewport widths

* fix(recovery): close Auto Guard review gaps

Problem:
Auto Guard missed wrapped dependency and configuration mutations, while desktop telemetry replayed cumulative recovery counters on every completed turn.

Root cause:
The high-risk classifier recognized only narrow command and path shapes, and the desktop sampled controller totals rather than per-turn deltas.

Fix:
Harden wrapper and configuration mutation classification, add atomic recovery metric draining, and cover both regressions with focused tests.

Verification:
- go test ./...
- go test -race ./internal/recovery ./internal/control
- cd desktop && go test ./...
- go vet ./...
- ./scripts/cache-guard.sh

* refactor(recovery): make Auto interruption-light

Problem:
Auto asked users to understand recovery internals and interrupted reversible workspace work, while Plan required select-then-confirm.

Root cause:
The recovery classifier treated config and dependency edits, strategy changes, and the second failure as human boundaries, and the desktop surface exposed internal guard concepts.

Fix:
- reserve human prompts for deterministic hard boundaries or three failed attempts
- keep low-risk recovery moving when optional review is unavailable
- simplify Plan and recovery decisions to two direct, accessible buttons
- align desktop, bot, docs, and localized copy

Verification:
- go test ./...
- pnpm --dir desktop/frontend test:all
- pnpm --dir desktop/frontend build
- browser verification for Auto, Plan + Auto, Goal + Auto, Plan confirmation, and recovery confirmation

* fix(recovery): align Auto boundaries and reviewer

Problem:
Auto could silently allow destructive or external commands, while the isolated reviewer still rejected bounded project-local dependency, config, strategy, and scope changes after failures.

Root cause:
The deterministic shell classifier omitted destructive Git variants, global package-manager forms, and external mutation CLIs. The fixed reviewer contract only allowed same_strategy verdicts after Auto policy moved bounded workspace recovery to the automatic path.

Fix:
- Classify destructive Git state changes, global package operations, HTTP/GitHub API mutations, and remote command families as hard boundaries while preserving proven read-only fast paths.
- Allow reviewer continue verdicts for bounded, task-aligned strategy and scope changes while keeping risk and uncertainty fail closed.
- Align the three-failure rationale and add policy and classifier regression coverage.

Verification:
- go test ./... -count=1
- go test -race ./internal/recovery ./internal/control ./internal/agent -count=1
- go vet ./...
- golangci-lint v2.12.2 ./internal/recovery/... (0 issues)
- ./scripts/cache-guard.sh

* fix(auto): scope reusable recovery approvals safely

Problem:
Auto Guard repeatedly interrupted bounded external operations, while several unlisted shell, PowerShell, Git checkout, and attached GitHub CLI flag forms could bypass the intended hard boundary.

Root cause:
Recovery confirmations were one-shot only and the shell classifier relied on an incomplete deny list without a host-owned semantic authorization scope.

Fix:
Add task-scoped semantic grants for explicitly bounded operations, expose opt-in actions across Desktop, CLI, and Bot, fail closed for unknown mutations, and cover destructive sibling forms and lifecycle boundaries.

Verification:
go test ./...
go test -race ./internal/recovery ./internal/control ./internal/agent
pnpm test:typecheck && pnpm test
pnpm run build
./scripts/cache-guard.sh

* refactor(auto): simplify recovery confirmation UI

Problem:
- Auto Guard confirmations exposed recovery diagnostics at equal visual priority, making simple decisions noisy.

Root cause:
- Raw recovery fields reused warning cards and appeared before the user's actual choices.

Fix:
- Show one concise action summary, recommend the safer alternative first, move optional task grants after the decisions, and flatten technical diagnostics behind disclosure.

Verification:
- pnpm test
- pnpm typecheck
- pnpm check:css
- In-app browser checks for default, expanded, and 620px layouts with no console errors.

* feat(auto): add optional recovery guidance

Problem: Auto Guard offered only a generic retry or one-shot continue, so users could not steer a safer alternative without adding another heavyweight decision.

Root cause: the simplified recovery surface removed free-text feedback entirely instead of keeping it behind progressive disclosure.

Fix: add a quiet guidance entry that expands an inline editor, routes text only through revise, clears unrelated task grants, restores keyboard focus, and localizes the flow.

Verification: pnpm test; pnpm typecheck; pnpm check:css; interactive in-app browser review of collapsed and expanded states.

* fix(auto): bind task grants to exact targets

Problem:
Task-scoped recovery grants for git pushes matched every ref on the same remote, stale grants remained attached to reused task IDs, and Desktop described a checked task grant as a one-shot continue.

Root cause:
The semantic grant key omitted the destination ref, runtime state did not retire grants when the host task scope changed, and recovery approval events exposed only a boolean instead of the host-classified scope.

Fix:
Scope reusable pushes to an explicit remote and destination, reject ambiguous or behavior-changing variants, retire grants on task-scope changes, expose a transient display scope to every frontend, and group explicit consent with the Continue action.

Verification:
go test ./...
go vet ./...
go test -race ./internal/recovery ./internal/eventwire ./internal/control ./internal/agent -count=1
pnpm test:all && pnpm build
(cd desktop && go test ./... -count=1)
./scripts/cache-guard.sh

* test(auto): cover legacy task-grant events

Problem:
Older backends do not send the optional task_grant_scope field.

Root cause:
The new exact-scope rendering tests only exercised events produced by the updated backend.

Fix:
Keep one grantable recovery fixture on the legacy wire shape and assert that Desktop renders the generic bounded-scope explanation.

Verification:
pnpm test:typecheck
pnpm exec tsx src/__tests__/decision-surface.test.tsx

* fix(auto): drain recovery writes before new session

Problem:
The macOS PR job intermittently failed while TempDir removed a session directory because /new could return with an old-session recovery snapshot still writing asynchronously.

Root cause:
ClearSession drained path-bound recovery persistence before deleting artifacts, but NewSession rotated the session path without the equivalent lifecycle barrier.

Fix:
Drain pending recovery writes for the old path before the final session snapshot and path rotation. Add a deterministic blocking persistence regression test that proves NewSession waits for the drain.

Verification:
- go test ./... -count=1
- go test -race ./internal/control -run '^(TestFreshSessionRotationsClearRecoveryState|TestNewSessionWaitsForPendingRecoveryPersistence)$' -count=10
- go test ./internal/control -run '^(TestFreshSessionRotationsClearRecoveryState|TestNewSessionWaitsForPendingRecoveryPersistence)$' -count=50
- go vet ./internal/control ./internal/recovery
- (cd desktop && go test ./... -count=1)
- ./scripts/cache-guard.sh
2026-07-22 02:52:24 +08:00
ikelvingoandSivanCola 1e1ba41d40 agent/save: make session filenames Windows-safe / 修复 Windows 会话文件名兼容性 (#6684)
* agent/save: fix Windows crash when model name contains ':'

Fixes #6683

NewSessionPath() only sanitized '/' and '\' from model names, missing ':'
and other Windows-illegal filename characters. On Windows, ':' is the
NTFS alternate data stream (ADS) marker, causing:

- System freeze / crash when file system scanners index the session file
- CLI permanently stuck: lease.lock path with ':' triggers 'session is
  in use' error that can never clear
- All sidecar files (.ckpt, .lock, .lease.*, .events.jsonl) corrupted

Add all Windows-illegal chars to the replacer: :<>"|?*

* test(agent): cover Windows-safe session filenames

Problem:
The Windows filename fix had no regression covering a real colon-bearing model label or persistence through the generated path.

Fix:
Add a focused test for the curated nemotron-3-nano:30b model shape, cover the newly sanitized punctuation, and save plus stat a session through the real persistence path.

Compatibility:
The test exercises future filename derivation only. Existing session files and safe model labels remain unchanged.

Verification:
- go test ./internal/agent -run TestNewSessionPath -count=1
- go test ./internal/agent ./internal/control ./internal/cli ./internal/boot
- cd desktop && go test ./...
- git diff --check

Co-authored-by: SivanCola <[email protected]>

---------

Co-authored-by: SivanCola <[email protected]>
2026-07-19 19:31:25 +08:00
SivanCola dd929bf2df Bound session save lock waits / 限制会话保存锁等待时间 (#6667)
Problem: A competing process holding the compatibility session save lock
wedged desktop navigation and true-quit shutdown indefinitely, kept the
session lease and WebView alive, and a timed-out final snapshot could
discard transcript changes that existed only in memory (#6637, Bug 3).

Root cause: flock/LockFileEx used blocking acquisition with no deadline;
the shutdown path treated a held lock like any other snapshot failure;
and during the two-phase recovery handoff a concurrent tab-layout save
could overwrite the recovery anchor with the controller's stale path.

Fix: Retry non-blocking lock acquisition for a bounded five-second window
on Unix and Windows and keep the in-memory transcript on failure; on a
true shutdown timeout persist the transcript to a process-specific
recovery branch that never re-acquires the stalled original lock, reusing
the standard session/event-log/branch-meta formats and recovery handoff;
make the lease-backed tab path authoritative during the handoff via a
lock-free lease-key mirror so layout saves never wait on lease file
operations.

Verification: go test -race ./internal/agent ./internal/control; cd
desktop && go test -race ./...; go vet ./... (root and desktop modules);
trial merge with origin/main-v2 clean; scripts/check-cache-impact.sh
reports no cache-sensitive files changed; full CI green including race,
desktop-windows, cache-impact, and CodeQL.
2026-07-19 14:45:12 +08:00
SivanCola dae65e250b Fix long-run freeze on stalled error bodies and session-switch lag/content loss / 修复重试卡死、切换会话卡顿与内容丢失 (#6613)
* fix(provider): bound non-OK response body reads in the retry loop

A 502/524 gateway under load can send headers and then stall the body on a
half-open connection. http.Client deliberately has no Timeout (streams), and
ResponseHeaderTimeout no longer applies once headers arrive, so the retry
loop's error-body io.ReadAll/io.Copy could block indefinitely with no retry
notification and no user-visible progress — the turn looked frozen until the
process was killed (#6607, #6346, #5760).

readErrorBody arms a hard deadline that closes the body to unblock the read;
context cancellation already aborts it via the transport. The regression test
drives a stalled 502 body through SendWithRetry and fails via watchdog if the
loop wedges (mutation-verified).

* fix(agent): salvage event-log tail bytes before repair truncates them

repairSessionEventLogTail's os.Truncate was the only place that permanently
discards persisted transcript bytes. A torn tail is usually one partial
record, but replay also stops at a buried undecodable or out-of-order record
(the dual-writer interleave shape) — then every intact turn past it was
silently lost, surfacing as 'reopened session lost messages' (#6607, #5315).

The discarded range is now appended to a .events.jsonl.damaged salvage
sidecar (one-line JSON incident header + raw bytes) before truncation. The
sidecar name must not end in .jsonl: older binaries scanning a shared session
directory would classify it as a primary transcript. It is registered in
SessionSidecarFiles so every delete surface removes it. Preservation is
best-effort — a full disk both tears tails and fails salvage — and never
blocks the repair itself.

* perf(agent): skip snapshot saves when the transcript is provably unchanged

Desktop switch/close/prune paths snapshot defensively on every navigation.
Each snapshot paid a full transcript serialize + digest + disk probe just to
discover there was nothing to write — on large sessions that is the seconds
of UI freeze reported as switch lag (#6607, #5337, #5185; single-surface
activation pays it once per hidden tab, twice per pruned runtime).

save() now short-circuits snapshot-mode saves when in-memory bookkeeping
proves a no-op: same path, same version, same rewriteVersion, ledger revision
known, no pending load-time repair or log damage. The check runs under the
save locks, not before them, so a saver that waited on a concurrent writer
still re-evaluates against the state it must persist (locked in by
TestSaveSnapshotCapturesContentUnderSaveLock).

Only save-verified baselines arm the fast path: a baseline adopted at load
pairs the disk transcript with whatever the meta sidecar said, which lags the
transcript after an interrupted save — the first save after a load must run
in full and heal a stale ledger (TestSameContentSaveHealsStaleLedgerDigest).

* fix(desktop): drop stale topic activations instead of applying them

activateTopic applied its result unconditionally after awaiting the backend:
when a newer navigation (another topic click via a different path, a tab
switch, session restore) completed first, the stale completion flipped the
visible tab away from the user's last click AND ran the single-surface prune
that deletes every other tab's cached state — blanking the transcript the
user was looking at (#6607, #5337 'switch shows an empty session').

The App-level navigation queue serializes queued topic clicks, but its
latest() guard only covers App-level effects; the setActiveTabId + state
prune inside activateTopic were unguarded. Anchor each activation to the
existing activeNavigationSeqRef epoch (the same mechanism rewind/fork already
use) and return without applying when a newer navigation superseded it.
Mutation-verified: disabling the guard fails both stale-apply assertions.

* fix(privacy): scrub and trash the damaged-salvage sidecar with its session

Review of #6613 found two lifecycle gaps for the new .events.jsonl.damaged
salvage sidecar, both privacy-relevant since it preserves raw transcript
bytes:

- doctor redact did not recognize the suffix: RedactSessions reported success
  while secrets in the salvaged tail survived. The bytes are undecodable by
  definition, so no format-aware masking can prove them clean; following the
  existing torn-tail precedent (compacted away regardless of content), the
  scrub now deletes the sidecar outright, honors dry-run, and skips leased
  sessions like every other artifact.
- the desktop trash flow uses its own explicit artifact list, so deleting a
  session left the sidecar in the live directory and purging the trash never
  removed it. The sidecar now moves with the other transcript artifacts
  through delete, restore, and purge.

* fix(agent): re-arm the snapshot fast path after a repair is persisted

Review of #6613 found that normalizedDirty and eventLogDamaged were never
cleared on the live session after a successful save persisted the repair, so
a session that was repaired once kept paying the full serialize + digest on
every defensive snapshot until restart — the switch-lag fix was incomplete
for exactly the sessions that needed it most.

Clear the flags (and the obsolete rawMessages capture) when a save-verified
baseline lands: the write included the normalization repair and healed the
on-disk log. Nothing reads the live session's copies after a save —
checkSnapshotWrite re-loads the on-disk state and consults that object's
flags, not these.

* fix(desktop): invalidate in-flight activations when a navigation is queued

Review of #6613 found the last-click-wins guard did not cover the production
navigation queue: App.enqueueNavigation serializes clicks, so a click made
while another request runs only becomes a pending queue entry — it does not
run activateTopic and therefore never advances the controller's navigation
epoch. The running stale activation then passed the guard, flipped the
visible tab, and pruned the newer surface's cached state.

useController now exposes noteNavigationIntent (the same epoch bump the
guard checks) and App.enqueueNavigation calls it at enqueue time, so any
queued click immediately invalidates the running activation's destructive
apply. The regression test drives the REAL enqueueNavigationRequest queue,
holds the first activation mid-flight, queues a second, and asserts the
stale completion neither flips the tab nor deletes cached state; a source
assertion locks the App.tsx wiring itself. Mutation-verified both ways.

* fix(doctor): include the damaged-salvage sidecar in session bundles

Proactive sweep after the #6613 review round: session bundles exist to debug
persistence incidents, and the .events.jsonl.damaged sidecar is the primary
evidence for truncation incidents (torn tails, dual-writer interleaves) —
but sessionBundleArtifacts, another explicit artifact list, did not include
it. Bundles copy artifact contents verbatim already (only paths/refs are
redacted), so shipping the sidecar adds no new sensitivity class.

* fix(agent): migrate the damaged-salvage sidecar on overlong-name renames

Review follow-up on #6613: migrateSessionSidecars is yet another explicit
sidecar list, and it did not include SessionEventLogDamaged. When
reconcileOverlongSessionFilenames renames a transcript to a bounded stem,
the salvage sidecar stayed under the retired name — an orphan file holding
raw session bytes that no delete, trash, redact, or bundle flow could reach
(they all derive paths from the live transcript name).

Regression: TestReconcileOverlongMigratesDiagnosticSidecars now seeds the
sidecar and asserts it migrates with the rename and leaves nothing under
the retired stem (mutation-verified).
2026-07-17 19:56:28 +08:00
SivanCola 0afcea408d Restore historical message timestamps and simplify history navigation / 恢复历史消息时间戳并简化历史入口 (#6586)
* fix(desktop): remove redundant history navigation

* fix(desktop): restore historical message timestamps
2026-07-16 23:48:14 +08:00
SivanColaandopen 6826411dd1 Restore v0.53 secret handling semantics (#6583)
* Restore v0.53 secret handling

* Harden verbatim artifact storage

---------

Co-authored-by: open <[email protected]>
2026-07-16 22:42:34 +08:00
SivanCola fdef0f5b41 fix(delivery): harden review_report protocol, read-only gating, and streaming liveness
Delivery-mode hardening from a full-workflow field trace (16-min screen
recording + persisted event log of the same session):

- review_report protocol: append an explicit submission contract to review
  subagent task prompts, nudge a finished-but-reportless review subagent on
  the same session (evidence preserved so the report can cite earlier reads)
  up to 2 times before failing, dump the failed transcript for diagnosis,
  and correct the error text that told the parent to call a tool it does
  not have.
- delivery intent classification: classify mutation/work expectations from
  the user task text only — host framing (subagent/workspace context blocks,
  delivery-runtime marker) contained incidental verbs ("resolve") that
  classified every wrapped subagent prompt as a mutation request; read-only
  registries can never satisfy a state-change expectation, so it is no
  longer armed for them (this deadlocked read_only_task reviews).
- final readiness budget: extend nudges past the base cap (3, up to 6) only
  while the model keeps earning new host-observable receipts, so a
  converging turn (fix -> verify -> re-review) is not cut off mid-recovery
  while a stalled turn still fails fast.
- title/preview hygiene: strip the trailing <delivery-runtime> marker in
  StripTransientUserBlocks, share one synthetic-user filter
  (agent.SyntheticUserPrefixes / IsUserAuthoredTurn) across session preview,
  meta turn counts, and desktop topic auto-titles so host-injected readiness
  nudges can no longer become titles ("你是谁? <delivery-run…") or inflate
  user-turn counts.
- streaming liveness: providers emit ChunkToolCallArgsDelta progress (per
  2KB) while tool-call arguments stream; the agent re-emits throttled
  partial ToolDispatch events with ArgChars; the desktop upserts a live
  "receiving arguments" tool card instead of dropping partial dispatches,
  and the composer pill folds streamed arg chars into its token estimate —
  a 30KB write_file body no longer looks like a hang for 80 seconds.
- right-panel freshness: a source-agnostic usageSeq keeps 会话指标/用量分析
  refreshing during sub-agent runs; the context gauge no longer collapses
  to 0/1M when a mid-turn snapshot races a rebuilt executor.

Verification: go test ./... (root and desktop modules), scripts/
cache-guard.sh (tail_avg 92/95/95 >= 90), desktop/frontend pnpm typecheck +
check:css + test:all, plus new regression tests covering every item above.
2026-07-13 02:59:39 +08:00
Sivan 5078a1987f Fix Windows encoded config reads 2026-07-10 02:40:43 +08:00
Sivan eb592775a1 Serialize LoadSession with in-process saves 2026-07-08 20:54:43 +08:00
SivanCola 99c1539142 fix(security): gate env/path protections behind [secrets] config, fix PWD and clone bugs
Split the credential-protection layers by blast radius:

- Durable surfaces (session transcripts, job artifacts) always redact.
- Live tool output redaction stays default-on but honors [secrets]
  redact_tool_output for fixture-heavy repos.
- Subprocess env filtering and the sensitive-path read denylist become
  explicit opt-ins (filter_subprocess_env, protect_sensitive_files,
  both default off): stripping GH_TOKEN/NPM_TOKEN broke gh, HTTPS git
  push, and npm publish; hiding .env broke legitimate edit workflows.

[secrets] is user-global: project reasonix.toml values are discarded on
load so a cloned repo cannot disable redaction or flip on the
workflow-breaking protections. The captured global section deep-copies
its *bool so the project-file TOML decode cannot write through it.

Fix the PWD false positive: bare "pwd" now requires a leading [_-]
separator (DB_PWD masks, POSIX PWD/OLDPWD pass through), keeping every
subprocess's working-directory context intact.

Fix RedactMessage writing through shared ToolCalls/MemoryCitations
backing arrays: the save path hands in live session messages, and
in-place masking would silently rewrite model-visible history
mid-conversation and churn the prompt cache. Clone before masking.

Fix SaveRecoveryBranch bypassing redaction: recovery branches are
durable transcripts, and comparing a raw snapshot against the
already-redacted on-disk content would misread coverage as divergence
and fork a bogus recovery branch. Document the digest-consistency
argument (Redact is deterministic and idempotent, so load/save cycles
are byte-stable) and pin it with a regression test.
2026-07-08 13:42:25 +08:00
SivanCola 344895e496 fix(security): redact secret-bearing tool output 2026-07-08 12:13:50 +08:00
SivanCola 1c17637267 Merge pull request #6126 from SivanCola/fix/sidecar-reconcile-and-doctor-out
Migrate diagnostic sidecars through overlong-name reconciliation / 超长名整理时迁移诊断 sidecar
2026-07-07 03:25:11 +08:00
merge-order-check a7b5a0487e Keep diagnostic sidecars out of overlong-name reconciliation and migrate them
The overlong-name pass filtered by a bare .jsonl suffix, so an
.events.jsonl or .conflicts.jsonl whose name crossed the 224-byte bound
(owner stem between 209 and 224 bytes) was renamed into a fake session
with fabricated meta, while the real transcript's rename orphaned both
logs under the retired stem. Filter with store.IsSessionTranscriptName
and carry the event log, event index, and conflict log through
migrateSessionSidecars like the other owned sidecars.

Also accept --out=PATH (equals form) in reasonix doctor session.
2026-07-07 02:34:39 +08:00
merge-order-check d2d37c47ed Move the persisted-rewrite baseline into agent.Session
The controller kept savedRewriteVersion as external bookkeeping about
the executor's session object, so every SetSession had to be paired
with a manual baseline reset and snapshot() had to guess whether
recovery had swapped the object — the guess that broke depth-cap
force-saves (#6120).

Store persistedRewriteVersion on the Session itself: save paths advance
it with the rewriteVersion captured in the same lock window as the
message snapshot (a compaction landing mid-save stays unpersisted, and
a slower save cannot roll it backwards), clones carry it, and swapped
sessions take their baseline with them. snapshotRewriteDecision and
both mark helpers plus their seven call-site pairings collapse into
Session.NeedsRewriteSave.

Behavior fix folded in: installing a clone that still holds an
unpersisted rewrite no longer stamps it persisted-by-construction; the
next snapshot correctly runs as an owned rewrite.
2026-07-07 02:30:42 +08:00
SivanCola a1fecd3aa0 fix(agent): keep dirty-resume saves off the append shortcut, raw-check recovery need
Two more sites judged the normalized load as if it were the disk truth:

- checkSnapshotWrite let a session resumed from a repaired transcript take
  the append-only shortcut with appendFrom measured against the normalized
  view. The event log replays the raw transcript, so the append event's
  index broke the replay chain and the whole appended turn silently
  vanished from disk on the next load (until a later save healed the log).
  A pending repair now falls through to the full rewrite, which persists
  the repair and the new turn together.

- SaveRecoveryBranch decided "recovery not needed" only against the
  normalized view, so a mid-history repair (e.g. an empty tool-call name
  backfilled on load) made a fully-covered snapshot look uncovered and
  forked a pointless recovery branch. Coverage now falls back to the raw
  transcript, mirroring checkSnapshotWrite.
2026-07-06 18:53:45 +08:00
SivanCola 09ce7df436 fix(agent): judge snapshot appends against the raw on-disk transcript
A mid-turn autosave can legitimately cut an assistant tool call from its
still-running result, leaving a dangling call at the tail of the saved
transcript. checkSnapshotWrite compared the next snapshot against the
LoadSession view of disk, where normalization has already fabricated a
placeholder answer for that call. The live session's real tool result
collides with the placeholder, so a pure append misread as diverged and
forked a "snapshot conflict" recovery branch 30 seconds after the first
mid-turn save of any tool-heavy turn.

Keep the pre-repair transcript on loaded sessions and fall back to it in
checkSnapshotWrite when the normalized prefix checks fail: append shape,
the owned-rewrite digest, and conflict-kind classification are now judged
against the bytes actually on disk. Appending the real results also lands
the transcript well-formed, so no repair is left pending in that path.
2026-07-06 18:42:42 +08:00
SivanCola 82a9d866d6 fix(agent): cap recovery chain depth and force-save at the cap
A save-conflict loop used to fork recovery branches from recovery
branches without bound — #5993 reached 8 nested levels, each fork
polluting the session list. The conflict triggers behind that loop are
fixed, but nothing prevents a future regression from spawning chains
again: BranchMeta.Recovered was display-only and SaveRecoveryBranch
never looked at the parent's recovery state.

- Stamp BranchMeta.RecoveryDepth on every recovery fork (parent depth
  + 1; legacy recovery metas count as depth 1).
- SaveRecoveryBranch refuses to fork a parent already at
  SessionRecoveryMaxDepth (3) with ErrSessionRecoveryDepthExceeded.
- At the cap the controller force-saves the transcript onto the
  current branch instead: this runtime is the only writer of recovery
  branches it created, so the force write keeps the data, re-anchors
  the persistence baseline (no immediate re-conflict), and stops the
  file proliferation. A warn log plus a distinct notice make the
  condition visible.
2026-07-06 15:47:28 +08:00
SivanCola f4591d7c1c fix(agent): let a same-content foreign stamp keep rewrite ownership
A mirror runtime healing the revision ledger over identical content
(same-content save recording a missed revision) advanced the revision
under its own writer id. The next owned rewrite on this side — a
cancelled turn's truncation or an auto-compaction — then read as a
stale-runtime conflict and forked a recovery branch, even though the
on-disk transcript was byte-for-byte what this session last persisted.

ownsPersistedState now consults the ledger's recorded content digest:
when the baseline digest, the on-disk transcript digest, and the
ledger digest all describe the same bytes, a foreign revision stamp is
bookkeeping over content this session owns, and rewriting it destroys
nothing of the stamper's. At worst the conflict relocates to the
stamper's next divergent save, where its own in-memory history forks a
recovery branch as usual — no content-loss path opens.

A stamp whose digest disagrees with the on-disk transcript still
revokes ownership: that is the aftermath of a save whose bytes landed
but whose record failed, the bytes cannot be attributed to a writer
(event writer ids are per-process, not per-session), and only the
conservative conflict path preserves them.
2026-07-06 14:05:08 +08:00
SivanCola 895972d792 fix(agent): anchor stale-baseline session saves to the persisted baseline
Replace the exact-append CAS exemption with a reachability guard:
locate the persisted baseline digest among the pending snapshot's
incremental prefix digests (re-anchored on the disk system message when
a resume swapped the prompt) and require the on-disk transcript to
still reach that depth.

This extends the stale-revision exemption to compatible-system appends
(resume swapped the leading system prompt, then a routine turn append
forked a spurious recovery branch), while closing the gap the
exact-append exemption opened: an append over a transcript another
runtime deliberately rewound below the baseline now conflicts again
instead of silently resurrecting the removed suffix.

ownsPersistedState no longer lets an absent revision ledger revoke
rewrite ownership: recorded revisions start at 1, so a disk revision of
0 means the meta sidecar was deleted or rebuilt by a listing-only
writer after this session's save — the digest+version match already
proves the transcript is ours. Cancel-turn truncations and compaction
rewrites therefore survive a swept sidecar. A foreign recorded revision
still revokes ownership, preserving the guard against stale controllers
force-rewinding a ledger another runtime has stamped.
2026-07-06 13:07:23 +08:00
SivanCola 7f9491966c fix(agent): allow safe session append from stale revision baseline 2026-07-06 12:25:04 +08:00
SivanCola 075b961254 Merge pull request #6030 from SivanCola/fix/lease-experience-cleanup
Polish lease UX: self-heal metadata damage, sanitize errors, retire lock sidecars / 打磨租约体验:元数据损坏自愈、错误脱敏、锁侧车清理
2026-07-06 01:03:53 +08:00
SivanCola 8ea1c4905b Merge pull request #6023 from SivanCola/fix/desktop-lease-key-canonicalization
Fix Windows desktop session lease self-lock on model switch / 修复 Windows 桌面端切换模型会话租约自锁
2026-07-06 00:53:27 +08:00
SivanCola c33c372a2f fix(desktop): self-heal lease metadata damage, sanitize surfaced errors, retire lock sidecars
Follow-up polish on the lease/CAS stack after the #6023/#6025 hotfixes,
closing the remaining ways a healthy single-window session could still
present as busy, leak holder details, or litter the session directory.

- Reclaim now arbitrates on the OS lock alone: a missing or unreadable
  lease.json (user cleanup, AV quarantine, crash tear) with a free lock is
  a leftover, not a holder. Previously an orphaned in-process entry whose
  info file disappeared could never be reclaimed - the fallback acquire
  re-hit the orphaned entry forever and every rebuild stayed busy. The
  desktop gate mirrors this: nil lease info allows the reclaim attempt,
  foreign holders are still refused.
- Raw lease errors (session path + host-pid-writer id) can no longer reach
  the UI: startup bind failures (StartupErr feeds the topbar banner and tab
  metas), ClearSession's bridge returns, and the recovery-lease callback
  (chat notices via snapshot errors) all render the user-facing busy
  message; raw details stay in slog. The busy message drops its dangling
  'before changing this setting' clause when no setting applies.
- SessionLease.Release retires the .lock/.lease.lock sidecars through the
  existing atomic take-then-remove helpers (non-blocking, no-op against any
  live holder or in-flight save), so ordinary use stops accumulating lock
  files that previously waited for the next boot reconcile (#6014).
- Autosave failures warn once per burst and at most once per 5 minutes per
  tab: retries are slog-only, the user notice fires when the burst gives
  up. Explicit action saves keep immediate feedback. Every failure is still
  logged.
- SaveRecoveryBranch tolerates event-index write failures like the other
  save paths: the recovery transcript and meta are already durable, so a
  failed listing accelerator no longer discards a successful recovery.
- Lease test suite now feeds user-shape (mixed-case) paths to the APIs
  under test instead of pre-canonicalized ones - the pattern that let the
  Windows case-fold mismatch (#5999) escape the suite - plus regressions
  for orphan-without-info, corrupt-info, foreign-info-refused, sidecar
  retirement on release, StartupErr sanitization, and the autosave warning
  debounce.

Refs #6014 #5999 #6027.
2026-07-05 23:59:24 +08:00
SivanCola 728d4af4ce fix(session): heal stale meta ledger on same-content saves
A save that lands its transcript bytes and then fails to record the
revision (fail-closed recordSessionContentRevision, or a crash between
the two writes) leaves the sidecar describing older content. The
same-content retry then took the up-to-date shortcut and never touched
the ledger again, so revision/content_digest stayed stale until the
next content-bearing save — contradicting the "later save can bump the
revision" recovery the fail-closed path promises. Idle sessions never
healed at all.

checkSnapshotWrite now surfaces the ledger digest it already reads, and
the up-to-date path records the deferred revision when that digest no
longer matches the on-disk transcript, reproducing the exact state the
interrupted save would have left. A missing or digest-less legacy
sidecar stays untouched: stamping those would bump revisions other
runtimes still hold as baselines.
2026-07-05 23:48:32 +08:00
SivanCola e6d574587a fix(session): harden the CAS meta ledger against torn reads and lost updates
The transcript CAS keyed off the .jsonl.meta sidecar, but the ledger was
maintained unprotected. On Windows this manufactured conflicts in normal
single-window use: recovery branches forked on every app start, recovery
files accumulated, and 'adopted the newer transcript' notices looped.

- Retry transient sidecar reads (I/O and JSON-decode failures, 20/50/100ms)
  before giving up: ReplaceFile's non-atomic copyOnto fallback tears
  concurrent readers, and AV/indexer handles fail reads transiently.
- Stop swallowing unreadable-ledger errors into revision 0: a persistently
  unreadable ledger now fails the save (transcript bytes are already
  durable; the autosave retry heals it) instead of desyncing the baseline
  and misreading the next honest save as a foreign write.
- LoadSession with an unreadable sidecar still opens the session but marks
  the baseline revision-unknown: revision CAS is disarmed (digest/prefix
  checks still catch real divergence) and owned rewrites anchor on
  digest+version, so compaction cannot become a permanent conflict. A
  missing sidecar remains a known revision-0 baseline.
- Hold the per-path meta lock across the whole read-modify-write in
  RenameSession, topic-title propagation, legacy topic migration, and
  restoreSessionTopicIndex, so a concurrent save's revision bump can no
  longer be rolled back by a stale read-back.
- Downgrade event-index write failures to a warning at both save() sites:
  the index is a listing accelerator and must not strand the baseline
  behind a transcript+revision that already landed.
- Capture save() snapshots under the save locks so concurrent in-process
  savers can no longer land out of order and misread the newer transcript
  as a stale-prefix conflict.

Regression tests cover fail-closed saves on a corrupt ledger, unknown-
revision baselines (load, append, owned rewrite), missing-meta behavior,
index-failure tolerance, a concurrent writers hammer asserting revision
monotonicity, and deterministic under-lock capture.

Fixes #6014. Fixes #5998. Fixes #5996. Refs #6003.
2026-07-05 22:34:43 +08:00
SivanCola aa503bca85 fix(desktop): unify session runtime keys with lease canonicalization
The lease registry keys sessions through agent's canonical form (clean,
absolute, lowercased on Windows), while desktop sessionRuntimeKey kept the
raw path case. On Windows every tab path contains uppercase segments
(C:\Users\...), so the tab's own lease never matched its session key:
ensureSessionLease re-acquired against the process's own registry entry,
reclaim then hit the tab's live OS lock handle, and every model / effort /
token-mode / provider-key rebuild failed with the "already open in another
Reasonix window" error on a single window — including fresh installs.

- Export agent.CanonicalSessionPath and layer it into sessionRuntimeKey so
  desktop identity checks agree with the lease registry byte-for-byte.
- Normalize the canonicalTabSessionPath fallback (project-scope sessions)
  with clean+abs so shape variants cannot split one file into two keys.
- Keep case-folded keys out of SessionPath: detach/attach paths now carry
  the display path separately from the runtime key.
- Release only the build's own lease on startup-build failure, and skip
  claiming a lease the build merely reused (a concurrent switch owns it).
- Supersede in-flight startup builds when a model/effort/token switch
  installs its controller, so a slow blank-session build can no longer
  overwrite the new runtime or strip its lease.
- Regression tests: lease/runtime key agreement, idempotence, Windows case
  folding, project-scope fallback normalization, fast-path lease reuse.

Fixes #5999. Fixes #6006. Fixes #5989. Fixes #5990. Refs #5996.
2026-07-05 22:17:38 +08:00
SivanCola 7bb7290c2f Hold session locks across trash/delete and fix lint
Review follow-ups on the event-log hardening:

- Close the probe-then-delete lease window: desktop trash, reconcile, and
  permanent delete now take a SessionRemovalGuard that holds the session's
  save lock and lease lock across the whole operation (built on #5977's
  delete-while-holding-flock primitive), so another process can no longer
  acquire the lease between a busy probe and the sidecar removal and then
  lose its freshly locked lease file. Live owners surface as a sanitized
  busy error; the lock files are deleted atomically with the release.
- Run overlong-named sessions (pre-bounded recovery cascade names) in
  checkpoint-only mode: their event log/index sidecars would exceed the
  filesystem name limit, so the probe treats them as non-native instead of
  failing saves with ENAMETOOLONG.
- Fix staticcheck QF1003: tagged switch for the save-reason mapping.
2026-07-05 14:03:03 +08:00
SivanCola b942018011 Merge remote-tracking branch 'origin/main-v2' into fix/session-recovery-event-log-cleanup 2026-07-05 13:53:39 +08:00
SivanCola f0221714c9 Harden session event log persistence and recovery cleanup
- Replay the event log tolerantly: torn tails are truncated by the next
  save instead of bricking LoadSession; foreign/legacy .events.jsonl files
  squatting the native log path are read-ignored and never written; logs
  from a newer schema fail loudly instead of being silently truncated.
- Bound log growth: past a size threshold the log compacts to a single
  replace event, and every rewrite/compaction refreshes the .jsonl
  checkpoint so direct readers and older binaries stay bounded-stale.
- Keep force saves (subagents, guardian, migrations, forks) single-file:
  they never bootstrap an event log and fold an existing one instead.
- Sweep the event log/index from every deletion surface through
  store.SessionSidecarFiles — /clear, serve delete, ACP delete, subagent
  delete, desktop trash — so deleted conversations cannot leak or be
  resurrected; guardian sidecars are excluded from transcript discovery.
- Guard desktop trash/delete with an external-lease busy check so removing
  lease files cannot break cross-process mutual exclusion.
- Migrate frozen-checkpoint readers to event-aware APIs: prompt history,
  serve session list, auto topic titles, duplicate-trash comparison,
  history search recency, and doctor byte totals.
- Count recovery-copy activity in topic summaries, hide recovery-only
  topics only when unpinned with no live runtime session, and mark
  conflict copies in the session tree and History drawer.
2026-07-05 13:34:23 +08:00
SivanCola 5311e0c9eb fix: open lock handle with DELETE access and commit overlong renames atomically
The Windows delete-disposition path only works when the handle carries
DELETE access, but os.OpenFile requests just GENERIC_READ|GENERIC_WRITE, so
RemoveAndUnlock always fell through to the post-close os.Remove fallback —
reopening the exact cleanup-vs-saver window the unlink-under-lock ordering
was meant to close (invisible to single-threaded CI). Open the lock file
with CreateFile(GENERIC_READ|GENERIC_WRITE|DELETE) and full share mode so
the disposition call runs on the locked handle; a Windows-only test pins
that the fallback counter never advances. A plain open of a lock file now
reads as held (sharing violation), matching the flock-held contract.

renameOverlongSession committed the transcript rename and then folded any
sidecar-migration error into a single return that dropped newID, so the
caller skipped re-parenting even though the old name was already gone —
orphaning child branches with no way to recover the mapping on a later run.
Split the rename (retryable: on failure nothing moved) from post-rename
migration (best-effort: newID is returned with joined warnings) so children
always re-parent once the transcript has moved.
2026-07-05 13:34:15 +08:00
SivanCola 963b43c625 fix: delete held lock files via handle disposition on Windows
Windows refuses a path-based os.Remove of a file this process still holds
open, so the unlink-under-lock cleanup introduced for lock/lease sidecars
failed with a sharing violation on every removal (windows-latest CI).

Replace the closure-based try-lock with a sessionLockFile handle whose
RemoveAndUnlock deletes the file atomically with the release: Unix unlinks
while the flock is still held, Windows marks the delete disposition on the
held handle so the name dies with it. Lease info sidecars keep the plain
remove-under-lease-lock path — nothing ever holds them open. The overlong
rename pass retires the old .lock through the same primitive.
2026-07-05 13:00:54 +08:00
SivanCola e5494594c2 fix: close rewrite-persistence races and migrate overlong session names
Capture the rewrite version when the snapshot decision is made and record
that captured version monotonically after a successful write, instead of
re-reading it afterwards: a compaction landing mid-save could be marked
persisted without ever reaching disk, turning the next autosave into a
spurious conflict and a recovery branch. A stale no-rewrite decision now
retries once as an owned rewrite, and disk-adoption resets the baseline to
the adopted session instead of inheriting the replaced session's counter.

Unlink stale lock/lease sidecars while still holding their flocks so a
concurrent saver cannot acquire an inode that is being deleted under it.

Rename transcripts whose basenames outgrew the sidecar name limit (the
leftover shape of the unbounded recovery cascade, #5923): conversation
bytes move verbatim to a bounded name, branch meta migrates with its ID
rewritten, children are re-parented, and disposable lock/lease files are
retired. Sessions past the limit could never be saved again because their
lock sidecars were uncreatable.
2026-07-05 12:43:15 +08:00
SivanCola 0f7f43e744 fix: prevent cascading session recovery 2026-07-05 11:50:05 +08:00
SivanCola 564afe1981 Fix session recovery persistence cleanup 2026-07-05 11:37:24 +08:00
SivanCola 96ff4b6503 Merge remote-tracking branch 'origin/main-v2' into fix/desktop-runtime-locks
# Conflicts:
#	internal/agent/session.go
2026-07-04 23:49:27 +08:00
SivanCola 1e4f38271c fix(control): close session data races on rewind/compact/turn and sidecar meta
Rewind truncated the live message slice in place, racing History/Save readers
on other goroutines; use Session.Snapshot/Replace and a lock-guarded Len().
Compact lacked the running-turn guard Rewind/Branch have, letting a manual
compact rewrite the log under the run loop. Run's turn++ was unlocked. The
branch-meta sidecar RMW helpers (UpdateSessionMeta, in-flight markers,
model/touch) now hold the per-path save lock so concurrent writers can't drop
each other's fields. Document the promptMu/sink.Emit contract.
2026-07-04 23:28:38 +08:00
SivanCola 565216c28c Skip revision bump on no-op session snapshot
checkSnapshotWrite now reports when the on-disk transcript already
matches the pending write byte-for-byte. save() skips the rewrite and
revision bump in that case instead of unconditionally recording a new
revision.

Without this, a routine no-op autosave (e.g. an idle tab's shutdown
snapshot) advanced the on-disk revision under any other runtime that
had cloned this file's persistence baseline via CloneWithMessages(If
Compatible). That runtime's next legitimate append then read as a
stale-runtime conflict and forked a spurious recovery branch — the
same symptom this PR fixes, reintroduced through a different trigger.

A load-time repair (LoadSession normalizing dangling tool calls, empty
names, truncated args) still forces a real write even when digests
match, so the repair actually reaches disk instead of being skipped as
a no-op forever.
2026-07-04 23:18:12 +08:00
SivanCola fd9f8027f4 Guard session baseline cloning 2026-07-04 21:52:20 +08:00
SivanCola 62cbf89624 Fix session rewrite recovery loops 2026-07-04 21:13:46 +08:00