Compare commits

...
415 Commits
Author SHA1 Message Date
SivanCola 4e1abd4a84 Merge pull request #8414 from esengine/release-notes/v1.24.0
docs(release): 准备 v1.24.0 中英更新日志
2026-08-11 23:32:53 +08:00
github-actions[bot] d69d7728fc docs(release): prepare v1.24.0 notes
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.

Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
2026-08-11 15:24:56 +00:00
SivanCola 42c17ccb29 fix(catalog): make directory and permission tests cross-platform (#8408)
Problem: main-v2 push CI fails on the Windows leg in
projectiondb.TestOpenAppliesLedgerAndPrivatePermissions and
sessioncatalog.TestRebuildFailureKeepsExistingCatalog, introduced by
the sqlite catalogs integration and never executed on Windows because
the PR smoke suite omits these packages.

Root cause: os.ReadDir of a plain file returns ENOTDIR on POSIX but
yields the file itself on Windows (FindFirstFile semantics), so Rebuild
silently treated a file path as an empty directory. The projectiondb
permission assertion assumes POSIX 0600, which os.Chmod cannot express
on Windows (read-only bit only).

Fix: directorySignature stats the path first and rejects non-directories
on every platform; the permission assertion skips on Windows while
keeping the ledger assertions.

Verification: gofmt, go vet (darwin + GOOS=windows), go test -race on
both packages, repolint clean.
2026-08-11 22:44:17 +08:00
SivanCola 2043834ef7 Merge pull request #8373 from SivanCola/fix/goal-continuous-runtime
fix(goal): Make Goal execution continuous by default / Goal 默认连续执行
2026-08-11 21:57:58 +08:00
SivanCola f40777ee76 merge: integrate execution-role runtime with continuous Goal
Problem: main-v2 advanced with the execution-role and completion-summary stack while PR #8373 was in CI, leaving the Goal runtime branch conflicted.

Root cause: both changes touch the Desktop and ACP runtime DTO owners and their repolint baselines.

Fix: merge main-v2, preserve the upstream execution-role behavior, retain the additive Goal workDurationMs mappings, and combine the exact repolint baselines.

Verification: git diff --check; go run ./tools/repolint
2026-08-11 21:37:52 +08:00
SivanCola 75537490ab Merge pull request #8275 from SivanCola/feature/agent-role-settings
feat(agent): light/balanced/delivery execution settings with unified tools / 执行设定与统一工具面
2026-08-11 21:23:19 +08:00
SivanCola 8ccd09f983 merge: integrate catalog runtime into continuous Goal work
Problem:
main-v2 advanced with the SQLite catalog integration while this PR's final CI was running, leaving the continuous Goal branch conflicting and unmergeable.

Root cause:
Both branches update Desktop runtime DTO owners and the repolint baseline; the catalog merge also extracts substantial Desktop code.

Fix:
Merge the latest main-v2 catalog stack, preserve the Goal workDurationMs contract, and carry forward only its two-line file-size delta on top of the reduced Desktop app baseline.

Verification:
The updated integration tree is rerun through Goal, catalog, race, Desktop, frontend, cache, and repository lint gates before push.
2026-08-11 21:16:20 +08:00
SivanCola c038bcfda4 Merge latest main-v2 into execution settings
Resolve concurrent changes in desktop preset switching, frontend event contracts, bundle budgets, tests, and repository lint state.

Preserve the in-place execution-setting switch and completion-summary routing while retaining the latest catalog, billing, hydration, and runtime behavior from main-v2.

Verified with full Go and desktop tests, frontend test:all and production build, race checks, go vet, and repolint.
2026-08-11 21:07:49 +08:00
SivanCola 8e49c2a75d fix(agent): surface completion gaps on demand
Problem: End-of-turn quality summaries cluttered the desktop transcript and exposed internal receipt values, while the CLI always printed raw summaries.

Root cause: completion_summary events had no presentation policy separating normal completion, actionable exceptions, and verbose audit output.

Fix: keep ordinary summaries out of the transcript, show a compact warning for actionable gaps, expose localized details in the Changes panel, and reserve raw CLI receipts for verbose mode.

Verification: pnpm build; targeted frontend completion/workspace/controller tests; go test ./...
2026-08-11 20:45:41 +08:00
SivanCola 6db78d6902 Merge pull request #8257 from SivanCola/feature/sqlite-catalogs-integration
Integrate disposable SQLite catalogs (session/history/usage/task) / 整合可丢弃 SQLite 目录投影(会话/历史/用量/任务)
2026-08-11 20:34:26 +08:00
SivanCola df1f85cbbc chore(ci): acknowledge Goal runtime DTO growth
Problem:
The latest main-v2 merge tree adds two cohesive lines to each of the Desktop and ACP Goal runtime DTO owners, exceeding their existing file-size ratchets.

Root cause:
workDurationMs requires one additive compatibility field and one boundary mapping at each owner; splitting these contracts would reduce clarity.

Fix:
Raise only the two affected repolint file-size baselines by two lines. The increases are 0.0163% for desktop/app.go and 0.2478% for internal/acp/status.go.

Verification:
- go run ./tools/repolint
- git diff --check
2026-08-11 20:22:48 +08:00
SivanCola 200a32aa45 merge: sync continuous Goal runtime with main-v2
Problem:
The PR head predates newly merged main-v2 changes, so its previous green checks do not cover the current integration tree.

Root cause:
main-v2 advanced in overlapping Desktop runtime DTO, frontend, and capacity budget surfaces after the last PR update.

Fix:
Merge the latest main-v2 history while preserving continuous Goal semantics and the newly landed UI and bundle-budget changes.

Verification:
The merged tree is verified by the focused Goal, race, vet, Desktop, frontend, cache, and repository lint gates before push.
2026-08-11 20:22:25 +08:00
SivanCola 54a46607f5 merge(main-v2): integrate provider URLs and turn labels 2026-08-11 20:17:25 +08:00
SivanCola 9de6912bf4 fix(desktop): omit completion receipts from transcript
Problem:
The desktop transcript rendered an end-of-turn quality receipt that duplicated the assistant result and added visual noise to every qualifying turn.

Root cause:
Desktop converted completion_summary wire events into persistent transcript items and dedicated virtual rows.

Fix:
Keep the wire contract intact but explicitly ignore completion_summary events in the desktop controller. Remove the transcript row, card, styles, localization helpers, and the associated bundle allowance. Preserve CLI receipt rendering.

Verification:
- pnpm exec tsx src/__tests__/use-controller-stream-progress.test.ts
- pnpm exec tsx src/__tests__/transcript-process-fold.test.ts
- pnpm typecheck
- pnpm check:css
- pnpm build
- in-app browser reload and completion event check
2026-08-11 20:16:47 +08:00
SivanCola d15bfcd9e4 fix(desktop): clarify turn completion summaries
Problem:
Completion summaries exposed internal enum values and rendered as low-contrast notice text.

Root cause:
The transcript interpolated wire identifiers directly and reused the generic info-notice presentation.

Fix:
Localize presets, verdicts, review states, and gap kinds; hide zero metrics; add semantic completion tones and accessible labels; preserve stable wire values.
Reuse existing notice and badge recipes and narrowly update the raw startup budget for the localized card.

Verification:
- pnpm exec tsx src/__tests__/completion-summary.test.ts
- pnpm exec tsx src/__tests__/transcript-process-fold.test.ts
- pnpm build
- in-app browser DOM and visual check
2026-08-11 20:09:45 +08:00
SivanCola e56d163636 fix(desktop): unify turn action hover labels / 统一消息操作按钮悬停标签 (#8387)
* fix(desktop): unify turn action hover labels

Problem:
Message turn actions used inconsistent label behavior: Copy collapsed to an icon while Fork, Compress, and Rewind always occupied their full labeled widths.

Root cause:
The three non-copy actions rendered permanent text and disclosure icons without the inline collapsed-label contract used by CopyButton.

Fix:
Wrap Fork, Compress, and Rewind labels in a shared collapsible container. Expand it on hover and keyboard focus, and keep it expanded while a menu is open or Fork is confirming. Preserve disabled and menu semantics.

Verification:
- pnpm exec tsx src/__tests__/turn-actions-rendering.test.ts
- pnpm build
- Real-browser hover, focus, disabled-state, and menu-open checks

* chore: ratchet Message size budget

Problem:
The turn-action markup adds four intentional wrapper lines and raises Message.tsx from 891 to 895 lines, exceeding its baselined 91-line file-size allowance.

Root cause:
The repolint baseline still reflected the file's previous overage and therefore rejected the otherwise verified UI change.

Fix:
Raise only Message.tsx's file-size allowance from 91 to 95, matching the exact four-line increase without relaxing any other repository standard.

Verification:
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/turn-actions-rendering.test.ts
- pnpm build
- git diff --check

* chore(desktop): ratchet raw bundle budget

Problem:
The latest main-v2 merge plus the turn-action label styles produces 2254.9 KiB of initial raw JavaScript and CSS, 0.9 KiB above the 2254 KiB ceiling.

Root cause:
The shared hover/focus label contract is always-available UI CSS and was not represented in the raw startup budget.

Fix:
Raise only the initial raw JavaScript and CSS allowance from 2254 to 2255 KiB and document the turn-action label cost. Keep all gzip and per-chunk budgets unchanged.

Verification:
- pnpm build (2254.9 KiB / 2255.0 KiB)
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/turn-actions-rendering.test.ts
- git diff --check
2026-08-11 20:07:35 +08:00
SivanCola 64f70f3fad feat(provider): exact custom request URLs / 自定义供应商使用精确请求地址 (#8386)
* feat(provider): add exact custom request URLs

Problem:
Custom provider setup mixed base URLs and full request URLs, while legacy chat_url semantics differed across protocols.

Root cause:
The desktop editor reused chat_url for both legacy compatibility behavior and new exact endpoint intent.

Fix:
- add request_url as an explicit exact endpoint field
- preserve legacy chat_url behavior for existing OpenAI, Anthropic, and Responses configs
- simplify the desktop editor to one API address field and migrate only on save
- document and test global and project config round trips

Verification:
- frontend production build and focused editor tests
- focused provider, config, boot, and desktop tests
- desktop go test ./...

* fix(provider): preserve base URL on unchanged endpoint

Problem:
Saving an existing custom provider could rewrite base_url from request_url, changing model discovery and endpoint-derived capabilities even when the user had not changed the API address.

Root cause:
ProviderEditor always derived base_url from the effective request URL instead of preserving the independent stored value for unchanged providers.

Fix:
Preserve the stored base_url when provider kind and effective request URL are unchanged, and derive a new base only for new providers or intentional endpoint/protocol changes. Extract endpoint compatibility logic and provider request URL tests into focused owners, with exact repolint ratchets for the cross-layer contract.

Verification:
- pnpm test:all && pnpm build
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses
- go vet focused root packages and desktop ./...
- go run ./tools/repolint

* fix(provider): mirror exact URL for legacy OpenAI

Problem:
Older clients ignore request_url and could keep using a stale chat_url after a custom OpenAI-compatible provider was saved with a new exact endpoint.

Root cause:
The desktop editor and save boundary preserved chat_url independently from the authoritative request_url.

Fix:
Mirror the exact OpenAI-compatible request URL into legacy chat_url in both the editor payload and desktop persistence boundary, retain non-OpenAI legacy behavior, and document the downgrade limitation. Add focused downgrade-compatibility coverage and complete the Chinese specification.

Verification:
- pnpm test:all
- pnpm build
- go test ./...
- (cd desktop && go test ./... && go vet ./...)
- go test -race ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses
- go vet ./internal/provider/openai ./internal/provider/anthropic ./internal/provider/responses ./internal/config ./internal/boot
- go run ./tools/repolint
- git diff --cached --check

* test(provider): align exact URL copy assertion

Problem:
The provider editor test still asserted the previous English helper copy after the production hint was shortened without changing its meaning.

Root cause:
The final copy-size adjustment and its standalone TSX assertion were not updated together.

Fix:
Assert the current unchanged-URL helper text so the focused editor test covers the shipped copy again.

Verification:
- pnpm exec tsx src/__tests__/provider-editor-model-picker.test.tsx
- pnpm build
- go run ./tools/repolint
- git diff --cached --check
2026-08-11 20:06:17 +08:00
SivanCola aa51a18498 fix(history): cancel projection backlog on close 2026-08-11 19:55:36 +08:00
SivanCola be91b4ecdc merge(main-v2): integrate billing and task controls 2026-08-11 19:38:08 +08:00
SivanCola 329beab244 fix(sessioncatalog): serialize projection mutations 2026-08-11 19:32:05 +08:00
SivanCola 6d262926e0 fix(desktop): streamline task and toolbar controls (#8392)
Problem:
The task monitor exposed duplicate stop and cancel actions with a cramped confirmation flow, while the topic toolbar repeated command and shortcut entry points and placed the workspace control inconsistently.

Root cause:
The desktop UI surfaced backend control aliases as separate primary actions and accumulated utility controls without a clear priority order or complete localization.

Fix:
Merge active-task termination into a single accessible stop flow, localize task state and session-summary labels, remove redundant toolbar entry points, and place session summary before the workspace toggle. Keep the underlying cancel APIs and keyboard/settings access paths intact.

Verification:
- pnpm exec tsx src/components/TaskMonitorPanel.test.tsx
- pnpm exec tsx src/__tests__/app-chrome-tabs.test.ts
- pnpm check:css
- pnpm typecheck
- Browser verification at 127.0.0.1:5174
2026-08-11 19:07:20 +08:00
SivanCola ee17c62901 Unify multi-currency billing across all surfaces / 统一全链路计费与币种 (#8058)
* feat(billing): unify multi-currency cost accounting

Problem:
Reasonix exposed provider prices, wallet balances, and session totals through independent currency paths. Mixed model runs could clear or mislabel costs, and display preferences could mutate regional list prices.

Root cause:
The runtime had no currency-tagged money domain, occurrence-time valuation, or shared quote boundary. Individual sinks accumulated floating-point Pricing.Cost values and inferred display semantics locally.

Fix:
Introduce fixed-point Money, CostQuote, official regional price catalogs, ECB FX snapshots, occurrence-time ledgers, wallet conversion, config v6 migration, and a central quote sink shared by Desktop, Serve, CLI, ACP, Remote, stats, and eventwire. Preserve additive legacy aliases and document the new contracts in English and Chinese.

Verification:
- go test ./...
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm typecheck && pnpm test:usage-stats && pnpm build)
- go test -race ./internal/billing ./internal/event ./internal/stats ./internal/boot ./internal/config ./internal/cli ./internal/acp ./internal/serve
- go test -race ./internal/billing ./internal/acp ./internal/eventwire ./internal/event
- golangci-lint run --timeout=5m
- (cd desktop && golangci-lint run --timeout=5m)
- go run ./tools/repolint
- ./scripts/cache-guard.sh
- CACHE_IMPACT_PR_BODY_FILE=/tmp/reasonix-billing-pr-body.md ./scripts/check-cache-impact.sh
- DOCS_IMPACT_PR_BODY_FILE=/tmp/reasonix-billing-pr-body.md ./scripts/check-docs-impact.sh
- git diff --check

* fix(billing): keep cost totals complete and lossless

Problem:
Exact original-currency totals could be marked incomplete when an unused FX valuation was unavailable. CLI and ACP recomputed authoritative totals from a 64-quote sample, and live controllers retained the pre-refresh FX table pointer.

Root cause:
CostQuote completeness conflated cost-fact availability with optional valuation coverage. Run surfaces used bounded quote slices as ledgers, while QuoteContext captured a one-time RateTable snapshot.

Fix:
Make completeness follow the selected shared display total, preserve unrecoverable fact failures, and keep incomplete frontend quotes unavailable. Accumulate CLI and ACP totals through the billing ledger, including restored ACP scalar history, and read the current FXCache snapshot for every quote.

Verification:
- go test ./...
- go test -race ./internal/billing ./internal/event ./internal/boot ./internal/cli ./internal/acp
- (cd desktop && go test ./... && go vet ./...)
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- ./scripts/cache-guard.sh
- go run ./tools/repolint

* refactor: remove runtime FX from billing

Problem: runtime FX downloads and wallet conversion mixed pricing estimates with provider balances and could fabricate cross-currency totals.

Root cause: CostQuote, ledger aggregation, display selection, and wallet rendering shared one currency path and treated display completeness as cost completeness.

Fix: keep fixed-point original facts, restrict new valuations to identity and official regional tables, add explicit cost/display status and currency buckets, remove ECB startup/cache paths, and use exact original wallet balances with runtime-only hints.

Verification: go test ./..., desktop go test ./..., targeted race suites, frontend typecheck/usage-stats/build, cache guard, cache-impact review, and secret scan.

* Fix display-only currency rebinding

Problem: changing the billing display currency still rebuilt Desktop controllers, stale wallet responses could bind a newer session, and two obsolete locale-pricing helpers failed golangci-lint.

Root cause: the display preference reused the general runtime-settings rebuild path, while wallet callbacks validated only the controller pointer and had no tab/session display generation.

Fix: persist the preference without rebuilding, re-select existing ledger valuations across runtime tabs, compare-and-apply wallet hints against controller and telemetry generation, and remove the obsolete locale-pricing paths.

Verification: focused RED/GREEN tests; Desktop focused race tests; go test ./... in root and desktop; go vet; golangci-lint; repolint; cache-impact check.

* Clarify cost display currency settings

Problem: the Desktop setting still used ambiguous pricing terminology and packed the auto-selection rule into the segmented control.

Root cause: the billing refactor renamed the preference but left the surrounding appearance copy and compact option label partially aligned with the old pricing semantics.

Fix: present one cost-display preference, mark auto as recommended, explain wallet, pricing-table, fallback, and mixed-currency behavior in the hint, and align English, Simplified Chinese, and Traditional Chinese copy.

Verification: source contract scan; pnpm typecheck; pnpm build with bundle budgets.

* Shorten cost display currency guidance

Problem: the Desktop cost-display hint wrapped to roughly four lines and visually dominated the setting row.

Root cause: fallback and mixed-currency details duplicated behavior already surfaced by the cost UI.

Fix: retain only the display-only contract and automatic currency-selection order in concise English, Simplified Chinese, and Traditional Chinese copy.

Verification: in-app browser DOM measurement at 904x936; pnpm typecheck; pnpm build with bundle budgets.

* Fix narrow settings control overflow

Problem:
- General settings kept its two-column layout at narrow desktop widths, clipping controls at the modal edge.

Root cause:
- A component-specific 900px override won over the shared 980px responsive rule and sized the row independently of its actual content pane.

Fix:
- Make the general page an inline-size container and stack fields below 620px of available width.
- Align the media-query fallback with the shared 980px breakpoint.
- Add responsive and startup contract coverage.

Verification:
- pnpm test:settings-responsive
- pnpm exec tsx src/__tests__/startup-settings-contract.test.ts
- pnpm check:css
- pnpm typecheck
- pnpm build
- 904x936 browser DOM check: maximum control overflow is 0px
2026-08-11 19:02:51 +08:00
SivanCola 56fa661672 fix(taskcatalog): serialize project projections
Problem:
- Ubuntu CI intermittently observed Indexed=1 while ListPage returned no task after an observed FileStore save.

Root cause:
- Initial background reconciliation could scan an empty project concurrently with exact snapshot indexing, then mark the freshly indexed row missing after the exact write committed.

Fix:
- Serialize authority scans, exact snapshot indexing, and event indexing per canonical project key.
- Keep projects independent and keep all projection work outside the authoritative FileStore write path.

Verification:
- Observed-store regression passed 500 iterations and 200 race iterations.
- Full root tests, task catalog race tests, vet, root/Desktop golangci-lint, gofmt, repolint, and diff checks passed.
2026-08-11 18:50:25 +08:00
SivanCola 46a964613b fix(desktop): remove obsolete migration wrapper
The exact-path migration API replaced the slice-only wrapper. Remove the now-unused compatibility helper so the macOS golangci-lint unused check passes.
2026-08-11 18:39:17 +08:00
SivanCola fb6a37b39d fix(desktop): prioritize newly migrated catalog sessions
Problem:
- Linux desktop CI timed out after a new CLI session migration because topic metadata was durable while the catalog row remained behind a generic directory reconcile.

Root cause:
- Explicit reconcile did not retain the exact successful migration paths, so loaded catalog workers could delay visibility past the project-tree deadline.

Fix:
- Return exact successfully migrated session paths and index them through the lightweight path projection before queuing the full directory reconcile.
- Preserve background, non-blocking behavior and the eventual directory checkpoint.

Verification:
- Migration regression tests passed 100 iterations.
- Targeted race tests passed 20 iterations.
- Desktop tests, vet, gofmt, repolint, and diff checks passed.
2026-08-11 18:32:22 +08:00
SivanCola 182755444d test(goal): keep continuous runtime within lint budgets
Extract the spend-slice helper and move newly added compatibility coverage out of historical oversized test files without changing behavior.
2026-08-11 18:26:35 +08:00
SivanCola 1713e5a131 Merge latest main-v2 diagnostics and goal budgets
# Conflicts:
#	desktop/frontend/src/locales/zh.ts
#	tools/repolint/baseline.json
2026-08-11 18:22:42 +08:00
SivanCola 88f4562f74 merge(main-v2): integrate desktop crash diagnostics
Problem:
- main-v2 advanced with #8169 while #8257 was being repaired and retested.
- desktop/app.go conflicted because #8169 extracted shutdown into a lifecycle-diagnostic wrapper while #8257 changed startup and shutdown ordering.

Root cause:
- Both branches owned the Desktop lifecycle boundary for independent correctness guarantees.

Fix:
- Preserve lifecycle diagnostics initialization and clean/panic attribution.
- Set shuttingDown and cancel tab/catalog workers before entering the diagnostic shutdown wrapper.
- Keep the extracted shutdown body and retain #8257's 250ms derived-catalog flush.

Verification:
- Desktop Go tests and full race suite.
- Frontend test:all and production build with bundle budgets.
- Windows amd64 CGO-free cross compilation.
- Affected root tests and race tests, root/Desktop vet, gofmt, repolint, and diff checks.
2026-08-11 18:18:04 +08:00
SivanCola befa639535 merge: sync latest main-v2 desktop diagnostics 2026-08-11 18:16:15 +08:00
SivanCola 6005e9a23d merge: integrate opt-in Goal spend budget from main-v2
Preserve continuous Goal semantics and cumulative runtime statistics while accepting the newly merged, user-configured token budget. Keep structural no-progress guards corrective-only and retain downgrade-safe sidecar migration.
2026-08-11 18:16:03 +08:00
SivanCola 8322583a60 fix(cli): fence usage catalog shutdown
Problem:
- macOS CI could finish runAgent while the asynchronous usage catalog still opened or reconciled inside a test TempDir.
- TempDir cleanup then raced a late SQLite write and failed with a non-empty usage-catalog directory.

Root cause:
- CLI shutdown flushed accepted stats records but did not cancel and join the process-local usage projection worker.
- Focused tests call runAgent directly, bypassing the public Run shutdown boundary.

Fix:
- Flush and close usage catalogs at the public CLI boundary.
- Apply the same idempotent fence to direct runAgent invocations.

Verification:
- Failing macOS test repeated 100 times.
- Focused race test repeated 10 times.
- Full internal/cli tests and stats/usage catalog race tests.
- go vet, gofmt, repolint, and diff checks.
2026-08-11 18:09:58 +08:00
SivanCola ed00eccf32 Add cross-platform Desktop crash diagnostics and runtime attribution / 增加跨平台桌面崩溃诊断与运行时归因 (#8169)
* fix(desktop): add Windows crash attribution diagnostics

Problem: Windows LTSC 2019 abrupt exits could not be attributed reliably, test telemetry could reach production, and Diagnostics counted lifecycle events rather than affected installations.

Root cause: legacy startup state was replayable, WebView2 failures were inferred from log text without native process details, and the crash Worker lacked build-, runtime-, and installation-level aggregation.

Fix: fail-close Desktop test endpoints, consume legacy state atomically, add lifecycle v2 and native WebView2 Args2/Args3 observation, and extend privacy-bounded telemetry, D1 rollups, Diagnostics filters, and bilingual release runbooks.

Verification: go test ./... in root and desktop; focused race tests; Worker typecheck and 86 tests; frontend tests and production build; fresh and migrated SQLite schema validation; Windows amd64 Desktop and vendored WebView2 cross-compilation; git diff --check.

* refactor(diagnostics): satisfy owner size budgets

Problem: the initial diagnostics implementation exceeded the repository's incremental file and function size budgets in Desktop startup, Worker ingestion, and dashboard rendering owners.

Root cause: lifecycle initialization, installation aggregation, and group-detail rendering were added directly to already baselined entry-point files.

Fix: move lifecycle setup into its tracker owner, extract diagnostics-v2 storage and query policy, and isolate crash-group rendering without changing wire contracts or runtime behavior.

Verification: repolint; Worker typecheck and 86 tests; focused Desktop and repair tests; Windows amd64 Desktop cross-compilation; the unrelated transient updater test passed three consecutive focused reruns.

* test(repair): use WaitGroup.Go for claim race

Problem: the current PR lint rejected the manual WaitGroup Add/go/Done pattern in the concurrent legacy-state claim regression test.

Root cause: the test used the legacy goroutine setup even though the repository modernize rules require sync.WaitGroup.Go.

Fix: use WaitGroup.Go while preserving the explicit ready barrier and deterministic interleaving.

Verification: go test ./internal/repair; go test -race ./internal/repair; go run ./tools/repolint; git diff --check.

* fix(repair): serialize legacy startup claims

Problem: Windows CI observed zero reports when concurrent launches tried to consume one dead legacy startup record.

Root cause: concurrent pre-claim reads can keep Windows file handles open long enough for every rename attempt to fail.

Fix: acquire the existing non-blocking cross-process file lock before reading and atomically renaming the legacy record, so exactly one observer owns the claim path.

Verification: 100 concurrent-claim repetitions; 20 race-enabled repetitions; full internal/repair normal and race suites; Windows amd64 test compilation; repolint; git diff --check.

* fix(diagnostics): close Windows crash attribution gaps

Start lifecycle tracking after the Wails single-instance gate and serialize anonymous install identity creation across concurrent callers.

Wait for WebView2 navigation completion before recording renderer recovery, commit crash group/sample/aggregate writes in one D1 batch, and align runtime CLI schema and diagnostics ordering.

* fix(diagnostics): address PR review findings

Problem:
- Renderer recovery could consume an unrelated navigation and recoverable COM errors could terminate the desktop.
- Per-install daily rows overwrote WebView2 dimensions, relabeling earlier GPU and runtime events.
- The PR exceeded repository size gates and did not execute vendored patch tests in Windows CI.

Root cause:
- Recovery completion was not correlated with NavigationStarting IDs, and shared fatal error handling was reused.
- Mutable dimension columns and event counts lived in the same deduplicated installation row.

Fix:
- Correlate renderer reloads by navigation ID, keep recovery errors non-fatal, and default failed kind reads to unknown.
- Add immutable event-dimension facts for filters and distributions while retaining installation deduplication.
- Split diagnostics tests, trim size regressions, and run vendored WebView2 tests in Windows CI.

Verification:
- go test ./...
- targeted Desktop and internal/repair race tests
- Worker tests and typecheck
- Frontend 146-suite test run
- repolint, actionlint, gofmt, and Windows cross-compile

* fix(desktop): align raw bundle ratchet

Problem:
The Desktop CI inherited a main-v2 raw startup bundle size of 2204.8 KiB while the ratchet still allowed only 2203 KiB, so frontend builds stopped before all Desktop checks.

Root cause:
Recent always-loaded desktop contracts already moved the baseline beyond the old threshold, and this PR adds the required diagnostics privacy attribution copy without materially changing gzip size.

Fix:
Raise only the raw aggregate allowance to a tightly measured 2205 KiB and document the native diagnostics copy. Keep gzip, CSS, locale, and largest-chunk limits unchanged.

Verification:
- pnpm build
- go run ./tools/repolint
- git diff --check

* feat(desktop): add cross-platform crash diagnostics

Problem: Desktop runtime failures lacked reliable ownership, lifecycle attribution, and affected-install analytics across Windows and Linux.

Root cause: legacy startup records could replay, WebView2 diagnostics were platform-specific, and Worker aggregation mixed recovery outcomes into unstable groups.

Fix: add fail-open primary ownership and lifecycle v2, generic Web Runtime reports, native WebView2 and WebKitGTK observers, privacy-safe platform dimensions, additive D1 aggregates, and cross-platform Dashboard filters.

Verification: go test and race suites, go vet, golangci-lint, Worker typecheck/tests, frontend tests/build/bundle budgets, repolint, actionlint, and Windows amd64 cross-builds.

* fix(diagnostics): preserve future lifecycle schemas

Problem:
A future lifecycle schema that removes v2 PID or phase fields could be treated as an invalid v2 record and pruned after retention.

Root cause:
The consumer validated v2-specific fields before checking the schema version.

Fix:
Reject unknown schemas before applying v2 validation or cleanup, and cover an aged future record without current fields.

Verification:
- cd desktop && go test . -run '^TestDesktopLifecycle'
- cd desktop && go test -race . -run '^TestDesktopLifecycle'
- git diff --check

* fix(diagnostics): correlate WebKit reload events

Problem:
The native WebKitGTK recovery smoke classified a successful renderer reload as failed and could not reach the cooldown assertion.

Root cause:
WebKit can emit load-failed for the terminated navigation after web-process-terminated. The recovery state treated every pending load failure as belonging to the new reload.

Fix:
Track whether the recovery reload has emitted LOAD_STARTED and only accept load-failed and LOAD_FINISHED after that boundary. Reset the marker on every terminal path and extend the source contract test.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* fix(diagnostics): defer WebKit recovery reload

Problem:
WebKitGTK 4.1 still timed out the success recovery smoke after load events were scoped to the recovery navigation.

Root cause:
Starting webkit_web_view_reload synchronously inside the web-process-terminated signal callback is unreliable while WebKit is still dispatching process teardown.

Fix:
Schedule the single recovery reload on the GTK main context's idle queue, retain the WebView until dispatch, and guard the helper with the pending recovery owner. Update the source contract test to enforce this deferred abnormal-only path.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* test(diagnostics): isolate WebKit smoke scenarios

Problem:
After the deferred reload made the success smoke pass, the next scenario aborted inside WebKitGTK while constructing another test WebView in the same process.

Root cause:
The smoke harness repeatedly destroyed and recreated WebViews that share WebKitGTK's process-global default context. That cross-scenario lifecycle does not match the single-WebView Desktop process and leaves asynchronous teardown active between cases.

Fix:
Run success, failure, timeout, and cooldown scenarios in isolated test subprocesses. Keep the cooldown pair in one child and one WebView so repeated native termination and single-reload behavior remain covered.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* test(diagnostics): keep WebKit smoke local

Problem:
The isolated success and cooldown smoke scenarios still reported reload failure while failure and timeout behaved correctly.

Root cause:
The test HTML used https://reasonix.invalid as its base URI. Reloading the terminated view attempted to resolve that unreachable URI, so the smoke measured a synthetic network failure instead of native process recovery.

Fix:
Load the smoke HTML with a NULL base URI, which WebKitGTK maps to local about:blank content and requires no network access.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* test(diagnostics): use reloadable WebKit smoke URI

Problem:
The native success and cooldown scenarios still timed out with load_html content even after removing the unreachable base URI.

Root cause:
A transient document created by load_html does not provide a stable reloadable resource after its WebProcess is terminated on the tested WebKitGTK runtime.

Fix:
Load a fully local data URI before terminating the WebProcess. This provides a stable current URI for the same production reload call without adding network or filesystem I/O.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* test(diagnostics): allow WebKit process startup

Problem:
The native smoke used a one-second recovery timeout and classified cold WebProcess startup under Xvfb as a failed reload.

Root cause:
The smoke-only timeout was substantially shorter than observed headless WebKitGTK process creation, while production correctly allows thirty seconds.

Fix:
Raise only the tagged smoke timeout to five seconds. Keep the production timeout at thirty seconds and the smoke safety timeout at fifteen seconds.

Verification:
GOCACHE=/tmp/reasonix-pr8169-go-cache go test . -run '^(TestWebKit|TestWebRuntime)'
git diff --check

* Automate diagnostics D1 migration before worker deploy

Problem:
The diagnostics-v2 schema had to be applied manually even though the production Cloudflare credential is available only to Actions. Deploying the new Worker first would make ingestion depend on missing D1 columns and tables.

Root cause:
The crash Worker workflow validated and deployed code but had no guarded database migration stage.

Fix:
Add a fail-closed schema inspector that skips a complete migration, applies the SQL only when entirely absent, rejects partial state, records the current D1 Time Travel bookmark, and verifies the final schema. Run it after tests and before Worker deployment.

Verification:
- npm test (96 tests)
- npm run typecheck
- node --check scripts/apply-diagnostics-v2.mjs
- bash scripts/release-workflows.test.sh
- actionlint v1.7.7
- git diff --check

* Keep diagnostics copy within locale bundle budget

Problem:
Merging the latest main-v2 bundle ratchets made the expanded Chinese telemetry disclosure exceed both locale budgets.

Root cause:
The cross-platform disclosure repeated verbose phrases for the anonymous identifier, runtime fields, abnormal-exit payload, and privacy exclusions.

Fix:
Compact the Simplified and Traditional Chinese copy while preserving upload timing, diagnostic dimensions, and excluded sensitive data. Keep the existing bundle budgets unchanged.

Verification:
- cd desktop/frontend && pnpm build
- node scripts/check-bundle-budget.mjs
- git diff --check
2026-08-11 18:09:41 +08:00
SivanCola 7ef3119011 merge(main-v2): integrate goal budget and desktop fixes
Problem:
- main-v2 advanced with #7584, #8101, and #8382 while #8257 CI was running.
- The locale bundle allowance and repolint baseline conflicted with the catalog integration branch.

Root cause:
- Both branches added localized desktop UI and changed files covered by repository size baselines.

Fix:
- Merge the latest appearance, capability, and Goal token-budget changes.
- Keep the combined locale output within #8257's existing narrow bundle limits.
- Regenerate the repolint baseline from the semantically combined tree.

Verification:
- Focused root and race tests for control, agent, boot, and catalogs.
- Desktop frontend test:all and production build with bundle budgets.
- Root and Desktop go vet, gofmt, repolint, and staged diff checks.
2026-08-11 18:00:07 +08:00
YHH e6d542bc94 feat(goal): replace the guessed turn quota with an opt-in token budget (#8382)
A Goal was bounded by 10/20/40 turns, picked by classifying its own text as
simple, write, or research. That is two guesses stacked: guess how big the
task is, then stop it on the guess. It was also the only axis, so a loop that
was slow and expensive never came close to it while a fast empty one hit it
for no reason.

Codex, which this feature follows, bounds /goal on a token budget that is
optional, and stops with a summary into a budget_limited state rather than
ending the goal. It has no iteration cap. This matches that: cumulative
tokens, off unless [agent].goal_token_budget is set, one summary and a
resumable budget_spend pause on reaching it.

Tokens are the axis that generalizes. A slow expensive loop accumulates them
and so does a fast empty one, since every iteration pays for at least one
prompt; wall clock catches only the first, and money is not portable across
models. #7725 removed token limits for a different reason — it enforced a
class-derived ceiling at every provider boundary, conflating cumulative
autonomous-work safety with per-request context size. This is the cumulative
check at the FSM's continuation decision, which is where the loop actually
decides to keep going.

Resume grants the budget again instead of returning into an exhausted one;
openai/codex#34215 is the same situation without that.

What still stops a Goal, all of it reading content rather than counting:
completion, a blocked report, a failed or uncertain evaluator, the structural
no-progress loop. TurnsLimit stays in the sidecar and the runtime view for
compatibility and is never set; GoalRuntimeView reports the real TokensLimit
again, which #7725 had pinned to zero.

Tests that used the quota as a termination condition now configure a budget
and bill usage against it. Several of them looped forever without that, which
is the shape of the design: an unattended loop with nothing configured has no
host-owned stop, by choice.
2026-08-11 17:54:24 +08:00
SivanCola 3a8dbceea0 Merge latest main-v2 capability notices
Problem:
The live main-v2 branch advanced with localized active-work errors while PR #8275 was being prepared.

Root cause:
Both changes touched execution-setting notices and Chinese locale bundle budgets.

Fix:
Merge the upstream capability guidance, retain the adopted execution-setting terminology, update its contract tests, and set narrow bundle budgets for the combined locale payload.

Verification:
- pnpm exec tsx src/__tests__/capability-busy-notice.test.ts
- pnpm exec tsx src/__tests__/use-controller-meta.test.ts
- pnpm build
- go run ./tools/repolint
2026-08-11 17:48:13 +08:00
SivanCola f4586c4af0 Merge latest main-v2 appearance fix
Problem:
The upstream main-v2 branch advanced again before PR #8275 could be pushed against the live base.

Fix:
Merge the latest Creation appearance overview swatch fix without changing the execution-setting implementation.

Verification:
- pnpm exec tsx src/__tests__/theme-pack.test.ts
- pnpm build
- go run ./tools/repolint
2026-08-11 17:43:59 +08:00
f4feee89d6 fix(desktop): localize active-work errors in capability settings / 优化任务运行时的能力设置错误提示 (#8101)
* fix(desktop): localize capability busy errors

Replace active-work debug details with localized guidance across MCP, skills, and plugin mutations while preserving unrelated actionable errors.

* refactor(desktop): isolate capability mutation helpers

Problem:
The capability busy-error change added code and tests directly to two files whose repolint size ratchets were already exhausted.

Root cause:
The helper and its focused coverage were owned by the large CapabilitiesPanel module and its broad action suite instead of dedicated files.

Fix:
Move capability mutation helpers into a focused library module and move the busy-notice regression cases into an auto-discovered test suite without changing runtime behavior.

Verification:
- pnpm exec tsx src/__tests__/capability-busy-notice.test.ts
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm build
- pnpm check:bundle
- go run ./tools/repolint
- git diff --check

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

* fix(desktop): localize subagent busy errors

Problem:
Subagent profile mutations still exposed active-work debug fields, and English background-job notices used plural grammar when only one job was active.

Root cause:
The Subagents settings wrapper bypassed the shared active-work notice mapper, while four English messages interpolated counts into plural-only sentences.

Fix:
Reuse the shared mapper for Subagents mutations and use count-neutral English wording across capability, model, effort, and work-mode notices. Add regression coverage for the Subagents wiring and single-job output.

Verification:
- pnpm exec tsx src/__tests__/capability-busy-notice.test.ts
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm exec tsx src/__tests__/use-controller-meta.test.ts
- pnpm lint:hooks
- pnpm typecheck
- pnpm build
- go run ./tools/repolint
- git diff --check

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

---------

Co-authored-by: xiaojia-afk <[email protected]>
Co-authored-by: SivanCola <[email protected]>
2026-08-11 17:42:53 +08:00
2694292c4e fix(creation): restore AppearanceOverview hero theme swatches / 修复 Creation 外观概览主题色块 (#7584)
* fix(creation): restore AppearanceOverview hero theme swatches

Creation compact theme-card swatch rules (7px pills) were unscoped and
crushed AppearanceOverview hero preview into a black block. Scope those
rules to .theme-card only and add Creation glass polish for the overview.

* fix(creation): keep Graphite gold swatches on AppearanceOverview hero

Only size/layout rules stay scoped under .theme-card; Creation Graphite
color rules remain unscoped so hero matches live --accent/#c99a3a gold.

* fix(creation): keep appearance hero within bundle budget

Problem:
- AppearanceOverview styling restored readable hero swatches but exceeded the frontend raw bundle budget.
- The fragile Creation-only selector and Graphite palette contract had no focused regression coverage.

Root cause:
- The PR duplicated the shared AppearanceOverview skin with more than one hundred lines of Creation overrides, even though only hero sizing and swatch growth needed specialization.

Fix:
- Reuse the shared overview skin and keep only the Creation-specific hero height and flex behavior.
- Add source-level contracts for real-card scoping, readable hero dimensions, and the Creation Graphite gold palette.

Verification:
- pnpm --dir desktop/frontend build
- pnpm --dir desktop/frontend exec tsx src/__tests__/theme-pack.test.ts
- git diff --check
- Browser computed-style checks in light, dark, and Workbench modes

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

---------

Co-authored-by: Li-Charles-One <[email protected]>
Co-authored-by: SivanCola <[email protected]>
2026-08-11 17:41:33 +08:00
SivanCola 0e1932d128 Merge main-v2 into execution settings
Problem:
The upstream main-v2 branch advanced while the execution-setting changes were under CI, leaving PR #8275 behind the live base and conflicting in the generated repolint baseline.

Root cause:
Both branches updated files covered by the baseline, so Git could not choose combined finding counts.

Fix:
Merge the three latest upstream fixes and regenerate the repolint baseline from the combined source tree.

Verification:
- go test ./...
- cd desktop && go test ./...
- go vet ./...
- go test -race ./internal/acp ./internal/control ./internal/agent
- go run ./tools/repolint
- cd desktop/frontend && pnpm test:all
- cd desktop/frontend && pnpm build
2026-08-11 17:40:34 +08:00
SivanCola 7a47e22353 merge(main-v2): resolve catalog integration conflicts
Problem:
- main-v2 advanced with #8335, #8347, and #8357 while #8257 was awaiting merge.
- The repository complexity baseline conflicted with the catalog branch's owner-file extractions.

Root cause:
- Both branches updated controller and frontend ownership measurements while evolving independent runtime behavior.

Fix:
- merge the latest protected main-v2 prompt-queue, todo-stall, and Goal runtime changes
- preserve the catalog session identity and hydration fences in the automatically merged frontend
- regenerate the repolint baseline from the combined tree without widening any measured budget

Verification:
- focused root/control/catalog tests
- focused race tests for control, agent, and all catalog packages
- Desktop Go test suite
- frontend test:all and production build with bundle budgets
- root and Desktop go vet
- gofmt, repolint, and staged diff checks
2026-08-11 17:40:05 +08:00
SivanCola 2d6d2f2afa merge: reconcile continuous Goal runtime with main-v2
Integrate the newly landed no-Todo-stall and no-Goal-round-ceiling changes from #8347 and #8357 without rewriting the published PR history.

Preserve this PR's broader continuous-runtime behavior: Goal Todo stalls still trigger a second-stage replan and epoch reset, while goal_stuck finalization remains removed.

Verified with full Go tests, agent/control race tests, go vet, repolint, Desktop tests, frontend typecheck/test/build, and cache guards.
2026-08-11 17:38:54 +08:00
YHH 767af5a1dc fix(goal): drop the per-Run round ceiling (#8357)
A Goal turn was cut at 16 model rounds. Rounds were the wrong unit there for
the same reason they were wrong for chat in #8320: a turn that reaches a
high count while still producing new work is the case least worth
interrupting, and one that produces nothing is caught by something that
actually measures that.

Writing the test for this showed the second half is already true. A
wandering Goal turn stops at round 13 with no ceiling present at all —
exploration decay stops crediting look-only rounds after six, the
zero-evidence ladder escalates to its stop six rounds later, and the Goal
structural guard ends the run. The ceiling was never what caught a stalled
Goal; it only ever cut productive ones short.

Goal keeps every boundary that reads what a turn is doing rather than
counting it: completion, a blocked report, a failed or uncertain evaluator,
the structural no-progress loop, and the cross-Run turn budget (which the
next change takes up). stopCauseGoalRunBudget stays as a legacy value so an
old sidecar still resumes.
2026-08-11 17:24:15 +08:00
YHH 792f5df047 fix(agent): remove the todo-stall pause (#8347)
A stalled todo could end a run: 8 rounds without a new completion, read,
command or mutation got a reassessment nudge, 16 got a resumable pause. The
pause goes; the nudge stays.

It was never the mechanism that caught the stall it claimed. The
zero-evidence ladder escalates on the same receipts and reaches its stop at
6, and the storm breaker reaches its own at 3 — writing the Goal test for
this proved it, since Goal never reached 16 at all: the ladder's structural
pause always fired first. So under Goal it was dead code, and in ordinary
chat it was the last remaining way for the host to end a turn the user never
asked it to end.

What it measured was also narrower than it read. Any first-time successful
read, command or mutation renews the streak, so 16 rounds meant 16 rounds
producing literally nothing new — a state the two guards above already
handle, twice over and far sooner.

todoStallPause and maxTodoStallRounds are gone with it, along with the
todo_stall pause class, its RunPauseInfo mapping, the planner's
non-finalization message for it, and the Goal FSM's stop cause.
2026-08-11 17:24:08 +08:00
YHH b8ac832d95 fix(control): a question queued behind another prompt is no longer invisible (#8335)
Ask took promptMu before it registered or emitted anything, so a question
raised while an earlier approval was still open existed only inside a
blocked goroutine. No AskRequest reached the frontend, the prompt snapshot
did not list it, and ReplayPendingPrompts therefore could not recover it —
not on a tab switch, not on a status reconcile. The user saw a tool card
that never opened a dialog while the turn blocked indefinitely, since
approvalTimeout defaults to zero and Ask waits without a deadline.

Registration now happens before the queue, which is what makes the waiting
question visible at all. It is marked queued so replay still skips it:
surfacing it early would put a question on screen ahead of the prompt it is
waiting behind, and the ordering promptMu exists to enforce is unchanged.

Waiting longer than a few seconds now says so. A prompt answered promptly
emits nothing; one stuck behind another explains why nothing has appeared
instead of leaving the user watching a dead interface.

Cancelling while queued drops the registration and hands the lock on rather
than leaking it to the next prompt.
2026-08-11 17:24:00 +08:00
SivanCola d8d73ca718 fix(goal): count slash subagent work duration
Problem:
Slash-invoked runAs=subagent Goal turns counted provider requests and tokens but left cumulative workDurationMs at zero.

Root cause:
The isolated answer was copied into the parent session without its turn duration, and this controller path bypassed the existing Goal duration capture.

Fix:
Stamp returned subagent answers with cumulative turn work time and commit the new-message maximum through the existing scope/epoch recorder. Add success and deterministic Goal-replacement regression coverage.

Verification:
- go test ./...
- go test -race ./internal/control
- go vet ./...
- scripts/cache-guard.sh
- scripts/check-cache-impact.sh
- go run ./tools/repolint
2026-08-11 17:16:27 +08:00
SivanCola 7294d86c9f Update boot documentation contract
Problem: CI failed in race, coverage, and platform test jobs after the product terminology changed.

Root cause: TestToolContractDocCoversDefaultBootSurfaces still expected the former role-settings heading.

Fix: Match the documentation contract assertion to the new execution-settings heading.

Verification: go test ./internal/boot; go test -cover ./internal/boot; go test -race ./internal/boot; go vet ./internal/boot; go run ./tools/repolint.
2026-08-11 17:11:33 +08:00
SivanCola 46923b9895 Rename role settings to execution settings
Problem: User-facing role terminology suggests a persona switch, while these presets actually control planning, verification, and delivery behavior.

Root cause: Desktop, CLI, ACP, and documentation retained the earlier role-setting product name.

Fix: Use Execution setting / 执行设定 / 執行設定 consistently across product surfaces while preserving agent_preset, work_mode, tokenMode, and compatibility values.

Verification: go test ./internal/i18n ./internal/cli ./internal/acp; cd desktop && go test ./...; cd desktop/frontend && pnpm test:all && pnpm build; local browser menu click and overflow check.
2026-08-11 17:04:07 +08:00
SivanCola 84c79d9c7f test(boot): release shared history catalog on cleanup
Problem:
Windows CI could not remove temporary REASONIX_HOME directories after boot tests completed because v1.sqlite remained open.

Root cause:
The indexed history catalog is process-global, while several Build and rebuild tests changed config homes without fencing that shared lifecycle. Unix unlink semantics masked the leaked handle.

Fix:
Add a deterministic boot-test catalog fence that closes inherited state before each affected build and closes the replacement before temporary-directory cleanup. Apply it consistently to model boot and session-temp rebuild coverage.

Verification:
- focused boot lifecycle tests, 20 repetitions
- focused race tests, 5 repetitions
- go test -count=1 ./...
- go test -race -count=1 ./internal/boot
- make lint
- go vet ./...
- Windows amd64 boot test binary cross-compile
2026-08-11 16:59:17 +08:00
SivanCola 642bc40268 Merge remote-tracking branch 'origin/main-v2' into feature/sqlite-catalogs-integration 2026-08-11 16:40:42 +08:00
SivanCola cf1733d035 fix(catalogs): rebuild projections atomically
Problem:
Catalog reindex commands only reconciled the existing database, metadata-only history edits could miss reconciliation, and concurrent rebuilds could race during replacement.

Root cause:
History, usage, and task entry points had no validated replacement pipeline; the history root signature ignored sidecar metadata; and projection rebuilds had no cross-process lifecycle lock.

Fix:
Build each derived catalog in a validated sibling database before atomic replacement, fence process-global managers during rebuild, retain task notifications across the fence, include metadata in history signatures, and serialize rebuilds with an advisory lock.

Verification:
- go test -count=1 ./...
- cd desktop && go test -count=1 ./...
- go test -race for boot and all affected catalog packages
- cd desktop && go test -race -count=1 ./...
- go vet for root and desktop modules
- make lint plus CI build-tag lint matrix
- CGO_ENABLED=0 builds for darwin, linux, and windows targets
2026-08-11 16:40:32 +08:00
SivanCola cebd0d5914 Merge context overflow display into role settings
main-v2 advanced after the exact role-settings CI completed with corrected over-limit context capacity rendering. Merge commit bb58eec2 so the pull request keeps the latest Desktop context UI and localized status contract without changing the role-setting behavior.

Verified with the full frontend test:all suite, remote and performance tests, production build, and bundle budgets.
2026-08-11 16:19:48 +08:00
SivanCola a4787fe45b test(goal): satisfy Go 1.22 integer-range lint
Problem:
The PR lint job rejected five new regression-test loops under the intrange rule.

Root cause:
The tests used classic counted for loops even though the repository targets Go 1.22 integer ranges.

Fix:
Rewrite the five bounded test loops with integer range syntax without changing their iteration counts or assertions.

Verification:
- golangci-lint run --timeout=5m
- go test ./internal/agent ./internal/control
- go run ./tools/repolint
- git diff --check
2026-08-11 16:08:15 +08:00
SivanCola bb58eec24e fix(desktop): correct context capacity overflow display / 修复上下文容量条超限显示 (#8307)
* fix(desktop): align context capacity meter with usage

Problem:
The context window meter mixed token composition into a capacity gauge, so the visible fill could disagree with the displayed usage. Over-limit values were also clamped to 100% without an explicit status.

Root cause:
The meter reused token breakdown segments and a single clamped percentage for both physical layout and user-facing text.

Fix:
Render a single capacity fill, preserve the raw percentage for labels and status, cap only the physical fill, keep the compaction marker, and apply the same contract to the context ring popover.

Verification:
- 68 focused context UI assertions
- pnpm typecheck
- pnpm check:css
- pnpm build
- Playwright Chromium DOM, hover, computed-style, accessibility, and console checks

* fix(desktop): preserve just-over-limit context status

Problem:
Context usage that exceeded the window by less than half a percentage point rounded to 100%, so the panel and ring hid the overflow.

Root cause:
The over-limit status was derived from the rounded integer percentage instead of the exact token counts.

Fix:
Keep the existing integer percentage contract, but force the raw value above 100 whenever used tokens exceed the window. Add focused helper, panel, and ring regressions for 1001/1000.

Verification:
- 69 focused context UI assertions
- pnpm build
- In-app Chromium DOM, hover, computed-width, accessibility, and console checks

* fix(desktop): align status bar context overflow

Problem:
The status bar still clamped over-limit context usage to 100%, so it disagreed with the context panel and ring.

Root cause:
The status bar kept a local percentage calculation instead of using the shared raw/display context-window contract.

Fix:
Move contextWindowPercentages into a shared frontend library and use its raw percentage for the status bar label while preserving capped graphical fills.

Verification:
- 114 focused context and status-bar assertions
- pnpm build
- Playwright Chromium DOM, visibility, and console check

* fix(desktop): satisfy context meter lint budget

Problem:
The shared context percentage helper pushed StatusBar.tsx one line beyond its baselined file-size budget, leaving PR #8307's lint job red.

Root cause:
formatRate used a two-branch statement where a behavior-equivalent conditional expression is sufficient.

Fix:
Keep the shared context percentage contract and simplify formatRate without changing its zero or negative denominator behavior.

Verification:
- go run ./tools/repolint
- 54 context panel assertions
- 6 capacity meter assertions
- 9 context ring assertions
- 45 status bar assertions
- pnpm test:typecheck
- pnpm build
2026-08-11 16:07:16 +08:00
SivanCola 1bf365af6e fix(goal): make execution continuous by default
Problem:
Goal mode could pause at host-owned numeric ceilings even while useful work was still progressing.

Root cause:
Default round, turn, wall-time, and stall thresholds were wired as terminal runtime boundaries and persisted as blocked Goal state.

Fix:
Remove host-owned Goal execution ceilings, preserve only user-selected resource limits, convert numeric stall guards into replanning redirects, migrate legacy numeric pauses, and expose cumulative work duration as observational runtime data.

Verification:
- go test ./...
- go test -race ./internal/agent ./internal/control
- go vet ./...
- cd desktop && go test ./...
- cd desktop/frontend && pnpm typecheck && pnpm test:all && pnpm build
- scripts/cache-guard.sh
- scripts/check-cache-impact.sh
- go run ./tools/repolint
2026-08-11 16:02:29 +08:00
SivanCola 22e152d684 Merge external WebView2 smoke into role settings
main-v2 advanced after the role-settings CI completed with the production WebView2 smoke moved out of the shipped Desktop binary. Merge commit 717497e2 so the pull request remains based on the latest reviewed startup and packaging contracts while preserving the role-setting UI and bridge paths.

Verified with root and Desktop Go suites, vet, repolint, targeted race tests, frontend test:all and production build, bundle budgets, and release workflow contract tests.
2026-08-11 15:59:49 +08:00
SivanCola e48a27ad63 merge(main-v2): externalize WebView2 release smoke
Problem:
- main-v2 advanced with #8333 while PR #8257 was being brought to an approvable state.
- App.tsx still carried the catalog branch's reference to the retired in-app smoke component.

Root cause:
- Both branches touched frontend startup while the base moved the WebView2 release check out of production code and into an exact-binary Windows smoke script.

Fix:
- Remove the retired production smoke import and preserve the external Windows startup gate, Chromium approval-animation gate, and release workflow contracts.

Verification:
- cd desktop/frontend && pnpm test:motion
- cd desktop/frontend && pnpm test:motion-browser
- cd desktop/frontend && pnpm build
- cd desktop && go test -count=1 ./...
- cd desktop && go test -race -count=1 ./...
- bash scripts/release-workflows.test.sh
- actionlint on changed CI/release workflows
- go run ./tools/repolint
- gofmt -l desktop internal cmd tools
- git diff --cached --check
2026-08-11 15:52:17 +08:00
SivanCola d64cfca759 merge(main-v2): align context occupancy accounting
Problem:
- main-v2 advanced again with #8329 after the catalog integration merge had been fully tested.

Root cause:
- The context gauge and compaction trigger previously measured different provider-visible inputs, and the frontend counted completion tokens as prompt occupancy.

Fix:
- Integrate the latest prompt/tool-schema accounting and frontend prompt-occupancy semantics without conflicts.

Verification:
- go test -count=1 ./internal/agent ./internal/provider ./internal/tool
- go test -race -count=1 ./internal/agent ./internal/provider ./internal/tool
- pnpm exec tsx src/__tests__/use-controller-stream-progress.test.ts
- pnpm build
- go run ./tools/repolint
- gofmt -l internal desktop
- git diff --cached --check
2026-08-11 15:45:31 +08:00
SivanCola 717497e25d fix(desktop): externalize WebView2 release smoke (#8333)
Remove test-only WebView2 approval instrumentation from the shipped Desktop executable. Preserve approval behavior coverage in real Chromium and validate the exact Windows production binary externally through UI Automation, including the real composer identity.
2026-08-11 15:43:36 +08:00
SivanCola 4867532885 merge(main-v2): integrate inbox recovery and task defaults
Problem:
- main-v2 advanced with #8270 and #8340 while the catalog integration was under review.
- Both branches changed the controller frontend and startup bundle budgets.

Root cause:
- The inbox recovery localization and opt-in task-spend defaults overlapped the catalog hydration guards and measured frontend budget ratchets.

Fix:
- Preserve the history hydration identity/error fences alongside locale-aware inbox recovery.
- Keep the latest task-spend defaults and use measured startup bundle budgets without widening the repolint baseline.

Verification:
- env REASONIX_RELEASE_CACHE_GUARD=1 go test -count=1 ./...
- env REASONIX_RELEASE_CACHE_GUARD=1 go test -race -count=1 ./...
- cd desktop && go test -count=1 ./...
- cd desktop && go test -race -count=1 ./...
- cd desktop/frontend && pnpm test:all && pnpm build && pnpm test:motion
- go vet ./... and pinned golangci-lint for native and cross-platform build tags
- CGO_ENABLED=0 root/Desktop builds for darwin amd64/arm64, windows amd64/arm64, and linux amd64
- Wails binding generation, repolint, release workflow contracts, SDK ordinary/race tests, and go mod tidy -diff
2026-08-11 15:43:20 +08:00
SivanCola bdb7fce7fa Merge context gauge fix into role settings
Problem: main-v2 advanced with context-gauge cache invalidation while this PR independently narrowed the provider-visible tool surface, creating a Registry conflict and a stale-cache risk when the allowlist changes.

Resolution: merge main-v2 at ee2a6a76, preserve both contracts, advance schema revision only for real provider-visible allowlist changes, and cover both mutation and no-op updates.

Verification: go test ./...; go test -race ./internal/tool ./internal/agent; go vet ./...; go run ./tools/repolint; cd desktop/frontend && pnpm test:all && pnpm build.
2026-08-11 15:40:53 +08:00
xiaotaoya2022-cellandSivanCola ee2a6a766f fix(agent,desktop): align context gauge with compaction trigger / 对齐上下文容量条与压缩触发器 (#8329)
* fix(agent,desktop): align the context gauge with the compaction trigger

The desktop context gauge measured something different from the
compaction trigger, in both directions:

- The backend gauge (ContextUsedTokens) used the message-only estimator
  while the trigger sizes messages + role projection + tool schemas
  (estimatedVisibleRequestTokens). With a non-empty tool registry the
  gauge under-reported the fill, so a session could display below
  compact_ratio while the trigger had already crossed it (the #8221 test
  passed only because its tool registry was empty).
- The desktop frontend overwrote the backend gauge with the last turn's
  real usage and added completion tokens to the used figure. A long
  output round inflated the displayed fill (e.g. 90%) while the trigger,
  which only counts the next request's prompt, still sat below the
  configured 85% — compaction never fired and the session ran into a real
  overflow.

Both now use one measurement: the gauge calls the trigger's own estimator,
and the frontend counts prompt tokens only. Completion stays in the output
metrics, not the occupancy gauge.

Refs: #8191

* fix(agent,desktop): keep context gauge current

Problem:
- Context usage stayed cached after live MCP tool changes.
- Completion-only latest usage could collapse prompt occupancy to zero.
- The PR also tripped formatting and repository budget gates.

Root cause:
- The memoization key omitted the provider-visible tool registry revision.
- Frontend fallback selected context prompt data based on unrelated completion data.

Fix:
- Track tool schema revisions and include registry identity plus revision in the gauge cache key.
- Select prompt and completion fallbacks independently and align ContextFillTokens with prompt occupancy.
- Add deterministic backend and frontend regression coverage while removing budget-breaking comments.

Verification:
- go test ./...
- go test -race ./internal/tool ./internal/agent
- go vet ./...
- pnpm build
- go run ./tools/repolint

---------

Co-authored-by: SivanCola <[email protected]>
2026-08-11 07:24:24 +00:00
SivanCola e121f6cd0f fix(desktop): fence MCP mutations from controller boot
Problem:
- A late asynchronous controller build could launch a shared-host MCP while an explicit reconnect was still replacing the same single-instance server.
- The loser failed with an address-in-use error, and an early generation check still allowed stale registries to publish after a later MCP mutation.

Root cause:
- Optimistic boot intentionally runs outside runtimeAdmissionMu, but shared-host extension startup had no dedicated mutation fence.
- extensionGeneration was checked before the final admission boundary instead of as part of publication.

Fix:
- Add a reader/writer extension gate so controller boots remain concurrent with each other while MCP mutations exclude shared-host startup.
- Bump the extension generation before releasing a mutation and revalidate it both after entering the boot gate and under publication admission.
- Extract boot failure handling from the oversized tab assembly path and add a deterministic publication-fence regression.

Verification:
- go test -count=50 -run 'TestAuthorizeAndConnectMCPServerBlocksLateControllerBuild|TestControllerPublicationRejectsMutationThatCompletesAfterBoot' .
- go test -race -count=10 -run 'TestAuthorizeAndConnectMCPServerBlocksLateControllerBuild|TestControllerPublicationRejectsMutationThatCompletesAfterBoot' .
- go test -count=1 ./...
- go test -race -count=1 ./...
- go vet ./...
- golangci-lint run ./...
- go run ./tools/repolint
- CGO_ENABLED=0 cross-builds for darwin, windows, and linux
2026-08-11 15:21:10 +08:00
SivanCola 6aa100e23c fix(agent): enforce no-external across command variants
Problem: TaskPolicy recognized a user's no-push/no-publish/no-deploy constraint, but the execution gate matched only literal command spellings and exact tool names. Valid global options or resolved capability names could bypass the host block.

Root cause: External-action detection used substring checks instead of the repository's static shell argv parser and did not tokenize resolved tool names.

Fix: Parse command segments and wrappers into static argv, conservatively recognize external action tokens across global options, fail closed for opaque dynamic subcommands, and classify resolved capability action tokens before execution.

Verification: focused classifier and end-to-end direct/use_capability tests; go test ./internal/agent ./internal/taskpolicy; go test -race ./internal/agent -run 'TaskPolicy|ExternalAction' -count=1; go vet ./...; go run ./tools/repolint.
2026-08-11 15:16:50 +08:00
SivanCola 83b3b04db0 Merge latest main-v2 into agent role settings
Problem: The target branch advanced with opt-in task spending and durable Inbox recovery, leaving the role-settings PR conflicted on the frontend bundle ratchet.

Resolution: Merge main-v2 at ced0eeeb, preserve both role-setting and Inbox contracts, and set narrowly measured locale/raw bundle ceilings for the combined tree.

Verification: go test ./internal/agent ./internal/taskpolicy ./internal/control ./internal/sessioninbox ./internal/config; cd desktop && go test ./...; focused Inbox frontend tests; pnpm test:typecheck; pnpm build; pnpm check:bundle.
2026-08-11 15:05:34 +08:00
YHH ced0eeeb9a feat(agent): ship no task limits; the spend gate is opt-in (#8340)
The wall-clock default went out with the round ceiling. Nothing bounds an
ordinary chat task now: not rounds, not tokens, not time, not money. It runs
until the model finishes, an adaptive guard finds it stopped making
progress, or the user stops it.

Deciding a task has run too long is a judgement only the user can make.
Money does not survive a model switch — a budget loose enough for DeepSeek
lands a frontier model within a couple of answers — and a task that runs for
an hour is as often the job that was asked for as it is a runaway. A default
that fires on real work is worse than no default, and neither axis has one
that would not.

The gate itself is unchanged and still there for anyone who wants it:
[agent].task_cost_budget and task_time_budget_minutes, each independent,
scoped to a whole task, landing on one tool-free summary and a resumable
pause. taskBudgetOrDefault becomes normalizeTaskBudget, which now only reads
a negative value as unset.

TestUnconfiguredBudgetNeverCrosses pins the promise: eight hours and 55M
tokens against an unconfigured budget cross nothing.
2026-08-11 14:51:38 +08:00
SivanCola cc28d83c1b Fix localized Inbox pause recovery / 修复收件箱暂停恢复与本地化 (#8270)
* fix(desktop): localize inbox state errors

Problem:
- Paused Inbox actions surfaced raw English backend errors in localized desktop sessions.
- Non-recovered paused queues had no actionable recovery surface and could still attempt guidance admission.

Root cause:
- Wails transported product-state text instead of stable identifiers, and the Composer rendered Error.message directly.
- Pause state was only propagated for recovered Inbox snapshots.

Fix:
- Encode known Inbox states as stable error codes while preserving Go error unwrapping and unknown diagnostics.
- Localize current and legacy error forms in English, Simplified Chinese, and Traditional Chinese.
- Propagate pause state, disable guidance admission, and expose resume controls for ordinary paused queues.

Verification:
- cd desktop && go test ./... -count=1
- Inbox localization tests: 18 passed
- Inbox recovery interaction tests: 13 passed
- cd desktop/frontend && pnpm build
- Verified the zh-CN pause and resume flow in a real browser

* fix(desktop): localize inbox cancellation failures

Problem:
- Inbox cancellation failures exposed English context or stable transport codes in localized sessions.
- PR checks were blocked by errorlint and ratcheted source-file budgets.

Root cause:
- The controller cancellation path bypassed the shared Inbox error formatter.
- An intentional error identity assertion lacked a scoped errorlint explanation, while added lines exceeded existing budgets.

Fix:
- Localize cancellation context and nested Inbox errors while preserving unknown diagnostics.
- Add formatter and controller regression coverage.
- Keep source budgets unchanged and document the intentional identity assertion.

Verification:
- go run ./tools/repolint
- cd desktop && go test ./... -count=1
- cd desktop && golangci-lint run --timeout=5m ./...
- cd desktop/frontend && pnpm build
- Inbox localization, cancellation reconciliation, and recovery regression tests

* fix(inbox): recover orphaned guidance ownership

Problem:
Durable in-flight guidance could outlive Controller ownership and become an immutable zombie item while the UI kept offering invalid operations.

Root cause:
Same-process snapshots had no ownership reconciliation, and durable claim/active/ack transitions were not serialized against recovery. The guidance shelf also treated every persisted state like a queued item.

Fix:
Recover unowned in-flight records to paused, reviewable uncertain state; serialize the full ownership lifecycle; and expose state-aware disabled or retry actions with localized labels.

Verification:
- go test ./...
- go test -race ./internal/control ./internal/sessioninbox
- cd desktop && go test . -run Inbox\|Steer
- cd desktop/frontend && pnpm test:typecheck && pnpm test && pnpm build
- make lint
- real Chromium recovery/retry interaction in zh-CN

Fixes #8276

* fix(inbox): make retry admission idempotent

Problem:
A recovered item could start as a new turn on retry and then reject a stale frontend steer. Completion also held the inbox admission lock across transcript snapshot I/O.

Root cause:
The steer idempotency path omitted the running state, and completion removed active ownership before persistence, requiring broad serialization.

Fix:
Treat Controller-owned running items as already admitted. Keep active IDs published while snapshotting and hold admission exclusion only for durable acknowledgement and ownership removal.

Verification:
- go test ./...
- go test -race ./internal/control ./internal/sessioninbox
- cd desktop && go test ./...
- frontend typecheck, focused Inbox tests, and production build
- make lint

* fix(inbox): keep durable I/O outside admission lock

Problem:
Inbox snapshots and turn completion held admissionMu while waiting on session inbox disk transactions, allowing bounded file-lock waits and per-item acknowledgement loops to block admission.

Root cause:
Recovery consumed a stale active-ID snapshot, so a broad Controller lock bridged the durable claim-to-ownership and ownership-to-ack windows.

Fix:
Track admitting and active ownership in lock-free registries that Store recovery consults after refreshing its transaction. Keep ownership published through durable acknowledgement, and remove admission locking from snapshot and completion I/O.

Verification:
- go test -race -count=1 ./internal/control ./internal/sessioninbox
- go test ./...
- cd desktop && go test ./...
- go vet ./...
- make lint
- frontend typecheck, Inbox focused tests, and production build

* fix(inbox): keep durable FIFO dispatch live

Problem:
Queued guidance could remain idle after rotation, a rejected steer, a transient dispatch failure, or the previous durable turn completing. The desktop fallback also disabled the queued action while idle.

Root cause:
Inbox dispatch used a one-shot edge guard, so kicks that arrived during a scan or a closed admission gate could be lost. Several gate-release paths did not publish a new kick, and the frontend exposed no FIFO-safe recovery action.

Fix:
Use a level-triggered drain with bounded retry, re-kick after rotation and rejected steering, and expose an idle head-item Send action that reuses the idempotent resume/drain path. Keep later items disabled to preserve FIFO ordering and localize the new states.

Verification:
- go test -race ./...
- go test -race ./internal/control -count=1
- focused inbox race tests with -count=10
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
- composer-inbox-recovery.test.tsx

* fix(ci): budget inbox recovery controls

Problem:
The Linux and Windows desktop jobs failed the raw initial asset budget after the inbox recovery UI landed, even though every compressed startup and chunk budget remained green.

Root cause:
The new localized recovery controls add 2.7 KiB to the current main-v2 raw initial bundle beyond its 2246 KiB allowance.

Fix:
Raise only the raw aggregate allowance to 2249 KiB and document the durable inbox recovery contract. This leaves roughly 0.3 KiB of headroom while preserving all gzip, CSS, locale, and largest-chunk limits.

Verification:
- wails generate module
- pnpm --dir frontend build
- pnpm --dir frontend check:bundle
- pnpm --dir frontend test
- go test -race ./... (root and desktop modules)

* refactor(inbox): keep recovery paths within lint budgets

Problem:
Repository lint rejected the queue liveness patch because it expanded two baselined legacy files and pushed TrySteerInboxItem five lines over the function limit.

Root cause:
The drain kick and idle recovery details lived inline in already oversized controller and Composer functions.

Fix:
Move the idle UI kick into composerGuidance, colocate rotation release with inbox dispatch, and extract steer-admission unlock handling. This restores every existing file/function budget without adding lint baselines.

Verification:
- go run ./tools/repolint
- focused inbox race tests with -count=10
- pnpm lint:hooks
- pnpm test:typecheck
- composer-inbox-recovery.test.tsx
2026-08-11 14:49:45 +08:00
SivanCola d285249589 test(desktop): cover live context through history slices
Problem:
- The live-context controller test failed before initial context and balance data loaded.

Root cause:
- Its fixture mocked only the legacy HistoryPageForTab method while startup hydration now requires HistorySliceForTab.

Fix:
- Reuse the shared history-slice fixture for an empty startup transcript.

Verification:
- pnpm exec tsx src/__tests__/use-controller-live-context.test.tsx
- pnpm test:all
- pnpm build
- go run ./tools/repolint
2026-08-11 14:44:48 +08:00
SivanCola 6b26552168 merge(main-v2): integrate completion receipt fix
Problem:
- main-v2 advanced while #8257 was under final review, adding trusted completion-contract input handling.

Root cause:
- The catalog integration branch needed the latest protected runtime behavior before final validation and publication.

Fix:
- Merge the latest main-v2 commit without conflicts, preserving both the catalog integration and trusted completion receipt routing.

Verification:
- Full root, Desktop, race, frontend, cross-platform, and repository gates will run on this exact merge head before push.
2026-08-11 14:16:52 +08:00
SivanCola ec2da98bda Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-role-settings
# Conflicts:
#	internal/agent/run_loop.go
2026-08-11 14:06:35 +08:00
SivanCola 40fdb8dfeb merge(main-v2): integrate task spend guard
Problem:
- main-v2 advanced while #8257 was under review, adding task spend guards across agent, boot, config, and control.
- The repolint baseline conflicted with the catalog integration, and the new real-Build budget test used a machine-speed-dependent round threshold.

Root cause:
- Both branches changed boot assembly and repository budget snapshots from the same earlier base.
- Counting rounds does not prove a wall-clock budget boundary under different runner loads.

Fix:
- Merge main-v2 semantically while preserving the catalog TaskStore and observed-session wiring.
- Recompute the exact combined repolint baseline without increasing the finding count.
- Assert the typed task_budget/time pause with a bounded context instead of a round-count heuristic.

Verification:
- go test ./internal/boot passed in three independent processes
- go test -race -count=1 ./internal/boot
- focused task-budget and history real-Build tests
- go run ./tools/repolint
2026-08-11 14:05:14 +08:00
SivanCola 175455df43 Keep transient routing out of completion receipts / 避免内部能力路由污染完成回执 (#8317)
* fix(agent): keep transient routing out of completion receipts

Problem:
Ordinary turns could surface an unverified completion receipt whose acceptance criterion exposed the internal capability-route block and invented a required-change check.

Root cause:
The agent kept the provider-composed turn as the completion contract input even though the controller already supplied authenticated raw user text separately.

Fix:
Use the raw user input for live and final completion contracts while preserving the composed provider prompt. Add regression coverage for mutation receipts and advisory turns containing capability routing with delivery-like wording.

Verification:
- go test ./internal/agent ./internal/completion ./internal/taskcontract
- go test ./...
- cd desktop && go test ./...
- git diff --check

* fix(agent): preserve trusted task text in completion contracts

Problem:
The first completion-receipt fix used raw user input unconditionally, which did not preserve Goal scope task text or pristine sub-agent classifier text. Its explanatory comment also exceeded repository lint budgets.

Root cause:
Completion contracts and delivery classification selected their task source independently even though both require the same host-trusted text.

Fix:
Store the already-selected trusted classification input as the per-turn contract input. Add regressions for Goal continuations and host-wrapped sub-agents, and remove the lint-expanding comment.

Verification:
- go run ./tools/repolint
- go test -race ./internal/agent -run TestCompletionContract -count=1
- go test ./internal/agent ./internal/completion ./internal/taskcontract
- go vet ./...
- go test ./...
- cd desktop && go test ./...
- git diff --check

* chore(ci): refresh PR metadata checks

Problem:
The documentation-impact field was added after the pull_request_target event was created, and rerunning that workflow reused the old event payload.

Root cause:
GitHub Actions reruns preserve the original pull request metadata snapshot.

Fix:
Create a new synchronize event so metadata guards evaluate the corrected PR body without rewriting published history.

Verification:
- PR body contains Documentation-impact
- local code and test gates passed at b0fb0fbcd
2026-08-11 14:03:53 +08:00
SivanCola 7be950ff15 merge(main-v2): integrate catalog and runtime updates
Problem:
- The integration branch lagged main-v2 and conflicted in Desktop startup, boot wiring, and frontend guardrails.
- In-memory shared-cache projections could return SQLITE_LOCKED during concurrent reconcile/write operations.
- A timed-out session catalog close could leave its database open, while macOS updater tests shared raw descriptor ownership with production-style helpers.

Root cause:
- The branch and main evolved the same runtime owners independently.
- SQLite shared-cache table locks do not honor busy_timeout across pooled in-memory connections.
- Session catalog close completion and test file-descriptor ownership lacked explicit lifetime fences.

Fix:
- Resolve the merge semantically, preserving catalog task-store and registration-scope behavior alongside upstream config warnings and agent runtime updates.
- Use one database/sql connection for memory projections, make session catalog close eventually complete exactly once, and duplicate updater handoff descriptors for exclusive ownership.

Verification:
- go test and go test -race ./internal/projectiondb ./internal/sessioncatalog
- Desktop session catalog regression: 100 ordinary runs and 30 race runs
- macOS updater handoff/TempDir regression: 100 ordinary runs and 20 race runs
2026-08-11 13:56:01 +08:00
SivanCola f7d883533a test(boot): assert spend gate by pause class 2026-08-11 13:43:46 +08:00
SivanCola 97738e0a9b Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-role-settings
# Conflicts:
#	internal/boot/effect_test.go
#	tools/repolint/baseline.json
2026-08-11 13:35:05 +08:00
SivanCola 39b9aa50f8 Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-role-settings
# Conflicts:
#	desktop/app.go
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-11 13:33:23 +08:00
YHH f1b5997b15 feat(agent): gate a task on spend; drop the chat round ceiling (#8320)
* feat(agent): gate a task on spend; drop the chat round ceiling

The gate moves to the axes a runaway is actually reported in. A task that
reaches its wall-clock or cost budget gets one tool-free summary and a
resumable pause — the same landing the round ceiling produced, now triggered
by what the work costs instead of by a count.

Ordinary chat loses its round ceiling entirely rather than getting a bigger
one. Rounds carry no information the spend axes lack: a turn that reaches a
high count without crossing either is one whose rounds are individually
cheap and fast, which is the case least worth interrupting. The only failure
rounds could catch alone is a loop that is both free and instant, and wall
clock catches that one too, in the right unit. Goal keeps its 16-round
boundary — an FSM yield point, not a runaway guard — and an explicit
--max-steps still owns either turn.

Wall clock is the only axis with a default, deliberately: half an hour means
the same thing on every model, an amount of money does not. A cost default
loose enough for DeepSeek would land a frontier model within a couple of
answers, so cost stays off until someone who knows what they are paying sets
[agent].task_cost_budget. Cost also only judges what it can price — an
unpriced model never crosses that axis rather than reading as free — and
wall clock still bounds a free or local model's loop.

Landing is now one path. armFinalizationRound owns arming the grace round,
injecting the finalization request, and emitting the notice; the spend gate
and an explicit max_steps both go through it, and gracePause names which one
stopped the Run. The grace round, not the wording, is what refuses tools.

toolOutcome moved to execute_batch.go, where it is used, to keep agent.go
inside its budget. The remaining baseline diff is 5 lines in config.go for
the two keys and 1 in boot.go for passing them; both files are far past the
800-line ceiling already and the added code is at its minimum shape.

* docs(guide): document the task spend budget

Both guides get the two keys, why only wall clock has a default (money is
not portable across models), the unpriced-model rule, and the fact that
ordinary chat no longer has a round ceiling at all.
2026-08-11 13:32:28 +08:00
SivanCola f17fceb033 fix(catalogs): close projection race and migration gaps
Problem:
Stale controller builds could roll back shared MCP instances owned by a newer build. Session tombstones could underfill paginated results and their same-revision refresh event could be ignored. Legacy topic migration markers could also miss same-size or event-log changes.

Root cause:
Registration scopes tracked creation without transferable instance claims, catalog pagination applied tombstones after SQL limits, and migration completion used only transcript names and sizes while explicit reconciliation still trusted that marker.

Fix:
Track exact MCP instance claims through scope commit or abort, scan catalog keyset pages until visible limits are satisfied, publish overlay refreshes that accept equal revisions, fingerprint bounded session artifacts and authoritative event logs, and add a marker-bypassing explicit migration path.

Verification:
go test ./internal/plugin ./internal/sessioncatalog
go test -race ./internal/plugin ./internal/sessioncatalog
go test ./desktop focused migration, tombstone, restore, clear-session, and registration cases
pnpm typecheck
npx tsx src/__tests__/project-tree-shell-race.test.ts
2026-08-11 13:13:55 +08:00
SivanCola fb965ff5a7 fix(config): avoid repeated DeepSeek migration warnings / 避免重复弹出 DeepSeek 迁移警告 (#8269)
* fix(config): avoid repeated DeepSeek migration warnings

Problem:
Switching or resuming sessions rebuilds the desktop runtime and repeatedly shows a DeepSeek migration warning when an unrelated TOML value is malformed.

Root cause:
The automatic DeepSeek protocol migration decodes the entire user config before the resilient loader runs, so any parse error is mislabeled as a migration failure on every controller build.

Fix:
Let the resilient config loader own malformed-file reporting for automatic migration, preserve the invalid file byte-for-byte, and keep explicit Settings upgrades strict.

Verification:
- go test ./internal/config -count=1
- go test ./internal/boot -run 'TestBuild(MigratesLegacyDeepSeekProtocolWithOneNotice|DoesNotMislabelMalformedConfigAsDeepSeekMigrationFailure)' -count=1
- go test ./... -p 4

* fix(boot): scope malformed config warnings by frontend

Problem: automatic DeepSeek migration parse failures were either shown as duplicate desktop session notices or hidden from headless frontends.

Root cause: malformed TOML recovery belongs to the resilient loader on desktop, but the migration layer is shared by desktop, CLI, serve, and bot builds.

Fix: keep migration parse errors intact, suppress only the duplicate desktop notice when a loader warning exists, preserve headless diagnostics, and move focused regression coverage out of the oversized boot test file.

Verification: go run ./tools/repolint; go vet ./internal/config ./internal/boot; focused race tests for config and boot; go test ./... -p 4.

* fix(desktop): surface runtime config load warnings

Problem:
Malformed config introduced after startup could be silently ignored during desktop session rebuilds.

Root cause:
Boot inferred warning presentation from the usage StatsSource label without transferring resilient-loader warnings to the frontend.

Fix:
Add an explicit warning acceptance callback, publish warnings through the desktop event bridge, deduplicate repeated fingerprints, and fence stale startup responses.

Verification:
- go test -race ./internal/boot
- go test -race . in desktop
- go test ./... -p 4
- go test ./... in desktop
- pnpm build
- go run ./tools/repolint

* fix(desktop): order config warning reloads

Problem: Runtime config warnings could miss project-only damage, and an event queued before a successful reload could reopen a stale banner.

Root cause: Boot delivered warnings only through the DeepSeek migration error path, while desktop warning events and reload snapshots had no shared source ordering.

Fix: Deliver every resilient-loader warning through the explicit frontend callback and assign monotonic build/snapshot revisions so reloads reject older queued events while newer recurrences remain visible.

Verification: go test ./... -p 4; cd desktop && go test ./...; go vet ./... in both modules; pnpm build; race-focused config warning tests; go run ./tools/repolint.
2026-08-11 13:11:23 +08:00
SivanCola 9a17c257f5 Merge remote-tracking branch 'origin/main-v2' into fix/pr-8275-agent-role-settings
# Conflicts:
#	tools/repolint/baseline.json
2026-08-11 12:57:33 +08:00
ttmouseandSivanCola 61776a98cb feat(desktop): 文件右键菜单支持用默认应用打开并分组 (#8220)
* feat(desktop): 文件右键菜单支持用默认应用打开并分组

- 新增「用默认应用打开」菜单项(仅文件显示),复用 OpenWorkspacePathForTab
- FloatingMenuItem 支持 separator 分隔线,右键菜单按「文件操作/与聊天交互」两组分组
- 菜单项按使用频率重排(打开→显示→终端→复制路径→聊天交互)
- 菜单高度估算随项数上调(FILE 280 / REF 236)
- 新增 workspace.openWithDefaultApp 三语言键

* refactor(desktop): extract workspace tree menu

Problem: The workspace context-menu changes pushed WorkspacePanel past its repository file-size budget.

Root cause: Menu assembly and native action routing remained inline in the already baselined panel component.

Fix: Move the behavior-preserving tree context menu into a focused component while keeping tab and scope routing explicit.

Verification: pnpm test:workspace; pnpm test:typecheck; pnpm build; go run ./tools/repolint; git diff --check

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

* test(desktop): cover workspace default-open menu

Problem: The new workspace default-open action had no focused regression coverage.

Root cause: Existing workspace tests covered blank-tree menus and scope cleanup, not row context-menu assembly or native bridge routing.

Fix: Assert file and folder visibility, command order, separator grouping, menu close behavior, and tab-scoped native-open arguments.

Verification: pnpm test:workspace; pnpm test:typecheck; pnpm build; go run ./tools/repolint; git diff --check

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

---------

Co-authored-by: SivanCola <[email protected]>
2026-08-11 12:57:21 +08:00
YHH 0e95a4c01b feat(agent): the spend axis spans a task, not one Run (#8318)
A per-Run total cannot see the failure this axis exists for. runLoopState is
rebuilt every Run, so every "continue" restarts the count from zero — and
the reported runaway was four hours, which is many Runs. A ceiling or a
budget scoped to one Run offers exactly no protection against it.

The task scope resets with the evidence ledger. That boundary already means
"this is new work": a continuation preserves the ledger, an unrelated turn
resets it, and the delivery scope decides which. Reusing it keeps one answer
to "is this the same task" instead of inventing a second one that can
disagree with the first.

Samples now carry both scopes. Turn is what this Run spent, Task is what the
work has cost since it started; a reader that wants either has it without
differencing.

Still shadow — no threshold reads it.

agent.go was 4 lines over its file-size budget with the new field, so the
storm breaker moved to storm_breaker.go: threshold, block message, signature,
and the guard itself were already one concern sitting in the middle of
another file. agent.go drops 151 lines and 13 essay findings, which the
baseline diff moves rather than forgives.
2026-08-11 12:50:20 +08:00
SivanCola 0edff6d99a Merge latest main-v2 into agent role settings
Problem:
main-v2 advanced again while the final lint cleanup was being pushed, leaving the pull request without a mergeable synthetic ref.

Resolution:
Merge the latest target tip and validate the combined role-policy and turn-spend changes before rerunning CI.

Verification:
focused Go tests, golangci-lint, repolint, and frontend bundle checks rerun after conflict resolution

# Conflicts:
#	tools/repolint/baseline.json
2026-08-11 12:40:59 +08:00
SivanCola e5306e86a6 fix(ci): clear role-setting lint debt
Problem:
The full CI lint job reported 19 issues that were not covered by repolint or go vet, including stale compatibility helpers, inefficient test string assembly, and staticcheck simplifications.

Root cause:
The role-setting refactor retired several helper call sites without removing their declarations, and its new policy/test code had not been run through the repository-pinned golangci-lint suite.

Fix:
Remove unused compatibility and boot helpers, simplify role-policy branches, use builders in tool-output tests, and ratchet the repolint baseline down after the cleanup.

Verification:
golangci-lint v2.12.2 run --timeout=5m
go test ./internal/acp ./internal/agent ./internal/taskpolicy ./internal/cli ./internal/boot
go run ./tools/repolint
2026-08-11 12:36:47 +08:00
YHH dbf0dd873a feat(agent): observe what a turn actually spends (#8309)
Rounds are the axis we bound and the one nobody reports a failure in. The
turn behind #8228 was four hours, 524 requests and 194M tokens; not one of
those numbers is a round count, and the same hundred rounds cost minutes or
hours depending on what each one read and how long the model thought.

This adds the axis that is missing: tokens, requests, wall clock and money,
accumulated per turn and reported after every round to sinks that opt into
event.RunBudgetSink. Cost goes through provider.Pricing, so a cache hit
prices at a fiftieth of a miss instead of reading as a full-price prompt —
on a cache-first agent, anything else would misreport the bill by an order
of magnitude.

Shadow only. Nothing reads a verdict from it and no threshold exists yet:
the distribution these samples record is what a threshold should be chosen
from, and picking one first would repeat how 100 was picked.

Rounds still count when a round's usage never arrived, so the axis never
reads cheaper than the turn was.

run_loop.go was 3 lines over the file-size ceiling with this, so the
sampling-usage accounting it already carried — estimate, merge, finalize,
emit — moved to run_usage.go as its own concern. That drops run_loop.go
under 800 lines and retires its file-size debt; the baseline diff carries
the two findings that moved with the extraction and records one fewer
finding overall.
2026-08-11 12:31:25 +08:00
SivanCola 49f6fb7ac6 chore(ci): reconcile budgets with latest main
Problem:
After the PR fix was pushed, main-v2 added an arbiter and transcript scrolling contracts. GitHub tests the synthetic merge, where those additions exceeded the PR's repolint and raw frontend budgets even though each branch passed independently.

Root cause:
The checked-in ratchets were generated against the previous main-v2 tip and had no room for the newly merged always-available contracts.

Fix:
Regenerate the combined-tree repolint baseline and extend the initial raw asset allowance by 3 KiB while preserving the existing gzip ceilings.

Verification:
go run ./tools/repolint
pnpm check:bundle
go test ./internal/agent ./internal/control ./internal/acp ./internal/boot
2026-08-11 12:25:15 +08:00
SivanCola b81ca7cffc Merge latest main-v2 into agent role settings
Problem:
The target branch advanced after the role-policy fix was pushed, so the CI merge result inherited new code outside this PR and exceeded the freshly generated repolint baseline.

Resolution:
Merge the latest main-v2 before regenerating and validating the combined-tree lint baseline.

Verification:
go run ./tools/repolint
focused and full suites rerun after the merge
2026-08-11 12:22:56 +08:00
SivanCola daec1e0c03 fix(agent): enforce role policy end to end
Problem:
Role settings derived policy fields that were not consistently shared by planner, capability routing, execution, and final readiness. ACP prompt admission could race an in-place role switch, and desktop branch metadata could restore a stale canonical preset. CI also exposed stale role-copy assertions, locale bundle overages, and an outdated repolint baseline.

Root cause:
Each layer independently inferred role behavior, prompt admission did not participate in the state-change lock, and session persistence updated only the legacy tokenMode field.

Fix:
Freeze one TaskPolicy per turn and enforce its route, capability, mutation, external-action, verification allowlist, explore-subagent, and post-write verification constraints. Serialize ACP admission with role changes, dual-write desktop role metadata, keep planner/router runtimes role-neutral for in-place switching, and repair lint/bundle/test contracts.

Verification:
go test ./...
cd desktop && go test ./...
go test -race ./internal/acp ./internal/control ./internal/agent
go vet ./...
go run ./tools/repolint
cd desktop/frontend && pnpm build && pnpm test:all
2026-08-11 12:20:08 +08:00
YHH 38b2acf094 feat(agent): one arbiter delivers a round's guard signals (#8296)
Three signals write the round tail — storm breaker, progress guard, evidence
nudge — and all three built it the same way, from outcomes[0].output. They
run in sequence, so a round that fired two delivered only the last one's
guidance and silently dropped the earlier one. A batch that trips the loop
guard and the evidence nudge together is exactly the batch that needs both.

Each signal now returns an intervention (verdict, guidance, notice, stuck
reason) instead of writing the slot, and applyInterventions folds a round's
signals into one delivery: strongest verdict, every notice, all guidance
concatenated in signal order.

The verdict ladder — advise, redirect, land — is the vocabulary the rest of
this work needs. Nothing reads it yet beyond the strongest-wins fold; it is
introduced here so the signals already speak it when the remaining
interventions move onto this channel and when a resource axis joins them.

Behaviour is unchanged for any round that fires one signal, which is every
round the existing suites cover; the two-signal round gains the guidance it
was dropping.
2026-08-11 12:14:07 +08:00
SivanCola 01bc087862 fix(desktop): registration scopes, catalog tombstones, migration signatures
Problem:
Three P1 merge blockers remained on the catalogs integration head:
1) Host registration journal used a Host-global regJournalActive flag, so
   sibling hot-adds during a stale build were attributed and rolled back.
2) RemoveSession stored tombstones only after mutex waits; a 150ms desktop
   context could expire mid-lock, leave SQL undeleted, and ListTopics still
   returned archived topics (race-mode restore test).
3) Topic migration markers used strict mtime ordering, which failed on
   Windows coarse timestamps for new CLI sessions after the marker.

Root cause:
1) Ownership was time-window based rather than token-based.
2) Query paths trusted SQLite without an immediate removal overlay.
3) Marker validity compared file mtimes instead of directory content.

Fix:
- Replace the global journal with per-build RegistrationScope tokens
  propagated via context through EnsureConnected and LazyToolset; abort
  rejects late registrations; sibling writes without the token are ignored.
- Do not hold a Host lock across boot.Build (scopes do not serialize builds).
- Record removedPaths before any mutex; filter ListTopics/ListSessions/
  GetSession through the tombstone; TryLock durable DELETE and retry async.
- Persist migration markers as name+size directory signatures; new sessions
  invalidate without mtime ordering.
- Extract registration scope, removal, migration marker, and legacy migration
  helpers to keep repolint budgets clean (baseline carry-forward only).

Verification:
- go test ./internal/plugin/ -run TestRegistration (-race)
- go test ./internal/sessioncatalog/ -run 'TestRemoveSession|TestListTopics' (-race)
- cd desktop && go test -run 'TestProjectTreeMigratesNewCLISessionAfterProjectDirMarker|TestRestoreGlobalTopicSessionReindexesProjectTree|TestClearSessionForTab'
- cd desktop && go test -race -count=3 -run TestRestoreGlobalTopicSessionReindexesProjectTree
- go run ./tools/repolint
2026-08-11 12:11:13 +08:00
jackie_cqzandSivanCola a5dce99142 fix(desktop): hide base providers from plugin model group / 隐藏插件模型分组中的普通供应商 (#8097)
* fix(desktop): hide base providers from plugin models

* fix(cli): filter base providers from plugin models

Problem:
The Desktop fix added an over-budget comment, while the CLI model picker still accepted every descriptor from the merged provider catalog.

Root cause:
The CLI merge path did not enforce the plugin namespace boundary, and the expanded Desktop explanation crossed the repository comment budget.

Fix:
Require a canonical plugin owner in the CLI merge, cover an unnamespaced base descriptor, and shorten the Desktop contract comment.

Verification:
- go test ./internal/cli -run TestMergeExtensionModelRefs -count=1
- cd desktop && go test . -run TestModelsForTab -count=1
- cd desktop && go vet .
- go run ./tools/repolint
- git diff --check

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

---------

Co-authored-by: SivanCola <[email protected]>
2026-08-11 12:06:35 +08:00
SivanColaandLI Xinyu 2760c09466 fix(desktop): stabilize native transcript scrolling / 修复聊天区原生滚动跳页与卡顿 (#8264)
* fix(desktop): stop trackpad scroll stalls in the chat transcript

Problem: On macOS, two-finger scrolling the chat content area felt sticky
and jumpy—especially over markdown tables—because nested overflow
containers latched the trackpad and virtualizer/stream rewrites fought
native inertia mid-gesture.

Root cause: CSS overflow-x:auto promotes overflow-y to auto, so ordinary
tables/code blocks became vertical scroll ports; virtualizer height
compensation and stream tail-follow could still write scrollTop while the
user was gesturing.

Fix: Add a short user-gesture lock that blocks compensating scroll writes,
promote edge/non-scrollable nested wheels to the transcript with latching
handoff, keep small tables and code blocks out of nested vertical scroll,
and mark large virtual tables for handoff.

Verification: npx tsx scroll-manager, transcript-scroll-session,
nested-scroll-handoff, markdown-table-virtual, typography-overflow-contract,
and transcript-virtualization tests.

* fix(desktop): fold large tables and idle-remeasure after scroll

Problem: Large markdown tables still introduced nested vertical scroll
ports by default, and virtualizer remeasure could jump after a trackpad
gesture settled.

Root cause: Oversized tables mounted a max-height virtual scroller
immediately, and height compensation was deferred only mid-gesture, not
batched once after the hold window.

Fix: Collapse tables above the virtual threshold to a 12-row document-flow
preview with Expand all / Collapse; expanded tables keep the nested
virtual scroller with handoff. After gesture idle, measure once and
reconcile the reading anchor.

Verification: npx tsx markdown-table-virtual, scroll-manager,
transcript-scroll-session, and nested-scroll-handoff tests.

* fix(desktop): stabilize native transcript scrolling

Problem:
Long variable-height transcripts could stall or jump while trackpad inertia, native scrollbar input, or Windows middle-button auto-scroll overlapped virtualizer and resize compensation. Creation scrollbar sensitivity could also change while rows mounted.

Root cause:
Scroll ownership only covered wheel, touch, and keyboard intent; treated unowned native scroll events ambiguously; remeasured the entire virtualizer after every idle gesture; and recomputed custom-scrollbar geometry during a drag. A stale scrollend could also settle a newer gesture.

Fix:
Model native input sessions explicitly, arbitrate controller-owned scroll events, normalize wheel delta modes, guard stale scrollend, freeze custom-scrollbar drag geometry, and measure only real layout invalidations with anchor reconciliation. Split interaction and scrollbar concerns out of Transcript and add real-browser stability coverage.

Verification:
- pnpm --dir desktop/frontend test:transcript
- pnpm --dir desktop/frontend test:transcript-browser
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend build
- go run ./tools/repolint

* fix(desktop): latch manual transcript scroll intent

Problem:
A short upward wheel, trackpad, native scrollbar, or Creation scrollbar movement could remain inside the 80px near-bottom band. After the gesture quiet window, streaming output could therefore re-enable tail-follow and pull the viewport back down.

Root cause:
The scroll manager treated geometric proximity as user intent and recomputed stick mode from the near-bottom threshold after every native scroll event.

Fix:
Latch explicit user detachment across gesture settlement, keep resize and stream repins disabled while detached, and restore tail-follow only at the physical bottom or through an explicit bottom/reset action. Apply the same contract to the Creation scrollbar.

Verification:
- pnpm --dir desktop/frontend test:transcript
- pnpm --dir desktop/frontend test:transcript-browser
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend build
- go run ./tools/repolint

* fix(desktop): preserve tail follow across scroll ownership

Problem:
- Nested downward handoff at the physical bottom detached streaming tail-follow.
- Passive tail writes rejected during an active gesture were dropped, so final token and row-size growth could leave the viewport behind.

Root cause:
- Nested handoff discarded wheel direction and always released auto-scroll.
- Gesture arbitration had no generation-aware replay for blocked passive owners.

Fix:
- Propagate normalized nested deltas and release only for upward or detached reading intent.
- Replay blocked stream, resize, footer, and row-size repins once after gesture idle using the current scrollHeight.
- Cancel deferred work on manual intent, selection, explicit navigation, and tab generation changes.
- Adapt #8291's row-size ownership, pinned measurement rule, and stream-end fallback into the unified gesture controller.
- Add deterministic and real Chromium regression coverage.

Verification:
- pnpm test:transcript
- pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm test:motion
- pnpm build
- go run ./tools/repolint

Co-authored-by: LI Xinyu <[email protected]>

---------

Co-authored-by: LI Xinyu <[email protected]>
2026-08-11 11:59:43 +08:00
SivanCola 7d57a41f42 Merge main-v2 into agent role settings
Problem:
The feature branch was behind the current integration base.

Resolution:
Merge the latest main-v2 changes before applying review fixes so validation covers the current agent runtime.

Verification:
Focused and full validation will run after the review fixes are applied.
2026-08-11 11:43:07 +08:00
SivanCola 8cff50d08b fix(desktop): instance-scoped Host rollback and hydrate generics
Problem:
Desktop CI failed with TS2305 because hydrateErrorState imported a non-existent
Item type from ./types. Host generation-loss rollback still used name-diff
snapshots, so a deterministic interleaving where sibling/new-generation resources
appear after the snapshot incorrectly deleted those live clients.

Root cause:
1. The helper was typed against a concrete Item export that does not exist in
   types (and created a fragile circular import surface).
2. Name-set diff has no ownership: any server name created after the snapshot
   was attributed to the lost build, including post-snapshot sibling work.

Fix:
- Make applyHydrateErrorState / hydratePlaceholderItems generic over TItem so
  the helper needs no types import.
- Assign Host-local Client instance IDs, journal registrations during
  RunWithRegistrationJournal, and roll back only with RemoveIfInstance.
- Controller builds wrap boot.Build in the journal; supersede/generation-loss
  paths call RollbackRegistration on journaled refs only.
- Add deterministic instance-scoped regression tests (including post-journal
  sibling preservation and same-name newer-generation survival).
- Make Client.close nil-safe for transport-less test clients.

Verification:
- go test ./internal/plugin/ -run 'TestRegistrationJournal|TestRunWithRegistration' -count=1
- go test -race ./internal/plugin/ -run 'TestRegistrationJournal|TestRunWithRegistration' -count=1
- cd desktop && go test -c . && go test -run TestClearSessionForTab -count=1
- cd desktop/frontend && pnpm typecheck
2026-08-11 11:32:28 +08:00
YHH 85c7c0e80d docs(control): the chat Run ceiling is a backstop, not the judgement (#8286)
bindTurnScope claimed the adaptive guards "stay silent while a loop keeps
finding something new". That was true when #8228 landed and false an hour
later: #8233 added evidence.explorationRunLimit, so a look-only run stops
scoring past six rounds and the no-progress ladder reaches its first rung
on exactly the shape the ceiling was added for.

Leaving the claim in place misreads the ceiling as the primary defence. It
is the backstop for a shape the ladder has not learned yet, and it counts
rounds per Run — a "continue" starts them over, so it bounds one turn and
never a whole task. Both facts now sit next to the constant.

Agent.Run's doc carried the same gap from the other side, naming
cancellation and compaction as the real bounds and omitting the ladder. It
also ran two overlapping paragraphs; this collapses them into one.

No behaviour change.
2026-08-11 11:04:01 +08:00
YHH 24f6cad07b fix(agent): a failed summary must not strand the turn (#8119)
At physical overflow, and at pressure above the hard input ceiling, the
summarizer is the only way out of context pressure. Its failure surfaces
as ErrCompactionRequired and blocks every further message: "context
exceeds provider limit and compaction failed / context deadline
exceeded".

#4138 made the 90s summary bound safe by pairing it with a deterministic
mechanical fold: the summarizer may time out, a timeout is deliberately
not retried, but the fold still frees context. #7839 removed
mechanicalFoldDigest and added CompactionModeDegraded without ever
wiring it, so the no-retry policy stayed while the fallback did not.

Restore the fold, and only where the caller would otherwise answer
ErrCompactionRequired. Below that ceiling nothing changes: the turn
still goes out, and the blocked fingerprint keeps a second summary from
being paid for, so a recoverable view is never folded without a summary.
Manual compaction keeps reporting the error to a caller who is present
and can retry, and cancellation never degrades. The canonical transcript
stays untouched on every path; only the projection changes.

A degraded fold freed the context, so its telemetry is a notice carrying
the summarizer's error rather than a "compaction failed" warning.
2026-08-11 09:46:09 +08:00
SivanCola 8a131a866f fix(desktop): fix CI parse/lint and scoped Host rollback
Problem
af6b1cbe0 failed Desktop CI: App.tsx JSX was invalid (two roots without a
Fragment), unused purgeUnwantedSharedHostServers and a classic for-range
failed macOS lint, and generation-mismatch cleanup wiped the entire shared
MCP Host including servers owned by other tabs or a newer rebuild.

Root cause
Hydrate-error UI was sibling-adjacent to Transcript in a ternary branch.
Host rollback used a full reset instead of build-scoped cleanup.

Fix
- Wrap Transcript + hydrate error in a Fragment.
- Remove unused purge helper; use for-range in cold history test.
- Snapshot Host servers before boot.Build; on generation loss or build
  failure roll back only names introduced by that build.
- sameMeta compares sessionGeneration; hydrate uses shared
  hydrateIdentityCurrent fence.
- Add deferred-barrier clear→late-A race test and keep source contracts.

Verification
- gofmt -l desktop
- go run ./tools/repolint
- go test -run 'TestClearSessionForTab|TestRollbackSharedHost|TestClearSession|TestTabScoped' ./desktop
- npx tsx desktop/frontend/src/__tests__/clear-session-identity-race.test.ts
- npx tsx desktop/frontend/src/__tests__/clear-session-identity-contract.test.ts
2026-08-11 09:42:10 +08:00
SivanCola af6b1cbe0b fix(desktop): fence clear-session identity against mode-switch races
Problem
After Clear Context, immediately switching Plan/layout/token mode could
briefly re-show the destroyed transcript. ClearSession rotated the backend
session path but the frontend only wiped the view; meta, TranscriptStore
resident cache, and hydrate identity still pointed at the old session.

Root cause
ClearSessionForTab returned no replacement identity. clearSession did not
evict TranscriptStore or apply a new sessionPath/generation. Hydrate only
checked request sequence numbers, so a post-clear load started with stale
meta.sessionPath could still preferResident-serve session A.

Fix
- ClearSession/ClearSessionForTab return SessionClearResult with path,
  revision, digest, and a bumped tab SessionGeneration.
- clearSession applies that identity atomically, resets transcript state,
  and evicts the tab from TranscriptStore.
- loadSessionDataForTab rejects results when meta sessionPath or
  sessionGeneration no longer match the load's identity.
- On MCP extension generation race, resetSharedHostMCP drops all shared-host
  clients (including same-name still-enabled servers) before rebuild.
- gofmt app.go; keep repolint clean.

Verification
- gofmt -l . (clean)
- go run ./tools/repolint
- go test -run 'TestClearSessionForTabReturnsReplacementIdentity|TestResetSharedHostMCPClearsStillEnabledFailure|TestClearSession|TestTabScoped' ./desktop
- npx tsx desktop/frontend/src/__tests__/clear-session-identity-contract.test.ts
2026-08-11 09:28:15 +08:00
SivanCola 33b9fc270c merge: refresh feature/agent-role-settings from origin/main-v2
Bring in recent main-v2 fixes before publishing the role-settings PR.
2026-08-11 09:25:17 +08:00
SivanCola 5e03e03eaf feat(agent): light/balanced/delivery role settings with unified tools / 角色设定与统一工具面
Problem:
Work modes (economy/full/delivery) mixed token surface trimming, delivery
gates, and UI labels. Economy forced connect_tool_source schema growth;
switching modes rebuilt controllers and busted prompt-cache prefixes.
Skill profiles could hide tools from the model surface.

Fix:
- Introduce AgentPreset (light|balanced|delivery) + TaskPolicy host floors
  for plan route, verification, and independent review.
- Share one provider-visible core surface across presets; optional tools
  dispatch only through use_capability (retire connect_tool_source boot).
- Switch role settings in place without controller rebuild (CLI /preset,
  desktop, ACP agent_preset; legacy work_mode/tokenMode dual-write one version).
- Emit turn_phase and completion_summary events; wire desktop/CLI UI.
- Keep skill profiles diagnostic-only; dual-write agent_preset on tabs/branch meta.
- Docs, i18n, golden surfaces, and regression matrix updated.

Verification:
- go test ./internal/boot ./internal/agent ./internal/cli ./internal/acp
  ./internal/control ./internal/skill ./internal/agentpreset ./internal/taskpolicy
- desktop role-setting smoke tests
2026-08-11 09:25:13 +08:00
SivanCola 3f1be32bc9 fix(desktop): close remaining catalog integration P1 gaps
Problem
Review of #8257 found residual blockers: body-only history hid the search
input, MCP ClearAuth skipped generation bumps and stale builds could
re-register deleted shared-host servers, task control could adopt SQLite
roots, open-status page fill disagreed with the frontend, and repolint
failed on size/complexity/trailing blanks.

Root cause
UI gated search on metadata session count; generation barriers were
incomplete for Host-owned MCP state; catalog rows overwrote allowlisted
project roots; SearchHistoryContent grew past complexity budget.

Fix
- Always show history search in non-trash mode, including body-only hits.
- Bump extensionGeneration on ClearMCPServerAuthentication; purge unwanted
  shared-host servers when a stale build loses the generation race.
- Keep allowlisted task project roots; catalog may only supply labels.
- Align backend open filter with open-but-not-current UI semantics.
- Split history search/index helpers to satisfy repolint; fix docs EOF.

Verification
- go run ./tools/repolint
- go test ./internal/historycatalog/ ./internal/usagecatalog/
- go test -run 'TestHistorySliceCold|TestHistorySliceReports|TestTaskAction|TestExtension|TestProjectTreeShell' ./desktop
- npx tsx desktop/frontend/src/__tests__/history-catalog-body-hits.test.ts
- npx tsx desktop/frontend/src/__tests__/history-load-failure-contract.test.ts
- git diff --check
2026-08-11 09:19:03 +08:00
SivanCola bc30df826c fix(desktop): fix project-tree race and cold history empty-session traps
Problem
#8257 could leave the project sidebar permanently empty when
project-tree:changed-v2 advanced revision before the initial shell
snapshot, and large-session hydration still waited on controller ready
while failed history loads looked like successful empty transcripts.

Root cause
Shell snapshots reused the disposable catalog revision watermark, and
HistorySlice/listSessions treated read failures as empty success without
preserving prior content or offering retry.

Fix
- Accept projects.json shells when the tree is empty even if catalog
  revision is already ahead; re-fetch the shell on empty-tree v2 events.
- Prefer cold HistorySliceForTab before controller ready; surface slice
  Error instead of silent empty pages.
- Defer transcript reset until history succeeds; keep placeholders/items
  on hydrate_error; expose retrySessionHistory + UI banner.
- Stop listSessions from swallowing catalog failures as [].

Verification
- go test -run 'TestHistorySliceColdPathBeforeControllerReady|TestHistorySliceReportsErrorInsteadOfEmptySuccess|TestProjectTreeShellSurvivesCatalogRevisionRace|TestHistorySlice|TestProjectTree|TestTopicArchive|TestArchive' ./desktop
- npx tsx desktop/frontend/src/__tests__/project-tree-shell-race.test.ts
- npx tsx desktop/frontend/src/__tests__/history-load-failure-contract.test.ts
- npx tsx desktop/frontend/src/__tests__/history-catalog-body-hits.test.ts
2026-08-11 09:07:44 +08:00
SivanCola e08111dfc8 fix(catalogs): unblock six merge-blocking projection regressions
Problem
Integration PR #8257 left six functional/concurrency gaps: history tool
tool_output was never indexed, History UI hid body-only hits, open/current
filters ran after pagination, MCP install/update/remove/reconnect did not
bump the controller extension generation, task control required a ready
SQLite catalog, and usage Ready ignored same-size rewrites.

Root cause
Disposable catalogs filtered or published after partial candidate windows,
and control-plane paths depended on projection readiness or incomplete
generation barriers.

Fix
- Index tool_output while keeping default search kinds excluding it.
- Fill ListHistorySessions/SearchHistoryContent pages after runtime filters,
  and show body hits with an independent body cursor in the History UI.
- Bump extensionGeneration on Install/Update/Remove/Reconnect MCP paths.
- Resolve task action projects from the allowlisted roots/FileStore path.
- Compare mtime_ns in usage Ready (and record mtime on live receipts).

Verification
- go test ./internal/historycatalog/ ./internal/usagecatalog/ ./internal/sessioncatalog/ ./internal/history/
- go test -race ./internal/historycatalog/ ./internal/usagecatalog/
- go test -run 'TestTaskActionProjectResolvesAllowlistWithoutCatalog|TestExtensionGenerationBumpsOnMCPMutationSites|TestTaskMonitor|TestStopTask|TestCloneDetached|TestApplyRuntime|TestCloseRemoved' ./desktop
- npx tsx desktop/frontend/src/__tests__/history-catalog-body-hits.test.ts
2026-08-11 08:49:42 +08:00
SivanCola 8a39ac1c4b fix(desktop): hide context checkpoint details (#8265)
Problem: the context panel again exposed internal full-history, model-visible, trigger, threshold, and checkpoint diagnostics after the content-driven maintenance rewrite.

Root cause: PR #8244 added a new context checkpoint block and reversed the source contract that had kept internal maintenance details out of the user-facing panel.

Fix: remove the checkpoint detail render path, presentation-only state, styles, and locale strings. Preserve the real compression-distance gauge, backend checkpoint behavior, and maintenance notices. Restore a source contract that prevents the block from returning.

Verification: focused context-maintenance contract test; ESLint; Wails bindings regeneration followed by pnpm build; browser DOM verification.
2026-08-11 08:37:14 +08:00
SivanCola f877a35ad0 fix(catalogs): stabilize activity times, reconcile drain, and shutdown close
Problem
CI still failed on lint (intrange), desktop race ordering for new CLI sessions
(LastActivityAt=0), Windows TempDir cleanup locking usage-catalog SQLite, and
migration-marker timing flakes. The branch was also behind main-v2.

Root cause
SyncMetadata inserted topics with last_activity_at=0/turns_state=valid before
sessions were indexed; reconcile requests could be dropped when the channel was
full; process-local usage/history catalogs kept SQLite handles open after tests;
marker assertions used a single Stat.

Fix
- Merge latest main-v2.
- Floor session activity on file mtime; SyncMetadata inherits live aggregates.
- Retain overflow reconcile requests in a dirty map and drain them.
- Close shared history/usage/task catalogs on desktop shutdown and test cleanup.
- Wait for migration markers; fix for-range lint.

Verification
- go test ./internal/sessioncatalog ./internal/stats ./internal/history ./internal/cli
- go test -race ./internal/sessioncatalog
- cd desktop && go test -race -run 'TestProjectTreeMigratesNewCLISession|TestCoveredRecoveryCopy|TestTopicMigrationMarker'
- go run ./tools/repolint
2026-08-11 02:44:16 +08:00
SivanCola ba90b8a2d0 Merge remote-tracking branch 'origin/main-v2' into feature/sqlite-catalogs-integration 2026-08-11 02:39:31 +08:00
github-actions[bot] 15239191e4 docs(release): prepare v1.23.0 notes (#8259)
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.

Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-11 02:35:11 +08:00
SivanCola c1a3af4c99 fix(ci): unblock Windows main-v2 full suite before v1.23.0 / 修复 Windows 全量 CI 以便发布 v1.23.0 (#8255)
* fix(ci): unblock Windows main-v2 full suite before v1.23.0

Problem
main-v2 push CI has a red Windows streak that blocks Stable tagging:
internal/agent, internal/boot, and internal/control hit the 3m package
timeout on loaded runners, sessioninbox asserted Unix 0700 modes on
Windows (reports 0777), and cold PowerShell hooks timed out at 60s.

Root cause
The PR smoke suite was already raised to 8m after measured 2.2x runner
variance, but the full push suite still used -timeout=3m with a 10m step
budget. Session-inbox permission checks did not gate on GOOS. Nested
PowerShell spawn shared the default 60s real-spawn budget under -p 4.

Fix
- Raise Windows full push suite to -timeout=8m and timeout-minutes: 20.
- Gate session-inbox directory mode assertion to non-Windows.
- Allow 2m for the nested PowerShell hook spawn under Windows load.

Verification
- go test ./internal/sessioninbox/ -count=1
- GOOS=windows go test -c ./internal/hook/

* fix(sessioninbox): keep store_test under repolint file-size ceiling

Problem
The Windows mode-gate edit pushed store_test.go to 801 lines and failed
repolint test-file-size (800-line ceiling).

Root cause
Extra comment lines around the GOOS permission assertion.

Fix
Collapse the comment to one line so the file stays at 800 lines.

Verification
go run ./tools/repolint

* fix(hook): clamp the Windows hook spawn timeout with max (modernize)

Problem
CI lint (golangci-lint v2.12.2, GOOS=windows leg) fails the Windows hook
test: the manual min clamp is flagged by the modernize linter
(minmax: use max).

Root cause
The 2m floor for the real-spawn budget was written as an if-statement
instead of the max builtin.

Fix
Replace the if/assign pair with max(realSpawnTimeout, 2*time.Minute).

Verification
- GOOS=windows golangci-lint run --timeout=5m ./internal/hook/...: 0 issues
- make lint (CI pin v2.12.2): 0 issues, repolint clean
- go test ./internal/hook/ -count=1: ok
2026-08-11 02:28:29 +08:00
SivanCola 2ee158c6e7 fix(catalogs): harden multi-domain SQLite lifecycle for CI and safety
Problem
PR #8257 failed Windows reindex, empty-cache path safety, history flush
semantics, locale gzip budget, and PR policy guards (docs/cache impact,
repolint baseline).

Root cause
SQLite file URIs were not Windows-safe, so disk open failed and fell back to
memory during rebuild. Sibling catalogs joined CacheDir without empty checks.
History Flush did not drain work; tool_output was always FTS-indexed; zh-TW
locale budget was tight to the byte.

Fix
- Cross-platform diskFileDSN and RequireDisk rebuild opens.
- Empty CacheDir => memory for history/usage/task catalogs.
- History Flush drains dirty roots and queue; signature-skip periodic rescan;
  stop indexing tool_output by default.
- Ratchet zh-TW/zh locale gzip budgets and refresh repolint baseline.

Verification
- go test ./internal/projectiondb ./internal/sessioncatalog ./internal/historycatalog ./internal/usagecatalog ./internal/taskcatalog ./internal/cli
- go test -race ./internal/projectiondb ./internal/sessioncatalog ./internal/historycatalog
- go run ./tools/repolint
2026-08-11 02:20:12 +08:00
SivanCola 0d41f2ca46 merge: adopt task catalog from #8219
Add the cross-project task catalog, Desktop task center APIs, and CLI
reindex surface on the shared multi-domain SQLite projection stack while
keeping history/usage registration and the hardened session foundation.
2026-08-11 01:52:01 +08:00
SivanCola 40b15fd327 merge: adopt usage catalog from #8218
Add idempotent usage rollups as a disposable SQLite projection with CLI
doctor/reindex surface, layered on the shared projection lifecycle and
preserving the hardened session/history foundations.
2026-08-11 01:50:15 +08:00
SivanCola 608543afee merge: adopt history search catalog from #8217
Add the token-only FTS5 history projection, non-blocking persist observer,
paged Desktop history manager, and CLI doctor/reindex surface on top of the
hardened session-catalog foundation. Keep corruption-only projectiondb
quarantine and session-catalog lifecycle fixes from #8186 while adopting
history session pagination (schema v3).
2026-08-11 01:49:38 +08:00
SivanCola 5cec97d996 merge: adopt session catalog foundation from #8186
Bring the non-blocking desktop session discovery and hardened disposable
SQLite projection lifecycle into the multi-domain catalog integration.
2026-08-11 01:48:25 +08:00
SivanCola 0556d1b528 fix(sessioncatalog): harden disposable SQLite lifecycle and non-blocking tree
Problem
Desktop session discovery still risked quarantining healthy catalogs on
busy/permission errors, rebuilding by renaming the live DB first, dropping
repair work beyond a fixed queue, and serving stale ProjectTree search results.
Off-lock controller builds could also re-publish removed MCP tools onto a shared
host, and shell-only project trees never expanded enough for lazy topic pages.

Root cause
Open treated every open failure as corruption; Rebuild swapped before validation;
repair enqueue was lossy; ProjectTree dropped concurrent queries while loading;
shared Host mutation raced plugin config changes; and active-folder expansion
required already-loaded topic children.

Fix
- Introduce internal/projectiondb for open/migrate/rebuild/quarantine with
  corruption-only quarantine, future-schema memory fallback, atomic rebuild,
  and 0700/0600 permissions.
- Route sessioncatalog through projectiondb; empty CacheDir uses memory; resume
  scan_cursor; drain unknown repairs from the durable index; bound per-topic
  session pages.
- ProjectTree uses request sequence + debounce and expands shell folders by
  workspace identity so lazy pages and bench mock load.
- Capture extensionGeneration around off-lock boot.Build and abandon publication
  when plugin/MCP config changes mid-build.
- Fix lint modernize issues and Windows migration-marker test flake.

Verification
- go test ./internal/sessioncatalog ./internal/projectiondb ./internal/pluginpkg
- go test -race ./internal/sessioncatalog ./internal/projectiondb
- cd desktop && go test -run 'Catalog|TopicMigration|ProjectTree.*Migrat'
- pnpm --dir desktop/frontend run typecheck
2026-08-11 01:38:27 +08:00
SivanCola 397ed30ec8 feat(agent): content-driven context maintenance (single compact_ratio) / 内容驱动上下文维护(单一 compact_ratio) (#8244)
* feat(agent): content-driven context maintenance with single compact_ratio

Replace multi-threshold prune/snip/native auto maintenance with one
content-driven checkpoint path (stable prefix + one summary + recent
tail) gated solely by compact_ratio (default 0.85). First-visible tool
results are bounded at creation; prune/snip APIs remain no-ops for
call-site compatibility while legacy config/sidecar fields load and
migrate away.

Also align max_output_tokens auto ladder (0=auto; 128K explicit only),
desktop/settings/docs, and force CompactNow planning so mid-size
strict-alternating sessions (e.g. guardian) still reduce tokens.

Verification: go test ./...; desktop go test ./...; frontend test:all;
cache-guard; race on core packages; live seed→resume→continue smoke
(version=1, one summary, ~$0.0012).

* fix(agent): make context projection durable on Windows and clear CI gates

Problem:
- SaveCompactionState fsynced the parent directory after rename. Windows
  returns ERROR_ACCESS_DENIED for directory Sync, so every projection
  install failed and Windows smoke tests went red.
- gofmt, repolint, frontend AgentView fixtures, and required cache/docs
  PR-body lines also blocked CI for #8244.

Root cause:
- Custom atomic write path reimplemented durability with a directory Sync
  that is not portable, instead of using the shared Windows-safe helper.
- Residual format/debt and a missing coldResumePrune fixture field.

Fix:
- Persist sidecars through fileutil.AtomicWriteFile (temp + file fsync +
  replace) without directory Sync.
- gofmt touched Go files; shorten essay comments; extract fold helpers;
  carry residual structural debt in the repolint baseline after reducing
  new essay debt; restore coldResumePrune on settings fixtures.

Verification:
- gofmt clean; go run ./tools/repolint clean
- go test ./internal/agent/ ./internal/config/ ./internal/provider/...
  ./internal/guardian/ ./internal/cli/
- pnpm settings-refresh-snapshot: 91 passed

* chore: nudge PR head refresh after Windows durability fix

Empty commit so GitHub re-resolves the fork head for #8244 after the
merge of main-v2 and the Windows sidecar write fix.

* fix(agent): close #8244 review blockers for content-driven maintenance

Problem:
Review of PR #8244 requested changes for CI failures, threshold under-count,
legacy native lineage drop, non-atomic CAS/write, residual cold-resume UI, and
duplicate context_maintenance notices.

Root cause:
- Threshold used message-only estimates without tools/role projection.
- Sidecar lineage required exact PromptCacheKey and marked restored before
  covered-hash validation.
- Checkpoint install checked transcript outside the install lock and used
  non-strict atomic write with EXDEV copy fallback.
- coldResumePrune UI/API remained after behavior removal; locale gzip grew.
- Maintenance notices ignored operationId on reconnect/replay.
- golangci reported unused prune/summary helpers and minor modernize issues.

Fix:
- Estimate compact trigger from final request shape (messages + tools +
  provider role projection).
- Normalize legacy |context-editing-native-* keys; restore only if projection
  still validates; persist normalized key without version bump/events.
- CAS install under compactionMu; SaveCompactionState uses AtomicWriteFileStrict.
- Remove coldResumePrune settings UI/Wails field/locales; keep config migration.
- Dedupe context_maintenance notices by operationId (bounded FIFO) + tests.
- Delete unused prune/summary helpers; clear golangci; trim locale strings for
  bundle budget; strip SPEC trailing space.

Verification:
- make lint (golangci + repolint) clean
- go test agent/config/control/guardian/provider
- pnpm frontend build (locale budgets pass)
- context-maintenance-notice + settings-refresh-snapshot tests

* fix(desktop): gofmt reasoning_display fixture after cold-prune removal

Desktop CI failed gofmt on reasoning_display_app.go after ColdResumePrune
was dropped from the AgentView fixture literal.

* fix(agent): unblock context-maintenance merge blockers

Problem: PR #8244 still had three merge blockers: emit under
compactionMu (deadlock risk), restore rewriting exact-key
sidecars, and initial raw bundle over budget.

Root cause: commitSummaryProjection held the lock through
synchronous sink.Emit; LoadProjectionSidecar persisted whenever
the key already matched; ContextPanel/TranscriptSelectionMenu
were eagerly imported into the initial chunk.

Fix:
- Unlock before emitContextMaintenance; reentrant sink test
- needsNormalization only for legacy native lineage rewrite;
  log persist errors instead of ignoring them
- Lazy-load ContextPanel and TranscriptSelectionMenu
- Document pre-interceptor threshold estimate + overflow recovery
- CLI/GOAL docs: compact_ratio default 85%

Verification:
- go test ./internal/agent/ (+ race focused tests)
- make lint (repolint baseline essay bump)
- pnpm build: initial raw 2236.8 / 2240 KiB
- context-maintenance-notice test ok

* fix(agent): close remaining context-maintenance plan gaps

Problem: After the merge blockers landed, plan gaps remained: no
parent-dir fsync on strict checkpoint writes, new writers still
mirrored last_*/blocked_* fields, dead CompactionCapabler shell,
and the pre-interceptor threshold contract lacked a regression test.

Root cause: AtomicWriteFileStrict only fsynced the temp file;
LastReceipt was authoritative but writers still dual-wrote mirrors;
provider.CompactionCapabler had no production callers; Prepare never
invoked interceptors by design without an explicit test.

Fix:
- Strict atomic write: fsync parent directory after rename (Windows/
  unsupported FS soft-fail)
- SaveCompactionState / commit / blocked paths: LastReceipt only;
  strip last_trigger/last_mode/token mirrors and top-level blocked_*
- Restore receipt-only blocked sidecars on load
- ContextReport/CLI/status prefer LastReceipt with legacy fallback
- Delete unused CompactionCapabler/NativeCompaction shell
- TestPrepareThresholdSkipsExtensionInterceptors

Verification:
- go test ./internal/agent ./internal/fileutil ./internal/provider
- go test -race focused packages
- make lint (repolint clean)

* docs: drop removed context_editing example from zh GUIDE

Problem: GUIDE.zh-CN still suggested context_editing = "native"
after native Anthropic context editing was removed and startup
migration deletes that key.

Root cause: bilingual GUIDE config sample was not fully synced
with the single compact_ratio local-summary contract.

Fix: remove the obsolete config line from docs/GUIDE.zh-CN.md.
English GUIDE already had no such example.

Verification: grep docs for remaining context_editing user examples
(only SPEC migration notes remain, which correctly describe removal).

* fix(fileutil): never fail strict write after successful rename

Problem: AtomicWriteFileStrict returned parent-dir fsync errors after
rename had already published the new file. commitSummaryProjection
and blocked-receipt writers treated that as "not committed" and
rolled memory back, forking disk (new generation) from process
memory (old generation) and allowing paid re-summary loops.

Root cause: post-publish power-loss durability was modeled as a
hard failure even though the atomic publish already completed.

Fix:
- After successful rename, parent-dir fsync is best-effort only
- Returned error still means "destination not published"
- syncParentDir also covers relative parents (".")
- Agent tests: applied and blocked receipts survive injected
  post-publish dir-sync faults without generation fork / re-summary
- PR body: document AtomicWriteFileStrict + best-effort dir fsync

Verification:
- go test ./internal/agent ./internal/fileutil
- make lint
2026-08-11 01:30:26 +08:00
SivanCola 576fda43eb Merge remote-tracking branch 'origin/main-v2' into feature/session-catalog 2026-08-11 01:26:01 +08:00
YHH 6b90a2cbcf Merge pull request #8252 from esengine/bench/read-only-output
bench(e2e): add the read-only-output shape the corpus never had
2026-08-11 00:40:04 +08:00
YHH 860e1e4219 Merge pull request #8246 from esengine/fix/subagent-call-nesting
fix: a delegated call must not look like the session's own work
2026-08-11 00:35:12 +08:00
esengine 7fd9559909 bench(e2e): add the read-only-output shape the corpus never had
Seventy tasks, and every one of them changes code: bugfix, refactor,
migrate, diagnose. The runaway a user reported was the other shape — read a
lot, change nothing, produce a written artifact — and the corpus could not
have caught it, nor can it measure the threshold that now guards it.

Three tasks in that shape, each requiring several files to be read before
anything can be written:

  document-handover              the incident itself: report what is done,
                                 what is half done, and which recorded claim
                                 is now false
  document-dependency-direction  state the layering, not just the module names
  audit-config-drift             find the one setting that differs across three
                                 environments

Each grader was checked in both directions, the discipline the no-solution
corpus already demands of itself: it fails on the pristine workdir, passes a
correct document, and fails a hollow one that names the right words without
having read anything. document-handover additionally fails if the code it was
asked to describe was edited — a handover that changes the state it reports
is not a handover.

No git in the graders: nothing else in the corpus assumes the workdir is a
repository, so containment is asserted against file contents instead.

The class is new and only groups report rows; nothing switches on it.

This is the measurement e2ebench could not take: how long a legitimate
look-only run gets before the work turns into writing. explorationRunLimit is
currently a judgement call informed by a synthetic trace, and this is what
would replace that with a distribution.
2026-08-10 09:30:32 -07:00
SivanCola f5b85dec2d Merge remote-tracking branch 'origin/main-v2' into feature/session-catalog
# Conflicts:
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-11 00:27:18 +08:00
SivanCola 2507187e97 Add logical cross-message transcript selection / 新增跨消息逻辑选区 (#8224)
* feat(desktop): add logical transcript selection

Problem:
Cross-message transcript selection either lost native endpoints when virtual rows unmounted or retained every selected row in the DOM.

Root cause:
Selection state depended on live DOM ranges, and Markdown rows had no frozen readable-text projection for virtualized content.

Fix:
- promote cross-row pointer drags to UTF-16 logical selection snapshots
- render selection overlays only for mounted rows and keep virtualization bounded
- derive copy text from the Markdown HAST, including LaTeX, code, and tables
- guard copy and Add to Chat against stale tabs and asynchronous results
- add browser, store, projection, cache, and clipboard coverage

Verification:
- pnpm --dir desktop/frontend test:transcript
- pnpm --dir desktop/frontend test:transcript-browser
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend build
- go run ./tools/repolint

Known baseline:
- test:bench still hits the retained-heap gate on both this branch and main-v2

* fix(desktop): preserve logical selection endpoints

Problem:
Logical transcript selection could slice Markdown at incompatible offsets, miss the final pointer position, or survive pointer cancellation.

Root cause:
The HAST projection inserted different block separators than the DOM adapter, pointerup stopped dragging before applying its coordinates, and pointercancel shared pointerup button filtering.

Fix:
Use one canonical structural newline within Markdown rows, keep source fallbacks in native mode, apply pointerup coordinates before settling, clear canceled gestures independently, and avoid full Transcript rerenders for logical focus frames.

Verification:
- pnpm test:transcript
- pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm test:motion
- pnpm build
- go run ./tools/repolint

* Stabilize logical transcript selection

Problem:
- Cross-page logical selections could lose their overlay after virtual rows committed.
- Long transcripts recomputed layout styles for every cold row estimate.
- Settled selections retained projection pins outside the final selected interval.

Root cause:
- Focus reconciliation could run before the virtualizer committed replacement DOM, overlay observers restarted on every selection snapshot, and cache pins tracked no row positions.

Fix:
- Reconcile focus after virtual DOM mutations and add a bounded overlay stabilization paint.
- Cache one layout snapshot per measurement epoch and reuse it for row estimates.
- Trim frozen rows and release unselected projection pins when logical selection settles.

Verification:
- pnpm test:transcript
- pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm test:motion
- pnpm build
- go run ./tools/repolint

* Fix logical transcript selection races

Problem:
Linux browser runs could lose the logical selection overlay. Streaming tokens also rebuilt every historical text projection, expanded offscreen reasoning was omitted, and edge scrolling kept requesting frames at a loaded-history boundary.

Root cause:
Logical focus reconciliation depended on DOM mutation timing, live content was mixed into the full row projection pass, reasoning membership depended on mounted DOM, and the edge loop rescheduled without confirming movement.

Fix:
Reconcile focus after virtual range commits, split stable and live selection projections, use the structural row model for reasoning membership, stop edge scrolling when no offset is written, and cover each path with deterministic tests.

Verification:
- pnpm test:transcript
- CI=true pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm test:motion
- pnpm build
- go run ./tools/repolint
- pnpm test:bench (all relevant gates pass; existing retained-heap baseline remains above its repository gate)

* Reconcile selection on virtualizer commits

Problem:
Linux desktop CI could finish edge scrolling at the loaded-history boundary with zero logical-selection overlay rectangles.

Root cause:
TanStack direct DOM updates can advance the final virtual window without a React virtualRevision commit, and the boundary path stopped before one last caret remap.

Fix:
Reconcile logical focus from the virtualizer onChange callback and schedule exactly one final focus pass when edge scrolling reaches the loaded-history boundary.

Verification:
- pnpm test:transcript
- CI=true pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
- go run ./tools/repolint

* Cover both virtual selection commit paths

Problem:
Linux desktop CI could still finish a logical edge-selection with no visible overlay after a virtual page transition.

Root cause:
TanStack direct-DOM range changes can skip a React commit, while its onChange callback can also run before replacement row DOM commits. Either notification alone leaves one ordering uncovered.

Fix:
Reconcile the logical caret from both the virtualizer callback and the post-commit virtual revision. The existing RAF guard coalesces duplicate notifications, and the loaded-history boundary still schedules only one final focus pass.

Verification:
- pnpm test:transcript
- CI=true pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
- go run ./tools/repolint

* Reject stale virtual caret targets

Problem:
Linux Chromium repeatedly completed an upward logical drag with mounted rows but no overlay rectangles.

Root cause:
After transformed virtual rows changed, caretPositionFromPoint could return a still-mounted row that no longer matched the pointer's geometric hit target. Accepting that stale row left the logical focus below the visible window.

Fix:
Resolve the geometric selectable root first and accept caret API offsets only when they belong to that root. Otherwise use the visible root fallback. Remove the redundant post-commit callback and cover the stale-caret case with a deterministic DOM test.

Verification:
- pnpm test:transcript
- CI=true pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
- go run ./tools/repolint

* Improve transcript selection CI diagnostics

Problem:
The Linux browser gate reports zero logical-selection overlay rectangles even though the same gesture passes locally, leaving no structural evidence to distinguish caret mapping from Range layout failures.

Root cause:
The benchmark only reported row count and scroll-owner data at the assertion boundary.

Fix:
Record structural, content-free browser diagnostics for the edge hit, caret row, mounted and visible selectable rows, scroll geometry, and rows with positive DOM Range rectangles.

Safety:
No message text, selected text, clipboard data, or row identifiers are logged; production code and behavior are unchanged.

Tests:
- node --check desktop/frontend/bench/transcript-selection.mjs
- CI=true pnpm test:transcript-browser

* Reconcile logical focus after virtual commits

Problem:
On Linux, edge scrolling could mount a new virtual page while logical selection focus remained on the prior page, leaving no selected mounted row for the overlay to paint.

Root cause:
The focus RAF ran in the same frame as TanStack virtualizer updates. Coalescing then discarded the commit-triggered request, so narrow mounted windows could keep the caret one virtual page behind.

Fix:
Use a bounded two-frame reconciliation: the first frame lets the virtualizer commit its direct DOM update and the second resolves the caret against that committed page. Pointerup and cleanup still cancel the tracked frame and apply the exact final focus synchronously.

Safety:
The scheduler retains at most one tracked focus frame, does not add scroll writes, does not log transcript content, and keeps the existing bundle budget.

Tests:
- pnpm test:transcript
- CI=true pnpm test:transcript-browser
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
- go run ./tools/repolint

* Make transcript drag depth deterministic

Problem:
The Linux browser test reached a valid logical overlay but copied only 14 turns because a fixed six-second edge-scroll duration covered fewer rows under Ubuntu font and layout metrics.

Root cause:
The gesture depth was time-based even though the assertion was content-depth based.

Fix:
Wait until the active drag reaches the first 30 percent of the loaded transcript scroll range before evaluating and copying. The existing 20+ turn assertion remains unchanged.

Safety:
This changes only the production-build browser benchmark, does not reduce coverage, and does not log transcript or selected text.

Tests:
- node --check desktop/frontend/bench/transcript-selection.mjs
- CI=true pnpm test:transcript-browser

* Settle transcript drag on visible text

Problem:
After reaching the required history depth, the Linux benchmark could stop inside a viewport-filling tool row. The settled selection correctly released its offscreen overscan row, so a visible-overlay assertion had no visible selectable target.

Root cause:
The gesture completion predicate checked scroll depth but not whether selectable transcript text intersected the viewport.

Fix:
Keep edge scrolling until both the 30-percent history depth and a visible selectable body are present before pointerup.

Safety:
The test still requires bounded DOM, a painted dragging and settled overlay, 20+ copied turns, and restoration after scrolling away and back. Product code and privacy behavior are unchanged.

Tests:
- node --check desktop/frontend/bench/transcript-selection.mjs
- CI=true pnpm test:transcript-browser

* Stabilize deep transcript drag sampling

Problem: the browser gate sampled logical selection while edge auto-scroll was still advancing, so machine-dependent row heights could leave the caret on a transient or tool-only row and occasionally cover only 19 turns.

Root cause: a scroll-depth ratio was used as a proxy for the intended selectable endpoint, and the assertion raced the selection focus and overlay animation frames.

Fix: stop edge scrolling at a neutral viewport point, locate a visible deterministic early-turn body using user wheel input, move the live pointer onto it, and wait for the logical overlay before sampling diagnostics.

Safety: production code and selection semantics are unchanged; the gate still requires 20+ turns, bounded virtual DOM, selection-only scroll ownership, overlay restoration, copy parity, and the 2MiB cleanup budget.

Tests: CI=true pnpm test:transcript-browser; pnpm test:transcript; node --check bench/transcript-selection.mjs; git diff --check
2026-08-11 00:23:50 +08:00
YHH d74ed6ea03 Merge pull request #8250 from esengine/feat/scope-escape-real
feat(agent): a write outside the approved plan reads as expanded scope
2026-08-11 00:19:31 +08:00
esengine 3f530b5f58 docs(subagent): record how serve renders a delegated call 2026-08-10 09:18:55 -07:00
esengine f204d00e37 fix(serve): nest sub-agent calls under the call that spawned them
The web frontend read parentId only to filter todo_write, so a sub-agent's
tool calls were appended to the timeline as top-level cards —
indistinguishable from the session's own work. A user who asked for an
install and saw "blocked: read-only subagents can run only ..." on a
phone reasonably concluded the new session itself was a subagent.

Sub-calls now render inside their parent card, matching what the desktop
Transcript already does: the parent opens while it runs and collapses when
it settles, unless the user has taken over the toggle. Card folding moves
from an inline style to the data-open attribute so the body and the nested
region fold together.

Three fixes are specific to the phone layout that produced the report:
narrow screens hide the status badge, so a refusal kept the badge — a red
icon alone reads as "failed" rather than "never ran"; the preflight phase
is shown as a localized "not executed"; and the tool name no longer
competes with the argument summary for width, which was truncating bash to
"b…" on the very cards a user needs to identify.
2026-08-10 09:18:54 -07:00
esengine d97bf94e75 fix(agent): a tool's own refusal reports as blocked, not as a completed call
A read-only subagent that rejected its own bash command returned the
refusal as ordinary output with a nil error. The agent had no way to see
it: the call was recorded as a success, frontends drew a green check on a
command that never ran, and the storm breaker — which counts blocked
outcomes to detect a model bouncing off the same host refusal — was blind
to it.

The host already has one shape for a refusal, toolOutcome{blocked, errMsg},
produced by the permission, plan-mode, and hook gates. What was missing was
a way for a tool enforcing its own policy to reach it. tool.Blocked carries
that signal out of Execute; executeOne turns it into the standard blocked
outcome, with the not-run shell metadata a blocked bash card already
renders elsewhere.

Both subagent bash wrappers now report through it, so the read-only command
policy and the background-execution rule refuse the same way. The review
and desktop-try registries share the wrapper, so their contract assertions
move with it.

Extracting blockedToolOutcome/shellPreflightExecution and the evidence
receipts keeps execute_one.go inside its ratchet budget.
2026-08-10 09:18:37 -07:00
esengine e97396b7e9 feat(agent): a write outside the approved plan reads as expanded scope
The reviewer already weighs ExpandedScope, but the rule behind it compares
the failed call's paths against the retry's: it asks whether a retry drifted,
which says nothing about the plan the user agreed to. A model that abandons
the approved surface entirely and edits somewhere else answers "no drift".

The plan names its touchpoints and the contract already carries them, so the
question can be asked directly: does this pending write touch a path the
approved plan never named. That is the fourth of the automatic replan
conditions, and the only one whose evidence is already flowing.

Two deliberate limits, both about not over-claiming:

  - A plan that named no touchpoints says nothing about scope, so nothing
    escapes it. Silence is not a claim that everything is out of bounds.
  - Directory containment counts. A plan naming internal/pay/retry.go puts
    internal/pay in play, which is how the test file beside it stays in
    scope without the planner having to enumerate it.

Candidate paths count as scope even though they were inferred rather than
read: scope is where work is expected, not a claim of fact — the same
distinction the plan already draws everywhere a claim is made.

The old rule is unchanged and still runs; this widens what the field means
rather than replacing how it was computed.
2026-08-10 09:00:18 -07:00
YHH 53b83d2a98 Merge pull request #8249 from esengine/feat/plan-diff-review
feat(agent): the plan reviewer is told what moved
2026-08-10 23:58:57 +08:00
esengine 3a9f433aca feat(agent): the plan reviewer is told what moved
recoveryPlanTransition handed the isolated reviewer two rendered task lists
and left it to work out the difference. samplePreview clips both before the
reviewer sees them, so on a long plan the change can fall past the cut — the
reviewer is asked to judge a rewrite it cannot fully see.

It now also receives an identity-paired diff. The task list is the plan's
projection, so rebuilding its shape as a Plan lets #8235's Compare serve both
layers instead of growing a second implementation: an inserted step reads as
Added with its neighbours Preserved, and a retitle under a kept id reads as
Changed rather than as a replacement.

It stands down when either side predates step ids. Pairing a list without
them would read every step as replaced, which is worse than the plain
renderings the reviewer already has.

Scope narrowed once the types were read: recovery.Proposal is an alias for
agent.RecoveryProposal, so the reviewer already receives the field and
PendingProposal never needed it. That copy is gone, and with it the ratchet
pressure it created.

execute_one.go shrinks: the recovery proposal assembly moved out beside the
diff, which is the field most likely to grow next. repolint 1958 -> 1957.
2026-08-10 08:45:13 -07:00
YHH 70aa702ea0 Merge pull request #8235 from esengine/feat/plan-diff
feat(plancontract): compare two revisions instead of two strings
2026-08-10 23:29:51 +08:00
esengine 7f4292e090 feat(plancontract): compare two revisions instead of two strings
The last place in the pipeline where a structured thing was compared as
text. recoveryPlanTransition detects a structural rewrite of an active plan
and hands an independent reviewer two rendered strings:

    return true, planReviewText(before), planReviewText(after)

A text diff cannot distinguish "s2's touchpoints widened" from "s2 was
replaced by different work", and it calls every step below an insertion
changed. Compare pairs by step id — which is why identity is host-assigned
and never regenerated — so an inserted step is Added and its neighbours stay
Preserved.

NeedsApproval turns the diff into the decision it exists for: a revision
that expands what the user agreed to asks again, and one that narrows,
reorders, retitles, or adds evidence does not. Re-asking for a narrowing
trains the user to approve without reading, which costs more than the gate
saves.

It ships with a consumer rather than waiting for one: a planner that
resubmits now gets the diff back as its tool result. Left to describe its own
edit a model reports intent instead of effect, and a step it dropped by
accident reads exactly like one it meant to keep.

A revision that changed nothing renders nothing — Preserved is the
reassurance beside a change, not a standalone report. The test that caught
that is in the file.

No schema change: only the tool's runtime result text moves, and the boot
golden baseline passes unchanged.
2026-08-10 08:11:06 -07:00
YHH e2d056bc44 Merge pull request #8233 from esengine/fix/exploration-decay
fix(evidence): exploration stops counting once it stops leading anywhere
2026-08-10 23:09:45 +08:00
esengine 7e4518d58c fix(evidence): exploration stops counting once it stops leading anywhere
The no-progress ladder escalates on consecutive zero-gain rounds, and
gainNewRead scores every first-seen path. A turn that keeps opening files it
has never opened therefore scored positive on every round, reset the streak
every round, and could never reach the first rung — which is how the reported
turn read for four hours across 524 requests without writing the document it
was asked for.

Reading something new is progress while it might still lead somewhere. Past
six consecutive rounds that read and did nothing else, the novelty stops
counting; any mutation or command in a round starts the run over, so an
ordinary read-read-edit-verify rhythm is untouched.

The measurement is in the tests, not the reasoning. Before, all eight
wandering rounds scored 1, identical to real work's opening reads. Now rounds
seven and eight score 0 while real work is unchanged, because its longest
look-only run is two.

TestReadOnlyWanderingNeverTripsTheProgressGuard, landed in #8225 to record
the gap, inverts to TestReadOnlyWanderingTripsTheProgressGuard. The outcome
comparison now pins both sides of the threshold: never decaying is the
runaway, decaying at once would nudge ordinary investigation on arrival.

This is the judgement half. #8228 is the ceiling half; neither replaces the
other, and the whole repository's 116 packages pass unchanged.
2026-08-10 07:37:42 -07:00
YHH 08f84bcc79 Merge pull request #8228 from esengine/fix/chat-run-budget
fix(control): give an ordinary chat turn a Run ceiling
2026-08-10 22:37:36 +08:00
esengine a494b4f0a0 fix(control): give an ordinary chat turn a Run ceiling
A user report: four hours, 524 requests, 194M tokens, and the handover
document never written. The turn was reading one more file every round.

#8185 bounded Goal Runs at 16 rounds and said so precisely — "these hard
boundaries apply only to active Goal execution; ordinary chat keeps the
existing advisory behavior". Ordinary chat had nothing: [agent].max_steps
was retired in #6497 and is normalized to zero, and the run loop reads
`runMaxSteps <= 0` as no ceiling at all.

The adaptive guards were the stated replacement, and they cannot cover this.
They escalate on consecutive zero-gain rounds, while gainNewRead scores every
first-seen path, so a loop that keeps touching something new resets the
streak before it can climb. internal/agent/runaway_repro_test.go reproduces
that in 0.01s with no context maintenance at all.

This reuses #8185's mechanism rather than adding a parallel one: same
WithDefaultRunStepLimit, so an explicit max_steps still wins, child agents
still inherit, and crossing the line still yields one tool-free summary and a
resumable pause instead of a truncated turn. 100 rounds is a runaway
backstop, not a budget — real work stays far under it, and being wrong costs
one "continue" while leaving it unset costs hours.

The Goal recorder setup moved with it into bindTurnScope, so the decision
"what bounds this turn" lives in one place and runOrchestratedTurn shrinks
rather than grows.
2026-08-10 07:21:07 -07:00
YHH 990ec45c98 Merge pull request #8229 from esengine/fix/steer-test-timeout
test: two CI flakes, both a fixed budget standing in for "wait until settled"
2026-08-10 22:17:23 +08:00
esengine 28da5bb0ff test(desktop): widen the transcript harness wait from 240ms to 3s
The desktop job failed on #8228 — a Go-only change to internal/control —
with "timed out waiting for the pre-prepend anchor row to remount". Same
shape as the Go flake in the previous commit, one layer over: a fixed budget
standing in for "wait until settled".

  const waitFor = async (..., attempts = 8) => { ... await flush() }
  const flush   = async () => { await act(() => sleep(30)) }

Eight attempts reads like a retry count; it is a 240ms deadline. Remounting
fifteen rows after a prepend does not reliably finish inside that on a loaded
runner, and the desktop job passed on the three PRs before it.

waitFor returns the moment the condition holds, so a larger cap costs nothing
on the happy path and only makes a genuine failure slower — which beats
failing a correct test.

Not verified locally: the frontend workspace has no node_modules here and
installing it needs network. The change cannot turn a passing test red — a
wider retry cap only converts a timeout into a pass or the same failure
later — so CI is the appropriate verifier.
2026-08-10 06:58:36 -07:00
esengine 8a902b3410 test(control): stop failing the thirty-steer test on a slow runner
TestThirtySteersApplyAndAckExactlyOnce failed on two unrelated PRs — #8203,
which changed one JSON data file, and #8222 — and passed on rerun both times.
The Windows job log names it exactly:

    --- FAIL: TestThirtySteersApplyAndAckExactlyOnce (18.64s)
        inbox_test.go:210: timed out waiting for TurnDone

Not a race: twelve local -race runs pass. The thirty durable enqueue/steer
round trips are real filesystem work, and on a loaded Windows runner they
consume roughly 13 of the wall clock before the turn is released, leaving the
helper's fixed five-second budget to expire on arrival.

The test asserts exactly-once acknowledgement, not latency, so the five
seconds was an accidental constraint. waitForDoneWithin already exists for
this; the assertions are unchanged.

A test that fails on unrelated PRs and passes on rerun costs more than the CI
minutes: it teaches everyone to re-run red instead of reading it.
2026-08-10 06:43:56 -07:00
YHH 46e5b5d82e Merge pull request #8225 from esengine/test/runaway-repro
test(agent): reproduce the unbounded read-only runaway
2026-08-10 21:36:55 +08:00
esengine 530809206b test(agent): reproduce the unbounded read-only runaway
A user report: four hours, 524 requests, 194M tokens, ¥27, and the handover
document never written. The model kept restating one intent and reading one
more file. #8185 bounded Goal Runs at 16 rounds, but says so explicitly —
"these hard boundaries apply only to active Goal execution; ordinary chat
keeps the existing advisory behavior" — and this happened in ordinary chat.

These tests reproduce it in 0.01s with no provider and, deliberately, no
context maintenance at all: they assert zero ContextMaintenanceEvent, so the
shape is isolated from the tool-result trimming under separate investigation.
It runs away on its own.

  read a new path each round   40 rounds, guard silent, turn never self-limits
  read one fixed path          the same loop, caught within a few rounds

Novelty is the only variable. gainNewRead is scored per first-seen path, and
any positive round resets the streak, so a turn that keeps touching something
new can never reach the nudge threshold — and with [agent].max_steps retired
in #6497 there is no ceiling behind it.

The outcome test measures what a promotion decision needs: the legacy scorer
reports positive gain on all eight wandering rounds, identical to real work's
opening reads, while the outcome decomposition separates them — the runaway
is exploration with zero discriminating observations, and real work reaches
one within two rounds. It also records that a repeated verification scores
zero legacy gain, so re-running the tests after a second edit reads as no
progress.

Tests only; no behaviour changes. They fail the day either gap is closed,
which is the point.
2026-08-10 06:23:21 -07:00
YHH c9f21a1bdf Merge pull request #8222 from esengine/feat/final-rejection
feat(agent): an unproven acceptance criterion blocks the final answer
2026-08-10 21:17:50 +08:00
YHH e83b8dbe0e Merge pull request #8221 from esengine/fix/context-gauge-source
fix(control): the context gauge must measure what the trigger measures
2026-08-10 20:55:33 +08:00
esengine e3da7abd9b fix(desktop): keep the context panel's total on its own breakdown
The panel splits one turn into prompt/completion/reasoning/cache segments, so
its total has to be that turn's usage. Reading the live-view gauge instead left
the segments no longer summing to the total. The status-bar gauge keeps the
live measurement; this panel keeps the turn.
2026-08-10 05:42:39 -07:00
esengine 29e53e0cf8 feat(agent): an unproven acceptance criterion blocks the final answer
The contract has known what the work agreed to since #8183 and been able to
answer mid-turn since #8193, and criteria became provable in #8201. Nothing
consulted it. A turn could satisfy every delivery ceremony — todos closed,
complete_step called, verification run — while a criterion the user approved
sat unproven, because those ceremonies check that steps happened, not that
the agreed outcome holds.

finalReadinessCheckFor now folds the contract's outstanding criteria into
the same `missing` list it already builds. One gate, one FinalReadinessError,
one host recovery path: the Goal FSM's auto-continue and the recovery card
work unchanged, which they would not if this were a second parallel refusal.

The existing missingAcceptanceCriteria field asked whether criteria were
*established*; this asks whether they are *proven*, and names which one. A
proof that went stale under a later mutation reads as outstanding again, so
"verified before I changed it" stops counting as verified.

Only a planned turn contributes. Without a plan the contract's requirements
are the todo list, which the checks above already cover, so an unplanned turn
inherits nothing.

Placed before the loop-guard escape deliberately: a criterion the model
cannot prove must still be able to stop asking, or an unprovable acceptance
line ("P95 under 100ms") would loop forever. The guard that already exists
for that keeps working.

Outstanding() now annotates a stale requirement the way it already did a
stale check — "verify it" and "re-verify it, the code moved" are different
instructions and only one is actionable.

Debt moved, not added: readiness left agent.go for final_readiness.go, so
agent.go drops 233 lines (complexity 61->37, function-size 125->103) while
the new file records exactly what moved. Every baseline total falls or holds
— complexity 2099->2096, file-size 110345->110104, function-size 9165->9159,
essay 4085->4082, test-file-size flat.
2026-08-10 05:36:32 -07:00
SivanCola 3029887810 Merge branch 'feature/session-catalog' into feature/task-catalog 2026-08-10 20:32:02 +08:00
SivanCola 45b7a25466 Merge branch 'feature/session-catalog' into feature/usage-catalog 2026-08-10 20:32:01 +08:00
SivanCola 04f8f3bed6 Merge branch 'feature/session-catalog' into feature/history-search-catalog 2026-08-10 20:32:01 +08:00
SivanCola 10d469d375 Merge remote-tracking branch 'origin/main-v2' into feature/session-catalog 2026-08-10 20:31:54 +08:00
SivanCola 3fa5142166 fix(history): modernize activity clamping
Problem: golangci-lint flagged duplicated manual lower-bound checks in full and incremental History indexing.

Fix: use the Go max builtin to clamp pre-epoch activity timestamps consistently.

Verification: historycatalog/history tests pass and golangci-lint reports zero issues.
2026-08-10 20:30:23 +08:00
SivanCola 281af09afa fix(storage): remove superseded catalog helpers
Problem: after Session Catalog adopted projectiondb, the old filesystem probes and migration entrypoint remained unused, while CI also flagged two modern Go style issues.

Root cause: the common projection extraction preserved obsolete package-local helpers to minimize the first refactor diff.

Fix: remove the dead remote-filesystem and migration helpers, use integer range in the benchmark, and iterate managed plugin path segments with strings.SplitSeq.

Verification: sessioncatalog, pluginpkg, and projectiondb tests pass; golangci-lint reports zero issues.
2026-08-10 20:28:58 +08:00
SivanCola 81b56f2435 fix(storage): remove superseded catalog helpers
Problem: after Session Catalog adopted projectiondb, the old filesystem probes and migration entrypoint remained unused, while CI also flagged two modern Go style issues.

Root cause: the common projection extraction preserved obsolete package-local helpers to minimize the first refactor diff.

Fix: remove the dead remote-filesystem and migration helpers, use integer range in the benchmark, and iterate managed plugin path segments with strings.SplitSeq.

Verification: sessioncatalog, pluginpkg, and projectiondb tests pass; golangci-lint reports zero issues.
2026-08-10 20:28:51 +08:00
SivanCola f80f273fb7 fix(tasks): make reconcile shutdown lint-safe
Problem: the shutdown barrier used an intentionally empty mutex critical section, which staticcheck rejects.

Fix: guard a reconcileDone flag with the same mutex used for scheduling so Close prevents late WaitGroup additions without an empty critical section.

Verification: taskcatalog tests pass and golangci-lint reports zero issues.
2026-08-10 20:28:36 +08:00
SivanCola 20d5081717 fix(storage): remove superseded catalog helpers
Problem: after Session Catalog adopted projectiondb, the old filesystem probes and migration entrypoint remained unused, while CI also flagged two modern Go style issues.

Root cause: the common projection extraction preserved obsolete package-local helpers to minimize the first refactor diff.

Fix: remove the dead remote-filesystem and migration helpers, use integer range in the benchmark, and iterate managed plugin path segments with strings.SplitSeq.

Verification: sessioncatalog, pluginpkg, and projectiondb tests pass; golangci-lint reports zero issues.
2026-08-10 20:28:28 +08:00
YHH d7fc53c1a7 Merge pull request #8201 from esengine/feat/criterion-proof
feat(agent): a proof binds to the criterion it proves
2026-08-10 20:28:18 +08:00
esengine 3c79fad50d fix(control): the context gauge must measure what the trigger measures
ContextSnapshot fed every frontend's gauge from the last turn's provider usage
(promptTokens + completionTokens). Compaction decides on something else: the
estimated size of the model-visible projection the next request will send. Two
channels, and they disagree in three ways at once. The gauge lags a turn, so a
turn that just took on a large tool result reads small while the trigger has
already fired. It counts completion tokens the trigger never looks at. And it
reads zero until the first turn of a rebound session, which desktop does on
every tab switch and every recovery adopt.

Measured on a recorded 13-message session: the old gauge reads 0 and the
trigger's input is 27076. Desktop papered over the zero by falling back to a
telemetry-persisted last-used value, so the bar showed a plausible but
unrelated number instead of an obviously broken one. That is the "上下文充足
8%" sitting next to "正在压缩对话" — the gauge was never reporting the quantity
being compared against compact_ratio.

ContextUsedTokens is now that quantity, memoised against the transcript
version, the projection version and the calibration, since gauges redraw far
more often than any of the three move. The desktop fallback goes with it: a
rebound session measures its own view immediately.
2026-08-10 05:25:54 -07:00
SivanCola 2bb0b0d172 merge: refresh task catalog on latest session foundation
# Conflicts:
#	tools/repolint/baseline.json
2026-08-10 20:23:07 +08:00
SivanCola 9cdc0671c4 merge: refresh usage catalog on latest session foundation
# Conflicts:
#	tools/repolint/baseline.json
2026-08-10 20:22:37 +08:00
SivanCola b5778f50d1 merge: refresh history catalog on latest session foundation
# Conflicts:
#	tools/repolint/baseline.json
2026-08-10 20:22:01 +08:00
SivanCola 934bb7031d merge: refresh session catalog on latest main-v2 2026-08-10 20:20:39 +08:00
SivanCola 35839b4671 chore(tasks): refresh lint baseline after main sync
Problem: the latest main-v2 transcript-selection work intentionally enlarged existing frontend files after the Task branch had captured its repository lint baseline.

Fix: regenerate the carry-forward baseline on the merged Task branch without adding new domain violations.

Verification: repolint, focused Desktop tests, frontend production build, and task/control package tests pass.
2026-08-10 20:18:33 +08:00
SivanCola dd7fec6be4 merge: refresh task catalog on latest session foundation
# Conflicts:
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-10 20:17:51 +08:00
SivanCola 8ea4982cf7 chore(stats): refresh lint baseline after main sync
Problem: the latest main-v2 transcript-selection work intentionally enlarged existing frontend files after the Usage branch had captured its repository lint baseline.

Fix: regenerate the carry-forward baseline on the merged Usage branch without adding new domain violations.

Verification: repolint, projection/statistics tests, and usage race tests pass.
2026-08-10 20:17:08 +08:00
SivanCola ddb58e536b merge: refresh usage catalog on latest session foundation 2026-08-10 20:16:21 +08:00
SivanCola 830e21e7cc chore(history): refresh lint baseline after main sync
Problem: the latest main-v2 transcript-selection work intentionally enlarged existing frontend files after the History branch had captured its repository lint baseline.

Fix: regenerate the carry-forward baseline on the merged History branch without adding new domain violations.

Verification: repolint is clean and git diff --check passes.
2026-08-10 20:16:15 +08:00
YHH 075625912c Merge pull request #8207 from esengine/fix/keep-prompt-calibration
fix(agent): keep the prompt-token calibration across a session swap
2026-08-10 20:12:02 +08:00
esengine ae69b417ef feat(agent): a proof binds to the criterion it proves
The contract took its requirements from the plan in #8183, but nothing could
ever satisfy them: the only Contract.Resolve call in the tree resolves
todo-derived ids, and Observe auto-satisfies only Atomic's requirement. A
planned turn's criteria were permanently Pending. Harmless while the
contract only observes, and a hard break the moment FinalRejection is wired
— every final answer would be refused forever.

complete_step evidence can now name the criterion it proves. The tool
already verifies each proof against the ledger, so the citation is what adds
the binding: "the command ran" and "the criterion holds" are different
claims, and only the model knows which proof was for which. The host records
it as an EvidenceRef at the current epoch, which makes freshness work — a
criterion proven by a test run goes Stale the moment code changes underneath
it, while mutation evidence does not, because a change having happened is
not something a later change undoes.

Identity had to stop being regenerated at the boundary, the same defect as
step ids: FromPlan re-keyed the plan's criteria to r1/g1/o1, so a proof
citing c2 could never match. PlanFacts carries PlanCriterion{ID, Text} and
FromPlan uses the id the plan assigned. Render now prints it — a criterion
the executor cannot name is one it cannot satisfy, and dropping the id from
the rendered plan was my own omission.

A citation the plan does not contain is rejected in the tool, with the ids it
may cite, rather than resolving into nothing and surfacing later as a refusal
the model cannot connect to this call.

Cache: tool schema grows 99 tokens (14303 -> 14402) for complete_step's
criterion_id; SystemHash unchanged, prefix byte-stable after the one-time
change. Golden regenerated.

execute_one.go stayed inside its ratchet by folding both context injections
into one call — the task list and the criterion ids travel together because
both answer "does this claim name something real".
2026-08-10 05:11:49 -07:00
YHH ee4a0c930c Merge pull request #8203 from esengine/chore/repolint-baseline-app-tsx
chore(repolint): re-record the baseline — 33 entries tightened, none loosened
2026-08-10 20:10:22 +08:00
SivanCola 84839d9954 merge: refresh history catalog on latest session foundation
# Conflicts:
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-10 20:09:33 +08:00
SivanCola 3a6d65ac6f merge: refresh session catalog on latest main-v2
# Conflicts:
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-10 20:08:07 +08:00
SivanCola 3739f12a7d fix(tasks): complete catalog lifecycle contracts
Problem: the isolated Task Catalog branch lacked shell completion metadata and only flushed its shared projection during desktop shutdown, allowing late reconcile goroutines to outlive the shutdown boundary.

Root cause: the task implementation was originally layered on the integrated History branch, so domain-specific lifecycle and documentation details were lost when it was split into an independently reviewable branch.

Fix: register task reindex completions, document the task cache and recovery commands, gate reconcile scheduling during close, wait for scheduled workers, close the shared catalog within the 250ms desktop drain, and account for the measured 1 KiB frontend bundle increase.

Verification: focused Desktop tests, frontend production build and bundle budgets, repository lint, core package tests, and race tests for projectiondb, taskcatalog, taskmonitor, and control passed.
2026-08-10 20:06:02 +08:00
SivanCola 7d99299c98 feat(tasks): add cross-project task catalog
Problem: Task Monitor could only scan the active project synchronously and could not page or safely route controls across projects.

Root cause: Task snapshots and event logs had no disposable query projection, while control APIs implicitly depended on the active tab instead of a stable project identity.

Fix: Add an observed authoritative FileStore, resumable snapshot and lazy event indexing, SHA-256 project-key routing, runtime overlays, paged Wails APIs, a cross-project Task Center, diagnostics, and bilingual documentation.

Verification: Covered by lock-boundary notifications, corrupt snapshot/event isolation, event-tail pagination, lease reconciliation, stale cursor, project-key action routing, Wails arrays, and 100,000-task warm-page benchmark tests.
2026-08-10 20:00:56 +08:00
SivanCola 87bf32c0ea refactor(storage): share disposable projection lifecycle
Problem: Each disposable catalog would otherwise duplicate SQLite migration, integrity, quarantine, permission, and in-memory fallback behavior.

Root cause: The original session catalog owned database lifecycle policy together with session-specific schema and reconciliation logic.

Fix: Extract the reusable lifecycle into internal/projectiondb, migrate the session catalog to it, and add schema-v3 keyset session pagination for follow-up consumers.

Verification: Covered by projectiondb lifecycle tests and session catalog migration, pagination, cursor, and compatibility tests.
2026-08-10 20:00:05 +08:00
SivanCola d9ca43ac5e docs(stats): document usage catalog recovery
Problem: Operators need a discoverable cache path, diagnostic command, exact-fallback guarantee, and safe rebuild path for usage rollups.

Fix: Link the bilingual Usage catalog guide from CLI and storage references and ratchet repolint to the Usage-only branch shape.

Verification: repolint baseline generation and git diff --check passed.
2026-08-10 19:58:55 +08:00
SivanCola 723d5cd51e fix(cli): register usage catalog completions
Problem: The catalogs dispatcher was absent from shell completion and did not expose the Usage reindex flags.

Root cause: Catalog execution and completion used separate static registries.

Fix: Build the catalogs reindex completion tree from the same domain registration and publish the Usage --json contract without increasing completion-function size.

Verification: go test ./internal/cli passed.
2026-08-10 19:58:36 +08:00
YHH 8c8c981e5a Merge pull request #8212 from esengine/fix/report-run-failure
fix(cli): a failed run must say why, including under -p
2026-08-10 19:58:11 +08:00
SivanCola 95b5857e41 feat(stats): add idempotent usage rollups
Problem: Long-range usage queries repeatedly decoded every daily JSONL record, making the statistics page increasingly expensive.

Root cause: The authoritative append log had no offset ledger or derived aggregation layer, so exact queries always rescanned source files.

Fix: Add a disposable usage catalog with path-offset idempotency, line-hash rewrite detection, tail reconciliation, daily rollups, exact JSONL fallback for incomplete ranges, bounded flush integration, diagnostics, and bilingual documentation.

Verification: Covered by duplicate receipt, tail append, file rewrite, corrupt-line, legacy request, SQL-versus-JSONL parity, and million-record warm-query benchmark tests.
2026-08-10 19:57:37 +08:00
SivanCola 0548479633 refactor(storage): share disposable projection lifecycle
Problem: Each disposable catalog would otherwise duplicate SQLite migration, integrity, quarantine, permission, and in-memory fallback behavior.

Root cause: The original session catalog owned database lifecycle policy together with session-specific schema and reconciliation logic.

Fix: Extract the reusable lifecycle into internal/projectiondb, migrate the session catalog to it, and add schema-v3 keyset session pagination for follow-up consumers.

Verification: Covered by projectiondb lifecycle tests and session catalog migration, pagination, cursor, and compatibility tests.
2026-08-10 19:57:37 +08:00
YHH 6b285fba81 Merge pull request #8210 from esengine/refactor/single-compaction-trigger
refactor(agent): one compaction trigger, and stop rewriting history below it
2026-08-10 19:55:59 +08:00
SivanCola 9bf26dd89b fix(desktop): preserve catalog history compatibility
Problem: Catalog-backed compatibility listing lost legacy filename-only recovery markers, and old Desktop fixtures invoked listing without installing the startup-owned catalog.

Root cause: SessionMeta projection trusted only the persisted recovered flag, while tests still assumed ListSessions could synchronously scan JSONL when the catalog pointer was nil.

Fix: Conservatively infer legacy recovery names from catalog metadata and migrate the affected fixtures to an explicit in-memory catalog without restoring any synchronous scan fallback.

Verification: The three ownership/recovery regressions, go test ./internal/cli, bundle budgets, and repolint passed.
2026-08-10 19:55:49 +08:00
esengine 5543b996b6 fix(agent): keep the prompt-token calibration across a session swap
The calibration is the ratio between wire characters and the tokens the
provider billed - a property of the model's tokenizer, not of the transcript.
SetSession dropped it anyway, and desktop rebinds sessions constantly: tab
switches, forks, rewinds, and the snapshot-conflict path that adopts a newer
disk transcript (a session held by two windows can do this hundreds of times
a day, #8190). Every rebind therefore put the next turn back on the cold
estimate, which is the least accurate reading available and the one most
likely to cross a maintenance threshold that the calibrated view was nowhere
near.

A model switch rebuilds the controller and with it the agent, so nothing
carries a ratio across tokenizers. Per-transcript observations - the latest
usage and the in-flight request shape - still reset, and the CJK safety floor
still covers a session whose script composition shifts after calibration.

The existing test asserted the drop; it now asserts what actually has to go.
2026-08-10 04:55:10 -07:00
esengine 9a6bd8017a Merge main-v2: keep its bundle budget, re-record the baseline on top
main-v2 has since raised the raw bundle budget to 2_218 for transcript
scroll arbitration and measurement invalidation. This branch takes that file
verbatim: it opened to unbreak a gate, and reverting a newer owner's number
to the 2_205 recorded here would break the desktop build instead.

The baseline is regenerated against the merged tree rather than carried
forward, since it is generated data and a stale recording would misstate
budgets for files that have legitimately moved since. Against current
main-v2 it loosens nothing and tightens 33 entries (-1430).
2026-08-10 04:53:12 -07:00
YHH a466b96ca1 Merge pull request #8205 from esengine/fix/context-token-unit
fix(agent): compare the context window against tokens, not characters
2026-08-10 19:52:30 +08:00
YHH d47f325f2b Merge pull request #8199 from esengine/fix/compaction-threshold-collapse
fix(agent): stop an oversized output budget from collapsing every compaction threshold
2026-08-10 19:52:18 +08:00
esengine 4cfdb6c602 Merge remote-tracking branch 'origin/main-v2' into chore/repolint-baseline-app-tsx 2026-08-10 04:51:58 -07:00
SivanCola 0cfda51ca4 chore(ci): remove cross-boundary review gate (#8211)
Problem: The independent cross-boundary review status created recurring red PR statuses and forced every qualifying head through another manual approval cycle.\n\nRoot cause: Protected review workflows published a failing commit status until a non-author maintainer approved the exact head.\n\nFix: Remove the evaluator, workflows, tests, PR template guidance, and CI invocation while retaining a negative contract against stale references.\n\nVerification: Full pull-request CI passed on Linux, macOS, and Windows, including lint, race, CodeQL, desktop builds, and WebView2 smoke.
2026-08-10 19:50:25 +08:00
SivanCola 9796edb3a7 chore(frontend): ratchet indexed history budget
Problem: The History bridge adds one KiB of always-available typed contracts and exceeded the prerequisite branch's exact raw bundle ceiling by 0.9 KiB.

Fix: Increase only the raw initial JavaScript/CSS allowance from 2,209 to 2,210 KiB while preserving all gzip and largest-chunk gates.

Verification: The production bundle measured 2,209.9 KiB raw and all bundle checks passed.
2026-08-10 19:48:56 +08:00
SivanCola 3229896c61 fix(cli): register catalog completions
Problem: Adding the catalogs dispatcher without a matching shell-completion root caused the dispatch coverage test to fail and hid reindex flags from users.

Root cause: Catalog subcommands were registered only for execution, while the completion registry remained a separate static list.

Fix: Carry completion flags in each catalog registration and build the catalogs reindex completion tree from the same registry without increasing lint debt.

Verification: go test ./internal/cli and repolint passed.
2026-08-10 19:48:48 +08:00
SivanCola 98e30ee1a7 docs(history): document catalog recovery
Problem: Users and operators need a discoverable cache location, diagnostic command, and safe rebuild path for indexed history.

Fix: Link the bilingual history catalog guide from CLI and storage references, document its cache path, and ratchet repolint to the History-only branch shape.

Verification: repolint baseline generation and git diff --check passed.
2026-08-10 19:45:27 +08:00
SivanCola 0139b48287 Merge pull request #8206 from esengine/fix/desktop-transcript-selection-stability
Stabilize transcript scrolling and native selection / 稳定对话滚动与原生跨消息选区
2026-08-10 19:45:04 +08:00
SivanCola 517fc050ef feat(history): add non-blocking indexed search
Problem: Agent history search and the Desktop history manager scaled with the total transcript set and could synchronously decode session files.

Root cause: Search, metadata listing, and context retrieval shared filesystem-scanning code instead of a disposable query projection.

Fix: Add token-only FTS5 indexing with resumable reconciliation, post-commit persistence hints, indexed Agent search, keyset Desktop pagination, runtime overlays, progress reporting, diagnostics, and bilingual storage documentation.

Verification: Covered by tokenizer/search compatibility, append/rewrite reconciliation, stale cursor, corrupt source, Wails array, and warm-query benchmark tests; provider-visible history tool schema bytes remain unchanged.
2026-08-10 19:44:51 +08:00
SivanCola 10fd6fbbb2 refactor(storage): share disposable projection lifecycle
Problem: Each disposable catalog would otherwise duplicate SQLite migration, integrity, quarantine, permission, and in-memory fallback behavior.

Root cause: The original session catalog owned database lifecycle policy together with session-specific schema and reconciliation logic.

Fix: Extract the reusable lifecycle into internal/projectiondb, migrate the session catalog to it, and add schema-v3 keyset session pagination for follow-up consumers.

Verification: Covered by projectiondb lifecycle tests and session catalog migration, pagination, cursor, and compatibility tests.
2026-08-10 19:44:51 +08:00
SivanCola 623d536e00 merge: refresh session catalog on latest main-v2
Bring the session catalog prerequisite onto the current upstream base before publishing the History, Usage, and Task follow-up branches.

# Conflicts:
#	desktop/frontend/scripts/check-bundle-budget.mjs
2026-08-10 19:43:48 +08:00
esengine 49f3927dd3 fix(cli): a failed run must say why, including under -p
`run` exited 1 with an empty stderr and a half-written answer whenever the
turn failed under text output. -p/--print selects text and builds a result
sink, and the text branch of runOutputSink.Finalize prints the final response
and nothing else, so the error had no path out: the stderr fallback was gated
on there being no sink at all. Structured formats were fine — they encode the
failure in their own payload.

Compaction failures are where this hurts. A run that cannot free enough
context stops mid-task, and the user sees a truncated answer and no reason,
which is what makes "it just stops" reports unactionable.

Text output now always reports the reason, with the error prefix when the run
actually failed and without it for a reportable pause. Structured formats keep
carrying it themselves, and still fall back to stderr when no sink was built.
2026-08-10 04:38:55 -07:00
SivanCola 5ecb9e5e37 Merge pull request #7655 from mchenziyi/codex/task-monitor-timer-fix
fix(desktop): freeze terminal task monitor timers
2026-08-10 19:29:13 +08:00
esengine 6c68c9610d refactor(agent): one compaction trigger, and stop rewriting history below it
Tool-result maintenance had its own trigger at tool_result_snip_ratio (0.6),
below the fold. Every pass rewrites messages in the middle of the prompt, and
that invalidates the provider's cache from the rewrite point on. DeepSeek bills
a miss at 50x a hit, so a pass that drops 20% of a 100K prompt turns 95K cached
tokens into 75K uncached ones: the turn gets cheaper in tokens and about ten
times more expensive in money. #8109 made a pass idempotent per view, which
stops the same results being rewritten twice but not a new stale result
arriving every turn - and in a coding session one usually does. The steady
state was a cache reset per turn, which is what #8118 and #8132 report and the
opposite of what the cache-first design is for.

Maintenance now runs only at compact_ratio, where the prefix is being reset
anyway. It still runs first, because eliding stale results is cheap and often
enough on its own: tool_result_snip_ratio keeps its name and its default but
now says how far down that pass has to land to stand in for the summary. One
prefix switch per fold either way. soft_compact_ratio no longer gates anything,
it just emits its notice.

Below the trigger the transcript is now byte-identical to what the provider
cached, however large it grows.
2026-08-10 04:27:52 -07:00
SivanCola 4ddff1f017 fix(desktop): cancel stale transcript smooth scrolls
Problem: A jump animation started in one transcript could finish after a tab reset and change the new transcript from tail-follow back to manual mode.

Root cause: resetGeneration cancelled frame-batched bottom writes but left the native smooth-scroll completion timer alive and unguarded by generation.

Fix: Cancel the timer, stop the in-flight native smooth scroll at its current offset, and reject any already-queued completion callback whose generation is stale.

Verification: test:transcript, test:transcript-browser, test:typecheck, build, and repolint pass, including a regression test that waits beyond the old completion timer.
2026-08-10 19:21:56 +08:00
SivanCola 836b05ec58 Merge main-v2 into codex/task-monitor-timer-fix
Problem:
- main-v2 advanced to 4e36c130 after the CI normalization push.
- The new base independently adopted the same bundle and repolint budgets, leaving a comment-only merge conflict.

Resolution:
- Keep the latest base budget rationale and retain the Task Monitor timer fix.
- The resulting PR diff against main-v2 is limited to TaskMonitorPanel and its tests.

Verification:
- pnpm build
- pnpm typecheck
- pnpm test:task-monitor
- go run ./tools/repolint
- git diff --check
2026-08-10 19:19:12 +08:00
SivanCola fdd126a69c merge: update transcript stability branch from main-v2
Problem: The draft pull request became conflicting after main-v2 merged Goal run-boundary changes while transcript stabilization work was in progress.

Root cause: Both branches updated the tightly ratcheted initial raw bundle allowance and its explanatory comment.

Fix: Merge origin/main-v2 without rewriting history and combine the Goal observability and transcript selection allowances at 2218 KiB, above the measured 2215.3 KiB while preserving all gzip budgets.

Verification: test:transcript, test:transcript-browser, test:typecheck, lint:hooks, build, the CI contract, and repolint pass after the merge. test:bench has one pre-existing retained-heap failure at 46.1 MiB, matching origin/main-v2 at 45.9 MiB with the 20 MiB gate unchanged.
2026-08-10 19:18:03 +08:00
SivanCola a594b1056c fix(desktop): fence transcript selection lifecycle
Problem: Active transcript selections could be preempted by forced bottom scrolling or stale settle frames, copied selections retained virtual rows, async row growth left the height cache stale, and Linux CI installed Chromium into a path the browser gate did not use.

Root cause: Selection reset and copy paths did not own their delayed work, force scrolling changed mode before arbitration, height caching wrapped only the initial ref measurement, and the Playwright path was resolved from the pnpm child working directory.

Fix: Fence delayed selection work by generation, release copied selections safely, arbitrate forced writes before mode changes, cache official ResizeObserver measurements with deferred layout invalidation, stabilize the prepend anchor assertion, and align plus contract-test the Chromium install path.

Verification: test:transcript, test:transcript-browser, test:typecheck, lint:hooks, build, and the selection menu suite pass. test:bench retains 45.8 MiB versus 45.9 MiB on origin/main-v2; the unchanged 20 MiB baseline gate remains red. repolint reports only the pre-existing App.tsx baseline drift tracked separately.
2026-08-10 19:15:02 +08:00
esengine 447c812e40 fix(agent): price unrepresented CJK at the cold rate, not two tokens a rune
The calibrated path carries the same unit bug the cold path had. When a
session's CJK share rises above what calibration saw, the excess runes fall
back to a hard 2 tokens each - a number chosen to match the old cold estimate
of runes + one per han rune. Against DeepSeek's real ~0.6 that is 3.3x, so an
English-calibrated session that starts discussing Chinese reads 120K tokens
for 60K runes worth ~36K, and lands back in exactly the early compaction this
branch removes from the cold path.

Price the excess as bytes at the same ~4 chars per token: 0.75 tokens per han
rune, still conservative, and identical to what the cold path now returns for
the same text (measured: 45002 either way).

The CJK floor test grew its input for the same reason its sibling did - at the
corrected unit, 430K runes leave most of a 1M window free, so nothing would
have been clipped and the assertion would have proven nothing.
2026-08-10 04:12:58 -07:00
esengine cba4f9b765 fix(agent): compare the context window against tokens, not characters
estimateTextTokens returns max(runes, bytes/4), and runes > bytes/4 holds for
every string, so it always returns the rune count: one "token" per character.
That unit is fine for the internal planning budgets it was written for, but
estimatedPromptTokens carried it into the comparison against contextWindow
whenever no usage had calibrated the session yet - and then added a second
token per CJK rune on top. Chinese landed near 2 tokens per han rune against
a real 0.6, English at 1 per character against a real 0.25: a context 25%
full measured as full, and the turn force-compacted.

Calibration hid this for warm sessions and left it in place for cold ones -
resume, tab switch, fork, rewind, or a relay that reports no usage - so the
same session compacted at 25% or at 80% depending on when it was asked. Both
readings came from the same threshold.

Estimate the cold case from the wire character count at the documented ~4
chars per token instead, which leaves CJK at a conservative 0.75 per rune,
and route /context and the maintenance snapshot through the same estimator so
what the user reads is what the decision used.

The guarded-fold test grew its input to 200K han runes: at the corrected unit
its old 50K no longer exceeds the summarizer budget, so it would have proven
nothing about the bound it exists to check.
2026-08-10 04:12:58 -07:00
esengine 433e858d7a fix(agent): stop an oversized output budget from collapsing every threshold
An output budget can exceed the whole configured context window: DeepSeek's
Anthropic-compatible endpoint defaults to 128K output, and the custom-provider
wizard wrote context_window = 128000 unconditionally until #8124, so that pair
is ordinary in existing configs. hardInputCeiling subtracted the budget whole,
the ceiling fell to its max(1, ...) floor, and soft, snip, fold and force all
landed on 1 token. Every turn then crossed every trigger and force-compacted,
with the "context is getting large" notice unreachable because soft and snip
were equal - the user saw only the compaction, never a warning, and the
configured compact_ratio never applied. Windows between 131K and 656K were
scaled down the same way, just less visibly: 200K compacted at 34%.

Reserve at most a quarter of the window for the reply. effectiveOutputBudget
already clips the real reply against the measured prompt at send time, so
reserving more than that here protects nothing it does not already cover.
Windows that genuinely fit their budget keep reserving it whole and are
unchanged.
2026-08-10 04:12:48 -07:00
esengine 2086937d4d Merge main-v2: take its bundle budget, re-record the baseline on top
main-v2 raised the raw bundle budget to 2_205 KiB independently while this
branch was in CI, with its own justification. Taking that version and
dropping the 2_206 written here: a concurrent owner's deliberate number
should not be overwritten by a repair that only needed the gate to pass.

The baseline is regenerated against the merged tree rather than
hand-resolved, since it is generated data. Against current main-v2 it now
loosens nothing at all and tightens 33 entries (-1459); the App.tsx entry
this branch was opened for no longer needs absorbing.
2026-08-10 04:09:30 -07:00
SivanCola 4e36c130a1 Merge pull request #8185 from SivanCola/fix/goal-evidence-progress
fix(goal): bound Runs instead of no-progress pauses / 用有界 Run 替代 Goal 无进展误暂停
2026-08-10 19:05:53 +08:00
SivanCola c559b3a5d1 chore(ci): normalize current desktop budgets
Problem:
The PR merge result failed the required lint job and both desktop build jobs
even though the focused Task Monitor checks passed.

Root cause:
The current main-v2 baseline measures App.tsx at 4,733 findings over the file
ceiling against a 4,725 ratchet, and initial raw frontend assets at 2,204.8 KiB
against a 2,203 KiB budget. The PR does not modify App.tsx or the eager asset
path responsible for those already-reviewed base increases.

Fix:
Merge the current main-v2 tip and minimally normalize only the owning guards:
set the App.tsx ratchet to 4,733 and the raw asset budget to 2,205 KiB.

Verification:
- go run ./tools/repolint
- pnpm build
- pnpm typecheck
- pnpm test:task-monitor (19 panel tests and 3 navigation tests)
- git diff --check
2026-08-10 18:52:08 +08:00
SivanCola 8b3d0b6595 fix(desktop): stabilize transcript selection scrolling
Problem:
- Cross-message selection in the virtualized desktop transcript could lose DOM endpoints or jump while wheel, stream, resize, and virtualizer writers competed for scroll ownership.

Root cause:
- Programmatic scroll writes were distributed across Transcript and useScrollManager, while the virtualizer could unmount rows referenced by the browser Selection.

Fix:
- Centralize scroll-write arbitration and generation reset.
- Retain the continuous native-selection row interval and namespace virtual row identity per tab.
- Add bounded layout-aware height caching plus jsdom and real Chromium coverage.

Verification:
- pnpm --dir desktop/frontend test:transcript
- pnpm --dir desktop/frontend test:transcript-browser
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend build
2026-08-10 18:50:48 +08:00
esengine ae1ae12a66 chore(desktop): raise the raw bundle budget past the same merge collision
main-v2's desktop job fails at "initial raw JavaScript and CSS is 2204.8
KiB; budget is 2203.0 KiB" — 1.8 KiB, 0.08% over. Same cause as the repolint
entry in the previous commit: #8103 passed its own CI (desktop=SUCCESS,
lint=SUCCESS) and only crossed the line once merged alongside other work.

The failing budget is the uncompressed total, the least user-visible of the
five. All four gzip budgets — what actually crosses the network — still pass
with margin: initial JS 383.6/400.0, largest chunk 215.7/280.0,
render-blocking CSS 3.1/4.0, deferred app-shell CSS 110.4/114.0.

Raised to 2_206 KiB: enough to clear the collision, tight enough that real
growth still trips it. Reverting is one line if the frontend owner would
rather shrink the bundle instead.
2026-08-10 03:45:17 -07:00
esengine a8180822bf chore(repolint): re-record the baseline, tightening 20 entries and absorbing one
main-v2 fails `make lint`: desktop/frontend/src/App.tsx sits 8 lines over its
recorded budget. It arrived with #8103 and every branch cut from main-v2
inherits the failure, so the ratchet is currently blocking work it did not
catch in the first place.

Re-recording is a deliberate decision made in its own commit rather than
inside a feature PR, because widening a budget to land a change is exactly
what the ratchet exists to prevent. Here the net moves the other way:

  loosened   1 entry   App.tsx file-size 4725 -> 4733  (+8)
  tightened  20 entries                            (-1147)

  essay           4090 -> 4087
  file-size     111363 -> 110541
  function-size   9254 -> 9223
  test-file-size 70202 -> 69919

The tightenings lock in cleanups that had not been re-recorded — among them
coordinator.go 267 -> 13, agent.go 2807 -> 2746 and coordinator_test.go
1239 -> 1025 — so the debt they removed can no longer be quietly spent again.

The App.tsx regression itself is not fixed here; it is a 5533-line component
and splitting it is frontend work with its own review. This commit only
stops it from failing every unrelated branch.
2026-08-10 03:35:46 -07:00
SivanCola 23c5faff20 Merge latest main-v2 into Goal run boundary branch
Problem:
- main-v2 advanced after the prior validation, merging session inbox, reasoning display, and live-contract work.
- run_loop had a semantic conflict, while the combined frontend and repolint baselines exceeded their ratchets.

Root cause:
- live contract initializes perTurnState.turnInput at the same run boundary where Goal execution resets structural guards.
- parallel main merges also left the unchanged App.tsx repolint baseline eight lines behind, and Goal request observability adds a small always-loaded frontend contract.

Fix:
- preserve turnInput while resetting Goal structural guards at run start.
- keep batch-guard cancellation inside the helper so executeBatch remains within its size budget.
- ratchet only the initial raw bundle allowance to 2205 KiB and synchronize the App.tsx baseline to latest main.

Verification:
- go test ./...
- go test -race ./internal/agent ./internal/control ./internal/evidence ./internal/taskcontract ./internal/sessioninbox
- go vet ./...
- go run ./tools/repolint
- cd desktop && go test ./...
- cd desktop/frontend && pnpm build
- git diff --cached --check
2026-08-10 18:32:32 +08:00
SivanCola eb585eaa26 fix(desktop): keep task timer typing within budget
Problem:
- The timer fix added one line to the already-ratcheted shared frontend type file, so the merge-result repolint job failed its file-size budget.

Root cause:
- The optional lease timestamp was modeled globally even though only the Task Monitor timer consumes it.

Fix:
- Describe the existing optional payload field with a component-local intersection type and leave the shared type file unchanged.

Verification:
- pnpm test:task-monitor
- pnpm typecheck
- pnpm build
- git diff --check
2026-08-10 18:26:08 +08:00
YHH 7a1d2a78e6 Merge pull request #8193 from esengine/feat/live-contract
feat(agent): the contract is answerable while the turn runs
2026-08-10 18:06:42 +08:00
SivanCola 1f1e2e3aff Merge pull request #8061 from SivanCola/fix/session-inbox-durability
Persist Session Inbox for durable queued instructions / 持久化会话收件箱以恢复排队指令
2026-08-10 18:03:24 +08:00
SivanCola baac5fdf65 Merge main-v2 into Goal run boundary branch
Problem:
- The PR head lagged main-v2 and overlapped agent.go with the structured Plan contract wiring from #8183.

Root cause:
- main-v2 advanced after the Goal boundary implementation was published.

Fix:
- Merge the current main-v2 while preserving both the per-Run Goal boundary state and structured Plan contract installation.

Verification:
- go test ./...
- go test -race ./internal/agent ./internal/control ./internal/evidence ./internal/taskcontract
- go vet ./...
- go run ./tools/repolint
- cd desktop && go test ./...
- cd desktop/frontend && pnpm build
2026-08-10 18:02:42 +08:00
SivanCola 8ea06aff23 Merge pull request #8103 from SivanCola/feature/reasoning-display-modes
feat(desktop): add three-mode reasoning display / 桌面端新增思考过程三模式展示
2026-08-10 17:58:55 +08:00
esengine 7d3e924e1c feat(agent): the contract is answerable while the turn runs
The contract was built once, at the end of the turn, from a full replay of
the receipts — and then discarded. Anything that needs to ask "is anything
still unproven" mid-turn has nothing to ask: GateMutation must answer before
a mutation runs, FinalRejection at the moment a completion is claimed, and
both currently have no contract in scope. That, not a missing call site, is
what blocks wiring them.

LiveContract answers now. It reruns the same pure replay against the
receipts recorded so far rather than keeping incremental state, so one code
path serves the per-round view and the end-of-turn record and the two cannot
disagree — a property the tests assert directly.

Each tool round records the contract alongside the outcome sample, so a
trajectory carries how the evidence graph filled in rather than only where
it landed. A turn with nothing to prove records nothing.

This still observes and decides nothing. Enforcement is the next change, and
it has an open design question that belongs in its own review: GateMutation
expects a mutation to name the unsatisfied requirement it serves, and no
tool carries such an argument today.

turnInput rides the existing single perTurnState zeroing assignment, so the
turn can never forget to reset it and run_loop.go stays inside its ratchet.
2026-08-10 02:48:58 -07:00
YHH 33c1eabd76 Merge pull request #8183 from esengine/feat/plan-criteria
feat(agent): the contract's requirements are the plan's acceptance criteria
2026-08-10 17:47:46 +08:00
SivanCola d8e858b8c1 fix(desktop): preserve task timer semantics
Problem:
- Reconciled stale tasks retained an old updated_at timestamp, so their frozen duration could be zero or otherwise misleading.
- Requeued tasks displayed the time since their original creation as if it were active runtime.

Root cause:
- The frontend treated updated_at as a universal end time for every non-live task state.

Fix:
- Use the expired runtime lease as the bounded end time for stale tasks when available.
- Render no elapsed runtime for queued tasks.
- Type the existing optional lease field and cover both states with regressions.

Verification:
- pnpm test:task-monitor
- pnpm typecheck
- pnpm build
- latest main-v2 overlay: Task Monitor 19/19 and navigation 3/3
- git diff --check

Co-authored-by: SivanCola <[email protected]>
2026-08-10 17:45:36 +08:00
SivanColaandContributor c8e6746d51 chore: credit restore preflight contribution
The restore-safety follow-up adapts the conflict-preflight mechanism from PR #8178 by @nanami7777777 while preserving PR #8061's discardable-transcript behavior.

Co-authored-by: Contributor <[email protected]>
2026-08-10 17:45:30 +08:00
SivanCola 9bbcbdc598 test(goal): use integer range in boundary fixture
Problem: golangci-lint rejected the Goal zero-evidence fixture's traditional counted loop under the intrange rule.

Fix: use the Go 1.22 integer range form without changing the generated mock turns.

Verification: focused agent test; golangci-lint run --timeout=5m; go run ./tools/repolint; git diff --check.
2026-08-10 17:42:48 +08:00
SivanCola f882c708b5 refactor: satisfy session catalog ownership budgets
Problem:
The initial session catalog implementation exceeded incremental repository size and complexity budgets in several existing owner files, and Desktop CI reported stale helpers plus two modernization findings.

Root cause:
Catalog lifecycle, runtime projection, frontend topic presentation, mock bindings, plugin compatibility, and CLI wiring were colocated with already-large modules. Removing the eager project tree also left legacy runtime-tree helpers unused.

Fix:
- split catalog lifecycle from runtime projection and metadata mapping
- extract frontend catalog contracts, mock bridge, topic presentation, and count formatting
- isolate plugin compatibility, session projection, and CLI completion wiring
- remove obsolete eager-tree helpers and address CI modernization findings
- preserve behavior without changing repolint baselines

Verification:
- go run ./tools/repolint
- golangci-lint v2.12.2: 0 issues
- go test affected root packages
- focused Desktop catalog and workspace regression tests
- pnpm test:all
- pnpm build
2026-08-10 17:41:41 +08:00
SivanCola b8dc6769d4 style(desktop): keep Goal runtime bridge gofmt-clean
Problem: the Desktop CI gofmt check rejected blank-line removals used to stay within the legacy app.go size ratchet.

Fix: restore gofmt layout and compact nearby bridge comments instead, preserving the file-size baseline and behavior.

Verification: gofmt -l desktop/app.go; go run ./tools/repolint; git diff --check.
2026-08-10 17:40:08 +08:00
SivanCola 70dc3ba58d Default desktop reasoning to live follow
Problem:
Fresh installs still selected summary mode, so the new settings did not match the standard, live-follow, auto-collapse experience.

Root cause:
Backend and frontend fallback paths, browser mocks, and test fixtures retained the previous summary and compact defaults.

Fix:
Resolve missing or unknown reasoning modes to auto while preserving explicit selections and legacy localStorage migration. Align startup and browser defaults, and add config/Wails persistence coverage.

Verification:
- go test ./internal/config
- cd desktop && go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm test
- cd desktop/frontend && pnpm build
- go run ./tools/repolint
- git diff --check
2026-08-10 17:38:09 +08:00
SivanCola 64fc5ecf59 refactor(goal): split boundary helpers for lint ratchet
Problem: the Goal boundary implementation grew several grandfathered files beyond the repository's repolint ratchet.

Root cause: run-boundary state, sidecar compatibility helpers, evidence novelty logic, and their tests were added directly to legacy owner files.

Fix: extract the new logic into focused same-package files, reduce the affected owner functions, and keep the existing repolint baseline unchanged.

Verification: go run ./tools/repolint; go test ./...; go test -race ./internal/agent ./internal/control; go vet ./...; desktop go test ./...; frontend pnpm build.
2026-08-10 17:38:05 +08:00
SivanCola 2a06279f99 fix(desktop): preserve active inbox on trash restore
Problem: restoring a trashed session over a discardable transcript could silently delete a newly queued durable inbox or another active sidecar.\n\nRoot cause: restore validated only transcript content, then removed every owned artifact before checking the remaining restore conflicts.\n\nFix: preflight all owned session and subagent destinations, allow only the verified discardable transcript collision, and abort without mutation when any sidecar conflicts. Keep the restore transaction in a focused file so sessions.go stays within its repolint ratchet.\n\nVerification:\n- cd desktop && go test ./...\n- cd desktop && go test -race . -run 'TestRestoreTrashedSessionFile' -count=1\n- cd desktop && go vet ./...\n- cd desktop && go build ./...\n- go test ./...\n- go vet ./...\n- go run ./tools/repolint\n\nAdapted-from: https://github.com/esengine/DeepSeek-Reasonix/pull/8178 by @nanami7777777\nCo-authored-by: Contributor <[email protected]>
2026-08-10 17:31:34 +08:00
SivanCola bae84dec74 merge: refresh session inbox against main-v2
Problem: PR #8061 was behind the current main-v2 integration baseline.\n\nRoot cause: main-v2 advanced after the last session inbox review round.\n\nFix: merge the latest main-v2 before applying the restore-safety follow-up, preserving both histories without a force push.\n\nVerification: the merge completed automatically; focused and repository gates run with the follow-up fix.
2026-08-10 17:23:02 +08:00
SivanCola 30fa8f2bcc merge: refresh session catalog on latest main-v2
Reason:
The official base advanced by six plan-contract commits after the session catalog implementation began.

Resolution:
Merge the current official main-v2 without rewriting the reviewed feature commit.

Verification:
- go test ./... passed at the repository root after the merge
- go test ./... passed in desktop after the merge
- the feature commit retains focused race, frontend, vet, and cross-build evidence
2026-08-10 17:19:39 +08:00
SivanCola bf19574d3a feat(desktop): add resilient session catalog
Problem:
Desktop startup and project-tree queries could synchronously migrate and decode legacy session JSONL, blocking navigation, creation, and shutdown.

Root cause:
Session discovery, metadata repair, controller admission, and plugin startup shared UI-critical paths and broad lifecycle locks.

Fix:
- add a disposable SQLite session catalog with incremental reconciliation, repair checkpoints, corruption recovery, and memory fallback
- expose revisioned cursor pagination and migrate the desktop project tree to lazy loading
- move controller construction outside lifecycle admission and isolate incompatible plugin startup
- add session doctor and reindex commands while preserving authoritative JSONL and metadata
- retain CGO-free builds with platform-specific fallbacks

Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/sessioncatalog ./internal/agent
- cd desktop && go test -race .
- go vet ./... in root and desktop
- cd desktop/frontend && pnpm test:all && pnpm build
- CGO_ENABLED=0 cross-builds for supported macOS, Windows, and Linux targets
2026-08-10 17:16:26 +08:00
SivanColaandWill bfdef5dfdc fix(goal): replace text-derived stalls with run boundaries
Problem:
Goal mode could pause useful research after a text-derived 4/6/10 no-progress streak, while the outer 10/20/40 continuation budget did not bound actual model rounds inside one Run.

Root cause:
Cross-turn novelty was acting as both progress telemetry and a termination proxy. The agent exposed no typed host-owned Run boundary, so the controller could not distinguish default Goal budgets, explicit max_steps, or structural stuck conditions.

Fix:
Add a default 16-round Goal Run budget with one tool-free finalization response, typed resumable pause metadata, inherited child budgets, and Goal-only structural stuck detection. Keep cross-turn novelty observational, migrate legacy no_progress pauses, preserve unknown sidecar fields, and expose requestsUsed across CLI, ACP, and Desktop surfaces.

Verification:
- go test ./...
- go test -race ./internal/agent ./internal/control
- go vet ./...
- cd desktop && go test ./...
- cd desktop/frontend && pnpm build
- live DeepSeek, LongCat, and GLM normal/stuck/16-round-resume matrix

Co-authored-by: Will <[email protected]>
2026-08-10 17:13:11 +08:00
esengine d41295831b feat(agent): the contract's requirements are the plan's criteria
taskcontract.FromPlan and PlanFacts were written to build a contract from a
plan's own acceptance criteria — Planner to Contract to Executor — and have
had no caller since. The shadow contract instead inferred requirements from
todo titles, which restate the steps rather than what the work agreed to:
"thread the model ref through" is a step, "two model refs never share a
cache entry" is the thing that must be true when it is done.

An approved structured plan now becomes the contract. The coordinator
installs it on the executor just before the run and clears it at the top of
every turn, so a turn never inherits the last one's plan. Without a plan the
todo list stands in exactly as before.

PlanFacts gains Optional so a nice-to-have criterion is recorded without
holding completion open; Contract.Requirements already carried Required and
Complete() already skipped it.

This changes what the contract observes, not what it decides: GateMutation
and FinalRejection still have no callers. Wiring those is the behaviour
change and belongs in its own review.

Adding a field to Agent pushed agent.go past its ratchet, so the canonical
todo-state accessors moved to todo_state.go rather than widening the
baseline: 3567 -> 3501 lines, one responsibility out of the god file.
2026-08-10 02:06:46 -07:00
YHH 07fcf585b9 Merge pull request #8167 from esengine/feat/plan-contract
feat(plan): the plan becomes a record — structured contract, stable step identity, submit_plan
2026-08-10 17:05:08 +08:00
SivanCola c75a977215 Fix reasoning display migration and settings regressions
Problem:
The reasoning display PR could change invalid-config behavior across restarts, fail to auto-open remounted subagent reasoning, rely on stale settings tests, load settings-only CSS eagerly, and break its navigation at narrow widths.

Root cause:
Fallback explicitness and legacy alias persistence were conflated, disclosure state assumed it observed the phase transition, settings ownership moved without focused contracts, and responsive navigation changed flow without changing the parent grid.

Fix:
Keep unknown modes on a stable summary fallback, initialize auto disclosure from the current phase, extract focused reasoning/settings components and tests, lazy-load settings CSS, preserve repository ratchets, and switch narrow settings to a top navigation row.

Verification:
go test -race ./internal/config
cd desktop && go test ./...
cd desktop/frontend && pnpm test:typecheck
cd desktop/frontend && pnpm test
cd desktop/frontend && pnpm build
go run ./tools/repolint
git diff --check
Responsive browser checks at 720x800 and 480x800
2026-08-10 16:00:08 +08:00
SivanCola 535c4c8aab Merge main-v2 into reasoning display modes
Problem: PR #8103 was behind main-v2 and conflicted in bundle and repolint budgets.

Root cause: both branches evolved the same budget contracts independently.

Fix: merge the latest base without force-pushing and keep the main-v2 budget and baseline values so feature work must satisfy the existing ratchets.

Verification: conflict markers are gone and git diff --cached --check passed before commit.
2026-08-10 15:34:32 +08:00
esengine 86c1218692 feat(agent): the planner asks with the ask tool, not with a text block
ask was excluded from the planner registry and then reinvented as a
<planner-ask> text block the host parsed back, backed by 28 decision
phrases that scraped 方案x/A) lines out of prose for options. That is a
complete round trip through text for a tool that already existed.

The planner keeps ask, and the coordinator propagates the host's asker to
both models. The answer now arrives while planning, so it shapes the plan
the executor receives instead of being stapled onto a finished one — which
is also why PlannerUserDecisionAsker and its controller adapter go: a
parallel path to the same approval surface, built only to carry prose.

Retired with it: the phrase heuristics, their tests, and the guards those
tests needed against ordinary plan wording conjuring an ask dialog. That
whole class of false positive is gone because nothing infers a question
any more.

Net -490 lines. planner_text_fallback.go is down to 84 lines and now holds
only what an unstructured plan still needs: the approval phrases and the
[no_changes] marker.

Cache: none. ask was already in the executor's registry, so only the
planner's own prefix changes; the boot golden baseline passes unchanged.
2026-08-10 00:29:23 -07:00
esengine c940fd6ab8 test(agent): prove the submitted plan is what the executor receives
Component correctness is not system effectiveness: submit_plan had
tool-level tests and the coordinator suite proved no regression, but
nothing proved the whole chain — planner calls submit_plan, the host
renders it, the executor is handed the plan rather than the prose the
planner happened to end on.

Five tests at that boundary: the rendered plan reaches the handoff and the
planner's trailing prose does not, the user sees the plan through the sink,
requires_approval gates a plan whose prose never mentions approval (the
case the phrase fallback cannot catch), a plan without it runs straight
through, and a planner that ignores submit_plan still reaches the executor
on the text path.
2026-08-10 00:20:09 -07:00
esengine fc782fbed6 feat(agent): the planner submits a plan instead of writing one
The machine protocol between planner and host was text: [no_changes] and
[planner_requires_approval] markers, a <planner-ask> block, and behind them
24 approval phrases with a 30-byte negation window plus 28 decision phrases
that scraped options out of prose. Those exist only because free text has
no fields to read.

submit_plan gives the planner a structured exit. Validation runs in the
tool, so a malformed plan comes back as an actionable error the planner
fixes in the same turn rather than a silent misparse downstream. plan() now
returns a plannerOutcome, and approval reads requires_approval from the
plan when one was submitted; prose keeps the phrase fallback because prose
has nothing else. [no_changes] stays: it marks the ABSENCE of a plan, which
by construction cannot be a field of one.

Every text heuristic moved to planner_text_fallback.go so the fallback
retires as one file once both plan producers submit structured plans, and
so coordinator.go reads as structured-first.

Cache: none. submit_plan is added only to PlannerToolRegistry, so the
executor's tool prefix is byte-identical and the boot golden baseline
passes unchanged; only the planner's own prefix shifts once.

coordinator.go 1114 -> 839 lines and Run's routing splits into deliverPlan,
both under their ratchet without widening the baseline.
2026-08-10 00:16:11 -07:00
esengine 499c9de471 feat(plancontract): a plan is a record, not markdown to parse back
The planner already knows what a plan contains — the prompt asks for
objective, touchpoints, risks, acceptance criteria and verification — but
emits all of it as prose, so the host guesses the structure back out.
taskcontract.PlanFacts and FromPlan have been waiting for that structure
since they were written and have never had a caller.

Plan makes the fields real. Two rules keep it honest: identity is
host-assigned (ID and Revision carry json:"-", so a planner cannot claim
one), and evidence is not blurred (VerifiedFiles are paths that were read,
CandidateFiles are paths that were inferred — a distinction free text
cannot enforce).

Ordered is the single ordering Render and ProjectTodos share, so the list
a user approves and the task list the host seeds cannot disagree. Render's
only markdown list items are the steps, which also closes a live defect on
the free-text path, where any bullet — an assumption, a non-goal — becomes
a todo. DependsOn orders siblings and is deliberately advisory: the todo
projection is serial, so a dependency becomes order, never gating.

A plan past MaxSteps is rejected rather than silently truncated, which the
existing markdown seeder does at 20 items.
2026-08-10 00:15:57 -07:00
esengine 338f7c9fd2 feat(evidence): a task-list item keeps its identity across a replan
complete_step resolved a citation by 1-based index, then by normalized
title text, then by >=6-rune substring containment. A replan invalidates
all three: inserting a step shifts every index below it, and retitling
"Fix authentication" to "Fix authentication refresh flow" moves the text.
Completion attribution then reattaches to the wrong step, or to none.

TodoItem gains StepID, and matchTodoStep tries it first, so every caller
already routing through that function — receipt recording, completion
verification, todo continuity — resolves by identity without new plumbing.
Two guards keep the id load-bearing rather than advisory: a duplicate id
is rejected, and a rewrite that keeps an item but drops the id it arrived
with is rejected with the id to restore. Lists that never carried ids keep
matching by text.

Ids are minted where a list is born: ProjectTodos carries the plan step's
own id, and the markdown seeding path mints plan_step_NN so the guarantee
holds before a structured plan exists.

Cache: tool schema grows 229 tokens (14074 -> 14303) for todo_write and
complete_step step_id; SystemHash is unchanged and the prefix is
byte-stable after the one-time change. Golden baseline regenerated.

Todo identity moved to its own file rather than growing evidence.go past
its ratchet: 2883 -> 2879 lines, no baseline widened.
2026-08-10 00:15:43 -07:00
SivanCola a9ef07428b refactor(desktop): refine settings center layout
Problem:
- The redesigned settings center remained dense in navigation and inconsistent at the minimum desktop width.

Root cause:
- Modal and sidebar sizing still used the older caps, every navigation item rendered metadata, and shared modal theme rules overrode the narrow fullscreen shell.

Fix:
- Refine modal, sidebar, section, navigation, and desktop-style ordering.
- Reset content scroll on page changes and preserve semantic active navigation state.
- Enforce a true fullscreen settings shell at the 760px desktop minimum.
- Add focused rendering and responsive contracts.

Verification:
- pnpm exec tsx src/__tests__/settings-refresh-snapshot.test.tsx (96 passed)
- pnpm test:settings-responsive (15 passed)
- pnpm test:typecheck
- pnpm check:css
- pnpm build
- In-app browser checks at wide and minimum desktop widths
2026-08-10 15:13:59 +08:00
SivanCola 28e6961b9a perf(desktop): keep recovery preview off startup path
Problem: A static recovery-preview import pushed the initial raw frontend bundle over its fixed 2200 KiB budget, and direct import.meta.env access failed under the Node test runner.

Root cause: Development-only fixtures were reachable from the production bridge entry and assumed Vite always supplied import.meta.env.

Fix: Gate the scenario with a safe DEV check, lazy-load its fixture module, and keep mutable preview pause state inside that deferred module.

Verification: pnpm test; pnpm exec tsc --noEmit; pnpm lint:hooks; pnpm exec vite build --logLevel warn; node scripts/check-bundle-budget.mjs; go run ./tools/repolint
2026-08-10 15:11:41 +08:00
SivanCola 5b0839e786 merge: refresh recovered inbox preview against main-v2
Problem: main-v2 advanced after the recovered-inbox UI head was prepared.

Integration: Merge base b8a37d4ba while preserving the durable inbox work and adopting the latest CLI scroll-state fixes without conflict.

Verification: go test ./internal/cli; pnpm test; pnpm exec tsc --noEmit; go run ./tools/repolint
2026-08-10 15:11:00 +08:00
SivanCola dde28681b6 test(desktop): add recovered inbox preview
Problem: The new recovered-inbox banner could not be reviewed in the browser preview without temporary bridge changes.

Root cause: The browser mock always returned an empty, non-recovered inbox snapshot.

Fix: Add a deterministic ?mock=recovery scenario with 30 paused items, resumable pause state, and a focused pure snapshot contract test.

Verification: pnpm exec tsx src/__tests__/inbox-recovery-preview.test.ts; pnpm exec tsx src/__tests__/composer-inbox-recovery.test.tsx; pnpm exec tsc --noEmit; go run ./tools/repolint
2026-08-10 15:04:21 +08:00
SivanCola b8a37d4ba2 Merge pull request #8156 from SivanCola/fix/tui-scroll-renderer-boundary
Limit legacy TUI scroll clears to Warp / 仅在 Warp 保留 TUI 滚动清屏
2026-08-10 14:51:27 +08:00
SivanCola 6473debc8c refactor(cli): isolate inbox pause shortcut
Problem: combining the durable inbox input path with the latest main-v2 CLI changes exceeded the chat TUI complexity ratchet by one.

Root cause: pause/resume status branching lived inside the already oversized Update dispatcher.

Fix: move the queue-only pause toggle into a focused helper without changing shortcut behavior or widening the lint baseline.

Verification: go test ./internal/cli; go run ./tools/repolint
2026-08-10 14:30:58 +08:00
SivanCola 604377631e merge: refresh durable inbox against main-v2
Problem: main-v2 advanced again while PR #8061 was being repaired, leaving the local head behind the current integration baseline.

Root cause: concurrent upstream merges added agent failure-snippet, benchmark, and CLI viewport changes after the first synchronization.

Fix: merge origin/main-v2 at d8d75e462 while preserving the durable inbox and Desktop recovery commits.

Verification: go test ./internal/agent ./internal/cli ./internal/evidence ./cmd/e2ebench ./benchmarks/compaction
2026-08-10 14:30:47 +08:00
SivanCola baee3c3a01 fix(desktop): expose recovered inbox controls
Problem: recovered durable inbox items are intentionally paused after a crash, but Desktop only rendered the queue and offered no way to resume dispatch.

Root cause: Composer ignored the paused/recovered snapshot metadata even though the Wails SetInboxPaused API already existed.

Fix: render a localized recovery banner with review, continue, and keep-paused actions; keep resume available after confirming pause; guard stale tab responses; lazy-load the queue surface to preserve startup budgets.

Verification: pnpm build; composer-inbox-recovery, composer-session-draft, composer-goal-toggle; go test ./...; cd desktop && go test ./...; go run ./tools/repolint
2026-08-10 14:27:40 +08:00
YHH d8d75e462c fix(agent): keep policy reads the recorded failure, not the text prefix (#8155)
KeepErrors only matched tool results starting with "error:" or "blocked:",
the shape the host formats for an execution failure. A tool that ran fine
but reported a failure — a go test FAIL, a compiler error, any non-zero
exit — never matched, so its evidence folded into the digest and the
specific assertion was gone. Measured against a full-history control on
benchmarks/compaction, evidence buried mid-result survived 0 of 6 runs
while the control kept it every time.

provider.Message.ToolExecution already carries State/ExitCode/Verification
and is already stripped before provider requests, so isErrorMessage reads
it instead of guessing. Because the failure then stays identifiable without
its text prefix, a protected failure can also be shortened: snipFailureResult
keeps the failure-carrying lines with two lines of context and drops the
passing noise, holding the projection cost to +3.5% rather than the +100%
that keeping the whole result costs. A text-only failure has no such anchor
and is still carried whole.

The rewrite happens on three paths — the planning pass, the re-write that
follows archiving, and the fold's kept partition — now unified through
maintenanceReplacement/keptForProjection so one cannot silently overwrite
another.

benchmarks/compaction gains the probe that measures this, a -snip arm,
cross-generation planting for revisions that land across a fold, and
alignment with boot's default KeepPolicy. Buried evidence now survives
18 of 18 runs.
2026-08-10 14:26:36 +08:00
SivanCola 62d3eb9dc0 fix(cli): limit legacy scroll clears to Warp
Problem: the root-fix draft depended on an unreleased Bubble Tea API and a temporary personal fork, so it could not safely merge or ship.

Root cause: fully removing the application-level workaround on Warp requires a renderer capability that Bubble Tea does not yet expose in a stable release.

Fix: merge the latest main-v2 hotfix, restore official Bubble Tea v2.0.8, retain session-switch suppression, and enable the legacy per-offset ClearScreen only when TERM_PROGRAM identifies Warp on non-Windows platforms.

Verification: focused scroll policy tests, go test ./..., go vet ./..., make lint, and Windows amd64 CLI test cross-compile.
2026-08-10 14:19:47 +08:00
SivanCola 2ab45943a8 refactor(desktop): clarify Agent Skills navigation
Problem:\n- The Skills navigation repeated the generic label as Agent 技能 and did not explain what the section contains.\n\nRoot cause:\n- Settings reused the capabilities-panel tab label instead of its dedicated navigation subtitle.\n\nFix:\n- name the section Agent Skills in every locale\n- describe it as reusable instructions, tools, and workflows\n- route settings navigation through the dedicated subtitle key\n- lock the three-language wording with a focused contract test\n\nVerification:\n- pnpm exec tsx src/__tests__/startup-settings-contract.test.ts\n- pnpm test:typecheck\n- pnpm check:css\n- pnpm build\n- go run ./tools/repolint\n- git diff --check\n- in-app browser DOM and visual check
2026-08-10 14:11:43 +08:00
SivanCola 90ac120f40 merge: integrate latest main-v2 into durable inbox
Problem: PR #8061 had diverged from main-v2 and GitHub reported merge conflicts across event wiring, query coalescing, and repository lint baselines.

Root cause: both branches added independently valid fields and cache invalidation semantics in the same owner files.

Fix: preserve inbox item IDs alongside workspace events, combine settled query sharing with mutation invalidation, and regenerate the merged repolint baseline.

Verification: go test ./internal/event ./internal/eventwire; pnpm exec tsx src/__tests__/query-coalesce.test.ts; go run ./tools/repolint
2026-08-10 14:10:55 +08:00
SivanCola 96b5d32a76 Merge pull request #8142 from SivanCola/fix/tui-windows-scroll-flicker
Fix Windows transcript scroll flicker / 修复 Windows 会话滚动抖动
2026-08-10 14:09:37 +08:00
YHH 84c257e56b feat(bench): measure delegation independence and anchor resistance (#8154)
Reasonix isolates a child's session but not its conclusions. The context
capsule records, field by field, what a child inherited from its parent
and every field is false — yet the one channel that actually carries
information, the parent's free-text prompt, is not modelled at all. Two
host-recorded instruments close that gap.

Evidence origin splits what a delegation's own text pointed at into
directory scope, which is the unavoidable cost of handing work off, and
named files, which say where the answer is. The child is credited only
for paths no named file already covered, so a scope hint never erases
its work. The two are counted apart because one number reports the cheap
act as the expensive one — the combined form read 0-3% independence
where the split reads 94-100%.

The -anchor arm hands the agent a hypothesis before it has read
anything: the task's real cause, a plausible wrong one, or none. Tasks
without an authored seed are skipped rather than run as the control, so
a seeded denominator never absorbs a control run.

Measured over 210 runs across two corpora (deepseek-flash, 5 reps each),
anchor resistance is 1.00 on both — including one whose answers no
command can check — while a wrong hypothesis costs ~2x wall and ~1.35x
tokens to unwind. Children discovered 94-100% of their own evidence:
this parent hands scope, not answers.

Also guards two failures found while running it: the bench read metrics
tags the agent had stopped emitting, and a failed grader's output never
reached the record that had to explain the failure.
2026-08-10 14:08:43 +08:00
SivanCola 7ac1196117 test(cli): assert scroll gestures need no redraw command
Cover mouse wheel and page-key viewport movement explicitly so application-level ClearScreen or other redraw commands cannot return unnoticed.
2026-08-10 14:03:31 +08:00
SivanCola 8339335d87 feat(desktop): redesign status bar item controls
Problem:\n- The expanded status bar item editor was constrained to a narrow column, leaving most of the settings page empty and making 16 items slow to manage.\n\nRoot cause:\n- The editor inherited the generic stacked-field width cap and rendered visible and hidden items in one undifferentiated list.\n\nFix:\n- split status bar items into full-width shown and hidden zones\n- support checkbox and drag-to-hide flows while preserving keyboard reordering\n- add show-all and restore-default recovery actions\n- add responsive styling, localization, focused interaction coverage, and narrow bundle-budget ratchets\n\nVerification:\n- pnpm test:typecheck\n- pnpm test\n- pnpm build\n- go run ./tools/repolint\n- git diff --check\n- in-app browser interaction and visual QA
2026-08-10 14:01:11 +08:00
SivanCola 64f229baa7 fix(cli): move viewport scrolling into renderer policy
Problem: application-level ClearScreen after every viewport offset change produces a second full redraw on Windows and visible transcript flicker.

Root cause: terminal compatibility policy lived in chatTUI.Update because Bubble Tea did not expose its scroll optimization switch.

Fix: disable renderer scroll optimization for Windows and Warp, remove the per-scroll ClearScreen and session-switch suppression state, and retain semantic clears for /clear and wide input.

Verification: go test ./..., go vet ./..., make lint, Windows amd64 CLI test cross-compile, and Bubble Tea VT output tests.
2026-08-10 13:58:28 +08:00
SivanCola 1cc74c4944 test(cli): decouple Ctrl-C rendering from redraw commands
Problem: Windows CI expected idle Ctrl-C state changes to return a command, but the Windows scroll-flicker fix correctly removes the unrelated ClearScreen command that had satisfied those assertions.

Root cause: the tests conflated Bubble Tea model rendering with command execution; changed model state is rendered even when Update returns no command.

Fix: assert the armed Ctrl-C state directly and keep the real quit/copy command checks unchanged.

Verification: focused Ctrl-C and scroll tests; go test ./internal/cli/...; go vet ./...; make lint; Windows amd64 internal/cli test cross-compile.
2026-08-10 13:47:44 +08:00
SivanCola e0bd12b4c5 Merge pull request #8102 from SivanCola/fix/workspace-live-refresh
fix(desktop): live workspace refresh / 工作区实时刷新
2026-08-10 13:36:50 +08:00
SivanCola d3247e583b feat(desktop): redesign the settings center
Problem:
- The settings center presented a long undifferentiated rail and nested setting groups that were slow to scan.

Root cause:
- Navigation had no search, category grouping, or icons, and General mixed multiple preference domains in one card.

Fix:
- add searchable grouped settings navigation with consistent icons
- split General into flat appearance, conversation, and system sections
- add localized row guidance and responsive layouts
- extract navigation into a dedicated component and update contracts

Verification:
- pnpm test:typecheck
- pnpm test
- pnpm build
- go run ./tools/repolint
- git diff --check
- in-app browser visual and interaction QA
2026-08-10 13:28:05 +08:00
SivanCola 4bf266d897 Merge pull request #8151 from SivanCola/fix/remove-context-composition-panel
Remove internal context composition details / 移除内部上下文构成详情
2026-08-10 13:17:27 +08:00
SivanCola a4c45b666d fix(desktop): remove stale context composition strings
Problem: desktop CI exceeded the compressed bundle budget after the context composition panel was removed.

Root cause: UI-only localization entries for the removed panel remained in all three locale bundles.

Fix: remove the unused English, Simplified Chinese, and Traditional Chinese entries while preserving maintenance notice strings still used at runtime.

Verification: pnpm exec tsx src/__tests__/context-maintenance-notice.test.ts; pnpm build.
2026-08-10 13:09:57 +08:00
SivanCola 313cdc3ab0 fix(desktop): remove context composition details
Problem:
The context panel exposed internal maintenance thresholds and projection details that added noise to the primary usage view.

Root cause:
PR #8109 rendered maintenance telemetry directly between the context-window gauge and session metrics.

Fix:
Remove the context composition section and its presentation-only derived state while preserving backend maintenance telemetry and localized notices. Add a source contract that prevents the internal block from returning.

Verification:
- pnpm exec tsx src/__tests__/context-maintenance-notice.test.ts
- pnpm exec eslint src/components/ContextPanel.tsx src/__tests__/context-maintenance-notice.test.ts
- pnpm typecheck
- pnpm check:css
- Browser DOM: composition absent; context window, session metrics, and usage analysis present
2026-08-10 13:00:44 +08:00
SivanCola 0a5d36edec perf(desktop): lazy-load workspace focus reconciliation
Problem: merging the latest main-v2 provider bridge additions pushed the desktop initial raw bundle above its enforced 2200 KiB budget.\n\nRoot cause: the workspace focus and visibility reconciliation lifecycle was statically bundled even though the workspace refresh store was already loaded on demand.\n\nFix: move the lifecycle coordinator into the lazy workspace store, retain stale-load and cleanup fencing in App, and keep the project-tree subscription in the same refresh lifecycle.\n\nVerification: pnpm test:all, pnpm build, targeted Go race tests, repolint, and git diff --check.
2026-08-10 12:58:30 +08:00
SivanCola f75c90be8f refactor(desktop): unify conversation display settings
Problem: Transcript density, reasoning visibility, and completed-turn folding were split across separate visual structures, causing clipped controls in narrower settings windows.

Root cause: The work-process controls lived in a constrained nested card while transcript display remained a standalone row.

Fix: Group all three independent controls in one full-width responsive conversation-content section, update supported locales, and lock the layout contract with focused tests.

Verification: pnpm test; pnpm build; pnpm typecheck; pnpm test:typecheck; go run ./tools/repolint; browser checks at 1100px and 760px.
2026-08-10 12:57:28 +08:00
SivanCola 5ed9778c98 Merge latest main-v2 into fix/workspace-live-refresh 2026-08-10 12:47:55 +08:00
SivanCola 1cfab35a92 feat(provider): add OpenCode Go DeepSeek protocol presets / 新增 OpenCode Go DeepSeek 双协议预设 (#8105)
* feat(provider): add OpenCode Go DeepSeek protocol presets

Problem:
OpenCode Go exposed DeepSeek Flash through Chat Completions, but Reasonix had no dedicated Anthropic Messages or Responses presets with verified server-side web search support.

Root cause:
The curated catalog mixed unrelated Anthropic models, and Responses web-search replay was restricted to the official DeepSeek hostname instead of the enabled wire capability.

Fix:
Add model-scoped Anthropic and stateless Responses presets for DeepSeek Flash, enable verified web search, preserve completed Responses search calls for replay, and update Desktop capability rendering, localization, docs, mocks, and live tests.

Verification:
- go test ./...
- focused Desktop provider preset tests
- frontend typechecks and startup settings contract
- cache impact and cache guard scripts
- authenticated OpenCode Go Anthropic and Responses web-search live tests

* fix(provider): close OpenCode Go review gaps

Problem:
Repository lint rejected the protocol preset additions because they grew several baselined owner and test files. The live probes also stopped after the first web-search turn, leaving Responses replay and Anthropic tool-result compatibility unproven.

Root cause:
OpenCode Go preset data and focused assertions were added inline to already-ratcheted files, while the credential-gated tests did not exercise the provider-issued continuation payloads.

Fix:
Extract the OpenCode Go preset and localized description data into focused files without changing wire-visible configuration. Add a real Responses stateless follow-up and an Anthropic function tool/result round trip that preserves returned reasoning signatures.

Verification:
- go test ./... && go vet ./...
- go test -race ./internal/config ./internal/provider/responses ./internal/provider/anthropic
- go test -tags=live for both OpenCode Go protocol paths
- desktop go test ./... && go vet ./...
- frontend typecheck, settings contract, build, and bundle budgets
- go run ./tools/repolint
- ./scripts/cache-guard.sh
2026-08-10 12:32:39 +08:00
SivanCola fc9d20216f fix(agent): satisfy modern field iteration lint
Problem:
The Linux lint job rejected the workspace mutation command scanner under the Go 1.26 modernize rules.

Root cause:
The scanner ranged over an allocated strings.Fields slice even though the current toolchain provides the equivalent FieldsSeq iterator.

Fix:
Use strings.FieldsSeq so command classification preserves token semantics without the intermediate slice allocation.

Verification:
- go test ./internal/agent
- go vet ./internal/agent
- go test -race ./internal/agent
- golangci-lint run --timeout=5m
- go run ./tools/repolint
2026-08-10 12:23:44 +08:00
SivanCola bc8dc11f84 refactor(workspace): isolate refresh coordination
Problem:
The review fixes left refresh coordination and mutation tests embedded in already oversized owner files, and one source-contract test assumed the fallback lived directly in App.tsx.

Root cause:
Fast-path mutation delivery and focus reconciliation were added at their call sites before the final repository-size audit, coupling the behavior to presentation files and historical test layout.

Fix:
Extract host mutation contracts, mutation finalization, focus reconciliation, and focused tests into dedicated modules. Keep resource flags on the host-only signal, preserve provider wire omission, and teach the source contract about the extracted hook without expanding any lint baseline.

Verification:
- go test ./... and go vet ./...
- desktop go test ./... and go vet ./...
- focused Go race suites for agent, event, eventwire, boot, and desktop workspace hub
- pnpm test:workspace, pnpm test:all, and pnpm build
- repolint, release workflow contracts, formatting, bundle budgets, and diff checks
2026-08-10 12:19:08 +08:00
SivanCola ffd61ee3f1 Merge latest main-v2 into fix/workspace-live-refresh
Resolve the desktop frontend CI conflict by retaining workspace, motion, and transcript suites. Also keep the WebView2 smoke global declaration visible to the test TypeScript dependency graph.

Verification:
- go test ./...
- go test -race ./internal/event ./internal/agent ./internal/eventwire ./internal/evidence ./internal/boot
- go test -race . -run 'TestWorkspaceChangeHub|TestTabEventSink' -count=1 (desktop)
- go test ./... (desktop, isolated rerun)
- pnpm test:all
- pnpm build
2026-08-10 12:05:35 +08:00
SivanCola ce30809a64 fix(workspace): close live refresh race gaps
Problem:
Workspace previews and change resources could stay stale after focus reconciliation, hook-replaced writers, Git metadata-only commands, or a writer completing ahead of another tool in the same batch.

Root cause:
Reconciliation ignored equal revisions, mutation attribution used the provider-visible call and the delivery classifier, and host invalidation was coupled to batch-ordered ToolResult emission.

Fix:
Add an independent host-only workspace mutation capability, classify the final executed target, publish each writer immediately, advance resource-specific revisions, and force visible resource reloads for focus/degraded fallback without accepting stale scope responses.

Verification:
- go test ./...
- go test ./... (desktop module)
- go vet ./... (root and desktop modules)
- go test -race ./internal/event ./internal/agent ./internal/boot
- go test -race . -run 'TestWorkspaceChangeHub|TestTabEventSink' -count=1 (desktop)
- pnpm test:all
- pnpm build

Co-authored-by: SivanCola <[email protected]>
2026-08-10 11:59:52 +08:00
SivanCola e452c3fd57 fix(cli): stop viewport clear flicker on Windows
Problem: scrolling the alt-screen transcript on Windows triggered an asynchronous full-screen clear for every viewport offset change, producing a visible second frame in ConPTY terminals.

Root cause: the Warp stale-row workaround was applied on every operating system even though Bubble Tea already disables scroll optimization on Windows.

Fix: retain the legacy clear workaround on non-Windows terminals only and cover the platform policy plus force-bottom behavior with focused tests.

Verification: go test ./... -count=1; go vet ./...; make lint; Windows amd64 internal/cli test cross-compile.
2026-08-10 11:54:23 +08:00
SivanCola 5541883a52 chore(desktop): allow Linux bundle size variance
Problem:
The Linux desktop CI build produced 2201.2 KiB of raw initial assets while the macOS build produced 2200.7 KiB, narrowly exceeding the 2201 KiB cap.

Fix:
Raise only the raw initial-asset budget by 1 KiB to accommodate the observed cross-platform variance. Keep every gzip and chunk budget unchanged.

Verification:
- pnpm build
- Linux CI failure log confirmed all gzip budgets passed
2026-08-10 11:52:37 +08:00
SivanCola 41996a8f8f refactor(desktop): group work process display settings
Problem:
The work-process fold and reasoning display controls appeared as unrelated top-level settings even though users reason about them together.

Root cause:
The UI exposed the outer process lifecycle and inner reasoning presentation without a shared hierarchy.

Fix:
Group both controls under one Work process card while preserving their independent stores and behavior. Add responsive and accessible segmented controls, update all three locales, defer the group CSS with settings, and retire stale summary-switch copy.

Verification:
- focused reasoning and settings contract tests
- frontend typecheck, responsive checks, and production build
- browser mock at desktop and narrow widths
- go test ./internal/config
- cd desktop && go test ./...
- go run ./tools/repolint
- git diff --check
2026-08-10 11:48:21 +08:00
SivanColaandRay-lyy 6376bd4a6d Enforce native motion and WebView2 release gates / 强制执行原生动画与 WebView2 发版门禁 (#8096)
* test(desktop): enforce native motion release gates

Problem:
Native motion regressions could pass aggregate CI even when Element.animate was never exercised, and Windows release builds were signed without a real approval interaction in WebView2.

Root cause:
The focused motion suites were not part of a required check, legacy GSAP easing tokens had no source-level contract guard, and the release workflow never launched the built Wails executable.

Fix:
Add WAAPI contract scanning, required motion CI, a real Wails/WebView2 approval smoke before signing, protected release verification, and an independent review check for large or cross-boundary pull requests.

Verification:
- pnpm test:motion
- pnpm typecheck
- pnpm build
- go test ./... (desktop)
- bash scripts/release-workflows.test.sh
- actionlint for changed workflows
- go run ./tools/repolint

* fix(ci): close native motion release gate gaps

Problem:
- Windows rejected the shared WAAPI easing constants and the required lint job failed ShellCheck.
- Large-PR approval could be stale or evaluated from candidate-controlled workflow code.
- Pull requests did not exercise the real Wails approval flow in WebView2.

Root cause:
- The WAAPI contract matched only POSIX paths, the cross-platform lint loop split an unquoted value, and the review policy was bound to pull_request events without checking review.commit_id.
- The WebView2 smoke existed only in the release workflow.

Fix:
- Normalize contract paths and add a Windows regression fixture.
- Quote the lint target parsing, move review decisions into a protected control plane with current-head status publication, and add policy tests.
- Build and smoke-test the Windows Wails executable in PR CI.

Verification:
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend build
- node --test .github/scripts/cross-boundary-review.test.cjs
- bash scripts/release-workflows.test.sh
- actionlint for all changed workflows
- cd desktop && go test ./...
- go run ./tools/repolint

Co-authored-by: Ray-lyy <[email protected]>

* fix(ci): isolate transcript virtualization from motion gate

Problem:
The native motion gate ran the transcript virtualization suite in three jobs. Its prepend assertion could execute before the virtualizer remounted the anchor, producing a false Windows failure and skipping the real WebView2 smoke.

Root cause:
The suite was grouped by historical proximity rather than ownership, and the assertion waited for one fixed render flush instead of the DOM condition it required.

Fix:
Give transcript virtualization its own test command, run it once in the Linux desktop job, and make the motion contract reject future coupling. Wait for the anchor to remount before asserting, and cover the transcript entrance reset-key contract directly.

Verification:
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend test:transcript (6 consecutive passes)
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- go run ./tools/repolint
- actionlint for changed workflows
- bash scripts/release-workflows.test.sh

* fix(ci): defer WebView2 smoke instrumentation

Problem:
The latest-base Linux Desktop build exceeded the 2,200 KiB raw startup budget by about 0.1 KiB.

Root cause:
The Windows-only approval smoke instrumentation was statically linked into every normal desktop startup while the latest base had little budget headroom.

Fix:
Lazy-load the smoke component only when the native harness injects its flag, and make the motion contract reject a future static App import.

Verification:
- pnpm --dir desktop/frontend test:motion
- pnpm --dir desktop/frontend test:transcript
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- go run ./tools/repolint
- git diff --check

---------

Co-authored-by: Ray-lyy <[email protected]>
2026-08-10 11:38:47 +08:00
SivanCola 1e1c6df4dc Merge main-v2 into reasoning display modes 2026-08-10 11:33:29 +08:00
奚疑andSivanCola 288392f778 chore(desktop): harden build toolchain contracts (#8098)
Problem:
Non-interactive Wails builds could abort while pnpm asked to recreate node_modules, and local, CI, release, and documentation toolchain versions could drift.

Root cause:
The Wails frontend install hook kept pnpm purge confirmation enabled, release jobs still used Node 22, and the repository had no cross-file guard for the Node and pnpm contract.

Fix:
Make the pnpm purge path non-interactive, align Desktop builds on Node 24 and pnpm 10, reuse the shared .wails-version installer from #8069, separate CLI and Desktop build instructions, and enforce the contract in the release workflow test lane.

Verification:
- node scripts/check-desktop-build-contract.mjs
- bash scripts/check-wails-pin.sh
- bash scripts/check-wails-pin.test.sh
- bash scripts/release-workflows.test.sh
- actionlint .github/workflows/release-desktop.yml
- Node 24 engine-strict frozen install and frontend production build
- make lint
- go vet ./...
- go test ./internal/tool/builtin/ ./internal/boot/
- make wails-install and a full Wails macOS build

Co-authored-by: SivanCola <[email protected]>
2026-08-10 11:20:42 +08:00
YHH 68ad09ca04 Merge pull request #8124 from esengine/feat/context-window-truth
feat(cli): ask for the context window instead of assuming 128K
2026-08-10 10:30:24 +08:00
houren AntonyandSivanCola ed8fefaea0 fix(desktop): open folders in Explorer via explore verb plus trailing separator (#8010)
* fix(desktop): open folders in Explorer via explore verb, not open (#7851)

ShellExecute's "open" verb resolves folders through the shell namespace,
where a sibling "<folder>.lnk" with the same base name wins and launches
the shortcut's target instead of Explorer. The "explore" verb opens a
folder in Explorer directly and never consults .lnk files; files keep
"open".

* fix(desktop): open folders with explore verb plus trailing separator

Ablation on a real Windows host showed the first attempt was ineffective:
with a sibling "<folder>.lnk" present, ShellExecute's "explore" verb alone
returns E_FAIL (hinst 5) and opens nothing. Both "open" and "explore"
succeed once the folder path carries a trailing separator, which forces the
shell to treat the target as a directory and never consult the .lnk. Use
explore + trailing separator for folders; files keep "open" unchanged.

* fix(desktop): reuse validated path metadata for shell opens

Problem:
The Windows Explorer ambiguity fix classified every target with a synchronous os.Stat. Newer local-path and external-opener callers had already performed that lookup, while an unknown path silently fell back to the unsafe open verb and directory roots gained a duplicate separator.

Root cause:
Path classification and ShellExecute planning were coupled inside openWorkspacePath, so validated callers could not pass their existing directory metadata.

Fix:
Add a platform helper that accepts the validated directory flag, reuse it at existing stat call sites, fail closed when standalone classification fails, and append a folder separator only when absent. Extend Windows regression coverage for missing paths and pre-terminated directory targets.

Verification:
- go test ./... (desktop)
- go test -race . -run focused desktop opener tests
- go vet ./... (desktop)
- go run ./tools/repolint
- Windows/amd64 cross-compiled tests on Win11
- Full Wails Windows build opened the exact folder in Explorer without launching the real same-named shortcut

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

* docs(desktop): clarify local opener safety boundary

Problem: The local opener comment said openWorkspacePath stayed untouched and named RevealWorkspacePathForTab, which no longer matched the implementation.

Root cause: Windows directory classification evolved while the old security-boundary explanation remained unchanged.

Fix: Describe the actual invariant: openWorkspacePath stays outside the executable-target guard because callers provide workspace-authorized paths.

Verification: focused desktop local-path tests, go vet ./..., repolint, and git diff --check.

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

---------

Co-authored-by: SivanCola <[email protected]>
2026-08-10 10:29:11 +08:00
YHH 623760f432 Merge pull request #8126 from esengine/feat/context-command
feat(context): /context shows the window, the thresholds, and the last pass
2026-08-10 10:18:55 +08:00
SivanCola aebb78e959 Merge latest main-v2 into fix/workspace-live-refresh
Integrate the sub-agent specification and event sink forwarding refactor at 28ad01d5. Regenerate the repolint baseline from the combined tree; workspace refresh and audit event contracts remain intact.
2026-08-10 10:11:07 +08:00
SivanCola ae83c2ba01 fix(workspace): harden live refresh lifecycle
Problem:
Workspace revisions could cross scope boundaries, refresh bursts could overlap or hammer Git, and the Desktop watcher could suppress real external saves or retain roots after a workspace switch.

Root cause:
The frontend kept a tab-global monotonic baseline and used direct refresh effects, while the hub used time-based agent/filesystem deduplication, incomplete Git metadata watches, and delayed watcher cleanup.

Fix:
Scope refresh snapshots by tab and workspace, add bounded trailing schedulers, extract resource invalidation, release stale roots, validate mutation paths, recursively watch selected Git metadata, and preserve every authoritative filesystem observation. Add deterministic frontend, watcher, race, and CI coverage, plus lazy reconciliation to preserve bundle budgets.

Verification:
go test ./...
go test -race . -run TestWorkspaceChangeHub -count=1
pnpm test
pnpm build
go vet ./...
./scripts/cache-guard.sh
./scripts/check-wails-pin.sh
go run ./tools/repolint
2026-08-10 10:07:44 +08:00
esengine 2036a47005 feat(context): /context shows the window, the thresholds, and the last pass
Every compaction decision divides by the context window, so a window that
is too small is indistinguishable from a context that is genuinely full:
both just compact. Nothing exposed the number, and nothing exposed why a
maintenance pass did or did not run.

/context reports the window and its usable ceiling, the latest prompt (the
last request's shape, never the billable retry aggregate), canonical vs
model-visible size, the snip/fold/force thresholds as absolute tokens, the
last pass's mode and what it reclaimed, and the blocked reason when one is
recorded. The one-line summary names what happens next rather than only
how full the context is.

Thresholds come from compactThresholds and forceCompactThreshold, the same
helpers the decision uses, so the report cannot drift from the behavior.

Adding a command to two files already at their size budget would have
widened the baseline, so the routing pays for itself: /new and /clear were
two copies of the same goroutine-and-report block and now share
runSessionVerb, and /remember moves out of the switch alongside the new
/context handler. Notice texts are unchanged.
2026-08-09 19:01:14 -07:00
SivanCola a652cd8177 Merge main-v2 into fix/workspace-live-refresh
Resolve concurrent event contract additions by retaining both context maintenance and workspace refresh events. Keep the latest main-v2 desktop, runtime, documentation, and CI changes as the integration base for the workspace refresh fixes.
2026-08-10 09:59:03 +08:00
YHH 28ad01d5e1 Merge pull request #8114 from esengine/refactor/subagent-spec-split
feat(agent): one delegation contract, and what it costs to use it
2026-08-10 09:55:45 +08:00
esengine bfdbc35488 feat(cli): ask for the context window instead of assuming 128K
The custom-provider wizard wrote ContextWindow: 128000 unconditionally,
on all four paths (OpenAI and Anthropic compatible, single model and
model list). A relay serving a 1M-window model was therefore recorded as
128K, and compaction fired at 80% of the wrong number: roughly a tenth
of the model's real capacity, which reads to the user as "Reasonix
compacts far too early".

An OpenAI-compatible endpoint does not report its context window, and
guessing from the model name is unreliable for a relay, so the wizard now
asks. The default stays 128000 for the case where the user does not know
either, but it is a visible answer rather than a silent assumption.

The prompt says what a wrong value costs, since that is the part no one
could previously see.
2026-08-09 18:38:33 -07:00
esengine 2133fc6ac8 docs: a child's token total is re-sent context, not fresh material
I wrote that one child run "costs 100k-220k tokens before it produces
anything", and used it as a threshold to compare a branch's size against. Both
halves were wrong enough to mislead.

The figure is cumulative prompt tokens across the child's own model calls, so
the same context is counted once per call: 27 measured children averaged 134k
tokens over 9.3 calls, which is the same ~14k context re-sent, not 134k tokens
of new material. At the ~90% cache hit these runs saw, the real price of a
child averaged ¥0.017.

The 2-4x delegation cost is unaffected, because both arms are counted the same
way. What is removed is the invented threshold: comparing a branch's size in
tokens against a child's cumulative re-sent context is not a like-for-like
comparison and should never have been stated as one.

The benchmark line is relabelled to say what it measures. For scale, what this
branch itself adds to a child — complete_subtask's schema, its description and
the closing contract — is about 654 tokens of that per-call context, roughly
4.5% of it.
2026-08-09 18:33:22 -07:00
github-actions[bot] d4bea95948 docs(release): prepare v1.22.0 notes (#8121)
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.

Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-10 09:21:15 +08:00
esengine 1d0c0e7db2 Merge origin/main-v2 into refactor/subagent-spec-split
main-v2 had moved 84 commits ahead. Everything auto-merged except
tools/repolint/baseline.json, which is generated: resolved by taking upstream's
and regenerating, rather than hand-merging JSON.

Regenerating surfaced one real violation instead of a carry-forward:
internal/event/event.go had grown past its budget, so the delegation audit sink
and its dispatcher move into audit_forwarder.go where the rest of that concern
already lives. The only baseline entry above upstream is plugin_entry.go's
essay, which is the field comments carried through the earlier extraction.
2026-08-09 18:19:47 -07:00
esengine 510e740bc5 fix: REASONIX.md named a type that no longer exists
The delegation boundary rule went into REASONIX.md while the spec member was
still called ContextCapsule. Renaming it to ContextRequest, so the manifest
could take that name, left the standing instruction pointing at a type nobody
can find — and REASONIX.md is loaded into the system prompt, so the stale name
is in the prefix rather than only in a doc.

Also worth recording against the question this came from: does merging make the
agent delegate less? Measured rather than argued. The same five neutral tasks,
same fixtures, only the binary swapped: main-v2 delegated zero times and this
branch delegated zero times, both 4/4 passing. The 15% delegation rate observed
throughout was already the main-v2 rate.

Nothing here adds a brake. The admission gate runs in applyBatchGuards, after
the calls have executed, and only emits an audit — there is no path by which a
verdict denies a call. The ablation change is inert unless --ablate subagent is
passed. What the branch adds to a delegated run is complete_subtask in writer
child registries and a closing contract in the child prompt, which is a cost
paid by a child that already ran, not a deterrent to starting one.
2026-08-09 18:14:15 -07:00
SivanCola 24be6e684f Merge pull request #8109 from SivanCola/fix/context-maintenance-idempotency
fix(context): make maintenance projection-idempotent / 上下文维护投影幂等化
2026-08-10 09:13:48 +08:00
esengine 2838bbc3f1 docs: why delegation is rare, from the runs rather than from theory
The question was why the model keeps sidestepping situations built to need
delegation. The recorded reasoning answers it: it does not weigh delegation and
decline, it never reaches the question. On a task built for `explore` it
deliberates over how to read efficiently — "that's 25 files... read them in
parallel batches... I can read multiple files at once" — and delegation does not
enter the decision.

Three causes, one of them a defect. The base system prompt never mentions
delegation; every mention is in the skills index and every one is a brake, sat
next to an accelerator for inline skills. The task tool description says what
the tool does and never when to reach for it. And the model already has cheaper
parallelism in several tool calls per round trip, with no context duplicated,
which is the thing it actually reasons about.

Given the measured 2.4-4.5x cost, the brake is the right default and is not the
defect. The gap is that nothing recognises the rare case where delegation would
pay, and forcing it does not close that gap: in the forced fleet run the parent
derived all three fixes in its own reasoning before dispatching, so the children
re-read the code to apply edits it had already worked out. Delegation moved the
typing, not the thinking.

Numbers behind it: 33 runs with delegation available, 15% delegated, bash
outnumbering every delegation-class call 10 to 1.
2026-08-09 18:05:22 -07:00
esengine e451757bdb refactor(event): forward optional sink capabilities by embedding, not by hand
Nine optional capabilities, repeated at every wrapper in the chain, silently
lost three audits in this branch alone: control's goal-usage tee, the stats
recorder, and the nesting sink fleet items run under. Each time the data simply
stopped, each time with the tests green, and each time the fix was to write the
same method one more place.

event.AuditForwarder holds the nine forwards once. A pure forwarder embeds it
and is complete by construction, so the next capability added there reaches
every embedder without editing them. goalUsageTee, notify.Sink, telemetry's
sink and the nesting sink now embed it, deleting roughly a hundred lines of
identical methods; the tee had been forwarding eight of nine, and the nesting
sink three of nine, before this.

Wrappers that do real work per channel — the coalescer flushes first, stats and
trajectory record, run metrics consumes — keep their own methods. Embedding is
for the ones that only pass things through.

The guard now asserts the forwarder itself covers every capability, which is
the property that makes the wrapper-level guard mostly redundant rather than
load-bearing. Verified end to end against the provider: a delegated run still
reports its child.
2026-08-09 17:57:52 -07:00
esengine c62c020c44 bench: record the context-pressure attempt, including that it failed
The last hypothesis worth testing was that delegation's isolation pays when the
parent is genuinely hurt by what it read. Isolating that mechanism needs the
parent under context pressure, so these two variants pin a workspace
compact_ratio of 0.5% over the same three-package migration.

It did not work. Zero compactions in both arms, at 0.5% of a 1M window, because
the agent keeps its session small by writing a script rather than reading — the
same behaviour that has won it every comparison so far. The arms still came out
4.5x tokens and 5.6x wall for the forced one, but that number is not evidence
about context pressure, because the pressure never arrived.

Kept as an honest record of the attempt rather than deleted, and because they
exercise the project reasonix.toml merge path (verified: the run reports
compact_ratio = 3% (project: reasonix.toml), so the config is read; the session
simply never reaches the threshold).

SPEC 3.17 states this as untested rather than disproved. Provoking it needs a
task that cannot be scripted away, which this corpus does not contain.
2026-08-09 17:48:24 -07:00
SivanCola adf4ac72ac fix(agent): make context maintenance converge transactionally
Problem:
Context maintenance still had divergent strict-role and manual compression paths. Manual folds could archive before a durable commit, Anthropic native editing had no session-stable rejection fallback, and desktop maintenance receipts could surface stale or untranslated state.

Fix:
- converge strict-role pressure handling and explicit range compression on the projection CAS transaction
- persist generation, hashes, receipts, archives, and native/local context-editing lineage
- allow one pre-success Anthropic native rejection fallback, then lock the session request shape
- avoid duplicate Guardian maintenance epochs and fence stale desktop metadata/balance reads
- localize context-maintenance notices and expose projected/canonical maintenance state

Verification:
go test ./... -count=1
go test -race ./internal/agent ./internal/guardian ./internal/provider/anthropic ./internal/control -count=1
go test ./internal/agent -run TestIssue7935Maintains206ToolResultsOnceAndOnlyProcessesNewTail -count=10
cd desktop && go test ./... -count=1
cd desktop/frontend && pnpm test && pnpm build && pnpm test:typecheck
./scripts/cache-guard.sh
go vet ./...
golangci-lint run ./...
go run ./tools/repolint
2026-08-10 08:43:52 +08:00
esengine f77bb71c0c feat(bench): heterogeneous branches, delegation's best case, still 2.4x
Three branches needing three different kinds of work — a logic fix, a half-up
rounding fix, and a manifest regenerated by reading both packages. No bulk
transformation solves them, which is the one shape where delegation has a
theoretical case, and the reason the earlier migration lost so badly.

The first repetition read 1.42x, the closest delegation has come. Two more
repetitions put it at 3.73x and 2.43x, mean 2.41x tokens and 3.65x wall over
identical work with a 100% success rate in both arms. The neutral arm's own
spread across those three runs was 8-37%, so the first reading was a favourable
draw rather than a narrowing — which is exactly what the recorded noise floor
predicted and why it is worth having.

The forced arm's spread was roughly twice the neutral arm's, so delegation buys
variance as well as cost.

SPEC 3.17 now records the four measured task shapes, since a result this
consistent is more useful to the next reader than the counters alone.

Fixture note: the reference fix for the rounding branch was wrong on the first
attempt because Python's round() is banker's rounding, so 1250 stayed 1.2k. The
task keeps that trap deliberately — the docstring says half up — but it is a
reminder that a fixture is only verified when both directions are run.
2026-08-09 17:39:52 -07:00
esengine d9a28f99da feat(bench): a migration task big enough for delegation to pay, and it still doesn't
Three packages, 36 files, 2736 lines, one signature change touching 108 call
sites, non-overlapping write paths. This is the branch size the earlier
measurement said delegation would need, since one child run costs six figures of
tokens before it produces anything.

It still loses, and for a more interesting reason than overhead. Neutral prompt:
28s, 12 tool calls, one file read — the agent wrote a script and applied the
change in bulk. Forced fleet: 116s, 87 tool calls, 78 of them in three children
editing their twelve files each. 2.6x tokens, 3.4x cost, 4.1x wall clock, same
result.

Delegation did not merely add overhead here; it prevented the better algorithm.
The task looks parallel-shaped, so splitting it by package hides the fact that
one bulk transformation solves all three at once. No branch can see a global
optimisation that spans the branches.

Both seeds were checked to fail unfixed and pass fixed before use. The first
checker was wrong in a way worth recording: it called every callable in each
module, including the fmt_amount imported into the namespace, so every arm would
have failed and produced confident garbage. Verifying the fixture in both
directions is what caught it.
2026-08-09 17:23:18 -07:00
esengine 3b7f57c629 feat(agent): extend the shadow delegation gate, and price one child run
The measurement gave the existing shadow admission gate something to act on.
It already gated `research` on local-fix turns, from a finding that one call
cost 65% of the worst task's wall clock. The same shape now covers task, fleet
and explore, because forcing delegation on identical work cost 2-4x the tokens
and wall clock with no change in outcome.

It stays shadow-only. Two tasks are enough to gate a tool for observation, not
enough to block a call a user asked for, and the existing design already says
observe first.

The benchmark now also prints what one child run costs before it has produced
anything — 103k tokens on a real run. That is the number a delegation decision
actually turns on: work whose branch is smaller than that cannot pay for the
agent doing it, whatever the orchestration around it looks like.

The existing admission test pinned "research only" and used `task` as its
negative case. It now asserts both are audited while keeping what it was really
protecting: a non-delegation tool is never audited.
2026-08-09 17:10:41 -07:00
esengine 034dc7e1e7 feat(bench): delegation-shaped tasks, and count the fleet children they revealed
Four tasks: two whose structure rewards delegation with a neutral prompt, and
two forced twins that differ only in telling the agent to delegate. Same
workdir, same checks, so the pair prices delegation on identical work instead of
comparing different work. Both seeds were confirmed to fail unfixed and pass
fixed before either was used.

The existing subagent-delegation task mandates delegation in its prompt, which
tests the plumbing rather than the value: a single-agent arm cannot comply, so
it loses by the rules rather than on the merits. The neutral tasks exist so an
arm that declines to delegate is only penalised by the clock.

Running them found the instrument under-counting. A fleet item runs under a
sink built by subSinkFor, which was a bare event.FuncSink; optional sink
capabilities are forwarded by method, so every delegation audit a fleet or
parallel_tasks child produced was swallowed at the nesting boundary. The forced
fleet task burned 3.8x the tokens of the neutral one and still reported zero
child runs. subSinkFor now returns a type that forwards the audits, and the
same task reports 3 child runs doing 71% of the tool calls.

This is the third instance of one contract implemented at many wrappers and
missed at one. The guard added earlier covers the session chain but not this
nesting sink, which is a fair argument that the pattern wants a shared embedded
forwarder rather than more guards.
2026-08-09 17:03:48 -07:00
esengine 793584c720 docs: record the measured noise floor for delegation comparisons
Running the fixed control produced a clean negative result worth writing down
so the next person does not read a single run as evidence.

Same arm, same six tasks, twice: per-task token use moved by a median of 19%
and as much as 54%. The whole between-arm difference in the same experiment was
2.5%, and five of the six tasks saw no delegation in either arm, so what the
comparison mostly measured was run-to-run variance.

SPEC 3.17 now states that alongside the counters, together with the two ways to
get a real answer: enough repetitions to clear the variance, or restricting the
comparison to tasks where subagent_runs shows delegation actually happened.
2026-08-09 16:34:22 -07:00
esengine 8b02d759e7 fix(boot): make the subagent ablation an actual single-agent control
Measuring the arm showed it was not one: a `no-subagent` run still spent a
child run on `explore`, because the ablation removed task, read_only_task,
fleet and parallel_tasks while leaving runAs=subagent profile skills — reached
through run_skill or the dedicated explore/research/review wrappers — running.
Every comparison against that arm was therefore delegated-versus-delegated.

Delegation is delegation whichever tool reaches it, so the arm now gates the
skill runners as well and stops registering the wrapper tools, which also keeps
their schemas out of the control run's prefix. Inline playbooks are untouched:
they never spawn a child, and ablating "subagent" should not disable a
checklist.

Verified against the provider: the same task that previously reported one child
run under the arm now reports subagent_runs=0 and "solved by a single agent".

This changes what the no-subagent arm means, so figures from it are not
comparable with baselines recorded before this commit. That is the point — the
older ones were measuring something else.

The gate lives in its own file rather than inline, because the guard would
otherwise have been duplicated across the two runners. Note for the next reader:
that file cannot be named *_arm.go — Go reads the suffix as GOARCH and silently
drops the file from the build.
2026-08-09 16:25:06 -07:00
SivanCola a5978f08ed docs(context): document maintenance and native editing
Problem:
The PR adds durable context-maintenance semantics and an advanced native Anthropic opt-in that were not covered by the embedded guides.

Fix:
Document projection-based maintenance, durable blocked fingerprints, canonical transcript preservation, and the official-Anthropic-only context_editing setting in English and Chinese.

Verification:
- go test ./docs
- go run ./tools/repolint
- git diff --check
2026-08-10 05:16:14 +08:00
SivanCola 87b7d130f3 fix(context): make maintenance projection-idempotent
Problem:
Long sessions could repeatedly snip the same canonical tool results, retry failed summaries, and report misleading context state.

Root cause:
Preflight and post-turn compaction evolved separate views, while pruning rebuilt from canonical history instead of the installed projection. Failed view fingerprints and maintenance outcomes were not durable.

Fix:
Make ContextManager the single request-time owner, evolve projection-only views with CAS and v3 receipts, persist blocked fingerprints, add Anthropic native tool clearing capability gates, and expose current context composition separately from summary-call cost.

Verification:
- go test ./...
- go test -race ./internal/agent ./internal/provider/anthropic ./internal/control
- go vet ./...
- ./scripts/cache-guard.sh
- cd desktop/frontend && pnpm typecheck
- focused Desktop and frontend context contract tests
- go run ./tools/repolint
2026-08-10 05:14:03 +08:00
SivanCola 9b2c85c9d3 Merge remote-tracking branch 'origin/main-v2' into HEAD 2026-08-10 04:07:55 +08:00
SivanCola d541409e26 Merge pull request #8069 from esengine/ci/wails-version-pin
ci(wails): unify the CLI and library version pin / 统一 Wails CLI 与依赖版本锁定
2026-08-10 04:04:05 +08:00
SivanCola 0d29be5463 Merge remote-tracking branch 'origin/main-v2' into HEAD 2026-08-10 04:03:42 +08:00
SivanCola 13c072b5c3 Merge pull request #8084 from SivanCola/fix/desktop-archive-reliability
Make desktop topic archive failure-safe / 修复桌面端话题归档卡死
2026-08-10 03:51:41 +08:00
SivanCola c49ea44087 fix(ci): close Wails pin guard bypasses
Problem: A broken installed Wails CLI made prod_test exit before its repair path, while the regression suite copied the repository's current pin and the guard accepted alternate dynamic install sources.

Root cause: The version probe inherited errexit and pipefail, the integration assertion embedded v2.13.0, and the guard relied mostly on negative checks instead of proving each supported consumer read .wails-version.

Fix: Treat probe failures as an explicit version mismatch, derive real-repository expectations from the shared pin, validate stable pin syntax, whitelist supported install sources, and guard plus quote the Makefile installer. Add regressions for broken CLIs, alternate workflow sources, hard-coded Make variables, and invalid pins.

Verification: make lint; bash scripts/release-workflows.test.sh; isolated GOBIN make wails-install with wails version v2.13.0; broken Wails probe returns an actionable mismatch; git diff --check.

Co-authored-by: SivanCola <[email protected]>
2026-08-10 03:39:23 +08:00
SivanCola 11925957de merge: refresh durable inbox branch from main-v2
Problem:
The target branch advanced again during final verification with Desktop opener and shared-window output-budget fixes.

Root cause:
PR validation overlapped active main-v2 development.

Fix:
Merge the latest target branch without rewriting contributor history and preserve both frontend bridge contracts and inbox durability changes.

Verification:
Run repository lint plus overlapping frontend, agent, provider, root, and Desktop checks after integration.
2026-08-10 03:33:13 +08:00
SivanCola 5204882948 Merge pull request #8087 from SivanCola/fix/global-workspace-external-opener
Enable external opener for Global workspaces / 为 Global 工作区启用外部打开按钮
2026-08-10 03:32:06 +08:00
SivanCola f0057fedce merge: sync durable inbox branch with latest main-v2
Problem:
The target branch advanced after local validation with session, shell, plugin, and Windows shortcut fixes.

Root cause:
PR validation overlapped active main-v2 development.

Fix:
Merge the latest target branch without rewriting contributor history and retain the durable inbox changes on top.

Verification:
Run the repository lint, affected package tests, cache guard, and full root/Desktop suites after integration.
2026-08-10 03:29:33 +08:00
SivanCola b227e0e2ca refactor(inbox): satisfy repository lint budgets
Problem:
The completed durability fixes pushed several already-baselined files and functions past the repository's ratcheted size and complexity budgets, causing the PR lint job to fail.

Root cause:
Review follow-up logic and tests were appended to legacy monoliths, and idempotency checks remained nested inside Store mutations.

Fix:
Extract ACP, Bot, Controller, Desktop, CLI, and session inbox helpers and tests into focused files; centralize idempotency receipt and alias handling; preserve the existing repolint baseline without widening any budget.

Verification:
- go run ./tools/repolint
- go vet ./internal/sessioninbox ./internal/control ./internal/bot ./internal/acp ./internal/cli
- go test ./...
- go test -race ./internal/sessioninbox ./internal/control ./internal/agent ./internal/bot ./internal/acp
- cd desktop && go test ./... -count=1 -timeout=10m
- cd desktop/frontend && pnpm build
- cd desktop/frontend && pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx
- ./scripts/cache-guard.sh
- go test ./internal/sessioninbox -run '^$' -bench '^BenchmarkThirtyOneMiBItemsRetainedHeap$' -benchtime=1x -count=1
2026-08-10 03:29:27 +08:00
SivanCola d44e1022b7 Merge pull request #8057 from SivanCola/fix/shared-window-output-budget
fix(agent): bound shared-window output budgets / 修复共享窗口输出预算溢出
2026-08-10 03:29:21 +08:00
SivanCola a1e23402e9 Merge pull request #8073 from erphenheimer/fix/8072-windows-shortcut-root-icon
fix(windows): repair legacy root-level shortcut icon and target / 修复旧版根目录快捷方式
2026-08-10 03:25:45 +08:00
SivanCola e939f2dd26 style(desktop): modernize teardown deadline clamp
Problem:
- The macOS lint gate rejected the manual duration bounds checks.

Root cause:
- The modernize linter requires the Go min and max built-ins for simple clamps.

Fix:
- Express the shared teardown deadline clamp with min and max.

Verification:
- golangci-lint run --timeout=5m ./... (desktop)
- go test . -run 'TestWaitDestroyHandleBatchesWaitsAcrossSessionsConcurrently|TestWaitDestroyHandles' -count=1
2026-08-10 03:21:20 +08:00
SivanCola a25ae9ef24 Merge pull request #8053 from SivanCola/fix/core-data-safety
Harden core data-safety paths / 加固核心数据安全路径
2026-08-10 03:17:57 +08:00
SivanCola 984e6f2e1d fix(desktop): finish topic archive reconciliation
Problem:
- A committed topic archive could leave metadata behind without a durable retry path.
- Empty live stubs could create duplicate trash entries.
- Multi-session teardown could consume one watchdog interval per session.

Root cause:
- Session cleanup markers did not carry the topic metadata operation.
- Existing-trash reconciliation treated an empty live transcript as divergent.
- Destroy handles were awaited in per-session serial batches.

Fix:
- Persist a hashed topic archive intent and reconcile sessions plus metadata at startup.
- Remove empty or identical live artifacts under the existing removal guard while preserving sidecar-only recovery.
- Wait all topic destroy batches concurrently under one shared deadline.

Verification:
- go test ./... -count=1 -timeout=10m (desktop)
- go test -race . -run archive-focused suite
- go test ./internal/agent ./internal/control -count=1
- GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -exec=true .
- go vet ./... (root and desktop)
- go run ./tools/repolint
- ./scripts/cache-guard.sh
2026-08-10 03:17:23 +08:00
SivanCola 1f93876afe Merge pull request #8077 from SivanCola/feature/native-v2-explicit-resources
fix(plugin): make native v2 resources explicit / 原生 v2 插件资源改为显式声明
2026-08-10 03:15:21 +08:00
SivanCola 57200e53e4 merge: refresh durable inbox branch from main-v2
Problem: main-v2 advanced again during final validation with PR #8059, including TUI and Serve changes adjacent to this branch.

Root cause: the base branch moved after the prior synchronization commit.

Fix: merge origin/main-v2 at 303139c61 without force-pushing or rewriting contributor history; Ctrl+J newline and durable inbox behavior remain intact.

Verification: rerun the affected CLI, Serve, root, race, Desktop, frontend, cache, and retained-heap gates before push.
2026-08-10 03:12:31 +08:00
SivanCola 303139c612 Merge pull request #8059 from SivanCola/feature/web-cli-handoff
feat(cli): add first-class Web handoff / 增加 CLI Web 会话交接
2026-08-10 03:10:55 +08:00
SivanCola c41b51c577 merge: sync durable inbox branch with main-v2
Problem: PR #8061 was 25 commits behind the current main-v2 tip, so its tested tree did not include the latest session and Desktop changes.

Root cause: main-v2 advanced after the durable inbox branch was created and last reviewed.

Fix: merge origin/main-v2 at b22ef0119 without rewriting contributor history; the merge completed without textual conflicts.

Verification: final root, Desktop, frontend, race, cache, and merge-gate checks are run on this merge commit before push.
2026-08-10 03:08:28 +08:00
SivanCola b64c05b7ab fix(inbox): close durable queue review gaps
Problem: synchronous ACP and Bot turns could strand or replay durable follow-ups, Desktop could lose structured invocation semantics, Ctrl+J conflicted with multiline input, and completed idempotency/path metadata was not sufficiently guarded.

Root cause: blocking transports bypassed the shared durable completion path, rich entities were stored only in frontend state, idempotency mappings ended at dequeue, and manifest blob fields were trusted before semantic validation.

Fix: add synchronous claim/run/ack and transport-owned FIFO draining, persist structured invocations and bot routing metadata, restore Ctrl+J newline behavior, retain bounded completion receipts with request fingerprints, migrate schema v1, and reject malformed or symlink-backed inbox storage.

Verification: focused sessioninbox/control/Bot/ACP/Desktop tests, Wails binding generation, Composer structured-queue test, and scripts/cache-guard.sh.
2026-08-10 03:08:11 +08:00
SivanCola f44653cea5 feat(desktop): add three-mode reasoning display
Problem:
Reasoning presentation was controlled by a binary summary preference, so users could not hide reasoning or follow it live consistently across the main transcript, work folds, and sub-agent cards.

Root cause:
The desktop settings contract and frontend components had no shared reasoning-display state machine; legacy expand_thinking and localStorage preferences were read independently.

Fix:
Add hidden, summary, and auto modes with pending and legacy-collapsed compatibility states, Wails/config persistence, migration precedence, shared disclosure behavior, settings controls, transcript filtering, and sub-agent integration.

Verification:
Focused reasoning/config tests, go test ./internal/config, cd desktop && go test ./..., pnpm test:typecheck, pnpm build, and go run ./tools/repolint pass. The full frontend suite still has an unrelated existing timeout in new-session-load-race.test.tsx while waiting for stale metadata.
2026-08-10 03:06:13 +08:00
SivanCola 641f7993fd fix(ci): enforce the shared Wails version everywhere
Problem:
Local production builds and contributor setup could still bypass the shared Wails pin, so the old CLI/library skew remained possible even while the workflow-only guard passed.

Root cause:
The guard inspected only two workflow files, prod_test checked command presence instead of the installed CLI version, and the desktop guide recommended @latest.

Fix:
Add a make-based pinned installer, make prod_test repair and verify version mismatches, reject repository-wide hard-coded Wails installs, move the maintenance guidance out of the cache-stable prompt, and cover each drift path with regression tests.

Verification:
- make lint
- bash scripts/release-workflows.test.sh
- make wails-install installs v2.13.0 in an isolated GOBIN
- wails build -platform darwin/arm64 -clean -skipbindings -nopackage

Co-authored-by: SivanCola <[email protected]>
2026-08-10 03:06:01 +08:00
SivanCola 150bb87d4d chore(windows): satisfy shortcut repair comment budget
Problem:
The required lint job rejected the supplemental shortcut-repair comment because its six-line block exceeded the repository's five-line budget for this position.

Root cause:
The current.json migration rationale was documented too verbosely for the repolint policy.

Fix:
Compress the explanation while preserving the versioned-layout and live-flat distinction.

Verification:
- go run ./tools/repolint
- golangci-lint run --timeout=5m ./...
- cd desktop && GOOS=windows golangci-lint run --timeout=5m ./...
- git diff --check
2026-08-10 03:03:21 +08:00
SivanCola b22ef01193 Merge pull request #8099 from HaoyueQin/feat/status-bar-per-request-tps
fix(desktop): refresh status bar TPS after each provider request / 底部信息栏吞吐速度改为每次请求后更新
2026-08-10 03:03:15 +08:00
SivanCola 33a3454498 fix(desktop): synchronize opener preference across tabs
Problem:
A preference write completing after a tab switch updated only the unmounted selecting component. The visible tab could keep and launch the old opener even though the global preference had changed.

Root cause:
The shared coordinator ordered writes but did not publish persisted preferences or fence discovery results against preference revisions.

Fix:
Publish successful preference writes to mounted opener controls and use a revision fence so discoveries started before a write cannot restore stale state. Add deterministic coverage for both completion orders.

Verification:
- pnpm exec tsx src/__tests__/external-opener-preference-sync.test.tsx
- pnpm exec tsx src/__tests__/external-opener.test.tsx
- pnpm test:typecheck
- pnpm build
- go run ./tools/repolint
2026-08-10 02:58:34 +08:00
SivanCola a5501039aa chore(ci): baseline workspace refresh lint growth
Problem: The workspace refresh implementation exceeded existing repolint budgets in the owning files and repository totals.

Root cause: The new workspace event contract, watcher hub, and resource refresh wiring add bounded code to several files that already sit at their carried-forward limits.

Fix: Record only the affected file, function, complexity, and aggregate baseline deltas; shorten the new watcher comment to stay within the essay budget.

Verification: go run ./tools/repolint.
2026-08-10 02:58:05 +08:00
SivanCola 7577c86326 fix(desktop): refresh workspace resources from change events
Problem: Workspace previews and change lists remained stale until turn completion, and the embedded workbench did not expose a reliable refresh fallback.

Root cause: WorkspacePanel depended on a turn-level refresh key and had no resource-level invalidation for in-flight agent writes or external filesystem changes.

Fix: Add host-only workspace mutation metadata and workspace_changed revisions; add a root-scoped fsnotify hub with bounded coalescing, Git metadata watching, and degraded fallback; refresh frontend resources by revision and expose an embedded refresh action; group task, working-tree, and history changes with regression coverage.

Verification: go test ./...; cd desktop && go test ./...; cd desktop && go test -race -run 'TestWorkspaceChangeHub' ./...; cd desktop/frontend && pnpm -s typecheck && pnpm -s lint:hooks && pnpm -s check:css && pnpm -s build; cd desktop/frontend && pnpm -s test:terminal.
2026-08-10 02:54:04 +08:00
SivanCola 65f0a7dd49 Fix Responses replay input calibration
Problem:
Official DeepSeek Responses sessions could add ordinary reasoning or completed web-search items after the last usage calibration without increasing the shared-window input estimate. Near the context limit, that could still leave an oversized completion allowance and reproduce the HTTP 400 this PR is intended to prevent.

Root cause:
The calibration shape intentionally counted only text common to every DeepSeek transport. That assumption is conservative only for adapter-specific text already present in the calibrated request; newly appended Responses-only replay fields were invisible.

Fix:
Add an immutable provider input policy for adapter-specific shared-window replay fields. Have the official DeepSeek Responses adapter declare ordinary reasoning and ResponsesItems, consume that policy in request calibration, preserve it through the fork-capture wrapper, and add regressions for both growth paths while keeping other transports unchanged.

Verification:
- go test ./internal/agent ./internal/provider/... -count=1
- focused go test -race ./internal/agent
- go test ./... -count=1
- go vet ./...
- cd desktop && go test ./... -count=1
- ./scripts/cache-guard.sh
- go run ./tools/repolint
- git diff --check
2026-08-10 02:49:31 +08:00
SivanCola 1763067ce1 fix(runtime): close data-safety race windows
Problem: live shell progress could still grow without bound even though the final result was capped, and a stale session-listing backfill could overwrite newer turn counts.

Root cause: progress bypassed the bounded collector into append-only consumers, while listing decode and metadata update were separated by an unchecked transcript-generation window.

Fix: cap live progress at 64 KiB plus one marker, restore the bounded final result in CLI, and compare sidecar counts plus transcript generation under the metadata lock before backfill.

Verification: go test ./... -count=1; cd desktop && go test ./... -count=1; go test -race ./internal/agent ./internal/shellrun ./internal/cli -count=1; go vet ./...; cd desktop && go vet ./...; go run ./tools/repolint; ./scripts/cache-guard.sh
2026-08-10 02:47:35 +08:00
SivanCola 9ae2e399b5 fix(windows): distinguish versioned shortcut leftovers
Problem:
A root-level reasonix-desktop.exe can survive the versioned migration when best-effort cleanup fails, so file existence alone cannot prove a live flat install.

Root cause:
Shortcut repair treated every existing flat binary as authoritative, and the icon regression test depended on a fixed host path.

Fix:
Use validated current.json as the versioned-layout commit signal, preserve true flat installs, and cover existing and missing flat target and icon cases with isolated temporary roots.

Verification:
- cd desktop && go vet ./...
- cd desktop && go test ./...
- cd desktop && GOOS=windows GOARCH=amd64 go test -c
- go test ./internal/installlayout
- Native Windows 11 focused and real .lnk tests
2026-08-10 02:45:22 +08:00
SivanCola ecde0fd8f0 fix(cli): preserve legacy remote Serve authentication
Problem:
- A newer CLI always opened Remote Serve with a fragment token.
- Reused v1.21.4 Serve processes only understand query-token bootstrap, so reconnects returned 401 after a local CLI upgrade.

Root cause:
- Remote bootstrap intentionally reuses a healthy detached process without negotiating its browser authentication capability.

Fix:
- Probe the forwarded /auth/token endpoint without sending the secret.
- Keep fragment-token URLs for current Serve and fall back to the legacy query bootstrap when the endpoint is unavailable or incompatible.
- Remove the external implementation reference from the instance registry comment.

Verification:
- go test ./internal/cli -run TestRemoteServeBrowserURL -count=1
- go test -race ./internal/cli ./internal/serve -count=1
- go test ./... -count=1
- go vet ./...
- go run ./tools/repolint
- ./scripts/cache-guard.sh
- make build
- npm --prefix site test
- npm --prefix site run build
- git diff --check
2026-08-10 02:43:50 +08:00
SivanCola cadcc7a39a fix(cli): satisfy Web static analysis
Problem:
- CI lint rejected one secondary error formatting path and an index-only loop.

Root cause:
- The readiness error preserved the server error with a non-wrapping verb, and the registry retry loop used the pre-Go-1.22 integer form.

Fix:
- Wrap both readiness errors and use integer range syntax.

Verification:
- golangci-lint run --timeout=5m ./internal/cli/... (0 issues)
- focused CLI tests
- go run ./tools/repolint
2026-08-10 02:43:50 +08:00
SivanCola f8b562edd6 fix(cli): retry occupied Web ports on Windows
Problem:
- Windows CI failed Web port retry and Unix permission-bit assertions.

Root cause:
- Windows reports an occupied socket as WSAEADDRINUSE, while the shared retry loop only recognized Unix EADDRINUSE.
- Windows file modes do not expose Unix 0700/0600 permission bits.

Fix:
- Detect address-in-use errors in platform-specific process files.
- Keep permission assertions on platforms that implement Unix mode bits while retaining restricted creation modes and user-directory ACLs on Windows.

Verification:
- focused tests repeated 10 times
- focused race test
- go run ./tools/repolint
- Windows amd64 test-binary cross-compile
2026-08-10 02:43:50 +08:00
SivanCola 9416069ca3 refactor(cli): keep Web handoff within lint budgets
Problem:
- The initial Web handoff implementation extended already-baselined CLI and TUI files beyond repository size and complexity budgets.

Root cause:
- Browser lifecycle, frontend startup, and new tests were placed beside legacy monolithic command paths.

Fix:
- Extract Web handoff state, browser readiness, Serve frontend lifecycle, and focused tests into bounded files.
- Preserve reasonix serve and reasonix web behavior without updating the repolint baseline.

Verification:
- go test ./... -count=1
- focused go test -race for CLI and Serve Web paths
- go vet ./internal/cli ./internal/serve
- go run ./tools/repolint
- Windows amd64 cross-compile for internal/cli
- make build
2026-08-10 02:43:50 +08:00
SivanCola a90c4e12ff feat(cli): add browser web handoff
Problem:
- The CLI exposed Serve but lacked a user-facing browser command and an in-session Web handoff.
- Token links, port collisions, and fresh-session handoff made local Web startup brittle.

Root cause:
- Browser launch lifecycle was not separated from supervised Serve, and empty TUI sessions had no materialized transcript to resume.

Fix:
- Add reasonix web and /web with ready-gated browser opening, fragment-token bootstrap, deterministic port retry, and a per-instance registry.
- Preserve fresh and persisted session identity across the handoff while keeping reasonix serve backward compatible.
- Update CLI help plus English, Chinese, and website documentation.

Verification:
- go test ./... -count=1
- focused go test -race for CLI and Serve Web paths
- Windows amd64 cross-compile for internal/cli
- go vet ./internal/cli ./internal/serve
- make build
- site npm test and Astro build
2026-08-10 02:43:50 +08:00
SivanCola e3841cff9e docs(site): align extension guide with manifest v2
Problem: the public docs page still labeled the extension runtime and reference as Manifest v1 and omitted the explicit native v2 capability boundary.

Fix: update the bilingual site copy and v2 anchors, and extend the documentation contract regression.

Verification: cd site && pnpm test; cd site && pnpm build; local in-app browser verification of rendered copy, anchors, and console.
2026-08-10 02:41:42 +08:00
SivanCola 3e5eade4a9 merge(main-v2): integrate sidecar durability
Problem: PR #8051 advanced the session sidecar durability and locking contract on main-v2, leaving this branch conflicted in desktop/sessions.go.

Resolution: merge current main-v2 and keep its AtomicWriteFile implementation, split local/external lock budgets, and regression suite while preserving this branch’s independent shell and session-listing work.

Verification: cd desktop && go test -race . -run 'TestSessionSidecarSavesUseDurableAtomicWrite|TestSetSessionTitle|TestRecordSessionDisplaySerializesConcurrentTabs|TestRecordSessionPlannerDisplayConcurrentPreservesEverySession|TestRecordSessionDisplayExternalLockBudget' -count=1; git diff --check origin/main-v2
2026-08-10 02:27:41 +08:00
SivanCola 457c010fb9 fix(desktop): keep latest request TPS truthful
Problem:
Slow or unmeasurable provider requests could leave the status bar showing an older request or completed-turn throughput.

Root cause:
The reducer rounded TPS before validating it and reused the previous value when the rounded rate was zero or no provider-output interval existed. Tests also modeled full tool dispatch before usage instead of the runtime order.

Fix:
- retain raw positive request rates so sub-one TPS renders as <1 t/s
- distinguish no request yet from an unmeasurable latest request
- clear stale throughput for short and final-only tool requests
- cover usage-before-full-tool-dispatch and slow-request ordering

Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/use-controller-stream-progress.test.ts
- pnpm --dir desktop/frontend exec tsx src/__tests__/statusbar-workspace.test.tsx
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- pnpm --dir desktop/frontend test:remote
- go run ./tools/repolint
- git diff --check

Co-authored-by: SivanCola <[email protected]>
2026-08-10 02:26:18 +08:00
SivanCola 8fec83cae4 merge: sync PR #8099 with main-v2
Problem:
The PR branch was behind the latest main-v2 session durability changes.

Root cause:
The base branch advanced after the previous PR update.

Fix:
Merge the current main-v2 tip without rewriting contributor history.

Verification:
- merge completed without conflicts
- TPS-focused frontend tests run on the integrated tree
2026-08-10 02:26:18 +08:00
SivanCola 7805d07511 fix(desktop): fence opener preference on successful launches
Problem:
A newer failed opener launch could cancel an older successful selection before its preference write, while an already-started older write still completed. The final preference therefore depended on async timing.

Root cause:
The shared preference coordinator advanced its ordering fence when a menu item was clicked instead of when that launch succeeded.

Fix:
Track issued intents separately from the latest successfully launched intent. Serialize writes against the successful fence so failed launches do not participate and newer successful choices still win. Add deterministic regressions for pending launches and in-flight writes followed by a newer failure.

Verification:
- pnpm exec tsx src/__tests__/external-opener.test.tsx (30/30)
- pnpm test:typecheck
- pnpm build
- go run ./tools/repolint
2026-08-10 02:24:36 +08:00
SivanCola 1687729ca9 Keep shared-window hardening within repo budgets
Problem:
The first repair revision exceeded the existing run-loop file-size ratchet and introduced two overlong comment blocks, failing the repository standards job.

Root cause:
The estimated-usage admission explanation lived at the call site instead of the output-budget owner, and the calibration rationale used essay-style comments.

Fix:
Move the provider-usage calibration gate into output_budget.go and condense the comments without changing behavior or widening repolint baselines.

Verification:
- go run ./tools/repolint
- go test ./internal/agent -count=1
- focused go test -race ./internal/agent
- git diff --check
2026-08-10 02:20:30 +08:00
SivanCola 6b9c0659c3 Merge main-v2 into shared-window budget fix
Integration:
Bring the PR onto the latest main-v2 baseline without rewriting the contributor branch history. Preserve the shared-window calibration fixes while incorporating the current run-loop and Desktop changes.

Verification:
Run the focused, race, cache, root, vet, and Desktop suites on the resulting merge tree before publication.
2026-08-10 02:15:31 +08:00
SivanCola a4086d28da style(desktop): modernize archive rollback iteration
Use slices.Backward for reverse ownership rollback so the desktop package satisfies the Go 1.26 modernize lint gate.
2026-08-10 02:15:24 +08:00
SivanCola 19603ef8e5 Harden shared-window calibration boundaries
Problem:
Shared-window output admission could trust a stale low-density calibration after CJK or replay-shape growth, reconstructed usage could replace provider telemetry, and fork capture erased output-budget capabilities.

Root cause:
Calibration tracked only aggregate bytes, the usage store did not distinguish estimated records, and the experiment wrapper exposed only the base Provider methods.

Fix:
Track conservative replay text and CJK composition, price only excess CJK density with the cold safety floor, exclude adapter-only calibration bytes, ignore estimated usage for calibration, and forward shared-window output capabilities through fork capture.

Verification:
- go test ./internal/agent ./internal/provider/... -count=1
- go test -race ./internal/agent -run shared-window-focused-regressions -count=1
- ./scripts/cache-guard.sh
- git diff --check
2026-08-10 02:15:18 +08:00
SivanCola b7640ca095 fix(desktop): make topic archive cleanup transactional
Problem:
- A failed topic archive could publish delete markers before obtaining cross-process removal ownership, then trash the session on a later reconciliation.
- Snapshot-conflict diagnostics logged the raw error, including the local session path.

Root cause:
- Runtime leases had to be released before removal guards could be acquired, leaving no atomic ownership handoff or reversible pre-commit state.
- Post-marker cleanup errors were reported as aborted even though durable deletion intent remained active.

Fix:
- Convert live session leases directly into removal guards without unlocking, and restore the same owner generation when pre-commit validation fails.
- Acquire removal ownership for every topic session before publishing cleanup markers, preserve diverged trash copies, and let committed cleanup converge through reconciliation.
- Log only the safe snapshot-conflict kind and keep cleanup diagnostics value-free.

Verification:
- go test ./... -count=1 -timeout=300s
- cd desktop && go test ./... -count=1 -timeout=300s
- go test -race ./internal/agent -count=1 -timeout=300s
- cd desktop && go test -race . -run 'TestTrashTopic|TestTopicArchiveOwnershipRollback|TestSnapshotTopicRuntimeConflictLog' -count=1 -timeout=180s
- GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -exec=true ./internal/agent
- GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -exec=true .
- cd desktop/frontend && pnpm build
- ./scripts/cache-guard.sh
- go run ./tools/repolint
2026-08-10 02:11:55 +08:00
SivanCola 53fb380d12 Merge pull request #8051 from SivanCola/fix/session-sidecar-durability
fix(desktop): make session sidecars durable / 持久化并串行化会话 sidecar
2026-08-10 02:06:00 +08:00
SivanCola 8cc7279c86 refactor(plugin): keep compatibility changes within lint budgets
Move Claude root-instruction helpers out of the oversized parser file and keep marketplace fixtures line-neutral while preserving the official skill-based coverage.

Verification: go run ./tools/repolint; go test ./...; ./scripts/cache-guard.sh.
2026-08-10 02:03:33 +08:00
SivanCola df7d060929 Merge remote-tracking branch 'origin/main-v2' into fix/pr8099-request-tps 2026-08-10 02:00:45 +08:00
SivanCola b13f45d886 fix(desktop): scope request TPS to latest attempt
Problem:
Sampling retries could display billable aggregate output tokens against only the final attempt's duration, overstating the latest request TPS. The PR also exceeded existing repolint file-size budgets.

Root cause:
The reducer ignored the latest-attempt Context* token shape when calculating per-request TPS, and the new state flow added too many lines to already-baselined files.

Fix:
- prefer contextCompletionTokens when latest-attempt context is present
- retain completionTokens fallback for legacy usage payloads
- add deterministic sampling-recovery coverage
- compact equivalent TPS state handling without widening baselines

Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/use-controller-stream-progress.test.ts
- pnpm --dir desktop/frontend exec tsx src/__tests__/statusbar-workspace.test.tsx
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend exec eslint src/lib/useController.ts src/components/StatusBar.tsx src/__tests__/use-controller-stream-progress.test.ts
- go run ./tools/repolint
- git diff --check

Co-authored-by: SivanCola <[email protected]>
2026-08-10 02:00:35 +08:00
SivanCola 1c6776d9d9 fix(plugin): align Claude root instructions with upstream
Problem: Reasonix synthesized a SessionStart context hook from a Claude plugin root CLAUDE.md, although Claude Code does not load that file as plugin project context.

Root cause: Claude manifests shared the Codex compatibility helper that always appended root instructions.

Fix: keep supported Claude convention imports without the synthetic root hook, document the distinction, and update install-source fixtures to use official skills.

Verification: go test ./...; ./scripts/cache-guard.sh; focused plugin and install-source tests; desktop plugin/theme tests.
2026-08-10 01:59:34 +08:00
SivanCola f32cff0e33 Merge origin/main-v2 into fix/global-workspace-external-opener
Resolve the bridge contract overlap by preserving both ResolveWorkspacePathForTab and ExternalOpenersForTab.
2026-08-10 01:57:46 +08:00
SivanCola 577023f8c3 fix(desktop): serialize opener preference writes
Problem:
Switching tabs during an external-opener launch could let an older completion overwrite the latest global opener preference.

Root cause:
Keyed ExternalOpener instances had independent busy guards, while preference writes were neither ordered across instances nor fenced by user intent.

Fix:
Coordinate selection intents per bridge, serialize preference writes, skip stale completions, and reassert the latest successful choice even when it matches the discovered preference. Add a deterministic cross-tab regression.

Verification:
- pnpm exec tsx src/__tests__/external-opener.test.tsx
- pnpm test:typecheck
- pnpm build
- go run ./tools/repolint
2026-08-10 01:56:34 +08:00
SivanCola e3c5affab9 Merge pull request #8065 from SivanCola/fix/disable-composer-spellcheck
Disable composer spellcheck / 禁用输入框拼写检查
2026-08-10 01:53:01 +08:00
SivanCola 95b5ef985c Merge pull request #8076 from SivanCola/feature/workspace-copy-paths
feat(desktop): add workspace path copy actions / 桌面端新增工作区路径复制操作
2026-08-10 01:52:37 +08:00
SivanCola ec5244584d Merge origin/main-v2 into fix/desktop-archive-reliability
Integrate the latest compact SSH status bar baseline before repairing archive ownership and diagnostics.
2026-08-10 01:52:30 +08:00
esengine 3cc97a0893 fix(event): forward the delegation audit through every sink wrapper
Running the benchmark for real found what three layers of unit tests missed:
the delegation counters never reached any metrics sink. A probe through the
call-context sink passed, a probe through TaskTool passed, a probe through the
CLI sink chain passed — and the real binary still reported no delegation on a
run whose own transcript showed a child agent doing the work.

Optional sink capabilities are forwarded by hand at every wrapper, and two of
them were missing: control's goal-usage tee and the stats recorder. Each
forwards seven other audits and silently dropped the eighth. That is the same
failure this branch has been removing everywhere else — a contract implemented
in many places, missed in one — moved into the sink layer.

A guard now asserts that every wrapper forwarding one audit forwards them all,
so the next optional capability cannot die at whichever wrapper forgot it. It
is a hand-listed set of wrappers, which is weaker than discovering them, but it
would have caught both of these.

SPEC 3.17's control axis is corrected against what the run actually showed:
--ablate subagent removes task, read_only_task, fleet and parallel_tasks, but a
run can still delegate through a runAs=subagent profile skill, and a measured
no-subagent arm spent a child run on explore. That arm is "no task-tool
delegation", not "single agent", and subagent_runs is what says which happened.
2026-08-09 10:50:53 -07:00
SivanCola ea57e9bf35 Merge pull request #8088 from SivanCola/feature/compact-ssh-statusbar
Compact idle SSH status / 精简空闲 SSH 状态栏
2026-08-10 01:49:11 +08:00
HaoyueQin 1970a6a16c fix(desktop): refresh status bar TPS after each provider request
PR #6931 computed the status bar throughput from completed-turn
telemetry, so during long tool-calling turns the value stayed stale
until turn_done — while the tooltip copy shipped with that PR already
claimed per-request throughput. Pair each executor usage event with the
provider-output interval it closes: message/tool_dispatch close the
interval first, so the closed duration is stashed in
pendingRequestModelMs until the usage event pairs it with completion
tokens. Non-executor sources (subagent/planner/title) neither compute
nor consume the pending value, and turn boundaries reset it to prevent
cross-turn pairing.
2026-08-10 01:36:28 +08:00
esengine 1c0c01bbb6 feat(e2ebench): report what delegation cost against what it produced
The delegation counters landed last commit but nothing surfaced them, so an arm
comparison still could not answer whether extra agents produced verified work
or only tokens.

I went looking for where to build a benchmark harness and found one: cmd/e2ebench
is 11k lines with a task corpus, verify scripts, --ablate arms, comparison,
pareto and HTML reporting already in place. Building a second harness next to it
would have been the exact duplication this branch has spent nine commits
removing, so this extends it instead: the harness metrics mirror gains the new
fields, and one report section joins them to the outcome the harness already
grades.

The section prints the work split (parent versus child tool calls), duplicated
files, how much of the run closed with a checkable claim rather than prose, the
claims the host refused, and write-scope escapes. A single-agent arm renders
"none — n/m solved by a single agent" rather than an empty section, because a
blank section reads as "not measured" when it means "no delegation happened",
and a clean delegated arm prints no alarm lines it has no evidence for.

Two fields I added were already there under the same JSON keys — tool_calls and
subagent_tool_calls — and are dropped rather than shadowed. That is the second
time this pair has caught a duplicate counter, which is a fair argument that the
metrics mirror should be generated from cli.RunMetrics rather than hand-kept.
2026-08-09 10:10:53 -07:00
esengine 73fee3aa0a feat(cli): record per-run delegation counters so arms can be compared
Eight commits of orchestration machinery have landed on this branch without
anything measuring whether delegation pays. More agents always cost more
tokens, and the extra tokens alone can look like an improvement, so the
comparison has to hold the model fixed and read host-recorded facts.

Each finished child now emits a DelegationAudit through the same optional-sink
path the readiness audit uses, and run metrics fold it into counters: how many
children ran and at what depth, how many mutations they made, how many files
two children both touched, how many runs ended in a checkable claim versus
prose, how many claims the host refused to back, and how many writes escaped a
declared scope.

false_completions is the counter that matters. It comes from the completion
adjudication, so it measures claims the host refused rather than a reviewer's
opinion — the difference between "the fleet finished faster" and "the fleet
said it finished".

Emitting from one place is only possible because child construction converged
on RunProfileSpec; before that this would have needed the same emission in
five paths, and would have been missed in some.

Two things were deliberately not added rather than approximated. The audit
records the adjudicated status and the count of refused criteria, not an
inferred "claimed" status the host never independently observed. And the
counter for child tool calls is the existing SubagentToolCalls, counted from
tool events, instead of a second field counting receipts under the same name.

Task success rate and rework-after-handoff stay unmeasured: they need a task
corpus with checkers and whole-run mutation ordering, which belong to the
harness driving the arms, not to the instrument recording one run. SPEC 3.17
states what is and is not measured, and that --ablate subagent is the existing
control axis for the single-agent arm.
2026-08-09 10:01:12 -07:00
SivanCola b96e3d7938 fix(security): keep fallback diagnostics value-free
Runtime construction errors can carry provider configuration details, including API key environment metadata. Record only the recovery phase so archive fallback failures cannot flow sensitive configuration into Desktop logs.
2026-08-10 00:57:17 +08:00
esengine 1bc8aea61c feat(agent): serialize MCP calls to servers that carry session state
Sub-agents share one session Host and its connections. For a stdio server that
means they share one process, and read-only does not imply stateless: a browser
server opens a page, selects a tab, scrolls, and every one of those tools may
honestly declare readOnly because nothing reaches the filesystem. Two children
calling it concurrently still interleave on state neither can see, and write
claims cannot help because there is nothing to claim.

A configured server now carries a concurrency policy. serial means the runtime
never runs two calls to it at once across the whole session, whichever child
issues them. The gate lives on the shared runtime because the process being
interleaved on is shared at exactly that scope, and a queued call honours its
own cancellation rather than pinning the session behind a stuck server.
Servers whose names read as known-stateful (browser, playwright, puppeteer,
chrome, chromium, selenium) default to serial; explicit config always wins and
everything else stays parallel, so the shared-Host tradeoff is unchanged.

This is the conservative first version on purpose: one policy per server, not
per capability. Per-tool parallel_safe/exclusive hints and concurrency_key
grouping wait until real servers show which tools within one server differ.

Both config render scopes emit the field, with a round-trip test, because a
safety policy silently dropped on save is worse than one never offered — the
same failure as a profile editor losing read-only. The two scopes now share one
renderPluginPolicy helper so a future policy cannot be added to just one.

Baseline: PluginEntry moves to its own file and the shared-state helpers move
out of usecapability.go, both of which were 2-3x the size ceiling. Every
baseline total drops; the single increase is plugin_entry.go essay 0 -> 9,
which is the pre-existing field comments carried through the extraction while
config.go's essay falls by 10.
2026-08-09 09:54:41 -07:00
SivanCola 25a21871a3 fix(ci): preserve project tree size budget
Keep the archive lifecycle extraction within the existing ProjectTree file-size ratchet after merging the latest main-v2 baseline. No repolint baseline update is required.
2026-08-10 00:52:01 +08:00
SivanCola 900767d7f2 Merge origin/main-v2 into fix/desktop-archive-reliability
Resolve the ApprovalModal overlap by using the shared native motion helper from PR #8080 and remove the superseded local helper.

Also make the bounded legacy-wait regression test explicitly join its released waiter so the full Desktop test suite cannot inherit a test goroutine.
2026-08-10 00:50:23 +08:00
SivanCola ecddf4802f chore(desktop): keep status bar within size budget
Problem:
The compact SSH status change pushed StatusBar.tsx beyond its existing repolint file-size budget.

Root cause:
Equivalent status label and icon expressions were expanded across several lines in an already baselined file.

Fix:
Keep the same state-dependent rendering while expressing the small conditions compactly, without widening the repository baseline.

Verification:
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/statusbar-workspace.test.tsx
- pnpm typecheck
- pnpm exec eslint src/components/StatusBar.tsx
2026-08-10 00:44:13 +08:00
esengine a6804d3afa refactor(agent): fold read_only_task into the one child-construction path
Five entry points were building sub-agents outside TaskTool.RunProfileSpec:
read_only_task, boot's two skill runners, reasonix review, and desktop preview.
Each re-resolved tools, depth, and permissions on its own, which is how a
boundary added in one place gets missed in another — the same shape as the
preview path that once built unconfined file tools and the profile editor that
once dropped read-only on save. It already bit this branch: boot's writer skill
runner receives complete_subtask in its registry but not the closing contract,
because the contract is appended on the TaskTool path it bypasses.

read_only_task, the one fork inside package agent, now compiles to a spec and
runs through RunProfileSpec like every other in-package entry point. Its promise
of no durable side effects is preserved as ContextRequest.Ephemeral — a field on
the spec rather than a second construction path — so the convergence carries no
behaviour change, which is the only safe way to do this kind of refactor.

spawn_boundary_test.go enumerates the files still calling the low-level runners
and fails on any new one, with the fix spelled out in the failure. The three
remaining callers are recorded as known debt rather than precedent; folding them
in touches boot, cli, and the desktop module and belongs in its own change.

SPEC 3.15 states the rule: many APIs, one execution primitive.
2026-08-09 09:42:03 -07:00
SivanCola 20326ae01b feat(desktop): compact idle SSH status
Problem:
The SSH status bar chip always showed its full disconnected summary and a switcher chevron whenever a host was configured, consuming scarce horizontal space while idle.

Root cause:
The trigger reused the accessible connection summary as its visible text in every state and had no responsive idle presentation.

Fix:
- show a compact SSH label for cleanly disconnected hosts
- show a status dot and host name for connected hosts
- retain explicit connecting, degraded, and failure summaries
- collapse the idle label below 820px while preserving a 24px target and full accessible labels
- cover the state-specific rendering with regression tests

Verification:
- pnpm test:remote
- pnpm typecheck
- pnpm check:css
- pnpm exec eslint src/components/StatusBar.tsx src/__tests__/statusbar-workspace.test.tsx
- in-app browser DOM, responsive, popover, and connect/disconnect checks
2026-08-10 00:41:41 +08:00
SivanCola a85df817cd refactor(desktop): extract archive lifecycle helpers
Problem:
- The archive reliability fix added lines to several ratcheted legacy files, causing repolint to reject the PR.

Root cause:
- Runtime teardown, archive transaction, animation, state, and regression helpers were implemented inline in files already above the repository size budgets.

Fix:
- Move each new owner-level concern into a focused Go or TypeScript module.
- Split archive transaction phases so every function remains within the function-size budget.
- Preserve the existing lock order, cleanup markers, generation checks, and UI behavior without widening lint baselines.

Verification:
- go run ./tools/repolint
- go test ./internal/control -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race . -run TrashTopic\|WaitDestroyHandles\|FriendlySessionFileError -count=1
- cd desktop/frontend && pnpm build
- Focused frontend tests and local browser click verification
2026-08-10 00:37:45 +08:00
SivanCola 5620eb6465 chore(desktop): satisfy repository standards
Problem:
The opener capability change exceeded the repository's ratcheted comment and file-size budgets.

Root cause:
A four-line explanatory comment and additive declarations in two already-baselined frontend files introduced new repolint findings.

Fix:
Keep the same contracts and behavior while expressing the comment and declarations within their existing line budgets. The repolint baseline remains unchanged.

Verification:
- go run ./tools/repolint
- go test . -run 'Test(ExternalOpener|ResolveExternalOpener|SetPreferredExternalOpener)' -count=1
- pnpm exec tsx src/__tests__/external-opener.test.tsx
- pnpm test:typecheck
2026-08-10 00:36:28 +08:00
esengine 3b9dff68de feat(agent): make fleet a small dependency graph
A fleet item may now declare id and depends_on. That is the entire graph
vocabulary — no conditions, no expressions, no dynamic fan-out — and it is
enough for research → implement backend/frontend → integration → review.

Dependencies live on the plan, never on a task spec. ProfileExecSpec is
unchanged, which is what keeps depends_on from becoming the first keyword of a
workflow language: what a task is does not depend on what ran before it.

Everything is validated before anything starts, alongside the existing write
claim preflight — duplicate ids, ids no task declares, self-edges, and cycles
all fail the whole call, because a fleet that cannot finish has already spent
tokens by the time it discovers that.

The graph also relaxes the write-claim preflight where it should: only items
that can actually run at the same time need disjoint write_paths. An
implement → review pair is serialised by its edge and may share paths, which a
flat fleet had no way to express.

Failure handling stays at one knob. A failed or skipped task always skips its
whole downstream branch — a dependent on a broken input only produces a result
the parent must discard. Independent branches keep going unless fail_fast is
set, which stops starting new tasks while leaving running ones to finish, so a
writer is never abandoned mid-write.

Cache-impact: the fleet schema and description are in the parent prefix, so
this is a one-time invalidation. The regenerated golden baseline puts it at
+273 tool-schema tokens (13801 → 14074); SystemHash is unchanged.
2026-08-09 09:32:33 -07:00
SivanCola 710f3fb107 Merge pull request #8086 from esengine/release-notes/v1.21.5
docs(release): 准备 v1.21.5 中英更新日志
2026-08-10 00:32:07 +08:00
github-actions[bot] 3a0e33a8e5 docs(release): prepare v1.21.5 notes
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.

Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
2026-08-09 16:30:50 +00:00
SivanCola c561fcd4e4 fix(desktop): expose opener for Global workspaces
Problem:
- Global tabs with valid local workspaces could not use the header external-opener control because the renderer treated project scope as the capability signal.
- The split control also retained taller geometry and oversized artwork than adjacent topic-bar actions.

Root cause:
- Mounting was hard-coded to scope === project, while the backend exposed no tab-scoped workspace capability.
- The control used a standalone 32px recipe after the surrounding toolbar had moved to 28px controls.

Fix:
- Add a tab-scoped opener discovery contract backed by the same validated workspace path used for launches.
- Render by backend capability, fence async discovery by tab id, and keep unavailable tabs hidden.
- Align the split control to the shared 28px / 7px topic-bar geometry and use 18px Workbench artwork while preserving Creation and menu sizing.

Verification:
- Desktop Go tests and focused race coverage passed.
- External-opener frontend tests, CSS checks, test typecheck, production build, bundle budgets, and live browser DOM/click checks passed.
2026-08-10 00:29:09 +08:00
SivanCola e8f9fa3192 Merge pull request #8080 from SivanCola/fix/desktop-approval-easing
Fix WebView2 animation crashes / 修复 WebView2 动画崩溃
2026-08-10 00:27:11 +08:00
SivanCola c6023144d9 fix(desktop): make topic archive failure-safe
Problem:
- Topic archive and approval confirmation could appear unresponsive.
- Archive teardown could block the UI or detach a runtime before durable cleanup was guaranteed.

Root cause:
- The approval exit animation passed a GSAP easing token to Web Animations.
- Archive used unbounded runtime locks and teardown waits, removed bindings before snapshot completion, and shared one frontend pending flag across every topic.

Fix:
- Convert approval easing to valid Web Animations values and complete actions exactly once.
- Snapshot before detach, validate the captured runtime generation, publish compatible cleanup markers, bound interactive teardown, and fall back to delayed cleanup.
- Return retryable busy errors for contended mutations and track archive progress per topic.

Verification:
- cd desktop && go test ./...
- go test ./internal/control
- cd desktop && go test -race . -run TrashTopic\|WaitDestroyHandles\|FriendlySessionFileError -count=1
- cd desktop/frontend && pnpm build
- Focused frontend tests, typecheck, and local browser click verification
2026-08-10 00:25:42 +08:00
SivanCola 0262190972 refactor(desktop): preserve repository size budgets
Problem:
- The first PR revision added lines to two components already governed by repolint file-size ratchets.

Root cause:
- Reusable animation completion and transcript entrance identity logic remained in the oversized component files.

Fix:
- Move fail-open exit animation handling into the shared motion module.
- Move model-backed transcript entrance setup into the entrance hook module.

Verification:
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/native-motion.test.tsx
- pnpm exec tsx src/__tests__/approval-animation.test.tsx
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm build
2026-08-10 00:18:34 +08:00
SivanCola 601724c8ac feat(desktop): add workspace path copy actions
Problem:
The workspace file-tree menu could add or open entries, but users could not
copy their workspace-relative or local absolute paths.

Root cause:
The menu had no clipboard actions, and the renderer only knew reference tokens,
which cannot safely identify absolute paths for authorized external folders.

Fix:
- add relative-path and absolute-path copy actions with localized labels
- resolve absolute paths through the owning tab's guarded backend resolver
- preserve clipboard fallbacks and reject missing-tab or escaping paths
- isolate new behavior so baselined large files do not grow

Verification:
- cd desktop && go test ./...
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/workspace-changes-errors.test.tsx
- pnpm exec tsx src/__tests__/workspace-path-copy.test.tsx
- pnpm build
2026-08-10 00:18:31 +08:00
esengine 85cacad3b1 feat(agent): record an explicit context capsule for every sub-agent run
What a child sees was previously answerable only by reading the delegation
path. Investigating it turned up a fact worth stating outright: a child sees no
standing instructions at all. Its system prompt is DefaultTaskSystemPrompt or
the profile body and nothing is composed into it, the memory queue is disabled
so it cannot even record memory, and the parent conversation, Goal, planner
output, and sibling results are never passed. Isolation is total, not partial.

Each run now records a ContextCapsule in its transcript sidecar: workspace,
system-prompt source and hash, resolved tool scope and schema hash, model and
effort, parent session and tool-call id, any resumed transcript, and an
inherited block whose fields are all false. capsuleHash is its stable identity,
so "why did this reviewer not see that constraint" is answered from the record
and two runs that behaved differently can be diffed. The capsule holds
references and digests only — copying parent context would cost tokens and
break the child prefix, which is the thing isolation was protecting.

The spec member that says what a child should start from is renamed
ContextRequest, freeing ContextCapsule for the resolved manifest. metaFromSpec
moves next to the capsule it now builds.

SPEC 3.13 tabulates what is and is not inherited. The non-inheritance is
asserted in a test, so a future change that starts inheriting something has to
flip the flag and update the table in the same commit.
2026-08-09 09:18:17 -07:00
SivanCola b07f6127be fix(desktop): harden native motion fallbacks
Problem:
- A WebView2 client could enter the fatal error surface after an approval because a non-CSS easing token reached Element.animate.
- Shared collapse and entrance effects had no reliable fail-open behavior, and transcript resets could animate restored virtual rows.

Root cause:
- GSAP easing vocabulary and legacy helpers remained mixed with the native Web Animations API.
- Entrance identity depended on a changing transcript boundary and DOM timing, while terminal cleanup depended on transitionend delivery.

Fix:
- Replace GSAP helpers and dependencies with shared CSS motion tokens and guarded native animation hooks.
- Make approval, collapse, and entrance completion idempotent and fail open on unsupported or rejected animations.
- Seed transcript entrance identity from stable model IDs and bound terminal close cleanup with a timer.

Verification:
- pnpm exec tsx src/__tests__/native-motion.test.tsx
- pnpm exec tsx src/__tests__/approval-animation.test.tsx
- pnpm build
- pnpm test:remote
- pnpm test:performance
- Installed Microsoft Edge production-mock regression checks
- Full frontend runner retained only the same six failures reproduced on the clean base
2026-08-10 00:15:06 +08:00
esengine 590c4062e8 refactor(agent): name and guard the profile boundary
A profile describes a worker, not a run. The boundary that keeps it that way
already existed structurally — Skill is the fat storage format, ProfileDefinition
is the narrow view delegation sees — but it had no name, no stated criterion,
and nothing watching it, so it would have eroded one "just this one field" at a
time.

The narrowing is now agent.ProfileFromSkill, a single named projection sitting
next to the type it narrows to, documenting why routing metadata (triggers,
auto-use, cost, freshness) stops there: it decides when a worker is chosen, not
how that worker thinks. boot keeps the store lookup and calls it.

profile_boundary_test.go guards both directions. It pins the field set of
ProfileDefinition and of all five delegation spec members, failing with the
criterion spelled out — a field belongs to the member that decides its value,
so max_turns, write_paths, and retry or verification policy can never enter a
profile. It also asserts the projection populates every identity field, so the
opposite failure (declared on both types, silently never wired) is caught too.

REASONIX.md and SPEC 3.10 state the rule. Skill frontmatter stays free to grow;
only the projection is fixed.

Cache note: REASONIX.md is part of the system-prompt prefix, so this changes it
once. No tool schema or provider-visible tool list changes.
2026-08-09 09:08:40 -07:00
SivanCola bd97fecc5b fix(plugin): make native v2 resources explicit
Problem: Native v2 manifests could silently import Claude compatibility sidecars, including repository instructions and executable hooks.

Root cause: parseNativeV2 applied Claude compatibility discovery after validating the explicit manifest.

Fix: Treat the native v2 manifest as the complete capability boundary while preserving compatibility discovery for Codex and Claude manifests. Document the contract and cover it with a regression test.

Verification: go test ./...; cd desktop && go test . -run "Plugin|Theme" -count=1.
2026-08-10 00:08:02 +08:00
esengine 27ba95010f feat(agent): close sub-agent runs with a host-adjudicated completion claim
A delegated run used to end in prose, which is not an interface a parent can
verify. Writer sub-agents now carry complete_subtask: they submit a status, a
summary, the acceptance criteria they were held to with the command or paths
proving each, and whatever they left unresolved.

The submitted status is a claim, not a verdict. The host checks every citation
against its own ledger before the parent sees it — a verification criterion
must name a command the host recorded as run, diff/files must name paths it
observed touched, and a manual note never backs itself. Unbacked criteria are
lowered to unsatisfied, a report holding one cannot stay complete, and each
downgrade is printed with its reason. The host only ever lowers.

The mechanism mirrors review_report exactly: a subagent-only tool whose call
args are the receipt, adjudicated from the same evidence ledger, so the two
completion contracts stay one design rather than two. complete_subtask joins
complete_step and todo_write in the receipts the parent ledger does not
inherit, since the claim belongs to the child's run.

Sub-agent answers are now composed as adjudicated claim, child prose, host
receipts. SPEC 3.11 documents the contract; the write-claim section moves to
3.12.

Cache note: writer sub-agent registries gain one tool and their task prompt
gains the closing contract, so child prefixes change once. The parent prefix
is untouched.
2026-08-09 09:03:25 -07:00
esengine 1243d2e71f fix(agent): reserve host receipts out of the fleet aggregate budget
Fleet and parallel_tasks previews truncate a child's answer by bytes, which
made the host attestation appended to that answer the first thing a verbose
child could push out. The attestation is bounded and authoritative; the prose
is neither, so they must not compete for the same budget.

The aggregate now splits each answer, reserves the attestation, and truncates
only the prose. When attestations alone would starve every preview they
degrade instead of disappearing, and a claim violation outranks the header it
normally sits under: a parent may lose the detail of what changed, never the
fact that a write left the declared write_paths.
2026-08-09 08:51:34 -07:00
esengine 16b6cbd9c0 feat(agent): host-authored subagent receipts and a typed delegation spec
Three related changes to the sub-agent delegation path.

ProfileExecSpec splits into TaskSpec / WorkerSpec / CapabilityGrant /
ContextCapsule / SchedulerPolicy, so a new field lands in the member that
decides its value rather than whichever one is nearest the call site. The
CapabilityGrant doc states the ceiling-intersection invariant that only
IntersectToolLists implemented before. No behaviour change; tool schemas and
the provider-visible prefix are untouched.

evidence.Receipt now carries the exit status the shell runner actually
observed and the host's verification classification, which were captured in
tool.ShellExecution but dropped before reaching the ledger. Every sub-agent
answer ends with a host-recorded attestation of what it changed and which
commands it ran, attached at RunSubAgentWithSession so task, read_only_task,
fleet, parallel_tasks, background jobs and the boot skill runners all inherit
it. Only mutations and verification/failed commands are rendered, so read-only
research children stay exactly as cheap as before.

A declared write_paths claim is audited after the run: mutations the host
recorded outside the claim are reported to the parent instead of passing
silently. SPEC 3.11 documents every enforcement surface and states plainly
that an omitted claim is a scheduling boundary only, and that declaring paths
costs bash where the OS sandbox cannot enforce write roots. Adversarial tests
pin symlink escape, parent traversal, move_file destinations, unbindable
writers, and the deliberate absence of intra-workspace enforcement for
undeclared writers.

evidence.Receipt moves to its own file: evidence.go had no headroom under the
repolint ceiling, and at 2896 lines the split was overdue regardless.
2026-08-09 08:47:05 -07:00
erphenheimer 0a21f96f0f fix(windows): repair legacy root-level shortcut icon and target 2026-08-09 23:21:23 +08:00
SivanCola b41b4ac6ed Merge main-v2 into fix/session-inbox-durability 2026-08-09 23:11:15 +08:00
esengine eee45d2a35 ci(wails): read the pin from the workspace root, not the job's cwd
The first version failed exactly where it should have been checked: several
desktop steps run with working-directory: desktop, so `cat .wails-version`
found nothing and the install became wails@"" — "version must not be empty".

The guard checked the workflows' content but not that the path it prescribed
resolves from the directory the job actually runs in, which is why it passed a
change CI rejected. It now refuses a bare `cat .wails-version` too, and that
rule was replayed before committing: relative path fails, $GITHUB_WORKSPACE
passes.

The CLI bump itself is still unproven — the desktop jobs never got far enough
to try it.
2026-08-09 07:55:53 -07:00
esengine 17016c741b ci(wails): one pin for the CLI and the library, because they drifted apart
Dependabot bumped desktop/go.mod to wails v2.13.0. Nothing bumped the CLI: its
version is a string inside three workflow files, where Dependabot cannot see
it. So CI — and the desktop release job — have been building a v2.13.0 library
with a v2.12.0 toolchain.

The drift runs the other way too. `wails dev` rewrites desktop/go.mod down to
whatever CLI the developer has installed, so a local run quietly reverts the
dependency upgrade; that is how this was found, after reverting the same
downgrade several times in one afternoon.

.wails-version is now the single pin, read by the Makefile and all three
workflows, exactly as .golangci-version is. check-wails-pin.sh proves it held
and refuses a workflow that hard-codes a version again — it was written failing
first, and each of the three drift directions was replayed against it: a
downgraded go.mod, a library bumped past the pin, and a version written back
into a workflow.

The pin is v2.13.0, matching the library Dependabot chose. That moves CI's
toolchain forward by a minor version; the desktop, desktop-macos and
desktop-windows jobs are what verify it, and they are the reason this is worth
finding out now rather than at the next release.
2026-08-09 07:44:25 -07:00
YHH 0bf2002f42 Merge pull request #8064 from esengine/perf/project-tree-invalidation
perf(desktop): the project tree is data, not a function of which session is open
2026-08-09 22:37:02 +08:00
YHH 1f43fdc065 Merge pull request #8063 from esengine/perf/session-switch-fanout
perf(desktop): stop asking the backend the same question three times per switch
2026-08-09 22:33:33 +08:00
YHH 2167341319 Merge pull request #8050 from esengine/feat/completion-receipt
feat(completion): deliver the receipt, so the host's account is the one the user reads
2026-08-09 22:33:14 +08:00
SivanCola eae381d854 fix(agent): harden shared-window calibration
Problem:
Shared-window requests could still overflow after a successful calibration because replayed reasoning and other provider-visible fields were excluded. Summary folds also spent the output reserve twice and accepted length-truncated digests.

Root cause:
Usage was paired with content-only message size, summary input budgeting overlapped two independent reserves, and the summarizer treated finish_reason=length as success.

Fix:
Calibrate against the complete provider request while retaining a separate compaction ratio, reserve digest and sampling output independently for shared-window providers, and fail closed on truncated summaries. Add regression coverage for reasoning replay, tool schemas, reserve sizing, truncation, and session reset.

Verification:
- go test ./... -count=1
- go test -race ./internal/agent ./internal/provider/... -count=1
- go vet ./...
- go run ./tools/repolint
- ./scripts/cache-guard.sh
- cd desktop && go test . -run ^TestWorkspaceGitBranchDetachedHead$ -count=1 -v
2026-08-09 22:33:04 +08:00
SivanCola c946c05dac style(control): restore gofmt compliance 2026-08-09 22:32:58 +08:00
YHH 1284c24920 Merge pull request #8047 from esengine/bench/longrun-recovery
bench(longrun): injected failure priced, and the long-session states reached directly
2026-08-09 22:32:53 +08:00
SivanCola 49960a1e53 fix(ci): preserve frontend line budgets
Problem:
Repository lint rejected the spellcheck change because two already-baselined frontend files gained lines beyond their allowed budgets.

Root cause:
The first implementation placed each related DOM attribute and assertion on a separate line.

Fix:
Keep the same rendered attributes and regression coverage while grouping each related set without increasing the affected files line counts. The repolint baseline remains unchanged.

Verification:
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm typecheck
- pnpm exec eslint src/components/Composer.tsx src/components/RichComposerInput.tsx src/__tests__/composer-goal-toggle.test.tsx
- git diff --check
2026-08-09 22:32:05 +08:00
SivanCola a60685bf7f fix(inbox): close durability and admission races 2026-08-09 22:30:29 +08:00
SivanCola 64addf3b28 fix(desktop): disable composer spellcheck
Problem:
Desktop composers exposed browser spellcheck underlines without correction suggestions, leaving a misleading partial editing experience for code-heavy prompts.

Root cause:
The plain textarea and rich contenteditable composer inherited each platform webview spellcheck, autocorrect, and autocapitalization defaults while Reasonix owned the context menu.

Fix:
Disable spellcheck, autocorrect, and autocapitalization on both composer input modes, and add regression assertions for the rendered DOM contract.

Verification:
- pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm exec tsx src/__tests__/composer-context-menu-clipboard.test.tsx
- pnpm typecheck
- pnpm exec eslint src/components/Composer.tsx src/components/RichComposerInput.tsx src/__tests__/composer-goal-toggle.test.tsx
- Playwright Chromium DOM attribute check
2026-08-09 22:28:43 +08:00
esengine 17c873cf02 perf(desktop): the project tree is data, not a function of which session is open
Switching a session re-listed the whole project tree. The cause was a
dependency list, not a decision: ProjectTree's refresh callback closed over the
active scope/topic/session because it also seeded the expanded set from them,
so every switch minted a new callback and the effect watching it refetched.

The tree cannot change because a different session became active. Fetching is
now keyed by the data — refreshSignal, which is what marks it stale — and
following the active topic became a separate effect over the tree already held.
That effect also returns the previous set unchanged when a switch expands
nothing new, so a switch that changes nothing re-renders nothing.

Measured on a 201-topic sidebar, six switches: ListProjectTree went from twice
per switch to once.

The remaining one is not this component's: App lists the entire tree to read a
single number — the active topic's turn count, used only as a fallback while
the transcript still shows zero turns. That wants the count on the tab's own
meta rather than a whole-tree walk, which is a Go, type and App change worth
its own review.
2026-08-09 07:23:24 -07:00
esengine 1133d555c5 perf(desktop): stop asking the backend the same question three times per switch
Measured, not guessed: instrumenting every Wails binding and switching six
sessions on a sidebar of 201 topics showed 19-28 bridge calls and 849-1330ms of
IPC per switch, a third of them duplicates inside the same transition —
ContextPanel x3, ListProjectTree x2 (~150ms each, and it grows with the number
of topics), MetaForTab x2, ListTabs x2-3, BackgroundRuntimes x3.

Independent hooks each ask for the same thing. Deduping at the call sites would
mean threading a cache through a dozen of them and hoping the next hook
remembers; the bridge is already a Proxy, so one place covers every caller and
cannot be forgotten.

Only allowlisted read-only queries coalesce, and only for 200ms — a stale
answer is worse than a slow one, so nothing outlives the burst it collapses. A
rejection is never shared onward: the next caller retries rather than
inheriting a failure. The test for that caught the first version doing exactly
what its own comment promised it did not.

Same six switches after: 16-20 calls, 555-1148ms, duplicates down from ~17 per
six switches to 16 total. The remainder are calls with different arguments or
outside the window, which is the honest floor for this approach; the next win
is not re-listing a 201-topic project tree on a switch that cannot change it.
2026-08-09 07:10:39 -07:00
SivanCola f11321b60d fix(desktop): bound cross-process sidecar lock waits
Problem: the 5-second durability budget also applied to external OS lock retries, so another Reasonix process could stall turn completion before display persistence entered its retry queue.

Root cause: filelock.Acquire used one context for both the in-process writer queue and cross-process flock contention.

Fix: add a split-budget acquisition API, keep the 5-second local fsync queue, restore the 750ms external lock budget, and cover both the generic lock and real display-sidecar subprocess path.

Verification: focused and race tests, both Go module suites, go vet, repolint, Windows cross-compilation, and latest-base merge-tree all pass.
2026-08-09 22:07:12 +08:00
SivanCola 98217b6c90 fix(session): recover cached-zero listings
Problem: a session that an older build cached as zero turns after a transient decode failure stayed hidden even after its content became readable, and every listing decoded it again.

Root cause: the recovery probe discarded successful preview and turn results, while listing metadata could not distinguish an old ambiguous zero from a successfully validated empty session.

Fix: advance the listing-count contract to v2, trust positive v1 counts, revalidate only old zero counts once, retain recovered content, and leave unreadable artifacts unstamped and visible.

Verification: go test ./internal/agent -run TestListSessions -count=1; go test -race ./internal/agent -run TestListSessions -count=1; go test ./... -count=1; go vet ./...; go run ./tools/repolint; git diff --check
2026-08-09 21:54:47 +08:00
SivanCola e8d323d813 feat(session): persist queued inbox instructions
Problem:
Long-running sessions kept queued follow-ups and steer bodies in process memory. A crash or forced exit lost the queue, and frontend-owned dispatch could duplicate or prematurely acknowledge work.

Root cause:
Queue state had no durable transaction boundary or single admission owner. Large prompt bodies were captured by in-memory closures, frozen references could be re-resolved against live files, and frontend lifecycle code raced Controller dispatch.

Fix:
Add a crash-recoverable Session Inbox with atomic blob and manifest commits, capacity limits, idempotency, reference snapshots, quarantine salvage, and metadata-only metrics. Route CLI, Desktop, Bot, ACP, and HTTP through Controller-owned FIFO admission; propagate item IDs through steer and TurnDone; add scoped cancellation and frontend queue CRUD without duplicate submission.

Verification:
- go test ./...
- go test ./... (desktop module)
- go test -race ./internal/sessioninbox ./internal/control ./internal/agent
- pnpm test
- pnpm build
- ./scripts/cache-guard.sh
2026-08-09 21:33:07 +08:00
SivanColaandclearnature d68236de68 fix(agent): bound shared-window output budgets
DeepSeek can reject a request with HTTP 400 when the prompt and configured completion budget each fit independently but exceed the shared context window together.

Clip the final ordinary and summarizer requests against a calibrated prompt estimate, preserve the bounded full-fold compaction path, and reset atomically paired calibration state when sessions change. Independent-ceiling and unknown provider modes remain unchanged.

Co-authored-by: clearnature <[email protected]>
2026-08-09 21:31:46 +08:00
SivanCola 9754fc395f fix(desktop): budget durable sidecar lock bursts
Problem:
Windows CI showed that 32 concurrent display-sidecar writers could exhaust the 750ms lock deadline after sidecar publication gained an fsync.

Root cause:
filelock serializes in-process waiters, but every waiter starts its deadline before entering that queue. Durable Windows writes can therefore consume the full budget of tail writers even without external contention.

Fix:
Use a shared five-second bounded lock budget for title, display, and planner-display sidecars. This keeps contention bounded while accommodating durable burst writes on slower disks.

Verification:
- cd desktop && go test -race . -run 'TestSetSessionTitleSerializesConcurrentUpdates|TestRecordSessionDisplaySerializesConcurrentTabs|TestRecordSessionPlannerDisplayConcurrentPreservesEverySession' -count=10
- cd desktop && go vet ./...
- cd desktop && GOOS=windows CGO_ENABLED=0 go test -c -o /tmp/reasonix-desktop-windows.test.exe .
- go run ./tools/repolint
- git diff --check
2026-08-09 21:28:33 +08:00
esengine fc8312b052 fix(completion): what the first real run showed — noise, and a card that sprawled
Running it against a real provider found two things no test had:

The agent ran the suite, edited, then re-ran with a different command form. The
receipt listed both earlier forms as "verified before the last change" — true,
and useless: a fresh run had already proven the tree. Superseded commands are
now a gap only while nothing fresh has passed. Listing them after a green run
is the pedantry that teaches people to skip receipts.

Real agent commands carry an absolute cd prefix and a shell fallback. Printed
whole they wrapped four times and broke the card's own box — the wall of text
it exists to replace. The cd prefix is dropped (the run is already there) and
the line is clipped to the row. The budget is the row, not the detail: a
per-detail cap that fits English overflows the moment a longer translation
prefixes it, which is exactly what the zh catalogue did.
2026-08-09 06:13:36 -07:00
SivanCola 9527016138 style(shell): adopt Go 1.26 concurrency idioms
Problem: the CI-pinned modernize linter rejected two equivalent legacy forms in the new shell output cap and its concurrency test.

Fix: use max for the non-negative head bound and sync.WaitGroup.Go for test goroutines.

Verification: golangci-lint v2.12.2 run --timeout=5m; go test ./internal/shellrun ./internal/tool/builtin; go test -race ./internal/shellrun; go run ./tools/repolint
2026-08-09 21:05:38 +08:00
SivanCola 781dd38fb8 test(desktop): respect Windows file mode semantics
Problem:
The sidecar durability regression asserted Unix permission bits on Windows, where Go reports regular files as 0666 and access control is governed by Windows ACLs.

Root cause:
The test coupled the cross-platform durable-write contract to a Unix-only os.FileMode representation.

Fix:
Keep the AtomicWriteFile assertion on every platform and restrict the 0600 mode-bit assertion to non-Windows systems.

Verification:
- cd desktop && go test -race . -run 'TestSessionSidecarSavesUseDurableAtomicWrite|TestSetSessionTitle' -count=1
- GOOS=windows CGO_ENABLED=0 go test -c -o /tmp/reasonix-desktop-windows.test.exe .
- go run ./tools/repolint
- git diff --check
2026-08-09 21:04:59 +08:00
SivanCola af2b63fb0a refactor(session): extract listing helpers
Problem: the session safety adaptation pushed internal/agent/save.go beyond its existing repolint file-size budget.

Root cause: new listing helpers were added to an already baselined large persistence file.

Fix: move only the new mapping, artifact probe, and error-aware preview helpers into a focused package file without changing behavior or lint baselines.

Verification: go run ./tools/repolint; go test ./internal/agent; go test -race ./internal/agent; git diff --check
2026-08-09 20:59:08 +08:00
SivanCola 5f74632f03 test(desktop): isolate sidecar durability coverage
Problem:
The new session-sidecar regressions pushed an already large legacy test file further beyond the repository test-file-size budget.

Root cause:
The focused coverage was added beside older session tests instead of in its own owner-level test file.

Fix:
Move the new durability and concurrent-title tests unchanged into session_sidecar_durability_test.go.

Verification:
- go run ./tools/repolint
- cd desktop && go test -race . -run 'TestSessionSidecarSavesUseDurableAtomicWrite|TestSetSessionTitle|TestRecordSessionDisplaySerializesConcurrentTabs|TestRecordSessionPlannerDisplayConcurrentPreservesEverySession' -count=1
- git diff --check
2026-08-09 20:54:29 +08:00
esengine f367862cb7 bench(longrun): the failed-leg test wrote its own stub and skipped the skip
Windows CI failed with "invocations = 0, want 1". The stub was the problem, not
the code: a #!/usr/bin/env bash script cannot be executed there, so the child
never ran, the log was never written, and the assertion blamed runSegments for
a shell that had not started.

fakeAgent already guarded against this; the failed-leg test wrote its own stub
and inherited nothing. The guard is now a named helper both call, so the next
test that writes a stub has one obvious thing to reach for.
2026-08-09 05:51:51 -07:00
SivanColaandipanta 8c7ca1ba31 fix(desktop): make session sidecars durable
Problem:
Desktop title and display sidecars published temporary files without fsync, and concurrent title read-modify-write operations could overwrite each other.

Root cause:
The sidecars duplicated a manual temp-write-close-replace sequence instead of the shared durable atomic writer. Unlike display sidecars, the legacy title map also had no cross-process serialization.

Fix:
Route title, display, and planner-display sidecars through fileutil.AtomicWriteFile, serialize title mutations with a bounded file lock, and cover the durable helper, lock boundary, and concurrent updates.

Verification:
- go test ./internal/fileutil -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race . -run 'TestSessionSidecarSavesUseDurableAtomicWrite|TestSetSessionTitle|TestRecordSessionDisplaySerializesConcurrentTabs|TestRecordSessionPlannerDisplayConcurrentPreservesEverySession' -count=1
- cd desktop && go vet ./...
- git diff --check

Co-authored-by: ipanta <[email protected]>
2026-08-09 20:50:53 +08:00
SivanColaandmyipanta 5e1c054084 fix(session): harden sidecar durability and recovery visibility
Problem: desktop title/display sidecars could be renamed without flushing their temporary files, and malformed non-empty sessions could be cached as empty and disappear from history.

Root cause: three desktop writers duplicated a close-and-replace sequence instead of the durable atomic helper, while preview decoding discarded errors before backfilling zero turns.

Fix: route title, display, and planner-display sidecars through AtomicWriteFile; preserve preview errors, avoid authoritative-empty backfills on decode failure, and list unreadable non-empty artifacts with a recovery warning.

Verification: go test ./internal/agent ./internal/fileutil; cd desktop && go test ./...; go test -race ./internal/agent

Source: adapts the durable-sidecar and corrupt-session visibility objectives from #5841.

Co-authored-by: myipanta <[email protected]>
2026-08-09 20:50:27 +08:00
esengine 1e7dd07028 feat(cli): the receipt, drawn — one quiet line when clean, the gaps when not
The clean case gets exactly one line, and it names its evidence: the user just
watched the tools run, so repeating the work back to them is noise. What no
transcript carries is the absence — the file nobody looked at again, the check
that never passed — so that is what the card spends its lines on.

The list is bounded at five with a stated remainder, because a receipt long
enough to scroll is a receipt nobody reads. An unrecognised gap kind still
reaches the user under its raw name: silently dropping one is precisely the
failure this card exists to prevent.

Gap phrasing is localized in all three catalogues; the details (a path, a
command) are data and pass through. Two paired resets in the TurnDone branch
became one statement so the card's call line did not grow the file.
2026-08-09 05:49:44 -07:00
SivanColaandmyipanta 3e9371225a fix(shell): bound foreground output memory
Problem: foreground commands retained unbounded combined stdout and stderr until the process exited, so noisy commands could exhaust host memory before result truncation.

Root cause: shellrun bounded only the diagnostic tail; its combined buffer had no owner-level limit.

Fix: cap shared foreground output at 10 MiB while preserving opening context and a rolling 64 KiB tail, with concurrent-write regression coverage.

Verification: go test ./internal/shellrun ./internal/tool/builtin; go test -race ./internal/shellrun

Source: adapts the safety objective from #6528.

Co-authored-by: myipanta <[email protected]>
2026-08-09 20:45:43 +08:00
esengine f635378d36 feat(completion): deliver the receipt, so the host's account is the one the user reads
The completion report has been computed since #8035 and seen by nobody: a
content-free audit of counts and enums, useful for benchmarks and invisible in
the product. A receipt naming no file and no command tells a reader nothing,
which is why this one carries content.

It rides TurnDone next to Readiness — the existing precedent for structured
host state on a finished turn — and crosses eventwire, so the JSON stream and
any frontend reading the wire contract get it without a second delivery path.

A turn with nothing to judge produces no receipt at all. An empty card is
noise, and noise is what makes people stop reading receipts.

The report is held on perTurnState, which beginRunTurn zeroes at the start of
each turn: the host reads it while emitting TurnDone, before the next turn can
clear it. perTurnState moved into its own file to make room, and its comment
debt moved with it — run_loop.go essay 45 -> 32, turnstate.go 0 -> 13, exactly
the thirteen lines that changed files, with agent.go and event.go untouched.
The receipt types likewise live in their own files rather than growing two
already-oversized ones.
2026-08-09 05:37:00 -07:00
esengine 0d5258eefd bench(longrun): reach the long-session states instead of waiting for them
A twelve-hour session is not interesting because of its length. It is
interesting because of the states it passes through — a session reloaded from
disk, a prefix rebuilt, a compaction crossing a turn boundary, a user arriving
mid-task with a new instruction. -segments N reaches those directly: leg 1
starts the session, later legs resume it with --continue, unambiguous because
each task already runs in its own home and therefore its own session directory.

A resumed leg is deliberately not handed the task again. Its prompt is a bare
continuation, because a leg that restates the work would hide exactly the
degradation this exists to expose. -steer replaces one leg's continuation with
a user turn, which belongs at a leg boundary rather than at a wall-clock
instant nothing can reproduce.

The step budget is divided, never multiplied: a segmented arm gets the same
max_steps as the control, or it would win by being allowed to work longer.

Each leg writes its own metrics file. They share a work dir, so a single path
would leave the last leg's numbers standing in for the whole run and the
earlier legs' tokens would vanish — a failure invisible in any single run, so
the accounting is pinned by a stub agent that records its argv and writes the
metrics it was told to. That test also proves leg 1 starts fresh, later legs
carry --continue and never restate the task, and a failed leg stops the run
rather than resuming a session the child never finished writing.

Renaming the metrics file broke a hard-coded skip in the checkpoint
snapshotter, which would have let every later leg's file read as a change the
agent made. Harness artifacts are now matched by prefix.
2026-08-09 05:21:39 -07:00
esengine b3381e60d8 bench(longrun): price injected failure, and stop calling a retry a recovery
The fault seam landed with absolute request indices, which quietly excludes the
tasks most likely to expose a brittle harness: a task that only ever makes four
requests never reaches request seven, so it joins the unfaulted group and
nobody notices the suite is measuring less than it claims. A cadence scales
with the run — every:5:500 — and an absolute index still wins where one is
asked for, so targeted failures stay put.

That gives a run its own control group for free. Under a cadence the short
tasks never hit a fault, so the cost of failure is measured against the same
suite, model and day rather than against a separate arm.

The readout separates two things that are easy to conflate. "Retried" is the
meter seeing another request after a failure — a harness that dies on the first
429 never gets there and was never really tested. "Still solved" is the task
landing anyway. A harness can retry forever and still finish nothing, and that
is not recovery.
2026-08-09 05:07:18 -07:00
czy 9dd2af26ce fix(desktop): freeze terminal task timers 2026-08-06 10:53:27 +08:00
1336 changed files with 101368 additions and 20022 deletions

No files matched your search

+6
View File
@@ -15,6 +15,12 @@ release workflow then asks that reporter to verify once the fix ships.
-
For a GSAP to WAAPI/CSS migration (or any cross-API replacement), document
the source-to-target contract here: easing syntax, time units, callbacks,
cancellation, reduced-motion behavior, and failure fallback. Verification must
assert that the target API was actually called; a mock that silently skips it
does not count.
## Documentation impact
Documentation-impact: TODO
+104 -10
View File
@@ -171,9 +171,14 @@ jobs:
timeout-minutes: 5
run: go test -timeout=3m -run '^TestBashPowerShellExecuteDetailedContract$|^TestBashPowerShell51PreflightRejectsAndAndDetailed$|^TestBashPowerShellOutputIsUTF8$|^TestBashPowerShellSurfacesNonZeroExit$|^TestBashPowerShellRejectsChaining$' ./internal/tool/builtin
# Full push suite still uses the same 8m package budget as Windows smoke:
# agent/boot/control already hit 180s package timeouts under runner load,
# so 3m fails on slow machines without a code regression. Keep the step
# above normal full-suite wall time (~8-12m) so a hang still dies on the
# job budget rather than a false package timeout.
- name: test (full)
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name != 'pull_request'
timeout-minutes: 10
timeout-minutes: 20
env:
# Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
# regression there silently tanks the cache hit rate the project is
@@ -182,7 +187,7 @@ jobs:
# Bound sandbox helper children in Windows tests so a failed OS-level
# launch cannot pin the Actions step after Go's package timeout fires.
WINDOWS_SANDBOX_WAIT_MS: "20000"
run: go test -p 4 -timeout=3m ./...
run: go test -p 4 -timeout=8m ./...
- name: test (Scoop desktop launch)
if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
@@ -321,7 +326,7 @@ jobs:
- name: Install Wails CLI
run: |
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")"
- name: gofmt
run: |
@@ -345,18 +350,24 @@ jobs:
- name: Install Linux build deps
run: |
sudo apt-get update
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.0-dev
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.0-dev xvfb
- name: Build frontend
run: |
wails generate module
pnpm --dir frontend install --frozen-lockfile
PLAYWRIGHT_BROWSERS_PATH=.pw-browsers pnpm --dir frontend exec playwright install --with-deps chromium
pnpm --dir frontend build
- name: Test desktop frontend
run: |
pnpm --dir frontend test:terminal
pnpm --dir frontend test:task-monitor
pnpm --dir frontend test:workspace
pnpm --dir frontend test:motion
pnpm --dir frontend test:motion-browser
pnpm --dir frontend test:transcript
pnpm --dir frontend test:transcript-browser
- name: Test usage statistics frontend
run: pnpm --dir frontend test:usage-stats
@@ -377,12 +388,44 @@ jobs:
- name: test
run: go test ./...
- name: WebKitGTK 4.0 native recovery smoke
run: xvfb-run -a env WEBKIT_DISABLE_COMPOSITING_MODE=1 go test -tags reasonix_webkit_smoke . -run '^TestWebKitNativeRecoverySmoke$'
# The extension work added new shared-state paths to the desktop
# runtime (tab/rebuild fences, extension UI). Race-sweep the module so
# regressions are CI-blocked, not just author-verified locally.
- name: test -race
run: go test -race ./...
# Ubuntu 24.04 ships WebKitGTK 4.1 only. Keep this capability build separate
# from the 22.04/4.0 leg so neither runtime becomes a distro whitelist.
desktop-linux-webkit41:
needs: changes
if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false')
runs-on: ubuntu-24.04
defaults:
run:
working-directory: desktop
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: desktop/go.mod
cache: true
cache-dependency-path: desktop/go.sum
- name: Install WebKitGTK 4.1 build dependencies
run: |
sudo apt-get update
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.1-dev xvfb
- name: Build and test WebKitGTK 4.1 observer
run: go test -tags webkit2_41 ./...
- name: WebKitGTK 4.1 native recovery smoke
run: xvfb-run -a env WEBKIT_DISABLE_COMPOSITING_MODE=1 go test -tags 'webkit2_41 reasonix_webkit_smoke' . -run '^TestWebKitNativeRecoverySmoke$'
# desktop/ is a separate module, so the root macOS matrix above does not
# compile or exercise the native PTY implementation.
desktop-macos:
@@ -449,7 +492,7 @@ jobs:
- name: Install Wails CLI
run: |
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")"
# go:embed of frontend/dist needs a built frontend before the package
# compiles, same as the ubuntu desktop leg.
@@ -459,9 +502,35 @@ jobs:
pnpm --dir frontend install --frozen-lockfile
pnpm --dir frontend build
- name: Test native motion contracts
run: pnpm --dir frontend test:motion
- name: Test WebView2 native smoke state machine
shell: pwsh
run: ../scripts/test-webview2-native-smoke.ps1 -SelfTest
# Build the real Wails executable on every desktop PR and exercise the
# exact production startup path in Microsoft's installed WebView2 runtime.
# The frontend and bindings were already built above, so this skips only
# duplicate build work, not the native shell.
- name: Build Wails executable for native startup smoke
timeout-minutes: 10
run: wails build -clean -s -skipbindings -nopackage -platform windows/amd64 -webview2 embed
- name: Smoke-test Wails/WebView2 native startup
shell: pwsh
timeout-minutes: 2
run: |
../scripts/test-webview2-native-smoke.ps1 `
-ExecutablePath "build/bin/reasonix-desktop.exe" `
-TimeoutSeconds 60 `
-HealthySeconds 5
- name: test (Windows desktop and update helper)
timeout-minutes: 15
run: go test ./...
run: |
go test ./...
(cd third_party/go-webview2 && go test ./pkg/edge)
# Installer packaging is packaging validation, not a code gate: run it
# on pushes to main-v2 (the release pipeline builds installers again
@@ -489,9 +558,33 @@ jobs:
go-version-file: go.mod
cache: true
- if: github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false'
uses: pnpm/[email protected]
with:
version: 10
run_install: false
- if: github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false'
uses: actions/setup-node@v7
with:
node-version: "24"
cache: pnpm
cache-dependency-path: desktop/frontend/pnpm-lock.yaml
- name: required native motion contracts
if: github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false'
run: |
pnpm --dir desktop/frontend install --frozen-lockfile
pnpm --dir desktop/frontend test:motion
- name: repo standards
run: go run ./tools/repolint
- name: wails version pin
run: |
bash scripts/check-wails-pin.sh
bash scripts/check-wails-pin.test.sh
# `make lint-install` reads the same file, so a local run and this job
# cannot drift onto different linter versions.
- id: golangci
@@ -512,9 +605,9 @@ jobs:
set -euo pipefail
command -v golangci-lint
for target in "darwin ." "windows ." "windows desktop"; do
set -- $target
echo "::group::golangci-lint GOOS=$1 ($2)"
(cd "$2" && GOOS="$1" golangci-lint run --timeout=5m ./...)
read -r target_os target_dir <<< "$target"
echo "::group::golangci-lint GOOS=$target_os ($target_dir)"
(cd "$target_dir" && GOOS="$target_os" golangci-lint run --timeout=5m ./...)
echo "::endgroup::"
done
@@ -529,7 +622,8 @@ jobs:
.github/workflows/release-npm.yml \
.github/workflows/release-desktop.yml \
.github/workflows/release-verify-issues.yml \
.github/workflows/docs-impact.yml
.github/workflows/docs-impact.yml \
.github/workflows/ci.yml
bash scripts/release-workflows.test.sh
node scripts/check-single-release-public-contract.mjs
+11 -3
View File
@@ -25,14 +25,22 @@ jobs:
node-version: '22'
cache: npm
cache-dependency-path: workers/crash-report/package-lock.json
- name: Validate
working-directory: workers/crash-report
run: |
npm ci
npm run typecheck
npm test
- name: Apply and verify diagnostics v2 D1 migration
working-directory: workers/crash-report
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: npm run migrate:diagnostics-v2
- name: Deploy
working-directory: workers/crash-report
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
npm ci
npm run typecheck
npm test
npx wrangler deploy
# Mirrors the ALERT_WEBHOOK repo secret into the worker so the ingest
# sentinel can push alerts (see runIngestSentinel). Managed here so
+29 -17
View File
@@ -310,7 +310,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: "22"
node-version: "24"
- uses: pnpm/[email protected]
with:
version: 10
@@ -371,7 +371,7 @@ jobs:
rm -f "$RUNNER_TEMP/cert.p12"
- name: Install Wails CLI
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
run: go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")"
# Official Windows releases must fail closed. Forks can still exercise
# unsigned packaging without repository secrets, but esengine releases
@@ -395,6 +395,32 @@ jobs:
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
run: scripts/desktop-build.sh "${{ matrix.platform }}" "${{ needs.resolve.outputs.version }}" "${{ needs.resolve.outputs.channel }}"
# Candidate code is immutable, but release validation belongs to the
# protected workflow control plane. Reuse this sparse checkout later for
# Authenticode verification rather than trusting a candidate-owned test.
- name: Checkout protected release verifier
if: runner.os == 'Windows' && (matrix.platform == 'windows/amd64' || env.HAS_SIGNPATH == 'true')
uses: actions/checkout@v7
with:
ref: ${{ github.workflow_sha }}
path: release-control
sparse-checkout: |
scripts/test-webview2-native-smoke.ps1
scripts/verify-windows-authenticode.ps1
sparse-checkout-cone-mode: false
persist-credentials: false
# Exercise the exact production Wails binary built for Stable on
# Microsoft's real WebView2 runtime before signing or publication.
- name: Smoke-test Wails/WebView2 native startup
if: matrix.platform == 'windows/amd64'
shell: pwsh
run: |
./release-control/scripts/test-webview2-native-smoke.ps1 `
-ExecutablePath "desktop/build/bin/reasonix-desktop.exe" `
-TimeoutSeconds 60 `
-HealthySeconds 5
# Sign every executable that users actually run before rebuilding the
# portable archive and NSIS installer. Signing only the finished NSIS
# container leaves reasonix-desktop.exe and its sidecars unsigned after
@@ -504,20 +530,6 @@ jobs:
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true'
run: cp signed-installer/*installer*.exe dist/
# Recovery builds intentionally check out the immutable product SHA,
# which can predate a verifier repair. Fetch only the verifier from the
# protected workflow commit so recovery can validate old immutable
# artifacts without rebuilding them from a moving product branch.
- name: Checkout protected release verifier
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true'
uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
path: release-control
sparse-checkout: scripts/verify-windows-authenticode.ps1
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Verify Windows Authenticode release contract
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true'
shell: pwsh
@@ -594,7 +606,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: "22"
node-version: "24"
- uses: actions/download-artifact@v8
with:
+2
View File
@@ -5,6 +5,8 @@
/npm/.stage/
/reasonix
/reasonix.exe
/e2ebench
/e2ebench.exe
# Website (Astro) build output
/site/dist/
+1
View File
@@ -0,0 +1 @@
v2.13.0
+20 -1
View File
@@ -8,8 +8,27 @@ branch.
### Fixed
- Goal now runs continuously by default: the former 16-round per-Run boundary,
10/20/40 cross-Run quotas, default wall-clock budget, and numeric
no-progress/Todo-stall pauses no longer stop valid work. Progress guards still
detect repeated host outcomes and zero-evidence work, but redirect the model
to re-plan instead of producing `goal_run_budget` or `goal_stuck`. Explicit
`[agent].goal_token_budget`, `--max-steps`, positive time/cost budgets, manual
pause/stop, genuine user/external blockers, and evaluator fail-closed behavior
remain available. The Goal token budget defaults to `0` (off); resuming its
`budget_spend` pause grants a fresh slice without clearing cumulative usage.
Goal status reports turns, provider requests, tokens, the optional configured
token threshold, and cumulative active work time. Bot `max_steps` also
defaults to `0` (continuous), while positive user configuration is enforced.
- Removed numeric Goal pauses in existing sidecars automatically normalize to
`running` without sending a model request. Active Goal sidecars write
`turnsLimit: -1` as a downgrade-safe unlimited sentinel while public runtime
APIs retain deprecated limit fields as `0`. The migration preserves unknown
fields, todos, checkpoints, usage, evidence, and historical metadata.
- Goal is now the sole long-task runtime. Historical AutoResearch sidecars
migrate transactionally into research-budget Goals. Invalid archives block
migrate transactionally into Goals with research compatibility metadata. Invalid archives block
fail closed and remain read-only, retaining the task id and compatibility mode
for a restart or `/goal resume` retry; successful Goal-only sidecars omit the
old task id and write an explicit downgrade fence so previous readers cannot
+2
View File
@@ -8,6 +8,8 @@ everything you need to get started.
- **Go 1.25+** — the project targets the latest stable Go release
- **Git** — for version control
- **Node.js** (optional) — only if you work on the desktop app (`desktop/`)
- **Wails CLI** (desktop only) — run `make wails-install` so the CLI matches
`desktop/go.mod`
## Getting started
+8 -1
View File
@@ -8,8 +8,9 @@ LDFLAGS := -s -w \
GOEXE := $(shell go env GOEXE)
# One pin for the Makefile and the CI lint job; see .github/workflows/ci.yml.
GOLANGCI_VERSION := $(shell cat .golangci-version)
WAILS_VERSION := $(shell tr -d '[:space:]' < .wails-version)
.PHONY: build vet fmt lint lint-go lint-install lint-cross lint-update test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean
.PHONY: build vet fmt lint lint-go lint-install lint-cross lint-update wails-install test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean
build:
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/reasonix$(GOEXE) ./cmd/reasonix
@@ -26,6 +27,8 @@ fmt:
# particular never surface in `go vet`.
lint: lint-go
go run ./tools/repolint
bash scripts/check-wails-pin.sh
bash scripts/check-wails-pin.test.sh
lint-go:
@command -v golangci-lint >/dev/null || { echo "golangci-lint not installed; run: make lint-install"; exit 1; }
@@ -41,6 +44,10 @@ lint-install:
lint-update:
go run ./tools/repolint -update
wails-install:
bash scripts/check-wails-pin.sh
go install "github.com/wailsapp/wails/v2/cmd/wails@$(WAILS_VERSION)"
# Linting one GOOS leaves every //go:build windows and darwin file unchecked.
lint-cross:
@for t in "linux ." "darwin ." "windows ." "linux desktop" "windows desktop"; do \
+26
View File
@@ -115,13 +115,39 @@ approvals, model selection, and workspace sessions.
### Path D: Build from source
Clone the repository first:
```sh
git clone https://github.com/esengine/DeepSeek-Reasonix.git
cd DeepSeek-Reasonix
```
#### CLI
The CLI build requires **Go 1.25+**. The module pins a `toolchain` directive;
keep `GOTOOLCHAIN=auto` so Go downloads the pinned toolchain, or install it.
```sh
make build # -> bin/reasonix(.exe)
make cross # -> dist/ (darwin|linux|windows × amd64|arm64)
```
#### Desktop
The desktop build additionally requires:
- **Node 24+ and pnpm 10** (`npm install -g pnpm@10`) for the frontend
- **Wails CLI** matching the shared `.wails-version` pin
```sh
make wails-install
cd desktop
wails build
```
See the [desktop build guide](desktop/README.md#prerequisites) for platform
webview dependencies and Linux build tags.
## Quick start
### CLI / TUI
+5
View File
@@ -15,6 +15,11 @@ agent. It is the Reasonix analog of Claude Code's CLAUDE.md.
the frontends `cli`, `serve`, `acp`, `bot`, `botruntime`, `boot` and the hosts
`cmd/`, `desktop/` may import `control`; nothing below a frontend may import
one. The declared sets live in `tools/repolint/layers.go`.
- Subagent delegation keeps five concepts apart: a profile says how a worker
thinks, `TaskSpec` what this call wants, `CapabilityGrant` what it may touch,
`ContextRequest` what it starts from, `SchedulerPolicy` when it runs. Put a
field in whichever member decides its value — profiles carry ceilings, never
per-call values. `internal/agent/profile_boundary_test.go` enforces it.
- Cache-first: the system-prompt prefix (base prompt + tools + memory) must stay
byte-stable across turns so DeepSeek's automatic prefix cache stays warm. Never
mutate it mid-session — ride the turn tail instead (see `control.Compose`).
+129 -4
View File
@@ -106,6 +106,61 @@ Each task under `e2e/tasks/<id>/` contains:
| `verify.sh` | The grader: exits 0 iff the agent's artifacts are correct. |
| `workdir/` | Optional seed workspace, copied into the temp run dir before the agent starts. |
## Anchor resistance
Multi-agent systems isolate conversations. They rarely isolate conclusions: a
sub-agent asked to "independently check this" usually arrives already holding
its parent's answer. Before adding an interface to prevent that, measure
whether it costs anything here — a handed-down conclusion that the agent
routinely overturns is not a problem worth building against.
The `-anchor` arms make that measurable on the `failing-test-diagnosis` tasks,
which have one knowable cause each. Each carries two authored hypotheses: the
real cause (`seed_correct`) and a plausible one that is not (`seed_wrong`).
The arm prefixes the prompt with its seed, so the agent meets the conclusion
before it has read anything.
```bash
go run ./cmd/e2ebench -task diagnose-float-total,diagnose-floor-division,diagnose-missing-file,diagnose-tie-order,diagnose-utf8-bom,diagnose-version-sort -json blind.json
go run ./cmd/e2ebench -anchor correct -task ...same... -json correct.json
go run ./cmd/e2ebench -anchor wrong -task ...same... -json wrong.json
```
Anchor resistance is the wrong arm's solve rate over the blind arm's on the
same tasks. A wrong arm that collapses says a handed-down conclusion survives
contact with the evidence, and that blind delegation is worth its cost; a wrong
arm that barely moves says the opposite. Nothing here is a single composite
"independence score" — the arms are reported separately because they answer
different questions.
Two limits are worth stating rather than discovering later. The seed goes to
the top-level agent, so it prices agent-level anchoring; it reaches a
sub-agent only if the parent delegates and repeats it, which the **evidence
origin** line under Delegation is what measures. And the seeded arms score a
smaller corpus than the blind one — every skipped task is named in the report,
because a seeded arm quietly scoring fewer tasks is not the same experiment.
### Evidence origin
The Delegation section reports how much of what the children looked at they
had to find themselves, and what the parent's own delegation text pointed at.
Both come from host receipts and the parent-authored task text before host
framing, never from anything an agent claims.
Two kinds of pointing are counted apart, because they are not the same act:
| | What it is | Blind delegation |
| --- | --- | --- |
| **scope hint** (`pkg/`) | Narrowing the search — the unavoidable cost of handing work off at all | expected, and recorded |
| **named file** (`pkg/romeo.py`) | Saying where the answer is | the number that should be zero |
Discovery is judged against named files only: a child sent to a directory
still had to work out which file in it mattered, so a scope hint never erases
its credit. Both stay absolute counts — a rate would hide how large the
hand-over was — while the discovery share is a ratio of summed paths across
children, not a mean of per-child rates, so a child that opened one file
cannot outweigh one that swept forty.
## Neutral metering
A harness comparison has an accounting problem before it has a measurement
@@ -146,9 +201,74 @@ metered this way precisely because it *does* self-report: if the proxy and
`.run-metrics.json` disagree about the same run, one of them is wrong and no
cross-harness number is ready to publish.
`-faults` injects provider failures at fixed request indices through the same
proxy, which is what LongRun needs: deterministic 429/500 at the same point of
a run, replayable across harnesses.
## Fault recovery
`-faults` injects provider failures through the same proxy — deterministic, and
replayable across harnesses. Two forms:
- `3:429` — a targeted failure at an exact request.
- `every:5:500` — a cadence. **A mixed-length suite needs this**: a task that
only ever makes four requests would never reach a fixed index and would join
the unfaulted group without anyone noticing.
An absolute index wins over the cadence, so a targeted failure stays where it
was asked for.
```sh
go run ./cmd/e2ebench -meter ~/.reasonix/config.toml -faults every:5:500 -trajectories t/
```
The readout separates two things that are easy to conflate:
```text
**Fault recovery** (31 runs failed on purpose, 47 injections): **retried** 94% (29) ·
**still solved** 61% (19/31) · in-run control 78% (14/18 never hit a fault)
```
- **retried** — the meter saw another request after the failure. A harness that
dies on the first 429 never reaches this, and *was never really tested*.
- **still solved** — the task landed anyway. A harness can retry forever and
still not finish; that is not recovery.
- **in-run control** — with a cadence, short tasks never hit a fault, so the
same run carries its own unfaulted baseline. The cost of failure is measured
against the same suite and model rather than a separate arm run at another
time under other conditions.
## Segmented runs
A twelve-hour session is not interesting because it is twelve hours long. It is
interesting because of the states it passes through: a session reloaded from
disk, a prefix rebuilt, a compaction crossing a turn boundary, a user arriving
mid-task with a new instruction. `-segments N` reaches those states directly
instead of waiting hours for them.
```sh
go run ./cmd/e2ebench -segments 3 -steer "also handle empty input@2" -trajectories t/
```
Leg 1 starts the session with the task. Later legs resume it with `--continue`,
which is unambiguous because each task already runs in its own home and
therefore its own session directory. A resumed leg is deliberately **not** given
the task again — its prompt is a bare continuation, because a leg that restates
the work would hide exactly the degradation this is meant to expose. A `-steer`
entry replaces one leg's continuation with a user turn.
Two properties are load-bearing:
- **The step budget is divided, never multiplied.** A segmented arm gets the
same `max_steps` as the control arm, split across legs with the remainder on
the last. Otherwise the arm would win by being allowed to work longer.
- **Each leg writes its own metrics file.** They share a work dir, so a single
`.run-metrics.json` would leave the last leg's numbers standing in for the
whole run and the earlier legs' tokens would simply vanish. `Segments` in the
JSON records how many legs a run had.
A leg that fails ends the run: resuming a session the child never finished
writing would measure crash recovery, which is a different experiment.
Only the last leg's trajectory digest is read, so time attribution and cognition
lines describe that leg rather than the whole run. Merging per-leg trajectories
is not done yet; `Segments` is what tells you the digest is partial.
## task.toml schema
@@ -162,6 +282,8 @@ decoder. The task ID is the directory name; tasks run in sorted ID order.
| `max_steps` | int | yes | Agent tool-call cap; passed through as `--max-steps` to `reasonix run`. |
| `no_solution` | bool | no | Ground truth: no reachable solution exists. The task leaves every accuracy denominator, its `verify.sh` grades the inverse contract, and it is scored on honesty instead. See [Completion Integrity](#completion-integrity). |
| `timeout_sec` | int | no | Per-task wall-clock timeout in seconds; defaults to `240` when omitted or `0`. |
| `seed_correct` | string | no | The task's real cause, phrased as a conclusion handed down before the run. Used by `-anchor correct`. See [Anchor resistance](#anchor-resistance). |
| `seed_wrong` | string | no | A plausible cause that is **not** the real one. Used by `-anchor wrong`. Author both seeds or neither: a task seeded on one side only would be scored in one arm and skipped in the other. |
Example (`tasks/fizzbuzz/task.toml`):
@@ -237,10 +359,13 @@ own outcome).
| `-json` | *(none)* | Write the JSON report here (optional). |
| `-trajectories` | *(none)* | Suite mode: write one `<task-id>.trajectory.jsonl` per task into this directory (the agent's full event stream with timestamps — see `reasonix run --trajectory`). The report gains a time-attribution line (tools vs. model) and each JSON result a `trajectory` digest. |
| `-force-planner` | `false` | Suite mode: prefix each prompt with a plan-first directive so the two-model turn engages regardless of the planner gate. Use for the "with planner" arm of an A/B; results carry `plan_forced` so arms are only comparable with equal forcing. |
| `-anchor` | `blind` | Suite mode: which hypothesis the agent holds before it looks at anything — `blind` (none, the control) \| `correct` \| `wrong`. The seeded arms prefix each prompt with the task's authored seed and **skip** tasks that have none, so an unseeded control run never lands in a seeded denominator. Results carry `anchor`. See [Anchor resistance](#anchor-resistance). |
| `-cache` | `cold` | Suite mode: `cold` runs each task as a fresh session (the fair cross-agent comparison arm); `warm` primes the provider prefix cache with a one-step run in the same workdir first, measuring the long-lived-session steady state. Never mix arms in one report — compare them with `-mode compare cold.json warm.json`. |
| `-budget` | `800000` | Abort once total tokens cross this (`0` = no cap). Remaining tasks are reported as skipped. |
| `-meter` | *(off)* | Suite mode: route the benchmarked provider through the neutral measuring proxy, using this `config.toml` as the source. Spend is then counted at the request boundary instead of trusted from the harness. See [Neutral metering](#neutral-metering). |
| `-faults` | *(none)* | Suite mode: inject provider failures at fixed request indices, e.g. `3:429,7:500`. Requires `-meter` — the proxy is the only place a fault can be injected. |
| `-faults` | *(none)* | Suite mode: inject provider failures through the meter — absolute indices (`3:429`) and/or a cadence that scales with the run (`every:5:500`). Requires `-meter`. See [Fault recovery](#fault-recovery). |
| `-segments` | `1` | Suite mode: split each task into N resumed legs (`--continue` between them). The step budget is **divided**, never multiplied. See [Segmented runs](#segmented-runs). |
| `-steer` | *(none)* | Suite mode: deliver a user turn at a leg boundary, e.g. `"also handle empty input@2"`. Requires `-segments` to reach that leg. |
Diff-mode flags:
+39 -12
View File
@@ -42,6 +42,7 @@ func main() {
window := flag.Int("window", 128_000, "context window in tokens")
control := flag.Bool("control", true, "fidelity: also score probes against full history")
arm := flag.String("arm", "full", "full | incremental: re-derive each digest from canonical, or fold the previous projection")
snip := flag.Bool("snip", false, "legacy no-op: automatic snip projections are gone; kept so old scripts do not fail")
out := flag.String("out", "", "write the JSON report here")
flag.Parse()
@@ -49,14 +50,14 @@ func main() {
res []genResult
err error
)
incremental := *arm == "incremental"
a := arms{incremental: *arm == "incremental", snip: *snip}
switch {
case *arm != "full" && *arm != "incremental":
err = fmt.Errorf("unknown arm %q", *arm)
case *mode == "cost":
res, err = runCost(*gens, *window, incremental)
res, err = runCost(*gens, *window, a)
case *mode == "fidelity":
res, err = runFidelity(*gens, *window, *control, incremental)
res, err = runFidelity(*gens, *window, *control, a)
default:
err = fmt.Errorf("unknown mode %q", *mode)
}
@@ -83,6 +84,8 @@ type genResult struct {
SummarizerInput int `json:"summarizer_input_tokens"`
LargestCall int `json:"largest_call_tokens"`
Mode string `json:"mode,omitempty"`
SnippedResults int `json:"snipped_results,omitempty"`
SnippedChars int `json:"snipped_chars,omitempty"`
Seconds float64 `json:"seconds"`
Error string `json:"error,omitempty"`
Survived map[string]int `json:"survived,omitempty"` // probe class -> 1 kept, 0 lost
@@ -97,9 +100,10 @@ type harness struct {
agentA *agent.Agent
path string
calls *callRecorder
snip bool
}
func newHarness(t *testingDir, p provider.Provider, window int, rec *callRecorder, incremental bool) *harness {
func newHarness(t *testingDir, p provider.Provider, window int, rec *callRecorder, arm arms) *harness {
sess := newSession()
path := filepath.Join(t.dir, "session.jsonl")
a := agent.New(p, tool.NewRegistry(), sess, agent.Options{
@@ -107,13 +111,23 @@ func newHarness(t *testingDir, p provider.Provider, window int, rec *callRecorde
ArchiveDir: filepath.Join(t.dir, "archive"),
SessionPath: path,
RecentKeep: 4,
Ablation: foldArm(incremental),
// boot's default when cfg.Agent.Keep is unset; without it the bench
// would measure a configuration no real session runs.
KeepPolicy: agent.KeepErrors,
Ablation: foldArm(arm.incremental),
}, rec.sink())
return &harness{sess: sess, agentA: a, path: path, calls: rec}
return &harness{sess: sess, agentA: a, path: path, calls: rec, snip: arm.snip}
}
// foldArm switches full re-derivation off, which is what makes a fold read the
// previous projection instead of the canonical transcript.
// arms selects the maintenance behaviour under test. Snipping is off by default
// so a run stays comparable with baselines recorded before it existed.
type arms struct {
incremental bool
snip bool
}
func foldArm(incremental bool) ablation.Set {
if incremental {
return ablation.New(ablation.FullFold)
@@ -128,6 +142,15 @@ func (h *harness) runGeneration(ctx context.Context, gen int, probes []probe) ge
h.calls.reset()
start := time.Now()
if h.snip {
// SnipStaleToolResults is intentionally a no-op; record zeros for
// report schema compatibility with pre-content-driven baselines.
st, serr := h.agentA.SnipStaleToolResults()
if serr != nil {
r.Error = serr.Error()
}
r.SnippedResults, r.SnippedChars = st.Results, st.SavedChars
}
err := h.agentA.CompactNow(ctx, "")
r.Seconds = time.Since(start).Seconds()
if err != nil {
@@ -140,12 +163,16 @@ func (h *harness) runGeneration(ctx context.Context, gen int, probes []probe) ge
}
if st, ok, sterr := agent.LoadCompactionState(h.path); sterr == nil && ok {
r.ProjectionTokens = st.Projection.ProjectionTokens
r.Mode = st.LastMode
if st.LastReceipt != nil && st.LastReceipt.Action == "summary" {
r.Mode = agent.CompactionModeSummarized
} else if st.LastMode != "" {
r.Mode = st.LastMode
}
}
return r
}
func runCost(gens, window int, incremental bool) ([]genResult, error) {
func runCost(gens, window int, a arms) ([]genResult, error) {
dir, cleanup, err := tempDir()
if err != nil {
return nil, err
@@ -154,7 +181,7 @@ func runCost(gens, window int, incremental bool) ([]genResult, error) {
rec := &callRecorder{}
p := &scriptedProvider{rec: rec, reply: syntheticDigest, window: window}
h := newHarness(dir, p, window, rec, incremental)
h := newHarness(dir, p, window, rec, a)
var out []genResult
for gen := range gens {
@@ -163,7 +190,7 @@ func runCost(gens, window int, incremental bool) ([]genResult, error) {
return out, nil
}
func runFidelity(gens, window int, control, incremental bool) ([]genResult, error) {
func runFidelity(gens, window int, control bool, a arms) ([]genResult, error) {
key := os.Getenv("DEEPSEEK_API_KEY")
if key == "" {
return nil, fmt.Errorf("fidelity mode needs DEEPSEEK_API_KEY")
@@ -179,7 +206,7 @@ func runFidelity(gens, window int, control, incremental bool) ([]genResult, erro
defer cleanup()
rec := &callRecorder{}
h := newHarness(dir, &recordingProvider{inner: p, rec: rec}, window, rec, incremental)
h := newHarness(dir, &recordingProvider{inner: p, rec: rec}, window, rec, a)
probes := probeSuite()
ctx := context.Background()
@@ -193,7 +220,7 @@ func runFidelity(gens, window int, control, incremental bool) ([]genResult, erro
return nil, verr
}
for _, probe := range probes {
if probe.plantAt > gen {
if probe.settledAt() > gen {
continue
}
answer, aerr := ask(ctx, p, visible, probe.question)
+1 -1
View File
@@ -12,7 +12,7 @@ func TestRepeatedFoldsStayBoundedAndKeepSucceeding(t *testing.T) {
reserve = 8192 // summaryOutputReserve: the digest the call must still return
maxCalls = 7 // maxSummarySpans + the merge pass
)
res, err := runCost(gens, window, false)
res, err := runCost(gens, window, arms{})
if err != nil {
t.Fatalf("runCost: %v", err)
}
+121 -4
View File
@@ -8,6 +8,7 @@ import (
"reasonix/internal/agent"
"reasonix/internal/provider"
"reasonix/internal/tool"
)
// probeAnswerContract rides with every probe question. A context full of tool
@@ -33,9 +34,13 @@ func invalidAnswer(s string) bool {
// The question is asked against the compacted context, so a wrong answer means
// compaction lost the fact — not that the model is weak.
type probe struct {
class string
plantAt int
plant func(*agent.Session)
class string
plantAt int
plant func(*agent.Session)
// later plants more of the same fact in later generations, so a decision
// that changes across folds is tested the way it actually happens: each
// revision lands on the far side of a compaction, not next to the last one.
later map[int]func(*agent.Session)
question string
want []string // answer must contain one of these, lowercased
reject []string // ...and none of these: the pre-correction answer
@@ -52,6 +57,24 @@ func toolRound(id, name, args, result string) func(*agent.Session) {
}
}
// failedToolRound is a tool round the host recorded as failed, the way a real
// non-zero bash run arrives. Without the execution record the keep policy sees
// only text, which is exactly the gap the buried-evidence probe measures.
func failedToolRound(id, name, args, result string, code int) func(*agent.Session) {
return func(s *agent.Session) {
exit := code
s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: name, Arguments: args}}})
s.Add(provider.Message{
Role: provider.RoleTool, ToolCallID: id, Name: name, Content: result,
ToolExecution: &provider.ToolExecution{
Kind: "shell",
State: tool.ShellStateFailed,
ExitCode: &exit,
},
})
}
}
func seq(fns ...func(*agent.Session)) func(*agent.Session) {
return func(s *agent.Session) {
for _, fn := range fns {
@@ -64,6 +87,50 @@ func seq(fns ...func(*agent.Session)) func(*agent.Session) {
// freshness and correction probes are the ones summaries classically get wrong:
// both have a plausible stale answer that reads as correct.
func probeSuite() []probe {
suite := append(factProbes(), revisionProbes()...)
return append(suite, snipProbes()...)
}
// buriedTestLog is a `go test -v` log whose only failure detail sits in the
// middle. Geometric snipping keeps the first 80 and last 12 lines, so this is
// precisely the shape it drops while reporting a plausible-looking remainder.
// It names a package no other probe touches: a run covering config/ would be a
// truthful "yes" for verification-freshness and score that probe as lost.
func buriedTestLog() string {
var b strings.Builder
pass := func(i int) {
fmt.Fprintf(&b, "=== RUN TestEmitterCase%03d\n--- PASS: TestEmitterCase%03d (0.00s)\n", i, i)
}
for i := range 120 {
pass(i)
}
b.WriteString("=== RUN TestEmitterQuoting\n")
b.WriteString(" emitter_test.go:412: assertion failed: expected beta-7d21, got gamma-4a88\n")
b.WriteString("--- FAIL: TestEmitterQuoting (0.01s)\n")
for i := 120; i < 200; i++ {
pass(i)
}
b.WriteString("FAIL\nexit status 1\nFAIL\treasonix/emitter\t0.234s\n")
return b.String()
}
// snipProbes plant a fact in the middle of a long tool result, out of reach of
// the head/tail geometry. They score the maintenance pass rather than the fold:
// every other probe in the suite is short enough that snipping cannot touch it.
func snipProbes() []probe {
return []probe{
{
class: "buried-evidence",
plantAt: 1,
plant: failedToolRound("bt1", "bash", `{"cmd":"go test ./emitter -v"}`, buriedTestLog(), 1),
question: "In the failed test, what value did the assertion expect? Reply with just the value.",
want: []string{"beta-7d21"},
reject: []string{"gamma-4a88"},
},
}
}
func factProbes() []probe {
return []probe{
{
class: "user-constraint",
@@ -165,6 +232,45 @@ func probeSuite() []probe {
}
}
// revisionProbes state a fact and then change it in a later generation. The
// revision lands on the far side of a fold, so carrying it forward means
// superseding the digest's own earlier claim rather than a neighbouring turn.
func revisionProbes() []probe {
return []probe{
{
class: "late-constraint",
plantAt: 3,
plant: userTurn("New hard rule from here on: never edit anything under internal/store/."),
question: "Are you allowed to edit files under internal/store/? Answer yes or no.",
want: []string{"no"},
reject: []string{"yes"},
},
{
class: "reversal-chain",
plantAt: 0,
plant: userTurn("Use PostgreSQL for the datastore."),
later: map[int]func(*agent.Session){
1: userTurn("Change of plan: drop PostgreSQL, use SQLite instead."),
2: userTurn("Final call: back to PostgreSQL after all."),
},
question: "Which datastore is the current decision? Reply with one word.",
want: []string{"postgresql", "postgres"},
reject: []string{"sqlite"},
},
{
class: "distractor-file",
plantAt: 1,
plant: userTurn("Draft note: the fix might belong in config/legacy_parser.go."),
later: map[int]func(*agent.Session){
3: userTurn("Confirmed: the fix belongs in config/emitter.go; the legacy parser is not involved."),
},
question: "Which file does the fix belong in? Reply with just the filename, no path.",
want: []string{"emitter"},
reject: []string{"legacy"},
},
}
}
// score reports whether an answer keeps the planted fact. A rejected token
// anywhere in the answer counts as lost even when the wanted token also
// appears, so "yes, but it was not re-run" does not pass as "no".
@@ -194,4 +300,15 @@ func matchesToken(lowered, want string) bool {
}), want)
}
func (p probe) String() string { return fmt.Sprintf("%s@gen%d", p.class, p.plantAt) }
// settledAt is the first generation whose answer is the one want/reject scores.
// A probe revised across folds has a different correct answer while the chain is
// still running, so asking before it settles would score a right answer as lost.
func (p probe) settledAt() int {
at := p.plantAt
for gen := range p.later {
at = max(at, gen)
}
return at
}
func (p probe) String() string { return fmt.Sprintf("%s@gen%d", p.class, p.settledAt()) }
+3
View File
@@ -46,6 +46,9 @@ func growSession(sess *agent.Session, gen int, probes []probe) {
if p.plantAt == gen {
p.plant(sess)
}
if revise, ok := p.later[gen]; ok {
revise(sess)
}
}
for i := range unitsPerGeneration {
workUnit(sess, gen, i)
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"strings"
"testing"
)
// Automatic snip projections are gone. SnipStaleToolResults remains as a no-op
// so older harness flags and call sites do not panic or rewrite history.
func TestSnipIsNoOp(t *testing.T) {
dir, cleanup, err := tempDir()
if err != nil {
t.Fatal(err)
}
defer cleanup()
rec := &callRecorder{}
const window = 64_000
h := newHarness(dir, &scriptedProvider{rec: rec, reply: syntheticDigest, window: window}, window, rec, arms{snip: true})
for gen := range 3 {
growSession(h.sess, gen, probeSuite())
}
before := renderAll(h.sess.Snapshot())
if !strings.Contains(before, "beta-7d21") {
t.Fatal("planted fact missing before the snip call")
}
st, err := h.agentA.SnipStaleToolResults()
if err != nil {
t.Fatal(err)
}
if st.Results != 0 || st.SavedChars != 0 {
t.Fatalf("snip no-op rewrote history: results=%d chars=%d", st.Results, st.SavedChars)
}
after := renderAll(h.sess.Snapshot())
if after != before {
t.Fatal("canonical transcript changed after SnipStaleToolResults")
}
if !strings.Contains(after, "beta-7d21") {
t.Error("planted fact missing after snip no-op")
}
}
+252 -185
View File
@@ -1,5 +1,6 @@
// Drives the context-maintenance E2E scenarios against the real DeepSeek API:
// seed → (idle past cache TTL) → resume A/B-compares cold-restart miss tokens with and without pruning.
// Cost-capped seed → resume → continue smoke for content-driven maintenance.
// Offline covers seed/resume; live continue needs DEEPSEEK_API_KEY and allows
// at most one summary after growing past compact_ratio=0.85.
package main
import (
@@ -17,105 +18,104 @@ import (
"reasonix/internal/provider"
_ "reasonix/internal/provider/openai"
"reasonix/internal/tool"
"reasonix/internal/tool/builtin"
)
const (
model = "deepseek-v4-flash"
baseURL = "https://api.deepseek.com"
fatResults = 20
fatBytes = 12_000
model = "deepseek-v4-flash"
baseURL = "https://api.deepseek.com"
windowTokens = 1_000_000
compactRatio = 0.85
defaultMaxUSD = 0.50
fatBytes = 32 * 1024
)
func prov() provider.Provider {
key := os.Getenv("DEEPSEEK_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "DEEPSEEK_API_KEY not set")
os.Exit(1)
}
p, err := provider.New("openai", provider.Config{Name: "e2e", BaseURL: baseURL, Model: model, APIKey: key})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return p
type costCap struct {
maxUSD float64
spent float64
}
func fakeGoFile(nonce string, i, size int) string {
var b strings.Builder
fmt.Fprintf(&b, "// module %s file%02d\npackage stress\n\n", nonce, i)
line := 0
for b.Len() < size {
fmt.Fprintf(&b, "func helper_%s_%02d_%04d(x int) int { return x*%d + %d }\n", nonce, i, line, line+3, line*7)
line++
func (c *costCap) add(u *provider.Usage, pricing *provider.Pricing) error {
if c == nil || u == nil || pricing == nil {
return nil
}
return b.String()
}
func seedSession(nonce string) *agent.Session {
s := agent.NewSession("You are a terse coding agent reviewing a Go codebase.")
s.Add(provider.Message{Role: provider.RoleUser, Content: "Review every file in module " + nonce + " one by one. Keep notes short."})
for i := range fatResults {
id := fmt.Sprintf("c%02d", i)
name := fmt.Sprintf("src/file%02d.go", i)
s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: fmt.Sprintf(`{"path":%q}`, name)}}})
s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "read_file", Content: fakeGoFile(nonce, i, fatBytes)})
s.Add(provider.Message{Role: provider.RoleAssistant, Content: fmt.Sprintf("Reviewed %s.", name)})
c.spent += pricing.Cost(u)
if c.maxUSD > 0 && c.spent > c.maxUSD {
return fmt.Errorf("cost cap exceeded: spent $%.4f > max $%.4f", c.spent, c.maxUSD)
}
return s
}
// oneShot appends a user message and runs a single completion, returning usage.
func oneShot(p provider.Provider, msgs []provider.Message, question string) (provider.Usage, error) {
req := provider.Request{
Messages: append(append([]provider.Message(nil), msgs...), provider.Message{Role: provider.RoleUser, Content: question}),
MaxTokens: 32,
}
ch, err := p.Stream(context.Background(), req)
if err != nil {
return provider.Usage{}, err
}
var u provider.Usage
for c := range ch {
switch c.Type {
case provider.ChunkUsage:
u = *c.Usage
case provider.ChunkError:
return u, c.Err
}
}
return u, nil
return nil
}
type meta struct {
SeededAt time.Time `json:"seeded_at"`
Nonces map[string]string `json:"nonces"`
SeedUse map[string]provider.Usage `json:"seed_usage"`
SeededAt time.Time `json:"seeded_at"`
ProjectionVersion uint64 `json:"projection_version"`
CanonicalTokens int `json:"canonical_tokens"`
ProjectedTokens int `json:"projected_tokens"`
TriggerTokens int `json:"trigger_tokens"`
SummaryCalls int `json:"summary_calls"`
SpentUSD float64 `json:"spent_usd"`
ContinueUsage *provider.Usage `json:"continue_usage,omitempty"`
}
func seed(dir string) {
p := prov()
if err := os.MkdirAll(dir, 0o755); err != nil {
type recordSink struct {
summaryStarts int
}
func (s *recordSink) Emit(e event.Event) {
if e.Kind == event.CompactionStarted {
s.summaryStarts++
}
}
func prov() (provider.Provider, *provider.Pricing) {
key := os.Getenv("DEEPSEEK_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "DEEPSEEK_API_KEY not set (use -offline for seed/resume)")
os.Exit(1)
}
p, err := provider.New("openai", provider.Config{
Name: "e2e", BaseURL: baseURL, Model: model, APIKey: key,
Extra: map[string]any{"max_output_tokens": 1024},
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
m := meta{SeededAt: time.Now(), Nonces: map[string]string{}, SeedUse: map[string]provider.Usage{}}
for _, arm := range []string{"pruned", "control"} {
nonce := fmt.Sprintf("%s%d", arm, time.Now().UnixNano()%1_000_000)
s := seedSession(nonce)
u, err := oneShot(p, s.Snapshot(), "How many files have you reviewed so far? Reply with just the number.")
if err != nil {
fmt.Fprintf(os.Stderr, "seed %s: %v\n", arm, err)
os.Exit(1)
}
if err := s.Save(filepath.Join(dir, arm+".jsonl")); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
m.Nonces[arm] = nonce
m.SeedUse[arm] = u
fmt.Printf("seeded %-7s prompt=%d hit=%d miss=%d\n", arm, u.PromptTokens, u.CacheHitTokens, u.CacheMissTokens)
pricing := &provider.Pricing{Input: 0.14, Output: 0.28, CacheHit: 0.014, Currency: "$"}
return p, pricing
}
func fatHistory(n int) []provider.Message {
result := strings.Repeat("x", fatBytes)
msgs := []provider.Message{
{Role: provider.RoleSystem, Content: "You are a terse coding agent."},
{Role: provider.RoleUser, Content: "Review the large tool outputs carefully."},
}
for i := range n {
id := fmt.Sprintf("tool-%d", i)
msgs = append(msgs,
provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: "{}"}}},
provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "read_file", Content: result},
)
}
return append(msgs,
provider.Message{Role: provider.RoleUser, Content: "summarize status when asked"},
provider.Message{Role: provider.RoleAssistant, Content: "standing by"},
)
}
func newAgent(p provider.Provider, sess *agent.Session, path string, sink event.Sink) *agent.Agent {
return agent.New(p, tool.NewRegistry(), sess, agent.Options{
ContextWindow: windowTokens,
CompactRatio: compactRatio,
RecentKeep: 2,
SessionPath: path,
WorkspaceID: "cm-e2e",
ModelRef: "deepseek/" + model,
MaxSteps: 2,
}, sink)
}
func writeMeta(dir string, m meta) {
b, _ := json.MarshalIndent(m, "", " ")
if err := os.WriteFile(filepath.Join(dir, "meta.json"), b, 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -123,8 +123,7 @@ func seed(dir string) {
}
}
func resume(dir string) {
p := prov()
func readMeta(dir string) meta {
raw, err := os.ReadFile(filepath.Join(dir, "meta.json"))
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -135,121 +134,189 @@ func resume(dir string) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
idle := time.Since(m.SeededAt).Round(time.Minute)
return m
}
out := map[string]provider.Usage{}
for _, arm := range []string{"pruned", "control"} {
s, err := agent.LoadSession(filepath.Join(dir, arm+".jsonl"))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
prunes := 0
if arm == "pruned" {
a := agent.New(nil, tool.NewRegistry(), s, agent.Options{ContextWindow: 1_000_000, ArchiveDir: filepath.Join(dir, "archive")}, event.Discard)
st, err := a.PruneStaleToolResults()
if err != nil {
fmt.Fprintln(os.Stderr, "prune:", err)
os.Exit(1)
}
prunes = st.Results
}
u, err := oneShot(p, s.Snapshot(), "Which file did you review first? Reply with just the path.")
if err != nil {
fmt.Fprintf(os.Stderr, "resume %s: %v\n", arm, err)
os.Exit(1)
}
out[arm] = u
fmt.Printf("resume %-7s idle=%s pruned=%d prompt=%d hit=%d miss=%d\n", arm, idle, prunes, u.PromptTokens, u.CacheHitTokens, u.CacheMissTokens)
}
b, _ := json.MarshalIndent(map[string]any{"idle": idle.String(), "resume_usage": out}, "", " ")
if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("resume-%d.json", time.Now().Unix())), b, 0o644); err != nil {
func seed(dir string, offline bool, cap *costCap) {
if err := os.MkdirAll(dir, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
c, pr := out["control"], out["pruned"]
if c.CacheMissTokens > 0 {
fmt.Printf("\ncold-restart miss tokens: control=%d pruned=%d (%.0f%% reduction)\n",
c.CacheMissTokens, pr.CacheMissTokens, 100*(1-float64(pr.CacheMissTokens)/float64(c.CacheMissTokens)))
path := filepath.Join(dir, "session.jsonl")
// ~80×32KB tool results: well under 85% of 1M for a cold estimate.
sess := &agent.Session{Messages: fatHistory(80)}
sink := &recordSink{}
var p provider.Provider
if !offline {
p, _ = prov()
}
}
// comprehension checks that the agent re-reads a file behind a prune placeholder
// instead of hallucinating: the answer is a number that exists only in the file.
func comprehension(trials int) {
p := prov()
pass := 0
for t := range trials {
dir, err := os.MkdirTemp("", "cm-e2e-")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
secret := fmt.Sprintf("%d", 1000+time.Now().UnixNano()%9000)
content := "package cfg\n\n// retention floor, milliseconds\nconst cacheRetentionFloor = " + secret + "\n" + strings.Repeat("// padding line filler for prune eligibility\n", 400)
if err := os.WriteFile(filepath.Join(dir, "config.go"), []byte(content), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
s := agent.NewSession("You are a terse coding agent. Use tools when you need file contents.")
s.Add(provider.Message{Role: provider.RoleUser, Content: "Read config.go and note its constants."})
s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "r1", Name: "read_file", Arguments: `{"path":"config.go"}`}}})
s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "r1", Name: "read_file", Content: content})
s.Add(provider.Message{Role: provider.RoleAssistant, Content: "Noted the constants in config.go."})
for i := range 4 {
s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("ack %d", i)})
s.Add(provider.Message{Role: provider.RoleAssistant, Content: "ok"})
}
reg := tool.NewRegistry()
for _, tl := range (builtin.Workspace{Dir: dir}).Tools("read_file") {
reg.Add(tl)
}
a := agent.New(p, reg, s, agent.Options{ContextWindow: 2000, RecentKeep: 2, MaxSteps: 5}, event.Discard)
st, err := a.PruneStaleToolResults()
if err != nil || st.Results == 0 {
fmt.Fprintf(os.Stderr, "trial %d: prune did not fire (st=%+v err=%v)\n", t, st, err)
os.Exit(1)
}
err = a.Run(context.Background(), "What is the exact numeric value of cacheRetentionFloor in config.go? Reply with just the number.")
reRead, answered := false, false
for _, msg := range s.Snapshot() {
if msg.Role == provider.RoleTool && strings.Contains(msg.Content, "cacheRetentionFloor = "+secret) {
reRead = true
}
if msg.Role == provider.RoleAssistant && strings.Contains(msg.Content, secret) {
answered = true
}
}
ok := err == nil && reRead && answered
if ok {
pass++
}
fmt.Printf("trial %d: re-read=%v answered=%v err=%v\n", t, reRead, answered, err)
os.RemoveAll(dir)
}
fmt.Printf("\ncomprehension: %d/%d passed\n", pass, trials)
if pass < trials {
a := newAgent(p, sess, path, sink)
before := a.ContextMaintenanceSnapshot()
if before.TriggerTokens <= 0 {
fmt.Fprintln(os.Stderr, "seed: missing trigger tokens")
os.Exit(1)
}
if before.ProjectedTokens >= before.TriggerTokens {
fmt.Fprintf(os.Stderr, "seed fixture already at/above trigger: projected=%d trigger=%d\n",
before.ProjectedTokens, before.TriggerTokens)
os.Exit(1)
}
if err := a.PrepareContext(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, "seed prepare:", err)
os.Exit(1)
}
after := a.ContextMaintenanceSnapshot()
if after.ProjectionVersion != 0 {
fmt.Fprintf(os.Stderr, "seed installed projection version %d below compact_ratio\n", after.ProjectionVersion)
os.Exit(1)
}
if sink.summaryStarts != 0 {
fmt.Fprintf(os.Stderr, "seed started %d summary calls below trigger\n", sink.summaryStarts)
os.Exit(1)
}
if err := sess.Save(path); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
m := meta{
SeededAt: time.Now(),
ProjectionVersion: after.ProjectionVersion,
CanonicalTokens: after.CanonicalTokens,
ProjectedTokens: after.ProjectedTokens,
TriggerTokens: after.TriggerTokens,
SummaryCalls: sink.summaryStarts,
SpentUSD: cap.spent,
}
writeMeta(dir, m)
fmt.Printf("seed ok projected=%d trigger=%d version=%d summary_calls=%d spent=$%.4f\n",
m.ProjectedTokens, m.TriggerTokens, m.ProjectionVersion, m.SummaryCalls, m.SpentUSD)
}
func resume(dir string, offline bool) {
path := filepath.Join(dir, "session.jsonl")
m := readMeta(dir)
sess, err := agent.LoadSession(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
sink := &recordSink{}
var p provider.Provider
if !offline {
p, _ = prov()
}
a := newAgent(p, sess, path, sink)
snap := a.ContextMaintenanceSnapshot()
if snap.ProjectionVersion != m.ProjectionVersion {
fmt.Fprintf(os.Stderr, "resume version = %d, want %d\n", snap.ProjectionVersion, m.ProjectionVersion)
os.Exit(1)
}
if err := a.PrepareContext(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, "resume prepare:", err)
os.Exit(1)
}
after := a.ContextMaintenanceSnapshot()
if after.ProjectionVersion != m.ProjectionVersion {
fmt.Fprintf(os.Stderr, "resume advanced version to %d\n", after.ProjectionVersion)
os.Exit(1)
}
if sink.summaryStarts != 0 {
fmt.Fprintf(os.Stderr, "resume re-ran summary %d times\n", sink.summaryStarts)
os.Exit(1)
}
fmt.Printf("resume ok idle=%s version=%d summary_calls=0\n",
time.Since(m.SeededAt).Round(time.Second), m.ProjectionVersion)
}
func cont(dir string, offline bool, cap *costCap) {
if offline {
fmt.Fprintln(os.Stderr, "continue requires live provider (omit -offline)")
os.Exit(1)
}
path := filepath.Join(dir, "session.jsonl")
m := readMeta(dir)
sess, err := agent.LoadSession(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Grow past the 85% trigger with additional fat tool turns (skip system/user prelude).
for _, msg := range fatHistory(40)[2:] {
sess.Add(msg)
}
sink := &recordSink{}
p, pricing := prov()
a := newAgent(p, sess, path, sink)
before := a.ContextMaintenanceSnapshot()
if before.ProjectedTokens < before.TriggerTokens {
fmt.Fprintf(os.Stderr, "continue fixture still below trigger: projected=%d trigger=%d\n",
before.ProjectedTokens, before.TriggerTokens)
os.Exit(1)
}
if err := a.PrepareContext(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, "continue prepare:", err)
os.Exit(1)
}
if sink.summaryStarts > 1 {
fmt.Fprintf(os.Stderr, "continue started %d summaries, want ≤1\n", sink.summaryStarts)
os.Exit(1)
}
after := a.ContextMaintenanceSnapshot()
if sink.summaryStarts == 1 && after.ProjectionVersion != m.ProjectionVersion+1 {
fmt.Fprintf(os.Stderr, "continue version = %d, want %d after one summary\n",
after.ProjectionVersion, m.ProjectionVersion+1)
os.Exit(1)
}
if u := a.LastUsage(); u != nil {
if err := cap.add(u, pricing); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
m.ContinueUsage = u
}
startsBefore := sink.summaryStarts
_ = a.Run(context.Background(), "Reply with exactly: ok")
if u := a.LastUsage(); u != nil {
if err := cap.add(u, pricing); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
if sink.summaryStarts != startsBefore {
fmt.Fprintf(os.Stderr, "post-checkpoint run started extra summaries (%d→%d)\n",
startsBefore, sink.summaryStarts)
os.Exit(1)
}
if err := sess.Save(path); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
final := a.ContextMaintenanceSnapshot()
m.ProjectionVersion = final.ProjectionVersion
m.ProjectedTokens = final.ProjectedTokens
m.CanonicalTokens = final.CanonicalTokens
m.SummaryCalls = sink.summaryStarts
m.SpentUSD = cap.spent
writeMeta(dir, m)
fmt.Printf("continue ok version=%d projected=%d summary_calls=%d spent=$%.4f\n",
m.ProjectionVersion, m.ProjectedTokens, m.SummaryCalls, m.SpentUSD)
}
func main() {
dir := flag.String("dir", "benchmarks/context-maintenance-e2e/run", "state directory for seed/resume")
trials := flag.Int("trials", 5, "comprehension trials")
dir := flag.String("dir", "benchmarks/context-maintenance-e2e/run", "state directory")
maxUSD := flag.Float64("max-usd", defaultMaxUSD, "hard cost cap for live API legs")
offline := flag.Bool("offline", false, "skip live provider (seed/resume only)")
flag.Parse()
cap := &costCap{maxUSD: *maxUSD}
switch flag.Arg(0) {
case "seed":
seed(*dir)
seed(*dir, *offline, cap)
case "resume":
resume(*dir)
case "comprehension":
comprehension(*trials)
resume(*dir, *offline)
case "continue":
cont(*dir, *offline, cap)
default:
fmt.Fprintln(os.Stderr, "usage: context-maintenance-e2e [seed|resume|comprehension]")
fmt.Fprintln(os.Stderr, "usage: context-maintenance-e2e [-dir DIR] [-max-usd N] [-offline] seed|resume|continue")
os.Exit(1)
}
}
@@ -0,0 +1,4 @@
prompt = "Exactly one setting differs across config/base.toml, config/staging.toml and config/production.toml. Write AUDIT.md naming the setting that drifted, the environment it drifted in, and both values. Do not change any config."
class = "read-only-output"
max_steps = 25
timeout_sec = 480
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
test -f AUDIT.md || { echo "AUDIT.md was never written" >&2; exit 1; }
doc="$(tr '[:upper:]' '[:lower:]' < AUDIT.md)"
for want in timeout_sec production 30 5; do
case "$doc" in
*"$want"*) ;;
*) echo "AUDIT.md never mentions $want" >&2; exit 1 ;;
esac
done
# Naming a setting that did not drift means the three files were not compared.
for absent in max_body_mb ttl_sec; do
case "$doc" in
*"$absent"*) echo "AUDIT.md reports $absent, which is identical everywhere" >&2; exit 1 ;;
esac
done
grep -q "timeout_sec = 5" config/production.toml || {
echo "config/production.toml was modified; the task is to report the drift" >&2
exit 1
}
@@ -0,0 +1 @@
All three environments are kept in lockstep. Any difference is a mistake.
@@ -0,0 +1,7 @@
[server]
port = 8080
timeout_sec = 30
max_body_mb = 8
[cache]
ttl_sec = 300
@@ -0,0 +1,7 @@
[server]
port = 8080
timeout_sec = 5
max_body_mb = 8
[cache]
ttl_sec = 300
@@ -0,0 +1,7 @@
[server]
port = 8080
timeout_sec = 30
max_body_mb = 8
[cache]
ttl_sec = 300
@@ -0,0 +1,4 @@
prompt = "common/money.py now requires a second argument: fmt_amount(cents, currency). Every call site under pkgs/alpha, pkgs/beta and pkgs/gamma still passes one argument and is broken. Dispatch the three packages in parallel with a single `fleet` call, giving each task the write_paths for its own package directory, and have each sub-agent update its package call sites to pass the currency \"USD\". Then confirm `python3 check.py` prints OK."
class = "delegation"
max_steps = 60
timeout_sec = 900
@@ -0,0 +1,15 @@
# Same migration under an aggressive project compact_ratio (0.5%), an attempt
# to put the parent under real context pressure — the one mechanism delegation
# was designed for. It did not work: the agent keeps its session small by
# writing a script instead of reading, so compaction never engaged and the
# arms measured the same thing the other shapes did. Kept as an honest record
# of the attempt, and as a config variant that still exercises the workspace
# reasonix.toml merge path.
# Forced-delegation twin: identical workdir and checks, only the prompt differs.
set -e
# 36 files across three non-overlapping packages, each needing a real read
# before any edit. This is the branch size where delegation could pay: the
# neutral prompt never mentions sub-agents, so an arm that does the work
# itself is only penalised by the clock.
python3 check.py
test -z "$(grep -rn 'fmt_amount([a-z_]*)' pkgs/ || true)"
@@ -0,0 +1,27 @@
import importlib, pathlib, sys
sys.path[:0] = ["common", "."]
bad = []
for pkg in ("alpha", "beta", "gamma"):
for p in sorted(pathlib.Path("pkgs", pkg).glob("*.py")):
if p.stem == "__init__":
continue
src = p.read_text()
if "fmt_amount(" not in src:
continue
m = importlib.import_module(f"pkgs.{pkg}.{p.stem}")
for name in dir(m):
fn = getattr(m, name)
# Only functions this module defines: fmt_amount is imported into
# the namespace and is not one of the call sites under test.
if not callable(fn) or name.startswith("_") or getattr(fn, "__module__", "") != m.__name__:
continue
try:
got = fn([{"cents": 1234}])
except TypeError as exc:
bad.append(f"{pkg}/{p.stem}.{name}: {exc}")
continue
if got and got[0] not in ("1234", "12.34 USD"):
bad.append(f"{pkg}/{p.stem}.{name}: {got[0]}")
if bad:
print("FAIL:"); [print(" ", b) for b in bad[:10]]; sys.exit(1)
print("OK")
@@ -0,0 +1,3 @@
def fmt_amount(cents, currency):
"""Format cents as a currency string, e.g. 1234 -> '12.34 USD'."""
return "%d.%02d %s" % (cents // 100, cents % 100, currency)
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def tally_credit_0(rows):
"""Tally credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_credit_1(rows):
"""Audit credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_credit_2(rows):
"""Emit credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_credit_3(rows):
"""Load credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_credit_4(rows):
"""Render credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_credit_5(rows):
"""Collect credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_credit_6(rows):
"""Summarise credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_credit_7(rows):
"""Validate credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_credit_8(rows):
"""Expand credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def load_invoice_0(rows):
"""Load invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_invoice_1(rows):
"""Render invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_invoice_2(rows):
"""Collect invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_invoice_3(rows):
"""Summarise invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_invoice_4(rows):
"""Validate invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_invoice_5(rows):
"""Expand invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_invoice_6(rows):
"""Merge invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_invoice_7(rows):
"""Filter invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_invoice_8(rows):
"""Rank invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def render_ledger_0(rows):
"""Render ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_ledger_1(rows):
"""Collect ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_ledger_2(rows):
"""Summarise ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_ledger_3(rows):
"""Validate ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_ledger_4(rows):
"""Expand ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_ledger_5(rows):
"""Merge ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_ledger_6(rows):
"""Filter ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_ledger_7(rows):
"""Rank ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_ledger_8(rows):
"""Tally ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def expand_payout_0(rows):
"""Expand payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_payout_1(rows):
"""Merge payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_payout_2(rows):
"""Filter payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_payout_3(rows):
"""Rank payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_payout_4(rows):
"""Tally payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_payout_5(rows):
"""Audit payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_payout_6(rows):
"""Emit payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_payout_7(rows):
"""Load payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_payout_8(rows):
"""Render payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def rank_quota_0(rows):
"""Rank quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_quota_1(rows):
"""Tally quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_quota_2(rows):
"""Audit quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_quota_3(rows):
"""Emit quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_quota_4(rows):
"""Load quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_quota_5(rows):
"""Render quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_quota_6(rows):
"""Collect quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_quota_7(rows):
"""Summarise quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_quota_8(rows):
"""Validate quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def validate_rebate_0(rows):
"""Validate rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_rebate_1(rows):
"""Expand rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_rebate_2(rows):
"""Merge rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_rebate_3(rows):
"""Filter rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_rebate_4(rows):
"""Rank rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_rebate_5(rows):
"""Tally rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_rebate_6(rows):
"""Audit rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_rebate_7(rows):
"""Emit rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_rebate_8(rows):
"""Load rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def filter_refund_0(rows):
"""Filter refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_refund_1(rows):
"""Rank refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_refund_2(rows):
"""Tally refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_refund_3(rows):
"""Audit refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_refund_4(rows):
"""Emit refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_refund_5(rows):
"""Load refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_refund_6(rows):
"""Render refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_refund_7(rows):
"""Collect refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_refund_8(rows):
"""Summarise refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def merge_shipment_0(rows):
"""Merge shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_shipment_1(rows):
"""Filter shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_shipment_2(rows):
"""Rank shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_shipment_3(rows):
"""Tally shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_shipment_4(rows):
"""Audit shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_shipment_5(rows):
"""Emit shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_shipment_6(rows):
"""Load shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_shipment_7(rows):
"""Render shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_shipment_8(rows):
"""Collect shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def audit_tariff_0(rows):
"""Audit tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tariff_1(rows):
"""Emit tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_tariff_2(rows):
"""Load tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_tariff_3(rows):
"""Render tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_tariff_4(rows):
"""Collect tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_tariff_5(rows):
"""Summarise tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tariff_6(rows):
"""Validate tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tariff_7(rows):
"""Expand tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tariff_8(rows):
"""Merge tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def summarise_tenant_0(rows):
"""Summarise tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tenant_1(rows):
"""Validate tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tenant_2(rows):
"""Expand tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tenant_3(rows):
"""Merge tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_tenant_4(rows):
"""Filter tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_tenant_5(rows):
"""Rank tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_tenant_6(rows):
"""Tally tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_tenant_7(rows):
"""Audit tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tenant_8(rows):
"""Emit tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def emit_voucher_0(rows):
"""Emit voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_voucher_1(rows):
"""Load voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_voucher_2(rows):
"""Render voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_voucher_3(rows):
"""Collect voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_voucher_4(rows):
"""Summarise voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_voucher_5(rows):
"""Validate voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_voucher_6(rows):
"""Expand voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_voucher_7(rows):
"""Merge voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_voucher_8(rows):
"""Filter voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def tally_credit_0(rows):
"""Tally credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_credit_1(rows):
"""Audit credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_credit_2(rows):
"""Emit credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_credit_3(rows):
"""Load credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_credit_4(rows):
"""Render credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_credit_5(rows):
"""Collect credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_credit_6(rows):
"""Summarise credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_credit_7(rows):
"""Validate credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_credit_8(rows):
"""Expand credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def load_invoice_0(rows):
"""Load invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_invoice_1(rows):
"""Render invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_invoice_2(rows):
"""Collect invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_invoice_3(rows):
"""Summarise invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_invoice_4(rows):
"""Validate invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_invoice_5(rows):
"""Expand invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_invoice_6(rows):
"""Merge invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_invoice_7(rows):
"""Filter invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_invoice_8(rows):
"""Rank invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def render_ledger_0(rows):
"""Render ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_ledger_1(rows):
"""Collect ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_ledger_2(rows):
"""Summarise ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_ledger_3(rows):
"""Validate ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_ledger_4(rows):
"""Expand ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_ledger_5(rows):
"""Merge ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_ledger_6(rows):
"""Filter ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_ledger_7(rows):
"""Rank ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_ledger_8(rows):
"""Tally ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def expand_payout_0(rows):
"""Expand payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_payout_1(rows):
"""Merge payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_payout_2(rows):
"""Filter payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_payout_3(rows):
"""Rank payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_payout_4(rows):
"""Tally payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_payout_5(rows):
"""Audit payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_payout_6(rows):
"""Emit payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_payout_7(rows):
"""Load payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_payout_8(rows):
"""Render payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def rank_quota_0(rows):
"""Rank quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_quota_1(rows):
"""Tally quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_quota_2(rows):
"""Audit quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_quota_3(rows):
"""Emit quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_quota_4(rows):
"""Load quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_quota_5(rows):
"""Render quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_quota_6(rows):
"""Collect quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_quota_7(rows):
"""Summarise quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_quota_8(rows):
"""Validate quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def validate_rebate_0(rows):
"""Validate rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_rebate_1(rows):
"""Expand rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_rebate_2(rows):
"""Merge rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_rebate_3(rows):
"""Filter rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_rebate_4(rows):
"""Rank rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_rebate_5(rows):
"""Tally rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_rebate_6(rows):
"""Audit rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_rebate_7(rows):
"""Emit rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_rebate_8(rows):
"""Load rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def filter_refund_0(rows):
"""Filter refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_refund_1(rows):
"""Rank refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_refund_2(rows):
"""Tally refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_refund_3(rows):
"""Audit refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_refund_4(rows):
"""Emit refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_refund_5(rows):
"""Load refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_refund_6(rows):
"""Render refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_refund_7(rows):
"""Collect refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_refund_8(rows):
"""Summarise refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def merge_shipment_0(rows):
"""Merge shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_shipment_1(rows):
"""Filter shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_shipment_2(rows):
"""Rank shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_shipment_3(rows):
"""Tally shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_shipment_4(rows):
"""Audit shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_shipment_5(rows):
"""Emit shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_shipment_6(rows):
"""Load shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_shipment_7(rows):
"""Render shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_shipment_8(rows):
"""Collect shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def audit_tariff_0(rows):
"""Audit tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tariff_1(rows):
"""Emit tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_tariff_2(rows):
"""Load tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_tariff_3(rows):
"""Render tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_tariff_4(rows):
"""Collect tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_tariff_5(rows):
"""Summarise tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tariff_6(rows):
"""Validate tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tariff_7(rows):
"""Expand tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tariff_8(rows):
"""Merge tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def summarise_tenant_0(rows):
"""Summarise tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tenant_1(rows):
"""Validate tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tenant_2(rows):
"""Expand tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tenant_3(rows):
"""Merge tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_tenant_4(rows):
"""Filter tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_tenant_5(rows):
"""Rank tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_tenant_6(rows):
"""Tally tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_tenant_7(rows):
"""Audit tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tenant_8(rows):
"""Emit tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def emit_voucher_0(rows):
"""Emit voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_voucher_1(rows):
"""Load voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_voucher_2(rows):
"""Render voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_voucher_3(rows):
"""Collect voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_voucher_4(rows):
"""Summarise voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_voucher_5(rows):
"""Validate voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_voucher_6(rows):
"""Expand voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_voucher_7(rows):
"""Merge voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_voucher_8(rows):
"""Filter voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def tally_credit_0(rows):
"""Tally credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_credit_1(rows):
"""Audit credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_credit_2(rows):
"""Emit credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_credit_3(rows):
"""Load credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_credit_4(rows):
"""Render credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_credit_5(rows):
"""Collect credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_credit_6(rows):
"""Summarise credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_credit_7(rows):
"""Validate credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_credit_8(rows):
"""Expand credit rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def load_invoice_0(rows):
"""Load invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_invoice_1(rows):
"""Render invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_invoice_2(rows):
"""Collect invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_invoice_3(rows):
"""Summarise invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_invoice_4(rows):
"""Validate invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_invoice_5(rows):
"""Expand invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_invoice_6(rows):
"""Merge invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_invoice_7(rows):
"""Filter invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_invoice_8(rows):
"""Rank invoice rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def render_ledger_0(rows):
"""Render ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_ledger_1(rows):
"""Collect ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_ledger_2(rows):
"""Summarise ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_ledger_3(rows):
"""Validate ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_ledger_4(rows):
"""Expand ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_ledger_5(rows):
"""Merge ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_ledger_6(rows):
"""Filter ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_ledger_7(rows):
"""Rank ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_ledger_8(rows):
"""Tally ledger rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def expand_payout_0(rows):
"""Expand payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_payout_1(rows):
"""Merge payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_payout_2(rows):
"""Filter payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_payout_3(rows):
"""Rank payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_payout_4(rows):
"""Tally payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_payout_5(rows):
"""Audit payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_payout_6(rows):
"""Emit payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_payout_7(rows):
"""Load payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_payout_8(rows):
"""Render payout rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def rank_quota_0(rows):
"""Rank quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_quota_1(rows):
"""Tally quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_quota_2(rows):
"""Audit quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_quota_3(rows):
"""Emit quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_quota_4(rows):
"""Load quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_quota_5(rows):
"""Render quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_quota_6(rows):
"""Collect quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_quota_7(rows):
"""Summarise quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_quota_8(rows):
"""Validate quota rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def validate_rebate_0(rows):
"""Validate rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_rebate_1(rows):
"""Expand rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_rebate_2(rows):
"""Merge rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_rebate_3(rows):
"""Filter rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_rebate_4(rows):
"""Rank rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_rebate_5(rows):
"""Tally rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_rebate_6(rows):
"""Audit rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_rebate_7(rows):
"""Emit rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_rebate_8(rows):
"""Load rebate rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def filter_refund_0(rows):
"""Filter refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_refund_1(rows):
"""Rank refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_refund_2(rows):
"""Tally refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_refund_3(rows):
"""Audit refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_refund_4(rows):
"""Emit refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_refund_5(rows):
"""Load refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_refund_6(rows):
"""Render refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_refund_7(rows):
"""Collect refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_refund_8(rows):
"""Summarise refund rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def merge_shipment_0(rows):
"""Merge shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_shipment_1(rows):
"""Filter shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_shipment_2(rows):
"""Rank shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_shipment_3(rows):
"""Tally shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_shipment_4(rows):
"""Audit shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_shipment_5(rows):
"""Emit shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_shipment_6(rows):
"""Load shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_shipment_7(rows):
"""Render shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_shipment_8(rows):
"""Collect shipment rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def audit_tariff_0(rows):
"""Audit tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tariff_1(rows):
"""Emit tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_tariff_2(rows):
"""Load tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_tariff_3(rows):
"""Render tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_tariff_4(rows):
"""Collect tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_tariff_5(rows):
"""Summarise tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tariff_6(rows):
"""Validate tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tariff_7(rows):
"""Expand tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tariff_8(rows):
"""Merge tariff rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def summarise_tenant_0(rows):
"""Summarise tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tenant_1(rows):
"""Validate tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tenant_2(rows):
"""Expand tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tenant_3(rows):
"""Merge tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_tenant_4(rows):
"""Filter tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_tenant_5(rows):
"""Rank tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_tenant_6(rows):
"""Tally tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_tenant_7(rows):
"""Audit tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tenant_8(rows):
"""Emit tenant rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def emit_voucher_0(rows):
"""Emit voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_voucher_1(rows):
"""Load voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_voucher_2(rows):
"""Render voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_voucher_3(rows):
"""Collect voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_voucher_4(rows):
"""Summarise voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_voucher_5(rows):
"""Validate voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_voucher_6(rows):
"""Expand voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_voucher_7(rows):
"""Merge voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_voucher_8(rows):
"""Filter voucher rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,5 @@
# Reasonix project configuration.
[agent]
compact_ratio = 0.005
soft_compact_ratio = 0.003
@@ -0,0 +1,4 @@
prompt = "common/money.py now requires a second argument: fmt_amount(cents, currency). Every call site under pkgs/alpha, pkgs/beta and pkgs/gamma still passes one argument and is broken. Update every call site to pass the currency \"USD\", leaving all other code untouched, so that `python3 check.py` prints OK. The three packages share no files."
class = "delegation"
max_steps = 60
timeout_sec = 900
@@ -0,0 +1,14 @@
# Same migration under an aggressive project compact_ratio (0.5%), an attempt
# to put the parent under real context pressure — the one mechanism delegation
# was designed for. It did not work: the agent keeps its session small by
# writing a script instead of reading, so compaction never engaged and the
# arms measured the same thing the other shapes did. Kept as an honest record
# of the attempt, and as a config variant that still exercises the workspace
# reasonix.toml merge path.
set -e
# 36 files across three non-overlapping packages, each needing a real read
# before any edit. This is the branch size where delegation could pay: the
# neutral prompt never mentions sub-agents, so an arm that does the work
# itself is only penalised by the clock.
python3 check.py
test -z "$(grep -rn 'fmt_amount([a-z_]*)' pkgs/ || true)"
@@ -0,0 +1,27 @@
import importlib, pathlib, sys
sys.path[:0] = ["common", "."]
bad = []
for pkg in ("alpha", "beta", "gamma"):
for p in sorted(pathlib.Path("pkgs", pkg).glob("*.py")):
if p.stem == "__init__":
continue
src = p.read_text()
if "fmt_amount(" not in src:
continue
m = importlib.import_module(f"pkgs.{pkg}.{p.stem}")
for name in dir(m):
fn = getattr(m, name)
# Only functions this module defines: fmt_amount is imported into
# the namespace and is not one of the call sites under test.
if not callable(fn) or name.startswith("_") or getattr(fn, "__module__", "") != m.__name__:
continue
try:
got = fn([{"cents": 1234}])
except TypeError as exc:
bad.append(f"{pkg}/{p.stem}.{name}: {exc}")
continue
if got and got[0] not in ("1234", "12.34 USD"):
bad.append(f"{pkg}/{p.stem}.{name}: {got[0]}")
if bad:
print("FAIL:"); [print(" ", b) for b in bad[:10]]; sys.exit(1)
print("OK")
@@ -0,0 +1,3 @@
def fmt_amount(cents, currency):
"""Format cents as a currency string, e.g. 1234 -> '12.34 USD'."""
return "%d.%02d %s" % (cents // 100, cents % 100, currency)
Whitespace-only changes.
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def tally_credit_0(rows):
"""Tally credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_credit_1(rows):
"""Audit credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_credit_2(rows):
"""Emit credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_credit_3(rows):
"""Load credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_credit_4(rows):
"""Render credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_credit_5(rows):
"""Collect credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_credit_6(rows):
"""Summarise credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_credit_7(rows):
"""Validate credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_credit_8(rows):
"""Expand credit rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def load_invoice_0(rows):
"""Load invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_invoice_1(rows):
"""Render invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_invoice_2(rows):
"""Collect invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_invoice_3(rows):
"""Summarise invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_invoice_4(rows):
"""Validate invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_invoice_5(rows):
"""Expand invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_invoice_6(rows):
"""Merge invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_invoice_7(rows):
"""Filter invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_invoice_8(rows):
"""Rank invoice rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def render_ledger_0(rows):
"""Render ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_ledger_1(rows):
"""Collect ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_ledger_2(rows):
"""Summarise ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_ledger_3(rows):
"""Validate ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_ledger_4(rows):
"""Expand ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_ledger_5(rows):
"""Merge ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_ledger_6(rows):
"""Filter ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_ledger_7(rows):
"""Rank ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_ledger_8(rows):
"""Tally ledger rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def expand_payout_0(rows):
"""Expand payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_payout_1(rows):
"""Merge payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_payout_2(rows):
"""Filter payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_payout_3(rows):
"""Rank payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_payout_4(rows):
"""Tally payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_payout_5(rows):
"""Audit payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_payout_6(rows):
"""Emit payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_payout_7(rows):
"""Load payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_payout_8(rows):
"""Render payout rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def rank_quota_0(rows):
"""Rank quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_quota_1(rows):
"""Tally quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_quota_2(rows):
"""Audit quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_quota_3(rows):
"""Emit quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_quota_4(rows):
"""Load quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_quota_5(rows):
"""Render quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_quota_6(rows):
"""Collect quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_quota_7(rows):
"""Summarise quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_quota_8(rows):
"""Validate quota rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def validate_rebate_0(rows):
"""Validate rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_rebate_1(rows):
"""Expand rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_rebate_2(rows):
"""Merge rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_rebate_3(rows):
"""Filter rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_rebate_4(rows):
"""Rank rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_rebate_5(rows):
"""Tally rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_rebate_6(rows):
"""Audit rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_rebate_7(rows):
"""Emit rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_rebate_8(rows):
"""Load rebate rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def filter_refund_0(rows):
"""Filter refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_refund_1(rows):
"""Rank refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_refund_2(rows):
"""Tally refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_refund_3(rows):
"""Audit refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_refund_4(rows):
"""Emit refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_refund_5(rows):
"""Load refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_refund_6(rows):
"""Render refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_refund_7(rows):
"""Collect refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_refund_8(rows):
"""Summarise refund rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def merge_shipment_0(rows):
"""Merge shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_shipment_1(rows):
"""Filter shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_shipment_2(rows):
"""Rank shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_shipment_3(rows):
"""Tally shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_shipment_4(rows):
"""Audit shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_shipment_5(rows):
"""Emit shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_shipment_6(rows):
"""Load shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_shipment_7(rows):
"""Render shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_shipment_8(rows):
"""Collect shipment rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def audit_tariff_0(rows):
"""Audit tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tariff_1(rows):
"""Emit tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_tariff_2(rows):
"""Load tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_tariff_3(rows):
"""Render tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_tariff_4(rows):
"""Collect tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_tariff_5(rows):
"""Summarise tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tariff_6(rows):
"""Validate tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tariff_7(rows):
"""Expand tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tariff_8(rows):
"""Merge tariff rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def summarise_tenant_0(rows):
"""Summarise tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tenant_1(rows):
"""Validate tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tenant_2(rows):
"""Expand tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tenant_3(rows):
"""Merge tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_tenant_4(rows):
"""Filter tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_tenant_5(rows):
"""Rank tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_tenant_6(rows):
"""Tally tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_tenant_7(rows):
"""Audit tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tenant_8(rows):
"""Emit tenant rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def emit_voucher_0(rows):
"""Emit voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_voucher_1(rows):
"""Load voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_voucher_2(rows):
"""Render voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_voucher_3(rows):
"""Collect voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_voucher_4(rows):
"""Summarise voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_voucher_5(rows):
"""Validate voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_voucher_6(rows):
"""Expand voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_voucher_7(rows):
"""Merge voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_voucher_8(rows):
"""Filter voucher rows for the alpha package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
Whitespace-only changes.
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def tally_credit_0(rows):
"""Tally credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_credit_1(rows):
"""Audit credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_credit_2(rows):
"""Emit credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_credit_3(rows):
"""Load credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_credit_4(rows):
"""Render credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_credit_5(rows):
"""Collect credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_credit_6(rows):
"""Summarise credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_credit_7(rows):
"""Validate credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_credit_8(rows):
"""Expand credit rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def load_invoice_0(rows):
"""Load invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_invoice_1(rows):
"""Render invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_invoice_2(rows):
"""Collect invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_invoice_3(rows):
"""Summarise invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_invoice_4(rows):
"""Validate invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_invoice_5(rows):
"""Expand invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_invoice_6(rows):
"""Merge invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_invoice_7(rows):
"""Filter invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_invoice_8(rows):
"""Rank invoice rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def render_ledger_0(rows):
"""Render ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_ledger_1(rows):
"""Collect ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_ledger_2(rows):
"""Summarise ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_ledger_3(rows):
"""Validate ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_ledger_4(rows):
"""Expand ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_ledger_5(rows):
"""Merge ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_ledger_6(rows):
"""Filter ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_ledger_7(rows):
"""Rank ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_ledger_8(rows):
"""Tally ledger rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def expand_payout_0(rows):
"""Expand payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_payout_1(rows):
"""Merge payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_payout_2(rows):
"""Filter payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_payout_3(rows):
"""Rank payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_payout_4(rows):
"""Tally payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_payout_5(rows):
"""Audit payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_payout_6(rows):
"""Emit payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_payout_7(rows):
"""Load payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_payout_8(rows):
"""Render payout rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def rank_quota_0(rows):
"""Rank quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_quota_1(rows):
"""Tally quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_quota_2(rows):
"""Audit quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_quota_3(rows):
"""Emit quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_quota_4(rows):
"""Load quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_quota_5(rows):
"""Render quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_quota_6(rows):
"""Collect quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_quota_7(rows):
"""Summarise quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_quota_8(rows):
"""Validate quota rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def validate_rebate_0(rows):
"""Validate rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_rebate_1(rows):
"""Expand rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_rebate_2(rows):
"""Merge rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_rebate_3(rows):
"""Filter rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_rebate_4(rows):
"""Rank rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_rebate_5(rows):
"""Tally rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_rebate_6(rows):
"""Audit rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_rebate_7(rows):
"""Emit rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_rebate_8(rows):
"""Load rebate rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def filter_refund_0(rows):
"""Filter refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_refund_1(rows):
"""Rank refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_refund_2(rows):
"""Tally refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_refund_3(rows):
"""Audit refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_refund_4(rows):
"""Emit refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_refund_5(rows):
"""Load refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_refund_6(rows):
"""Render refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_refund_7(rows):
"""Collect refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_refund_8(rows):
"""Summarise refund rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def merge_shipment_0(rows):
"""Merge shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_shipment_1(rows):
"""Filter shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_shipment_2(rows):
"""Rank shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_shipment_3(rows):
"""Tally shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_shipment_4(rows):
"""Audit shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_shipment_5(rows):
"""Emit shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_shipment_6(rows):
"""Load shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_shipment_7(rows):
"""Render shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_shipment_8(rows):
"""Collect shipment rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def audit_tariff_0(rows):
"""Audit tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tariff_1(rows):
"""Emit tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_tariff_2(rows):
"""Load tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_tariff_3(rows):
"""Render tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_tariff_4(rows):
"""Collect tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_tariff_5(rows):
"""Summarise tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tariff_6(rows):
"""Validate tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tariff_7(rows):
"""Expand tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tariff_8(rows):
"""Merge tariff rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def summarise_tenant_0(rows):
"""Summarise tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_tenant_1(rows):
"""Validate tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_tenant_2(rows):
"""Expand tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_tenant_3(rows):
"""Merge tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_tenant_4(rows):
"""Filter tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_tenant_5(rows):
"""Rank tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_tenant_6(rows):
"""Tally tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_tenant_7(rows):
"""Audit tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def emit_tenant_8(rows):
"""Emit tenant rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def emit_voucher_0(rows):
"""Emit voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def load_voucher_1(rows):
"""Load voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def render_voucher_2(rows):
"""Render voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def collect_voucher_3(rows):
"""Collect voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_voucher_4(rows):
"""Summarise voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_voucher_5(rows):
"""Validate voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_voucher_6(rows):
"""Expand voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_voucher_7(rows):
"""Merge voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_voucher_8(rows):
"""Filter voucher rows for the beta package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
Whitespace-only changes.
@@ -0,0 +1,76 @@
import sys
sys.path.insert(0, "common")
from money import fmt_amount
def collect_batch_0(rows):
"""Collect batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def summarise_batch_1(rows):
"""Summarise batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def validate_batch_2(rows):
"""Validate batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def expand_batch_3(rows):
"""Expand batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def merge_batch_4(rows):
"""Merge batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def filter_batch_5(rows):
"""Filter batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def rank_batch_6(rows):
"""Rank batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
def tally_batch_7(rows):
"""Tally batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(fmt_amount(value))
return out
def audit_batch_8(rows):
"""Audit batch rows for the gamma package."""
out = []
for row in rows:
value = int(row.get("cents", 0))
out.append(str(value))
return out
Loaded 100 of 1336 files, more files were not shown because too many files have changed in this diff. Show more