diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index 28b5aad408..0623c0b194 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -1822,6 +1822,751 @@ performance holds remain separate. Full source/wheel correctness and a matched, unprofiled default-SQLite whole-path comparison are still required for a new implementation; all earlier milestone, adoption and complete-cost gaps remain. +### 2026-11-07 — Reuse complete Todo display sources within a managed request + +The existing `todo.context.page` owner can compose summary, planning and retained +terminal display views from one complete supplied source. The quota preparation +caller explicitly requests the Agent source's three views and passes retained +terminal rows to its frontier consumer. User-source and general display callers +keep their requested single view. Source selection and each display rule retain +their existing owners. The orchestration fallback reuses an independent copy of +the already selected summary instead of requesting it again. + +Inside an already revision-pinned logical request, exact source JSON and request +options bound the transport memo. Changed source facts and later requests +revalidate; mutable callers receive independent decoded values. Large opt-in +inputs keep the original per-view transport, and incomplete bundles fail before +memo admission. The batch input threshold does not predict response size or +promise inline transport. An explicit multi-view request can use the existing +private response snapshot when the combined output exceeds the socket limit. + +Independent review of the initial proposal found that the generic transport +expanded single-view calls into three views. An 8,000-row delivery-response +caller retained the same final output but materialized about 11.18 MB of unused +combined response instead of its roughly 3.73 MB single-view socket response. +That performance failure is retained. The corrected caller-owned composition +removes the implicit expansion; matching single-view and complete governed +source/wheel qualification are recorded with the revision being proposed. + +The initial proposal against `main@32167518b3` passed six synthetic source and isolated-wheel +101-Todo File/SQLite governed CLI lifecycles. They retained the verified artifact, +original completion time, four settlement receipts, one debit, Effect restart +recovery and rejected evidence replacement. A dropped write-scope response +mutation fails at the independent public-guard scope check. Frozen display +fixtures and focused source-isolation tests also cover the existing three views. +One profiled SQLite guard reduced JSON parameter bytes from about 1.900 MB to +1.556 MB and calls from 89 to 87; loaded modules remained 503. These counts are +diagnostic and do not qualify complete wire cost, latency or long-horizon benefit. + + +The corrected single-call comparison uses the same synthetic source on +`main@dee549663`, the reviewed defective head and the repaired source. All three +produce the same final delivery response. The repaired request parameters and serialized response +bytes match main exactly; its nonempty response snapshot bytes are +zero for both row counts. Five warm calls per arm gave the following exploratory +medians, including the display and delivery-response projections: + +| Rows | Main seconds | Defective proposal seconds | Repair seconds | Defective response snapshot bytes | Repair snapshot bytes | +| --- | --- | --- | --- | --- | --- | +| 8,000 | 0.159 | 0.636 | 0.161 | 9,480,165 | 0 | +| 15,000 | 0.284 | 1.344 | 0.286 | 17,775,165 | 0 | + +This reproduces and removes the single-view regression. It does not qualify the +complete CLI, daemon CPU/memory, full marginal cost or long-horizon benefit. + + +The repaired source and isolated wheel pass six 101-Todo File/SQLite governed +CLI lifecycles with the verified artifact, original completion time, four +receipts, one debit, restart and refusal/replay checks. A recursive dropped-scope +response mutation fails at the independent public guard oracle. The isolated +PostgreSQL 16.15 suites pass 390 store and 11 service cases with zero skipped. + +Three interleaved unprofiled default-SQLite whole-CLI pairs have median wall time +12.24 seconds on main and 12.71 seconds on the repair. Two candidate runs are +slower; ranges overlap. A separate profiled guard reduces parameter bytes from +1,899,603 to 1,546,162 and calls from 90 to 87, with 503 modules unchanged. +These observations do not establish a whole-path performance benefit. Daemon +CPU/memory, total wire/file writes, complete marginal cost and adoption remain +unqualified; the provider-first transaction remains the next accepted boundary. + +This is a reversible display-transport increment, not the complete provider-first +Todo projection transaction. Provider reads, revisions, lease/actor fences and +state writes retain their existing owners. The remaining canonical-source +transaction, matched complete-cost qualification and affected backend acceptance +stay open. I3 remains at least one minor release; M1/#8011, full R0/V10/R1, +D2/D5/D6/D3, context/model/split-install, adoption and all frozen failures remain. + +#### Provider-first transaction requirements before the next proposal + +The following records the prerequisite gap at the display-only revision. The +next checkpoint reports the canonical transaction proposal and its measured +limits; it does not turn these earlier observations into a historical pass. + +The next implementation must compose the existing provider-admitted +`canonicalTodoCollection` and projection owners. The native snapshot owner binds +Goal, store incarnation, provider revision, cursor and query to complete rows, +leases, read-model validation and acceptance guards. A continuation is a position +within that read, not a reusable current-head handle; a changed snapshot restarts +the complete read. Displaying a retained source does not obtain fresh authority. + +`quota_selection.py` still sends a complete summary source alongside encoded +selection rows and source-row facts. Its request also includes the clock, +actor/profile, capabilities, closure evidence, route/handoff facts and resume +inputs. Move canonical row materialization into the existing typed owner before +collapsing that transport. Preserve the Python-value codec where strict integer, +boolean or text distinctions remain observable. Provider rows cannot substitute +for independent caller evidence or pending intent; all-done counts alone cannot +reconstruct a closure declaration. + +`goal_vision_wait` separately consumes complete lineage, scoped blockers, acceptance gaps and resume facts. The combined transaction must preserve each +causal obligation, including when two obligations share a witness. Full notes, +write scopes, archive/settled identity and source provenance remain part of its +parity oracle. + +The real PostgreSQL integration case connects the existing native snapshot read +to all three display views, checks unchanged provider/source data, observes a +new provider revision after a separate commit, and refuses a stale continuation. +This qualifies a composition prerequisite. The combined guard transaction, +PostgreSQL source/wheel host journey and complete-cost benefit remain open. + +### 2026-11-07 — Canonical Todo transaction proposal and observed lifecycle cost + +The proposal materializes canonical selection and resume rows in the existing +TypeScript owners after binding actor/profile/clock inputs. `todo.read` composes +the Agent source's explicitly requested summary, planning and retained-terminal +views, the User summary, quota selection and the existing Vision-wait basis. +Within that quota request, matching native references share one complete +provider read. Complete field digests, store incarnation, provider revision, +cursor and the original query/selection replay recipe bind the source. A later +request reads again; this is not a cache of provider authority. + +The actual governed CLI query now retains its source recipe through quota +preparation. Previously, the full query response lost the ephemeral origin +even though status had preserved it. Both query and selection paths now carry +the complete provenance into the same typed transaction. Provider outages keep +their original diagnostic code/payload, and stale references fail before +projection. Source fields, Python scalar facts and owner-declared Todo source +lanes cannot be overwritten through reference metadata. Independent caller +metadata remains complete; callers that transform provider rows use the full +supplied-source codec. Strict integer/float/bool and list/tuple distinctions, +legacy fnmatch facts, lane/ordinal precedence and distant microsecond clocks +retain their observable semantics. No Todo-id lookup restores lost provenance. + +The existing Vision-wait owner retains causal obligations and shared witnesses. +Prepared basis reuse requires the same actor, complete terminal rows and logical +request identity; independent lineage callers keep their existing path. A +standalone display still requests one view. Complete notes, evidence, write +scopes, archive/replay identity and independent closure/pending intent remain +in the parity boundary. Existing source-lane constants are exported for native +provenance validation; no new lane vocabulary, persisted state or authority is +introduced. + +Production code at `66ff8bf48`, compared with `main@e2371c476`, passes four +101-Todo source/isolated-wheel × File/default-SQLite CLI lifecycles. A separate +verifier accepts sum=45; original completion time, four receipts, one debit, +Effect restart, invalid Vision/workspace refusal, evidence-replacement refusal +and settled replay remain intact. The wheel's affected sources match the source +checkout. The final focused suites pass 132 Python and 86 TypeScript cases; +the broader real CLI suite passed 244 cases before the final diagnostic/lane +corrections, which the focused suites cover. Real disposable PostgreSQL 16.15 +passes 391 store and 11 service cases with zero skips, including two-role native +quota composition, one complete provider load, unchanged data and stale-source +refusal. This does not qualify a PostgreSQL source/wheel host journey. + +Five interleaved, unprofiled source CLI pairs use the same 101-Todo fixture and +default SQLite provider. Each runs provenance, bootstrap, claim, guard, required +context reads, artifact verification, completion, negative cases, writeback, +spend, restart and terminal/refusal replay. Fixture setup and final teardown are +reported separately. Normal bytecode conditions are inherited equally. The +candidate is slower in three pairs and wall-time ranges overlap: + +| Observation, median unless stated | Main | Proposal | +| --- | ---: | ---: | +| Lifecycle wall time (range) | 14.093 s (13.765–14.933) | 14.145 s (13.461–15.190) | +| Fixture setup / final teardown | 2.403 / 0.062 s | 2.338 / 0.062 s | +| CLI child CPU + observed daemon CPU (historical, invalid unit conversion) | 5.511 s | 5.757 s | +| Daemon boundary physical footprint | 140,787,912 B | 146,456,920 B | +| CLI maximum resident size | 70,746,112 B | 70,221,824 B | +| Observed daemon disk / logical writes | 9,048,064 / 14,090,240 B | 10,227,712 / 15,646,720 B | +| CLI output / required context bytes | 177,595 / 21,168 B | 179,482 / 21,257 B | + +The CPU row above used an invalid Mach-clock conversion and must not be used +as CPU evidence. Three independent CPU-bound child calibrations show that this +macOS arm64 runtime reports daemon usage in Mach ticks: multiply by the measured +timebase `125/3` before converting nanoseconds to seconds. The original raw +observations remain unchanged. Correcting the five historical pairs gives +7.071→7.469 seconds of combined observed CPU including setup daemon work. +Subtracting each initial daemon baseline gives 6.706→7.107 seconds for the CLI +episode; corresponding daemon disk/logical writes are +8,892,416/13,783,040→10,072,064/15,355,904 bytes. These corrected observations +still do not demonstrate a whole-path benefit. + +Daemon CPU/writes are cumulative process observations at CLI boundaries, +excluding final exit work. CLI CPU excludes the unsampled provenance process +and harness work. CLI RSS and daemon footprint are separate observations, not a +continuous full-system peak. These bounds do not measure complete model cost, +human attention or installed-host/long-horizon benefit. + +A separate instrumented pair observes the following communication delta; its +elapsed time is excluded from the wall-time comparison: + +| Diagnostic | Main | Proposal | +| --- | ---: | ---: | +| Guard RPCs / JSON parameter bytes | 90 / 1,903,233 | 84 / 837,940 | +| Full episode socket sent / received bytes | 3,537,350 / 6,380,604 | 1,948,250 / 6,388,112 | +| Loaded guard LoopX modules | 503 | 503 | + +Both guard role sources use the native transaction. Fewer request bytes and +calls demonstrate transport adoption, while received bytes, observed writes +and CPU do not improve together. Whole-path acceleration or complete marginal +cost benefit is therefore **not qualified**. The next decision remains with +this transaction's original acceptance: independent review and attribution of +the remaining lifecycle costs before proposing further optimization. These +negative results do not justify another leaf-RPC migration or a new provider. + +The proposal was rebased onto `main@e58ba30bc`. A final scope-fit check found that +the read transport's provenance binding exceeded the decision-function budget. +That complete binding and selected-row evaluation restoration were extracted +into a local helper; 24 focused parity cases and the maintainability ratchet +pass. The cost table remains the observation at `66ff8bf48`, before this helper +extraction, and is not an exact-final-head cost qualification. TypeScript +typecheck and Ruff pass. Mypy retains main's existing missing return annotation in +`decision_capture.py`; the standard premerge also retains main's peer-boundary +scanner failure on unchanged benchmark documentation. Neither is a passing +skip. Required exact-head review, validation/readiness and maintainer merge +authorization remain separate; there is no merge or installed-host upgrade. +The original task stays open for benefit and remaining acceptance. Retain I3 +for at least one minor release, M1/#8011, full R0/V10/R1, D2/D5/D6/D3, +context/model/split-install, adoption/full-cost gaps and every frozen failure. + +### 2026-11-08 — Complete quota response transport and calibrated cost + +Independent review reproduced a remaining regression at `e7db1711f`: an +8,000-row composed quota result serialized 13,080,176 bytes and caused +13,080,177 bytes of synchronous response-snapshot writes. Main's separate +views stayed below that response threshold. The existing Todo read adapter now +uses lossless gzip for quota results above the shared 8 MiB inline budget and +within the existing complete-frame budget. It returns the existing result-piece +envelope with encoding and decoded extent, keeping base64 pieces within the +socket budget. Larger reads retain their existing paging. The Python adapter +checks complete digest/identity, bounded decoded size, exact extent and one +complete gzip member; unknown encoding, truncation and trailing data fail. +The original response-budget value is shared with the server rather than +duplicated. No provider cache, persisted state, rule owner or authority changes. + +The same 8,000-row diagnostic now emits 561,291 wire bytes with zero response +snapshot bytes. Complete results match the previous independent summary, +planning and terminal views; unknown fields and Unicode remain intact. A +single diagnostic elapsed time is not acceleration evidence. Twelve transport +cases plus the existing 24 parity cases pass; two native File/SQLite 8,000-row +quota cases preserve both role sources, complete views and provider data/revision +with no response snapshots. Twenty-two TypeScript context/quota cases and eight +real receipt/replan/reentry cases pass. The earlier over-specific test oracle +failed because display owners intentionally expose different fields; the final +oracle compares their complete independent outputs rather than changing them. + +Production `2c9d6360e`, based on `main@8a4d4f9da`, passes four fresh 101-Todo +source/isolated-wheel × File/default-SQLite CLI lifecycles, including sum=45, +original completion time, four receipts, one debit, restart and negative/replay +cases. The final wheel matches the affected source files. Real disposable +PostgreSQL 16.15 again passes 391 store and 11 service cases, zero skips. +TypeScript typecheck, Ruff and the maintainability ratchet pass. These checks +do not qualify a PostgreSQL source/wheel host journey or installed-host adoption. + +Five new interleaved unprofiled default-SQLite pairs use the unchanged 101-Todo +fixture, full negative/replay lifecycle and inherited bytecode conditions. No +heavy tests run concurrently. CPU uses the calibrated Mach timebase `125/3`; +daemon CPU/writes subtract the initial setup baseline: + +| Observation, median unless stated | Main | Proposal | +| --- | ---: | ---: | +| Lifecycle wall time (range) | 13.894 s (12.686–16.481) | 14.974 s (14.131–15.344) | +| Fixture setup / final teardown | 2.347 / 0.062 s | 2.299 / 0.062 s | +| CLI child + observed daemon CPU after setup | 7.086 s | 7.403 s | +| Observed daemon disk / logical writes after setup | 8,884,224 / 13,795,328 B | 10,076,160 / 15,130,624 B | +| Daemon boundary physical footprint | 141,032,832 B | 150,520,728 B | +| CLI maximum resident size | 71,467,008 B | 71,696,384 B | + +Four candidate runs are slower and wall ranges overlap. The large-response +write regression is repaired, but complete lifecycle benefit remains +**unqualified**. Final daemon exit, unsampled provenance/restart helpers, +harness CPU, continuous system peak, model cost and human attention remain +outside these observations. The original transaction task stays open for +independent exact-head review and attribution of the remaining CPU/write cost. +Earlier premerge peer-boundary and mypy failures remain validation holds until +an exact final check proves otherwise. Retain I3 for at least one minor release, +M1/#8011, full R0/V10/R1, D2/D5/D6/D3, context/model/split-install, +adoption/full-cost gaps and all frozen failures. + +### 2026-11-08 — Legacy projection kind and remaining native read cost + +Independent review found that source precedence survived composition but its +projection kind did not. When canonical and legacy asset inputs referenced the +same dictionary, object identity selected the user-summary path even when the +existing precedence rule had chosen the legacy asset. Legacy string counts +became null; a `next`-only input bypassed the missing-Todo-ID refusal. + +Production `4c3ed5d39` makes the existing selector return both source and its +existing projection kind. Every display/inventory caller still uses the same +selected source. No new vocabulary, persisted field or authority is introduced. +Final static validation required an explicit variadic source-candidate tuple +annotation. Its execution bytecode remains identical; the typing failure is +retained and the corrected mypy run leaves only main's existing annotation hold. +Thirteen new cases compare complete summaries with the independent original +selector for both roles, shared/copy inputs, canonical precedence and actual +guard selection/refusal. Six fail at the preceding head; all pass after repair. +The first test draft assumed numeric legacy counts and an unbound guard; its +failures are retained, and the final oracle preserves the original string +values and bound Agent/Turn path. The focused suite passes 96 cases. Source and +isolated wheel independently preserve legacy counts and refuse missing IDs; +four fresh 101-Todo source/wheel × File/SQLite lifecycles preserve the artifact, +completion time, four receipts, one debit, restart and negative/replay checks. +Real disposable PostgreSQL again passes 391 store and 11 service cases with +zero skips; its server is stopped. + +Attribution of the preceding five paired cost runs points to repeated native +reads during guard, writeback, spend and settled replay. A separate instrumented +complete lifecycle observes eight additional native `todo.read` calls, each +adding 147,456 observed daemon disk-write bytes: 1,179,648 bytes in total. +Instrumented elapsed times are excluded from qualification. A standalone probe +of the exact native snapshot owner records four read-only SQLite connections +per snapshot and the same 147,456-byte process-counter increment. Alternating +identity reads use one read-only connection and 36,864 bytes; provider revision +and database content digest remain unchanged. These observations locate read +lifecycle cost; they do not identify physical-device writes or justify removing +identity, revision, WAL, lease or recovery checks. + +The Python `invalid-vision` profile is dominated by imports with the same call +counts in both arms. Its wall weights do not attribute the paired CPU difference +or justify another import migration. The next accepted work is a bounded study +of the existing SQLite read/connection lifecycle, preserving fresh incarnation +and revision checks through the typed owners. Provider edits require their +ordinary task scope/ownership preparation. The previous corrected cost table +remains at `2c9d6360e`, not this compatibility repair; no fresh speedup claim is +made. Keep the original task, independent exact-head review, whole-path/full-cost +and host/model/adoption acceptance open, with I3, M1/#8011, R0/V10/R1, +D2/D5/D6/D3, context/split-install and all frozen failures retained. + +### 2026-11-07: selected SQLite snapshot read lifecycle + +The existing `AuthorityStore` now offers an optional storage-only paired +identity/head read, consumed by the existing canonical snapshot owner. +`SqliteAuthorityStore` exposes it only for `existingOnly` stores. Identity and +the complete proven head share one synchronous read transaction; no connection, +head or provider authority is retained between calls. General SQLite stores, +File, PostgreSQL and custom stores without the optional method retain the +original identity/load ordering and failure precedence. No persisted state, +wire vocabulary, capability, selector or Python rule owner is added. + +Provider selection still validates the configured incarnation, and snapshot +admission still independently reopens the pathname after loading the head. +Revision, cursor, full read-model digest, stale continuation, WAL, lease and +receipt/replay checks remain. A real directory replacement after the head read +is refused without partial rows. Suppressing the final incarnation check in a +private mutation makes that same public snapshot oracle fail. + +Runtime commit `61f7c3c33` passes 873 related TypeScript cases and type checking, +four complete source/wheel File/SQLite 101-Todo lifecycles, and isolated real +PostgreSQL 16.15 store391/service11, with no skips. The lifecycles preserve sum45, +original completion time, four receipts, one debit, restart and refusal/replay; +all 23 changed production files match the wheel. Native before/head diagnostics +retain identical complete response and database hashes, reducing read-only +connections from four to three and observed daemon disk-counter bytes from +147,456 to 110,592 per snapshot. Those counters do not measure physical-device +writes, and the instrumented diagnostic does not qualify elapsed performance. + +Five new interleaved, unprofiled complete default-SQLite groups compare the +same 101-Todo fixture at main `8a4d4f9da`, prior `c40d8ec31` and runtime +`61f7c3c33`, on CPython3.13.5 / Node26.0.0 / qualified SQLite3.53.1. All fifteen +lifecycles preserve the same independently verified outcome and settlement. + +| Observed metric, median (range where shown) | Main | Prior | Paired read | +| --- | --- | --- | --- | +| Lifecycle wall time | 14.945 s (12.664–16.597) | 14.924 s (14.570–19.232) | 14.871 s (14.412–16.775) | +| CLI + sampled daemon CPU after setup | 7.084 s | 7.388 s | 7.292 s | +| Daemon disk-counter bytes after setup | 8,896,512 | 10,076,160 | 8,314,880 | +| Daemon logical-write counter bytes after setup | 13,897,728 | 15,618,048 | 12,840,960 | +| Fixture setup / final teardown wall time | 2.373 / 0.060 s | 2.387 / 0.062 s | 2.336 / 0.062 s | + +Two of five candidate episodes are slower than main and two slower than the +prior head; the wall-time ranges overlap. Reduced observed writes do not remove +the higher median CPU than main or qualify overall benefit. CPU uses the +verified Mach timebase125/3, subtracting each daemon's setup baseline. Sampling +excludes final daemon exit, unsampled restart helpers, provenance and harness +CPU. CLI RSS and daemon boundary footprint are separate observations, not a +continuous whole-system peak. No heavy validation ran during these groups; +model cost, human attention and installed-host adoption remain unmeasured. +These new observations do not replace or relabel any historical campaign. +The next route is independent review and attribution of the remaining complete +CLI CPU cost before another optimization, retaining the fresh read fences. + +The standard premerge passes six direct checks and 18 of 19 selected checks. +The peer hard-cut scanner still fails on the same unchanged benchmark README +at both main and candidate. The original task remains open for independent +exact-head review, matched whole-path/full marginal costs and host/model/adoption +acceptance. Preserve I3 for a minor release, M1/#8011, R0/V10/R1, D2/D5/D6/D3, +context/split-install and every prior failure; neither connection counts nor +passing validation establishes the complete minimal-kernel outcome. + +Latest-main integration qualification at `ce0e41a26` over `df67371c3` retains +the same 23 PR production-source hashes. Fresh source/wheel File/SQLite four +complete lifecycles, 49 TypeScript cases, types and isolated PostgreSQL391/11 +pass. The saved-context/legacy Python checks report 107 passed and 14 skipped; +those explicit native Codex cases remain unqualified. The inherited missing +return annotation is repaired on main and fresh kernel mypy passes; retain its +historical failure. Standard premerge still passes six direct and 18/19 +selected checks, with the existing peer scanner failure. + +Five fresh unprofiled default-SQLite pairs at this integration preserve the same +101-Todo outcome. Main/candidate lifecycle wall medians are 14.631/14.633 s +(ranges14.260–16.869/14.122–14.941); observed CLI plus daemon CPU after setup is +7.038/7.080 s; daemon disk-counter bytes are 8,880,128/8,290,304 and logical-write +counter bytes14,966,784/14,282,752. Fixture setup medians2.362/2.346 s and final +teardown0.062/0.058 s remain separate. Two candidates are slower and ranges +overlap. The same sampling exclusions above apply; reduced write counters do +not qualify a complete speedup or marginal-cost/adoption benefit. Complete the +remaining cost coverage and establish any repeatable CPU regression before +choosing further implementation; do not assume that another read/import +optimization is justified. + +### Complete-cost coverage and current-main transaction checkpoint + +The existing proposal is integrated at `23227ee5` with main `0c28f6004`. +Its 23 PR production-source hashes remain unchanged. Fresh checks pass 59 +focused Python and 63 TypeScript cases, typing/mypy, four complete source/wheel +File/SQLite 101-Todo lifecycles and isolated PostgreSQL16.15 store391/service11, +with no skips in those runs. The same artifact, original completion time, +receipts, debit, restart and refusals/replay are preserved. Standard premerge +retains the existing peer scanner failure (six direct and18/19 selected pass). +The first PostgreSQL attempt lacked the checkout's `pg` dependency; its failure +is retained separately from the successful installed-environment run. + +Five new unprofiled default-SQLite pairs at this current integration preserve +the identical fixture/outcome. Main/candidate wall medians are14.255/14.382s +(ranges13.971–14.887/13.428–14.684); sampled CLI+daemon CPU after setup is +6.873/7.141s; observed daemon disk counters8,904,704/8,306,688B and logical-write +counters14,491,648/13,443,072B. Setup2.314/2.187s and teardown0.061/0.057s remain +separate. Three candidate episodes are slower and ranges overlap. Reduced +write counters still do not qualify overall or complete marginal-cost benefit. + +A separate passive exit-observation pair expands the accounting boundary. +Python self/waited-child usage covers the harness, provenance/verifier and CLI +children; the initial owned daemon is already in waited-child usage, so only +the later orphan daemon is added. Independent production locator PIDs require +both daemon exit records; dropping one record fails the coverage oracle. +Main daemon CPU after its last samples is0.147/0.045s; candidate0.149/0.046s. +The nearly equal tails do not explain the sampled CPU difference. These exit +callbacks cover the controlled shutdown/cache-retirement path, while observer +emission, native post-callback work, orphan native children, physical-device +writes and continuous system peaks remain outside this accounting. This +instrumented pair is a coverage diagnostic, not a latency qualification or a +replacement for any historical metric. + +A second instrumented complete pair records per-RPC calibrated process CPU. +`todo.read` increases from18 to26 calls and adds0.172s sampled Node CPU; this +locates the next investigation in the existing Todo/provider read transaction. +Its timings cannot certify performance or justify removing current-source, +incarnation, revision, WAL, lease or recovery checks. Characterize a cohesive +reuse opportunity within one admitted request before further implementation; +do not infer an import migration or cross-request authority cache from these +observations. Original Task/vision, independent exact-head review, +I3/minor-release, M1/#8011, R0/V10/R1, D2/D5/D6/D3, context/model/split-install, +PostgreSQL host/wheel, full-cost/adoption gaps and all frozen failures remain. + +### Refresh summary transaction checkpoint + +At runtime `68fae1d0` integrated with main `87a153d4d`, a complete-lifecycle caller trace +locates two independent native User/Agent summary requests inside one refresh +qualification. The existing Python adapter now passes the pair through the +existing TypeScript Todo transaction. That owner validates the source and each +request, sharing complete provider materialization when their references match. +Independent metadata, clock/actor binding and all source/identity/revision/WAL, +lease, receipt/recovery checks remain; supplied sources or references from a closed request scope retain +the standalone path. No authority is cached across requests. This is an internal +existing CLI/readback improvement with no new product operation or configuration. + +The same 101-Todo source/wheel File/SQLite lifecycles preserve sum45, original +completion time, four receipts, one debit, restart, refusal/replay and settlement +readback. A recursive dropped-scope response mutation fails the independent +scope oracle in the real SQLite CLI; its disposable daemon is stopped. The +instrumented lifecycle reduces todo.read26 to24 and each refresh summary pair +from two RPCs to one; that is a mechanism diagnostic, not a latency result. +Current113 Python/63 TypeScript, typing/mypy and isolated real PostgreSQL16.15 +store391/service11 pass, zero skips in the qualified runs; both servers stopped. +All24 PR production files match the isolated wheel. The first missing argument +and first service-env skip are retained as failed/unqualified attempts. Standard +premerge passes six direct and18/19 selected checks, retaining the unchanged +peer hard-cut scanner failure. The public-boundary subcheck is clean; separate +complete local checks still expose unrelated registered-Goal authority errors. + +Five matched, interleaved, unprofiled default-SQLite source episodes per arm +compare main `b78246ff7`, the prior proposal `94c99e28a` and runtime +`fc032cbf5`, before the later main integration. Each fixture driver uses that +arm's exact CLI interpreter; all15 prequota PID-set checks preserve the same +prewarmed daemon. The earlier overlapping-validator and mismatched-interpreter +campaigns are retained only as diagnostics. Runtime isolation is preserved. + +| Observation median | Main | Prior proposal | Refresh proposal | +| --- | ---: | ---: | ---: | +| Lifecycle wall, seconds | 22.092 | 23.620 | 22.033 | +| Timed child + sampled daemon CPU after setup, seconds | 14.756 | 15.353 | 14.907 | +| Sampled daemon disk counter, bytes | 8,888,320 | 8,310,784 | 8,093,696 | +| Sampled daemon logical-write counter, bytes | 13,946,880 | 13,840,384 | 13,107,200 | +| Observed daemon lifetime peak, bytes | 141,508,280 | 139,887,008 | 138,510,608 | + +Wall ranges are21.898–24.716 /22.615–24.863 /21.824–24.767s; +setup medians2.336/2.404/2.392s and final teardown0.062/0.062/0.058s are +separate. All five refresh candidates are faster than the prior proposal, but +three are slower than main and ranges overlap. Timed child CPU includes the +independent verifier. These counters exclude complete exit/helper/harness, +provenance, model and attention costs; disk counters and sampled lifetime peaks +are not physical-device writes or continuous system peaks. The complete benefit +remains unqualified. These observations do not qualify the later main integration, +or replace historical/frozen failures. The integrated production hashes remain +identical, and its four source/wheel File/SQLite lifecycles were rerun. + + +The original full declaration and every historical/frozen failure remain. I3 +for at least one minor release, M1/#8011, R0/V10/R1, D2/D5/D6/D3, +context/model/split-install, PostgreSQL host/wheel, full-cost/adoption and +independent exact-head review remain open. No main merge or installed-host +upgrade is included. Further implementation must follow the complete cost +comparison and preserve every current freshness/authority fence. + +### 2026-11-08 — Matched source/wheel process cost through native exit + +The original proposal #8302 is integrated with target main `21013c0c1` as +`9e147ddf2`. All24 PR production-source hashes remain unchanged. Each arm's +1840 packaged tracked sources match its rebuilt isolated wheel; five unchanged +source-only files remain outside package-data patterns: the Canary baseline, +three Claude plugin/config files and the shadow-drain test process. This checks +packaged-source parity, not identical source/wheel inventories or fingerprints. + +The frozen default-SQLite comparison completes five alternating pairs per +distribution,20 complete101-Todo lifecycles, on macOS27 arm64, +CPython3.13.5/Node26.0.0. Fixture initialization, CLI and verifier use the exact +arm interpreter/package. Package loading precedes checkout fixture helpers; +temporary/process isolation precedes module imports. An initial wheel preparation +refused source-loaded fixture provenance before authority initialization; that +failed preparation remains separate. Both wheels required the existing verified +Chat build before construction. Every matched prequota PID-set check retains +the correctly sourced prewarmed daemon; controlled restart creates a second, +separately accounted daemon. Bytecode is disabled and each CLI is a new process. +No builds or validators overlap the matched runs, and the frozen count is not +expanded after results. + +All20 episodes independently retain the same sum45 artifact, original completion +time, four committed receipts and one debit, required scopes, changed-evidence +refusal, completion/writeback/spend/terminal replay, controlled restart and +settled-Turn execution refusal. Identical passive OS waiters observe both native +daemons through real exit. External `wait4` captures the harness and its waited +subtree; directly reaped daemon waiters are already included, so only orphan +native subtrees and observed orphan-waiter CPU are added. This avoids double +counting and includes native exit CPU that earlier sampled comparisons omitted. + +| Distribution | Whole harness wall median (range), seconds, main → candidate | Observed process CPU median, seconds | Complete managed-daemon CPU median, seconds | +| --- | --- | --- | --- | +| Source | 25.904 (25.589–26.629) →26.477 (25.603–26.643) | 17.489 →17.749 | 1.782 →1.887 | +| Wheel | 26.248 (25.670–28.200) →26.489 (26.135–27.437) | 17.984 →18.341 | 1.787 →1.932 | + +Source setup/lifecycle/teardown medians are2.602/22.581/0.062 seconds on main +and2.669/22.807/0.058 on candidate; wheel medians are2.712/22.568/0.062 and +2.651/22.878/0.062. Whole wall measures harness start through OS exit, including +readback and observer work outside these three phases; their medians need not +sum to its median. Candidate wall is slower in4/5 source pairs and2/5 wheel +pairs; observed CPU is higher in4/5 pairs for each distribution. Wall ranges +overlap, and no significant wall improvement is claimed. Nearest-rank empirical +p95 from five observations equals the maximum: source26.629→26.643 seconds, +wheel28.200→27.437; these are not stable population percentiles. + +A separate instrumented wheel pair preserves all lifecycle/provenance/exit +oracles and traces367→344 RPCs with3,506,215→1,874,964 JSON parameter bytes. +It diagnoses the transport mechanism and does not qualify speed. Managed-daemon +CPU is1.864/1.896 seconds, about9.5%/10.0% of observed process CPU in that pair. +Other Node helpers/probes remain inside the CLI waited subtree together with +Python startup and local processing; their individual contributions are +unresolved. Calling the entire remaining CPU Python startup would misattribute +the evidence. Further work must first attribute this subtree's material costs, +then select a bounded change through the existing typed owner and repeat the +same outcome/recovery checks. The original task remains open for this work and +independent exact-head review; fewer RPCs or passing tests do not establish +whole-path benefit. + +Current-main integration passes99 focused Python and83 TypeScript cases, zero +TypeScript skips, and control-plane typing. The earlier real PostgreSQL16.15 +store391/service11 qualification belongs to unchanged production sources; it is +not a new run at this integrated head. Current-head File whole-CLI and +PostgreSQL host/wheel journeys remain unqualified. Observer CPU after result +publication, physical-device writes, continuous system peak, environment/build +cost, model/attention and installed adoption remain unknown or outside this +comparison; missing tails are not zero. Earlier failures and frozen campaigns +are neither replaced nor relabeled. Full marginal/long-horizon benefit remains +unqualified. Keep I3 for at least one minor release, M1/#8011, full R0/V10/R1, +D2/D5/D6/D3, context/model/split-install, full-cost/adoption and all freshness, +identity/revision/WAL/lease/receipt/recovery fences. No main merge or host +upgrade is included. + +### 2026-11-08 — CLI CPU attribution and fresh source-scan path construction + +One separate wheel attribution pair compares main `21013c0c1` and proposal +`24ca29f4e`, retaining the complete101-Todo lifecycle and every prior +source/package, scope, warm-PID, artifact, original completion-time, four-receipt, +one-debit, refusal/replay/restart and native-exit oracle. Each arm records22 +Python console processes, three independent Node helpers/probes and two managed +daemons through actual exit. The helpers are two version probes and one setup +helper; all their exits precede the associated caller's readback. PATH-resolved +Node programs keep their exact executable and arguments. Python console CPU +profiles use `process_time`; no timing qualification is inferred from this +instrumented pair. + +| Diagnostic CPU, seconds, main → proposal | Observation | +| --- | --- | +| Python CLI self, sampled before profile dump | 20.447 →20.099 | +| Python profile exclusive total | 18.230 →17.922 | +| Import machinery cumulative | 14.505 →14.403 | +| Compilation exclusive | 7.488 →7.464 | +| Complete managed-daemon subtree | 1.780 →1.890 | +| Three independent Node helper/probe subtrees | 0.286 →0.281 | + +Import cumulative time includes nested compilation and must not be added to +the exclusive compilation total or the self sample. Module loading may happen +inside an operation; this does not separately identify interpreter startup. +The fixed bytecode-disabled condition makes loading/compilation prominent, while +the helper contribution is bounded. Python interpreter/wrapper/profiler overhead +and publication tails prevent treating these figures as an unprofiled cost +decomposition. They do not explain or reverse the earlier frozen pair's CPU +increase, establish speedup, or qualify normal-bytecode installed adoption. + +The same profiles locate source-scan path handling in the existing filesystem +adapter. Runtime `a57fd3ef7` computes the POSIX relative-directory prefix once +per directory instead of constructing an additional relative `Path` for every +candidate file. Source selection, sorted rows, file metadata, `Path.stat` and its +platform error policy remain; full-source hashing and every freshness/identity, +revision/WAL/lease/receipt/recovery fence are unchanged. This bounded companion +refactor adds no capability, provider, persisted field, vocabulary or authority +cache. The native TypeScript owners retain their existing rules. + +The immutable parent and new scanner produce the exact same404 metadata tuples +over the same real source tree.64 focused Python cases and kernel mypy/Ruff +pass. Before/head real `runtime.ping` calls over disposable copied packages +both rotate fingerprint and PID after an owned JSON file changes. A deliberate +omitted-JSON-source mutation fails that same independent runtime oracle; all +owned probe runtimes are stopped. Eight before/head × source/wheel × File/SQLite +complete CLI lifecycles retain the original artifact, completion time, scopes, +four receipts, one debit, refusals/replays and both native exits. These qualify +correctness and the exercised real path, not performance. + +The rebuilt monolithic wheel matches1840 packaged tracked sources; only the +existing `effect_runtime.py` changes among the original24 production files. +The same five source-only omissions remain. One independent import probe first +ran in a checkout working directory and loaded source; its failed preparation +is retained. Corrected qualification uses a non-checkout working directory, +clears `PYTHONPATH` and requires the module to belong to the isolated installation +before comparing bytes. The lifecycle harness retains its own independent +package-root checks. The initial wrong-directory Chat build command also remains +a preparation failure; the dashboard owner's build then verifies the bundle. + +Wider loading changes remain a separate next decision: compare real normal and +disabled bytecode behavior at the existing quota/context and refresh dispatch +owners before moving required dependencies. The path-allocation delta is an +interim local reduction with no whole-path/marginal-cost benefit claim. Current +PostgreSQL host/wheel, model/attention, continuous peak, physical-device writes, +installed adoption and full long-horizon qualification remain open. Unchanged +PostgreSQL store/service owners retain their earlier real qualification; no new +PostgreSQL run is claimed here. Preserve all frozen failures, the existing peer +hard-cut validation hold, I3 for at least one minor release, M1/#8011, full +R0/V10/R1, D2/D5/D6/D3 and context/model/split-install/full-cost/adoption acceptance. +The original task and independent exact-head review remain open; no main merge +or installed-host upgrade is included. + +### 2026-11-08 — Normal-bytecode lifecycle and observation limits + +One before/head wheel pair exercises the same complete 101-Todo SQLite +lifecycle at `24ca29f4eb2afe0bbf27df00002abd005febf256` / +`1a4d9d8f92491db5913cfa59061bcd58c677868d`; packaged runtime bytes are +`9e147ddf2085b080eba0cd2d7888ec896b270333` / +`a57fd3ef771013c80add55821321e98e577836e6`. Target main remains +`21013c0c113445b71a30f6ce06ab61455b56f1f2`. This checkpoint changes only the +paired RFC; the production proposal is retained for independent review. + +Both arms use newly installed wheels, the same CPython 3.13.5 and dependency +versions, and fresh byte-identical copies of the existing fixture helpers. +Installation and fixtures start with zero pyc files. Pre-run provenance checks +use `-B`, a non-checkout working directory and cleared `PYTHONPATH`; imported +module ownership is checked before all 1,840 packaged tracked source hashes. +The same five source-only omissions remain. Measured processes use default +bytecode, with write-disable, cache-prefix and importtime variables unset; +`sys.dont_write_bytecode=False` and `sys.pycache_prefix=None` are read back. +Data and temporary directories are isolated before fixture imports. Interpreter +stdlib and OS file caches are shared and are outside the isolation claim. + +| Cache stage, identical path sets in both arms | All installed pyc | LoopX pyc | +| --- | ---: | ---: | +| Installation complete | 0 | 0 | +| Driver before fixture import | 1 | 0 | +| Fixture/setup complete | 317 | 223 | +| Bootstrap complete | 358 | 264 | +| Prequota complete | 433 | 339 | +| Guard and required reads complete | 665 | 571 | +| Settled replay and teardown complete | 699 | 605 | + +The driver's first pyc belongs to virtualenv support. Five fixture pyc files +appear during setup in each fresh fixture copy. All 605 final LoopX pyc headers +match the interpreter magic and their source timestamp/size. These are cache +validity and phase observations, not a count of modules loaded by one guard or +a measurement of default-mode compilation/import CPU. + +Both original lifecycle runs exit successfully. Artifact sum45 and its digest, +original completion time, required scopes, four committed receipts, one debit, +unchanged prequota warm PID set, controlled restart, evidence-replacement +refusals and completion/writeback/spend/terminal/settled-Turn replay pass. Each +arm retains all 22 console command/phase/wrapper exit records, three exact-argv +Node helper exits and both managed daemon exits. Waited descendants are counted +once; only orphan daemon subtrees and their observed waiter CPU are added to +the outer waited subtree. + +**The actual console-PID observation subcheck failed.** The first observer +incorrectly interpreted macOS `proc_listchildpids`' PID count as a byte count, +recording 0/22 actual console PIDs. An independent disposable child probe +reproduced and corrected that API interpretation before the unstarted head +arm. Head captures 21/22 actual console PIDs; bootstrap is still outside the +bounded sampling window. Wrapper PID/argv/exit records remain complete, but +actual-PID coverage is incomplete. Both failed post-run subchecks are retained; +neither lifecycle is rerun. A preliminary parse with the host's incompatible +Python 3.9 also fails; syntax validation with the executing 3.13 interpreter +passes. The fix and evidence gap remain in the original task's recovery record. + +| Single-pair raw observation, before → head | Value | +| --- | ---: | +| Whole harness wall | 24.044 → 25.180 s | +| Observed process CPU including orphan additions | 10.698 → 10.724 s | +| Rounded console waited-subtree CPU from OS time files | 6.020 → 5.980 s | +| Complete managed-daemon subtree CPU | 1.900 → 1.920 s | +| Three Node helper subtrees | 0.273 → 0.291 s | +| Setup / final teardown wall | 5.503 / 0.062 → 3.338 / 0.054 s | + +The observer correction changes bounded sampling waits between arms, and the +head bootstrap has a large wall delay. Fixture, verifier, observer and readback +work remain in the whole-harness total. These observations cannot qualify a +speedup, regression or paired CPU difference, explain the earlier frozen cost +campaign, or be subtracted from the bytecode-disabled profile as a compilation +saving. Console waited CPU also includes local work and descendants; it is not +Python startup alone. No frozen sample is repeated, expanded or relabeled. +Publication/exit tails, physical writes, continuous system peaks, build/setup +environment cost, model/attention and installed adoption remain unknown. + +This closes the finite default-cache correctness question while preserving the +cost/attribution gap. Retain the existing typed transaction and source-scan +proposal. The next bounded work should inspect the required dependency closure +at the existing quota/context/refresh callers, then select a complete reversible +loading change with normal-default semantic parity. Valid caches alone do not +justify moving a required import or removing a fresh read; do not repeat this +pair to recover PID observations. The original task, independent exact-head +review/readiness, peer hard-cut hold and all I3/minor-release, M1/#8011, full +R0/V10/R1, D2/D5/D6/D3, context/model/split-install, PostgreSQL host/wheel and +full-cost/adoption requirements remain open. No main merge or host upgrade is +included. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index 1cd4692672..0c3d5fec27 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -1391,6 +1391,608 @@ lineage、pause/gates 与 pending intents。新增更多 leaf RPC 或有损裁 解决的性能 hold 保持独立。新实现仍需完整 source/wheel 正确性,以及不插桩、 匹配条件的默认 SQLite 全路径比较;既有里程碑、采纳与完整成本缺口继续开放。 +### 2026-11-07 — 在受管请求内复用完整 Todo 展示来源 + +既有 `todo.context.page` owner 可从同一完整输入来源组合 summary、planning 和 +保留 terminal 的展示视图。Quota preparation 调用方显式请求 Agent 来源的三种 +视图,并把保留 terminal 的 rows 传给 frontier 消费方。User 来源及通用展示调用方 +保留自己请求的单一视图。来源选择和每种展示规则保留原 owner。Orchestration +fallback 复用已选 summary 的独立副本,避免再次请求相同展示。 + +已经固定 runtime revision 的逻辑请求内,以完整 source JSON 和请求选项约束 +传输 memo。来源事实变化和后续请求重新验证;可变调用方获得独立解码值。较大的 +显式组合输入保留原逐视图传输,不完整组合响应在进入 memo 前失败。Batch 输入 +阈值不能预测响应大小,也不保证 socket 内联传输;显式多视图的合计输出超过 +socket 限制时,仍可能使用既有私有响应 snapshot。 + +初始提案的独立评审发现,通用传输把单视图调用扩大为三种视图。8,000 条记录的 +单次 delivery-response 调用最终输出相同,却把原约 3.73 MB 的单视图 socket +响应扩大成约 11.18 MB 的组合响应并写盘。保留这次性能失败。修复后的调用方 +显式组合移除了这种隐式扩大;单视图对照和完整受管 source/wheel 验证按本次提案 +revision 记录。 + +初始提案对照 `main@32167518b3`,源码与隔离安装 wheel 的六组 101-Todo File/SQLite +受管 CLI 生命周期通过,保留已验证产物、原完成时间、四种 settlement receipt、 +一次 debit、Effect 重启恢复及 evidence 替换拒绝。刻意丢弃 write-scope 响应字段的 +mutation 在独立公开 guard scope 检查失败。冻结展示 fixture 和聚焦来源隔离用例 +同时覆盖既有三种视图。一次带 profiler 的 SQLite guard 观察到 JSON 参数约 +1.900 MB → 1.556 MB,调用 89 → 87,已加载模块仍为 503。这些计数仅用于诊断, +不验收完整 wire 成本、延迟或长程收益。 + + +修复后的单次调用对照在 `main@dee549663`、已评审的缺陷 head 和修复源码上 +使用相同合成来源,三者最终 delivery response 相同。修复的请求参数和响应字节 +与 main 完全一致;两种记录规模的非空响应 snapshot 字节都为零。每个版本五次 +热调用的探索性中位数如下,包含 display 和 delivery-response 两种 projection: + +| Rows | Main 秒 | 缺陷提案秒 | 修复秒 | 缺陷响应 snapshot 字节 | 修复 snapshot 字节 | +| --- | --- | --- | --- | --- | --- | +| 8,000 | 0.159 | 0.636 | 0.161 | 9,480,165 | 0 | +| 15,000 | 0.284 | 1.344 | 0.286 | 17,775,165 | 0 | + +此对照复现并消除了单视图性能回归,不能验收完整 CLI、daemon CPU/内存、完整 +边际成本或长程收益。 + + +修复后的源码及隔离 wheel 通过六组 101-Todo File/SQLite 受管 CLI 生命周期, +保留已验证产物、原完成时间、四种 receipt、一次 debit、重启恢复与拒绝/重放。 +递归丢弃响应中的 write scope 的 mutation 在独立公开 guard oracle 失败。隔离 +PostgreSQL 16.15 的 390 个 store 和 11 个 service 用例通过,零 skipped。 + +三组交错、无 profiler 的默认 SQLite 完整 CLI 对照中,main 墙钟中位数为 +12.24 秒,修复为 12.71 秒;两次候选更慢,范围重叠。独立 profiled guard 的参数 +字节从 1,899,603 降到 1,546,162,调用从 90 降到 87,模块仍为 503。这些观察 +未证明完整路径性能收益。Daemon CPU/内存、总 wire/file 写入、完整边际成本及 +采纳仍未验收;完整 provider-first 事务继续作为下一验收边界。 + +这是可独立回滚的展示传输增量,尚非完整 provider-first Todo projection +transaction。Provider 读取、revision、lease/actor fence 和状态写入保留原 owner。 +后续 canonical-source 事务、匹配的完整成本对照及受影响后端验收仍开放。I3 至少 +保留一个 minor release;M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3、context/model/ +split-install、adoption 和所有冻结失败继续保留。 + +#### 下一提案之前的 provider-first 事务要求 + +以下保留展示增量 revision 时的前置缺口。下一 checkpoint 记录 canonical 事务 +提案及其测量限制;不会把先前观察改写成历史通过。 + +下一实现应组合既有经过 provider 准入的 `canonicalTodoCollection` 和 projection +owner。Native snapshot owner 把 Goal、store incarnation、provider revision、cursor +及 query 绑定到完整 rows、leases、read-model 验证和 acceptance guards。Continuation +只是同一次读取中的位置,不是可复用的 current-head handle;snapshot 变化后应重新 +完整读取。展示保留来源不会获取新的 authority。 + +`quota_selection.py` 仍在 encoded selection rows 和 source-row facts 之外发送 +完整 summary source。请求还包含时钟、actor/profile、capabilities、closure evidence、 +route/handoff facts 和 resume 输入。合并传输之前,先把 canonical row materialization +放入既有 typed owner。严格 integer、boolean 或文本区别仍可观测时,保留 +Python-value codec。Provider rows 不能替代独立 caller evidence 或 pending intent; +all-done 计数本身无法重建 closure declaration。 + +`goal_vision_wait` 还独立消费完整 lineage、scoped blockers、acceptance gaps 和 +resume facts。合并事务应保留每个 causal obligation,包括两个 obligation 共用 +witness 的情况。完整 note、write scopes、archive/settled identity 和来源 provenance +仍属于 parity oracle。 + +真实 PostgreSQL 集成用例把既有 native snapshot read 接到三种展示视图,检查 +provider/source 数据不变,在独立 commit 后观察新的 provider revision,并拒绝旧 +continuation。它验证的是组合前置条件;完整 guard transaction、PostgreSQL +source/wheel host journey 和完整成本收益仍开放。 + +### 2026-11-07 — Canonical Todo 事务提案与已观测生命周期成本 + +提案在绑定 actor/profile/clock 输入后,由既有 TypeScript owner materialize +canonical selection 与 resume rows。`todo.read` 组合显式请求的 Agent summary、 +planning、保留 terminal 的视图,以及 User summary、quota selection 和既有 +Vision-wait basis。同一次 quota 请求中,匹配的 native reference 共享一次完整 +provider read。完整字段 digest、store incarnation、provider revision、cursor +和原 query/selection replay recipe 约束来源。后续请求重新读取;不缓存 provider +authority。 + +实际受管 CLI query 现在通过 quota preparation 保留来源 recipe。此前 status +虽保留 ephemeral origin,完整 query response 却丢失它。Query 与 selection 路径 +现在都把完整 provenance 传入同一 typed transaction。Provider outage 保留原 +诊断 code/payload,stale reference 在 projection 前拒绝。Reference metadata +不能覆盖 provider 原字段、Python scalar facts 或 owner 声明的 Todo source lane。 +独立 caller metadata 保持完整;改写 provider row 的调用方使用完整 supplied-source +codec。严格 integer/float/bool、list/tuple 区别、旧 fnmatch facts、lane/ordinal +优先级及远期微秒时钟均保留可观察语义。不通过 Todo id 查找补回丢失的 provenance。 + +既有 Vision-wait owner 保留 causal obligation 与共用 witness。复用 prepared +basis 必须匹配 actor、完整 terminal rows 及逻辑 request identity;独立 lineage +调用方保留原路径。Standalone display 仍只请求一种视图。完整 note、evidence、 +write scope、archive/replay identity、独立 closure/pending intent 均属于 parity +边界。既有 source-lane 常量导出给 native provenance 校验复用,没有新增 lane +词汇、持久状态或 authority。 + +生产代码 `66ff8bf48` 对照 `main@e2371c476`,通过四组 101-Todo 源码/隔离 wheel +× File/默认 SQLite CLI 生命周期。独立 verifier 验收 sum=45;原完成时间、四份 +receipt、一次 debit、Effect 重启、无效 Vision/workspace 拒绝、证据替换拒绝与已 +结算 replay 均保留。Wheel 的受影响源码与 checkout 一致。最终聚焦用例通过 +132 个 Python 和 86 个 TypeScript;较广真实 CLI suite 在最后诊断/lane 修正 +之前通过 244 个用例,这些最后修正由聚焦 suite 覆盖。真实 disposable PostgreSQL +16.15 通过 391 个 store 和 11 个 service 用例,零 skip;包含两种 role 的 native +quota 组合、一次完整 provider load、数据不变与 stale-source 拒绝。这不验收 +PostgreSQL 源码/wheel host journey。 + +五组交错、无 profiler 的源码 CLI 对照使用相同 101-Todo fixture 与默认 SQLite。 +每组执行 provenance、bootstrap、claim、guard、必需 context read、产物验证、 +completion、反例、writeback、spend、restart 和 terminal/refusal replay。Fixture +setup 与最终 teardown 单独报告,两边同样继承正常 bytecode 条件。三组候选更慢, +墙钟范围重叠: + +| 观察,未说明时为中位数 | Main | 提案 | +| --- | ---: | ---: | +| 生命周期墙钟(范围) | 14.093 秒(13.765–14.933) | 14.145 秒(13.461–15.190) | +| Fixture setup/最终 teardown | 2.403/0.062 秒 | 2.338/0.062 秒 | +| CLI child CPU+已观测 daemon CPU(历史记录,单位换算无效) | 5.511 秒 | 5.757 秒 | +| Daemon 边界 physical footprint | 140,787,912 B | 146,456,920 B | +| CLI 最大 resident size | 70,746,112 B | 70,221,824 B | +| 已观测 daemon disk/logical writes | 9,048,064/14,090,240 B | 10,227,712/15,646,720 B | +| CLI output/必需 context 字节 | 177,595/21,168 B | 179,482/21,257 B | + +上表 CPU 使用了错误的 Mach 时钟换算,不能作为 CPU 证据。三个独立 CPU busy +子进程校准确认,该 macOS arm64 runtime 的 daemon usage 单位为 Mach tick, +须先乘实测 timebase `125/3`,再把纳秒换成秒。原始观察保持不变。修正五组历史 +对照后,包含 setup daemon 工作的 combined observed CPU 为 7.071→7.469 秒; +再减各自初始 daemon baseline,CLI episode 为 6.706→7.107 秒,相应 daemon +disk/logical writes 为 8,892,416/13,783,040→10,072,064/15,355,904 字节。 +修正后的观察仍未证明完整路径收益。 + +Daemon CPU/写入是 CLI 边界处累计 process 观察,不含最终退出工作。CLI CPU +不含未采样的 provenance process 和 harness 工作。CLI RSS 与 daemon footprint +分别观测,不是连续全系统峰值。这些边界不测量完整 model cost、人工注意力或 +installed-host/长程收益。 + +另一组独立插桩对照观察到以下通信变化;其耗时不进入墙钟比较: + +| 诊断 | Main | 提案 | +| --- | ---: | ---: | +| Guard RPC/JSON 参数字节 | 90/1,903,233 | 84/837,940 | +| 完整 episode socket 发送/接收字节 | 3,537,350/6,380,604 | 1,948,250/6,388,112 | +| Guard 加载 LoopX 模块 | 503 | 503 | + +Guard 两种 role 的来源均使用 native transaction。请求字节和调用减少证明传输 +路径已采纳,而接收字节、已观测写入与 CPU 没有一起改善。因此完整路径提速或 +完整边际成本收益**未取得资格**。后续决策保留原事务验收:独立评审,并归因剩余 +生命周期成本后再提出优化。负面结果不支持另做 leaf-RPC migration 或新增 provider。 + +提案 rebase 到 `main@e58ba30bc`。最后 scope-fit 检查发现 read transport 的 +provenance binding 超过 decision-function budget,已把完整来源绑定和 selected-row +evaluation 恢复提取为局部 helper;24 个聚焦 parity 用例和 maintainability ratchet +通过。成本表仍是提取 helper 前 `66ff8bf48` 的观察,不是最终 exact head 的成本 +资格。TypeScript typecheck 与 Ruff 通过。Mypy 保留 main 已有的 +`decision_capture.py` 缺少返回类型错误;标准 premerge 也保留 main 对未修改 +benchmark 文档的 peer-boundary scanner 失败。两者均不是通过的 skip。Exact-head +review、验证/readiness 与 maintainer merge 授权保持独立;没有合入或 installed-host +升级。原 Task 为收益及剩余验收继续开放。I3 至少保留一个 minor release;M1/#8011、 +完整 R0/V10/R1、D2/D5/D6/D3、context/model/split-install、adoption/full-cost +缺口及所有冻结失败继续保留。 + +### 2026-11-08 — 完整 quota 响应传输与校准成本 + +独立评审在 `e7db1711f` 复现剩余回归:8,000-row quota 组合结果序列化为 +13,080,176 字节,产生 13,080,177 字节同步 response-snapshot 写入;main 的独立 +视图低于该响应阈值。既有 Todo read adapter 现在对超过共享 8 MiB inline budget、 +且处于原 complete-frame budget 内的 quota 结果使用无损 gzip,经既有 result-piece +envelope 返回 encoding 与 decoded extent,base64 分片保留 socket 预算余量。 +更大的 read 保留原分页。Python adapter 验证完整 digest/identity、有界 decoded +size、精确 extent 与单一完整 gzip member;未知 encoding、截断或 trailing data +均拒绝。原响应预算值与 server 共用,不重复定义;没有 provider cache、持久状态、 +rule owner 或 authority 变更。 + +相同 8,000-row 诊断现在为 561,291 wire 字节,response snapshot 字节为零。 +完整结果与原独立 summary、planning、terminal 视图一致,未知字段和 Unicode +保真。单次诊断耗时不证明提速。12 个 transport 用例及既有 24 个 parity 用例通过; +两个 native File/SQLite 8,000-row quota 用例保留双 role 来源、完整视图及 provider +数据/revision,且不产生 response snapshot。22 个 TypeScript context/quota +用例与 8 个真实 receipt/replan/reentry 用例通过。此前过强的测试 oracle 失败, +因为 display owner 有意展示不同字段;最终比较各自完整独立输出,不修改展示行为。 + +生产代码 `2c9d6360e` 基于 `main@8a4d4f9da`,通过四条新 101-Todo 源码/隔离 +wheel × File/默认 SQLite CLI 生命周期,含 sum=45、原完成时间、四份 receipt、 +一次 debit、重启、反例与 replay。最终 wheel 与受影响源文件一致。真实 disposable +PostgreSQL 16.15 再次通过 391 个 store 和 11 个 service 用例,零 skip。 +TypeScript typecheck、Ruff 与 maintainability ratchet 通过;这些检查不验收 +PostgreSQL 源码/wheel host journey 或 installed-host adoption。 + +五组新的交错、无 profiler 默认 SQLite 对照使用不变的 101-Todo fixture、完整 +反例/replay 生命周期及同样继承的 bytecode 条件,无并发重型测试。CPU 按校准 +Mach timebase `125/3` 换算;daemon CPU/写入减初始 setup baseline: + +| 观察,未说明时为中位数 | Main | 提案 | +| --- | ---: | ---: | +| 生命周期墙钟(范围) | 13.894 秒(12.686–16.481) | 14.974 秒(14.131–15.344) | +| Fixture setup/最终 teardown | 2.347/0.062 秒 | 2.299/0.062 秒 | +| CLI child+setup 后已观测 daemon CPU | 7.086 秒 | 7.403 秒 | +| Setup 后已观测 daemon disk/logical writes | 8,884,224/13,795,328 B | 10,076,160/15,130,624 B | +| Daemon 边界 physical footprint | 141,032,832 B | 150,520,728 B | +| CLI 最大 resident size | 71,467,008 B | 71,696,384 B | + +四组候选更慢,墙钟范围重叠。大响应写入回归已修复,但完整生命周期收益仍 +**未取得资格**。最终 daemon 退出、未采样 provenance/restart helper、harness CPU、 +连续系统峰值、模型成本与人工注意力仍不在上述观察中。原事务 Task 为独立 exact-head +评审及剩余 CPU/写入归因保持开放。此前 premerge peer-boundary 与 mypy 失败保留 +为验证 hold,直到 exact-final 检查证明其已消除。I3 至少保留一个 minor release; +M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3、context/model/split-install、 +adoption/full-cost 缺口及全部冻结失败继续保留。 + +### 2026-11-08 — 旧格式投影类型与剩余 native read 成本 + +独立评审发现,组合读取保留了来源优先级,却丢失所选投影类型。当 canonical 与 +旧 asset 输入共享同一字典时,即使既有优先级选中旧 asset,对象身份仍使其走 +user-summary 路径,旧字符串计数变为 null;只有 `next` 的输入绕过缺少 Todo ID +的拒绝。 + +生产代码 `4c3ed5d39` 让既有选择器同时返回来源和原投影类型;display/inventory +调用方仍使用同一选中来源,没有新增词汇、持久字段或 authority。 +最后静态校验补充了可变长度 source-candidate tuple 注解,执行 bytecode 不变; +该类型失败保留,修正后的 mypy 仅剩 main 既有 annotation hold。13 个新用例以原 +独立选择器的完整 summary 为对照,覆盖双 role、共享/复制输入、canonical 优先级 +及真实 guard 选择/拒绝;前一 head 六个失败,修复后全过。首版测试错误假定旧计数 +为数值,且未绑定 guard;其失败保留,最终 oracle 遵循原字符串值和绑定 Agent/Turn +路径。聚焦 suite 96 个通过。源码与隔离 wheel 独立保留旧计数并拒绝缺少 ID;四条新 +101-Todo 源码/wheel × File/SQLite 生命周期保留产物、原完成时间、四份 receipt、 +一次 debit、重启、反例及 replay。真实 disposable PostgreSQL 再次通过 store391、 +service11,零 skip,服务已停止。 + +对前五组成本数据归因后,剩余增量指向 guard、writeback、spend 和 settled replay +中的重复 native read。另一组插桩完整生命周期观察到八次新增 native `todo.read`, +每次增加 147,456 个已观测 daemon disk-write 字节,共 1,179,648 字节;插桩耗时不 +进入资格比较。对真实 native snapshot owner 的独立探针观察到每个 snapshot 四个 +只读 SQLite connection,同样增加 147,456 个 process-counter 字节。交错 identity +read 使用一个只读 connection、36,864 字节,provider revision 和数据库内容 digest +不变。这定位了 read lifecycle 成本,不代表物理设备写入,也不支持删除 identity、 +revision、WAL、lease 或 recovery 检查。 + +Python `invalid-vision` profile 主要是 import,两边调用数相同;其 wall 权重不能 +归因成对 CPU 差异,也不支持另做 import migration。下一个既有验收步骤是研究原 +SQLite read/connection lifecycle,通过 typed owner 保留新鲜 incarnation 与 +revision 校验;provider 编辑先按普通流程准备 Task scope/ownership。前一修正 +成本表仍对应 `2c9d6360e`,不是本次兼容性修正,不新增提速声明。原 Task、独立 +exact-head 评审、完整路径/完整成本、host/model/adoption 验收继续开放;I3、 +M1/#8011、R0/V10/R1、D2/D5/D6/D3、context/split-install 及全部冻结失败保留。 + +### 2026-11-07:已选 SQLite snapshot 的读取生命周期 + +既有 `AuthorityStore` 增加可选的 storage-only identity/head 成对读取,由原 +canonical snapshot owner 消费。`SqliteAuthorityStore` 仅在 `existingOnly` store +提供该方法;身份和完整已验证 head 使用同一个同步只读事务,不跨调用保留连接、 +head 或 provider authority。普通 SQLite、File、PostgreSQL 及未提供可选方法的 +custom store 保留原 identity/load 顺序和失败优先级。不新增持久状态、wire 词表、 +capability、selector 或 Python 规则 owner。 + +Provider 选择仍校验已配置 incarnation;snapshot 准入在读取 head 后仍独立重开 +pathname。Revision、cursor、完整 read-model digest、stale continuation、WAL、 +lease 和 receipt/replay 检查保留。读取 head 后真实替换数据库目录会被拒绝,且 +不返回部分行。私有 mutation 取消最终 incarnation 检查后,同一公开 snapshot +oracle 失败。 + +Runtime commit `61f7c3c33` 通过相关 TypeScript 873 用例与类型检查、四条完整 +source/wheel File/SQLite 101-Todo 生命周期,以及真实隔离 PostgreSQL 16.15 +store391/service11,零 skip。生命周期保留 sum45、原完成时间、四种回执、一次 +debit、重启和 refusal/replay;23 个变更 production 文件与 wheel 逐字节一致。 +Native before/head 诊断保持完整响应与数据库 hash 相同,只读连接从四次降到 +三次,每个 snapshot 的 observed daemon disk-counter 字节从 147,456 降到 +110,592。这些计数不代表物理设备写入,插桩诊断耗时不进入性能资格。 + +新一轮五组交错、无插桩的默认 SQLite 完整生命周期,对照同一 101-Todo fixture +在 main `8a4d4f9da`、前一版本 `c40d8ec31` 和 runtime `61f7c3c33` 的表现,环境为 +CPython3.13.5/Node26.0.0/已合格 SQLite3.53.1。十五条生命周期均保留同一独立 +验证成果与原结算。 + +| 已观测指标,中位数(所示范围) | Main | 前一版本 | 成对读取 | +| --- | --- | --- | --- | +| 生命周期墙钟 | 14.945 秒(12.664–16.597) | 14.924 秒(14.570–19.232) | 14.871 秒(14.412–16.775) | +| setup 后 CLI+采样 daemon CPU | 7.084 秒 | 7.388 秒 | 7.292 秒 | +| setup 后 daemon disk-counter 字节 | 8,896,512 | 10,076,160 | 8,314,880 | +| setup 后 daemon logical-write counter 字节 | 13,897,728 | 15,618,048 | 12,840,960 | +| Fixture setup/最终 teardown 墙钟 | 2.373/0.060 秒 | 2.387/0.062 秒 | 2.336/0.062 秒 | + +五组中候选两次慢于 main、两次慢于前一版本,墙钟范围重叠。已观测写入减少, +但 CPU 中位数仍高于 main,完整收益未合格。CPU 使用已验证 Mach timebase125/3, +扣除每个 daemon 的 setup 基线;未采样最终退出、部分 restart helper、provenance +和 harness CPU。CLI RSS 与 daemon 边界 footprint 分开观察,不代表连续全系统 +峰值。对照期间没有重型验证;model cost、人工注意力和 installed-host adoption +仍未测。新结果不替换或重标任何历史 campaign。下一步先独立评审并归因完整 CLI +剩余 CPU 成本,再选择优化,保留新鲜读取栅栏。 + +标准 premerge 六项直接检查及 19 项选中检查中的 18 项通过;peer hard-cut scanner +在 main 与候选上仍因同一未改动 benchmark README 失败。原 Task 的独立 exact-head +评审、匹配完整路径/完整边际成本及 host/model/adoption 验收继续开放。保留至少 +一个 minor release 的 I3、M1/#8011、R0/V10/R1、D2/D5/D6/D3、context/ +split-install 及全部历史失败;连接数或验证通过均不证明完整最小化内核目标已完成。 + +在最新 main `df67371c3` 上的集成 `ce0e41a26` 保持 23 个 PR production-source +hash 不变。重新通过四条完整 source/wheel File/SQLite 生命周期、49 个 TS +用例、类型检查和隔离 PostgreSQL391/11。保存上下文/旧格式 Python 检查为 +107 passed、14 skipped;要求显式 native Codex 的用例仍未合格。Main 已修复 +原 missing-return annotation,新的 kernel mypy 通过;历史失败保留。标准 +premerge 仍通过六项直接及 18/19 项选中检查,既有 peer scanner 失败保留。 + +集成后的五组新默认 SQLite 无插桩对照保留同一 101-Todo 成果。Main/候选 +生命周期墙钟中位数为 14.631/14.633 秒(范围 14.260–16.869/14.122–14.941), +setup 后已观测 CLI+daemon CPU 为 7.038/7.080 秒,daemon disk-counter 为 +8,880,128/8,290,304 字节,logical-write counter 为 14,966,784/14,282,752 字节。 +Fixture setup 中位数 2.362/2.346 秒、最终 teardown 0.062/0.058 秒仍分开。 +两次候选更慢,范围重叠;上文采样缺口仍适用,写入计数下降不证明完整提速、 +边际成本或采纳收益。先补齐剩余成本覆盖,并确认可重复的 CPU 退化,再选后续 +实现;不能假定应继续优化读取或 import。 + +### 完整成本覆盖与当前 main 事务检查点 + +既有提案在 `23227ee5` 整合 main `0c28f6004`,23 个 PR production-source hash +不变。重新通过 Python59、TypeScript63、类型/mypy、四条完整 source/wheel +File/SQLite 101-Todo 生命周期及隔离 PostgreSQL16.15 store391/service11, +这些运行零 skip。成果、原完成时间、回执、debit、重启和 refusal/replay 保留。 +标准 premerge 保留既有 peer scanner 失败(六项直接和18/19项选中通过)。 +第一轮 PostgreSQL 因 checkout 缺 `pg` 依赖失败,保留该失败,并单列环境补齐后的 +成功验证。 + +当前集成的五组新默认 SQLite 无插桩对照保持同一 fixture/成果。Main/候选墙钟 +中位数为14.255/14.382秒(范围13.971–14.887/13.428–14.684);setup 后采样 +CLI+daemon CPU 为6.873/7.141秒;已观测 daemon disk counter 为 +8,904,704/8,306,688B,logical-write counter 为14,491,648/13,443,072B。 +Setup2.314/2.187秒、teardown0.061/0.057秒分开。三次候选更慢,范围重叠; +写入计数减少仍不证明整体或完整边际成本收益。 + +另一次被动退出观测扩展了记账范围。Python self/waited-child usage 覆盖 +harness、provenance/verifier 和 CLI 子进程;最初归属 harness 的 daemon 已计入 +waited-child,只额外加上后来的 orphan daemon。以独立的 production locator PID +要求两条 daemon 退出记录,移除任一记录会触发 coverage oracle 失败。Main 的两个 +daemon 在最后采样后又消耗0.147/0.045秒 CPU,候选为0.149/0.046秒;尾部接近, +不能解释采样 CPU 差异。退出回调覆盖受控 shutdown/cache-retirement 路径;观测 +写出、回调后的 native 工作、orphan native 子进程、物理设备写入和连续系统峰值 +仍未覆盖。该插桩对照仅用于覆盖诊断,不是延迟资格,也不替换历史指标。 + +另一组插桩完整对照记录每个 RPC 的已校准进程 CPU:`todo.read` 从18增至26次, +额外消耗0.172秒采样 Node CPU,下一步应研究原 Todo/provider 读取事务。插桩耗时 +不认证性能,也不支持删除 current-source、incarnation、revision、WAL、lease 或 +recovery 校验。先表征同一准入请求内完整复用的机会,再实施;这些观察不支持盲目 +迁移 import 或跨请求缓存权威。原 Task/vision、独立 exact-head 评审、 +I3/minor-release、M1/#8011、R0/V10/R1、D2/D5/D6/D3、context/model/ +split-install、PostgreSQL host/wheel、完整成本/采纳缺口和全部冻结失败保留。 + +### Refresh 摘要事务检查点 + +在整合 main `87a153d4d` 的 runtime `68fae1d0`,完整生命周期调用追踪定位到同一 +refresh 资格检查分别执行两次原生 User/Agent 摘要请求。现有 Python 适配器把两份 +请求交给原 TypeScript Todo 事务;该 owner 逐项校验来源和独立输入,仅在引用匹配 +时共享完整 provider 物化。独立 metadata、clock/actor 绑定,以及 source/identity/ +revision/WAL、lease、receipt/recovery 检查保留;supplied source 或已结束请求 +scope 的引用沿用 standalone 路径。跨请求不缓存权威。既有 CLI/读回直接使用此 +内部改善,不新增产品操作或配置。 + +同一 101-Todo 的 source/wheel File/SQLite 生命周期保持 sum45、原完成时间、 +四份回执、一次 debit、重启、拒绝/重放和结算读回。递归移除 scope 的响应变异在 +真实 SQLite CLI 的独立 scope oracle 处失败;隔离 daemon 已停止。插桩生命周期的 +`todo.read` 从26到24,每个 refresh 摘要对从两次 RPC 到一次;这是机制诊断, +不能作为延迟结果。当前113 Python/63 TypeScript、类型/mypy和真实隔离 +PostgreSQL16.15 store391/service11通过,合格运行零 skip,两个 server 均停止。 +24个 PR 生产文件与隔离 wheel 一致。漏传必需参数的首次失败、缺少 service 环境 +变量的首次 skip 均保留为失败/未合格尝试。标准 premerge通过六项直接及18/19 +项选中检查,保留未改动的 peer hard-cut scanner 失败。公开边界子检查通过; +独立完整 local check 仍暴露其它 registered Goal 的 authority 错误。 + +后续 main 整合前,以 main `b78246ff7`、旧提案 `94c99e28a` 和 runtime `fc032cbf5` +各五条相同默认 SQLite source 生命周期作交错无插桩比较。每组 fixture driver 使用 +该组精确 CLI 解释器;15条 prequota PID-set检查均保留同一已预热 daemon。更早的 +并行验证负载、解释器不匹配对照仅作为诊断保留,未放松 runtime 隔离。 + +| 观察中位数 | Main | 旧提案 | Refresh 提案 | +| --- | ---: | ---: | ---: | +| 生命周期墙钟,秒 | 22.092 | 23.620 | 22.033 | +| Setup 后计时子进程+采样 daemon CPU,秒 | 14.756 | 15.353 | 14.907 | +| 采样 daemon disk counter,字节 | 8,888,320 | 8,310,784 | 8,093,696 | +| 采样 daemon logical-write counter,字节 | 13,946,880 | 13,840,384 | 13,107,200 | +| 已观察 daemon lifetime peak,字节 | 141,508,280 | 139,887,008 | 138,510,608 | + +墙钟范围为21.898–24.716/22.615–24.863/21.824–24.767秒;setup中位数 +2.336/2.404/2.392秒、最终teardown0.062/0.062/0.058秒分开。五次 refresh +候选均比旧提案快,但三次比 main 慢、范围重叠。计时子进程 CPU 包含独立 verifier; +完整 exit/helper/harness、provenance、model、attention 成本未覆盖;disk counter +和采样 lifetime peak 不等于物理设备写入或连续系统峰值。完整收益仍未合格。这些 +观察不认证之后的 main 整合,也不替换历史/冻结失败;整合后的生产 hash 相同, +已重新运行四条 source/wheel File/SQLite 生命周期。 + +原完整声明和所有历史/冻结失败保留。I3至少一个 minor release、M1/#8011、 +R0/V10/R1、D2/D5/D6/D3、context/model/split-install、PostgreSQL host/ +wheel、完整成本/采纳及独立 exact-head评审仍开放;不包含合入main或installed-host +升级。后续实现必须依据完整成本比较,保留所有新鲜性/权限栅栏。 + +### 2026-11-08 — Source/wheel 匹配全进程成本覆盖 native 退出 + +原提案 #8302 与 target main `21013c0c1` 整合为 `9e147ddf2`,24 个 PR +production-source hash 不变。每 arm 的 1840 个 packaged tracked source 与新建 +隔离 wheel 相同;五个未变的 source-only 文件仍在 package-data pattern 外: +Canary baseline、三份 Claude plugin/config 文件及 shadow-drain test process。 +这验证 packaged-source parity,不宣称 source/wheel inventory 或 fingerprint +完全相同。 + +冻结的默认 SQLite 对照完成每发行形态五组交替配对,共 20 条完整 101-Todo +生命周期,环境为 macOS27 arm64、CPython3.13.5/Node26.0.0。Fixture 初始化、 +CLI 与 verifier 使用精确 arm 的解释器/package;先加载实际 package 再加载 +checkout fixture helper,在模块 import 前建立 temporary/process 隔离。首次 +wheel 准备在 authority 初始化前拒绝 source-loaded fixture provenance;失败准备 +单独保留。两个 wheel 均先通过原 owner 的 Chat build。全部匹配流程的 prequota +PID 集合保留正确来源的预热 daemon;受控重启创建第二个独立记账 daemon。 +Bytecode 关闭,每条 CLI 是新进程;匹配运行不与 build/validator 重叠,结果出来 +后不扩充冻结数量。 + +全部 20 条独立保留相同 sum45 产物、原完成时间、四份 committed receipt、一次 +debit、required scopes、changed-evidence refusal、completion/writeback/spend/ +terminal replay、受控重启及 settled-Turn execution refusal。相同的被动 OS waiter +观察两个 native daemon 的真实退出。External `wait4` 捕获 harness 及其 waited +subtree;直接回收的 daemon waiter 已计入,只额外计 orphan native subtree 和 +已观测 orphan-waiter CPU,避免双计并覆盖此前采样遗漏的 native 退出 CPU。 + +| 发行形态 | Whole harness 墙钟中位数(范围),秒,main → candidate | 已观测 process CPU 中位数,秒 | 完整 managed-daemon CPU 中位数,秒 | +| --- | --- | --- | --- | +| Source | 25.904(25.589–26.629)→26.477(25.603–26.643) | 17.489 →17.749 | 1.782 →1.887 | +| Wheel | 26.248(25.670–28.200)→26.489(26.135–27.437) | 17.984 →18.341 | 1.787 →1.932 | + +Source setup/lifecycle/teardown 中位数为 main2.602/22.581/0.062 秒、 +candidate2.669/22.807/0.058;wheel 为 2.712/22.568/0.062 和 +2.651/22.878/0.062。Whole wall 从 harness 启动测到 OS 退出,包含三阶段外 +的 readback 与 observer 工作;各阶段中位数无需等于 whole 中位数。Source 五组中 +候选四次墙钟更慢,wheel 五组中两次;两个发行形态均有 4/5 组候选已观测 CPU +更高。墙钟范围重叠,不宣称显著改善。五个样本的 nearest-rank empirical p95 +等于最大值:source26.629→26.643 秒、wheel28.200→27.437;不是稳定总体分位数。 + +另一个独立插桩 wheel pair 保留所有 lifecycle/provenance/exit oracle,观察 +RPC367→344、JSON 参数字节 3,506,215→1,874,964。它诊断 transport 机制,不验收 +速度。该 pair 的 managed-daemon CPU 为 1.864/1.896 秒,约占已观测 process CPU +的 9.5%/10.0%。其他 Node helper/probe 与 Python 启动、本地处理一同位于 CLI +waited subtree,各自贡献尚未归因;不能将全部剩余 CPU 称为 Python 启动。下一步 +先归因该 subtree 的主要成本,再通过原 typed owner 选择有限改动并复验相同成果/ +恢复检查。原 Task 为此工作及独立 exact-head 评审保持开放;RPC 减少或测试通过 +不证明完整路径收益。 + +当前 main 集成通过 99 个聚焦 Python、83 个 TypeScript 用例(TS 零 skip)及 +control-plane typing。此前真实 PostgreSQL16.15 store391/service11 资格属于 +未变的 production source,不是当前集成 head 的新运行。当前 head 的 File 完整 +CLI 与 PostgreSQL host/wheel journey 仍未合格。Observer 发布结果后的 CPU 尾部、 +物理设备写入、连续系统峰值、环境/build 成本、model/attention 和 installed +adoption 仍是 unknown 或在本次范围外;未观测尾部不记为零。历史失败与冻结 +campaign 不替换、不重标签,完整边际/长程收益仍未合格。保留至少一个 minor +release 的 I3、M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3、context/model/ +split-install、full-cost/adoption 及完整 freshness、identity/revision/WAL/ +lease/receipt/recovery 栅栏。本轮不合入 main、不升级 host。 + +### 2026-11-08 — CLI CPU 归因与新鲜源码扫描的路径构造 + +一个独立 wheel 归因 pair 对照 main `21013c0c1` 与提案 `24ca29f4e`,保留完整 +101-Todo 生命周期及原 source/package、scope、预热 PID、产物、原完成时间、 +四份 receipt、一次 debit、refusal/replay/restart 和 native 退出 oracle。每 arm +记录 22 个 Python console 进程、三个独立 Node helper/probe、两个 managed +daemon 的真实退出。Helper 是两个 version probe 和一个 setup helper,均在 +所属调用方读回前退出。PATH 解析的 Node 保留精确 executable/argv;Python +console CPU profile 使用 `process_time`,不以该插桩 pair 验收速度。 + +| 诊断 CPU,秒,main → proposal | 观测 | +| --- | --- | +| Profile dump 前采样的 Python CLI self | 20.447 →20.099 | +| Python profile exclusive total | 18.230 →17.922 | +| Import machinery cumulative | 14.505 →14.403 | +| Compilation exclusive | 7.488 →7.464 | +| 完整 managed-daemon subtree | 1.780 →1.890 | +| 三个独立 Node helper/probe subtree | 0.286 →0.281 | + +Import cumulative 含嵌套 compilation,不能与 compile exclusive 或 self sample +相加。模块加载可能发生在操作内部,尚未独立识别解释器启动。固定的 +bytecode-disabled 条件使加载/编译突出,helper 贡献有限;解释器/wrapper/ +profiler 开销及发布尾部仍使这些值不能作为无插桩成本分解。它们不解释或推翻 +此前冻结 pair 的 CPU 增量,不证明提速,也不验收正常 bytecode 的 installed adoption。 + +同一 profile 定位原 filesystem adapter 的 source-scan 路径处理。Runtime +`a57fd3ef7` 每目录只构造一次 POSIX 相对目录前缀,免去每候选文件额外的相对 +`Path` 构造。源码选择、行排序、文件元数据、`Path.stat` 及其平台错误策略保留; +完整 source hash 与 freshness/identity、revision/WAL/lease/receipt/recovery +栅栏不变。这是有限的同域 refactor,不新增 capability、provider、持久字段、 +词汇或 authority cache;原生 TypeScript owner 继续持有原规则。 + +不可变 parent 与新 scanner 在同一真实源码树返回完全相同的 404 个 metadata +tuple。64 个聚焦 Python 用例及 kernel mypy/Ruff 通过。Before/head 的真实 +`runtime.ping` 在 disposable package 中均在所属 JSON 文件变化后切换 fingerprint +与 PID;故意漏掉该 JSON source 的 mutation 在同一独立 runtime oracle 上失败, +所有自有 probe runtime 已停止。八条 before/head × source/wheel × File/SQLite +完整 CLI 生命周期保留原产物、完成时间、scope、四份 receipt、一次 debit、 +refusal/replay 及两个 native 退出。这验收正确性与已执行真实路径,不验收性能。 + +新建单体 wheel 与 1840 个 packaged tracked source 相同;原 24 个 production +文件中仅 `effect_runtime.py` 改动,仍保留相同五个 source-only omission。独立 +import probe 首次从 checkout 工作目录加载了源码,其失败准备保留;修正后的 +核验使用非 checkout 工作目录、清空 `PYTHONPATH`,并在比较字节前断言模块属于 +隔离安装。Lifecycle harness 始终保留自己的独立 package-root 检查。首次在错误 +目录执行 Chat build 命令也保留为准备失败,随后 dashboard owner 的 build 验证 +bundle。 + +更大加载改动仍是下一步的独立决策:先在既有 quota/context 与 refresh dispatch +owner 对照真实 normal/disabled bytecode,再移动必需依赖。路径 allocation +变化仅是局部中间结果,不新增 whole-path/边际成本收益声明。当前 PostgreSQL +host/wheel、model/attention、连续峰值、物理设备写入、installed adoption 和 +完整长程资格仍开放;未变的 PostgreSQL store/service owner 保留此前真实资格, +本轮不宣称新 PG 运行。保留全部冻结失败、既有 peer hard-cut validation hold、 +至少一个 minor release 的 I3、M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3 及 +context/model/split-install/full-cost/adoption 验收。原 Task 与独立 exact-head +评审继续开放,本轮不合入 main、不升级 installed host。 + +### 2026-11-08 — 正常 bytecode 生命周期及观测限制 + +仅执行一对 before/head wheel,复用完整 101-Todo SQLite 生命周期:源码为 +`24ca29f4eb2afe0bbf27df00002abd005febf256` / +`1a4d9d8f92491db5913cfa59061bcd58c677868d`,打包 runtime 字节为 +`9e147ddf2085b080eba0cd2d7888ec896b270333` / +`a57fd3ef771013c80add55821321e98e577836e6`,目标 main 仍为 +`21013c0c113445b71a30f6ce06ab61455b56f1f2`。本 checkpoint 仅改双语 RFC, +生产提案保留并等待独立评审。 + +两端均新建 wheel 安装,使用相同 CPython 3.13.5、依赖版本及既有 fixture helper +的全新逐字节相同副本。安装与 fixture 初始 pyc 均为零。运行前的来源核验使用 +`-B`、非 checkout 工作目录及清空的 `PYTHONPATH`,先确认导入模块属于隔离安装, +再核对全部 1840 个 packaged tracked source 哈希;保留相同五个 source-only +omission。测量进程使用默认 bytecode,清除 write-disable、cache-prefix 与 +importtime 变量,读回 `sys.dont_write_bytecode=False`、`sys.pycache_prefix=None`。 +导入 fixture 前即隔离 data/临时目录。解释器 stdlib 与 OS 文件缓存共享,不属于 +本次隔离声明。 + +| 缓存阶段,两端路径集合完全相同 | 全部 installed pyc | LoopX pyc | +| --- | ---: | ---: | +| 安装完成 | 0 | 0 | +| Driver 导入 fixture 前 | 1 | 0 | +| Fixture/setup 完成 | 317 | 223 | +| Bootstrap 完成 | 358 | 264 | +| Prequota 完成 | 433 | 339 | +| Guard 与 required reads 完成 | 665 | 571 | +| Settled replay 与 teardown 完成 | 699 | 605 | + +Driver 首个 pyc 来自 virtualenv support。两端全新 fixture 副本在 setup 各生成 +五个 pyc;最终 605 个 LoopX pyc header 全部匹配解释器 magic 及源码 timestamp/ +size。这是缓存有效性与阶段观测,不是单次 guard 的模块数量,也未测出默认模式 +的 compile/import CPU。 + +两端原生命周期均成功退出:sum45 artifact 及 digest、原 completion time、 +required scopes、四个 committed receipt、一个 debit、prequota 不变的 warm +PID 集合、受控重启、evidence replacement refusal,以及 completion/writeback/ +spend/terminal/settled-Turn replay 均通过。每端保留全部 22 个 console 的 +command/phase/wrapper 退出记录、三个 exact-argv Node helper 退出及两个 managed +daemon 退出。Waited descendants 只计算一次,仅把 orphan daemon subtree 与已观察 +的 waiter CPU 加入 outer waited subtree。 + +**实际 console PID 观测子检查失败。** 首个 observer 把 macOS +`proc_listchildpids` 返回的 PID 数量误作字节数,记录为 0/22。独立 disposable +child probe 复现并修正 API 解释后,才执行尚未开始的 head。Head 记录 21/22, +bootstrap 仍落在有界采样窗口之外。Wrapper PID/argv/exit 记录完整,但实际 +PID 覆盖不完整;两次运行后的子检查失败都保留,不重跑任一生命周期。使用 host +不兼容 Python 3.9 的初步 parse 也失败,执行用 3.13 的语法验证通过。修复与证据 +缺口保留在原 Task 的恢复记录中。 + +| 单对原始观测,before → head | 数值 | +| --- | ---: | +| Whole harness wall | 24.044 → 25.180 s | +| 包含 orphan additions 的 observed process CPU | 10.698 → 10.724 s | +| OS time 文件中的四舍五入 console waited-subtree CPU | 6.020 → 5.980 s | +| 完整 managed-daemon subtree CPU | 1.900 → 1.920 s | +| 三个 Node helper subtree | 0.273 → 0.291 s | +| Setup / 最终 teardown wall | 5.503 / 0.062 → 3.338 / 0.054 s | + +Observer 修正使两端的有界采样等待不同,head bootstrap 另有较大 wall 延迟。 +Fixture、verifier、observer 与 readback 工作仍计入 whole harness。这些观测不能 +验收提速、回退或 paired CPU 差异,不能解释既有冻结成本实验,也不能与 +bytecode-disabled profile 相减后声称 compile saving。Console waited CPU 包含 +本地处理及 descendants,不能统称 Python startup。冻结样本不重跑、不扩充、不 +重标签;publication/exit tail、物理写入、连续系统峰值、build/setup environment +成本、model/attention 与 installed adoption 仍 unknown。 + +这回答了有限的默认缓存正确性问题,但保留成本/归因缺口。继续保留既有 typed +transaction 与 source-scan 提案。下一段应检查既有 quota/context/refresh +caller 的必需依赖闭包,再选一个完整可回退的加载改动并验证 normal-default 语义 +一致性。有效缓存本身不证明应移动必需 import 或移除 fresh read;不为补 PID +观测重复本 pair。原 Task、独立 exact-head review/readiness、peer hard-cut hold +及全部 I3/minor-release、M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3、 +context/model/split-install、PostgreSQL host/wheel、full-cost/adoption 要求 +继续开放;本轮不合入 main、不升级 host。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md index 0aeffb0e98..8b4adc4cfa 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md @@ -535,6 +535,15 @@ context projects only that Todo's current target, a short next-choice cue and counterevidence references. Its existing question-scoped read command exposes the full decision, exact-profile feedback and prior execution attribution; the default projection does not repeat that full record. +Before a current work decision exists, the same opt-in starting read can expose +bounded measurement feedback from the Todo's canonical question links. This +reuses Explore's exact-profile comparison owner, includes source references and +omitted-group navigation, and does not infer a target, accepted outcome metric +or proxy classification. A shared question link is evidence association, not +immutable attribution of execution to that Todo. Recording the first decision +remains optional; default-off and exact-field first reads retain their prior +semantics. Production File/SQLite reader qualification and actual model benefit +are separate obligations. Counterevidence requests reassessment at the next concrete choice; it is not a new hard gate, fidelity certification, permission to waive correctness or an instruction to replan every Turn. At a later meaningful capture, existing diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md index 9aee8eeefa..d19f45ab96 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md @@ -415,6 +415,11 @@ Explore evidence 可用;开启该设置不启动工作或自动开启其他 ca 普通 task-scoped Explore context 只投影该 Todo 当前目标、下一选择短提示和反证引用。 既有 question-scoped 扩读命令返回完整决策、精确 profile 的反馈和前次执行归属; 默认投影不重复完整记录。 +尚无当前工作决策时,同一 opt-in 首读可呈现来自该 Todo 规范问题关联的有界测量反馈。 +复用 Explore 精确 profile 比较 owner,保留来源引用及被省略组的读取入口,不推断目标、 +验收成果指标或代理分类。共享问题关联只表示证据关联,不是该 Todo 执行测量的不可变归属。 +首次决策仍为可选记录;默认关闭及按字段首次读取保留原语义。真实 File/SQLite reader +资格与实际模型收益分别验收。 反证要求在下一个具体选择处重新判断,不构成新的硬门、fidelity 认证、放松正确性约束的 权限或每轮重规划指令。后续有意义的 capture 可复用 execution observation,把前次 提议方法连到实际方法、候选和证据,也允许有据采用不同方法。 diff --git a/loopx/capabilities/explore/README.md b/loopx/capabilities/explore/README.md index daa00573c3..877a59c22a 100644 --- a/loopx/capabilities/explore/README.md +++ b/loopx/capabilities/explore/README.md @@ -139,6 +139,16 @@ The comparison uses findings from the current work, target, and acceptance reference. A different target or acceptance reference needs its own eligible pair, even when the measured metric, evaluator and inputs stay the same. The comparison is advisory and carries no retention or execution obligation. +On an admitted work turn, the progressive starting read includes the enabled +Yardstick's decision guidance and any compact cue for the selected Todo. Use +the Explore context reader for the attachment template and scoped evidence. +Before a first decision is recorded, the same opt-in entry shows up to three +exact-profile measurement groups from questions canonically linked to that +Todo, with omitted counts and the existing task reader. Distinct questions get +a place before additional metrics from one question. These are declared +comparisons, including shared findings; they do not establish which Todo ran a +measurement or select an outcome/proxy profile. Default-off input and the +benchmark's optional exact-field first read retain their existing behavior. Manually captured measured findings also appear in the existing result pages and, when linked to eligible work, its exact-source comparison cards. They need diff --git a/loopx/capabilities/explore/turn_context.py b/loopx/capabilities/explore/turn_context.py index 46af6028e3..6413f2ed91 100644 --- a/loopx/capabilities/explore/turn_context.py +++ b/loopx/capabilities/explore/turn_context.py @@ -13,6 +13,7 @@ dispatch_interaction_projection_hooks, TURN_START_HOOK_RESULT_SCHEMA_VERSION, TurnStartHookRegistration, + TurnStartContextProjector, dispatch_turn_start_hooks, ) from ...control_plane.effect_runtime import effect_runtime_request_scope, effect_runtime_result @@ -155,6 +156,30 @@ def explore_turn_context( )) +def turn_context_projector( + *, registry_path: Path, runtime_root: Path, goal_id: str, + agent_id: str | None, todo_id: str | None = None, +) -> TurnStartContextProjector | None: + """Bind an entrypoint's existing read request after governing selection. + + Merely enabling a Goal or supplying unrelated hooks cannot request this + read. The decision owner supplies facts; this adapter changes no admission. + """ + if not agent_id: + return None + + def project(payload: Mapping[str, Any], dispatch: Mapping[str, Any] | None) -> dict[str, Any] | None: + channel = (payload.get("interaction_contract") or {}).get("agent_channel") or {} + selected = payload.get("selected_todo") or {} + focus = (selected.get("todo_id") if payload.get("normal_delivery_allowed") is True + and channel.get("delivery_allowed") is True and selected.get("status") == "open" else None) + return extend_turn_start_dispatch(dict(dispatch or {}), + registry_path=registry_path, runtime_root=runtime_root, + goal_id=goal_id, agent_id=agent_id, todo_id=todo_id, selected_todo_id=focus) + + return project + + def extend_turn_start_dispatch( dispatch: dict[str, Any] | None, *, @@ -163,12 +188,20 @@ def extend_turn_start_dispatch( goal_id: str, agent_id: str | None, todo_id: str | None = None, + selected_todo_id: str | None = None, ) -> dict[str, Any] | None: if not agent_id: return dispatch - _, graph, gate = _policy(registry_path, goal_id, runtime_root) + goal, graph, gate = _policy(registry_path, goal_id, runtime_root) if not graph and not gate["enabled"]: return dispatch + if selected_todo_id is not None: + from ..todo_replan_cadence.machine_defaults import work_yardstick_enabled + + # This optional policy's current-work card follows the resolved work + # identity. With the policy off, preserve the caller's original scope. + if work_yardstick_enabled(goal, runtime_root): + todo_id = selected_todo_id def produce() -> dict[str, Any]: return { diff --git a/loopx/chat_server.py b/loopx/chat_server.py index c36db51033..009c96ba20 100644 --- a/loopx/chat_server.py +++ b/loopx/chat_server.py @@ -1727,7 +1727,8 @@ def do_GET(self) -> None: SSH_HOST_CATALOG_PATH: self._ssh_hosts, } if path in get_dispatch: - return get_dispatch[path]() + with effect_runtime_request_scope(): + return get_dispatch[path]() if self._dispatch_goal_result_read(path): return setup_parts = path.strip("/").split("/") diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 741e45b729..22ad811942 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -258,10 +258,6 @@ def _dispatch_quota_turn_start_hooks( ) dispatch = extend_cadence_turn_start_dispatch(dispatch, registry_path=registry_path, runtime_root=root, goal_id=args.goal_id, agent_id=args.agent_id) - if args.agent_id: - from ..capabilities.explore.turn_context import extend_turn_start_dispatch as extend_explore - dispatch = extend_explore(dispatch, registry_path=registry_path, runtime_root=root, - goal_id=args.goal_id, agent_id=args.agent_id, todo_id=args.todo_id) local_private_state_mutated = bool(dispatch.get("uncertain_local_effects")) or any( isinstance(result, Mapping) and result.get("local_private_state_mutated") is not False @@ -299,14 +295,12 @@ def _prepare_quota_command_execution( return goal_ref, turn_start_hook_dispatch, context -def _attach_turn_start_hook_dispatch( - payload: dict[str, object], - dispatch: Mapping[str, object] | None, -) -> None: - if dispatch and (dispatch.get("registered_count") or dispatch.get("failures")): - payload["turn_start_capability_hook_dispatch"] = { - key: value for key, value in dispatch.items() if key != "contexts" - } +def _quota_turn_context_projector(args, *, registry_path: Path, runtime_root: Path): + from ..capabilities.explore.turn_context import turn_context_projector + + return turn_context_projector(registry_path=registry_path, runtime_root=runtime_root, + goal_id=args.goal_id, agent_id=args.agent_id, todo_id=args.todo_id) + def _emit_bounded_settlement(payload, args, *, runtime_root, reader_argv, print_payload): """Preserve the committed receipt even when bounded result delivery fails.""" @@ -603,6 +597,8 @@ def handle_quota_command( retained_todo_id=retained_action_selection_todo_id, ) heartbeat_receipt_existing = action_selection.receipt + context_projector = _quota_turn_context_projector(args, + registry_path=registry_path, runtime_root=runtime_root) payload = _export("build_live_quota_should_run_decision")( status_payload, goal_id=args.goal_id, @@ -636,13 +632,13 @@ def handle_quota_command( turn_instance_id=heartbeat_turn_id, interaction_projection_hooks=interaction_projection_hooks, turn_start_hook_dispatch=turn_start_hook_dispatch, + turn_context_projector=context_projector, context_navigation=( native_context_activation(args)["progressive_enabled"] and not bool(getattr(args, "turn_envelope", False)) ), goal_ref=goal_ref, ) - _attach_turn_start_hook_dispatch(payload, turn_start_hook_dispatch) action_selection_preflight = ( _export("reconcile_requested_quota_action_selection")( payload, @@ -713,6 +709,7 @@ def handle_quota_command( context=context, interaction_projection_hooks=interaction_projection_hooks, turn_start_hook_dispatch=turn_start_hook_dispatch, + turn_context_projector=context_projector, action_selection=action_selection, cache_metadata=cache_metadata, goal_ref=goal_ref, diff --git a/loopx/cli_commands/quota_monitor_poll.py b/loopx/cli_commands/quota_monitor_poll.py index 501f302def..97620c099e 100644 --- a/loopx/cli_commands/quota_monitor_poll.py +++ b/loopx/cli_commands/quota_monitor_poll.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING from ..control_plane.coordination.local_authority import CanonicalAuthorityRequired -from ..control_plane.capability_hooks import InteractionProjectionHookRegistration +from ..control_plane.capability_hooks import InteractionProjectionHookRegistration, TurnStartContextProjector from ..control_plane.quota.effective_action import EffectiveAction from ..control_plane.quota.error_codes import HeartbeatReceiptIdentityConflictError from ..control_plane.quota.heartbeat_receipt import ( @@ -186,6 +186,7 @@ def record_automatic_heartbeat_stall( status_collector: Callable[..., dict[str, object]], decision_builder: Callable[..., dict[str, object]], host_observation_resolver: Callable[..., object], + turn_context_projector: TurnStartContextProjector | None = None, ) -> tuple[dict[str, object], dict[str, object], object, str]: """Commit and reproject the automatic no-spend heartbeat observation.""" @@ -263,6 +264,7 @@ def record_automatic_heartbeat_stall( turn_instance_id=turn_id, interaction_projection_hooks=interaction_projection_hooks, turn_start_hook_dispatch=turn_start_hook_dispatch, + turn_context_projector=turn_context_projector, goal_ref=goal_ref, ) rebuilt["heartbeat_stall_writeback"] = { diff --git a/loopx/cli_commands/turn.py b/loopx/cli_commands/turn.py index 7a4ac7350e..2601fa604e 100644 --- a/loopx/cli_commands/turn.py +++ b/loopx/cli_commands/turn.py @@ -225,10 +225,6 @@ def handle_turn_command( from ..capabilities.semantic_preference.agent_preferences import extend_turn_start_dispatch as extend_preferences turn_start_hook_dispatch = extend_preferences(turn_start_hook_dispatch, runtime_root=runtime_root, registry_path=registry_path, goal_id=args.goal_id, agent_id=args.agent_id) - from ..capabilities.explore.turn_context import extend_turn_start_dispatch as extend_explore - turn_start_hook_dispatch = extend_explore(turn_start_hook_dispatch, - registry_path=registry_path, runtime_root=runtime_root, - goal_id=args.goal_id, agent_id=args.agent_id, todo_id=getattr(args, "todo_id", None)) if delegated_context is not None: # Invocation material shares normal capture/delivery. It is not a # capability observation, another decision or a new read command. @@ -239,12 +235,17 @@ def handle_turn_command( # from the same live status, scheduler context and capability hooks, so # this Turn takes all of them -- and its later settle-against inputs -- # from the shared decision owner instead of rebuilding them per command. + from ..capabilities.explore.turn_context import turn_context_projector + decision_owner = build_fresh_turn_decision_owner( args, registry_path=registry_path, runtime_root=runtime_root, runtime_root_arg=runtime_root_arg, turn_start_hook_dispatch=turn_start_hook_dispatch, + turn_context_projector=turn_context_projector(registry_path=registry_path, + runtime_root=runtime_root, goal_id=args.goal_id, agent_id=args.agent_id, + todo_id=getattr(args, "todo_id", None)), goal_ref=goal_ref, request_subject=delegated_context.get("request_subject") if delegated_context else None, ) diff --git a/loopx/cli_commands/turn_decision.py b/loopx/cli_commands/turn_decision.py index 0bbdaf7ac3..9813ff5693 100644 --- a/loopx/cli_commands/turn_decision.py +++ b/loopx/cli_commands/turn_decision.py @@ -31,6 +31,7 @@ periodic_report_pending_intent_interaction_hook, ) from ..control_plane.quota.live_decision import build_live_quota_should_run_decision +from ..control_plane.capability_hooks import TurnStartContextProjector from ..control_plane.quota.effective_action import EffectiveAction from ..control_plane.quota.turn_envelope import ( TurnControllerInput, build_turn_controller_input, build_turn_envelope, @@ -139,6 +140,7 @@ def _build_turn_decision( scheduler_execution_context: Mapping[str, Any] | SchedulerExecutionContextResolution, operator_inbox_urgency_projector: Callable[..., dict[str, Any]], turn_start_hook_dispatch: Mapping[str, Any] | None = None, + turn_context_projector: TurnStartContextProjector | None = None, goal_ref: Mapping[str, object] | None = None, request_subject: Mapping[str, Any] | None = None, ) -> Callable[..., dict[str, Any]]: @@ -191,6 +193,7 @@ def build_turn_decision( receipt_bound_replan_obligation_id=bound_replan, turn_instance_id=turn_instance_id if bound_replan else None, turn_start_hook_dispatch=dict(turn_start_hook_dispatch or {}), + turn_context_projector=turn_context_projector, workspace_path=workspace_path, goal_ref=goal_ref, interaction_projection_hooks=( @@ -304,6 +307,7 @@ def build_fresh_turn_decision_owner( runtime_root: Path, runtime_root_arg: str | None, turn_start_hook_dispatch: Mapping[str, Any] | None = None, + turn_context_projector: TurnStartContextProjector | None = None, goal_ref: Mapping[str, object] | None = None, request_subject: Mapping[str, Any] | None = None, ) -> FreshTurnDecisionOwner: @@ -342,6 +346,7 @@ def build_fresh_turn_decision_owner( scheduler_execution_context=scheduler_execution_context, operator_inbox_urgency_projector=operator_inbox_urgency_projector, turn_start_hook_dispatch=turn_start_hook_dispatch, + turn_context_projector=turn_context_projector, goal_ref=goal_ref, request_subject=request_subject, ), requested_todo_id=getattr(args, "todo_id", None), request_subject=request_subject, diff --git a/loopx/control_plane/agents/profile.py b/loopx/control_plane/agents/profile.py index 0c75d154d0..580e6c341d 100644 --- a/loopx/control_plane/agents/profile.py +++ b/loopx/control_plane/agents/profile.py @@ -2,6 +2,7 @@ import re from fnmatch import fnmatchcase +from functools import lru_cache from typing import Any, Mapping from ..runtime.public_safety import public_safe_compact_text @@ -200,18 +201,35 @@ def agent_profile_candidate_rank( return 1 action_kind = normalize_todo_action_kind(item.get("action_kind")) or "" avoided = agent_profile.get("avoid_action_kinds") - if action_kind and isinstance(avoided, list) and any( + avoid = bool(action_kind and isinstance(avoided, list) and any( fnmatchcase(action_kind, str(pattern)) for pattern in avoided - ): - return 2 + )) preferred = agent_profile.get("preferred_action_kinds") - if action_kind and isinstance(preferred, list) and any( + prefer = bool(action_kind and isinstance(preferred, list) and any( fnmatchcase(action_kind, str(pattern)) for pattern in preferred - ): - return 0 + )) default_task_classes = agent_profile.get("default_task_classes") - if isinstance(default_task_classes, list): - task_class = str(item.get("task_class") or "").strip().lower() - if task_class and task_class in default_task_classes: - return 0 - return 1 + task_class = str(item.get("task_class") or "").strip().lower() + default_class = bool(task_class and isinstance(default_task_classes, list) + and task_class in default_task_classes) + from ..effect_runtime import _runtime_fingerprint_for_request + return _typed_profile_rank(avoid, prefer, default_class, _runtime_fingerprint_for_request()) + + +@lru_cache(maxsize=16) +def _typed_profile_rank(avoid: bool, prefer: bool, default_class: bool, revision: str) -> int: + """Legacy fnmatch IO stays here; advisory rank precedence has one TS owner. + + Memoize the eight immutable Boolean combinations at their runtime revision, + never item, profile, source, authority or an execution decision. + """ + from ..effect_runtime import effect_runtime_result + + result = effect_runtime_result("todo.quota_planning.project", { + "schema_version": "todo_profile_rank_request_v0", + "facts": {"avoid": avoid, "prefer": prefer, "default_class": default_class}, + }) + rank = result.get("rank") if isinstance(result, dict) else None + if type(rank) is not int or rank not in (0, 1, 2): + raise RuntimeError("invalid typed advisory profile rank") + return rank diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 969da23a9c..fb6556eb5e 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -18,7 +18,7 @@ import {projectExploreMeasurementFeedback} from "./explore_measurements.ts"; * provenance and applicability; no timestamp or revision is normalized away. */ export function exploreReusableContextPaths(content: JsonObject = {}): (string | number)[][] { return [ - ...["recent_nodes", "recent_findings", "writeback_results", "measurement_feedback", "work_yardsticks", "work_yardstick_capture", + ...["recent_nodes", "recent_findings", "writeback_results", "measurement_feedback", "work_measurements", "work_yardsticks", "work_yardstick_capture", "path_delta_attachment_template", "linked_question_attachment_template", "optional_question_evolution_example", "optional_execution_observation_example", "result_writeback_guidance", "result_attachment_template", "optional_measurement_example", "measurement_guidance", @@ -338,14 +338,34 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { ...(resultNode == null ? [] : ["--result-node", resultNode]), ) : null; const writeback = exploreResultWritebackAffordance(); - const workCues = params.yardstick_enabled === true ? rows(params.work_todos) + const workTodos = params.yardstick_enabled === true ? rows(params.work_todos) .filter(todo => todo.role === "agent" && !todo.done && todo.claimed_by === agent && ["open", "pending", "in_progress", "blocked"].includes(String(todo.status)) - && (todoId === null || todo.todo_id === todoId)) - .map(todo => projectWorkYardstickCue({enabled: true, goal_id: goal, agent_id: agent, todo_id: todo.todo_id, + && (todoId === null || todo.todo_id === todoId)) : []; + const currentWorkCues = workTodos.map(todo => projectWorkYardstickCue({enabled: true, goal_id: goal, agent_id: agent, todo_id: todo.todo_id, findings: findings.filter(row => (todo.explore_result_node_refs as unknown[] ?? []).includes(row.node_id)), measurement_feedback: measurementFeedback})) - .filter((cue): cue is JsonObject => cue !== null && (resultNode === null || cue.node_id === resultNode)) : []; + .filter((cue): cue is JsonObject => cue !== null); + const workCues = currentWorkCues.filter(cue => resultNode === null || cue.node_id === resultNode); + // A first decision may use measurements recorded before any Yardstick + // declaration. Reuse exact-profile feedback through canonical question links; + // those links do not establish which Todo executed a shared finding. + const workMeasurements = workTodos.flatMap(todo => { + if (todo.status !== "open" || currentWorkCues.some(cue => (cue.binding as JsonObject).todo_id === todo.todo_id)) return []; + const linked = new Set(requireStringArray(todo.explore_result_node_refs ?? [], "Explore task question refs")); + const available = measurementFeedback.filter(row => linked.has(row.node_id)); + if (!available.length) return []; + // Give distinct linked questions a place before filling the remaining + // budget with further metrics. No metric is inferred to be the outcome. + const first = new Map(); + for (const row of available) if (!first.has(row.node_id)) first.set(row.node_id, row); + const representatives = new Set(first.values()); + const visible = [...representatives, ...available.filter(row => !representatives.has(row))].slice(0, 3); + return [{todo_id: todo.todo_id, measurement_feedback: visible, + omitted_measurement_feedback: available.length - visible.length, + details_command: command("turn-context", "--agent-id", agent, "--todo-id", String(todo.todo_id), "--result-limit", "20"), + guidance: "Recorded comparisons for questions linked by this Todo; shared findings do not prove this Todo executed them. Match each metric and recorded scope to the accepted outcome before choosing work. No outcome/proxy profile has been selected here. Latest recorded is not current or retained, and best is not acceptance or permission. Use the existing task reader for omitted groups; a decision attachment remains optional."}]; + }); return { ok: true, goal_id: goal, agent_id: agent, graph_enabled: graph, harness_enabled: harness, ...(todoId === null ? {} : {todo_id: todoId}), @@ -354,11 +374,13 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { counts: projection.counts ?? {}, ...(params.yardstick_enabled === true ? { work_yardstick_capture: {optional: true, field: "work_yardstick_decision", schema_version: WORK_YARDSTICK_DECISION_SCHEMA, - guidance: "At a meaningful target, yardstick or next-method change, optionally attach the current decision to the existing Explore result. No change needs no attachment. Cite prior_finding_id from this Todo's current decision (null initially). Distinguish the accepted outcome's evaluation profile from an unqualified proxy; record counterevidence and bounded next work without changing user constraints. Read the ordinary task-scoped context for omitted decisions.", + guidance: "Before a proxy would discard a viable candidate or redirect work, check agreement with the accepted outcome in this scope. When agreement is unknown or the signals disagree, use one bounded accepted-outcome comparison if available within the existing constraints. At a meaningful target, yardstick or method change, optionally record that decision and its counterevidence in the same Todo's Explore result. Cite its prior_finding_id (null initially); omit the attachment when the decision is unchanged. Read the task-scoped Explore context for the template and omitted decisions.", template: {schema_version: WORK_YARDSTICK_DECISION_SCHEMA, target: {summary: "", scope: "", acceptance_ref: ""}, authoritative: {metric: "", unit: "", higher_is_better: true, comparison_scope: "", input_revision: "", evaluation_ref: ""}, proxy: null, counterevidence_refs: [], next_action: {method_id: "", reason: "", cost_bound: ""}, prior_finding_id: null}}, + ...(workMeasurements.length ? {work_measurements: workMeasurements.slice(0, 3), + omitted_work_measurements: Math.max(0, workMeasurements.length - 3)} : {}), ...(workCues.length ? {work_yardsticks: workCues.slice(0, 3).map(cue => { if (resultNode !== null) return cue; const comparison = cue.search_comparison as JsonObject | undefined; diff --git a/loopx/control_plane/capability_hooks.py b/loopx/control_plane/capability_hooks.py index 7b1c482d7f..464830766f 100644 --- a/loopx/control_plane/capability_hooks.py +++ b/loopx/control_plane/capability_hooks.py @@ -69,6 +69,11 @@ InteractionProjectionProducer = Callable[[], Mapping[str, Any]] TurnStartProducer = Callable[[], Mapping[str, Any]] +# Invocation-owned read composition after selection; no persisted policy or +# provider authority is created by supplying this adapter. +TurnStartContextProjector = Callable[ + [Mapping[str, Any], Mapping[str, Any] | None], dict[str, Any] | None +] PostWritebackProducer = Callable[[Mapping[str, Any]], Mapping[str, Any]] diff --git a/loopx/control_plane/coordination/authority_store.ts b/loopx/control_plane/coordination/authority_store.ts index fcf45fa85c..f4dd15f8f1 100644 --- a/loopx/control_plane/coordination/authority_store.ts +++ b/loopx/control_plane/coordination/authority_store.ts @@ -135,6 +135,14 @@ export type AuthorityStoreLoadResult = | { status: "missing" } | AuthorityStoreReadFailure; +/** Same identity/load failure precedence as separate reads. A provider may + * bind both observations to one read transaction; this never proves the + * pathname still refers to that incarnation after the transaction closes. */ +export interface AuthorityStoreSnapshotResult { + identity: AuthorityStoreIdentityResult; + authority: AuthorityStoreLoadResult; +} + export type AuthorityStoreCommitResult = | { status: "applied"; provider_revision: string; cursor: string } | { @@ -182,6 +190,9 @@ export interface AuthorityStore { readonly providerKind?: AuthorityStoreProviderKind; storeIdentity(): Promise; loadAuthority(): Promise; + /** Optional read acceleration. Callers retain their independent final + * incarnation check. It grants no creation or write authority. */ + readAuthoritySnapshot?(): Promise; commitAuthority(commit: AuthorityStoreCommit): Promise; readReceipt(operationId: string): Promise; /** Optional bounded acceleration; never a weaker receipt verification path. */ @@ -189,6 +200,15 @@ export interface AuthorityStore { scanCommitted(afterCursor: string | null, limit: number): Promise; } +export async function readAuthoritySnapshot(store: AuthorityStore): Promise { + if (store.readAuthoritySnapshot !== undefined) return store.readAuthoritySnapshot(); + const identity = await store.storeIdentity(); + // Unavailable identity still needs the legacy absence probe; failed identity + // must keep its precedence without attempting a second provider operation. + const authority = identity.status === "failed" ? identity : await store.loadAuthority(); + return {identity, authority}; +} + export async function readAuthorityReceipts(store: AuthorityStore, operationIds: readonly string[]): Promise { if (operationIds.length < 1 || operationIds.length > 64) throw new TypeError("receipt batch requires 1..64 operations"); diff --git a/loopx/control_plane/coordination/canonical_snapshot_page.ts b/loopx/control_plane/coordination/canonical_snapshot_page.ts index 60be32d63f..33991ecbd8 100644 --- a/loopx/control_plane/coordination/canonical_snapshot_page.ts +++ b/loopx/control_plane/coordination/canonical_snapshot_page.ts @@ -5,7 +5,7 @@ import type {JsonObject} from "../effect_program.ts"; import {requireJsonObject} from "../runtime_decode.ts"; import {MAX_LOCAL_SNAPSHOT_BYTES} from "../effect_runtime_snapshot.ts"; import {decodeProjectionReadback, confirmProjectionReadback} from "../todos/projection_delivery.ts"; -import {authorityStoreSourceAuthority, type AuthorityStore} from "./authority_store.ts"; +import {authorityStoreSourceAuthority, readAuthoritySnapshot, type AuthorityStore} from "./authority_store.ts"; import {canonicalAuthoritySha256, hasExactAuthorityKeys, requireAuthorityStoreId} from "./authority_store_codec.ts"; import {canonicalTodoCollection} from "./local_authority_read.ts"; import {canonicalAuthorityRouteFailure, requireCanonicalAuthority} from "./local_authority_write.ts"; @@ -75,19 +75,17 @@ export async function readCanonicalSnapshotFromStore(value: unknown, store: Auth const source = authorityStoreSourceAuthority(store); const base = {schema_version: CANONICAL_SNAPSHOT_PAGE_RESULT, source_authority: source, decision_read_from_provider: true, legacy_fallback_used: false}; - const identity = await store.storeIdentity(); + const {identity, authority: head} = await readAuthoritySnapshot(store); if (identity.status !== "available") { // A promoted but absent store has the same caller contract as the old // single-read path: recovery must see "missing". Identity is unavailable // before a store exists, so distinguish absence from an identity failure // without opening a replacement authority. if (identity.status === "unavailable") { - const absent = await store.loadAuthority(); - if (absent.status === "missing") return {...base, ...absent}; + if (head.status === "missing") return {...base, ...head}; } return {...base, ...identity}; } - const head = await store.loadAuthority(); if (head.status !== "loaded") return {...base, ...head}; const query = canonicalAuthoritySha256({goal_id: goal, include_leases: input.include_leases, projection_readback: readback}); // Fail before expensive projection work when a continuation is already stale. diff --git a/loopx/control_plane/coordination/sqlite_authority_store.ts b/loopx/control_plane/coordination/sqlite_authority_store.ts index f36649b980..c187944f6c 100644 --- a/loopx/control_plane/coordination/sqlite_authority_store.ts +++ b/loopx/control_plane/coordination/sqlite_authority_store.ts @@ -7,7 +7,7 @@ import type { DatabaseSync } from "node:sqlite"; import type { JsonObject } from "../effect_program.ts"; import type { AuthorityStore, AuthorityStoreCommit, AuthorityStoreCommitResult, AuthorityStoreCommittedTransaction, - AuthorityStoreIdentityResult, AuthorityStoreLoadResult, AuthorityStoreReadFailure, AuthorityStoreHead, + AuthorityStoreIdentityResult, AuthorityStoreLoadResult, AuthorityStoreSnapshotResult, AuthorityStoreReadFailure, AuthorityStoreHead, AuthorityStoreReceiptResult, AuthorityStoreReceiptBatchResult, AuthorityStoreScanResult } from "./authority_store.ts"; import { AuthorityStoreProtocolError, canonicalAuthorityBytes, canonicalAuthorityObject, canonicalAuthorityObjectList, canonicalAuthoritySha256, @@ -163,12 +163,16 @@ export class SqliteAuthorityStore implements AuthorityStore { readonly goalId: string; readonly existingOnly: boolean; readonly expectedIdentity: string | undefined; + readonly readAuthoritySnapshot?: () => Promise; constructor(directory: string, goalId: string, options: {existingOnly?: boolean; expectedIdentity?: string} = {}) { this.goalId = requireAuthorityStoreId(goalId, "goal id"); this.path = sqliteAuthorityPath(directory, this.goalId); this.existingOnly = options.existingOnly ?? false; this.expectedIdentity = options.expectedIdentity; + // Only an already selected read-only store offers the paired read. General + // stores retain their existing identity/initialization and load behavior. + if (this.existingOnly) this.readAuthoritySnapshot = this.readSnapshot.bind(this); } private open(write: boolean): DatabaseSync | null { @@ -407,6 +411,27 @@ export class SqliteAuthorityStore implements AuthorityStore { finally { db?.close(); } } + private async readSnapshot(): Promise { + let db: DatabaseSync | null = null; + let identity: AuthorityStoreIdentityResult = {status: "unavailable", + reason_code: "store_identity_unavailable", reason: "SQLite authority database is missing"}; + try { + db = this.open(false); + if (!db) return {identity, authority: {status: "missing"}}; + db.exec("BEGIN"); + identity = {status: "available", store_identity: this.identity(db)}; + const current = this.current(db); + return {identity, authority: current === null ? {status: "missing"} : { + status: "loaded", cursor: current.state.cursor.toString(), + provider_revision: current.provider_revision, head: current.state.projection}}; + } catch (error) { + const failure = readFailure(error); + return {identity: identity.status === "available" ? identity : failure, authority: failure}; + } finally { db?.close(); } + // No await while the transaction owns the connection. Neither the opened + // handle nor its head survives this method; final identity reopens the path. + } + /** No await between BEGIN and ROLLBACK: another DatabaseSync request must not * block this event loop while the transaction holder awaits filesystem I/O. * The transaction excludes writers; it cannot roll back external run files. */ diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 040251b1de..8da649943d 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -169,6 +169,12 @@ def remember_todo_context(self, key: bytes, result: bytes) -> None: ) +def todo_projection_request_identity() -> object | None: + """Bind a source codec to its current logical request, never to a TTL.""" + state = _REQUEST_RUNTIME_REVISION.get() + return state if state is not None and not state._closed else None + + class EffectRuntimeRemoteError(RuntimeError): """A typed exception returned by the managed TypeScript runtime.""" @@ -324,10 +330,11 @@ def _scan_runtime_source_snapshot(root: Path) -> _RuntimeSourceSnapshot: for directory, child_directories, filenames in os.walk(root): child_directories.sort() relative_directory = os.path.relpath(directory, root) - if relative_directory == ".": - relative_directory = "" + relative_prefix = ( + "" if relative_directory == "." else relative_directory.replace(os.sep, "/") + "/" + ) for filename in sorted(filenames): - relative = Path(relative_directory, filename).as_posix() + relative = relative_prefix + filename dot = filename.rfind(".") if ((dot <= 0 or filename[dot:] not in _RUNTIME_SOURCE_SUFFIXES) and relative not in _RUNTIME_SOURCE_PATHS): @@ -1601,6 +1608,17 @@ def effect_runtime_result( large_local_snapshot: bool = False, ) -> Any: state = _REQUEST_RUNTIME_REVISION.get() if method == "todo.context.page" else None + def validate_result(result: Any) -> Any: + # A caller must explicitly request a bundle. A request scope or a small + # source does not establish that it will consume additional views. + if set(params) == {"summary_display_views"} and (not isinstance(result, dict) or any( + not isinstance(result.get(view), list) + or any(not isinstance(row, dict) for row in result[view]) + for view in ("summary", "planning", "terminal") + )): + raise RuntimeError("invalid TypeScript Todo source views") + return result + key = None if state is not None: # Preserve JSON order and scalar types, including bool/int, signed zero @@ -1613,7 +1631,7 @@ def effect_runtime_result( if key is not None: prior = state.todo_context_result(key) if prior is not None: - return json.loads(prior) # A caller cannot mutate another view. + return validate_result(json.loads(prior)) # A caller cannot mutate another view. result = effect_runtime_request( method, params, @@ -1621,6 +1639,7 @@ def effect_runtime_result( retry_safe=retry_safe, large_local_snapshot=large_local_snapshot, ).get("result") + validate_result(result) if state is not None and key is not None and isinstance(result, dict): try: encoded = json.dumps(result, ensure_ascii=False, allow_nan=False, diff --git a/loopx/control_plane/effect_runtime_server.ts b/loopx/control_plane/effect_runtime_server.ts index 6c4d1d151c..a4817c8282 100644 --- a/loopx/control_plane/effect_runtime_server.ts +++ b/loopx/control_plane/effect_runtime_server.ts @@ -15,7 +15,7 @@ import { import { atomicWriteJson, withFileMutationLock } from "./effect_runtime_io.ts"; import { retireRuntime } from "./effect_runtime_compile_cache.ts"; import { sqliteRuntimeIdentity } from "./coordination/sqlite_runtime.ts"; -import {openPrivateResponseSink, readPrivateJsonSnapshot, writePrivateResponse} from "./effect_runtime_snapshot.ts"; +import {MAX_INLINE_RESPONSE_BYTES, openPrivateResponseSink, readPrivateJsonSnapshot, writePrivateResponse} from "./effect_runtime_snapshot.ts"; import { requireJsonObject as requiredObject, requireNonEmptyString as requiredString, @@ -26,7 +26,6 @@ const RESPONSE_SCHEMA = "loopx_effect_runtime_response_v1"; const INFO_SCHEMA = "loopx_effect_runtime_info_v0"; const STARTUP_ERROR_SCHEMA = "loopx_effect_runtime_startup_error_v0"; const MAX_REQUEST_BYTES = 8 * 1024 * 1024; -const MAX_INLINE_RESPONSE_BYTES = 8 * 1024 * 1024; // Explicit opt-in: ordinary effects retain a bounded request/response wire. const LOCAL_SNAPSHOT_METHODS = new Set(localSnapshotMethods); const DEFAULT_IDLE_MS = 5 * 60 * 1_000; diff --git a/loopx/control_plane/effect_runtime_snapshot.ts b/loopx/control_plane/effect_runtime_snapshot.ts index 6bf287a57b..cdc86696c6 100644 --- a/loopx/control_plane/effect_runtime_snapshot.ts +++ b/loopx/control_plane/effect_runtime_snapshot.ts @@ -11,6 +11,7 @@ import {requireJsonObject, requireNonEmptyString} from "./runtime_decode.ts"; import { BARE_SHA256_PATTERN } from "./content_digest.ts"; export const MAX_LOCAL_SNAPSHOT_BYTES = 64 * 1024 * 1024; +export const MAX_INLINE_RESPONSE_BYTES = 8 * 1024 * 1024; async function privateFile(path: string, flags: number): Promise { if (!isAbsolute(path) || !basename(dirname(path)).startsWith("loopx-effect-")) { diff --git a/loopx/control_plane/goals/goal_vision_wait.py b/loopx/control_plane/goals/goal_vision_wait.py index 0796306709..cf82301556 100644 --- a/loopx/control_plane/goals/goal_vision_wait.py +++ b/loopx/control_plane/goals/goal_vision_wait.py @@ -79,6 +79,20 @@ def build_goal_vision_wait_state( """ summary = agent_todo_summary if isinstance(agent_todo_summary, dict) else {} + from ..todos.summary_item import quota_vision_wait_basis + basis = quota_vision_wait_basis(summary, source_items, agent_id) if lineage_source_items is None else None + if basis is not None: + result = effect_runtime_result("goal.vision_wait.project", { + "agent_todo_summary": {"vision_wait_states": summary.get("vision_wait_states"), + "current_agent_blocker_items": basis["current_agent_blocker_items"]}, + "agent_id": agent_id, "acceptance_gaps": acceptance_gaps or [], + "selectable_advancement_count": selectable_advancement_count, "wait_basis": basis, + }, large_local_snapshot=True) + if result is not None and (not isinstance(result, dict) + or result.get("schema_version") != GOAL_VISION_WAIT_STATE_SCHEMA_VERSION + or result.get("state") != "waiting"): + raise RuntimeError("TypeScript vision wait state shape mismatch") + return result planning_input = summary if source_items is not None: planning_input = { diff --git a/loopx/control_plane/goals/vision_wait_coverage.ts b/loopx/control_plane/goals/vision_wait_coverage.ts index 128e9cb9a8..a2ea4b02ad 100644 --- a/loopx/control_plane/goals/vision_wait_coverage.ts +++ b/loopx/control_plane/goals/vision_wait_coverage.ts @@ -159,10 +159,13 @@ function projectVisionWaitFromFacts(input: {request: JsonObject; gaps: JsonObjec const summary = request.agent_todo_summary === null ? {} : requireJsonObject(request.agent_todo_summary, "agent_todo_summary"); const cachedWait = cachedWaitProof(summary, agentId, causalIds); if (cachedWait) return cachedWait; - const resumePlan = projectTodoResumePlanning(requireJsonObject(request.resume_planning_request, "resume_planning_request")); + const basis = request.wait_basis == null ? null : requireJsonObject(request.wait_basis, "wait basis"); + const resumePlan = basis === null ? projectTodoResumePlanning(requireJsonObject(request.resume_planning_request, "resume_planning_request")) : + requireJsonObject(basis.resume_planning, "prepared resume planning"); const blockers = currentAgentBlockers(summary.current_agent_blocker_items, agentId); const candidates = Array.isArray(resumePlan.blocked_successor_items) ? resumePlan.blocked_successor_items as JsonObject[] : []; - const lineageRows = Array.isArray(request.lineage_source_items) ? request.lineage_source_items : candidates; + const lineageRows = Array.isArray(request.lineage_source_items) ? request.lineage_source_items : + basis !== null && Array.isArray(basis.lineage_source_items) ? basis.lineage_source_items : candidates; const coverage = projectWaitCoverage(causalIds, candidates, blockers, lineageRows); if (coverage.covered !== true) return null; const waitSelection = selectedWaitItems(blockers, candidates, new Set(coverage.witness_todo_ids as string[])); diff --git a/loopx/control_plane/quota/live_decision.py b/loopx/control_plane/quota/live_decision.py index 23a98ebfab..0b94936bd1 100644 --- a/loopx/control_plane/quota/live_decision.py +++ b/loopx/control_plane/quota/live_decision.py @@ -13,6 +13,7 @@ from ..goals.goal_frontier.fallback_source import live_fallback_authority_items from ..capability_hooks import ( InteractionProjectionHookRegistration, + TurnStartContextProjector, dispatch_interaction_projection_hooks, ) from .effect_program import ( @@ -220,8 +221,12 @@ def _project_turn_start_required_reads( turn_instance_id: str | None, registry_path: Path, runtime_root: Path, -) -> bool: - """Order evidence before work and report whether the decision changed.""" + context_projector: TurnStartContextProjector | None = None, +) -> Mapping[str, Any] | None: + """Order evidence after selection and retain its source dispatch.""" + + if context_projector is not None: + dispatch = context_projector(payload, dispatch) # Keep failure observations even when no evidence read was produced. The # typed envelope projects their cache/dependent-action policy for the host. @@ -231,7 +236,7 @@ def _project_turn_start_required_reads( } projected = _turn_start_required_reads(dispatch) if not projected: - return False + return dispatch existing = payload.get("required_reads") required_reads = ( [dict(item) for item in existing if isinstance(item, Mapping)] @@ -270,7 +275,7 @@ def _project_turn_start_required_reads( registry_path=str(registry_path), runtime_root=str(runtime_root), ) - return True + return dispatch def _fresh_operator_inbox_observation_count( @@ -595,6 +600,7 @@ def build_live_quota_should_run_decision( interaction_projection_hooks: Sequence[InteractionProjectionHookRegistration] | None = None, turn_start_hook_dispatch: Mapping[str, Any] | None = None, + turn_context_projector: TurnStartContextProjector | None = None, context_navigation: bool = False, workspace_path: Path | None = None, goal_ref: Mapping[str, object] | None = None, @@ -756,7 +762,7 @@ def build_live_quota_should_run_decision( turn_start_hook_dispatch, registry=registry_path, runtime_root=runtime_root, goal_id=goal_id, agent_id=agent_id, ) - _project_turn_start_required_reads( + turn_start_hook_dispatch = _project_turn_start_required_reads( payload, turn_start_hook_dispatch, available_capabilities=available_capabilities, @@ -764,6 +770,7 @@ def build_live_quota_should_run_decision( turn_instance_id=turn_instance_id, registry_path=registry_path, runtime_root=runtime_root, + context_projector=turn_context_projector, ) hook_dispatch = dispatch_interaction_projection_hooks(interaction_projection_hooks) projections = hook_dispatch["projections"] diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index c0479ab174..13c2465704 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -488,14 +488,51 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include("interaction_contract", "agent_channel", "required_reads"); const work = ["interaction_contract", "agent_channel", "work_context"]; include(...work, "instruction"); + const channel = object(interaction.agent_channel); + const sources = object(channel?.work_context)?.sources; + const sourceRows = Array.isArray(sources) ? sources.map((source, index) => ({source, index})) : []; + const selectedTodo = object(context.selected_todo); + const identityAgent = object(context.agent_identity)?.agent_id; + const agentId = context.agent_id ?? identityAgent; + // Deliver the opted-in decision cue before work, while the candidate or + // test can still change. The template and evidence stay on their owner read. + const cueStart = pointers.length; + if (context.effective_action === EffectiveAction.NORMAL_RUN && context.normal_delivery_allowed === true + && typeof selectedTodo?.todo_id === "string" && typeof context.goal_id === "string" && typeof agentId === "string" + && (identityAgent === undefined || identityAgent === agentId)) { + for (const {source, index} of sourceRows) { + const row = object(source), view = object(row?.content), graph = object(view?.graph); + if (row?.kind !== "explore_turn_context" || row.capability_id !== "explore" + || view?.ok !== true || view.graph_enabled !== true || view.goal_id !== context.goal_id + || view.agent_id !== agentId || (view.todo_id !== undefined && view.todo_id !== selectedTodo.todo_id) + || typeof object(graph?.work_yardstick_capture)?.guidance !== "string") continue; + const graphPath = [...work, "sources", index, "content", "graph"]; + include(...graphPath, "work_yardstick_capture", "guidance"); + const measurements = graph?.work_measurements; + if (Array.isArray(measurements)) { + const current = measurements.findIndex(card => object(card)?.todo_id === selectedTodo.todo_id); + if (current >= 0) include(...graphPath, "work_measurements", current); + } + const cues = graph?.work_yardsticks; + if (Array.isArray(cues)) { + const current = cues.findIndex(cue => object(cue)?.todo_id === selectedTodo.todo_id); + if (current >= 0) include(...graphPath, "work_yardsticks", current); + } + break; + } + } + // Place the cue after the run decision, before variable-size Goal and work + // fields. This also works when the full selection falls back to plain paging. + const decisionEnd = pointers.indexOf("/normal_delivery_allowed"); + if (decisionEnd >= 0 && pointers.length > cueStart) { + const cuePointers = pointers.splice(cueStart); + pointers.splice(decisionEnd + 1, 0, ...cuePointers); + } // If the full starting selection needs paging, put the current decision and // its read obligations in one independently readable opening. The rest is // still required under the original ordering and authority. const opening = pointers.splice(0); include(...work, "user_todos"); - const channel = object(interaction.agent_channel); - const sources = object(channel?.work_context)?.sources; - const sourceRows = Array.isArray(sources) ? sources.map((source, index) => ({source, index})) : []; // A complete scoped preference can be larger than a reader page. Visit // current narrative sources first, with their availability and provenance, // before selecting other sources' metadata: complete-parent coalescing keeps diff --git a/loopx/control_plane/quota/should_run_prepare.py b/loopx/control_plane/quota/should_run_prepare.py index f9488fa50b..1db6f76bdf 100644 --- a/loopx/control_plane/quota/should_run_prepare.py +++ b/loopx/control_plane/quota/should_run_prepare.py @@ -98,9 +98,7 @@ todo_item_task_class as projection_todo_item_task_class, ) from ..todos.quota_summary import ( - select_planning_inventory_source_items, - select_quota_todo_source_items, - select_quota_todo_summary, + select_quota_todo_contexts, select_task_orchestration_authority_items, ) from ..todos.summary_item import compact_todo_summary_item, compact_todo_summary_items @@ -237,6 +235,7 @@ def _prepare_quota_replan_context( project_asset: dict[str, Any], user_todo_summary: dict[str, Any] | None, agent_todo_summary: dict[str, Any] | None, + agent_todo_terminal_source_items: list[dict[str, Any]], authoritative_fallback_todo_items: FallbackTodoSource, effective_available_capabilities: Any, work_lane_contract: dict[str, Any] | None, @@ -251,11 +250,6 @@ def _prepare_quota_replan_context( replan_explore_reader: Callable[[str, str | None], Mapping[str, Any] | None] | None, ) -> tuple[dict[str, Any], dict[str, Any] | None]: """Project the current frontier and contextualize its receipt-bound review.""" - replan_todo_items = select_planning_inventory_source_items( - item.get("agent_todos"), - project_asset.get("agent_todos") if project_asset else None, - include_terminal=True, - ) goal_frontier_context = build_goal_frontier_projection_context_from_status( replan_explore_reader=replan_explore_reader, goal_id=safe_goal_id, @@ -265,7 +259,7 @@ def _prepare_quota_replan_context( project_asset=project_asset, user_todo_summary=user_todo_summary, agent_todo_summary=agent_todo_summary, - agent_todo_source_items=replan_todo_items, + agent_todo_source_items=agent_todo_terminal_source_items, fallback_todo_source_items=authoritative_fallback_todo_items, available_capabilities=effective_available_capabilities, work_lane_contract=work_lane_contract, @@ -296,7 +290,7 @@ def _prepare_quota_replan_context( source_status=status_payload, stride_strategy=goal_frontier_context["stride_strategy"], explore_reader=replan_explore_reader, goal_acceptance_contract=(agent_todo_summary or {}).get("goal_acceptance_contract"), - todo_items=replan_todo_items, + todo_items=agent_todo_terminal_source_items, ) return goal_frontier_context, replan_obligation @@ -701,40 +695,29 @@ def _prepare_quota_should_run_item( item=item, project_asset=project_asset, available=available_capabilities, ) effective_available_capabilities = availability["effective"] - user_todo_summary = select_quota_todo_summary( + user_todo_summary, agent_todo_summary, agent_todo_display_views, user_todo_source_items = select_quota_todo_contexts( item.get("user_todos"), project_asset.get("user_todos") if project_asset else None, - agent_identity=agent_identity, - filter_user_gate_blocks_agent=True, - available_capabilities=effective_available_capabilities, - ) - agent_todo_summary = select_quota_todo_summary( item.get("agent_todos"), project_asset.get("agent_todos") if project_asset else None, agent_identity=agent_identity, available_capabilities=effective_available_capabilities, ) - user_todo_source_items = select_quota_todo_source_items( - item.get("user_todos"), - project_asset.get("user_todos") if project_asset else None, - ) - agent_todo_source_items = select_quota_todo_source_items( - item.get("agent_todos"), - project_asset.get("agent_todos") if project_asset else None, - ) - agent_todo_planning_source_items = select_planning_inventory_source_items( - item.get("agent_todos"), - project_asset.get("agent_todos") if project_asset else None, - ) + # Only this Agent source is consumed as summary, planning and retained + # terminal rows below. User-source and general display calls stay single-view. + agent_todo_source_items = agent_todo_display_views["summary"] + agent_todo_planning_source_items = agent_todo_display_views["planning"] task_orchestration_agent_items = select_task_orchestration_authority_items( item.get("agent_todos"), project_asset.get("agent_todos") if project_asset else None, role="agent", + fallback_source_items=agent_todo_source_items, ) task_orchestration_user_blockers = select_task_orchestration_authority_items( item.get("user_todos"), project_asset.get("user_todos") if project_asset else None, role="user", + fallback_source_items=user_todo_source_items, ) agent_scoped_user_todo_override = _agent_scoped_user_todo_override( state=state, @@ -986,6 +969,7 @@ def _prepare_quota_should_run_item( project_asset=project_asset, user_todo_summary=user_todo_summary, agent_todo_summary=agent_todo_summary, + agent_todo_terminal_source_items=agent_todo_display_views["terminal"], authoritative_fallback_todo_items=authoritative_fallback_todo_items, effective_available_capabilities=effective_available_capabilities, work_lane_contract=work_lane_contract, diff --git a/loopx/control_plane/todos/context_projection.ts b/loopx/control_plane/todos/context_projection.ts index 2802a09ccd..9c9c6fba0c 100644 --- a/loopx/control_plane/todos/context_projection.ts +++ b/loopx/control_plane/todos/context_projection.ts @@ -24,6 +24,19 @@ const CONTEXT_FIELDS = [ /** Select before redaction; never convert nested scopes or booleans to prose. */ export function projectTodoContextPage(input: JsonObject): JsonObject { + if (input.summary_display_views !== undefined) { + if (Object.keys(input).length !== 1) throw new EffectRuntimeRequestError("summary display views cannot be combined with other views"); + const source = requireJsonObject(input.summary_display_views, "summary display views"); + if (Object.keys(source).some(key => key !== "source" && key !== "handoff_gates")) { + throw new EffectRuntimeRequestError("invalid summary display views fields"); + } + requireJsonObject(source.source, "summary source"); + return { + summary: projectTodoSummaryDisplay(source), + planning: projectTodoSummaryDisplay({source: source.source, include_terminal: false}), + terminal: projectTodoSummaryDisplay({source: source.source, include_terminal: true}), + }; + } if (input.summary_display !== undefined) { if (Object.keys(input).length !== 1) throw new EffectRuntimeRequestError("summary display cannot be combined with other views"); return {items: projectTodoSummaryDisplay(requireJsonObject(input.summary_display, "summary display"))}; @@ -188,14 +201,14 @@ const SUMMARY_FIELDS = [ "route_key", "completed_at", "updated_at", "completion_turn_key", "superseded_by", "handoff_note", ]; -const SUMMARY_SOURCES = [ +export const SUMMARY_SOURCES = [ "active_next_action_items", "active_next_action_executable_items", "first_open_items", "backlog_items", "unclaimed_priority_open_items", "claimed_open_items", "claimed_advancement_open_items", "claimed_monitor_open_items", "monitor_open_items", "current_agent_claimed_open_items", "current_agent_claimed_advancement_items", "current_agent_claimed_monitor_items", "blocker_items", "resume_blocked_items", "monitor_blocked_resume_candidates", "current_agent_monitor_blocked_resume_candidates", "unclaimed_monitor_blocked_resume_candidates", "items", ]; -const PLANNING_SOURCES = ["items", "deferred_items", "blocker_items", "monitor_open_items"]; +export const PLANNING_SOURCES = ["items", "deferred_items", "blocker_items", "monitor_open_items"]; function compactSummaryRecord(item: JsonObject, body: unknown): JsonObject { const row: JsonObject = {index: item.index ?? null, text: body}; diff --git a/loopx/control_plane/todos/goal_todo_projection.py b/loopx/control_plane/todos/goal_todo_projection.py index e2a40dac00..3dcea4291e 100644 --- a/loopx/control_plane/todos/goal_todo_projection.py +++ b/loopx/control_plane/todos/goal_todo_projection.py @@ -74,7 +74,9 @@ def observe_todo_acceptance( canonical, registry_path=registry_path, goal_id=goal_id) summary = fields.get("agent_todos") if isinstance(summary, dict) and "goal_acceptance_contract" in summary: - return {**fields, "agent_todos": {**summary, "goal_acceptance_contract": acceptance}} + from .summary_item import preserve_canonical_todo_source + return {**fields, "agent_todos": preserve_canonical_todo_source(summary, + {**summary, "goal_acceptance_contract": acceptance})} return fields @@ -248,7 +250,11 @@ def project_todo_read(params: dict[str, Any]) -> dict[str, Any]: from ..effect_runtime import MAX_LOCAL_SNAPSHOT_BYTES from ..runtime.private_permissions import protect_private_path - request = {"schema_version": "todo_read_request_v0", **params} + native_read = None + if params.get("kind") == "select": + from .summary_item import canonical_todo_select_read + native_read = canonical_todo_select_read(params) + request = {"schema_version": "todo_read_request_v0", **(native_read or params)} encoded = json.dumps(request, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8") chunks: list[bytes] = [] identity = None @@ -277,7 +283,8 @@ def project_todo_read(params: dict[str, Any]) -> dict[str, Any]: break if not isinstance(piece, dict) or piece.get("offset") != consumed: raise ValueError("invalid Todo read result page") - current = (piece.get("sha256"), piece.get("byte_count")) + current = (piece.get("sha256"), piece.get("byte_count"), + piece.get("content_encoding"), piece.get("decoded_byte_count")) if identity is None: identity = current if identity != current or type(current[1]) is not int or current[1] <= consumed: @@ -294,7 +301,7 @@ def project_todo_read(params: dict[str, Any]) -> dict[str, Any]: complete = b"".join(chunks) if consumed != current[1] or hashlib.sha256(complete).hexdigest() != current[0]: raise ValueError("Todo read result digest mismatch") - result = json.loads(complete) + result = _decode_todo_result_bytes(complete, current[2], current[3]) break if not isinstance(after, dict) or after.get("offset") != consumed or after.get("sha256") != current[0]: raise ValueError("invalid Todo read result continuation") @@ -305,6 +312,11 @@ def project_todo_read(params: dict[str, Any]) -> dict[str, Any]: raise ValueError("invalid typed Todo read result") if result.get("rejection"): raise EffectRuntimeRejected(result["rejection"]["message"]) + if result.get("canonical") is not None and (native_read is not None or params.get("kind") == "quota_projection"): + from ..coordination.local_authority import validate_canonical_todo_read + read = native_read or next(request["summary_request"]["source_reference"]["read"] + for request in params["requests"] if "source_reference" in request["summary_request"]) + validate_canonical_todo_read(result["canonical"], goal_id=read["source"]["goal_id"]) if params.get("kind") == "group": summary = result.get("summary") if not isinstance(summary, dict) or summary.get("schema_version") != "todo_summary_v0": @@ -313,9 +325,50 @@ def project_todo_read(params: dict[str, Any]) -> dict[str, Any]: frontier = summary.get("advancement_frontier_revision_index") if not isinstance(frontier, dict) or frontier.get("schema_version") != "todo_frontier_revision_index_v0": raise ValueError("invalid typed Todo summary frontier index") + if params.get("kind") in {"canonical_fields", "query"} or native_read is not None: + _bind_todo_read_sources(result, native_read or params, native_selection=native_read is not None) return result +def _decode_todo_result_bytes(data: bytes, encoding: Any, size: Any) -> dict[str, Any]: + import json + from ..effect_runtime import MAX_LOCAL_SNAPSHOT_BYTES + + if encoding is None: + if size is not None: + raise ValueError("invalid Todo read result encoding") + elif encoding == "gzip" and type(size) is int and 0 < size <= MAX_LOCAL_SNAPSHOT_BYTES: + import zlib + try: + decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) + decoded = decoder.decompress(data, size + 1) + except zlib.error as error: + raise ValueError("invalid compressed Todo read result") from error + if len(decoded) != size or not decoder.eof or decoder.unused_data or decoder.unconsumed_tail: + raise ValueError("invalid compressed Todo read result") + data = decoded + else: + raise ValueError("invalid Todo read result encoding") + result = json.loads(data) + if not isinstance(result, dict): + raise ValueError("invalid typed Todo read result") + return result + + +def _bind_todo_read_sources(result: dict[str, Any], read: dict[str, Any], *, native_selection: bool) -> None: + from .summary_item import bind_canonical_todo_sources + + bind_canonical_todo_sources(result, read) + if native_selection and result.get("projection"): + from .succession_warning import _EvaluatedTodo + projection = result["projection"] + for field, decisions in result["field_evaluations"].items(): + summary = projection["summaries"][field] + summary["items"] = [_EvaluatedTodo(item, decision) + for item, decision in zip(summary["items"], decisions, strict=True)] + projection["todos"] = [item for summary in projection["summaries"].values() for item in summary["items"]] + + def restore_todo_field_evaluations(result: dict[str, Any]) -> dict[str, Any]: from .succession_warning import _EvaluatedTodo diff --git a/loopx/control_plane/todos/handoff_note.py b/loopx/control_plane/todos/handoff_note.py index dcf0ff4272..f8711ac97b 100644 --- a/loopx/control_plane/todos/handoff_note.py +++ b/loopx/control_plane/todos/handoff_note.py @@ -30,7 +30,12 @@ def encode(value: Any, path: list[Any]) -> Any: else: result = str(value or "") if isinstance(value, (tuple, set, float)) or not isinstance(value, (dict, list, str, bool, int, type(None))): - facts.append([path, str(value or ""), repr(value)]) + scalar_integer: str | None = None + try: + scalar_integer = str(int(cast(Any, value))) + except (TypeError, ValueError, OverflowError): + pass + facts.append([path, str(value or ""), repr(value), scalar_integer]) return result record = encode(item, []) diff --git a/loopx/control_plane/todos/quota_selection.py b/loopx/control_plane/todos/quota_selection.py index a66a40c318..1447c0a2f7 100644 --- a/loopx/control_plane/todos/quota_selection.py +++ b/loopx/control_plane/todos/quota_selection.py @@ -6,7 +6,7 @@ import json -from ..agents.profile import agent_profile_candidate_rank +from ..agents.profile import AGENT_PROFILE_ACTION_PATTERN, agent_profile_candidate_rank from ..effect_runtime import ( MAX_LOCAL_SNAPSHOT_BYTES, EffectRuntimeRejected, effect_runtime_result, ) @@ -115,10 +115,17 @@ def project_quota_planning( filter_user_gate_blocks_agent: bool, available_capabilities: Any, resolve_capacity: bool = False, current_time: str | None = None, summary_kind: str | None = None, + consume_display_views: bool = False, request_only: bool = False, ) -> dict[str, Any]: identity = agent_identity if isinstance(agent_identity, dict) else {} profile = identity.get("agent_profile") profile = profile if isinstance(profile, dict) and profile else None + native_profile = profile is None or all( + not isinstance(profile.get(key), list) or all( + isinstance(pattern, str) and AGENT_PROFILE_ACTION_PATTERN.fullmatch(pattern) + for pattern in profile[key] + ) for key in ("avoid_action_kinds", "preferred_action_kinds") + ) agent = normalize_todo_claimed_by(identity.get("agent_id")) observed_now = now_utc() if current_time is None else parse_timestamp(current_time) if observed_now is None: @@ -161,18 +168,20 @@ def integer_text(raw: Any) -> str | None: except (TypeError, ValueError): return None - resume_request = build_referenced_todo_resume_planning_request( + from .summary_item import canonical_todo_source_reference + reference = canonical_todo_source_reference(value) if summary_kind else None + resume_request = None if reference is not None or request_only else build_referenced_todo_resume_planning_request( value, agent_id=agent, item_limit=8, available_capabilities=(available_capabilities or []) if resolve_capacity else None, ) - try: - wire = effect_runtime_result("todo.quota_planning.project", { + request = { "schema_version": "todo_quota_planning_request_v4", "current_time": observed_now.isoformat(), **({"summary_request": { - "kind": summary_kind, "source": todo_read_source(value), + "kind": summary_kind, + **({"source_reference": reference} if reference is not None else {"source": todo_read_source(value)}), "integer_counts": {"source": integer_text(value.get("completed_without_successor_count")), "warning": integer_text((value.get("todo_succession_warning") or {}).get("count")) if isinstance(value.get("todo_succession_warning"), dict) else None}, @@ -180,9 +189,12 @@ def integer_text(raw: Any) -> str | None: "text": str(value.get("next") or "").strip(), **({"claimed_by": str(value.get("next_claimed_by") or "").strip()} if str(value.get("next_claimed_by") or "").strip() else {})}), - "source_rows": {key: [encode(row, include_payload=False) if isinstance(row, dict) else None - for row in value.get(key, [])] - for key in TODO_SUMMARY_SOURCE_KEYS if isinstance(value.get(key), list)}, + "materialize_source": native_profile, + **({"source_rows": {key: [encode(row, include_payload=False) if isinstance(row, dict) else None + for row in value[key]] for key in TODO_SUMMARY_SOURCE_KEYS if isinstance(value.get(key), list)}} + if not native_profile else {}), + "materialize_resume": reference is not None or request_only, + "consume_display_views": consume_display_views, }} if summary_kind else {}), "frontier_deadline": value.get("frontier_deadline"), "route_items": build_todo_route_continuation_facts(value, handoff_gates=handoff_gates), @@ -191,12 +203,14 @@ def integer_text(raw: Any) -> str | None: for gate in handoff_gates], "source_contract": _closure_source_facts(value), "resume": resume_request, + "resume_options": {"agent_id": agent, "item_limit": 8, + "available_capabilities": normalize_required_capabilities(available_capabilities) if resolve_capacity else None}, "selection": { "observed_at": observed_at, "available": normalize_required_capabilities(available_capabilities), "items": [encode(item) for item in all_open_items], - "active_items": active("active_next_action_items"), - "active_executable_items": active("active_next_action_executable_items"), + "active_items": [] if summary_kind and native_profile else active("active_next_action_items"), + "active_executable_items": [] if summary_kind and native_profile else active("active_next_action_executable_items"), "agent_id": agent, "profile": profile, "user_gate_scope": filter_user_gate_blocks_agent, "monitor_supported": todo_summary_monitor_writeback_supported(value), @@ -205,7 +219,15 @@ def integer_text(raw: Any) -> str | None: "frontier_revision_index": value.get("advancement_frontier_revision_index"), "diagnostic_limit": 3, "backlog_limit": 8, "visibility_limit": 16, }, - }, large_local_snapshot=True) + } + if request_only: + return request + try: + if reference is not None: + from .goal_todo_projection import project_todo_read + wire = project_todo_read({"kind": "quota_projection", "requests": [request]})["quota_wires"][0] + else: + wire = effect_runtime_result("todo.quota_planning.project", request, large_local_snapshot=True) except EffectRuntimeRejected as exc: raise ValueError(str(exc)) from None if summary_kind: diff --git a/loopx/control_plane/todos/quota_selection.ts b/loopx/control_plane/todos/quota_selection.ts index 2007c11642..aced61bfd1 100644 --- a/loopx/control_plane/todos/quota_selection.ts +++ b/loopx/control_plane/todos/quota_selection.ts @@ -1,6 +1,10 @@ import {COORDINATION_STATE_CONTRACT} from "../coordination/coordination_state_contract.generated.ts"; -import {decodeTodoReadSource, readAgent, pythonFieldText, writeScopes, readScope, readScopes, taskClass} from "./read_codec.ts"; -import {projectTodoSummaryDisplay} from "./context_projection.ts"; +import {decodeTodoReadSource, readAgent, readExcludedAgents, readCapabilities, readResume, + pythonFieldText, pythonFieldIsInteger, pythonFieldIsList, pythonFieldInteger, writeScopes, readScope, readScopes, taskClass} from "./read_codec.ts"; +import {readTodoPriority, todoPriorityRank} from "./priority.ts"; +import {removedTodoContinuationPolicy} from "./legacy_continuation_policy_migration.ts"; +import {stripPythonWhitespace} from "../coordination/todo_agents.ts"; +import {projectTodoSummaryDisplay, projectTodoContextPage} from "./context_projection.ts"; import {isBoundedBlockedRetry} from "../quota/settlement_phase.ts"; import {parseTodoTimestampMicros} from "../runtime_timestamp.ts"; import {countTodoWork, decodeMonitorSchedule, monitorIsDue, monitorHasScheduleGap, @@ -9,7 +13,7 @@ import type { JsonObject } from "../effect_program.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { requireJsonObject, requireBoolean, requireInteger, requireStringArray, optionalNonEmptyString } from "../runtime_decode.ts"; -import { projectTodoResumePlanning } from "./resume_planning.ts"; +import { projectTodoResumePlanning, materializeResumePlanningRequest } from "./resume_planning.ts"; import { gateAddressesAgent, actionAddressesAgent, claimAllowsAgent } from "./agent_scope.ts"; import { missingRequiredCapabilities } from "../agents/capability_gate.ts"; import {claimedAdvancementCountFromIndex} from "./frontier_revision.ts"; @@ -416,6 +420,9 @@ function quotaPlanningWire(planning: JsonObject): JsonObject { * visibility and the typed source closure rules. */ export function projectTodoQuotaPlanning(value: unknown): JsonObject { let request = requireJsonObject(value, "quota planning"); + if (request.schema_version === "todo_profile_rank_request_v0") { + return {rank: quotaProfileRank(requireJsonObject(request.facts, "profile rank facts"))}; + } if (request.summary_request !== undefined) request = prepareQuotaSummary(request); if (!["todo_quota_planning_request_v0", "todo_quota_planning_request_v1", "todo_quota_planning_request_v2", "todo_quota_planning_request_v3", "todo_quota_planning_request_v4"].includes(String(request.schema_version))) { @@ -491,8 +498,30 @@ export function projectTodoQuotaPlanning(value: unknown): JsonObject { const planning = {schema_version: "todo_quota_planning_v0", resume_planning: resume, ...selected, ...closure, ...(hasSourceContract ? {handoff_lanes: handoff, route_lanes: route} : {}), ...(now === null ? {} : frontier)}; - return request.summary_request !== undefined ? composeQuotaSummary(request, planning) : - referenced ? quotaPlanningWire(planning) : planning; + if (request.summary_request !== undefined) { + const wire = composeQuotaSummary(request, planning); + const summary = requireJsonObject(request.summary_request, "summary request"); + if (summary.consume_display_views === true) { + const display = {source: summary.source, handoff_gates: (request.handoff_items as JsonObject[]).map(row => row.display)}; + wire.display_views = projectTodoContextPage({summary_display_views: display}); + } + if (summary.consume_vision_wait_basis === true) { + if (summary.consume_display_views !== true) throw new EffectRuntimeRequestError("wait basis requires explicitly consumed terminal source rows"); + const terminal = requireJsonObject(wire.display_views, "display views").terminal as JsonObject[]; + const source = {items: terminal, deferred_items: terminal.filter(row => row.status === "deferred"), + resume_blocked_items: terminal.filter(row => row.resume_ready === false && row.status === "open"), + current_agent_blocker_items: terminal}; + wire.vision_wait_basis = { + resume_planning: projectTodoResumePlanning(materializeResumePlanningRequest(source, {agent_id: selection.agent_id, item_limit: 5})), + current_agent_blocker_items: terminal.map(row => Object.fromEntries(["todo_id", "task_class", "status", "priority", "claimed_by", "excluded_agents", "reason"].filter(key => Object.hasOwn(row, key)).map(key => [key, row[key]]))), + lineage_source_items: terminal.map(row => Object.fromEntries(["todo_id", "successor_todo_ids", "resume_condition"].filter(key => Object.hasOwn(row, key)).map(key => [key, row[key]]))), + }; + } + if (summary.consume_source_summary === true) wire.source_summary = projectTodoSummaryDisplay({source: summary.source, + handoff_gates: (request.handoff_items as JsonObject[]).map(row => row.display)}); + return wire; + } + return referenced ? quotaPlanningWire(planning) : planning; } type Path = (string | number)[]; @@ -501,6 +530,74 @@ const truth = (v: unknown): boolean => Array.isArray(v) ? !!v.length : summaryOb const get = (v: JsonObject, k: string, fallback: unknown = null): unknown => Object.hasOwn(v, k) ? v[k] : fallback; const summaryRows = (v: unknown): JsonObject[] => Array.isArray(v) ? v.filter(summaryObject) : []; +/** One advisory ranking owner, used by native materialization and the legacy codec. */ +function quotaProfileRank(facts: JsonObject): number { + const avoid = requireBoolean(facts.avoid, "avoid"), prefer = requireBoolean(facts.prefer, "prefer"); + const defaultClass = requireBoolean(facts.default_class, "default_class"); + return avoid ? 2 : prefer || defaultClass ? 0 : 1; +} + +function nativeProfileRank(item: JsonObject, profile: JsonObject | null): number { + if (profile === null) return 1; + const action = stripPythonWhitespace(pythonFieldText(item, "action_kind")).toLowerCase(); + const valid = /^[a-z][a-z0-9_-]{0,63}$/.test(action); + // Native profiles have already passed the registered profile grammar: only + // literal action characters and '*' are allowed. Legacy fnmatch stays a codec. + const matches = (value: unknown) => valid && Array.isArray(value) && value.some(pattern => { + if (typeof pattern !== "string" || !/^[a-z0-9][a-z0-9_*-]{0,79}$/.test(pattern)) { + throw new EffectRuntimeRequestError("native quota profile requires normalized action patterns"); + } + return new RegExp("^" + pattern.split("*").join(".*") + "$", "u").test(action); + }); + const explicit = stripPythonWhitespace(pythonFieldText(item, "task_class")).toLowerCase(); + return quotaProfileRank({avoid: matches(profile.avoid_action_kinds), prefer: matches(profile.preferred_action_kinds), + default_class: !!explicit && Array.isArray(profile.default_task_classes) && profile.default_task_classes.includes(explicit)}); +} + +/** Materialize facts from the complete source, preserving lane/ordinal lineage. + * This is the same quota owner, not another Todo eligibility or authority rule. */ +export function materializeQuotaRow(item: JsonObject, profile: JsonObject | null): JsonObject { + const text = (key: string) => stripPythonWhitespace(pythonFieldText(item, key)); + const classification = taskClass({...item, text: ["title", "text"].filter(key => text(key)).map(key => pythonFieldText(item, key)).join(" ")}); + const rawIndex = item.index; + const index = pythonFieldInteger(item, "index"); + const priority = todoPriorityRank(readTodoPriority(Object.hasOwn(item, "priority") ? item : + {...item, text: truth(item.text) ? pythonFieldText(item, "text") : pythonFieldText(item, "title")})); + const schema = item.schema_version, legacySchema = COORDINATION_STATE_CONTRACT.todo_read_record.item_schema_version; + const nativeSchema = COORDINATION_STATE_CONTRACT.todo_domain_record.item_schema_version; + const legacy = schema === legacySchema || schema == null && typeof item.source_section === "string" && !!item.source_section.trim(); + const unknown = schema != null && schema !== legacySchema && schema !== nativeSchema; + const address = (legacy || unknown) && pythonFieldIsInteger(item, "index") && Number(rawIndex) >= 0 ? Number(rawIndex) : null; + const status = text("status").toLowerCase(); + // The retained action-kind substring heuristic only classifies legacy gates. + // Explicit task_class always wins; this never broadens a permission boundary. + const action = text("action_kind").toLowerCase(); + const gate = text("task_class") ? classification === "user_gate" : classification === "user_gate" || + ["approval", "approve", "boundary", "gate", "blocker", "credential", "private", "production", "leaderboard", "submission", "public_claim"].some(hint => action.includes(hint)); + const instant = (key: string) => { + const value = parseTodoTimestampMicros(text(key)); + if (value === null) return null; + // Convert decimal seconds once; rounding the microsecond integer first + // loses valid distant-clock facts before the quota owner compares them. + const absolute = value < 0n ? -value : value; + return Number(`${value < 0n ? "-" : ""}${absolute / 1000000n}.${String(absolute % 1000000n).padStart(6, "0")}`); + }; + return {presentation: [priority, address ?? 999999, address === null ? pythonFieldText(item, truth(item.completed_at) ? "completed_at" : "updated_at") : "", + address === null ? pythonFieldText(item, "todo_id") : ""], + claim: readAgent(item.claimed_by), bound: readAgent(item.bound_agent), blocks: readAgent(item.blocks_agent), + excluded: readExcludedAgents(item.excluded_agents), + global: item.global_gate === true || ["1", "true", "yes", "y", "global_gate", "global-gate"].includes(text("global_gate").toLowerCase()), + gate, removed: removedTodoContinuationPolicy(text("removed_continuation_policy")) !== null || + removedTodoContinuationPolicy(text("continuation_policy")) !== null, + actionable: item.done !== true && (!status || status === "open") && + (!readResume(item.resume_when) || item.resume_ready === true) && + !(summaryObject(item.goal_acceptance_guard) && item.goal_acceptance_guard.allowed === false), + due_at: instant("next_due_at"), expires_at: instant("expires_at"), + watch_only: classification === "continuous_monitor" && (item.watch_only === true || text("watch_only").toLowerCase() === "true"), + task_class: classification, priority, index, profile_rank: nativeProfileRank(item, profile), + required: readCapabilities(item.required_capabilities), targets: readCapabilities(item.target_capabilities), raw_claimed: truth(item.claimed_by)}; +} + /** Source selection is part of the existing quota read, using the display owner. */ export function prepareQuotaSummary(request: JsonObject): JsonObject { const summary = requireJsonObject(request.summary_request, "summary request"); @@ -511,13 +608,17 @@ export function prepareQuotaSummary(request: JsonObject): JsonObject { if (summary.kind !== "user" && summary.kind !== "project_asset") { throw new EffectRuntimeRequestError("invalid quota summary kind"); } - const pool = requireJsonObject(summary.source_rows, "source rows"); + const materialize = summary.materialize_source === true; + const pool = materialize ? null : requireJsonObject(summary.source_rows, "source rows"); + const profile = summaryObject(requireJsonObject(request.selection, "selection").profile) + ? requireJsonObject(request.selection, "selection").profile as JsonObject : null; const selectedFacts: JsonObject[] = []; let selected: JsonObject[] = projectTodoSummaryDisplay({source: summary.source, handoff_gates: (request.handoff_items as JsonObject[]).map(row => row.display)}, (key, index) => { - const lane = pool[key]; - if (!Array.isArray(lane)) throw new EffectRuntimeRequestError("quota summary source lane missing"); - selectedFacts.push(requireJsonObject(lane[index], "quota source fact")); + const lane = materialize ? source[key] : pool![key]; + if (!Array.isArray(lane) || materialize && !pythonFieldIsList(source, key)) throw new EffectRuntimeRequestError("quota summary source lane missing"); + const row = requireJsonObject(lane[index], "quota source fact"); + selectedFacts.push(materialize ? materializeQuotaRow(row, profile) : row); }).map((payload, index) => { const fact = selectedFacts[index]!; const presentation = [...fact.presentation as [number, number, string, string]]; @@ -540,7 +641,12 @@ export function prepareQuotaSummary(request: JsonObject): JsonObject { return x[0] - y[0] || x[1] - y[1] || authorityUnicodeCompare(x[2], y[2]) || authorityUnicodeCompare(x[3], y[3]); }); const selection = requireJsonObject(request.selection, "selection"); - return {...request, summary_request: {...summary, legacy}, selection: {...selection, items: selected, + const encoded = (key: string) => (pythonFieldIsList(source, key) ? summaryRows(source[key]) : []) + .map(row => ({...materializeQuotaRow(row, profile), payload: row, display_source: {record: row}})); + return {...request, ...(summary.materialize_resume === true ? {resume: materializeResumePlanningRequest(source, + requireJsonObject(request.resume_options, "resume options"))} : {}), + summary_request: {...summary, legacy}, selection: {...selection, items: selected, + ...(materialize ? {active_items: encoded("active_next_action_items"), active_executable_items: encoded("active_next_action_executable_items")} : {}), source_open_count: legacy ? get(source, "open", get(source, "open_count", selected.length)) : get(source, "open_count", selected.length)}}; } diff --git a/loopx/control_plane/todos/quota_summary.py b/loopx/control_plane/todos/quota_summary.py index a94153aa3f..596732435d 100644 --- a/loopx/control_plane/todos/quota_summary.py +++ b/loopx/control_plane/todos/quota_summary.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any, Callable +from copy import deepcopy from ..goals.goal_vision_policy import COMPLETED_TODO_CHAIN_REPLAN_THRESHOLD @@ -474,14 +475,105 @@ def select_quota_todo_summary( return summary +def select_quota_todo_summaries( + user_source: Any, agent_source: Any, *, agent_identity: dict[str, Any] | None, +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """Compose the refresh caller's two native summaries in one transaction. + + Supplied and expired source references retain the standalone adapters. + The native owner still validates every source and independent request. + """ + from .summary_item import _CanonicalTodoSummary + + if isinstance(user_source, _CanonicalTodoSummary) and isinstance(agent_source, _CanonicalTodoSummary): + requests = [project_quota_planning(source, all_open_items=[], + source_open_count=source.get("open_count"), agent_identity=agent_identity, + filter_user_gate_blocks_agent=index == 0, available_capabilities=None, + summary_kind="user", request_only=True) + for index, source in enumerate((user_source, agent_source))] + if all("source_reference" in request["summary_request"] for request in requests): + from .goal_todo_projection import project_todo_read + from .quota_selection import _decode_quota_summary_wire + + result = project_todo_read({"kind": "quota_projection", "requests": requests}) + wires = result.get("quota_wires") + if not isinstance(wires, list) or len(wires) != 2: + raise RuntimeError("invalid complete quota summary response") + return (_decode_quota_summary_wire(wires[0], user_source), + _decode_quota_summary_wire(wires[1], agent_source)) + return (select_quota_todo_summary(user_source, None, agent_identity=agent_identity, + filter_user_gate_blocks_agent=True), + select_quota_todo_summary(agent_source, None, agent_identity=agent_identity)) + + +def select_quota_todo_contexts( + user_canonical: Any, user_asset: Any, agent_canonical: Any, agent_asset: Any, + *, agent_identity: dict[str, Any] | None, available_capabilities: Any, +) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, list[dict[str, Any]]], list[dict[str, Any]]]: + """The quota caller explicitly consumes both summaries and Agent displays. + + One native read transaction materializes each complete source. A supplied + compatibility source retains its full codec and cannot acquire authority. + """ + from .quota_selection import _decode_quota_summary_wire + from .goal_todo_projection import project_todo_read + + values: list[dict[str, Any]] = [] + roles: list[str] = [] + requests: list[dict[str, Any]] = [] + for role, canonical, asset in (("user", user_canonical, user_asset), ("agent", agent_canonical, agent_asset)): + selected = _select_todo_source(canonical, asset) + if selected is None: + continue + kind, source = selected + values.append(source) + roles.append(role) + requests.append(project_quota_planning(source, all_open_items=[], source_open_count=source.get("open_count"), + agent_identity=agent_identity, filter_user_gate_blocks_agent=role == "user", + available_capabilities=available_capabilities, resolve_capacity=True, + summary_kind=kind, consume_display_views=role == "agent", request_only=True)) + requests[-1]["summary_request"]["consume_source_summary"] = role == "user" + requests[-1]["summary_request"]["consume_vision_wait_basis"] = role == "agent" + summaries: dict[str, dict[str, Any]] = {} + views: dict[str, list[dict[str, Any]]] = {"summary": [], "planning": [], "terminal": []} + user_rows: list[dict[str, Any]] = [] + if requests: + result = project_todo_read({"kind": "quota_projection", "requests": requests}) + wires = result.get("quota_wires") + if not isinstance(wires, list) or len(wires) != len(requests): + raise RuntimeError("invalid complete quota projection response") + for role, source, wire in zip(roles, values, wires, strict=True): + summaries[role] = _decode_quota_summary_wire(wire, source) + if role == "user": + user_rows = wire.get("source_summary") + if not isinstance(user_rows, list) or any(not isinstance(row, dict) for row in user_rows): + raise RuntimeError("invalid User quota source display") + if role == "agent": + raw = wire.get("display_views") if isinstance(wire, dict) else None + if not isinstance(raw, dict) or set(raw) != set(views) or any( + not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows) + for rows in raw.values() + ): + raise RuntimeError("invalid explicit Agent quota display views") + views = raw + from .summary_item import _QuotaProjectedSummary + basis = wire.get("vision_wait_basis") + if not isinstance(basis, dict): + raise RuntimeError("invalid prepared quota vision wait basis") + summaries[role] = _QuotaProjectedSummary(summaries[role], basis, views["terminal"], + (agent_identity or {}).get("agent_id")) + return summaries.get("user"), summaries.get("agent"), views, user_rows + + def _select_todo_source( canonical_value: Any, project_asset_value: Any, -) -> dict[str, Any] | None: - sources = (canonical_value, project_asset_value) +) -> tuple[str, dict[str, Any]] | None: + """Keep the projection kind selected by precedence, including aliased values.""" + sources: tuple[tuple[str, Any], ...] = (("user", canonical_value), ("project_asset", project_asset_value)) if not is_canonical_attention_todo_summary(canonical_value): sources = tuple(reversed(sources)) - return next((source for source in sources if isinstance(source, dict)), None) + return next(((kind, source) for kind, source in sources if isinstance(source, dict)), None) def select_quota_todo_source_items( @@ -490,8 +582,20 @@ def select_quota_todo_source_items( ) -> list[dict[str, Any]]: """Return the authoritative pre-hot-path todo source for semantic checks.""" - source = _select_todo_source(canonical_value, project_asset_value) - return todo_summary_source_items(source) if source is not None else [] + selected = _select_todo_source(canonical_value, project_asset_value) + return todo_summary_source_items(selected[1]) if selected is not None else [] + + +def select_quota_todo_display_views( + canonical_value: Any, + project_asset_value: Any, +) -> dict[str, list[dict[str, Any]]]: + """Select the source once for the quota caller's three consumed displays.""" + from .summary_item import todo_source_display_views + + selected = _select_todo_source(canonical_value, project_asset_value) + return (todo_source_display_views(selected[1]) if selected is not None + else {"summary": [], "planning": [], "terminal": []}) def select_planning_inventory_source_items( @@ -502,8 +606,9 @@ def select_planning_inventory_source_items( ) -> list[dict[str, Any]]: """Select the canonical non-terminal rows shared by planning read models.""" - source = _select_todo_source(canonical_value, project_asset_value) - return todo_planning_source_items(source, include_terminal=include_terminal) if source is not None else [] + selected = _select_todo_source(canonical_value, project_asset_value) + return (todo_planning_source_items(selected[1], include_terminal=include_terminal) + if selected is not None else []) def _task_orchestration_authority_items( @@ -532,6 +637,7 @@ def select_task_orchestration_authority_items( project_asset_value: Any, *, role: str, + fallback_source_items: list[dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: canonical_items = _task_orchestration_authority_items( canonical_value, @@ -551,4 +657,7 @@ def select_task_orchestration_authority_items( ) if selected is not None: return selected + if fallback_source_items is not None: + # Preserve the independent mutable display returned by the old call. + return deepcopy(fallback_source_items) return select_quota_todo_source_items(canonical_value, project_asset_value) diff --git a/loopx/control_plane/todos/read_codec.ts b/loopx/control_plane/todos/read_codec.ts index 03a2e74bfb..60ba4f952a 100644 --- a/loopx/control_plane/todos/read_codec.ts +++ b/loopx/control_plane/todos/read_codec.ts @@ -95,7 +95,7 @@ export function readScopes(value: unknown): JsonObject[] { function listValues(value: unknown): unknown[] { return Array.isArray(value) ? value : text(value || "").split(/[,;|]/u).filter(Boolean); } -function readCapabilities(value: unknown): string[] { +export function readCapabilities(value: unknown): string[] { const result: string[] = []; for (const raw of listValues(value)) { try { @@ -149,6 +149,36 @@ export function decodeTodoReadSource(value: JsonObject): JsonObject { export function pythonFieldText(row: JsonObject | unknown[], key: string | number): string { return pythonValues.get(row)?.get(String(key))?.[0] as string ?? scalarText((row as JsonObject)[key]); } +/** JSON alone cannot retain Python's strict int/float presentation distinction. */ +export function pythonFieldIsInteger(row: JsonObject, key: string): boolean { + return typeof row[key] === "number" && Number.isInteger(row[key]) && + !pythonValues.get(row)?.has(key); +} + +/** Tuple/set JSON arrays retain their original Python container distinction. */ +export function pythonFieldIsList(row: JsonObject, key: string): boolean { + return Array.isArray(row[key]) && !pythonValues.get(row)?.has(key); +} + +/** Python int() spelling is scalar IO; selection and ordering stay with owners. */ +export function pythonFieldInteger(row: JsonObject, key: string, fallback = 999999): number { + const value = row[key], scalar = pythonValues.get(row)?.get(key); + if (scalar !== undefined && scalar.length > 2) return typeof scalar[2] === "string" ? Number(scalar[2]) : fallback; + if (value == null) return fallback; + if (typeof value === "boolean") return Number(value); + if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : fallback; + if (typeof value !== "string") return fallback; + // Python permits decimal Unicode digits and single separating underscores. + let digits = ""; + for (const character of stripPythonWhitespace(value)) { + if (!/\p{Nd}/u.test(character)) {digits += character; continue;} + const code = character.codePointAt(0)!; + let start = code; + while (start > 0 && /\p{Nd}/u.test(String.fromCodePoint(start - 1))) start--; + digits += String((code - start) % 10); + } + return /^[+-]?[0-9](?:_?[0-9])*$/.test(digits) ? Number(digits.replaceAll("_", "")) : fallback; +} function fieldRepr(row: JsonObject | unknown[], key: string | number): string { return pythonValues.get(row)?.get(String(key))?.[1] as string ?? repr((row as JsonObject)[key]); } diff --git a/loopx/control_plane/todos/read_projection.ts b/loopx/control_plane/todos/read_projection.ts index dda1c4bbcf..6adc35eef7 100644 --- a/loopx/control_plane/todos/read_projection.ts +++ b/loopx/control_plane/todos/read_projection.ts @@ -308,30 +308,36 @@ function emptySummary(role: string): JsonObject { export async function selectTodoReadFields(input: JsonObject): Promise { const fields = requireJsonObject(input.fields, "Todo source fields"), evaluations = requireJsonObject(input.field_evaluations, "Todo source evaluations"); if (!Array.isArray(input.roles) || input.roles.some(role => role !== "user" && role !== "agent")) throw new EffectRuntimeRequestError("Todo read roles must be user or agent"); - const summaries: JsonObject = {}, todos: unknown[] = []; + const summaries: JsonObject = {}, selectedEvaluations: JsonObject = {}, todos: unknown[] = []; let unfiltered = 0, uncapped = 0; for (const role of input.roles as string[]) { const key = `${role}_todos`, raw = object(fields[key]), items = Array.isArray(raw.items) ? raw.items : []; unfiltered += items.length; let summary = emptySummary(role); + let visibleEvaluations: unknown[] = []; if (items.length) { const projected = await projectTodoReadGroup({...input, items, lineage: [], lineage_facts: [], role, source_section: raw.source_section || summary.source_section, evaluated: true, evaluations: evaluations[key], item_limit: input.limit ?? null, full_selection: true, selection: {role, status: input.status ?? null, todo_id: input.todo_id ?? null, agent_id: input.agent_id ?? null}}); if (projected.rejection) return projected; - if ((projected.items as unknown[]).length) summary = requireJsonObject(projected.summary, "selected Todo summary"); + if ((projected.items as unknown[]).length) { + summary = requireJsonObject(projected.summary, "selected Todo summary"); + visibleEvaluations = projected.visible_evaluations as unknown[]; + } } if (raw.goal_acceptance_contract && typeof raw.goal_acceptance_contract === "object") { summary.goal_acceptance_contract = raw.goal_acceptance_contract; } if (input.limit != null) summary = compactTodoReadSummary(summary, role, Number(input.limit)); summaries[key] = summary; + selectedEvaluations[key] = visibleEvaluations.slice(0, (summary.items as unknown[]).length); todos.push(...summary.items as unknown[]); uncapped += Number(summary.total_count || 0); } return {schema_version: TODO_READ_RESULT, projection: {source: input.source_name!, summaries, todos, - unfiltered_count: unfiltered, uncapped_todo_count: uncapped}}; + unfiltered_count: unfiltered, uncapped_todo_count: uncapped}, + ...(input.include_source_evaluations === true ? {field_evaluations: selectedEvaluations} : {})}; } export async function projectSourceTodoFields(input: JsonObject): Promise { @@ -381,9 +387,13 @@ export async function projectSourceTodoFields(input: JsonObject): Promise { const input = requireJsonObject(value, "Todo read request"); - if (input.schema_version !== TODO_READ_REQUEST || !["group", "fields", "canonical_fields", "select", "source_fields", "query", "historical_fields", "completion_evidence"].includes(text(input.kind))) { + if (input.schema_version !== TODO_READ_REQUEST || !["group", "fields", "canonical_fields", "select", "source_fields", "query", "historical_fields", "completion_evidence", "quota_projection"].includes(text(input.kind))) { throw new EffectRuntimeRequestError("Todo read request schema mismatch"); } + if (input.kind === "quota_projection") { + const {readCanonicalQuotaProjection} = await import("./read_transaction.ts"); + return readCanonicalQuotaProjection(input); + } if (input.kind === "completion_evidence") { const {readCompletionEvidence} = await import("./read_transaction.ts"); return readCompletionEvidence(input); diff --git a/loopx/control_plane/todos/read_transaction.ts b/loopx/control_plane/todos/read_transaction.ts index 53e4dea6f4..9ad1ed6128 100644 --- a/loopx/control_plane/todos/read_transaction.ts +++ b/loopx/control_plane/todos/read_transaction.ts @@ -9,14 +9,20 @@ import {LOCAL_COORDINATION_TODO_SNAPSHOT_PAGE_REQUEST_SCHEMA, import {projectCompletionEvidence, projectCanonicalTodoFields, selectTodoReadFields, projectTodoReadGroup, TODO_READ_RESULT} from "./read_projection.ts"; import {id, canonicalTodoReadItems, structureCanonicalTodo} from "./read_codec.ts"; import {compactTodoReadSummary, compactThinTodoRead, compactTodoQueueSummary, todoReadProjectionContract} from "./read_presentation.ts"; -import {projectTodoContextPage} from "./context_projection.ts"; +import {projectTodoContextPage, SUMMARY_SOURCES, PLANNING_SOURCES} from "./context_projection.ts"; +import {SOURCE_KEYS as RESUME_SOURCES} from "./resume_planning.ts"; import {sealProjectionEnvelope, PROJECTION_ENVELOPE_SEAL_REQUEST} from "../projection_envelope.ts"; +import {canonicalAuthoritySha256, canonicalAuthorityJson} from "../coordination/authority_store_codec.ts"; +import {decodeTodoReadSource} from "./read_codec.ts"; +import {projectTodoQuotaPlanning} from "./quota_selection.ts"; +import type {LocalAuthorityProviderDependencies} from "../coordination/local_authority_provider.ts"; const includesArchivedRows = (query: JsonObject): boolean => query.read_scope === "completed_history" || query.include_archived === true; +const canonicalSourceRows = new Set([...SUMMARY_SOURCES, ...PLANNING_SOURCES, ...RESUME_SOURCES]); -export async function readCanonicalTodoProjection(input: JsonObject): Promise { - const canonical = await collectCanonicalTodoSource(requireJsonObject(input.source, "canonical Todo read source")); +export async function readCanonicalTodoProjection(input: JsonObject, dependencies: LocalAuthorityProviderDependencies = {}): Promise { + const canonical = await collectCanonicalTodoSource(requireJsonObject(input.source, "canonical Todo read source"), dependencies); if (canonical.status !== "loaded") return {schema_version: TODO_READ_RESULT, canonical}; const todos = canonical.todos; const guards = canonical.goal_acceptance_work_guards ?? {}; @@ -24,20 +30,149 @@ export async function readCanonicalTodoProjection(input: JsonObject): Promise [key, canonical[key]])), + field_digests: Object.fromEntries(Object.entries(requireJsonObject(projected.fields, "Todo fields")) + .map(([key, field]) => [key, canonicalAuthoritySha256(field)]))}} : {})}; return todoListPayload(input, originalRecords(canonical.todos), projected, canonical); } -async function collectCanonicalTodoSource(source: JsonObject): Promise { +function validateCanonicalSource(canonical: JsonObject, expected: JsonObject): void { + if (canonical.status !== "loaded" || Object.keys(expected).length !== 4 || + ["source_authority", "store_identity", "provider_revision", "cursor"].some(field => expected[field] !== canonical[field])) { + throw new EffectRuntimeRequestError("quota canonical source changed; restart the complete guarded read"); + } +} + +/** Full-tree transport, with provider facts immutable. Independent additions and + * the acceptance owner's observation do not acquire provider provenance. */ +function resolveCanonicalSource(fields: JsonObject, reference: JsonObject, expectedField?: string): JsonObject { + if ((reference.field !== "agent_todos" && reference.field !== "user_todos") || + expectedField !== undefined && reference.field !== expectedField) { + throw new EffectRuntimeRequestError("quota source field must name the consumed Todo role"); + } + const field = requireJsonObject(fields[reference.field], "quota source field"); + if (canonicalAuthoritySha256(field) !== reference.field_sha256) { + throw new EffectRuntimeRequestError("quota source projection no longer matches the original complete read"); + } + const record = JSON.parse(JSON.stringify(field)) as JsonObject; + if (!Array.isArray(reference.changes)) throw new EffectRuntimeRequestError("quota source changes must be an array"); + for (const raw of reference.changes) { + const change = requireJsonObject(raw, "quota source change"); + if (!Array.isArray(change.path) || !change.path.length || + change.path.some(key => typeof key !== "string" && !(typeof key === "number" && Number.isSafeInteger(key) && key >= 0)) || + (change.delete === true) === Object.hasOwn(change, "value")) { + throw new EffectRuntimeRequestError("invalid complete-source change"); + } + const path = change.path as (string | number)[]; + let parent: any = record; + for (const key of path.slice(0, -1)) { + if (parent === null || typeof parent !== "object" || !Object.hasOwn(parent, key)) { + throw new EffectRuntimeRequestError("quota source change path is absent"); + } + parent = parent[key]; + } + const last = path.at(-1)!; + if (parent === null || typeof parent !== "object" || Array.isArray(parent) && + (typeof last !== "number" || last >= parent.length || change.delete === true)) { + throw new EffectRuntimeRequestError("quota source change parent is invalid"); + } + if (change.delete === true) { + if (!Object.hasOwn(parent, last)) throw new EffectRuntimeRequestError("quota source deletion is absent"); + delete parent[last]; + } else Object.defineProperty(parent, last, {value: change.value, enumerable: true, writable: true, configurable: true}); + } + for (const [key, value] of Object.entries(field)) { + if (key !== "goal_acceptance_contract" && (!Object.hasOwn(record, key) || canonicalAuthoritySha256(value) !== canonicalAuthoritySha256(record[key]))) { + throw new EffectRuntimeRequestError("canonical Todo facts cannot be replaced by caller source differences"); + } + } + if ([...canonicalSourceRows].some(key => Object.hasOwn(record, key) && !Object.hasOwn(field, key))) { + throw new EffectRuntimeRequestError("canonical Todo facts cannot be added in caller source lanes"); + } + if (!Array.isArray(reference.python_values ?? []) || (reference.python_values as unknown[][] | undefined)?.some(fact => + !Array.isArray(fact) || !Array.isArray(fact[0]) || !fact[0].length || + Object.hasOwn(field, fact[0][0]) && fact[0][0] !== "goal_acceptance_contract")) { + throw new EffectRuntimeRequestError("canonical Todo scalar facts cannot be replaced by caller source differences"); + } + return decodeTodoReadSource({record, python_values: reference.python_values ?? []}); +} + +/** Resolve complete source differences against one freshly validated provider + * revision. Nothing is retained between RPCs; supplied evidence remains supplied. */ +export async function readCanonicalQuotaProjection(input: JsonObject, dependencies: LocalAuthorityProviderDependencies = {}): Promise { + if (!Array.isArray(input.requests) || !input.requests.length || input.requests.length > 2) { + throw new EffectRuntimeRequestError("quota projection requires one or two explicit source requests"); + } + const reads = new Map(); + const wires: JsonObject[] = []; + for (const value of input.requests) { + let request = requireJsonObject(value, "quota projection request"); + const summary = requireJsonObject(request.summary_request, "quota summary request"); + if (summary.source_reference !== undefined) { + const reference = requireJsonObject(summary.source_reference, "quota source reference"); + const read = requireJsonObject(reference.read, "quota source read"); + if (read.kind !== "canonical_fields" || read.query != null) { + throw new EffectRuntimeRequestError("quota source requires the original complete canonical read"); + } + const expected = requireJsonObject(reference.expected_source, "quota source identity"); + const key = JSON.stringify(canonicalAuthorityJson({read, expected})); + let projected = reads.get(key); + if (projected === undefined) { + projected = await readCanonicalTodoProjection(read, dependencies); + const canonical = requireJsonObject(projected.canonical, "quota canonical source"); + if (canonical.status !== "loaded" || projected.rejection) return {...projected, schema_version: TODO_READ_RESULT}; + validateCanonicalSource(canonical, expected); + reads.set(key, projected); + } + const source = {record: resolveCanonicalSource(requireJsonObject(projected.fields, "quota source fields"), reference), + python_values: reference.python_values ?? []}; + request = {...request, summary_request: {...summary, source}}; + } + wires.push(projectTodoQuotaPlanning(request)); + } + return {schema_version: TODO_READ_RESULT, quota_wires: wires}; +} + +async function collectCanonicalTodoSource(source: JsonObject, dependencies: LocalAuthorityProviderDependencies = {}): Promise { const todos: unknown[] = [], leases: unknown[] = [], guards: JsonObject = {}; let after: unknown = null, first: JsonObject | null = null; do { const page = await readCanonicalSnapshot({schema_version: LOCAL_COORDINATION_TODO_SNAPSHOT_PAGE_REQUEST_SCHEMA, runtime_root: source.runtime_root, goal_id: source.goal_id, include_leases: source.include_leases === true, - projection_readback: source.projection_readback ?? null, after}); + projection_readback: source.projection_readback ?? null, after}, dependencies); if (page.status !== "page") return { ...page, schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA}; if (first === null) first = page; @@ -174,7 +309,19 @@ async function todoListPayload(input: JsonObject, original: JsonObject[], fields coverage: {scope: "goal_todo_query", expected_count: projection.uncapped_todo_count, included_count: projection.uncapped_todo_count, shown_count: shownCount, available_count: projection.uncapped_todo_count, omitted: []}}); + const completeSource = !query.role && !query.status && !query.todo_id && !query.agent_id && query.limit == null && + !query.thin && !query.json_references && !summary && !fullText && !includeArchived; + const sourceFields = requireJsonObject(fields.fields, "canonical Todo fields"); + const sourceIdentity = Object.fromEntries(["source_authority", "store_identity", "provider_revision", "cursor"].map(key => [key, canonical[key]])); + const {query: _query, read_observed_at: _readObserved, ...producer} = input; return {schema_version: TODO_READ_RESULT, payload: result, + ...(completeSource ? {source_projection: {source: sourceIdentity, source_row_fields: [...canonicalSourceRows], + field_digests: Object.fromEntries(["user_todos", "agent_todos"].map(key => [key, canonicalAuthoritySha256(result[key])])), + replay_read: {...producer, kind: "canonical_fields", read_pipeline: [{expected_source: sourceIdentity, + sources: Object.fromEntries(["user_todos", "agent_todos"].map(field => [field, + {field, field_sha256: canonicalAuthoritySha256(sourceFields[field]), changes: [], python_values: []}])), + selector: {roles, status: null, todo_id: null, agent_id: null, limit: null, source_name: sourceName, + observed_at: input.observed_at, evaluated_at: input.evaluated_at}}]}}} : {}), ...(canonical.goal_acceptance_oracle_observation_basis ? {canonical_acceptance: { goal_acceptance_contract: canonical.goal_acceptance_contract, goal_acceptance_oracle_observation_basis: canonical.goal_acceptance_oracle_observation_basis, diff --git a/loopx/control_plane/todos/read_transport.ts b/loopx/control_plane/todos/read_transport.ts index ac101ce498..003555974b 100644 --- a/loopx/control_plane/todos/read_transport.ts +++ b/loopx/control_plane/todos/read_transport.ts @@ -1,14 +1,17 @@ /** Python transport only; native callers consume readTodo in process. */ import {createHash} from "node:crypto"; +import {gzipSync} from "node:zlib"; import {readTodo, TODO_READ_REQUEST, TODO_READ_RESULT} from "./read_projection.ts"; import type {JsonObject} from "../effect_program.ts"; -import {MAX_LOCAL_SNAPSHOT_BYTES, openPrivateSnapshotSource} from "../effect_runtime_snapshot.ts"; +import {MAX_INLINE_RESPONSE_BYTES, MAX_LOCAL_SNAPSHOT_BYTES, openPrivateSnapshotSource} from "../effect_runtime_snapshot.ts"; import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; import {BARE_SHA256_PATTERN} from "../content_digest.ts"; const completeBytes = MAX_LOCAL_SNAPSHOT_BYTES - 2 * 1024 * 1024; const pieceBytes = Math.floor(completeBytes / 4) * 3; +const inlineBytes = MAX_INLINE_RESPONSE_BYTES - 4096; +const quotaPieceBytes = Math.floor(inlineBytes / 4) * 3; async function sourceFacts(value: unknown): Promise { const ref = requireJsonObject(value, "Todo fact source reference"); @@ -48,18 +51,25 @@ export async function readTodoRuntime(value: unknown): Promise { ...(input.include_full_source === true ? {items, evaluations} : {}), ...(input.include_task_orchestration_authority === true ? {orchestration} : {})}; } - const bytes = Buffer.from(JSON.stringify(result), "utf8"); - if (page === null && bytes.length <= completeBytes) return result; + const decoded = Buffer.from(JSON.stringify(result), "utf8"); + // Keep one complete source transaction for a large quota bundle. The + // lossless adapter codec changes no rows or selection/wait semantics. + // Reads beyond the existing complete-frame budget retain their old paging. + const compressed = input.kind === "quota_projection" && decoded.length > inlineBytes && decoded.length <= completeBytes; + const bytes = compressed ? gzipSync(decoded) : decoded; + const size = compressed ? quotaPieceBytes : pieceBytes; + if (page === null && !compressed && bytes.length <= completeBytes) return result; const digest = createHash("sha256").update(bytes).digest("hex"); const offset = page === null ? 0 : page.offset; - if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 || offset >= bytes.length || offset % pieceBytes !== 0 || + if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 || offset >= bytes.length || offset % size !== 0 || (page !== null && page.sha256 !== digest)) { return {schema_version: TODO_READ_RESULT, canonical: {status: "failed", reason_code: "canonical_snapshot_changed", reason: "Todo read result changed; restart the complete read, never append a newer page", source_authority: "unknown_fail_closed", decision_read_from_provider: false, legacy_fallback_used: false}}; } - const end = Math.min(bytes.length, offset + pieceBytes); + const end = Math.min(bytes.length, offset + size); return {schema_version: TODO_READ_RESULT, result_piece: {offset, byte_count: bytes.length, sha256: digest, + ...(compressed ? {content_encoding: "gzip", decoded_byte_count: decoded.length} : {}), data: bytes.subarray(offset, end).toString("base64"), next: end === bytes.length ? null : {offset: end, sha256: digest, observed_at: observed}}}; } diff --git a/loopx/control_plane/todos/resume_planning.ts b/loopx/control_plane/todos/resume_planning.ts index 797a64a228..1432a7e321 100644 --- a/loopx/control_plane/todos/resume_planning.ts +++ b/loopx/control_plane/todos/resume_planning.ts @@ -12,6 +12,10 @@ import { } from "./resume_condition.ts"; import { automaticResumeFields, selectPendingResume } from "./automatic_resume.ts"; +import {id, object, pythonFieldText, pythonFieldInteger, pythonFieldIsList, copyTodoPythonValues, readAgent, readExcludedAgents, readResume, + readScopes, readScope, writeScopes, taskClass} from "./read_codec.ts"; +import {readTodoPriority, todoPriorityRank} from "./priority.ts"; +import {stripPythonWhitespace} from "../coordination/todo_agents.ts"; export const RESUME_PLANNING_REQUEST = "todo_resume_planning_request_v0"; export const REFERENCED_RESUME_PLANNING_REQUEST = "todo_resume_planning_request_v1"; @@ -21,12 +25,46 @@ const DEFERRED_POLICY = "quota may wake the current peer only for ready deferred "diagnostic visibility and executor-excluded todos remain visible but non-selectable"; const MONITOR_POLICY = "open advancement todos gated by todo_done: must " + "project as successor replan/state repair instead of quiet monitor wait"; -const SOURCE_KEYS = ["items", "backlog_items", "first_open_items", "deferred_items", +export const SOURCE_KEYS = ["items", "backlog_items", "first_open_items", "deferred_items", "deferred_resume_candidates", "resume_blocked_items", "monitor_open_items", "current_agent_claimed_monitor_items", "claimed_monitor_open_items"] as const; type SourceKey = typeof SOURCE_KEYS[number]; type ClaimLane = "current_agent" | "unclaimed" | "other_agent" | "executor_excluded_self"; +/** Native input materialization for this existing resume owner. The explicit + * field set is the shipped compatibility presentation, not a source filter. */ +export function materializeResumePlanningRequest(source: JsonObject, options: JsonObject): JsonObject { + const truth = (v: unknown): boolean => Array.isArray(v) ? !!v.length : v !== null && typeof v === "object" ? !!Object.keys(v).length : !!v; + const compactFields = ["schema_version", "todo_id", "role", "status", "priority", "title", "archive_state", "source_section", "task_class", "action_kind", "task_domain", "task_repository", "continuation_policy", "required_write_scopes", "required_capabilities", "target_capabilities", "decision_scope", "required_decision_scopes", "claimed_by", "blocks_agent", "excluded_agents", "unblocks_todo_id", "resume_when", "resume_monitor_generation", "resume_condition", "resume_ready", "no_followup", "successor_todo_ids", "target_key", "cadence", "next_due_at", "expires_at", "last_checked_at", "result_hash", "consecutive_no_change", "material_change", "material_change_generation", "max_no_change_before_replan", "route_continuation_replan_required", "route_continuation_reason", "route_id", "route_key", "completed_at", "updated_at", "superseded_by", "content_revision", "blocked_retry"]; + const encode = (value: unknown): JsonObject => { + const row = object(value), text = stripPythonWhitespace(pythonFieldText(row, "text")); + const payload: JsonObject = {index: row.index ?? null, text, + ...Object.fromEntries(compactFields.filter(key => row[key] != null).map(key => [key, row[key]]))}; + copyTodoPythonValues(row, payload); + for (const [key, normalized] of [["required_write_scopes", writeScopes(payload.required_write_scopes)], + ["decision_scope", readScope(payload.decision_scope)], ["required_decision_scopes", readScopes(payload.required_decision_scopes)]] as const) { + if (normalized !== null && (!Array.isArray(normalized) || normalized.length)) payload[key] = normalized; + else delete payload[key]; + } + payload.task_class = taskClass({...payload, text: ["title", "text"].map(key => pythonFieldText(payload, key)).filter(text => text.trim()).join(" ")}); + const condition = object(row.resume_condition); + const index = pythonFieldInteger(payload, "index"); + const status = (value: unknown) => typeof value === "string" && ["open", "done", "blocked", "deferred"].includes(value.trim().toLowerCase()) ? value.trim().toLowerCase() : null; + return {payload, id: id(row.todo_id), status: status(row.status), claim: readAgent(row.claimed_by), + excluded: readExcludedAgents(row.excluded_agents), resume: readResume(row.resume_when), done: row.done === true, + ready: typeof row.resume_ready === "boolean" ? row.resume_ready : null, ready_truthy: truth(row.resume_ready), + priority: todoPriorityRank(readTodoPriority(payload)), index, + target_id: id(condition.target_todo_id || condition.target), target_status: status(condition.target_status), + target_class: taskClass({task_class: condition.target_task_class})}; + }; + return {schema_version: RESUME_PLANNING_REQUEST, + sources: Object.fromEntries(SOURCE_KEYS.map(key => [key, pythonFieldIsList(source, key) ? (source[key] as unknown[]).map(encode) : []])), + agent_id: options.agent_id ?? null, item_limit: options.item_limit ?? 5, + has_deferred_count: Object.hasOwn(source, "deferred_count"), has_visible_deferred_count: truth(source.deferred_count), + deferred_count: source.deferred_count ?? null, deferred_work_counts: source.deferred_work_counts ?? null, + available_capabilities: options.available_capabilities ?? null}; +} + /** Codec facts are separate from policy. Payload fields remain lossless while * comparisons use the existing reader's normalized values and stable order. */ interface Item { diff --git a/loopx/control_plane/todos/succession_warning.py b/loopx/control_plane/todos/succession_warning.py index 2b51b19e3e..02bcef80e4 100644 --- a/loopx/control_plane/todos/succession_warning.py +++ b/loopx/control_plane/todos/succession_warning.py @@ -263,7 +263,8 @@ def evaluate_succession(items: list[dict[str, Any]], lineage: list[dict[str, Any def public_todo_summary(summary: dict[str, Any]) -> dict[str, Any]: """Drop the internal full-graph handoff once a consumer has selected rows.""" - return {**summary, "items": [ + from .summary_item import preserve_canonical_todo_source + return preserve_canonical_todo_source(summary, {**summary, "items": [ dict(item) if isinstance(item, dict) else item for item in summary.get("items") or [] - ]} + ]}) diff --git a/loopx/control_plane/todos/summary_item.py b/loopx/control_plane/todos/summary_item.py index a730f5de6a..758b68c727 100644 --- a/loopx/control_plane/todos/summary_item.py +++ b/loopx/control_plane/todos/summary_item.py @@ -7,6 +7,154 @@ from .handoff_note import todo_read_source + +class _CanonicalTodoSummary(dict[str, Any]): + """Ephemeral source lineage; dict serialization exposes only product fields.""" + + def __init__(self, value: dict[str, Any], *, read: dict[str, Any], + expected: dict[str, Any], field: str, digest: str, + basis: bytes, request_identity: object, evaluations: Any = None, + source_row_fields: tuple[str, ...] = ()) -> None: + super().__init__(value) + self.read, self.expected, self.field, self.digest = read, expected, field, digest + self.basis, self.request_identity = basis, request_identity + self.evaluations = evaluations + self.source_row_fields = source_row_fields + + def __deepcopy__(self, memo: dict[int, Any]) -> _CanonicalTodoSummary: + from copy import deepcopy + result = _CanonicalTodoSummary({}, read=deepcopy(self.read, memo), + expected=deepcopy(self.expected, memo), field=self.field, digest=self.digest, + basis=self.basis, request_identity=self.request_identity, + evaluations=deepcopy(self.evaluations, memo), source_row_fields=self.source_row_fields) + memo[id(self)] = result + result.update(deepcopy(dict(self), memo)) + return result + + +def preserve_canonical_todo_source(source: dict[str, Any], value: dict[str, Any]) -> dict[str, Any]: + if not isinstance(source, _CanonicalTodoSummary): + return value + return _CanonicalTodoSummary(value, read=source.read, expected=source.expected, + field=source.field, digest=source.digest, basis=source.basis, + request_identity=source.request_identity, evaluations=source.evaluations, + source_row_fields=source.source_row_fields) + + +def bind_canonical_todo_sources(result: dict[str, Any], read: dict[str, Any]) -> None: + from ..effect_runtime import todo_projection_request_identity + + identity = todo_projection_request_identity() + metadata = result.get("source_projection") + fields = result.get("fields", (result.get("projection") or {}).get("summaries", result.get("payload"))) + if identity is None or not isinstance(metadata, dict) or not isinstance(fields, dict): + return + for field, value in fields.items(): + digest = metadata.get("field_digests", {}).get(field) + if not isinstance(value, dict) or not isinstance(digest, str): + continue + basis = json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8", "backslashreplace") + observed = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8", "backslashreplace") + if sha256(observed).hexdigest() != digest: + # A modified response or a numeric JSON spelling mismatch cannot + # mint a native reference. Keep the complete supplied source codec. + continue + fields[field] = _CanonicalTodoSummary(value, read={key: value for key, value in metadata.get("replay_read", read).items() if key != "return_projection"}, + expected=metadata["source"], field=field, digest=digest, + basis=basis, request_identity=identity, + evaluations=(result.get("field_evaluations") or {}).get(field), + source_row_fields=tuple(metadata.get("source_row_fields", []))) + + +def canonical_todo_select_read(params: dict[str, Any]) -> dict[str, Any] | None: + """Compose the existing selector only when every consumed field is native.""" + fields, references = params.get("fields") or {}, {} + read = expected = None + for role in params.get("roles") or []: + field = f"{role}_todos" + source = fields.get(field) + if not isinstance(source, _CanonicalTodoSummary): + return None + reference = canonical_todo_source_reference(source) + if reference is None or (params.get("field_evaluations") or {}).get(field) != source.evaluations: + return None + if read is not None and (read != reference["read"] or expected != reference["expected_source"]): + return None + read, expected = reference["read"], reference["expected_source"] + references[field] = {key: value for key, value in reference.items() if key not in {"read", "expected_source"}} + if read is None: + return None + selector = {key: value for key, value in params.items() if key not in {"kind", "fields", "field_evaluations"}} + return {**read, "return_projection": True, + "read_pipeline": [*(read.get("read_pipeline") or []), + {"expected_source": expected, "sources": references, "selector": selector}]} + + +def canonical_todo_source_reference(value: dict[str, Any]) -> dict[str, Any] | None: + from ..effect_runtime import todo_projection_request_identity + + if (not isinstance(value, _CanonicalTodoSummary) + or value.request_identity is not todo_projection_request_identity()): + return None + current = todo_read_source(value) + original = json.loads(value.basis) + changes: list[dict[str, Any]] = [] + + def diff(before: Any, after: Any, path: list[Any]) -> None: + if isinstance(before, dict) and isinstance(after, dict): + for key in before.keys() - after.keys(): + changes.append({"path": [*path, key], "delete": True}) + for key, child in after.items(): + if key not in before: + changes.append({"path": [*path, key], "value": child}) + else: + diff(before[key], child, [*path, key]) + elif isinstance(before, list) and isinstance(after, list) and len(before) == len(after): + for index, (left, right) in enumerate(zip(before, after, strict=True)): + diff(left, right, [*path, index]) + elif type(before) is not type(after) or before != after: + changes.append({"path": path, "value": after}) + + diff(original, current["record"], []) + if (any(change["path"][0] in value.source_row_fields or + change["path"][0] in original and change["path"][0] != "goal_acceptance_contract" + for change in changes) or any(fact[0] and fact[0][0] in original + and fact[0][0] != "goal_acceptance_contract" for fact in current.get("python_values", []))): + # Transformed Todo facts remain complete supplied source, rather than + # retaining provider provenance. The native owner declares row lanes. + return None + return {"read": value.read, "expected_source": value.expected, + "field": value.field, "field_sha256": value.digest, + "changes": changes, "python_values": current.get("python_values", [])} + + +class _QuotaProjectedSummary(dict[str, Any]): + """Prepared wait facts over an explicitly consumed immutable source view.""" + + def __init__(self, value: dict[str, Any], basis: dict[str, Any], rows: list[dict[str, Any]], agent: Any): + from ..effect_runtime import todo_projection_request_identity + super().__init__(value) + self.wait_basis, self.agent = basis, agent + self.request_identity = todo_projection_request_identity() + self.rows = json.dumps(rows, ensure_ascii=False, separators=(",", ":")).encode("utf-8", "backslashreplace") + + def __deepcopy__(self, memo: dict[int, Any]) -> _QuotaProjectedSummary: + from copy import deepcopy + result = _QuotaProjectedSummary({}, deepcopy(self.wait_basis, memo), [], self.agent) + memo[id(self)] = result + result.update(deepcopy(dict(self), memo)) + result.rows, result.request_identity = self.rows, self.request_identity + return result + + +def quota_vision_wait_basis(summary: dict[str, Any], rows: Any, agent: Any) -> dict[str, Any] | None: + from ..effect_runtime import todo_projection_request_identity + if (not isinstance(summary, _QuotaProjectedSummary) or summary.request_identity is None + or summary.request_identity is not todo_projection_request_identity() or summary.agent != agent): + return None + observed = json.dumps(rows, ensure_ascii=False, separators=(",", ":")).encode("utf-8", "backslashreplace") + return summary.wait_basis if observed == summary.rows else None + TODO_SUMMARY_SOURCE_KEYS = ( "active_next_action_items", "active_next_action_executable_items", @@ -98,3 +246,30 @@ def todo_planning_source_items(value: dict[str, Any], *, include_terminal: bool if not any(value.get(key) for key in TODO_PLANNING_SOURCE_KEYS): return [] return _project({"source": todo_read_source(value), "include_terminal": include_terminal}) + + +def todo_source_display_views(value: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: + """Compose displays only for a caller consuming all three existing views.""" + from ..effect_runtime import MAX_REQUEST_BYTES, EffectRuntimeRejected, effect_runtime_result + from .handoff_gate import todo_summary_handoff_gates + + has_summary = any(value.get(key) for key in TODO_SUMMARY_SOURCE_KEYS) + has_planning = any(value.get(key) for key in TODO_PLANNING_SOURCE_KEYS) + if not has_summary and not has_planning: + return {"summary": [], "planning": [], "terminal": []} + request = {"summary_display_views": { + "source": todo_read_source(value), "handoff_gates": todo_summary_handoff_gates(value), + }} + # This bounds the opt-in batch input; it does not predict response size or + # promise inline transport. Large sources retain the existing view limits. + if len(json.dumps(request, ensure_ascii=False, separators=(",", ":")).encode("utf-8", errors="backslashreplace")) > MAX_REQUEST_BYTES // 4: + return {"summary": todo_summary_source_items(value), + "planning": todo_planning_source_items(value), + "terminal": todo_planning_source_items(value, include_terminal=True)} + try: + result = effect_runtime_result("todo.context.page", request, large_local_snapshot=True) + except EffectRuntimeRejected as error: + raise ValueError(str(error)) from error + return {"summary": result["summary"] if has_summary else [], + "planning": result["planning"] if has_planning else [], + "terminal": result["terminal"] if has_planning else []} diff --git a/loopx/control_plane/work_items/semantic_replan_writeback.py b/loopx/control_plane/work_items/semantic_replan_writeback.py index 3ccaa710f2..3a6bd8e5e1 100644 --- a/loopx/control_plane/work_items/semantic_replan_writeback.py +++ b/loopx/control_plane/work_items/semantic_replan_writeback.py @@ -21,7 +21,7 @@ from ..todos.active_state_todo_parser import parse_active_state_todos from ..todos.quota_summary import ( select_planning_inventory_source_items, - select_quota_todo_summary, + select_quota_todo_summaries, ) from ..todos.succession_warning import todo_succession_gap_items from .autonomous_replan_ack import ( @@ -262,16 +262,8 @@ def qualify_replan_writeback( ) raw_user_todos = todo_projection.get("user_todos") raw_agent_todos = todo_projection.get("agent_todos") - user_todos = select_quota_todo_summary( - raw_user_todos, - None, - agent_identity=agent_identity, - filter_user_gate_blocks_agent=True, - ) - agent_todos = select_quota_todo_summary( - raw_agent_todos, - None, - agent_identity=agent_identity, + user_todos, agent_todos = select_quota_todo_summaries( + raw_user_todos, raw_agent_todos, agent_identity=agent_identity, ) agent_todo_source_items = select_planning_inventory_source_items( raw_agent_todos, diff --git a/tests/cli_commands/test_quota_stall_context_reprojection.py b/tests/cli_commands/test_quota_stall_context_reprojection.py index 7803ff6b78..1eb2e2d9ae 100644 --- a/tests/cli_commands/test_quota_stall_context_reprojection.py +++ b/tests/cli_commands/test_quota_stall_context_reprojection.py @@ -89,6 +89,9 @@ def produce(): args = build_parser().parse_args([ "--format", "json", "quota", "should-run", "--goal-id", goal, "--turn-instance-id", turn, + # This case asserts the inline CLI body. Progressive delivery returns + # navigation instead and is covered by real captured-reader journeys. + "--context-delivery", "off", "--delta-delivery", "off", ]) capture = tmp_path / "capture" capture.mkdir() diff --git a/tests/control_plane/test_canonical_quota_projection.py b/tests/control_plane/test_canonical_quota_projection.py new file mode 100644 index 0000000000..ba363973b0 --- /dev/null +++ b/tests/control_plane/test_canonical_quota_projection.py @@ -0,0 +1,239 @@ +"""Canonical provider-to-quota reads retain complete input and request fences.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.effect_runtime import effect_runtime_request_scope +from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted +from loopx.control_plane.todos.goal_todo_projection import project_todo_read, todo_summaries_from_fields +from loopx.control_plane.todos.quota_selection import project_quota_planning +from loopx.control_plane.todos.quota_summary import select_quota_todo_contexts, select_quota_todo_summaries, select_quota_todo_summary +from loopx.control_plane.todos.summary_item import canonical_todo_source_reference +from loopx.control_plane.todos.list_readback import list_goal_todos +from tests.control_plane import test_quota_settlement_cli as fixture + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_provider_projection_uses_complete_source_differences_and_scope_exit(tmp_path, provider): + project, runtime, registry = fixture._write_fixture(tmp_path, claimed=False) + fixture._initialize_fixture_authority(project, runtime, provider=provider) + with effect_runtime_request_scope(): + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, + goal_id=fixture.GOAL_ID, include_summary=True) + fields = canonical["_todo_fields"] + source = fields["agent_todos"] + reference = canonical_todo_source_reference(source) + assert reference is not None, "the real native read must supply a validated reference" + assert reference["changes"] == [] + copied = deepcopy(source) + proof = {"agent_id": fixture.AGENT_ID, "state": "waiting", "evidence": {"revision": 9007199254740993, "ratio": -0.0}} + copied["vision_wait_states"] = [proof] + request = project_quota_planning(copied, all_open_items=[], source_open_count=1, + agent_identity={"agent_id": fixture.AGENT_ID}, filter_user_gate_blocks_agent=False, + available_capabilities=[], resolve_capacity=True, summary_kind="user", request_only=True) + assert "source" not in request["summary_request"] + assert "source_rows" not in request["summary_request"] + assert request["resume"] is None + assert request["summary_request"]["source_reference"]["changes"] + user, agent, views, user_rows = select_quota_todo_contexts(fields["user_todos"], None, copied, None, + agent_identity={"agent_id": fixture.AGENT_ID}, available_capabilities=[]) + assert user is not None and agent is not None + assert views["terminal"][0]["text"] == copied["items"][0]["text"] + assert agent["vision_wait_states"][0] is proof + assert user_rows == [] + assert canonical_todo_source_reference(deepcopy(copied)) is not None + invalid = deepcopy(request) + invalid["summary_request"]["source_reference"]["expected_source"]["cursor"] = "foreign" + with pytest.raises(ValueError, match="canonical source changed"): + project_todo_read({"kind": "quota_projection", "requests": [invalid]}) + invalid = deepcopy(request) + invalid["summary_request"]["source_reference"]["field_sha256"] = "0" * 64 + with pytest.raises(ValueError, match="original complete read"): + project_todo_read({"kind": "quota_projection", "requests": [invalid]}) + invalid = deepcopy(request) + invalid["summary_request"]["source_reference"]["changes"].append( + {"path": ["items", 0, "required_write_scopes"], "value": ["foreign.json"]}) + with pytest.raises(ValueError, match="canonical Todo facts"): + project_todo_read({"kind": "quota_projection", "requests": [invalid]}) + invalid = deepcopy(request) + invalid["summary_request"]["source_reference"]["python_values"] = [ + [["items", 0, "claimed_by"], "forged-agent", "'forged-agent'", None]] + with pytest.raises(ValueError, match="canonical Todo scalar facts"): + project_todo_read({"kind": "quota_projection", "requests": [invalid]}) + transformed = deepcopy(source) + transformed["items"][0]["note"] = "Explicit caller transformation" + assert canonical_todo_source_reference(transformed) is None + transformed = deepcopy(source) + transformed["items"][0]["done"] = 0 + assert canonical_todo_source_reference(transformed) is None + transformed = deepcopy(source) + transformed["items"] = tuple(transformed["items"]) + assert canonical_todo_source_reference(transformed) is None + selected = todo_summaries_from_fields(fields=fields, source=canonical["source_authority"], + rollout_events=[], roles=["user", "agent"], status=None, todo_id=None, + agent_id=fixture.AGENT_ID, limit=None) + for role in ["user", "agent"]: + selected_ref = canonical_todo_source_reference(selected.summaries[f"{role}_todos"]) + assert selected_ref is not None, "the production selector must retain native lineage" + assert len(selected_ref["read"]["read_pipeline"]) == 1 + repeated = todo_summaries_from_fields(fields=selected.summaries, source=selected.source, + rollout_events=[], roles=["user", "agent"], status=None, todo_id=None, + agent_id=fixture.AGENT_ID, limit=None) + assert len(canonical_todo_source_reference(repeated.summaries["agent_todos"])["read"]["read_pipeline"]) == 2 + _, selected_agent, _, _ = select_quota_todo_contexts(None, None, selected.summaries["agent_todos"], None, + agent_identity={"agent_id": fixture.AGENT_ID}, available_capabilities=[]) + assert selected_agent is not None + fresh = list_goal_todos(registry_path=registry, runtime_root_arg=str(runtime), goal_id=fixture.GOAL_ID) + for role in ["user", "agent"]: + assert canonical_todo_source_reference(fresh[f"{role}_todos"]) is not None + user_request = project_quota_planning(fresh["user_todos"], all_open_items=[], source_open_count=0, + agent_identity={"agent_id": fixture.AGENT_ID}, filter_user_gate_blocks_agent=True, + available_capabilities=[], summary_kind="user", request_only=True) + invalid = deepcopy(user_request) + added = [{"todo_id": "todo_supplied_intent", "role": "user", "status": "open", "done": False, + "text": "Independently supplied action", "task_class": "user_action"}] + invalid["summary_request"]["source_reference"]["changes"] = [ + {"path": ["active_next_action_items"], "value": added}] + with pytest.raises(ValueError, match="canonical Todo facts"): + project_todo_read({"kind": "quota_projection", "requests": [invalid]}) + transformed = deepcopy(fresh["user_todos"]) + transformed["active_next_action_items"] = added + assert canonical_todo_source_reference(transformed) is None + _, fresh_agent, _, _ = select_quota_todo_contexts(fresh["user_todos"], None, fresh["agent_todos"], None, + agent_identity={"agent_id": fixture.AGENT_ID}, available_capabilities=[]) + assert fresh_agent is not None + assert canonical_todo_source_reference(source) is None + assert canonical_todo_source_reference(copied) is None + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=fixture.GOAL_ID) + assert after["provider_revision"] == canonical["provider_revision"] + assert after["todos"] == canonical["todos"], "source differences grant no provider write" + + +def test_supplied_tuple_source_retains_the_legacy_list_requirement(): + source = {"schema_version": "todo_summary_v0", "items": ( + {"todo_id": "todo_container", "text": "Required action", "status": "open", "done": False, + "task_class": "advancement_task"},), "open_count": 1} + with pytest.raises(ValueError, match="quota summary source lane missing"): + project_quota_planning(source, all_open_items=[], source_open_count=1, + agent_identity={"agent_id": "agent-a"}, filter_user_gate_blocks_agent=False, + available_capabilities=[], summary_kind="user") + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_refresh_summary_pair_preserves_independent_inputs_and_freshness(tmp_path, monkeypatch, provider): + from loopx.control_plane import effect_runtime as runtime_owner + + project, runtime, _ = fixture._write_fixture(tmp_path, claimed=False) + fixture._initialize_fixture_authority(project, runtime, provider=provider) + identity = {"agent_id": fixture.AGENT_ID} + requests = [] + dispatch = runtime_owner.effect_runtime_request + def observed(method, params, **kwargs): + if method == "todo.read" and params.get("kind") == "quota_projection": + requests.append(params) + return dispatch(method, params, **kwargs) + monkeypatch.setattr(runtime_owner, "effect_runtime_request", observed) + with effect_runtime_request_scope(): + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, + goal_id=fixture.GOAL_ID, include_summary=True) + fields = canonical["_todo_fields"] + user, agent = deepcopy(fields["user_todos"]), deepcopy(fields["agent_todos"]) + proof = {"agent_id": fixture.AGENT_ID, "state": "waiting", "evidence": {"revision": 9007199254740993}} + agent["vision_wait_states"] = [proof] + expected = (select_quota_todo_summary(user, None, agent_identity=identity, filter_user_gate_blocks_agent=True), + select_quota_todo_summary(agent, None, agent_identity=identity)) + assert len(requests) == 2 + requests.clear() + actual = select_quota_todo_summaries(user, agent, agent_identity=identity) + assert actual == expected + assert actual[1]["vision_wait_states"][0] is proof + assert len(requests) == 1 and len(requests[0]["requests"]) == 2 + assert all("source_reference" in row["summary_request"] for row in requests[0]["requests"]) + assert all(not row["summary_request"]["consume_display_views"] for row in requests[0]["requests"]) + stale = deepcopy(agent) + stale.expected = {**stale.expected, "cursor": "foreign"} + with pytest.raises(ValueError, match="canonical source changed"): + select_quota_todo_summaries(user, stale, agent_identity=identity) + directory = runtime / "authority" / f"{provider}-v0" + retained = directory.with_name(directory.name + "-retained") + directory.rename(retained) + try: + with pytest.raises(LocalCoordinationAuthorityUnavailable) as original: + read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=fixture.GOAL_ID) + with pytest.raises(LocalCoordinationAuthorityUnavailable) as paired: + select_quota_todo_summaries(user, agent, agent_identity=identity) + assert paired.value.code == original.value.code + assert paired.value.payload == original.value.payload + finally: + retained.rename(directory) + expected = (select_quota_todo_summary(user, None, agent_identity=identity, filter_user_gate_blocks_agent=True), + select_quota_todo_summary(agent, None, agent_identity=identity)) + assert select_quota_todo_summaries(user, agent, agent_identity=identity) == expected + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_quota_source_outage_preserves_the_canonical_read_diagnostics(tmp_path, provider): + project, runtime, _ = fixture._write_fixture(tmp_path, claimed=False) + fixture._initialize_fixture_authority(project, runtime, provider=provider) + with effect_runtime_request_scope(): + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, + goal_id=fixture.GOAL_ID, include_summary=True) + request = project_quota_planning(canonical["_todo_fields"]["agent_todos"], + all_open_items=[], source_open_count=1, agent_identity={"agent_id": fixture.AGENT_ID}, + filter_user_gate_blocks_agent=False, available_capabilities=[], + summary_kind="user", request_only=True) + directory = runtime / "authority" / f"{provider}-v0" + retained = directory.with_name(directory.name + "-retained") + directory.rename(retained) + try: + with pytest.raises(LocalCoordinationAuthorityUnavailable) as original: + read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=fixture.GOAL_ID) + with pytest.raises(LocalCoordinationAuthorityUnavailable) as bundled: + project_todo_read({"kind": "quota_projection", "requests": [request]}) + assert bundled.value.code == original.value.code + assert bundled.value.payload == original.value.payload + finally: + retained.rename(directory) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_large_native_quota_reply_preserves_complete_views_without_snapshot_write(tmp_path, monkeypatch, provider): + from loopx.control_plane import effect_runtime as runtime_owner + from loopx.control_plane.todos.summary_item import todo_planning_source_items, todo_summary_source_items + + project, runtime, _ = fixture._write_fixture(tmp_path, claimed=False) + state = project / f".codex/goals/{fixture.GOAL_ID}/ACTIVE_GOAL_STATE.md" + with state.open("a") as file: + for index in range(7999): + file.write(f"\n- [ ] [P1] Build synthetic work {index}\n" + f" \n") + fixture._initialize_fixture_authority(project, runtime, provider=provider) + snapshots, requests = [], [] + read = runtime_owner._read_local_snapshot_response + dispatch = runtime_owner.effect_runtime_request + def observed(envelope, sink, **kwargs): + snapshots.append(kwargs["method"]) + return read(envelope, sink, **kwargs) + def requested(method, params, **kwargs): + if method == "todo.read" and params.get("kind") == "quota_projection": + requests.append(params) + return dispatch(method, params, **kwargs) + monkeypatch.setattr(runtime_owner, "_read_local_snapshot_response", observed) + monkeypatch.setattr(runtime_owner, "effect_runtime_request", requested) + with effect_runtime_request_scope(): + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=fixture.GOAL_ID, include_summary=True) + fields = before["_todo_fields"] + source = fields["agent_todos"] + expected = {"summary": todo_summary_source_items(source), "planning": todo_planning_source_items(source), + "terminal": todo_planning_source_items(source, include_terminal=True)} + snapshots.clear() + _, agent, views, _ = select_quota_todo_contexts(fields["user_todos"], None, source, None, + agent_identity={"agent_id": fixture.AGENT_ID}, available_capabilities=["shell", "filesystem_write", "network"]) + assert agent is not None and all(len(rows) == 8000 for rows in views.values()) + assert views == expected + assert len(requests) == 1 + assert all("source_reference" in item["summary_request"] for item in requests[0]["requests"]) + assert "todo.read" not in snapshots + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=fixture.GOAL_ID) + assert after["provider_revision"] == before["provider_revision"] + assert after["todos"] == before["todos"] diff --git a/tests/control_plane/test_dependency_vision_wait_cli.py b/tests/control_plane/test_dependency_vision_wait_cli.py index da56e35e90..d0ae64aa2d 100644 --- a/tests/control_plane/test_dependency_vision_wait_cli.py +++ b/tests/control_plane/test_dependency_vision_wait_cli.py @@ -14,6 +14,7 @@ from loopx.cli import main from test_dated_vision_wait_cli import REPO, isolated_effect # noqa: F401 +from tests.control_plane.codex_checkpoint_fixture import read_cli_policy_payload GOAL = "dependency-vision-fixture" AGENT = "agent-waiter" @@ -36,7 +37,7 @@ def call(*args, expected_exit=0): with redirect_stdout(output): result = main(["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *args]) - payload = json.loads(output.getvalue()) + payload = read_cli_policy_payload(output.getvalue(), returncode=result) assert result == expected_exit, payload return payload diff --git a/tests/control_plane/test_quota_legacy_asset_aliasing.py b/tests/control_plane/test_quota_legacy_asset_aliasing.py new file mode 100644 index 0000000000..64e57e8ae2 --- /dev/null +++ b/tests/control_plane/test_quota_legacy_asset_aliasing.py @@ -0,0 +1,81 @@ +"""Legacy source format and quota decisions cannot depend on object aliasing.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.testing.quota_fixtures import quota_status_payload +from loopx.control_plane.todos import quota_summary +from loopx.quota import build_quota_should_run + + +def legacy_asset(*, next_only=False): + counts = {"total": "3", "open": "1", "done": "2"} + if next_only: + return {**counts, "next": "Verify the synthetic result.", "next_index": "7"} + return {**counts, "items": [{ + "todo_id": "todo_legacy_asset", "index": 7, "text": "Verify the synthetic result.", + "status": "open", "priority": "P1", "task_class": "advancement_task", + }]} + + +@pytest.mark.parametrize("role", ["user", "agent"]) +@pytest.mark.parametrize("shared", [False, True]) +@pytest.mark.parametrize("next_only", [False, True]) +def test_composed_legacy_source_matches_independent_summary(role, shared, next_only): + asset = legacy_asset(next_only=next_only) + canonical = asset if shared else deepcopy(asset) + before = deepcopy(asset) + expected = quota_summary.select_quota_todo_summary( + canonical, asset, filter_user_gate_blocks_agent=role == "user", + ) + user, agent, _, _ = quota_summary.select_quota_todo_contexts( + canonical if role == "user" else None, asset if role == "user" else None, + canonical if role == "agent" else None, asset if role == "agent" else None, + agent_identity=None, available_capabilities=None, + ) + assert expected["total_count"] == "3" and expected["done_count"] == "2" + assert (user if role == "user" else agent) == expected + assert canonical == before and asset == before + + +def test_declared_canonical_source_keeps_precedence_over_legacy_asset(): + asset = legacy_asset() + canonical = {**deepcopy(asset), "schema_version": "todo_summary_v0", + "source_section": "Agent Todo", "total_count": 9, "open_count": 1, "done_count": 8} + expected = quota_summary.select_quota_todo_summary(canonical, asset) + _, actual, _, _ = quota_summary.select_quota_todo_contexts( + None, None, canonical, asset, agent_identity=None, available_capabilities=None, + ) + assert actual == expected + assert actual["total_count"] == 9 and actual["done_count"] == 8 + + +@pytest.mark.parametrize("shared", [False, True]) +def test_real_guard_preserves_legacy_counts_and_selected_work(shared, tmp_path): + asset = legacy_asset() + status = quota_status_payload(goal_id="legacy-asset-fixture", status="active", + recommended_action="Verify the synthetic result.", agent_todos=asset, + coordination={"registered_agents": ["agent-a"]}) + item = status["attention_queue"]["items"][0] + item["agent_todos"] = asset if shared else deepcopy(asset) + item["project_asset"]["agent_todos"] = asset + result = build_quota_should_run(status, goal_id="legacy-asset-fixture", agent_id="agent-a", + turn_instance_id="legacy-asset-turn", runtime_root=tmp_path) + assert result["normal_delivery_allowed"] is True + assert result["selected_todo"]["todo_id"] == "todo_legacy_asset" + assert result["agent_todo_summary"]["total_count"] == "3" + assert result["agent_todo_summary"]["done_count"] == "2" + + +@pytest.mark.parametrize("shared", [False, True]) +def test_real_guard_refuses_legacy_next_without_a_todo_identity(shared, tmp_path): + asset = legacy_asset(next_only=True) + status = quota_status_payload(goal_id="legacy-asset-fixture", status="active", + recommended_action="Verify the synthetic result.", agent_todos=asset, + coordination={"registered_agents": ["agent-a"]}) + item = status["attention_queue"]["items"][0] + item["agent_todos"] = asset if shared else deepcopy(asset) + item["project_asset"]["agent_todos"] = asset + with pytest.raises(ValueError, match="todo_id"): + build_quota_should_run(status, goal_id="legacy-asset-fixture", agent_id="agent-a", + turn_instance_id="legacy-asset-turn", runtime_root=tmp_path) diff --git a/tests/control_plane/test_runtime_source_read_batching.py b/tests/control_plane/test_runtime_source_read_batching.py index 434e58bb22..0f1f6a76ce 100644 --- a/tests/control_plane/test_runtime_source_read_batching.py +++ b/tests/control_plane/test_runtime_source_read_batching.py @@ -11,6 +11,169 @@ from loopx.control_plane import effect_runtime as runtime +def test_todo_source_views_share_only_the_exact_request_source(monkeypatch): + from loopx.control_plane.todos.summary_item import ( + todo_source_display_views, + ) + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method in {"todo.context.page", "todo.read"}: + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Keep the full acceptance", + "role": "agent", "status": "done", "done": True, + "note": "Original recovery detail", "successor_todo_ids": ["todo_next"]}]} + with runtime.effect_runtime_request_scope(): + views = todo_source_display_views(source) + assert views["summary"] == [] + assert views["planning"] == [] + terminal = views["terminal"] + assert len(terminal) == 1 + assert terminal[0]["note"] == "Original recovery detail" + assert terminal[0]["successor_todo_ids"] == ["todo_next"] + assert len(requests) == 1 + terminal[0]["successor_todo_ids"].append("todo_unrelated") + assert todo_source_display_views(source)["terminal"][0]["successor_todo_ids"] == ["todo_next"] + # The same Todo id cannot alias changed full-source facts. + source["items"][0]["note"] = "Changed recovery detail" + assert todo_source_display_views(source)["terminal"][0]["note"] == "Changed recovery detail" + assert len(requests) == 2 + # A later logical request starts with a new source read and result. + with runtime.effect_runtime_request_scope(): + assert todo_source_display_views(source)["terminal"][0]["note"] == "Changed recovery detail" + assert len(requests) == 3 + + +def test_large_or_unscoped_todo_sources_keep_single_view_transport(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_planning_source_items + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Keep acceptance", "status": "done", + "note": "Recovery detail " * (runtime.MAX_REQUEST_BYTES // 32)}]} + with runtime.effect_runtime_request_scope(): + assert todo_planning_source_items(source, include_terminal=True)[0]["note"] == source["items"][0]["note"] + assert set(requests[0]) == {"summary_display"} + source["items"][0]["note"] = "Small source outside the shared request" + assert todo_planning_source_items(source, include_terminal=True)[0]["note"] == source["items"][0]["note"] + assert set(requests[1]) == {"summary_display"} + + +def test_explicit_large_source_falls_back_to_existing_single_view_limits(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_source_display_views + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Completed work", "status": "done", "done": True, + "note": "Complete recovery detail " * (runtime.MAX_REQUEST_BYTES // 64)}]} + with runtime.effect_runtime_request_scope(): + views = todo_source_display_views(source) + assert views["summary"] == views["planning"] == [] + assert views["terminal"][0]["note"] == source["items"][0]["note"] + assert len(requests) == 3 + assert all(set(request) == {"summary_display"} for request in requests) + + +def test_incomplete_source_views_fail_before_entering_the_request_memo(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_source_display_views + + calls = [] + original = runtime.effect_runtime_request + + def response(method, params, **kwargs): + if method != "todo.context.page": + return original(method, params, **kwargs) + calls.append(params) + return {"result": {"summary": [], "planning": [], + "terminal": None if len(calls) == 1 else []}} + + monkeypatch.setattr(runtime, "effect_runtime_request", response) + source = {"items": [{"todo_id": "todo_retained", "text": "Acceptance", "status": "done"}]} + with runtime.effect_runtime_request_scope(): + with pytest.raises(RuntimeError, match="invalid TypeScript Todo source views"): + todo_source_display_views(source) + assert todo_source_display_views(source)["summary"] == [] + assert len(calls) == 2 + + +def test_single_view_delivery_does_not_compute_or_snapshot_unused_views(monkeypatch): + from loopx.control_plane.work_items.delivery_history import project_delivery_response + + requests, snapshots = [], [] + original = runtime.effect_runtime_request + read_snapshot = runtime._read_local_snapshot_response + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + def snapshot(*args, **kwargs): + snapshots.append(args) + return read_snapshot(*args, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + monkeypatch.setattr(runtime, "_read_local_snapshot_response", snapshot) + source = {"items": [{"todo_id": f"todo_{i}", "text": "Work", "role": "agent", + "status": "open", "successor_todo_ids": ["todo_next"]} + for i in range(8000)]} + with runtime.effect_runtime_request_scope(): + result = project_delivery_response({"todo_id": "todo_0", "agent_id": "agent-a"}, source) + assert result["schema_version"] == "delivery_response_v0" + assert len(requests) == 1 + assert set(requests[0]) == {"summary_display"} + assert requests[0]["summary_display"]["include_terminal"] is True + assert snapshots == [], "a sub-limit single display must not materialize unused views" + + +def test_quota_caller_explicitly_composes_only_the_agent_source(monkeypatch): + from loopx.control_plane.testing.quota_fixtures import quota_status_payload, quota_todo_item, quota_todo_summary + from loopx.quota import build_quota_should_run + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method in {"todo.context.page", "todo.read"}: + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + agent = quota_todo_summary([quota_todo_item(todo_id="todo_agent", title="Agent work", claimed_by="agent-a")]) + user = quota_todo_summary([quota_todo_item(todo_id="todo_user", title="User action", role="user", blocks_agent="agent-b")]) + status = quota_status_payload(goal_id="goal-a", status="active_state_agent_todo", + recommended_action="Agent work", agent_todos=agent, user_todos=user, + coordination={"registered_agents": ["agent-a", "agent-b"]}) + with runtime.effect_runtime_request_scope(): + result = build_quota_should_run(status, goal_id="goal-a", agent_id="agent-a", available_capabilities=["shell"]) + transactions = [p for p in requests if p.get("kind") == "quota_projection"] + assert len(transactions) == 1 + sources = [r["summary_request"] for r in transactions[0]["requests"]] + assert [r["consume_display_views"] for r in sources] == [False, True] + assert sources[1]["source"]["record"]["first_open_items"][0]["todo_id"] == "todo_agent" + assert sources[0]["source"]["record"]["first_open_items"][0]["todo_id"] == "todo_user" + assert result["agent_identity"]["agent_id"] == "agent-a" + + def serial_fingerprint(root: Path) -> str: """Independent reference: selected adapter, release identity and source bytes.""" digest = hashlib.sha256() diff --git a/tests/control_plane/test_todo_read_response_transport.py b/tests/control_plane/test_todo_read_response_transport.py new file mode 100644 index 0000000000..2067f1c741 --- /dev/null +++ b/tests/control_plane/test_todo_read_response_transport.py @@ -0,0 +1,120 @@ +"""Complete Todo replies retain bytes and fail closed at the transport boundary.""" +import gzip +import hashlib +import json +from base64 import b64encode +from copy import deepcopy + +import pytest + +from loopx.control_plane import effect_runtime as runtime +from loopx.control_plane.todos.goal_todo_projection import project_todo_read + + +def response(data, *, encoding="gzip", size=None): + piece = {"offset": 0, "byte_count": len(data), "sha256": hashlib.sha256(data).hexdigest(), + "data": b64encode(data).decode(), "next": None} + if encoding is not None: + piece.update(content_encoding=encoding, decoded_byte_count=size) + return {"schema_version": "todo_read_result_v0", "result_piece": piece} + + +def test_compressed_read_preserves_complete_unknown_fields(monkeypatch): + expected = {"schema_version": "todo_read_result_v0", "fields": { + "agent_todos": {"items": [{"todo_id": "todo_original", "note": "完整要求🙂" * 4000, + "required_write_scopes": ["result.json"], "extra": {"empty": [], "absent_is_not_null": None}}]}}} + decoded = json.dumps(expected, ensure_ascii=False).encode() + packet = response(gzip.compress(decoded), size=len(decoded)) + monkeypatch.setattr(runtime, "effect_runtime_result", lambda *args, **kwargs: packet) + assert project_todo_read({"kind": "source_fields"}) == expected + + +def test_legacy_uncompressed_read_keeps_the_existing_result(monkeypatch): + expected = {"schema_version": "todo_read_result_v0", "fields": {"extra": [True, 1, 1.0, None]}} + data = json.dumps(expected).encode() + monkeypatch.setattr(runtime, "effect_runtime_result", lambda *args, **kwargs: response(data, encoding=None)) + assert project_todo_read({"kind": "source_fields"}) == expected + + +@pytest.mark.parametrize("mutation", ["truncated", "trailing", "too_small", "too_large", "boolean", "oversized", "unknown"]) +def test_compressed_reply_rejects_invalid_extent_or_encoding(monkeypatch, mutation): + decoded = b'{"schema_version":"todo_read_result_v0","fields":{"note":"required"}}' + data, size, encoding = gzip.compress(decoded), len(decoded), "gzip" + if mutation == "truncated": + data = data[:-8] + elif mutation == "trailing": + data += gzip.compress(b"{}") + size += 2 + elif mutation == "too_small": + size -= 1 + elif mutation == "too_large": + size += 1 + elif mutation == "boolean": + size = True + elif mutation == "oversized": + size = runtime.MAX_LOCAL_SNAPSHOT_BYTES + 1 + else: + encoding = "unknown" + packet = response(data, size=size, encoding=encoding) + monkeypatch.setattr(runtime, "effect_runtime_result", lambda *args, **kwargs: packet) + with pytest.raises(ValueError, match="Todo read result"): + project_todo_read({"kind": "source_fields"}) + + +def test_compressed_reply_identity_cannot_change_between_pages(monkeypatch): + decoded = b'{"schema_version":"todo_read_result_v0","fields":{"note":"required"}}' + data = gzip.compress(decoded) + first = response(data, size=len(decoded)) + first["result_piece"].update(data=b64encode(data[:10]).decode(), + next={"offset": 10, "sha256": first["result_piece"]["sha256"]}) + second = response(data, size=len(decoded) + 1) + second["result_piece"].update(offset=10, data=b64encode(data[10:]).decode()) + packets = iter([first, second]) + monkeypatch.setattr(runtime, "effect_runtime_result", lambda *args, **kwargs: next(packets)) + with pytest.raises(ValueError, match="identity changed"): + project_todo_read({"kind": "source_fields"}) + + +@pytest.mark.parametrize("size", [8000, 15000]) +def test_real_quota_bundle_keeps_complete_rows_without_response_snapshots(monkeypatch, size): + from loopx.control_plane.testing.quota_fixtures import quota_status_payload + from loopx.control_plane.todos import goal_todo_projection + from loopx.control_plane.todos.summary_item import todo_planning_source_items, todo_summary_source_items + from loopx.quota import build_quota_should_run + + snapshots, decoded = [], [] + original = runtime._read_local_snapshot_response + def observed(envelope, sink, **kwargs): + snapshots.append(kwargs["method"]) + return original(envelope, sink, **kwargs) + monkeypatch.setattr(runtime, "_read_local_snapshot_response", observed) + decode = goal_todo_projection._decode_todo_result_bytes + def retain_decoded(*args): + result = decode(*args) + decoded.append(result) + return result + monkeypatch.setattr(goal_todo_projection, "_decode_todo_result_bytes", retain_decoded) + rows = [{"todo_id": f"todo_build_{i:05d}", "text": "Build work", "role": "agent", "status": "open", + "done": False, "task_class": "advancement_task", "priority": "P1", "claimed_by": "agent-a", + "successor_todo_ids": [f"todo_build_{(i + 1) % size:05d}"]} for i in range(size)] + rows[0].update(note="完整验收与恢复🙂", required_write_scopes=["result.json"], evidence="verifier:accepted") + source = {"schema_version": "todo_summary_v0", "source_section": "Agent Todo", "total_count": size, + "open_count": size, "done_count": 0, "items": rows, "first_open_items": rows[:1]} + original_source = deepcopy(source) + # The supported standalone transports independently define each view's + # complete fields, including deliberate presentation differences. + expected = {"summary": todo_summary_source_items(source), "planning": todo_planning_source_items(source), + "terminal": todo_planning_source_items(source, include_terminal=True)} + snapshots.clear() + status = quota_status_payload(goal_id="goal-a", status="active_state_agent_todo", recommended_action="Build work", + agent_todos=source, coordination={"registered_agents": ["agent-a"]}) + with runtime.effect_runtime_request_scope(): + result = build_quota_should_run(status, goal_id="goal-a", agent_id="agent-a", available_capabilities=["shell"]) + assert result["selected_todo"]["todo_id"] == "todo_build_00000" + assert result["decision"] == "autonomous_replan_required" + assert "todo.read" not in snapshots, "quota response must stay within the shared socket frame" + wires = next(reply["quota_wires"] for reply in decoded if "quota_wires" in reply) + for name, view in wires[-1]["display_views"].items(): + assert [row["todo_id"] for row in view] == [row["todo_id"] for row in rows] + assert view == expected[name] + assert source == original_source, "transport compression grants no source mutation" diff --git a/tests/control_plane/test_vision_wait_coverage.py b/tests/control_plane/test_vision_wait_coverage.py index 53d8d71380..ce58e95464 100644 --- a/tests/control_plane/test_vision_wait_coverage.py +++ b/tests/control_plane/test_vision_wait_coverage.py @@ -2,6 +2,8 @@ from __future__ import annotations +from tests.control_plane.codex_checkpoint_fixture import read_cli_policy_payload + import json import shutil from pathlib import Path @@ -198,7 +200,7 @@ def state(metadata: str | None) -> str: ) == 0 ) - authored = json.loads(capsys.readouterr().out) + authored = read_cli_policy_payload(capsys.readouterr().out, returncode=0) assert authored["ok"] is True else: run = vision_run(declared=declared) @@ -234,7 +236,7 @@ def state(metadata: str | None) -> str: "ark_managed_agent_goal", ] assert cli_main(args) == 0 - missing = json.loads(capsys.readouterr().out) + missing = read_cli_policy_payload(capsys.readouterr().out, returncode=0) assert missing["effective_action"] == "autonomous_replan_required" runtime_revision = 0 @@ -250,14 +252,14 @@ def reload_source_authority() -> None: state_file.write_text(state("status=open claimed_by=worker")) reload_source_authority() assert cli_main(args) == 0 - continued = json.loads(capsys.readouterr().out) + continued = read_cli_policy_payload(capsys.readouterr().out, returncode=0) assert continued["selected_todo"]["todo_id"] == "todo_source_b" state_file.write_text( state("status=deferred claimed_by=worker resume_when=todo_done:todo_recovery") ) reload_source_authority() assert cli_main(args) == 0 - waiting = json.loads(capsys.readouterr().out) + waiting = read_cli_policy_payload(capsys.readouterr().out, returncode=0) assert waiting["execution_obligation"]["must_attempt_work"] is False assert ( waiting["scheduler_hint"]["goal_runtime_continuation"]["disposition"] == "defer" @@ -424,7 +426,7 @@ def test_cli_current_agent_wait_preserves_canonical_blocker_priority( "quota", "should-run", "--verbose", "--goal-id", GOAL, "--agent-id", AGENT, "--runtime-profile", "ark_managed_agent_goal", ]) == 0 - result = json.loads(capsys.readouterr().out) + result = read_cli_policy_payload(capsys.readouterr().out, returncode=0) wait = result["goal_frontier_projection"]["vision_wait_state"] assert wait["reason_code"] == "current_agent_blocker" assert wait["selected_todo_id"] == "todo_source_a" diff --git a/tests/control_plane/test_work_yardstick_cli.py b/tests/control_plane/test_work_yardstick_cli.py index b25cc101d5..5560475451 100644 --- a/tests/control_plane/test_work_yardstick_cli.py +++ b/tests/control_plane/test_work_yardstick_cli.py @@ -1,4 +1,4 @@ -"""Same-work SQLite journey: evidence → ordinary Turn → real action → readback. +"""Same-work journey: evidence → ordinary Turn → real action → readback. The deterministic actor proves transport/adoption attribution, not model quality or a causal benchmark gain. No evaluator, credentials or active Goal are used. @@ -6,13 +6,21 @@ import hashlib from itertools import groupby import json +import shlex +import pytest +from benchmark.runtime.context_delivery import preflight_context_instruction from tests.control_plane.test_quota_settlement_cli import ( - _initialize_fixture_authority, _projected_cli_args, _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, + _configure_read_only_todo, _heartbeat_receipt_count, _initialize_fixture_authority, _projected_cli_args, + _spend_run_count, _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, +) +from loopx.capabilities.explore.result_log import ( + append_explore_result_event, build_explore_finding_event, build_explore_node_event, + explore_result_log_path, load_explore_result_events, ) -from loopx.capabilities.explore.result_log import explore_result_log_path, load_explore_result_events from loopx.capabilities.explore.turn_context import explore_turn_context +from loopx.control_plane.turn_driver.decision_capture import read_context_read_reference from loopx.presentation.explore_results_api import _result_rows @@ -36,8 +44,10 @@ def attachment(): evidence_refs=["result:reversal"], work_yardstick_decision=decision()) -def setup(tmp_path, enabled=True, *, active=False): +def setup(tmp_path, enabled=True, *, active=False, provider="sqlite", write_scope=None, read_only=False): project, runtime, path = _write_fixture(tmp_path) + if read_only: + _configure_read_only_todo(project) config = json.loads(path.read_text()) if active: config["goals"][0]["status"] = "active" @@ -46,8 +56,10 @@ def setup(tmp_path, enabled=True, *, active=False): config["goals"][0]["explore_graph"] = {"enabled": True} config["goals"][0]["quota"]["allowed_slots"] = 4 config["goals"][0].setdefault("execution_profile", {})["yardstick_enabled"] = enabled + if write_scope is not None: + config["goals"][0]["coordination"]["write_scope"] = write_scope path.write_text(json.dumps(config)) - _initialize_fixture_authority(project, runtime, provider="sqlite") + _initialize_fixture_authority(project, runtime, provider=provider) return project, runtime, path @@ -77,6 +89,306 @@ def context(runtime, path, *, detail=True): return compact +def starting_page(project, runtime, path, turn, saved, monkeypatch): + data = saved.read_bytes() + assert json.loads(data)["effective_action"] == "normal_run" + native = saved.parent / "native" + native.mkdir(mode=0o700) + env = {"LOOPX_CLI": "loopx", "LOOPX_GOAL_ID": GOAL_ID, "LOOPX_AGENT_ID": AGENT_ID, + "LOOPX_CODEX_CONTEXT_DIR": str(native)} + prompt = preflight_context_instruction(env, saved, hashlib.sha256(data).hexdigest(), turn[-1]) + argv = shlex.split(prompt.split("```sh\n", 1)[1].split("\n```", 1)[0]) + with monkeypatch.context() as patch: + patch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") + patch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + page = run(project, runtime, path, *argv[argv.index("quota"):]) + assert saved.read_bytes() == data + return page + + +def wide_write_scope(): + return [ + f"modules/component-{i:03d}/experiments/artifact-encodings/public-validation/" + "accepted-input-compatibility/fixture-baseline/ordered-results/recorded-comparison/**" + for i in range(100) + ] + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_yardstick_cue_reaches_the_starting_read_before_candidate_selection(tmp_path, enabled, monkeypatch): + project, runtime, path = setup(tmp_path, enabled, active=True) + turn = binding("yardstick-before-choice") + capture = tmp_path / "capture" + guard = run(project, runtime, path, "quota", "should-run", "--codex-app", *turn, + "--decision-output-dir", str(capture), "--context-delivery", "on", "--delta-delivery", "off") + assert guard["schema_version"] == "context_capture_navigation_v1" + opening = starting_page(project, runtime, path, turn, capture / "decision.json", monkeypatch)["content"] + # The independently readable page carries the decision cue. The complete + # attachment template stays available on the existing Explore reader. + cue_path = "/content/graph/work_yardstick_capture/guidance" + assert (cue_path in opening) is enabled + assert "/content/graph/work_yardstick_capture/template" not in opening + graph = context(runtime, path, detail=False)["graph"] + if enabled: + assert "use one bounded accepted-outcome comparison" in opening + assert graph["work_yardstick_capture"]["optional"] is True + assert graph["work_yardstick_capture"]["template"]["proxy"] is None + else: + assert "work_yardstick_capture" not in graph + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_yardstick_opening_keeps_the_current_cue_with_a_multi_page_write_scope(tmp_path, provider, monkeypatch): + scope = wide_write_scope() + project, runtime, path = setup(tmp_path, active=True, provider=provider, write_scope=scope) + source = tmp_path / "finding.json" + source.write_text(json.dumps(attachment())) + receipt = refresh(project, runtime, path, "prior-finding", source, "--delivery-boundary", "in_flight_continuation") + assert receipt["explore_result_delivery"]["ok"] + run(project, runtime, path, *_projected_cli_args(receipt["settlement_owed"]["command"], turn_instance_id="prior-finding")) + + turn = binding("wide-goal-before-choice") + capture = tmp_path / "capture" + run(project, runtime, path, "quota", "should-run", "--codex-app", *turn, + "--decision-output-dir", str(capture), "--context-delivery", "on", "--delta-delivery", "off") + saved = capture / "decision.json" + original = saved.read_bytes() + opening = starting_page(project, runtime, path, turn, saved, monkeypatch) + assert opening["offset"] == 0 and not opening["eof"] + assert len(json.dumps(opening, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 12000 + content = opening["content"] + graph = context(runtime, path, detail=False)["graph"] + guidance_at = content.index(',"value":', content.index('/graph/work_yardstick_capture/guidance"')) + cue_at = content.index(',"value":', content.index('/graph/work_yardsticks/0"')) + guidance, _ = json.JSONDecoder().raw_decode(content, guidance_at + len(',"value":')) + cue, _ = json.JSONDecoder().raw_decode(content, cue_at + len(',"value":')) + assert guidance == graph["work_yardstick_capture"]["guidance"] + assert cue == graph["work_yardsticks"][0] and cue["todo_id"] == TODO_ID + + _, reference = read_context_read_reference(opening["reference_file"]) + assert reference["sha256"] == hashlib.sha256(original).hexdigest() + chunks = [content] + offset = len(content.encode()) + page = opening + while not page["eof"]: + assert page["next_offset"] == offset + words = shlex.split(page["next_command"]) + page = run(project, runtime, path, *words[words.index("quota"):]) + _, follow = read_context_read_reference(page["reference_file"]) + assert follow["path"] == reference["path"] and follow["sha256"] == reference["sha256"] + assert page["offset"] == offset + chunks.append(page["content"]) + offset += len(page["content"].encode()) + assert offset == page["total_bytes"] + values = {row["pointer"]: row["value"] for row in json.loads("".join(chunks))["selections"]} + assert values["/effective_action"] == "normal_run" + assert values["/goal_boundary"] == json.loads(original)["goal_boundary"] + assert values["/goal_boundary"]["write_scope"] == scope + assert saved.read_bytes() == original + assert _heartbeat_receipt_count(runtime, turn[-1]) == _spend_run_count(runtime) == 1 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("enabled", [None, False]) +def test_disabled_yardstick_preserves_exact_first_read_option(tmp_path, provider, enabled, monkeypatch): + project, runtime, path = setup(tmp_path, False, active=True, provider=provider) + if enabled is None: + config = json.loads(path.read_text()) + del config["goals"][0]["execution_profile"]["yardstick_enabled"] + path.write_text(json.dumps(config)) + turn = binding("off-exact-first-read") + capture = tmp_path / "capture" + run(project, runtime, path, "quota", "should-run", "--codex-app", *turn, + "--decision-output-dir", str(capture), "--context-delivery", "on", "--delta-delivery", "off") + saved = capture / "decision.json" + original = saved.read_bytes() + native = tmp_path / "native" + native.mkdir(mode=0o700) + env = {"LOOPX_CLI": "loopx", "LOOPX_GOAL_ID": GOAL_ID, "LOOPX_AGENT_ID": AGENT_ID, + "LOOPX_CODEX_CONTEXT_DIR": str(native)} + prompt = preflight_context_instruction(env, saved, hashlib.sha256(original).hexdigest(), turn[-1]) + # This existing optional route is an agent decision, not an instruction to + # drain an unrelated default opening when the policy is off. + assert "For known fields, replace --initial-read with repeated --select pointers" in prompt + argv = shlex.split(prompt.split("```sh\n", 1)[1].split("\n```", 1)[0]) + argv.remove("--initial-read") + wanted = ["/effective_action", "/normal_delivery_allowed", "/heartbeat_receipt", + "/selected_todo", "/goal_id"] + for pointer in wanted: + argv.extend(["--select", pointer]) + with monkeypatch.context() as patch: + patch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") + patch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + page = run(project, runtime, path, *argv[argv.index("quota"):]) + assert page["eof"] + values = {row["pointer"]: row["value"] for row in json.loads(page["content"])["selections"]} + assert values == {pointer: json.loads(original)[pointer[1:]] for pointer in wanted} + assert saved.read_bytes() == original + assert _heartbeat_receipt_count(runtime, turn[-1]) == 1 and _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_first_work_reads_linked_measurements_without_a_yardstick_decision(tmp_path, provider, monkeypatch): + project, runtime, path = setup(tmp_path, active=True, provider=provider, write_scope=wide_write_scope()) + source = tmp_path / "finding.json" + finding = attachment() + profiles = decision() + del finding["work_yardstick_decision"] + finding["evidence_refs"] = ["result:a", "result:b"] + finding["measurements"] = [ + {**profiles[profile], "candidate_ref": f"candidate:{candidate}", + "evidence_ref": f"result:{candidate}", "value": value} + for profile, points in [("authoritative", [("a", 10), ("b", 20)]), + ("proxy", [("a", 9), ("b", 8)])] + for candidate, value in points + ] + source.write_text(json.dumps(finding)) + receipt = refresh(project, runtime, path, "plain-measurements", source, + "--delivery-boundary", "in_flight_continuation") + assert receipt["explore_result_delivery"]["ok"] + run(project, runtime, path, *_projected_cli_args(receipt["settlement_owed"]["command"], turn_instance_id="plain-measurements")) + events_before = load_explore_result_events(explore_result_log_path(runtime, GOAL_ID)) + turn = binding("first-linked-comparison") + capture = tmp_path / "capture" + run(project, runtime, path, "quota", "should-run", "--codex-app", *turn, + "--decision-output-dir", str(capture), "--context-delivery", "on", "--delta-delivery", "off") + saved = capture / "decision.json" + original = saved.read_bytes() + page = starting_page(project, runtime, path, turn, saved, monkeypatch) + assert page["offset"] == 0 and not page["eof"] + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 12000 + marker = "/content/graph/work_measurements/0\"" + start = page["content"].index(',"value":', page["content"].index(marker)) + len(',"value":') + comparison, _ = json.JSONDecoder().raw_decode(page["content"], start) + assert comparison["todo_id"] == TODO_ID + metrics = {row["metric"]: row for row in comparison["measurement_feedback"]} + assert metrics["quality"]["best"]["candidate_ref"] == "candidate:b" + assert metrics["screen"]["best"]["candidate_ref"] == "candidate:a" + assert metrics["quality"]["latest"]["measurement"]["value"] == 20 + assert comparison["omitted_measurement_feedback"] == 0 + assert "search_comparison" not in comparison and "authoritative" not in comparison + assert "work_yardsticks" not in context(runtime, path, detail=False)["graph"] + assert load_explore_result_events(explore_result_log_path(runtime, GOAL_ID)) == events_before + assert saved.read_bytes() == original + assert _heartbeat_receipt_count(runtime, turn[-1]) == _spend_run_count(runtime) == 1 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("enabled", [False, True]) +def test_receipt_reentry_keeps_the_selected_work_outside_the_global_context_window(tmp_path, provider, enabled, monkeypatch): + project, runtime, path = setup(tmp_path, enabled, active=True, provider=provider) + identity = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID] + todo_ids = [TODO_ID] + for index in range(4): + added = run(project, runtime, path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--claimed-by", AGENT_ID, "--priority", "P1", + "--text", f"Validate independent representation {index}", + "--task-class", "advancement_task", "--action-kind", "validate", + "--target-key", f"representation-{index}") + todo_ids.append(added["todo_id"]) + log = explore_result_log_path(runtime, GOAL_ID) + for index, todo in enumerate(todo_ids): + node = f"question-{index}" + run(project, runtime, path, "todo", "update", *identity, "--todo-id", todo, + "--explore-result-node-ref", node) + append_explore_result_event(log, build_explore_node_event(goal_id=GOAL_ID, + agent_id=AGENT_ID, node_id=node, title=node, node_kind="question", status="exploring")) + evidence = f"result:representation-{index}" + append_explore_result_event(log, build_explore_finding_event(goal_id=GOAL_ID, + agent_id=AGENT_ID, node_id=node, finding_id=f"finding-{index}", + title=f"Observed representation {index}", status="confirmed", evidence_refs=[evidence], + measurements=[{**decision()["authoritative"], "value": index, + "candidate_ref": f"candidate:{index}", "evidence_ref": evidence}])) + global_view = run(project, runtime, path, "explore", "turn-context", *identity) + visible = {card["todo_id"] for card in global_view["graph"].get("work_measurements", [])} + target = next(todo for todo in todo_ids if todo not in visible) + events_before = load_explore_result_events(log) + turn_id = "selected-context-reentry" + turn = [*identity, "--turn-instance-id", turn_id] + command = ["quota", "should-run", "--codex-app", *turn, + "--context-delivery", "on", "--delta-delivery", "off"] + + def capture(name, *options): + directory = tmp_path / name + run(project, runtime, path, *command, *options, "--decision-output-dir", str(directory)) + saved = directory / "decision.json" + return saved, json.loads(saved.read_bytes()) + + _, discovery = capture("discovery") + assert discovery["interaction_contract"]["cli_channel"]["selection_required"] + assert discovery["interaction_contract"]["agent_channel"]["delivery_allowed"] is False + _, chosen = capture("chosen", "--todo-id", target) + receipts_after_selection = _heartbeat_receipt_count(runtime, turn_id) + saved, reentered = capture("reentered") + assert chosen["selected_todo"]["todo_id"] == reentered["selected_todo"]["todo_id"] == target + assert reentered["effective_action"] == "normal_run" and reentered["normal_delivery_allowed"] + assert reentered["interaction_contract"]["agent_channel"]["delivery_allowed"] is True + assert chosen["heartbeat_receipt"]["settlement_identity"] == reentered["heartbeat_receipt"]["settlement_identity"] + sources = reentered["interaction_contract"]["agent_channel"]["work_context"]["sources"] + view = next(source["content"] for source in sources if source.get("kind") == "explore_turn_context") + if enabled: + assert view["todo_id"] == target + assert [card["todo_id"] for card in view["graph"]["work_measurements"]] == [target] + else: + assert "todo_id" not in view and "work_measurements" not in view["graph"] + # The public CLI wraps the same view with invocation-routing metadata. + expected = {key: value for key, value in global_view.items() if key != "source_runtime_route"} + reference = view["graph"].get("result_writeback_guidance_ref") + if reference is not None: + # An existing native profile shares the unchanged guidance through + # this same capture. Verify the target before comparing its view. + assert reference == "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[1].optional_attachments[0].guidance" + shared = reentered["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0]["guidance"] + graph = dict(expected["graph"]) + assert graph.pop("result_writeback_guidance") == shared + expected = {**expected, "graph": {**graph, "result_writeback_guidance_ref": reference}} + assert view == expected + opening = starting_page(project, runtime, path, turn, saved, monkeypatch)["content"] + assert ("/content/graph/work_measurements/0" in opening) is enabled + assert target in opening + assert load_explore_result_events(log) == events_before + assert _heartbeat_receipt_count(runtime, turn_id) == receipts_after_selection + assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_receipt_settlement_does_not_require_a_closed_work_context(tmp_path, provider): + project, runtime, path = setup(tmp_path, active=True, provider=provider, read_only=True) + turn_id = "settlement-context-reentry" + turn = binding(turn_id) + guard = run(project, runtime, path, "quota", "should-run", "--codex-app", *turn) + original_identity = guard["heartbeat_receipt"]["settlement_identity"] + + def reenter(expected): + counts = _heartbeat_receipt_count(runtime, turn_id), _spend_run_count(runtime) + replay = run(project, runtime, path, "quota", "should-run", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", turn_id) + channel = replay["interaction_contract"]["agent_channel"] + assert replay["normal_delivery_allowed"] is False and channel["delivery_allowed"] is False + assert replay["effective_action"] == expected + assert replay["heartbeat_receipt"]["settlement_identity"] == original_identity + assert channel["work_context"]["complete"] + assert all(source.get("error_code") != "context_source_unavailable" + for source in channel["work_context"].get("sources", [])) + assert (_heartbeat_receipt_count(runtime, turn_id), _spend_run_count(runtime)) == counts + + run(project, runtime, path, "todo", "complete", *turn, "--claimed-by", AGENT_ID, + "--evidence", "artifact:verified-synthetic-evidence") + reenter("unsettled_host_turn_recovery") + archived = run(project, runtime, path, "todo", "archive-completed", "--goal-id", GOAL_ID, + "--max-active-done", "0", "--execute") + assert archived["moved_count"] == 1 + reenter("unsettled_host_turn_recovery") + run(project, runtime, path, "refresh-state", *turn, + "--classification", "validated_change", "--delivery-batch-scale", "single_surface", + "--delivery-outcome", "outcome_progress", "--no-global-sync", "--suppress-external-sinks") + reenter("unsettled_host_turn_recovery") + run(project, runtime, path, "quota", "spend-slot", *turn, + "--slots", "1", "--source", "heartbeat", "--execute") + reenter("heartbeat_settled_skip") + assert _heartbeat_receipt_count(runtime, turn_id) == _spend_run_count(runtime) == 1 + + def test_measured_yardstick_restarts_comparison_when_the_acceptance_basis_changes(tmp_path): project, runtime, path = setup(tmp_path, active=True) source = tmp_path / "finding.json" @@ -114,7 +426,7 @@ def record(turn, acceptance, candidates, previous=None): assert comparison["preferred_candidate_ref"] == "candidate:new-d" -def test_measured_yardstick_changes_next_work_from_local_screening_to_a_whole_goal_comparison(tmp_path): +def test_measured_yardstick_changes_next_work_from_local_screening_to_a_whole_goal_comparison(tmp_path, monkeypatch): project, runtime, path = setup(tmp_path, active=True) acceptance = tmp_path / "acceptance.json" acceptance.write_text(json.dumps(dict(outcome_id="encoded-artifact", owner_agent_id=AGENT_ID, @@ -160,7 +472,10 @@ def dictionary_encode(items): assert first["explore_result_delivery"]["ok"] run(project, runtime, path, *_projected_cli_args(first["settlement_owed"]["command"], turn_instance_id="turn-pair")) - guard = run(project, runtime, path, "quota", "should-run", "--codex-app", *binding("turn-compare")) + compare_turn = binding("turn-compare") + capture = tmp_path / "compare" + guard = run(project, runtime, path, "quota", "should-run", "--codex-app", *compare_turn, + "--decision-output-dir", str(capture)) compact = run(project, runtime, path, "explore", "turn-context", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID)["graph"]["work_yardsticks"][0] assert compact["comparison_candidate_ref"] == "candidate:dictionary" @@ -168,6 +483,13 @@ def dictionary_encode(items): assert "different mechanism or a compatible combination" in compact["comparison_cue"] assert "full implementation and validation cost" in compact["comparison_cue"] assert compact["comparison_cue"] in json.dumps(guard) + opening = starting_page(project, runtime, path, compare_turn, capture / "decision.json", monkeypatch)["content"] + # A page may end in a later entry. Decode the complete cue before that + # boundary without relying on any subsequent context read. + cue_start = opening.index(',"value":', opening.index("/content/graph/work_yardsticks/")) + delivered, _ = json.JSONDecoder().raw_decode(opening, cue_start + len(',"value":')) + assert delivered["todo_id"] == TODO_ID + assert delivered["comparison_candidate_ref"] == "candidate:dictionary" detailed = run(project, runtime, path, "explore", "turn-context", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--result-node", compact["node_id"])["graph"]["work_yardsticks"][0] assert detailed["next_action"] == choice["next_action"] @@ -178,7 +500,7 @@ def dictionary_encode(items): # Deterministic consumer. Combine the evidenced baseline with an independent # representation change and verify the original end-to-end outcome. - selected_base = {"candidate:dictionary": base}[compact["comparison_candidate_ref"]] + selected_base = {"candidate:dictionary": base}[delivered["comparison_candidate_ref"]] runs = [[index, len(list(group))] for index, group in groupby(selected_base["indices"])] artifact = project / "combined-encoding.json" artifact.write_bytes(encode({"keys": selected_base["keys"], "runs": runs})) diff --git a/tests/control_plane_ts/canonical_snapshot_page.test.ts b/tests/control_plane_ts/canonical_snapshot_page.test.ts index 82eb1f2c6d..7006e22832 100644 --- a/tests/control_plane_ts/canonical_snapshot_page.test.ts +++ b/tests/control_plane_ts/canonical_snapshot_page.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import {mkdtemp, rm, readdir, writeFile} from "node:fs/promises"; +import {mkdtemp, rm, readdir, writeFile, rename} from "node:fs/promises"; import {tmpdir} from "node:os"; import {join} from "node:path"; import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; @@ -20,6 +20,8 @@ import {engageCanonicalRoute} from "./local_promotion_fixture.ts"; import {legacyCoordinationWriterFencePath} from "../../loopx/control_plane/coordination/legacy_writer_fence.ts"; import {listLocalCoordinationTodos, readLocalCoordinationTodo} from "../../loopx/control_plane/coordination/local_authority_read.ts"; import {observeLocalCoordinationOwnership} from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; +import {localAuthorityProviderPaths, selectLocalSqliteAuthority} from "../../loopx/control_plane/coordination/local_authority_provider.ts"; +import type {AuthorityStore} from "../../loopx/control_plane/coordination/authority_store.ts"; async function fixture(t: test.TestContext, kind: "file" | "sqlite" = "file") { const root = await mkdtemp(join(tmpdir(), "canonical-snapshot-")); @@ -28,6 +30,88 @@ async function fixture(t: test.TestContext, kind: "file" | "sqlite" = "file") { return {store: new Store(root, "goal-a"), contender: new Store(root, "goal-a")}; } const qualifiedSqlite = sqliteRuntimeIdentity().sqlite_authority_qualified; +if (qualifiedSqlite) { + test("selected SQLite snapshot retains the complete independent read contract", async t => { + const {store} = await fixture(t, "sqlite"); + const source = productionScaleCoordinationFixture("goal-a", "native"); + await seedSnapshot(store, source.projection); + const identity = await store.storeIdentity(); + assert.equal(identity.status, "available"); + if (identity.status !== "available") throw new Error("fixture identity missing"); + const selected = new SqliteAuthorityStore(join(store.path, ".."), "goal-a", + {existingOnly: true, expectedIdentity: identity.store_identity}); + const baseline = await readCanonicalSnapshotFromStore(snapshotRequest(), store, CANONICAL_SNAPSHOT_LOCAL_BYTES); + const before = await selected.loadAuthority(); + const result = await readCanonicalSnapshotFromStore(snapshotRequest(), selected, CANONICAL_SNAPSHOT_LOCAL_BYTES); + assert.deepEqual(result, baseline); + assert.deepEqual(result.todos, source.projection.todos); + assert.deepEqual(await selected.loadAuthority(), before); + (result.todos as JsonObject[])[0]!.note = "caller mutation"; + assert.deepEqual(await readCanonicalSnapshotFromStore(snapshotRequest(), selected, CANONICAL_SNAPSHOT_LOCAL_BYTES), baseline); + }); + + test("selected SQLite snapshot rejects a real replacement after the head read", async t => { + const root = await mkdtemp(join(tmpdir(), "snapshot-replacement-")); + t.after(() => rm(root, {recursive: true, force: true})); + const directory = localAuthorityProviderPaths(root, "goal-a").sqlite; + const store = new SqliteAuthorityStore(directory, "goal-a"); + const projection = productionScaleCoordinationFixture("goal-a", "native").projection; + assert.equal((await selectLocalSqliteAuthority(root, "goal-a", true)).ok, true); + await engageCanonicalRoute(root); + await seedSnapshot(store, projection); + const replacement = new SqliteAuthorityStore(join(root, "replacement"), "goal-a"); + await seedSnapshot(replacement, projection); + const identity = await store.storeIdentity(); + if (identity.status !== "available") throw new Error("fixture identity missing"); + const selected = new SqliteAuthorityStore(directory, "goal-a", + {existingOnly: true, expectedIdentity: identity.store_identity}); + // Replace the database directory after the loaded result returns. The + // final read must reopen the pathname, not reuse an earlier connection. + let replaced = false; + const observed = new Proxy(selected, {get(target, key) { + const value = Reflect.get(target, key, target); + if ((key === "loadAuthority" || key === "readAuthoritySnapshot") && typeof value === "function") { + return async () => { + const result = await value.call(target); + if (!replaced) { + await rename(directory, join(root, "previous")); + await rename(join(root, "replacement"), directory); + replaced = true; + } + return result; + }; + } + return typeof value === "function" ? value.bind(target) : value; + }}); + const result = await readCanonicalSnapshotPage(snapshotRequest({runtime_root: root}), + {createStore: () => observed}); + assert.equal(replaced, true); + assert.equal(result.status, "failed"); + assert.equal(result.reason_code, "canonical_snapshot_changed"); + assert.equal("todos" in result, false); + assert.equal(result.legacy_fallback_used, false); + }); + + for (const kind of ["missing", "wrong_identity", "invalid_head"] as const) { + test(`selected SQLite ${kind} preserves original failure precedence`, async t => { + const {store} = await fixture(t, "sqlite"); + if (kind !== "missing") await seedSnapshot(store, kind === "invalid_head" ? {} : + productionScaleCoordinationFixture("goal-a", "native").projection); + const selected = new SqliteAuthorityStore(join(store.path, ".."), "goal-a", + {existingOnly: true, ...(kind === "wrong_identity" ? {expectedIdentity: `sqlite:${"0".repeat(32)}`} : {})}); + // Explicitly omit any acceleration to retain the immutable two-read + // caller contract as a comparison on the same real SQLite database. + const fallback = new Proxy(selected, {get(target, key) { + if (key === "readAuthoritySnapshot") return undefined; + const value = Reflect.get(target, key, target); + return typeof value === "function" ? value.bind(target) : value; + }}) as AuthorityStore; + const read = (target: AuthorityStore) => readCanonicalSnapshotFromStore(snapshotRequest(), target) + .then(value => ({value}), error => ({error: error.message, code: error.code})); + assert.deepEqual(await read(selected), await read(fallback)); + }); + } +} if (!qualifiedSqlite) test.skip("SQLite snapshot conformance requires a qualified SQLite runtime", () => {}); for (const kind of (qualifiedSqlite ? ["file", "sqlite"] : ["file"]) as ("file" | "sqlite")[]) { registerCanonicalSnapshotConformance(kind, t => fixture(t, kind)); diff --git a/tests/control_plane_ts/context_selection.test.ts b/tests/control_plane_ts/context_selection.test.ts index 43ac3f534b..6a189326aa 100644 --- a/tests/control_plane_ts/context_selection.test.ts +++ b/tests/control_plane_ts/context_selection.test.ts @@ -146,6 +146,45 @@ function selectedFacts(context: unknown, selections: {path: (string | number)[]} return [...facts].sort(); } +test("only an admitted same-source Yardstick can precede the complete Goal boundary", () => { + const source = {kind: "explore_turn_context", capability_id: "explore", content: { + ok: true, graph_enabled: true, goal_id: "g", agent_id: "a", todo_id: "current", + graph: { + work_yardstick_capture: {guidance: "compare the accepted outcome", template: "optional detail"}, + work_yardsticks: [{todo_id: "other", cue: "unrelated experiment"}, {todo_id: "current", cue: "current experiment"}], + work_measurements: [{todo_id: "other", measurement_feedback: ["foreign measurements"]}, + {todo_id: "current", measurement_feedback: ["linked comparisons without a decision"]}], + }, + }}; + const context = { + goal_id: "g", agent_identity: {agent_id: "a"}, effective_action: "normal_run", + normal_delivery_allowed: true, selected_todo: {todo_id: "current"}, + goal_boundary: {rule: "Use only the authorized scope.", write_scope: ["work/**"]}, + interaction_contract: {schema_version: "loopx_interaction_contract_v0", + agent_channel: {work_context: {sources: [source]}}}, + }; + const initial = (capture = context) => (evaluateTurnEnvelope({ + operation: "context_select", context: capture, initial_read: true, + }).selections as {pointer: string; path: (string | number)[]}[]); + const rows = initial(); + const choice = rows.findIndex(row => row.pointer.endsWith("/work_yardsticks/1")); + assert.ok(choice > rows.findIndex(row => row.pointer === "/normal_delivery_allowed")); + assert.ok(choice < rows.findIndex(row => row.pointer === "/goal_boundary")); + const measurements = rows.findIndex(row => row.pointer.endsWith("/work_measurements/1")); + assert.ok(measurements > rows.findIndex(row => row.pointer === "/normal_delivery_allowed")); + assert.ok(measurements < rows.findIndex(row => row.pointer === "/goal_boundary")); + assert.ok(!selectedFacts(context, rows).some(fact => /unrelated experiment|foreign measurements|optional detail/.test(fact))); + for (const binding of [{goal_id: "another-goal"}, {agent_id: "another-agent"}, {todo_id: "other"}]) { + const capture = structuredClone(context); + Object.assign(capture.interaction_contract.agent_channel.work_context.sources[0].content, binding); + assert.ok(initial(capture).every(row => !/work_yardstick|work_measurements/.test(row.pointer))); + } + for (const gate of [{normal_delivery_allowed: false}, {effective_action: "autonomous_replan_required"}, + {agent_id: "a", agent_identity: {agent_id: "another-agent"}}]) { + assert.ok(initial({...context, ...gate}).every(row => !/work_yardstick|work_measurements/.test(row.pointer))); + } +}); + test("initial navigation coalesces complete branches without changing selected facts or explicit readers", () => { const context = {facts: [{"a/b": "Keep the final stop.", "~": null}, {allowed: false, evidence: []}], single: {only: "A single address need not grow a container."}, unread: {history: "optional body"}}; diff --git a/tests/control_plane_ts/explore_turn_context.test.ts b/tests/control_plane_ts/explore_turn_context.test.ts index d2205cc8b3..f582a7171e 100644 --- a/tests/control_plane_ts/explore_turn_context.test.ts +++ b/tests/control_plane_ts/explore_turn_context.test.ts @@ -5,6 +5,81 @@ import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import {projectStrideExploreBasis, strideReviewMismatch} from "../../loopx/control_plane/work_items/stride_policy.ts"; import {projectExploreTurnContext} from "../../loopx/control_plane/capabilities/explore_turn_context.ts"; +test("initial work measurement feedback preserves linked scopes without choosing an outcome profile", () => { + const profile = {metric: "quality", unit: "points", higher_is_better: true, + comparison_scope: "inputs:shared", input_revision: "v1", evaluation_ref: "judge:public"}; + const finding = (id: string, node: string, order: number, candidate: string, value: number | null, + changes: JsonObject = {}) => ({finding_id: id, node_id: node, recorded_order: order, + measurements: [{...profile, ...changes, candidate_ref: candidate, evidence_ref: `result:${id}`, value}]}); + const todo = {todo_id: "todo_current", role: "agent", status: "open", done: false, claimed_by: "worker", + explore_result_node_refs: ["shared", "local"]}; + const request = {goal_id: "research", agent_id: "worker", route: ["loopx"], yardstick_enabled: true, + graph_enabled: true, harness_gate: {enabled: false}, work_todos: [todo], plan: {}, + projection: {nodes: [], findings: [ + finding("first", "shared", 1, "candidate:a", 10), + finding("second", "shared", 2, "candidate:b", 20), + finding("foreign", "unlinked", 3, "candidate:foreign", 1000), + finding("local-a", "local", 4, "candidate:a", 9, {metric: "screen"}), + finding("local-b", "local", 5, "candidate:b", 8, {metric: "screen"}), + finding("unknown", "local", 6, "candidate:c", null, {input_revision: "v2"}), + finding("other-unit", "local", 7, "candidate:c", 30, {unit: "percent"}), + ]}}; + const original = structuredClone(request); + const graph = projectExploreTurnContext(request).graph as JsonObject; + const card = graph.work_measurements[0]; + assert.equal(card.todo_id, todo.todo_id); + assert.equal(card.measurement_feedback.length, 3); + assert.equal(card.omitted_measurement_feedback, 1); + const outcome = card.measurement_feedback.find(row => row.node_id === "shared"); + assert.equal(outcome.best.candidate_ref, "candidate:b"); + assert.equal(outcome.best.value, 20); + assert.equal(outcome.latest.finding_id, "second"); + assert.equal(outcome.input_revision, "v1"); + const unknown = card.measurement_feedback.find(row => row.input_revision === "v2"); + assert.equal(unknown.latest.measurement.value, null); + assert.equal(unknown.best, null); + assert.equal(unknown.improvement, null); + assert.ok(card.measurement_feedback.every(row => row.node_id !== "unlinked")); + assert.ok(!Object.hasOwn(card, "authoritative") && !Object.hasOwn(card, "search_comparison")); + assert.ok(!Object.hasOwn(graph, "work_yardsticks")); + assert.ok(card.details_command.includes("--todo-id") && card.details_command.includes(todo.todo_id)); + assert.deepEqual(request, original); + for (const enabled of [false, undefined]) { + const off = projectExploreTurnContext({...request, yardstick_enabled: enabled}).graph as JsonObject; + assert.ok(!Object.hasOwn(off, "work_measurements")); + assert.deepEqual(off.measurement_feedback, graph.measurement_feedback); + } + for (const changes of [{claimed_by: "another"}, {done: true}, {status: "blocked"}, {explore_result_node_refs: []}]) { + const unavailable = projectExploreTurnContext({...request, work_todos: [{...todo, ...changes}]}).graph as JsonObject; + assert.ok(!Object.hasOwn(unavailable, "work_measurements")); + } + const scoped = projectExploreTurnContext({...request, result_node: "unlinked"}).graph as JsonObject; + assert.ok(!Object.hasOwn(scoped, "work_measurements")); + const shared = projectExploreTurnContext({...request, + work_todos: [todo, {...todo, todo_id: "todo_shared"}]}).graph as JsonObject; + assert.deepEqual(shared.work_measurements.map(row => row.todo_id), ["todo_current", "todo_shared"]); + assert.deepEqual(shared.work_measurements[0].measurement_feedback, shared.work_measurements[1].measurement_feedback); + const tied = structuredClone(request); + tied.projection.findings[1].measurements[0].value = 10; + const tiedGraph = projectExploreTurnContext(tied).graph as JsonObject; + const tie = tiedGraph.work_measurements[0].measurement_feedback.find(row => row.node_id === "shared"); + assert.equal(tie.best.candidate_ref, "candidate:a"); + assert.equal(tie.latest.measurement.candidate_ref, "candidate:b"); + assert.equal(tie.improvement, 0); + const declared: JsonObject = {...request, result_node: "local", projection: {...request.projection, + findings: [...request.projection.findings, {finding_id: "declared", node_id: "shared", recorded_order: 8, + evidence_refs: [], work_yardstick: {binding: {goal_id: "research", agent_id: "worker", + todo_id: todo.todo_id, turn_instance_id: "turn:declared"}, decision: { + schema_version: "work_yardstick_decision_v0", target: {summary: "Improve the accepted outcome", + scope: "Original work", acceptance_ref: "acceptance:original"}, authoritative: profile, + proxy: null, counterevidence_refs: [], prior_finding_id: null, + next_action: {method_id: "method:retained", reason: "Continue the recorded choice", cost_bound: "Existing budget"}, + }}}]}}; + const differentQuestion = projectExploreTurnContext(declared).graph as JsonObject; + assert.ok(!Object.hasOwn(differentQuestion, "work_measurements")); + assert.ok(differentQuestion.measurement_feedback.every(row => row.node_id === "local")); +}); + test("recent questions use UTC instants, preserve microseconds and retain legacy timestamps", () => { for (const timestamps of [ ["2026-01-02T08:00:00+08:00", "2026-01-02T00:01:00Z", "2026-01-01T19:02:00-05:00"], diff --git a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts index 888d1c6af7..ab4a952be1 100644 --- a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts +++ b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts @@ -19,6 +19,10 @@ import { } from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; import { openLocalAuthorityStoreHandle } from "../../loopx/control_plane/coordination/local_authority_provider.ts"; import {readLocalCoordinationTodoSource} from "../../loopx/control_plane/coordination/local_authority_read.ts"; +import {CANONICAL_SNAPSHOT_PAGE_REQUEST, readCanonicalSnapshotFromStore} from "../../loopx/control_plane/coordination/canonical_snapshot_page.ts"; +import {projectTodoContextPage} from "../../loopx/control_plane/todos/context_projection.ts"; +import {readCanonicalTodoProjection, readCanonicalQuotaProjection} from "../../loopx/control_plane/todos/read_transaction.ts"; +import {canonicalReadRoot} from "./local_promotion_fixture.ts"; import {coordinationTodoReadModel} from "../../loopx/control_plane/coordination/coordination_projection.ts"; import {inspectGoalAcceptance, refineGoalAcceptanceOutcome} from "../../loopx/control_plane/goals/acceptance_authority.ts"; import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; @@ -124,6 +128,62 @@ async function cleanScope(tenantId: string, goalId: string): Promise { } if (database && installed) { + test("PostgreSQL canonical quota transaction binds one provider read and complete caller differences", async t => { + await installed; + const options = {tenant_id: `tenant-${randomUUID()}`, goal_id: `goal-${randomUUID()}`}; + t.after(() => cleanScope(options.tenant_id, options.goal_id)); + const root = await canonicalReadRoot(t, options.goal_id); + const store = new PostgreSqlAuthorityStore(database, options); + const projection = authorityProjectionFixture(options.goal_id, [{todo_id: "todo_native_quota", role: "agent", + status: "open", done: false, archive_state: "active", text: "Deliver the complete required artifact", priority: "P1", + task_class: "advancement_task", required_write_scopes: ["output.json"], note: "Retained source note", evidence: "artifact:retained"}]); + const seeded = await store.commitAuthority({operation_id: "quota-native-seed", expected_provider_revision: null, + next_projection: projection, events: [], receipts: []}); + assert.equal(seeded.status, "applied"); + let loads = 0; + const observed = new Proxy(store, {get(target, property) { + if (property === "loadAuthority") return async () => {loads++; return target.loadAuthority();}; + const value = Reflect.get(target, property); + return typeof value === "function" ? value.bind(target) : value; + }}); + const dependencies = {createStore: () => observed}; + const read = {schema_version: "todo_read_request_v0", kind: "canonical_fields", + source: {runtime_root: root, goal_id: options.goal_id, include_leases: false, projection_readback: null}, + observed_at: 1790812800, evaluated_at: "2026-10-01T00:00:00Z", rollout_events: []}; + const initial = await readCanonicalTodoProjection(read, dependencies); + const fields = initial.fields as JsonObject, lineage = initial.source_projection as JsonObject; + const request = (field: string, changes: JsonObject[], displays: boolean): JsonObject => ({ + schema_version: "todo_quota_planning_request_v4", current_time: "2026-10-01T00:00:00Z", + summary_request: {kind: "user", materialize_source: true, materialize_resume: true, consume_display_views: displays, + source_reference: {read, expected_source: lineage.source, field, field_sha256: (lineage.field_digests as JsonObject)[field], changes, python_values: []}, + integer_counts: {source: null, warning: null}}, + source_contract: fields[field], handoff_items: [], route_items: [], + resume_options: {agent_id: "agent-a", item_limit: 8, available_capabilities: []}, + selection: {observed_at: 1790812800, available: [], agent_id: "agent-a", profile: null, user_gate_scope: field === "user_todos", + monitor_supported: false, items: [], active_items: [], active_executable_items: [], source_open_count: 1, source_complete: true, + diagnostic_limit: 3, backlog_limit: 8, visibility_limit: 16}, + }); + const requests = [request("user_todos", [], false), request("agent_todos", [{path: ["vision_wait_states"], value: [{agent_id: "agent-a", state: "waiting"}]}], true)]; + loads = 0; + const result = await readCanonicalQuotaProjection({requests}, dependencies); + assert.equal(loads, 1, "both role requests share this one native provider read only"); + const wires = result.quota_wires as JsonObject[]; + assert.equal(wires[0]!.display_views, undefined, "the User caller did not request three displays"); + const views = wires[1]!.display_views as JsonObject; + assert.equal((views.terminal as JsonObject[])[0]!.note, "Retained source note"); + assert.deepEqual((views.summary as JsonObject[])[0]!.required_write_scopes, ["output.json"]); + const forged = structuredClone(requests); + ((forged[1]!.summary_request as JsonObject).source_reference as JsonObject).changes = [{path: ["items", 0, "note"], value: "Forged source fact"}]; + await assert.rejects(readCanonicalQuotaProjection({requests: forged}, dependencies), /canonical Todo facts/); + const untouched = await store.loadAuthority(); + assert.equal(untouched.status, "loaded"); + if (untouched.status !== "loaded") throw new Error("synthetic provider disappeared"); + assert.deepEqual(untouched.head, projection); + const changed = await store.commitAuthority({operation_id: "quota-native-source-change", expected_provider_revision: untouched.provider_revision, + next_projection: authorityProjectionFixture(options.goal_id, (projection.todos as JsonObject[]).map(row => ({...row, text: "A new requirement"}))), events: [], receipts: []}); + assert.equal(changed.status, "applied"); + await assert.rejects(readCanonicalQuotaProjection({requests}, dependencies), /canonical source changed/); + }); test("PostgreSQL planning reads the Agent declaration and work links from the canonical head", async t => { await installed; const options = {tenant_id: `tenant-${randomUUID()}`, goal_id: `goal-${randomUUID()}`}; @@ -187,6 +247,71 @@ if (database && installed) { "todo_source_lineage_mismatch"); }); + test("PostgreSQL canonical snapshot feeds complete display views without becoming a fresh authority read", async t => { + await installed; + const options = {tenant_id: `tenant-${randomUUID()}`, goal_id: `goal-${randomUUID()}`}; + t.after(() => cleanScope(options.tenant_id, options.goal_id)); + const store = new PostgreSqlAuthorityStore(database, options); + const retainedNote = "Keep every acceptance clause.\nRecover the original artifact and settlement identity."; + const todos = [ + {todo_id: "todo_01_open", role: "agent", status: "open", done: false, archive_state: "active", + text: "Implement the requested outcome", task_class: "advancement_task", + required_write_scopes: ["result.json"], note: retainedNote}, + {todo_id: "todo_02_blocker", role: "agent", status: "blocked", done: false, archive_state: "active", + text: "Wait for the required input", task_class: "blocker", claimed_by: "author"}, + {todo_id: "todo_03_completed", role: "agent", status: "done", done: true, archive_state: "active", + text: "Preserve the verified artifact", task_class: "advancement_task", + note: retainedNote, evidence: "artifact:verified-result", completed_at: "2026-01-01T00:00:00Z"}, + ]; + const projection = authorityProjectionFixture(options.goal_id, todos); + const committed = await store.commitAuthority({expected_provider_revision: null, + operation_id: "seed-display-source", events: [], receipts: [], next_projection: projection}); + assert.equal(committed.status, "applied"); + if (committed.status !== "applied") return; + const request = {schema_version: CANONICAL_SNAPSHOT_PAGE_REQUEST, runtime_root: tmpdir(), + goal_id: options.goal_id, include_leases: true, projection_readback: null, after: null}; + const first = await readCanonicalSnapshotFromStore(request, store); + assert.equal(first.status, "page"); + assert.equal(first.next, null); + const source = {source: {record: {items: first.todos}}, handoff_gates: []}; + const before = JSON.stringify(first); + const views = projectTodoContextPage({summary_display_views: source}); + const ids = (rows: unknown) => (rows as JsonObject[]).map(row => row.todo_id); + assert.deepEqual(ids(views.summary), ["todo_01_open", "todo_02_blocker"]); + assert.deepEqual(ids(views.planning), ["todo_01_open", "todo_02_blocker"]); + assert.deepEqual(ids(views.terminal), ["todo_01_open", "todo_02_blocker", "todo_03_completed"]); + assert.deepEqual((views.planning as JsonObject[])[0].required_write_scopes, ["result.json"]); + const terminal = (views.terminal as JsonObject[])[2]; + assert.equal(terminal.note, retainedNote); + assert.equal(terminal.evidence, "artifact:verified-result"); + assert.equal(terminal.completed_at, "2026-01-01T00:00:00Z"); + assert.equal(JSON.stringify(first), before, "display cannot mutate the complete source snapshot"); + const unchanged = await store.loadAuthority(); + assert.equal(unchanged.status, "loaded"); + if (unchanged.status !== "loaded") return; + assert.equal(unchanged.provider_revision, committed.provider_revision); + assert.deepEqual(unchanged.head, projection, "display cannot write or compact provider records"); + + const changed = authorityProjectionFixture(options.goal_id, todos.map(row => + row.todo_id === "todo_01_open" ? {...row, status: "done", done: true} : row)); + const later = await store.commitAuthority({expected_provider_revision: committed.provider_revision, + operation_id: "advance-display-source", events: [], receipts: [], next_projection: changed}); + assert.equal(later.status, "applied"); + if (later.status !== "applied") return; + const fresh = await readCanonicalSnapshotFromStore(request, store); + assert.equal(fresh.status, "page"); + assert.notEqual((fresh.snapshot as JsonObject).provider_revision, (first.snapshot as JsonObject).provider_revision); + assert.deepEqual((fresh.todos as JsonObject[]).filter(row => row.todo_id !== "todo_01_open"), + (first.todos as JsonObject[]).filter(row => row.todo_id !== "todo_01_open"), "non-target provider rows stay unchanged"); + assert.deepEqual(ids(projectTodoContextPage({summary_display_views: { + source: {record: {items: fresh.todos}}, handoff_gates: [], + }}).planning), ["todo_02_blocker"]); + assert.deepEqual(ids(projectTodoContextPage({summary_display_views: source}).planning), ["todo_01_open", "todo_02_blocker"], + "the retained source is historical evidence, never a newly read provider head"); + await assert.rejects(readCanonicalSnapshotFromStore({...request, + after: {snapshot: first.snapshot, todo_offset: 1, lease_offset: 0}}, store), /canonical snapshot changed/); + }); + registerAuthorityStoreConformance("PostgreSQL provider", async (t) => { await installed; const tenantId = `tenant-${randomUUID()}`; diff --git a/tests/control_plane_ts/quota_summary_transaction.test.ts b/tests/control_plane_ts/quota_summary_transaction.test.ts index 39858b6a4f..a2081b70c5 100644 --- a/tests/control_plane_ts/quota_summary_transaction.test.ts +++ b/tests/control_plane_ts/quota_summary_transaction.test.ts @@ -1,7 +1,8 @@ import assert from "node:assert/strict"; import test from "node:test"; import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; -import {composeQuotaSummary, prepareQuotaSummary} from "../../loopx/control_plane/todos/quota_selection.ts"; +import {composeQuotaSummary, prepareQuotaSummary, materializeQuotaRow} from "../../loopx/control_plane/todos/quota_selection.ts"; +import {decodeTodoReadSource, pythonFieldInteger} from "../../loopx/control_plane/todos/read_codec.ts"; function planning(): JsonObject { return {lanes: { @@ -88,3 +89,45 @@ test("source provenance follows lane precedence even when equal identities carry assert.equal(selected[0]!.gate, true); assert.equal((selected[0]!.payload as JsonObject).text, "Preferred source"); }); + +test("native materialization preserves distant microseconds without double rounding", () => { + assert.equal(materializeQuotaRow({next_due_at: "2500-01-01T00:00:00.000001Z"}, null).due_at, 16725225600.000002); + assert.equal(materializeQuotaRow({next_due_at: "2500-01-01T00:00:00.000031Z"}, null).due_at, 16725225600.00003); + assert.equal(materializeQuotaRow({expires_at: "2500-01-01T00:00:00.999999Z"}, null).expires_at, 16725225600.999998); +}); + +test("native materialization retains microsecond clocks, profile precedence and explicit gate classification", () => { + const row = {todo_id: "todo_monitor", index: "2_0", text: "Observe the source", priority: "P1", + task_class: "continuous_monitor", status: "open", action_kind: "private_observation", + claimed_by: "agent-a", bound_agent: "agent-b", blocks_agent: "agent-c", excluded_agents: ["agent-b"], + global_gate: "false", watch_only: "true", next_due_at: "1970-01-01T00:00:00.123456Z", + expires_at: "1970-01-01T00:00:00.654321Z", required_capabilities: ["shell"], target_capabilities: ["network"]}; + assert.deepEqual(materializeQuotaRow(row, {avoid_action_kinds: ["private_*"], + preferred_action_kinds: ["private_*"], default_task_classes: ["continuous_monitor"]}), { + presentation: [1, 999999, "", "todo_monitor"], claim: "agent-a", bound: "agent-b", blocks: "agent-c", + excluded: ["agent-b"], global: false, gate: false, removed: false, actionable: true, + due_at: 0.123456, expires_at: 0.654321, watch_only: true, task_class: "continuous_monitor", + priority: 1, index: 20, profile_rank: 2, required: ["shell"], targets: ["network"], raw_claimed: true, + }); + assert.equal(materializeQuotaRow({...row, next_due_at: "2026-02-30T00:00:00Z"}, null).due_at, null); + assert.equal(materializeQuotaRow({...row, task_class: "advancement_task"}, null).gate, false); +}); + +test("native facts keep Python scalar IO and JSON presentation as separate boundaries", () => { + const floating = decodeTodoReadSource({record: {schema_version: "todo_item_v0", source_section: "Agent Todo", + index: 1, todo_id: "todo_float", text: "Read source", priority: "P1", updated_at: "retained-time"}, + python_values: [[["index"], "1.0", "1.0", "1"]]}); + assert.deepEqual(materializeQuotaRow(floating, null).presentation, [1, 999999, "retained-time", "todo_float"]); + assert.equal(materializeQuotaRow(floating, null).index, 1); + const decimal = decodeTodoReadSource({record: {index: "2.5"}, + python_values: [[["index"], "2.5", "Decimal('2.5')", "2"]]}); + assert.equal(pythonFieldInteger(decimal, "index"), 2); + assert.equal(pythonFieldInteger({index: "\u0662_\u0660"}, "index"), 20); + assert.equal(pythonFieldInteger({index: "1.0"}, "index"), 999999); + const first = {todo_id: "todo_same", text: "Preferred lane", task_class: "user_gate"}; + const input = request({first_open_items: [first], items: [{...first, text: "Other lane", task_class: "advancement_task"}]}, {materialize_source: true}); + const selected = (prepareQuotaSummary(input).selection as JsonObject).items as JsonObject[]; + assert.equal(selected.length, 1); + assert.equal(selected[0]!.gate, true); + assert.equal((selected[0]!.payload as JsonObject).text, "Preferred lane"); +}); diff --git a/tests/control_plane_ts/todo_context_projection.test.ts b/tests/control_plane_ts/todo_context_projection.test.ts index 85d7bf9d0d..d1f3904ad0 100644 --- a/tests/control_plane_ts/todo_context_projection.test.ts +++ b/tests/control_plane_ts/todo_context_projection.test.ts @@ -197,6 +197,19 @@ test("summary display matches pre-cutover synthetic CLI characterization", () => } }); +test("one complete source projects all existing views without changing their oracle", () => { + const source = {source: {record: structuredClone(displayFixture.source)}, handoff_gates: []}; + const before = structuredClone(source); + const result = projectTodoContextPage({summary_display_views: source}); + assert.deepEqual(result.summary, displayFixture.summary); + assert.deepEqual(result.planning, displayFixture.planning); + assert.deepEqual(result.terminal, displayFixture.terminal); + assert.deepEqual(source, before); + assert.throws(() => projectTodoContextPage({summary_display_views: source, summary_display: {}})); + assert.throws(() => projectTodoContextPage({summary_display_views: {...source, include_terminal: true}})); + assert.throws(() => projectTodoContextPage({summary_display_views: {source: null}})); +}); + test("handoff preserves identity and nested precedence without granting completion", () => { const item = {todo_id: "todo_original", role: "agent", status: "open", claimed_by: "Agent A", text: "Read the complete requirement", note: "Keep the unfinished validation", evidence: "opaque evidence", diff --git a/tests/presentation/test_linked_goal_sources.py b/tests/presentation/test_linked_goal_sources.py index fe72d96c35..d568779ad5 100644 --- a/tests/presentation/test_linked_goal_sources.py +++ b/tests/presentation/test_linked_goal_sources.py @@ -56,7 +56,7 @@ def native_source(w, name='original', task_text='Check the original question aga append_explore_result_event(log, build_explore_node_event(goal_id='solver-goal', node_id=name, node_kind='question', title=name, summary='Original native record', recorded_at=clock)) append_explore_result_event(log, build_explore_edge_event(goal_id='solver-goal', - from_node='attempt', to_node='question', edge_type='depends_on')) + from_node='attempt', to_node='question', edge_type='depends_on', recorded_at=clock)) raw = {'id': name, 'label': 'Original experiment', 'connection': {'kind': 'local'}, 'environment': {'kind': 'host'}, 'scope': {'goal_id': 'solver-goal'}, 'registry': str(registry), 'runtime_root': str(runtime), @@ -230,6 +230,44 @@ def test_source_failure_preserves_links_peers_and_native_recovery(workspace): assert not w['count'].exists() +def test_research_reads_pin_code_revision_without_retaining_source_authority(workspace, monkeypatch): + from loopx.control_plane import effect_runtime + + raw, request, before = native_source(workspace) + request('/api/chat/goal-sources', {'parent_goal_id': 'research-owner', 'source': raw}) + query = urlencode({'goal_id': 'solver-goal', 'source_namespace': GoalSource.parse(raw).namespace}) + fingerprint = effect_runtime._runtime_fingerprint + qualifications = [] + + def qualify(): + revision = fingerprint() + qualifications.append(revision) + return revision + + monkeypatch.setattr(effect_runtime, '_runtime_fingerprint', qualify) + + def read(suffix): + qualifications.clear() + value = request('/api/chat/explore-results?' + query + suffix) + assert len(qualifications) == 1 + return value + + assert read('&view=history')['total'] == 3 + log = explore_result_log_path(Path(raw['runtime_root']), 'solver-goal') + append_explore_result_event(log, build_explore_node_event(goal_id='solver-goal', + node_id='new-evidence', title='New source observation', recorded_at='2026-01-04T00:15:00Z')) + assert any(row.get('title') == 'New source observation' for row in read('&view=history')['items']) + assert read('&view=history-timing')['last_recorded_at'] == '2026-01-04T00:15:00Z' + source_registry = Path(raw['registry']) + source_registry.unlink() + with pytest.raises(HTTPError) as unavailable: + read('&view=history-timing') + assert unavailable.value.code == 503 + source_registry.write_bytes(before[source_registry]) + assert read('&view=history-timing')['last_recorded_at'] == '2026-01-04T00:15:00Z' + assert not workspace['count'].exists() + + def test_original_task_detail_cannot_follow_a_replaced_source(workspace): raw, request, _ = native_source(workspace) request('/api/chat/goal-sources', {'parent_goal_id': 'research-owner', 'source': raw})