From 7b41b856a7630a5f7085d3971e0baf369f08024f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:56:11 +0800 Subject: [PATCH 01/37] refactor(turn): compose one complete governed input across callers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 635 ++++++++++++++- ...ller-executor-unified-contract-v0.zh-CN.md | 445 ++++++++++- docs/reference/protocols/loopx-turn-v0.md | 42 + .../quota-cli-hot-path-compaction-v0.md | 70 +- docs/reference/required-work-context.md | 39 + loopx/ark_managed_agent_host.py | 2 +- loopx/cli_commands/quota.py | 73 +- loopx/cli_commands/quota_capture.py | 76 +- loopx/cli_commands/quota_registration.py | 7 +- loopx/cli_commands/turn.py | 19 +- loopx/cli_commands/turn_decision.py | 16 +- loopx/cli_commands/turn_inspection.py | 4 + loopx/cli_commands/turn_run_once.py | 26 +- loopx/cli_commands/turn_todo_writeback.py | 65 +- loopx/control_plane/goals/acceptance.py | 2 + .../goals/acceptance_authority.ts | 18 +- .../goals/outcome_requirements.ts | 103 +++ loopx/control_plane/heartbeat/task_body.py | 57 +- loopx/control_plane/quota/cli_projection.py | 188 +---- loopx/control_plane/quota/context_delivery.ts | 12 +- loopx/control_plane/quota/turn_envelope.py | 24 +- loopx/control_plane/quota/turn_envelope.ts | 723 ++++++++++++++++-- loopx/control_plane/turn_driver/codex_cli.py | 34 +- .../turn_driver/codex_context.py | 42 +- .../turn_driver/codex_sessions.py | 17 +- .../turn_driver/decision_capture.py | 25 +- loopx/control_plane/turn_driver/driver.py | 93 ++- loopx/control_plane/turn_driver/executor.py | 76 +- .../turn_driver/host_candidate.py | 145 +++- .../work_items/context_readback.py | 6 +- .../work_items/interaction_contract.ts | 70 +- .../presentation/renderers/quota_markdown.py | 4 + .../renderers/turn_envelope_markdown.py | 4 + packages/loopx-ark-turn/tests/test_host.py | 59 ++ packages/loopx-memt-turn/README.md | 20 +- packages/loopx-memt-turn/tests/test_host.py | 53 ++ scripts/external_scheduler_worker.py | 8 + scripts/qualify-native-goal-release.py | 1 + .../test_codex_context_capture.py | 70 +- .../test_codex_context_lifecycle.py | 33 + .../test_complete_turn_decision.py | 713 +++++++++++++++++ .../test_context_capture_read.py | 206 +++++ .../test_context_delivery_cli.py | 8 +- .../test_effective_turn_replan.py | 9 +- .../test_quota_acceptance_references.py | 125 ++- .../test_quota_authoring_references.py | 173 +++++ .../test_quota_cli_projection.py | 75 +- .../test_quota_context_reference_owner.py | 23 +- .../test_quota_decision_capture.py | 37 +- .../test_quota_explore_guidance_references.py | 135 ++++ .../test_quota_identity_precondition.py | 14 +- .../test_quota_rejected_decision_contract.py | 318 ++++++++ .../test_quota_settlement_cli.py | 4 +- .../test_quota_task_title_references.py | 122 +++ .../test_quota_vision_audit_references.py | 370 +++++++++ .../control_plane/test_replan_staged_read.py | 150 +++- .../test_reward_memory_host_isolation.py | 18 +- .../test_selected_work_requirements.py | 47 +- .../test_turn_completion_source.py | 17 + .../test_turn_envelope_capability_facts.py | 19 + .../test_turn_envelope_settlement.py | 3 +- .../test_turn_host_context_delivery.py | 135 ++-- .../test_turn_host_decision_capture.py | 505 +++++++++++- .../test_work_acceptance_context.py | 166 +++- .../test_work_context_host_delivery.py | 10 +- .../goal_outcome_refinement.test.ts | 233 +++++- .../interaction_contract.test.ts | 26 + tests/control_plane_ts/turn_envelope.test.ts | 113 +++ tests/test_ark_managed_agent_host.py | 2 +- tests/test_loopx_turn_driver.py | 37 +- tests/test_turn_envelope.py | 8 +- 71 files changed, 6470 insertions(+), 757 deletions(-) create mode 100644 tests/control_plane/test_complete_turn_decision.py create mode 100644 tests/control_plane/test_quota_authoring_references.py create mode 100644 tests/control_plane/test_quota_explore_guidance_references.py create mode 100644 tests/control_plane/test_quota_rejected_decision_contract.py create mode 100644 tests/control_plane/test_quota_task_title_references.py create mode 100644 tests/control_plane/test_quota_vision_audit_references.py diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 04c6afa8d2..a9635ed296 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -381,6 +381,170 @@ current source validity and authority; recovering an old capture cannot revive cancelled work. The executor's task conversation is a separate consumer of semantic material, not a relabeled delta of the controller's Core packet. +For cold delivery, use the current Agent's outcomes as the default expanded +scope. Other Agents' independent outcomes remain discoverable in an index with +identity, owner, objective, revision, work links, unmet requirement identities, +current review/check status and exact captured detail references. Global Goal +intent, shared requirements, permissions and stops always remain in the required +input. Expand cross-Agent dependencies, shared acceptance and material blockers +or conflicting evidence before the affected decision, regardless of ownership. +Goal-wide replanning and final acceptance widen the scope to every affected +outcome; a local delivery slice need not reread unrelated historical evidence. +Ownership is a default, not a permission boundary or sufficient relevance test. +Unbound work, unknown relations or unsupported source shapes retain complete +delivery until the existing typed owner can prove the narrower requirement set. + +The canonical contract still retains complete requirements and evidence. Reuse +the existing exact-capture outcome indexing and staged replan reader as the +starting point for one ordinary/replan/detail pipeline, extending its index with +current decision facts rather than merely outcome counts. Preserve full text +for the required scope; defer supporting provenance and review detail through +integrity-bound reads with explicit ordering. A dependent decision must consume +its required detail first. Keep current conclusions, freshness and unresolved +requirements inline; an Agent review must not become an independent check. +Interface folding alone does not reduce model input. The delivery projection, +required-read owner and host recovery contract must migrate together; existing +captures retain their original complete-read obligations. Missing detail or +uncertain retention selects the original capture's supported recovery, never a +new admission or an assumed read receipt. Do not shorten retained requirements, +prune declaration origins or infer relevance from prose to meet a byte target. + +Qualify this C2 change on the same cold, retained warm and context-loss sources: +default own work, cross-Agent dependency/constraint, unbound work, changed peer +evidence, broad replan and Goal acceptance. Count actual model input, paging and +detail round trips, recovery, latency and complete outcome cost. A smaller first +page is not a benefit when required follow-up reads or missed constraints erase +the saving. This is proposed delivery behavior; the current complete-source +reader remains authoritative until replacement and rollback are qualified. + +The first reader prerequisite now has an actual ordinary CLI caller. With the +original capture file and its byte SHA-256, `quota context-read --outcome-ref` +pages an outcome index; supplying the exact returned reference pages that whole +outcome. Ordinary and staged replan reads use the same TypeScript registry and +index owner. The index includes owner/work and groups requirement identities by +identical current check/planning/review facts; missing facts remain unknown. +Equal bodies share a reference, while changed same-id revisions remain separate. +Full detail retains original descriptions, origins, review evidence and unknown +fields. Every page verifies the original file again. The returned continuation +also binds the selected JSON byte digest, refusing changed renderings under the +same original-file hash. Its cursor addresses the selected JSON, and it cannot +establish full-capture retention. This is a read +prerequisite, not the default own-Agent cutover: original full-read obligations, +host continuity and Goal authority remain unchanged. + +The existing canonical acceptance read also supplies a versioned candidate +reading plan from the same complete work snapshot, without another authority +read or RPC. It includes the Agent's outcomes, selected work, transitive typed +dependencies/lineage, shared work/check scope, exact declared sources or review +evidence, and failed/stale observations with their related work. Unbound work, +unavailable relationships/lifetime and Goal-wide checks retain all outcomes. +The captured index labels `required_in_scope` only when the plan matches the +Goal/Agent/Todo, provider revision and complete acceptance-source digest. Changed +oracle facts, additional carrier versions or unsupported plans widen to full. +The original full-capture reader still returns `scope_applied=false`: its index +does not change that capture's full-read obligations. Ordinary acceptance +inspection retains its existing complete format. + +The opt-in native transport now applies that plan jointly to required reads, +delivery and recovery. `outcome_context_projection_v0` identifies a derived +model presentation, never a partial canonical decision or another decision +owner. The original complete capture stays immutable and digest-bound. The +presentation expands all required outcomes and keeps every global field, plus +the complete current outcome index and exact original-source references. +Unknown optional fields, unresolved acceptance reads or unrecognized source +guidance retain full delivery. The existing interaction owner changes only its +exact fulfilled outcome-reading instruction; objective adoption, original and +cumulative requirements, work reconciliation, independent-check distinctions, +freshness, permissions and stops retain their obligations. No prose relevance +classifier is introduced. Registered relation closure still cannot prove +arbitrary semantic independence: read complete optional detail before relying +on it, and widen to the original full source when relevance is unknown or a +Goal-wide decision is needed. + +The native candidate and compaction recovery point to the exact required model +artifact, rather than the machine's full canonical source. The existing delta +codec therefore delivers a newly required body's actual text; it cannot borrow +an unread optional body from a hidden baseline. The current full-source recovery +reference is published before optional scope composition, then replaced with +the exact required artifact before encoding. An interruption during composition +therefore cannot leave the preceding capture selected for recovery. Full page +coverage remains an +explicit native-hook continuity assumption, not model-retention proof. Optional +detail resolves through the same original-source registry, revalidating the +file and presentation; it does not acknowledge full-capture retention. Ordinary +transport-off and old captures retain their original behavior. Other Host +adapters, installed App adoption, actual model decisions and matched +accepted-outcome/full-cost qualification remain open. + +On one 24-outcome / 253-requirement synthetic snapshot, 61 alternating warm +real-provider owner calls against the prior implementation measured File median +5.890→6.317 ms and SQLite 2.239→2.521 ms; p95 was 15.267→17.580 and +6.504→9.412 ms respectively. All prior response fields were identical; plan +metadata added 405/415 serialized bytes. This isolates the acceptance owner in +one TypeScript process, excluding bridge, CLI, cold startup and model costs. +That foundation added cost before the native scope cutover; it was not an +efficiency win. The native presentation additionally allocates an exact required +artifact and calls the same typed owner once before the existing codec. These +costs must be included with page, detail and recovery costs; a smaller first +input does not establish a net benefit. + +The native slice passes 406 real-entry Python tests (five explicit native/SDK +cases unqualified), 169 TypeScript tests and four actual Codex +`0.163.0-alpha.11.1` deterministic-loopback checks. The latter inspect actual +model-request bytes for cold/full, retained delta and same-capture recovery; +they do not establish free-form model behavior. Four new File/SQLite scope +assertions fail at `1ac29089f`. An integration recovery-reference failure was +repaired by replacing metadata only for explicitly scoped artifacts, preserving +ordinary original-file byte identity. +An interruption regression first failed with the preceding capture still +selected; publishing current recovery before the optional scope call fixes it. +Standard premerge still fails two checks also reproduced on `8b37000b3`: +the existing oversized `run_loopx_turn_once` decision function and stale +`turn_route` producer coverage after the earlier owner convergence. The native +increment does not change those files; they remain C1 qualification gaps, +not passing checks or exceptions. + +Final measurements include the recovery-publication fix at runtime commit +`e1c69289a`. Against immutable `8b37000b3` (the preceding C1 plus the same target-main large +page profile), three warmed cohorts per side supply 21 samples per provider and +phase on a 24-outcome / 253-requirement source. The warm case adds a real typed +dependency and verifies delivery of its previously unread body. Complete +serialized page output for File cold/warm/recovery is +105,380/40,473/103,372→37,908/33,495/38,594 bytes. Median latency is: + +| Phase | File, before→after ms | SQLite, before→after ms | +| --- | ---: | ---: | +| Cold full input | 72.856→48.749 | 67.237→46.477 | +| Warm scope expansion | 38.744→51.944 | 35.953→52.165 | +| Lost-context full recovery | 109.126→61.425 | 109.725→56.777 | +| Index plus one optional complete detail | 96.918→108.557 | 92.460→109.251 | + +The additional scope RPC remains: warm bridge calls rise from two to three. +File warm artifact files total 131,020→160,192 serialized bytes; the index/detail +operation still uses two calls and now revalidates the derived presentation. +Cold and recovery use one page instead of two. These measurements cover Python +native projection, pages and lifecycle with warmed runtime; machine load was +not controlled. They exclude initial acceptance collection, process startup, +child CPU, model/cache tokens, billing, human/setup work and accepted outcomes. +Cold/recovery transport improves while warm and detail medians regress. This +is not the original-quota comparison or a positive complete-cost verdict. + +Small sources provide a counterexample to size savings: four real-entry +File/SQLite fixtures with two outcomes and four requirements grow from +36,293–40,548 to 37,948–42,203 bytes, exactly 1,655 bytes each. Index and scope +metadata exceed their short optional bodies. The proposal does not derive a +semantic omission rule or production size threshold from the larger fixture. + +On the same 24-outcome / 253-requirement synthetic source, rendering the former +count-only index shape allocates 8,190 bytes; the staged state groups allocate +18,979 bytes. The old 12 KiB index budget fails. Retaining these decision facts +justifies a separate 20 KiB index ceiling; history, horizon, whole-envelope and +production performance limits stay unchanged. Grouping exact states avoids +repeating the same status fields for each requirement. These are serialized +bytes, not a token/latency/outcome benefit or an immutable base/head comparison. +The source-path scope checks and transport costs above do not qualify general +semantic independence or matched accepted-outcome/full-cost benefit. + ## 6. Alternatives and design choices | Alternative | Disposition | @@ -575,7 +739,7 @@ reject an unsupported inner version before Codex prompt execution or DSH runner loading, without spend. The complete-contract/context/DSH/stdio selection passes 352 cases; TypeScript checking and focused lint pass. This qualifies these interaction semantics, not every required field or packaged-host adoption. -Remaining cache consistency investigation, other old reader expectations, staged instruction parity and +At this historical checkpoint, cache consistency investigation, other old reader expectations, staged instruction parity and actual optional/external consumers, version admission and packaged host continuity remain unfinished. Operation-SDK checks without an explicit native runtime are untested, not qualified. The default writer change remains @@ -583,6 +747,257 @@ unshipped; the PR is Draft for unfinished author work, not merely pending review established. Historical effect decoding is temporary; it must not become a second permanent executable view. +The external scheduler candidate now validates a versioned complete input through +`assert_complete_turn_decision` and the existing TypeScript complete-reader owner +before reading cadence or wake permission. This is a pure reader: it neither +recomposes the captured host binding nor creates a legacy executor view. Missing +bindings, unsupported versions/required semantics, altered content and invalid +same-capture references fail before the wake command. Unversioned ordinary quota +packets retain their existing compatibility path; retiring that path requires +reconciling supported deployed workers. File/SQLite source-CLI-to-worker tests use +local child markers, not model execution. This qualifies that input boundary, +not attached-host task delivery, settlement or accepted-outcome/full-cost benefit. + +The optional Ark adapter reuses `extract_turn_authority` / `render_prompt` from +that same host-input owner; no optional-package contract builder is needed. +File/SQLite `turn plan --host generic-cli --iteration-context fresh` now feeds +its complete request through the real Ark SDK with a synthetic HTTP transport. +Checks compare the actual user-message contract with the original CLI source, +recover a persisted running receipt without a new input, replay completion +without provider calls, and reject altered source/version/binding or explicitly null +complete input before provider or receipt creation. Core state and spend remain +unchanged. Historical Ark request and recovery tests remain. This qualifies the +SDK wire boundary, not a deployed cloud session, model outcomes or Core settlement. +Ark and MemT now share the real File/SQLite CLI request fixture. MemT rejects +that complete input before provider IO because the executable legacy registry +supplies no exact `goal_ref`. The existing `source_session_v1` profile carries +an exact lifetime but is lifecycle-only: the real generic Turn planning entrypoint +rejects it before capture or execution. Supplying a synthetic reference after +planning would hide this producer/consumer gap. Complete-input corruption also +rejects before provider or receipt creation; historical MemT SDK/snapshot tests +remain, but they do not qualify end-to-end complete-input delivery. The owning +change on the MemT path must carry an admitted exact lifetime from an executable registry +through the shared Turn request and recovery owners, preserving recreation and +stale-instance rejection. Do not relax MemT's isolation or enable the +lifecycle-only profile as an adapter workaround. This is a demonstrated adoption +gap, not a qualified native-memory path. The operation-SDK native runtime and +packaged App adoption also remain unqualified. The internal effect envelope and historical request decoder +still serve settlement/result/session callers; remove them only after those +callers and outstanding receipts use the replacement, not merely because model +input no longer contains the old envelope. + +New Codex/DSH requests now read their shared dispatch session lineage from the +complete contract, reusing the TS complete-input validator and adaptive primary +work owner. Model-input validation uses the same owner and rejects mismatched +Goal/Agent/Todo identities between complete input and the internal effect adapter, +even when both digests are independently valid. The complete path no longer +repeats Python version/signature validation; historical requests keep their +original decoder, persisted session schema, path and scope. Real File/SQLite CLI +capture, failed-attempt recovery and once-only settlement replay pass. Ordinary +and adaptive work restore existing Codex/DSH sessions under Todo and Agent +scopes. The same fixture preserves legal baseline paths but fails the added +identity-conflict cases before migration and passes on the candidate. Native +operation SDK environment tests still include skips; its runtime is not qualified. +Session identity migration does not retire all result qualification and settlement +readers of the internal envelope. Full retirement and net-cost +benefit remain unproven. The MemT lifetime gap blocks its dependent path only, +not these independent migrations. + +Planning and failed-attempt session lookup now pass the original complete contract +through the same identity owner, including journal inspection. An omitted contract +still decodes historical records; an explicitly null contract rejects. Live and +cached vision-result normalization also use this shared identity read instead of +rebuilding Goal/Agent from the effect adapter. Session scope/lifetime fences and +the native app-server callback remain with their existing owners. This removes +those identity-only reads, not the full internal adapter: execution routing, +result evidence/profile interpretation and settlement still require migration. +Adaptive recovery fixtures are composed before signing: historical unverified +selections preserve primary-Todo writeback, while a conflicting verified task +source rejects before host execution or quota spend. The source fence is retained. +Completion-recovery qualification checks the original durable completion and spend +receipts without resubmitting completion. Four obsolete replay-call assertions +also failed on the prior baseline; their repaired oracle retains changed-task-text +and historical pre-source acceptance cases on real File/SQLite stores. + +Completion task-body resolution now lives with the existing TypeScript interaction +source owner. New `run-once` requests validate the complete capture against the +internal adapter, then resolve and hash the complete contract's admitted source; +they no longer reconstruct that source from the adapter. Historical journals use +the same resolver through a thin Python transport. Exact Unicode/whitespace bytes, +missing/ambiguous-source refusals and accepted-receipt recovery remain covered. +This retires one Python semantic algorithm without introducing a second packet +view. The old 48-case File/SQLite capture/completion selection passes at +`599cf1f72`; candidate complete-source checks also reject altered bindings and +explicit null inputs. This adds a TypeScript boundary call where the historical +resolver was local Python; net full-path cost remains unmeasured. Result profiles, +evidence interpretation and other settlement consumers still need migration. + +The Codex result schema and shared candidate adapter now use one checked +complete-contract profile read for evidence applicability. They no longer decide +that requirement from the internal capsule on new requests. The existing +TypeScript integrity/source/lineage validator serves both this read and model +input validation; no new packet view, wire field or per-evidence-field call is +introduced. Omitted contracts retain historical capsule decoding; explicit null +or altered contracts reject. Empty/non-object profiles preserve their previous +non-applicability, stop results still need no evidence, and supplied evidence is +passed unchanged to the existing result validator. This migrates applicability, +not evidence-content validation, execution routing or all settlement consumers. +Each schema/material-candidate read adds one checked boundary call; this is a +semantic convergence step, not an established performance improvement. +In a 31-pair warm-boundary measurement using one real SQLite CLI host request +(45,405 JSON bytes, synthetic provider), schema construction plus material-result +conversion rose from a median 0.013 ms at `68223e337` to 19.6 ms in the candidate +(p95 0.019/22.8 ms); legitimate outputs matched. This local added cost is retained +as negative evidence. It excludes cold startup, model work and the full Turn; +overall outcome/cost qualification remains open. Checks cover real File/SQLite +capture/recovery plus Codex/shared-host consumers; they do not prove packaged App +adoption or a model-outcome improvement. + +The execution entry now checks complete-contract identity once before dispatch +or journal recovery. Settlement-plan fallback uses that checked identity, and +the recovery assessment reuses it within the same invocation instead of reading +the adapter's Agent again. Existing settlement plans and receipts are never +rebuilt, and omitted historical contracts retain their exact decoder. The value +is not cached across host attempts, source replacement or later invocations. +This also closes a recovery bypass: an existing settlement plan previously +skipped source validation, allowing a null/altered complete request to return an +existing journal outcome. On real File/SQLite CLI journals the new refusals leave +journal bytes and spend counts unchanged and invoke no host/settlement callbacks. +Ten targeted cases fail the new refusal assertions at `ce996ea0f`; the candidate's +32 capture/recovery cases and broader 605-case regression pass (overlapping). +The entry adds one checked boundary call, reused for both consumers. On the same +45,405-byte real SQLite CLI request, 31 alternating warm samples of the existing +settlement-plan read plus recovery identity preserve legitimate output parity: +median 0.016 to 12.382 ms, p95 0.024 to 14.883 ms. This is added local validation +cost, not an efficiency win; it excludes cold startup, full Turn and model costs. +Full-path performance and remaining routing/settlement retirement stay open. + +Codex CLI prompt construction now delegates valid complete requests directly to +that existing complete validator, removing its redundant standalone envelope +version call. Historical requests retain their version check; a failed complete +validation still checks the version to preserve diagnostic precedence when both +inputs are malformed. No validation cache, new wire field or second owner is +introduced, and prompt text is unchanged. This is a call-boundary consolidation, +not retirement of the remaining driver routing or settlement adapter. + +Against `e1dcd95dd`, complete and historical prompt bytes match, and six negative +input cases retain the same refusal diagnostics. On one 45,429-byte real SQLite +CLI request producing a 34,043-byte prompt, 61 alternating warm samples reduce +median prompt construction from 24.229 to 14.819 ms and p95 from 30.189 to +17.854 ms; bridge tracing confirms two calls become one. This local reduction +is about 39%, excluding cold startup, model execution and full-Turn cost. It does +not cancel the separately measured profile/settlement overhead or qualify the +original-quota outcome comparison. An earlier pre-diagnostic-refinement sample +was 21.279/12.994 ms median and 29.729/17.321 ms p95; retain it as earlier evidence, +not a second independent final qualification. + +Planning now receives the complete contract before route/session/transaction +construction, rather than attaching it after the plan is built. Planning and +failed-session reconciliation share the existing TypeScript complete-source +validator through one coarse lineage operation. It selects Goal/Agent/work +identity and returns the action-signature preimage in the same boundary call; +it replaces the previous signature-only call, without a per-field RPC or cache. +Explicit null, unsupported or altered complete contracts reject, including an +already-resumed request. Omitted historical contracts keep their exact decoder. +The receipt owner still supplies Todo-less replan identity. The route and launch +increments below describe the subsequent consumer migration. + +The Python JSON/hash transport codec remains deliberately unchanged. A direct +JavaScript hash trial failed numeric/exponent preimage parity and would have +changed persisted Turn keys; File/SQLite characterization now protects those +values, Unicode, adaptive work selection and resume idempotency. On one real +SQLite CLI request (45,380 JSON bytes), 31 alternating warm samples against +`088867421` preserve complete/historical plan and failed-session output parity. +Planning median/p95 changes from 39.632/47.964 to 42.608/53.270 ms; recovery changes +from 12.330/20.858 to 18.136/23.242 ms. These are local added validation costs, +not a full-Turn or original-quota outcome comparison. No model, installed App +or PostgreSQL authority qualification is implied. Remaining route/material +adapter retirement and end-to-end cost qualification keep C1 open. + +The live Host launch recheck now composes the receipt-bound live observation +through the same complete-contract owner as initial planning, then supplies that +contract to the shared planner. This replaces its raw-quota-to-historical-plan +shortcut. Stop and changed-action refusals still precede composition; the live +observation neither replaces the original captured model input nor mints another +receipt. Remaining effect/material adapter retirement is still migration work. + +The route classifier now lives in the existing TypeScript envelope owner. The +explicit registered action/frontier table is exhaustive at compile time; unknown +historical actions still reach the Host. Signature refusal, attempt, replan before +ordinary-delivery gating, capability intent identity, repair, user action and +quiet wait keep their ordering. Python retains a thin controller transport and +historical scalar-text encoding, not a second classification table. Complete +validation ignores historical decoding hints. The persisted Python action-hash +codec and historical selected-work reader are unchanged. + +Planning receives the route in its existing coarse lineage/signature call; +complete and historical ordinary plans still use two bridge calls. Against the +previous Python classifier, 1,152 action/gate combinations agree; complete and +historical plan outputs also agree on a fixed real SQLite CLI fixture. These +planner comparisons use the same current TypeScript endpoint and therefore are +not an exact old/head end-to-end latency experiment. The standalone controller +route now needs one bridge call: 61 alternating warm samples on a 44,891-byte +request increase median/p95 from 0.004792/0.010125 to 13.252792/15.855042 ms. +That is a measured regression for this caller, not an efficiency gain. It remains +an explicit cost gap for controller boundary consolidation; no cache or duplicate +classifier is introduced to hide it. File/SQLite entry, recovery and once-only +settlement qualification, remaining adapter retirement, packaged adoption and +original-quota outcome/full-cost qualification remain separate acceptance work. + +The production `turn managed-step` caller now combines envelope construction and +that same route evaluation in one TypeScript request. The Python adapter returns +a local immutable envelope snapshot paired with its route; this is neither a new +agent wire layout nor a second decision owner. Decoding or editing a returned +dictionary cannot change that snapshot or attach its qualification to another +wire dictionary. The controller retains its existing user-action precedence, +receipt/predecessor checks and journal-owned retry budget. Plain historical or +external envelopes still use the existing route reader and signature refusal. +The construction adapter shares host-binding and capture-argument preparation +with the ordinary envelope builder; the managed caller's redundant intermediate +builder has been removed. The standalone-reader regression above remains a +historical measurement; only the fresh production construction/consumption pair +can avoid that extra request. Remaining selected-work/effect/material decoding, +large cold-context recovery and packaged/outcome qualification remain open. +The local snapshot retires when this caller consumes the unified typed +controller result directly; it adds no persisted schema or compatibility wire. + +On one fixed 31,088-byte synthetic source, 61 alternating warm samples compare +the previous Python producer/controller functions at `ddf96b79c` with the fused +call, using the same current TypeScript server and unchanged build/route rules. +The final-source component median/p95 is 22.477/25.417 versus 13.739/17.479 ms. A separate +public `managed-step` CLI comparison uses real isolated File/SQLite stores and +21 alternating warm samples per arm/provider: median is 135.989 versus 137.316 ms +for File and 149.310 versus 146.081 ms for SQLite; p95 is 209.730 versus 192.544 ms +and 222.299 versus 240.848 ms respectively. A separate transport-count probe +observes 70 versus 69 total bridge requests on each provider; this removes one +route request, not the other live-decision calls. Complete outputs agree, the failed +journal remains byte-identical and neither arm spends quota. Request count +improves; mixed whole-CLI medians and tails do not establish a speedup. Small local samples and +shared host resources limit tail inference. These are source-bound observations, +not an entire immutable old-tree comparison, cold-start, installed App, model +outcome or original-quota full-cost qualification. + +Complete readers now also compare the adapter's signed action document with the +existing producer projection of the admitted complete source. Independent source +and adapter hashes previously proved only their separate integrity: a rehashed +adapter could retain Goal/Agent/Todo while changing execution or writeback facts. +The shared check covers the producer's existing signature dimensions without a +second field list, new RPC or wire view. Historical-only inputs keep their old +reader. Existing complete requests whose profile adapter disagrees with their +source now reject instead of silently reading the source profile; this deliberate +refusal change preserves the historical profile behavior separately. + +Against `b9e2c5120`, File/SQLite planning refusal assertions fail before the fix. +Six independently rehashed variants cover run eligibility, effective action, +delivery permission, user intervention, execution policy and spend permission; +the candidate rejects them before host/writeback/spend callbacks and leaves no +Turn journal or settlement receipt. An existing lane lock can still be allocated. +On a 44,891-byte real SQLite CLI fixture, 101 alternating warm in-process +TypeScript samples preserve legitimate lineage output; median validation changes +from 4.864 to 5.163 ms and p95 from 5.570 to 6.010 ms. These are added local costs, +excluding bridge transport, live composition, cold startup, models and full Turn +cost. They do not qualify the original-quota outcome comparison or C1 cutover. + ### C1 caller replacement and cutover unit The cutover unit is a supported entrypoint **and its downstream consumers**, @@ -618,6 +1033,22 @@ are reconciled. Rollback likewise separates outstanding work from new admission. A version transition does not rewrite saved prompts, journals or source hashes as if they had always used the new contract. +Code convergence is part of each migration slice, not only the final C9 cleanup. +Both entrypoints must call the same semantic implementation for composition, +reference validation, context reconstruction and settlement. Keep genuinely +host-specific launch/observation mechanics in thin adapters; do not force their +different lifecycles into a new general-purpose runner. The existing typed owners +remain responsible for their respective decisions. + +For each slice, record the before/after callsites, shared owner, removed duplicate +algorithm and remaining compatibility caller with its removal condition. Qualify +both routes with the same independently authored semantic cases and their real +transport/recovery tests. A wrapper that only delegates can be transitional; +copying a rule into a newly named common module while leaving two implementations +is not convergence. The next change to a shared rule should be made and tested in +one owner and reach both routes. Do not make declining line counts an acceptance +metric or remove unique host coverage to meet one. + The first C1 PR must include the two production composition callsites above and at least one complete producer-to-final-input journey, with an explicit list of remaining readers. Qualifying that bounded slice does not enable the new writer @@ -721,10 +1152,202 @@ Extra acceptance/Explore reads, actual request framing, cache behavior and outcome differences are unmeasured. The legacy receipt toggle produced identical data and does not add an independent workload. No net-benefit conclusion follows. -The next implementation slice should first qualify exact same-contract sharing -through composition, readers and recovery, then assess deferred-detail choices -with the matched workload above. Keep cold and staged totals separate and retain -negative results; do not force a byte target by dropping unique instructions. +The acceptance-sharing increment in #7430 moves the original Python display +algorithm into the existing TypeScript composition owner, before hashing/capture; +the old display helper now delegates to that same implementation. Complete readers +and historical staged reads use one reference resolver. Equal contracts retain +one full owner; different revisions, JSON types, provenance and negative evidence +remain distinct. Existing references follow relocated owners. Dangling, cyclic +or unavailable references reject even with valid integrity hashes. Small objects +stay inline when a reference would cost more. + +Isolated File/SQLite quota and native-host fixtures cover complete acceptance at +the actual synthetic Codex subprocess input, failed-attempt recovery and original +once-only settlement; shared full/delta/lost-baseline reconstruction is also +exercised. These qualify transport semantics, not model compliance. The legacy +display adapter adds a transport call; complete composition shares inside its +existing call. No latency improvement is claimed. + +A fixed-source acceptance fixture with four identical carriers produces 23,432 +compact UTF-8 JSON bytes for the fused contract versus 22,237 for the original +non-staged CLI (+5.37%). With capture staging, fused bytes are 23,515 versus +24,509 for original entry/review/writeback together, excluding extra material +reads. The original display-sharing helper is frozen at `895f38fc5` for this +comparison. The acceptance-free workload above remains unchanged. This absorbs +an existing quota efficiency into the common path; it does not establish an +advantage over quota or full-cost benefit. The subsequent guidance increment below narrows that gap; next qualify the remaining +vision sharing in its existing owner after the title increment below, before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do +not force a byte target by dropping unique instructions. + +The next #7430 increment shares replan vision-authoring instructions through +that same TypeScript composition boundary. Its retained owner is the existing +durable-writeback settlement step. The legacy Python display helper delegates +sharing and only adds its historical presentation metadata. The complete +reader and historical unstaged/staged adapters resolve the same exact reference; +no second authoring policy is introduced. JSON type, revision or instruction +differences stay inline, and a reference must be smaller. Conflicting inline/ref +values, wrong step kinds, missing targets and chains reject even if hashes match. +The settlement plan, all original instructions and the example remain intact in +the capture; the historical writeback stage retains its existing example omission. + +Two old CLI spellings now expose the same shared complete contract; material +readback, rejection before effects and native input are qualified separately. +The exercised native outer-controller inputs lack an inline settlement authoring +owner, so their authoring +remains inline; sharing never invents that owner or transfers settlement authority. +At the authoring checkpoint, four legacy projection tests expected multiline-title +or Explore-guidance sharing absent from the complete candidate and failed at both +the preceding and authoring heads. The guidance increment below resolves two; +the title increment below resolves the other two. Historical failures remain recorded, not waived or relabeled. +These slices do not qualify installed App adoption. + +For the fixed synthetic authoring fixture, the original non-staged CLI is 22,062 +compact UTF-8 JSON bytes and the fused contract is 23,266 (+5.46%). With capture +staging, original entry/review/writeback total 24,311 versus 23,349 fused bytes. +The original CLI projection module is frozen at `895f38fc5`; inputs and projected +read stages are held fixed. Additional reads, request framing, tokens/cache, +latency and model outcomes remain unmeasured. Sharing eliminates a duplicate +implementation and duplicate instructions in the candidate, but does not prove +an improvement over quota's existing sharing or a positive full-cost diff. + + +The Explore guidance increment removes the Python exact-match loop and reuses the +same TypeScript composition boundary for complete and legacy display callers. +Only identical instructions with an existing durable-writeback Explore attachment +share that same-capture owner when the reference is smaller. Unique/short text and +missing owners stay inline. Invalid, conflicting or chained references reject; +no instruction is shortened and an optional attachment gains no new obligation. +Historical staged and unstaged readers resolve the reference before moving context, +then reuse Explore's existing domain projection. Independent Explore reads still +return full guidance. The compatibility helper retires with the legacy display +callers; it owns no separate decision or sharing algorithm. + +Real isolated File/SQLite CLI captures now satisfy the two previously failing +Explore-sharing cases, including standalone material comparison, historical +readback and invalid-capture refusal without state/spend changes. Existing staged +CLI tests still exercise unavailable-registry reads, tamper refusal and actual +writeback/settlement. Delta reconstruction preserves changed instructions and +lost-baseline recovery; incomplete context falls back to full delivery. These are +transport and local integration results, not installed App or model-outcome proof. + + +Broader caller qualification also migrates old test assumptions to the admitted +complete contract: native subprocess capture/retry compares the entire original +contract, and generated settlement commands still settle only the original Turn. +Historical capability readers retain explicit canonical-fixture initialization, +refusal facts and scheduler information without granting execution authority. +Rejected capability admissions still emit their typed diagnostic/historical +projection, whereas admitted calls emit the complete contract. This remaining +failure-path migration belongs to C1 before full cutover; it is not a permanent +second contract. Synthetic native/stdio providers do not qualify model benefit. + +With the same original projection frozen at `895f38fc5`, the synthetic guidance +fixture uses 22,848 compact UTF-8 JSON bytes on the original non-staged path and +24,320 fused (+6.44%). Captured original entry/review/writeback total 26,081 versus +24,403 fused bytes. The guidance-free fixture remains 20,543 versus 21,738 bytes +non-staged. Required extra reads, framing, tokens/cache, latency and accepted model +outcomes remain unmeasured. Shared code and restored quota information efficiency +do not establish a positive complete-cost diff; C1 remains open. + +The current-Task title increment removes the Python exact-source matching algorithm. +Complete composition and the legacy display delegate now share its TypeScript +owner. Only an exact duplicate of `text` or `title` in the unique, verified, +complete current selected Task can become a same-capture `title_ref`; the full +original declaration and its provenance stay inline. Missing, differing, +incomplete, ambiguous, failed, done or archived sources do not qualify. The +historical character threshold, including Unicode code points, stays unchanged. +The old display-only label-prefix omission remains local and is not applied to +the complete contract. This delegate retires with the legacy display callers. + +Complete readers validate identity and reference ownership again, rejecting +conflicting inline values, chains, malformed paths and unavailable targets even +with valid hashes. Historical envelopes retain the full Task source, without a +relocated dangling lane reference. Real File/SQLite CLI with differential delivery +off now passes the two previous multiline-title cases; captured variants also +verify exact readback and refusal before state mutation or spend. Delta and +lost-baseline/full recovery preserve changed requirements. Existing independent +Todo detail still returns the full declaration. This qualifies local transport +semantics, not installed App adoption or model behavior. + +For a fixed synthetic current-Task fixture, the original non-staged CLI frozen +at `895f38fc5` is 28,585 compact UTF-8 JSON bytes and the fused contract is 29,780 +(+4.18%). Captured original entry/review/writeback total 29,906 versus 29,863 fused +bytes. The no-current-Task control remains 20,543 versus 21,738 non-staged bytes. +These totals exclude request framing, extra material reads, tokens/cache, latency +and accepted outcomes. They restore quota's existing sharing in the common path; +they do not establish positive full-cost benefit. + +The vision-audit increment now moves the nested exact-match algorithm from Python +into that same TypeScript owner. Complete and historical display callers share +only an identical full audit, including unknown facts, negative evidence and +ordered obligations, when its reference is smaller. Different JSON types are not +equal: the old Python `True == 1` comparison could erase a channel difference; +that case now stays inline. Short audits also stay inline. The root retains the +complete audit and judge; legacy display metadata remains in its adapter. + +Complete recovery rejects missing, malformed, chained or conflicting audit/judge +references even with recomputed valid hashes. Existing historical envelopes still +obtain their judge from the inline root, so no moved reference dangles; their +existing projection limits are unchanged. Real isolated File/SQLite CLI captures +with an active prior vision verify the full judge, exact capture readback and +rejection before state mutation or spend, without differential delivery enabled. +Delta/full and lost-baseline recovery retain changed requirements. Fixtures with +no active vision correctly emit no audit and cannot qualify this path. + +With the original CLI projection frozen at `895f38fc5`, the fixed synthetic audit +fixture uses 22,337 non-staged compact UTF-8 JSON bytes versus 23,333 fused (+4.46%). +Captured original entry/review/writeback total 23,658 versus 23,416 fused bytes. +The no-audit control remains 20,543 versus 21,738 non-staged bytes. These are not +tokens/cache, latency, extra reads, framing or accepted outcomes. Restored sharing +and fewer rule owners do not prove positive full-cost benefit. The legacy audit +delegate retires with its display callers; remaining consumer retirement and +packaged/outcome qualification still keep C1 open. + +Rejected admission now uses the same complete composer before immutable capture +when the decision has an interaction contract, including an unhealthy paused +source. A false `ok` still prevents host handoff and native context-baseline +publication and retains the failing exit status. Composition grants no delivery +or settlement authority. Invalid complete sources fail validation before capture. +Errors before a work contract exists retain their original typed diagnostic and +recovery facts, with either CLI flag spelling; they do not acquire a manufactured +work contract or pass through the historical envelope renderer. + +Isolated real File/SQLite CLI checks cover missing work, unavailable capability +and selection-required reentry under both App-heartbeat and generic profiles. +They preserve the original receipt on reentry and create no spend, baseline or +unauthorized state mutation. Capture consumers are being moved to exact complete +readback while keeping explicit historical long-route recovery coverage. This +qualifies local transport and refusal semantics, not installed App adoption or +all-provider/full-path delivery. Remaining consumer retirement and packaged +single-session outcome/cost qualification still keep C1 open. + +Subsequent observation qualification separates two issues. #7464 repairs the +cache test's comparison of independent live observation clocks while retaining +all other facts. #7468 preserves the existing authority owner's typed rejection +and backup/migration recovery in `quota status/plan`, whose early failure return +had hidden that information. Unmigrated ordinary reads remain refused under the +accepted #7032 cutoff; File/SQLite retain their scope-isolation checks. The new +recovery assertions fail on the previous implementation and the corrected +scope/diagnostic/authority selection passes 156 tests. These independently +reviewable fixes are absorbed by the integration candidate, not evidence of +installed adoption or improved outcomes/cost. Earlier failed runs remain +historical evidence; these diagnosed failures are no longer open investigations. + +For an unhealthy-paused synthetic source, the original CLI projection frozen at +`895f38fc5` uses 6,325 compact UTF-8 JSON bytes and the complete contract uses +6,639 (+4.96%). The extra binding preserves common validation; this is no efficiency +win. Tokens/cache, latency, extra reads, framing and accepted outcomes remain +unmeasured for this case. No live model experiment is implied by these checks. + +The shared-owner prerequisite is independently reviewable in #7457. Its +Task-title and Explore operations use one coarse legacy projection call; complete +composition reuses those same functions inside its existing call. The integration +branch absorbs the prerequisite commits rather than carrying a copied owner. +New `should-run` emission accepts the already-composed contract or original +predecision diagnostic only; it no longer falls back to legacy packet/envelope +projection. Historical capture recovery remains explicit. Component measurements +show reduced bridge cost versus two calls, but higher cost than the original +Python projection; this is not evidence of a positive full-path quota comparison. Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, @@ -782,7 +1405,7 @@ are stable design references, not claims that a canonical Todo is complete. | Task / dependency | Owner and concrete replacement | Decisive exit / rollback | | --- | --- | --- | | **C1 — Unified contract; first** | Interaction/work-context + quota/Turn owners: inventory fields and instruction clauses, capture one schema, migrate actual ordinary quota/Turn producers and thin renderer | Single-session real-entry parity with necessary information, action binding and once-only settlement, plus the quota-baseline effectiveness/full-cost comparison in §9; retain version-pinned readers until cutover qualifies | -| **C2 — One context-delivery pipeline; C1** | Context owner + host observers: absorb full/delta/detail/recovery into the canonical contract; remove packet-format branches and duplicate context copies | Same-capture recovery and exact role/session continuity through normal/replan/resume; full fallback without a new admission; rollback encoding to full | +| **C2 — One context-delivery pipeline; C1** | Context owner + host observers: absorb full/delta/detail/recovery into the canonical contract; default to own outcomes plus global constraints and relevant cross-Agent material, with an exact detail index; remove packet-format branches and duplicate context copies | Cold/warm/context-loss and normal/replan/resume preserve required text, cross-Agent adoption and exact role/session continuity at measured complete cost; unsupported scope falls back to full without new admission; rollback encoding to full | | **C3 — Session binding and native lifecycle; C1** | Session/runtime owner: extend existing binding for role/generation; reuse native create/resume/observe/interrupt and ordered queues | Two roles cannot overwrite the Agent native ID, cross-read baselines or race execution; recovery observes existing attempts before retry; disable new role admission to roll back | | **C4 — Shared governed lifecycle; C1** | Effect/Turn owner + outer worker adapters: converge duplicated prepare/dispatch/observe/validate/writeback/settle orchestration around existing typed operations and receipts | App/Turn/external worker agree on admission, original result and single debit under partial failure; scheduler effects retain their true host owner; switch callers back only after receipt reconciliation | | **C5 — Replan and result authoring boundary; C1** | Goal/Vision and interaction owners: expose current facts and typed controller operations; remove ordinary executor obligations to manufacture Core writeback fields in the two-session profile | Fact-first decision preserves default/stride policy differences, all evidence and required validation; invalid proposed writes reject; single-session authoring remains qualified during rollout | diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 9c98c54223..4374ab4310 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -294,6 +294,115 @@ session、role、epoch、schema、source capture 下证明已完整送达并保 已取消工作。Executor 任务对话独立消费语义材料,不是 controller Core packet 换名后的 delta。 +冷态交付默认展开当前 Agent 的成果。其他 Agent 的独立成果保留可发现索引,包含 +身份、owner、目标、revision、工作关联、未满足要求标识、当前 review/check 状态及 +精确 capture 正文引用。全局 Goal 意图、共同要求、权限与停止条件始终进入必读输入。 +跨 Agent 依赖、共同验收,以及影响当前决策的阻塞或矛盾证据,须在相关决策前自动 +展开,不受 owner 限制。Goal 级重规划和整体验收扩大到所有受影响成果;局部交付 +无需重读无关历史证据。Owner 只是默认范围,不是权限边界或充分的相关性判断。 +工作未绑定、关联未知或来源形状不受支持时,保持完整交付,直至既有 typed owner +能够证明较窄的必读范围。 + +Canonical 契约仍保留完整要求和证据。以现有同 capture 成果索引及 replan 分阶段 +reader 为起点,合并普通/replan/detail 管线;索引须补足当前决策事实,不能只有成果 +数量。必读范围保留要求全文,支持性来源和 review 细节通过绑定完整性、顺序明确的 +reader 按需交付;依赖该材料的决策须先读完它。当前结论、新鲜度与未满足要求仍 +内联,Agent review 不能变成独立检查。界面折叠本身不会减少模型输入;交付投影、 +required-read owner 与 host 恢复契约须一起迁移,已有 capture 保留原完整读取义务。 +正文缺失或保留情况不确定时,执行原 capture 支持的恢复,不能重新准入或假定已读。 +不能为字节目标缩短保留要求、裁剪声明来源或靠正文关键词推断相关性。 + +C2 须在相同冷态、已保留温态及上下文丢失来源上验证:默认自己工作、跨 Agent +依赖/约束、未绑定工作、同伴证据变化、广域 replan 和 Goal 验收。计入实际模型输入、 +分页及正文读取往返、恢复、延迟和完整成果成本;首屏变小不能抵消必要补读或漏掉 +约束的成本。这是拟议交付行为,替换与回滚完成资格验证前,仍以当前完整来源 reader +为准。 + +第一项 reader 前置现已有实际普通 CLI 调用方:给定原 capture 文件和文件字节 +SHA-256,`quota context-read --outcome-ref` 分页读取成果索引,附上返回的精确引用 +则分页读取该成果全文。普通与 staged replan 复用同一 TypeScript registry/index +owner;索引保留 owner/工作关联,并将当前 check/planning/review 事实完全相同的 +要求标识分组,缺失事实保持未知。相同正文共享引用,同 id 的变化版本仍分别保留。 +详情保留原描述、声明来源、review 证据与未知字段。每页重新核验原文件,返回的 +接续命令还绑定所选 JSON 字节 digest,拒绝在原文件 hash 未变时混合变化的渲染。 +游标仅定位所选 JSON,不能证明完整 capture 已保留。这是读取前置,尚未切换默认自己 +Agent:原完整读取义务、Host 连续性和 Goal 权限保持不变。 + +既有 canonical acceptance 读取还从同一份完整工作快照提供版本化的候选读取计划, +不增加 authority 读取或 RPC。计划包含自己的成果、选定工作、传递的 typed 依赖/ +lineage、共同工作/check scope、精确声明来源或 review 证据,以及失败/过期观察及其 +相关工作。工作未绑定、关系/生命周期不可用或 Goal 全局检查均保留全部成果。 +捕获索引仅在计划与 Goal/Agent/Todo、provider revision 和完整验收来源 digest +一致时标记 `required_in_scope`;oracle 事实变化、额外载体版本或不支持的计划均 +扩大为完整读取。原完整 capture reader 仍返回 `scope_applied=false`:索引不改变 +该 capture 的完整读取义务;普通 acceptance inspect 保持原完整格式。 + +可选 native transport 现在将计划同时接入 required-read、交付与恢复。 +`outcome_context_projection_v0` 只标识派生的模型展示,不是部分 canonical decision +或新的决策 owner。原完整 capture 保持不可变和 digest 绑定;展示展开全部必读成果, +保留所有全局字段、完整当前成果索引及精确原来源引用。未知可选字段、未解决的 +acceptance read 或不认识的来源指导均保持完整交付。既有 interaction owner 仅替换 +自己精确生成、已履行的成果读取指导;目标采用、原始和累计要求、工作核对、独立检查 +区别、新鲜度、权限及停止义务保留,不引入自然语言相关性分类器。注册关系闭包仍 +不能证明任意语义独立性:依赖可选成果前先读完整正文,相关性未知或需要 Goal 全局 +决定时扩大为原完整来源。 + +native candidate 与压缩恢复指向真正交付的必读模型产物,而不是机器保存的完整 +canonical 来源。既有 delta codec 因而会交付新增必读正文,不能从隐藏 baseline 借用 +模型未读过的可选正文。当前完整来源的恢复引用先于可选 scope 组合发布,再在编码前 +替换为精确必读产物;组合期间中断不能让恢复仍选中前一次 capture。完整分页覆盖仍只是 +显式 native-hook 连续性假设,不证明模型 +保留。可选详情复用同一原来源 registry,重验文件和派生展示,不确认完整 capture +保留。transport 关闭和旧 capture 保持原行为;其他 Host adapter、正式 App 采用、 +实际模型决定及匹配验收成果/完整成本仍未资格。 + +同一 24 成果/253 要求合成快照,以 61 组交替温态真实 provider owner 调用对照 +旧实现:File 中位数 5.890→6.317 ms,SQLite 为 2.239→2.521 ms;p95 分别为 +15.267→17.580 和 6.504→9.412 ms。原响应字段全等,计划元数据增加 405/415 个 +序列化字节。此对照仅隔离同一 TypeScript 进程的 acceptance owner,不含 bridge、 +CLI、冷启动或模型成本;此基础在 native 范围切换之前增加成本,不是效率提升。 +native 展示还增加精确必读产物及既有 codec 之前的一次 typed owner 调用,必须与 +分页、补读和恢复成本一起计入;首包缩小不证明净收益。 + +native 切片通过 406 项真实入口 Python(五项显式 native/SDK 案例未资格)、169 项 +TypeScript 和四项真实 Codex `0.163.0-alpha.11.1` 静态 loopback 检查。后者核实际 +模型请求中的冷态全文、保留差分与同 capture 恢复字节,不证明自由生成模型行为。 +四个新增 File/SQLite 范围断言在 `1ac29089f` 失败。集成中发现的恢复引用回归已修复: +只为显式 scoped 产物替换 metadata,普通恢复保留原文件字节身份。 +中断回归最初因仍选中前次 capture 而失败;将当前恢复发布前移到可选 scope 调用之前 +后通过。标准 premerge 仍有两项在 `8b37000b3` 同样复现的失败:既有 +`run_loopx_turn_once` 决策函数过大,以及前序 owner 收敛后 `turn_route` 的 producer +覆盖过期。本 native 增量未修改这些文件;它们仍是 C1 资格缺口,不是通过或例外。 + +最终运行源码 `e1c69289a` 的成本含上述恢复发布顺序修复;对照不可变 `8b37000b3`(前序 C1 与相同 target-main 大页配置),24 成果/253 要求 +来源上,每侧三组温态 cohort、每 provider/阶段 21 样本。温态新增真实 typed 依赖并核 +先前未读正文的交付。File 冷态/温态/恢复的完整分页序列化输出为 +105,380/40,473/103,372→37,908/33,495/38,594 字节。延迟中位数: + +| 阶段 | File,原→新 ms | SQLite,原→新 ms | +| --- | ---: | ---: | +| 冷态全文输入 | 72.856→48.749 | 67.237→46.477 | +| 温态范围扩大 | 38.744→51.944 | 35.953→52.165 | +| 上下文丢失后全文恢复 | 109.126→61.425 | 109.725→56.777 | +| 索引加一项可选全文 | 96.918→108.557 | 92.460→109.251 | + +新增 scope RPC 仍在,温态 bridge 调用由二增三;File 温态产物文件共 +131,020→160,192 个序列化字节。索引/详情仍为两调用,但增加派生展示重验;冷态和恢复由两页 +变一页。测量含温态 runtime 下 Python native 投影、分页与生命周期,未控制机器负载; +不含首次验收收集、进程启动、子进程 CPU、模型/缓存 tokens、账单、人工/实施及验收 +成果。冷态/恢复传输改善,温态及详情中位数退化;这不是原 quota 对照或完整成本正向结论。 + +小来源给出体积节省的反例:四个真实入口 File/SQLite 夹具各含两成果、四要求, +由 36,293–40,548 增至 37,948–42,203 字节,每项恰好增加 1,655 字节,索引和范围 +元数据超过短可选正文。提案不从大夹具推导语义省略规则或生产体积阈值。 + +同一 24 成果/253 要求的合成来源,按旧的仅数量索引形状渲染分配 8,190 字节, +staged 状态分组分配 18,979 字节,原 12 KiB 索引预算失败。保留这些当前决策事实, +据此将独立索引上限调整为 20 KiB;history、horizon、完整 envelope 与生产性能 +限额均不变。完全相同状态分组避免逐要求重复状态字段。这里只测序列化字节, +不是 token/延迟/成果收益或不可变 base/head 对照。上述源码范围检查和传输成本不证明 +一般语义独立性或匹配验收成果/完整成本收益。 + ## 6. 备选方案与取舍 | 备选方案 | 结论 | @@ -433,12 +542,194 @@ reader 覆盖仍保留。八项聚焦用例通过,使用注入的记录 runner File/SQLite 原生输入检查确认不支持的内层版本在 Codex prompt 执行或 DSH runner 加载前拒绝,且不扣额。完整契约/context/DSH/stdio 集合 352 项通过,TypeScript 检查及聚焦 lint 通过。这只证明上述交互语义,不代表所有必需字段或打包宿主已验收。 -剩余 cache 一致性调查、其他旧 reader 断言、分阶段指令等价和实际 optional/external +在该历史检查点,剩余 cache 一致性调查、其他旧 reader 断言、分阶段指令等价和实际 optional/external 消费者、版本准入及打包宿主连续性仍未完成。缺少显式原生 runtime 的 operation SDK 检查属于未测,不能算资格。默认 writer 变更尚未交付;PR 因作者实现未完而保持 Draft,不是仅等待评审。尚无模型或成本收益证据。历史 effect 解码只是临时迁移, 不得成为第二份永久可执行视图。 +External scheduler 候选在读取节奏或唤醒许可前,通过 +`assert_complete_turn_decision` 复用现有 TypeScript 完整 reader owner 验证带版本输入。 +这是纯读取:不重组已捕获的 host binding,也不生成旧 executor 视图。缺失 binding、 +不支持的版本/必需语义、内容篡改和无效同 capture 引用均在 wake command 前拒绝。 +无版本普通 quota packet 暂保留原兼容路径,退役须核对受支持的已部署 worker。 +File/SQLite 源码 CLI 到 worker 验证使用本地子进程标记,不执行模型;这里只取得 +该输入边界资格,不代表 attached host 任务交付、结算或验收成果/完整成本收益。 + +可选 Ark adapter 复用同一宿主输入 owner 的 `extract_turn_authority`/ +`render_prompt`,无需在可选包里新增契约 builder。File/SQLite 的真实 +`turn plan --host generic-cli --iteration-context fresh` 已将完整请求送入 +真实 Ark SDK,并使用合成 HTTP transport 验证。检查对照实际 user-message +中的契约与原 CLI 来源,恢复持久化 running receipt 时不重新提交输入,重放完成 +结果时不再调用 provider;来源/版本/binding 被篡改或完整输入显式为 null 时,在调用 +provider 或创建 receipt 之前拒绝。Core 状态与记账保持不变,历史 Ark 请求和恢复 +测试继续保留。这验证了 SDK wire 边界,尚未验证实际云端 session、模型成果或 +Core 结算。Ark 与 MemT 现共用真实 File/SQLite CLI 请求夹具。MemT 在 provider IO +之前拒绝该完整输入,因为可执行的 legacy registry 不提供精确 `goal_ref`。 +现有 `source_session_v1` 虽有精确生命周期,但属于 lifecycle-only;真实通用 Turn +规划入口会在 capture 或执行前拒绝它。规划后补造引用会掩盖这一生产者/消费者缺口。 +完整输入损坏也会在 provider 或 receipt 创建前拒绝;历史 MemT SDK/snapshot 测试 +继续保留,但不能证明端到端完整输入送达。MemT 路径的所属边界修复须将可执行 registry +准入的精确生命周期沿共享 Turn 请求与恢复 owner 传递,并保留重建及旧实例拒绝。 +不能通过放松 MemT 隔离或启用 lifecycle-only profile 来绕过。这是已证实的采用缺口, +尚未取得原生记忆路径资格。operation-SDK 原生 runtime 与打包 App 采用也仍待验。 +内部 effect envelope 与历史请求 decoder +仍服务结算/结果/会话 caller;只有这些 caller 和未结 receipt 已采用替代契约后 +才能移除,不能仅因模型输入不再包含旧 envelope 就删除。 + +新请求的 Codex/DSH 共享 session lineage 已从完整契约读取,复用 TS 的 +完整输入校验与自适应主任务选择 owner。模型输入校验使用同一 owner,并拒绝完整 +输入与内部 effect adapter 的 Goal/Agent/Todo 不一致,即使两份摘要各自有效。 +完整路径不再重复调用 Python 的版本与签名校验;历史请求继续使用原 decoder, +持久化 session schema、路径与 scope 不变。File/SQLite 真实 CLI capture、失败恢复 +和一次结算重放通过;普通/自适应任务在 todo/agent scope 均能读回 Codex/DSH +原 session。相同夹具在迁移前基线保留合法路径通过,但新增身份冲突用例失败, +在候选通过。原生 operation SDK 环境仍有跳过,不能宣称其 runtime 已验证。 +会话身份迁移未移除全部结果资格与结算的内部 envelope caller,尚未达到完整退役 +或净成本收益结论。MemT 生命周期缺口仅阻塞 +其依赖路径,不阻塞这些独立迁移。 + +规划与失败重试的 session 查找现在传递原始完整契约,journal inspection 也复用 +同一身份 owner。缺省契约仍解码历史记录;显式 null 契约会拒绝。实时与缓存 vision +结果的归一化也复用该身份读取,不再从 effect adapter 重建 Goal/Agent。 +会话 scope/生命周期门与原生 app-server callback 仍由既有 owner 负责。 +本片移除这些仅用于读取身份的旧调用,尚未删除内部 adapter:执行路由、结果证据/ +profile 判定与结算仍须迁移。 +自适应恢复夹具在签名前构造:历史未验证选择仍按 primary Todo 写回;冲突的已验证 +任务来源在 host 执行和额度支出前拒绝。既有来源校验门保持有效。 +完成恢复验证读取原始持久完成及支出回执,不重新提交完成。四项过时的重复调用断言 +在前一基线同样失败;修正后的验收保留真实 File/SQLite 中后续任务文本变化和历史 +未带来源凭据但已经接受完成的恢复场景。 + +完成任务正文的解析现由既有 TypeScript interaction 来源 owner 统一负责。 +新 `run-once` 请求先校验完整 capture 与内部 adapter 的绑定,再解析并计算完整 +契约中已准入正文的摘要,不再从 adapter 重建该来源。历史 journal 经 Python +薄 transport 使用同一解析规则,保留 Unicode/空白原始字节、缺失/重复来源拒绝 +及已接受完成回执的恢复。此次删除一套 Python 语义算法,没有新增第二种 packet +视图。迁移前 `599cf1f72` 的 48 项 File/SQLite 捕获及完成检查通过;候选完整来源 +检查也拒绝被修改的 binding 和显式 null 输入。历史解析原为本地 Python,现在 +增加一次 TypeScript 边界调用;完整路径净成本尚未测量。结果 profile、证据解释 +和其余结算消费者仍须继续迁移。 + +Codex 结果 schema 与共享 candidate adapter 现通过同一经过校验的完整契约 +profile 读取判断证据要求是否适用;新请求不再从内部 capsule 决定该要求。 +该读取与模型输入校验复用既有 TypeScript 完整性/来源/身份校验器,没有新增 +packet 视图、wire 字段或按证据字段分别调用。缺省契约仍解码历史 capsule; +显式 null 或被修改的契约会拒绝。空/非对象 profile 保持原来的不适用行为, +停止结果仍不要求证据,已提供证据原样交给既有结果验证器。此次迁移的是适用性 +判断,未改写证据内容校验,也尚未迁移全部执行路由和结算消费者。 +schema/material candidate 每次读取各新增一次校验边界调用;这是语义收敛, +尚不能据此宣称性能改善。 +使用同一真实 SQLite CLI host 请求(JSON 45,405 字节、合成 provider)进行 +31 组交替热边界测量,schema 构造加 material 结果转换的中位耗时从 +`68223e337` 的 0.013 ms 增至候选的 19.6 ms(p95 为 0.019/22.8 ms), +合法输出一致。保留这一局部新增成本的负向证据;测量不包括冷启动、模型工作或 +完整 Turn,总体成果/成本资格仍开放。检查覆盖真实 File/SQLite 捕获恢复与 +Codex/共享 host 消费者,不能证明打包 App 已采用或模型成果改善。 + +执行入口现在在 dispatch 或 journal 恢复前校验一次完整契约身份。结算计划缺失时 +用该身份补建,恢复检查在同一次调用内复用它,不再读取 adapter 的 Agent。 +已有结算计划与回执不重建;缺省完整契约的历史记录保留原解码与字符串处理。 +该值不会跨 host 尝试、来源替换或后续调用缓存。此举同时关闭一个恢复绕过点: +此前已有结算计划会跳过来源校验,null/被修改的完整请求仍可返回已有 journal +结果。真实 File/SQLite CLI journal 的新拒绝保留 journal 字节与扣额数, +不调用 host 或结算 callback。十个定向用例在 `ce996ea0f` 的新增拒绝断言处失败; +候选 32 项捕获/恢复检查和扩大后的 605 项回归通过(相互重叠)。入口新增一次 +校验边界调用,由两个消费者共用。同一份 45,405 字节真实 SQLite CLI 请求, +31 组交替热测已有结算计划读取与恢复身份,合法输出一致:中位数从 0.016 增至 +12.382 ms,p95 从 0.024 增至 14.883 ms。这是新增局部校验成本,不是效率收益; +不含冷启动、完整 Turn 与模型成本。完整路径性能及其余路由/结算退役仍开放。 + +Codex CLI 构建提示词时,有效完整请求现在直接调用既有完整校验 owner,删除重复的 +单独 envelope 版本调用。历史请求保留版本检查;完整校验失败时仍检查版本,以保留 +两处同时损坏时的诊断优先级。未引入校验缓存、新 wire 字段或第二 owner,提示词正文 +不变。这是调用边界合并,不代表剩余 driver 路由或结算 adapter 已退役。 + +对照 `e1dcd95dd`,完整与历史提示词字节一致,六类无效输入保留相同拒绝诊断。 +同一份 45,429 字节真实 SQLite CLI 请求生成 34,043 字节提示词;61 组交替热测中, +提示词构建中位数从 24.229 降至 14.819 ms,p95 从 30.189 降至 17.854 ms; +桥接追踪确认两次调用变为一次。局部降幅约 39%,不含冷启动、模型执行和完整 Turn +成本,不抵销单独测得的 profile/结算开销,也不证明相对原 quota 的成果收益。 +诊断补充前的较早样本为中位 21.279/12.994 ms、p95 29.729/17.321 ms;保留其 +早期证据身份,不作为第二份独立的最终资格。 + +规划现在在构建路由、会话和事务之前接收完整契约,不再在计划完成后附加它。 +规划与失败会话恢复通过一个粗粒度身份操作复用既有 TypeScript 完整来源校验; +同一次边界调用选择 Goal/Agent/工作身份并返回动作签名前像,替代原先仅取签名的 +调用,不增加逐字段 RPC 或缓存。显式空值、不支持版本、被修改的完整契约均拒绝, +已处于 resume 的请求也一样;缺省完整契约的历史输入保留原 decoder。 +Todo-less replan 身份仍由回执 owner 提供;下文路由与启动增量记录后续消费者迁移。 + +Python JSON/hash 传输编码特意保持不变。直接采用 JavaScript 哈希的试验未通过 +小数/指数前像一致性检查,会改变已持久化的 Turn key;File/SQLite 特征化现在 +覆盖这些数值、Unicode、自适应工作选择和重复恢复。一个真实 SQLite CLI 请求 +(45,380 JSON bytes)相对 `088867421` 的 31 组交替热态采样,完整/历史计划及 +失败恢复输出保持一致。规划 median/p95 从 39.632/47.964 变为 42.608/53.270 ms; +恢复从 12.330/20.858 变为 18.136/23.242 ms。这是局部增加的校验成本,不是完整 +Turn 或原 quota 成果对照,也不代表模型、已安装 App 或 PostgreSQL authority +资格。剩余路由/材料 adapter 退役及端到端成本资格继续使 C1 保持开放。 + +Host 启动前实时复查现在将回执绑定的实时观察交给初始规划所用的同一完整契约 +composition owner,再把完整契约传入共享 planner,替换原先 raw quota 直接进入历史 +规划的捷径。Stop 和动作变化仍先于 composition 拒绝;实时观察不替换原始 capture +中的模型输入,也不生成另一份回执。剩余 effect/材料 adapter 仍需退役。 + +路由分类现在归既有 TypeScript envelope owner 所有。显式注册 action/frontier 表在 +编译期检查穷尽性,历史未知 action 仍进入 Host;签名拒绝、attempt、先 replan 后 +普通 delivery 门、能力 intent 身份、repair、用户行动和安静等待保留原顺序。 +Python 仅保留 controller 薄传输与历史 scalar-text 编码,不再拥有第二份分类表。 +完整契约校验忽略历史解码提示;已持久化动作 hash 的 Python 编码及历史工作选择 +reader 均保持不变。 + +规划在既有粗粒度身份/签名调用中同时取得 route;完整与历史普通计划仍各需两次 +桥接调用。相对旧 Python 分类器,1,152 组 action/gate 组合一致;固定真实 SQLite +CLI fixture 的完整与历史计划输出也一致。计划对照共用当前 TypeScript endpoint, +因此不是精确旧版/新版端到端延迟实验。controller 单独读取 route 现在新增一次 +桥接:44,891 字节请求的 61 组交替热测,median/p95 从 0.004792/0.010125 升至 +13.252792/15.855042 ms。这是该 caller 的实测回归,不是效率收益;作为 controller +调用边界合并的明确成本缺口保留,不引入缓存或重复分类器掩盖它。File/SQLite +入口、恢复、一次结算资格,剩余 adapter 退役、打包采用和原 quota 成果/完整成本 +资格仍分别验收。 + +生产 `turn managed-step` caller 现在将 envelope 构造与同一路由求值合并在一次 +TypeScript 请求中。Python adapter 返回绑定 route 的本地不可变 envelope 快照;它 +不是新的 Agent wire 版式,也不拥有第二份决策规则。读取或修改返回的字典不会 +改变快照,也不能把它的资格附到另一份 wire 字典。controller 保留既有用户行动 +优先级、回执/前驱校验和 journal 所有的重试预算;普通历史或外部 envelope 仍走 +原 route reader 与签名拒绝。构造 adapter 和普通 envelope builder 共用宿主绑定及 +capture 参数准备,删除 managed caller 原本多余的中间 builder。上段独立 reader +回归保留为历史实测;只有生产中新生成与消费的调用对能够省去这次额外请求。 +剩余工作选择/effect/材料解码、大型冷上下文恢复、打包与成果资格继续开放。 +caller 直接消费统一 typed controller 结果后,本地快照随之退役;它不新增持久化 +schema 或兼容 wire。 + +固定 31,088 字节合成来源的 61 组交替热测,将 `ddf96b79c` 的原 Python producer/ +controller 函数与合并调用比较;共用当前 TypeScript 服务和未变的 build/route 规则。 +最终源码的组件 median/p95 为 22.477/25.417 对 13.739/17.479 ms。另以真实隔离 File/SQLite +store 经公开 `managed-step` CLI,各 provider/arm 取 21 组交替热测:File median +为 135.989 对 137.316 ms,SQLite 为 149.310 对 146.081 ms;p95 分别为 +209.730 对 192.544 ms、222.299 对 240.848 ms。另行传输计数探测发现每个 provider +的桥接总请求数从 70 降为 69:省去一次 route 请求,其余 live-decision 调用仍在。 +完整输出一致,失败 journal 逐字节不变,两臂均不扣额。请求数改善,但整条 CLI 的 +median 和尾部结果混合,尚不能证明加速;本地小 +样本及共享机器资源限制尾部推断。这是具名源码边界的观察,不是整个不可变旧树 +对照、冷启动、已安装 App、模型成果或原 quota 完整成本资格。 + +完整 reader 现在还将 adapter 的已签名动作文档,与原完整来源经过既有 producer +投影的结果比较。原先来源和 adapter 各自的 hash 只能证明各自完整:重新计算 hash +后的 adapter 可以保留 Goal/Agent/Todo,却改变执行或写回条件。共享校验复用既有 +签名维度,不新增第二份字段清单、RPC 或 wire 视图。仅含历史输入时保留旧 reader。 +完整请求的 profile adapter 与来源不一致时,现在明确拒绝,替代此前忽略冲突、直接 +采用来源 profile 的行为;这是有意的拒绝语义变化,历史 profile 行为另行保留。 + +相对 `b9e2c5120`,File/SQLite 的规划拒绝断言在修复前失败。六类独立重算 hash 的 +反例覆盖运行资格、有效动作、交付许可、用户介入、执行策略和扣额许可;候选在 +host/写回/扣额回调前拒绝,不产生 Turn journal 或结算回执。既有 lane 锁仍可能 +先分配。一个 44,891-byte 真实 SQLite CLI fixture 的 101 组交替热态进程内 +TypeScript 采样保持合法身份输出一致;校验 median 从 4.864 增至 5.163 ms,p95 从 +5.570 增至 6.010 ms。这是局部新增成本,不含桥接、实时 composition、冷启动、模型 +或完整 Turn 成本,不证明相对原 quota 的成果收益或 C1 切换资格。 + ### C1 消费链替换与切换单元 切换单元是一个受支持入口**及其下游消费者**,不是一个输出开关。对 @@ -465,6 +756,16 @@ Draft,不是仅等待评审。尚无模型或成本收益证据。历史 effec 受支持恢复窗口核清前保留历史 decoder。回退同样区分未结工作与新准入。版本迁移不能 把保存的 prompt、journal 或源 hash 改写成仿佛一直使用新契约。 +代码收敛属于每个迁移切片,不只留给最终 C9 清理。两个入口须共用 composition、引用 +校验、上下文重建与结算的语义实现。真实的宿主 launch/observation 差异留在薄 adapter; +不为强行统一不同生命周期而另造通用 runner。各项决策仍由既有 typed owner 负责。 + +每个切片记录迁移前后调用点、共享 owner、已删除的重复算法,以及剩余兼容 caller 和 +退出条件。两条路径共用独立编写的语义用例,并分别验证真实传输与恢复。仅委托调用的 +wrapper 可以过渡保留;给公共模块起新名字却继续保留两套规则实现,不算收敛。下一次 +修改共同规则,应只改、只测一个 owner 并传递到两条路径。代码行数下降不是验收指标, +也不能为此删除宿主独有的验证覆盖。 + C1 首个 PR 必须覆盖上述两个生产组合调用点,并交付至少一条从生产端到最终输入的 完整旅程,同时明确剩余 reader。这个有界切片通过资格,不代表可向未验收 host 开启 新 writer,也不关闭 C1。最终切换须在全部受支持 reader 验收后删除活跃格式选择。 @@ -546,9 +847,143 @@ projection 为 20,543,完整候选为 21,738:**相对 CLI 增加 5.82%**, 及成果差异均未测。切换 legacy receipt 得到相同数据,不增加独立 workload。这些数字 不支持净收益结论。 -下一实施切片应先在 composition、reader 和 recovery 中验证同契约精确共享,再用上述 -可比负载评估延后详情的取舍。冷启动与分阶段总量分别保留,保留负结果,不能靠删除独有 -指令达到字节目标。 +#7430 的验收共享增量把原 Python 展示算法移入既有 TypeScript composition owner, +在 hash/capture 前执行;旧展示 helper 只委托同一实现。完整契约 reader 与历史分阶段 +读取共用一个引用解析器。相同契约保留一份完整 owner;不同 revision、JSON 类型、来源 +和负面证据分别保留。已有引用随 owner 的迁移同步调整;即使完整性 hash 正确,悬空、 +循环或不可用引用也会拒绝。引用比原对象更大时仍保留短对象原文。 + +隔离 File/SQLite 的 quota 和 native host fixture 覆盖真实合成 Codex 子进程输入中的 +完整验收、失败后恢复和原身份一次性结算,也验证共享后的 full/delta/丢失基线重建。 +这些证明传输语义,不证明模型遵从。旧展示 adapter 增加一次传输调用;完整契约在原有 +composition 调用内部完成共享。未声称延迟改善。 + +固定 source 的四份相同验收 fixture 中,融合契约为 23,432 个紧凑 UTF-8 JSON 字节, +原未分阶段 CLI 为 22,237(+5.37%)。启用 capture 分阶段时,融合契约为 23,515, +原 entry/review/writeback 合计为 24,509,未计追加材料读取。对照冻结 `895f38fc5` +的原展示共享 helper;上述不含验收副本的 workload 数字不变。这把 quota 已有的效率 +吸收到共同路径,不能证明优于 quota 或完整成本获益。下述 guidance 增量进一步缩小缺口;下一步先在既有 owner 验证剩余 +vision 共享;标题由下述增量处理,然后再评估延后详情。冷启动与分阶段总量分别保留, +保留负结果,不能靠删除独有指令达到字节目标。 + +#7430 后续增量通过同一个 TypeScript composition 边界共享 replan 的 vision-authoring +指令,保留的 owner 是既有 durable-writeback 结算步骤。旧 Python 展示 helper 委托 +共享判断,仅添加历史展示元数据。完整 reader 和历史未分阶段/分阶段 adapter 解析同一个 +精确引用,不另建 authoring 策略。JSON 类型、版本或指令不同则保留原文,引用必须更小。 +即使 hash 匹配,冲突的原文/引用、错误步骤类型、缺失目标和引用链也会拒绝。 +结算计划、全部原指令和示例保留在 capture 中;历史 writeback 阶段维持原有的示例省略。 + +两个旧 CLI 参数写法现在暴露同一份共享完整契约;材料回读、产生效果前拒绝和 native +输入分别验证。已验证的 Native 外层 controller 输入没有内联结算 authoring owner,authoring 仍保留原文; +共享不会虚构该 owner 或转移结算权。authoring 检查点的四项旧 projection 测试期待 +完整候选尚未采用的多行标题或 Explore 指导共享,在此前及 authoring head 都失败。 +下述 guidance 增量解决其中两项,标题增量解决另外两项。历史失败继续保留,不豁免或改写。 +这些切片不证明已安装 App 的采用。 + +固定的合成 authoring fixture 中,原未分阶段 CLI 为 22,062 个紧凑 UTF-8 JSON 字节, +融合契约为 23,266(+5.46%)。启用 capture 分阶段时,原 entry/review/writeback 合计 +24,311,融合为 23,349。原 CLI projection 模块冻结在 `895f38fc5`,输入及投影读取阶段 +保持一致。追加读取、请求包装、tokens/cache、延迟和模型成果尚未测量。共享消除了 +候选中的重复实现和重复指令,但不能证明优于 quota 已有共享或完整成本 diff 正向。 + + +Explore guidance 增量删除 Python 的精确匹配循环,完整契约与旧展示调用者复用同一个 +TypeScript composition 边界。仅当指令完全一致、已有 durable-writeback Explore +附件 owner 且引用更小时,才共享同一 capture 的 owner。独有/短文本及缺失 owner +保留原文。非法、冲突或成链引用拒绝;不缩短指令,也不给可选附件新增义务。 +历史分阶段及未分阶段 reader 在移动上下文前解析引用,再复用 Explore 既有领域投影。 +独立 Explore 读取仍返回完整指导。兼容 helper 随旧展示调用者退役,不拥有独立决策或共享算法。 + +真实隔离 File/SQLite CLI capture 现在通过此前失败的两项 Explore 共享用例, +包括独立材料对照、历史回读及非法 capture 在状态/额度不变时拒绝。 +既有分阶段 CLI 测试仍执行 registry 不可用时读取、篡改拒绝及实际写回/结算。 +差分重建保留变化后的指令并支持 baseline 丢失恢复;不完整上下文回退完整交付。 +这些证明传输及本地集成,不证明已安装 App 或模型成果。 + + +扩大调用者验证也将旧测试假设迁移到准入后的完整契约:native 子进程 capture/重试 +比较整份原始契约,生成的结算命令仍仅结算原 Turn。历史 capability reader 维持 +显式初始化隔离 canonical fixture、拒绝事实及 scheduler 信息,不因此授予执行权。 +capability 准入被拒时仍交付 typed diagnostic/历史投影,准入成功才交付完整契约。 +这个失败路径迁移仍属于 C1 切换前缺口,不是永久第二套契约。 +合成 native/stdio provider 不证明模型收益。 + +同样冻结 `895f38fc5` 原投影,合成 guidance fixture 的原未分阶段路径为 22,848 个 +紧凑 UTF-8 JSON 字节,融合为 24,320(+6.44%)。启用 capture 后,原 +entry/review/writeback 合计 26,081,融合为 24,403。不含 guidance 的 fixture +未分阶段仍为 20,543 对 21,738 字节。必需追加读取、请求包装、tokens/cache、延迟及 +模型验收成果尚未测量。代码共享及恢复 quota 已有的信息效率不能证明完整成本 diff 正向; +C1 继续保持未完成。 + +当前 Task 标题增量删除 Python 的精确来源匹配算法,完整 composition 和旧展示委托 +共用 TypeScript owner。只有与唯一、已核验、完整的当前选中 Task 的 `text` 或 `title` +精确重复时,才转换为同一 capture 的 `title_ref`;完整原始声明及来源继续内联。 +来源缺失、不同、不完整、歧义、失败、已完成或已归档均不符合共享条件。历史字符阈值 +(包括 Unicode 码点计数)保持不变。旧展示按 label 前缀省略标题的逻辑留在展示层, +不应用于完整契约;委托 helper 随旧展示调用者退役。 + +完整 reader 再次验证身份及引用归属,即使 hash 有效,也拒绝冲突原文、引用链、 +非法路径及不可用目标。历史 envelope 保留完整 Task 来源,不携带移动后悬空的 lane +引用。真实 File/SQLite CLI 在差分关闭时已通过此前两项多行标题用例;capture 变体 +还验证精确回读及状态/额度变化前拒绝。差分及 baseline 丢失/全量恢复保留变化后的要求; +独立 Todo detail 仍返回完整声明。这证明本地传输语义,不证明已安装 App 或模型行为。 + +固定合成当前 Task fixture 中,冻结 `895f38fc5` 的原未分阶段 CLI 为 28,585 个紧凑 +UTF-8 JSON 字节,融合契约为 29,780(+4.18%)。启用 capture 后,原 +entry/review/writeback 合计 29,906,融合为 29,863。不含当前 Task 的控制样例 +未分阶段仍为 20,543 对 21,738 字节。这些总量未含请求包装、追加材料读取、tokens/cache、 +延迟及验收成果;它们将 quota 已有共享恢复到共同路径,不能证明完整成本收益正向。 + +vision audit 增量现已将 Python 嵌套精确匹配算法迁入同一 TypeScript owner。完整契约 +和历史展示只在引用更小时共享完全相同的 audit,包括未知事实、负面证据及有序义务。 +不同 JSON 类型不相等:旧 Python 的 `True == 1` 比较可能抹掉 channel 差异,现保留 +该差异原文;短 audit 也保持内联。根保留完整 audit 和 judge,旧展示 metadata 留在 adapter。 + +完整恢复即使面对重新计算的有效 hash,也拒绝缺失、非法、链式或冲突的 audit/judge +引用。历史 envelope 仍从内联根取得 judge,不产生移动后的悬空引用;其已有投影限制 +不变。真实隔离 File/SQLite CLI 在含活动历史 vision、差分关闭时,验证完整 judge、 +精确 capture 回读及状态/额度变化前拒绝。差分/全量及 baseline 丢失恢复保留变化的 +要求。没有活动 vision 的 fixture 正确地不产生 audit,不能用来验收这条路径。 + +冻结原 CLI projection 于 `895f38fc5`,固定合成 audit 样例的未分阶段紧凑 UTF-8 JSON +为原 22,337 字节对融合 23,333(+4.46%);capture 原 entry/review/writeback 合计 +23,658 对融合 23,416。不含 audit 的控制样例仍为 20,543 对 21,738。它们不计 +tokens/cache、延迟、追加读取、请求包装及验收成果。恢复共享、减少规则 owner 仍不 +证明完整成本正向。旧 audit 委托随展示调用者退役;剩余 consumer 退役及打包/成果 +资格继续使 C1 保持开放。 + +拒绝准入现在只要已有 interaction contract,就在不可变 capture 前使用同一完整 +composer,包括 health 为 false 的暂停来源。`ok=false` 仍阻止 host handoff 和原生 +context baseline 发布,保留失败退出码;组合不授予交付或结算权限。非法完整来源在 +capture 前验证失败。工作契约形成前的错误则在两种 CLI flag 写法下保留原 typed +诊断及恢复事实,不补造工作契约,也不再经过历史 envelope renderer。 + +真实隔离 File/SQLite CLI 覆盖 App heartbeat 和 generic 两种 profile 下的工作 +缺失、能力不可用及 selection-required reentry。重入保留原 receipt,不产生额度 +记账、baseline 或越权状态修改。Capture consumer 正迁向完整原件精确回读,同时 +显式保留历史长路径恢复覆盖。这验收本地传输和拒绝语义,不代表已安装 App 采用或 +全部 provider/入口交付。剩余 consumer 退役及打包后的单会话成果/完整成本资格 +继续使 C1 保持开放。 + +后续观察验证区分了两个问题。#7464 修复测试对两次独立实时观察时钟的误比,同时 +保留其他全部事实。#7468 让 `quota status/plan` 保留既有权威 owner 的 typed 拒绝 +及备份/迁移指引,修复提前返回错误时丢失这些信息的问题。按照已接受的 #7032 +截止规则,未迁移 Goal 的普通读取继续拒绝;File/SQLite 保留范围隔离断言。 +新的恢复断言在旧实现上失败,修复后的范围/诊断/权威集合 156 项通过。这些可独立 +评审的修复已由集成候选吸收,不代表安装采用或成果/成本改善。早期失败运行仍作为 +历史证据保留;这几项已归因失败不再列为未完成调查。 + +固定 unhealthy-paused 合成来源在原 CLI projection `895f38fc5` 下为 6,325 个 +紧凑 UTF-8 JSON 字节,完整契约为 6,639(+4.96%)。新增 binding 保留统一验证, +这不是效率收益;该样例的 tokens/cache、延迟、追加读取、请求包装及验收成果仍未 +测量。这些检查不代表启动过真实模型实验。 + +共享 owner 前置通过 #7457 独立评审。Task 标题与 Explore 操作在旧投影中合用 +一次粗粒度调用;完整 composition 在既有调用内部复用相同函数。集成分支吸收 +前置提交,不复制一份 owner。新的 `should-run` emission 只接受已组合契约或 +原始 predecision 诊断,不再回退旧 packet/envelope 投影;历史 capture 恢复 +仍为显式路径。组件测量显示桥接成本比两次调用更低,但高于原 Python 投影, +不能作为完整 quota 路径净收益的证据。 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 @@ -590,7 +1025,7 @@ backoff 继续归当前 scheduler owner。本文不引入模型空轮询或新 | 任务 / 依赖 | Owner 与实际替换内容 | 决定性退出 / 回退 | | --- | --- | --- | | **C1 — 统一契约;首先** | Interaction/work-context + quota/Turn owner:盘点字段与指令条款,捕获同一 schema,迁移真实普通 quota/Turn producer 与 thin renderer | 单会话真实入口保留必要信息、动作绑定和一次结算,并完成第 9 节相对 quota 基线的效果/完整成本对照;cutover 合格前保留绑定版本的 reader | -| **C2 — 一个上下文交付管线;C1** | Context owner + host observer:将 full/delta/detail/recovery 纳入 canonical 契约,删除 packet 格式分支和重复上下文副本 | 正常/replan/resume 中同 capture 恢复及精确 role/session 连续性;full 回退不重新准入;编码可回退 full | +| **C2 — 一个上下文交付管线;C1** | Context owner + host observer:将 full/delta/detail/recovery 纳入 canonical 契约;默认自己的成果、全局约束及相关跨 Agent 材料,提供精确正文索引;删除 packet 格式分支和重复上下文副本 | 在计量完整成本下,冷/温态、上下文丢失及正常/replan/resume 保留必读全文、跨 Agent 采用和精确 role/session 连续性;不支持的范围回退 full 且不重新准入;编码可回退 full | | **C3 — 会话绑定与原生生命周期;C1** | Session/runtime owner:扩展 role/generation,复用 native create/resume/observe/interrupt 和有序队列 | 两角色不覆盖 Agent native ID、不借用基线、不竞态执行;重试前观察既有 attempt;关闭新角色准入即可开始回退 | | **C4 — 共享受治理生命周期;C1** | Effect/Turn owner + 外层 worker adapter:围绕既有 typed 操作与回执合并重复 prepare/dispatch/observe/validate/writeback/settle 编排 | App/Turn/external worker 在部分失败下对准入、原结果与一次扣额一致;scheduler effect 保留真实 host owner;核对回执后才切回旧调用方 | | **C5 — Replan 与结果编写边界;C1** | Goal/Vision 与 interaction owner:提供当前事实和 typed controller 操作,移除双会话 profile 中普通 executor 编造 Core 写回字段的义务 | 先看事实保留 default/stride policy 区别、全部证据与必要验证;非法写回拒绝;rollout 期间单会话编写仍合格 | diff --git a/docs/reference/protocols/loopx-turn-v0.md b/docs/reference/protocols/loopx-turn-v0.md index 8c29f31e68..d08b473cad 100644 --- a/docs/reference/protocols/loopx-turn-v0.md +++ b/docs/reference/protocols/loopx-turn-v0.md @@ -1006,6 +1006,48 @@ writeback ordering. ## Cross-Iteration Context Policy +Captured supporting outcomes have one ordinary/replan read owner. With an +original decision file and its exact byte digest, use +`loopx --format json quota context-read --capture-file CAPTURE --capture-sha256 SHA256 --outcome-ref` +to page the current outcome index. Add the exact returned outcome reference +after `--outcome-ref` to page its full original detail. Follow the returned byte +cursor/command through EOF; every page revalidates that original file. The page +`sha256` binds the whole capture, while offsets and `total_bytes` address only +the selected index/detail JSON. Follow the returned `--selection-sha256` as well: +it binds the derived JSON layout and rejects mixed renderings after a reader +upgrade even when the original file is unchanged. Index rows retain owner/work and requirement +identities grouped by exact existing status, planning and Agent-review facts; +missing facts remain unknown, and an Agent review is not an independent check. +Same-id revisions have distinct references. A failed/unsupported detail read +requires the original full-capture recovery, never another guard. + +The canonical selected-work read can attach `outcome_context_read_plan_v0` +from the same complete authority snapshot. The index's `required_in_scope` +labels reflect that plan's typed work, shared acceptance/source/evidence and +current failure closure only when its identity, provider revision and whole +acceptance-source digest match. Missing, changed or unsupported coverage keeps all +outcomes required in the candidate plan; broad replan reads also keep all. +`scope_applied=false`: the plan is advisory until delivery, required reads and +Host recovery migrate together. It does not prove arbitrary semantic independence. +These optional reads do not replace existing full-read obligations. +Native retention observation rejects scoped pages +even when their digest matches the capture. Reading a new authority revision +later cannot replace the original observation; no Goal/provider read, admission, +Host invocation, writeback or quota spend occurs in this reader. + +给定原 decision 文件及其精确字节 digest,`context-read --outcome-ref` 分页读取成果 +索引,附返回的精确引用则读取成果全文。普通与 replan 复用同一 typed owner;按 +返回游标读到 EOF,每页重验原文件。`sha256` 绑定整个 capture,offset/total_bytes +只定位所选索引/正文。接续命令的 `--selection-sha256` 还绑定派生 JSON 字节, +防止 reader 升级后在原文件 hash 未变时混合渲染。索引按精确当前状态分组要求标识,保留 owner、工作关联和 +未知,Agent review 不充当独立检查;同 id 的不同版本分别引用。选定工作读取可从 +同一完整 authority 快照附 `outcome_context_read_plan_v0`。身份、provider revision +及完整验收来源 digest 一致时,`required_in_scope` 反映 typed 工作、共同验收/来源/ +证据与当前失败闭包。覆盖缺失、变化或不支持以及广域 replan 均保留全部成果。 +显式 `scope_applied=false`:交付、必读和 Host 恢复共同迁移前,计划仅供参考,不能 +证明任意语义独立性。可选详情不替代现有完整必读义务,不能建立 Host 完整保留证明。 +读取失败恢复原 capture,不重新准入;不读取新 Goal 状态或执行/写回/扣额。 + Each `turn plan` or `turn run-once` invocation declares an iteration context policy independently from the Todo and Goal lifecycle: diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index ae2ec08b9b..fc468363f9 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -85,27 +85,55 @@ neither enables native tracing nor changes the default-off transport switch. 控制字段和原始捕获均保留。`--include-detail agent-todos` 保留内联标题。 这是普通 CLI 序列化去重,差分开关关闭和开启时均适用,不会开启原生追踪或改变默认关闭。 -Exact Task-title and Explore-guidance sharing now use the existing TypeScript -context owner; the Python quota projection delegates both operations in one -coarse transport call, preserving each explicit detail selector. With both -selectors enabled, or no context source carrier, it needs no sharing call. Current -quota and Turn entrypoints retain their schemas. Explore guidance shares only -identical inline durable-writeback instructions within the same response, and -stays inline when the reference would be larger. Before relocating context, -Turn and staged replan readers restore that guidance from its settlement owner. -Dangling, chained, conflicting or incorrectly typed references reject rather -than silently losing instructions. Settlement commands, identities and original -source content remain unchanged. This shared-owner prerequisite does not qualify -the proposed complete-contract cutover or claim an end-to-end efficiency gain. - -精确任务标题与 Explore 指引共享复用既有 TypeScript 上下文 owner,Python quota -投影通过一次粗粒度调用委托这两项操作,分别保留显式 detail 选择;两项 detail -均启用或不存在上下文源载体时无需共享调用。当前 quota 与 Turn 入口保持各自 -schema。Explore 仅共享本响应 -内与 durable-writeback 指引完全相同的正文;引用更大时保留内联。Turn 与分阶段 -replan reader 在移动上下文前,从原结算 owner 恢复指引。悬空、链式、冲突或类型 -错误的引用会被拒绝,不静默丢失指令。结算命令、身份与原始源内容保持不变。 -这一共享 owner 前置不代表完整契约切换已通过验收,也不声称端到端效率提升。 +During the C1 migration, exact current-Task title sharing is owned by the +TypeScript decision composer in `turn_envelope.ts`. The legacy Python display +helper shares one coarse transport call with Explore-guidance sharing; each +explicit detail selector is preserved. No source carrier or both detail selectors +means no sharing call. Its label-prefix omission stays display-only. +Complete `loopx_turn_envelope_v1` decisions share before hashing/capture and +validate the same-capture target on recovery. Conflicting inline/reference values, +chains, malformed paths or a no-longer-verified selected Task reject even with +valid integrity hashes. This source sharing does not require differential delivery. +The `--include-detail` behavior above describes the historical display projection; +it does not select an alternative complete contract. Retire its delegate with the +legacy display callers while retaining supported historical capture recovery. + +C1 迁移中,当前 Task 标题的精确共享归 `turn_envelope.ts` 的 TypeScript decision +composer 所有;旧 Python 展示 helper 与 Explore 指引共享合用一次粗粒度调用, +分别保留 detail 选择;无源载体或两项 detail 均开启时无需共享调用。按 label 前缀 +省略仍仅属于展示。 +完整 `loopx_turn_envelope_v1` 在 hash/capture 前共享,恢复时验证同一 capture 的目标。 +原文/引用冲突、引用链、非法路径或当前选中 Task 无法继续核验时,即使完整性 hash +有效也拒绝。该来源共享不要求开启差分。上文 `--include-detail` 描述历史展示投影, +不会选择另一套完整契约;委托随旧展示调用者退役,支持的历史 capture 恢复继续保留。 + +The same composer also owns exact nested vision-audit sharing. The root keeps the +full audit and judge; the existing `projection_ref` carries only matching audit +and judge decision fields. Type-sensitive full-JSON comparison preserves different +channel facts and short audits inline. Complete readers reject unavailable owners, +reference chains and conflicting copies before effects. Historical envelopes keep +the inline-root judge, and the old display adds only its existing metadata. This +delegate retires with the same caller migration; it is not a second rule owner. + +同一 composer 也拥有嵌套 vision audit 的精确共享。根保留完整 audit 和 judge;已有 +`projection_ref` 仅携带匹配的 audit/judge 判定字段。完整 JSON 的类型敏感比较使 +不同 channel 事实和短 audit 保持内联。完整 reader 在产生效果前拒绝不可用 owner、 +引用链及冲突副本。历史 envelope 保留内联根 judge,旧展示只添加既有 metadata; +委托随同一调用链迁移退役,不构成第二个规则 owner。 + +A declined `should-run` decision with an interaction contract uses that same +complete composer before capture. Failure still prevents host handoff and native +context-baseline publication; validation never grants work or settlement. An +error before an interaction contract exists remains the original typed diagnostic +and recovery payload, with either CLI flag spelling. It is neither a fabricated +work contract nor a historical-envelope fallback. Readers must preserve the +original receipt and capture when transport fails after admission. + +已有 interaction contract 的拒绝 `should-run` 判定也在 capture 前使用同一完整 +composer。失败仍阻止 host handoff 和原生 context baseline 发布,验证不授予工作 +或结算权限。interaction contract 形成前的错误在两种 CLI flag 写法下都保留原 +typed 诊断及恢复 payload,不补造工作契约、不回退历史 envelope。准入后传输失败 +时,reader 必须保留原 receipt 和 capture。 When a replan action carries a complete `vision_authoring` schema, the default `quota should-run` packet now retains it, alongside its executable writeback diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index fefe903dd4..adde9d0631 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -128,6 +128,30 @@ retains its complete body and declaration. Todo-less reads and the explicit reduces repeated binding observations without treating a shorter context as acceptance, authority or a measured model benefit. +Within the opt-in native context transport, a verified same-capture reading plan +now produces `outcome_context_projection_v0`: an explicit model presentation, +with the current Agent's and related outcomes expanded and other independent +outcomes indexed with current states and exact original-source references. +Global intent, checks, permissions, stops and pending reads stay intact. The +interaction owner adjusts its own fulfilled outcome-reading instruction; read +complete optional detail before relying on it, and recover the complete original +source for unknown relevance or Goal-wide acceptance. Unknown optional fields, +unsupported guidance or a pending acceptance read keep full delivery. The +canonical decision and ordinary inspection still retain all requirements. + +Native full/delta baselines and compaction recovery bind the exact required +presentation, including its original-source digest. A newly required outcome +must deliver its body before reuse; machine storage of optional text is not +model reading. Publish current full-source recovery before optional scope +composition, then the exact required artifact before encoding, so interruption +cannot recover the preceding input. `quota context-read --outcome-ref` on the +presentation rechecks +its original full source through the same registry and exact-page reader. +Those detail pages cannot establish full-presentation retention. Old full +captures keep their original full-read obligation; transport off is unchanged. +These are source-path guarantees, not installed App adoption, actual model +retention or accepted-outcome/full-cost benefit. + For an exact Todo, the existing command now returns one complete record: ```sh @@ -500,6 +524,21 @@ context delivery 时,未标记或混合归属仅在内部桥接传路径与修 没有选定工作的读取及显式 `goal-acceptance inspect` 保留完整列表;正文减少不等于 验收、执行权限或已经测得模型收益。 +可选 native context transport 在同 capture 读取计划完整匹配时生成显式模型展示 +`outcome_context_projection_v0`:展开当前 Agent 及相关成果,其他独立成果保留当前 +状态索引和精确原来源引用。全局意图、检查、权限、停止和未完成读取保持完整;既有 +interaction owner 仅调整自己的已履行成果读取指导。依赖可选成果前先读完整正文, +相关性未知或 Goal 整体验收时读原完整来源。未知可选字段、不支持的指导或尚未完成的 +acceptance read 均保留全量交付;canonical decision 和普通 inspect 不删减要求。 + +native 全量/差分 baseline 与压缩恢复绑定真正交付的必读展示及其原来源 digest。 +新增必读成果须先交付正文,机器保存可选文字不代表模型已读。 +可选 scope 组合前先发布当前完整来源的恢复引用,编码前再发布精确必读产物,避免 +中断后恢复前次输入。对该展示运行 +`quota context-read --outcome-ref` 会通过同一 registry 和精确分页 reader 重验原完整 +来源;详情分页不确认完整展示已保留。旧 full capture 仍按原完整义务读取,transport +关闭不变。这些是源码路径保证,不证明正式 App 采用、模型保留或验收成果/完整成本收益。 + `todo list --todo-id …` 默认只返回一份完整 `todo.text`,保留来源、修订、身份、 状态和关系。精确响应移除列表及角色视图,不保留兼容开关;`--thin` 只用于概览。 缺失/过滤返回未匹配,歧义/来源故障报错,不退回旧摘要。读到记录不等于获得执行 diff --git a/loopx/ark_managed_agent_host.py b/loopx/ark_managed_agent_host.py index a31e9932d1..35aae70589 100644 --- a/loopx/ark_managed_agent_host.py +++ b/loopx/ark_managed_agent_host.py @@ -49,7 +49,7 @@ def build_ark_managed_agent_host_contract() -> dict[str, Any]: "quota_should_run.interaction_contract.cli_channel." "runtime_capability_reentry" ), - "cli_projection_ref": "quota_should_run.runtime_capability_reentry", + "cli_projection_ref": "quota_should_run.interaction_contract.cli_channel.runtime_capability_reentry", "packet_schema_version": RUNTIME_CAPABILITY_REENTRY_SCHEMA_VERSION, "delivery_channel": "quota_tool_result", "goal_prompt_mutated": False, diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 540f0ea7a1..11520f9030 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -35,7 +35,6 @@ from ..control_plane.quota.cli_projection import ( compact_quota_monitor_poll_cli_payload, compact_quota_plan_cli_payload, - compact_quota_should_run_cli_payload, ) from ..control_plane.quota.effect_program import SettlementIdentity from ..control_plane.quota.error_codes import ( @@ -60,7 +59,7 @@ reconcile_existing_heartbeat_receipt_for_turn, render_existing_heartbeat_receipt_payload, ) -from ..control_plane.quota.turn_envelope import build_turn_envelope +from ..control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision from ..presentation.renderers.quota_event_markdown import ( render_quota_monitor_poll_markdown, render_quota_slot_preview_markdown, @@ -305,30 +304,28 @@ def _project_quota_cli_payload( *, captured_decision_path: str | None = None, ) -> dict[str, object]: - """Project already-decided facts; preserve typed failures on envelope rejection. + """Carry complete decisions and predecision diagnostics without a second view. - The envelope is an additive hot-path view over a decided payload. A typed - validation/failure payload has no interaction contract to project, so a - renderer rejection keeps the typed diagnostic itself (with the skip reason) - instead of masking it with a crash (issue #3687). + A collection/validation failure has no executable interaction to compose. + Preserve that original diagnostic and recovery instead of attempting the + historical envelope renderer or manufacturing a work contract. """ from ..control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if args.quota_command == "should-run": + if (payload.get("schema_version") == "loopx_turn_envelope_v1" + and isinstance(payload.get("interaction_contract"), Mapping)) or ( + payload.get("ok") is False and not isinstance(payload.get("interaction_contract"), Mapping) + ): + return payload + raise EffectRuntimeRejected( + "should-run emission requires a composed complete decision; " + "historical captures require their explicit recovery reader" + ) if not bool(getattr(args, "turn_envelope", False)): if args.quota_command in {"status", "plan"}: return compact_quota_plan_cli_payload(payload, detail_sections=detail_sections) - if args.quota_command == "should-run": - if captured_decision_path and "vision" not in detail_sections: - from ..control_plane.quota.turn_envelope import _turn_envelope_result - payload = _turn_envelope_result("replan_cli_view", payload=payload) - return compact_quota_should_run_cli_payload( - payload, - include_todo_summary_detail="agent-todos" in detail_sections, - include_user_todo_summary_detail="user-todos" in detail_sections, - include_goal_boundary_detail="goal-boundary" in detail_sections, - include_vision_detail="vision" in detail_sections, - ) if args.quota_command == "monitor-poll": return compact_quota_monitor_poll_cli_payload( payload, include_decision_detail="decisions" in detail_sections, @@ -384,6 +381,22 @@ def _emit_quota_result( payload = {**payload, "replan_read_path": str(capture_directory / "decision.json"), "replan_read_argv": current_loopx_cli_argv()} + if args.quota_command == "should-run" and context is not None: + attach_host_poll_receipt( + context.status_payload, + args, + payload, + registry_path=registry_path, + ) + # A decided refusal has the same complete contract as an admission. `ok` + # still controls exit status and host handoff; composition grants no work. + if args.quota_command == "should-run" and isinstance(payload.get("interaction_contract"), Mapping): + payload = compose_turn_decision(payload, scheduler_execution_context=context.scheduler_context if context is not None else None) + if args.quota_command == "should-run": + # Validate new decisions before durable capture or host handoff. This + # projection is identity for complete decisions and early diagnostics. + payload = _project_quota_cli_payload(payload, args, detail_sections, + context.scheduler_context if context is not None else None) try: capture_decision(capture_directory, payload) if payload.get("ok") is True: @@ -403,19 +416,13 @@ def _emit_quota_result( decision_identity = (str(payload.get("goal_id") or ""), str(agent.get("agent_id") or "") if isinstance(agent, Mapping) else str(args.agent_id or "")) captured_payload = payload - payload = _project_quota_cli_payload( - payload, args, detail_sections, - context.scheduler_context if context is not None else None, - captured_decision_path=( - str(capture_directory / "decision.json") if capture_directory is not None else None - ), - ) - if args.quota_command == "should-run" and context is not None: - attach_host_poll_receipt( - context.status_payload, - args, - payload, - registry_path=registry_path, + if args.quota_command != "should-run": + payload = _project_quota_cli_payload( + payload, args, detail_sections, + context.scheduler_context if context is not None else None, + captured_decision_path=( + str(capture_directory / "decision.json") if capture_directory is not None else None + ), ) succeeded = bool(payload.get("ok")) # Handoff releases the immutable original before any presentation. Native @@ -427,7 +434,7 @@ def _emit_quota_result( payload = project_captured_context(payload, directory=capture_directory, request=delivery_request[0], capture_id=delivery_request[1], goal_id=decision_identity[0], agent_id=decision_identity[1], - surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet") + surface="complete_turn" if captured_payload.get("schema_version") == "loopx_turn_envelope_v1" else ("turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet")) if (args.format == "json" and succeeded and capture_directory and native_context): import hashlib @@ -439,7 +446,7 @@ def _emit_quota_result( root=getattr(args, "codex_context_root", None), create=True) payload = project_codex_context(payload, directory=directory, capture_directory=capture_directory, goal_id=decision_identity[0], agent_id=decision_identity[1], - surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", + surface="complete_turn" if captured_payload.get("schema_version") == "loopx_turn_envelope_v1" else ("turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet"), capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None)) recovery = payload.get("recovery") if recovery is None: diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index c068f6d00a..72eb366d36 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -14,7 +14,7 @@ from typing import Any from ..control_plane.content_digest import BARE_SHA256_PATTERN -from ..control_plane.effect_runtime import EffectRuntimeRejected +from ..control_plane.effect_runtime import EffectRuntimeRejected, EffectRuntimeRemoteError, EffectRuntimeStartupError from ..control_plane.quota.error_codes import QuotaCommandValidationError, QuotaDecisionTransportError from ..control_plane.turn_driver.decision_capture import ( validate_handoff_socket, CONTEXT_CAPTURE_READ_GUIDANCE, CONTEXT_CAPTURE_PAGE_GUIDANCE, context_capture_read_command, @@ -37,7 +37,9 @@ def _capture_page(path: Path, raw: bytes, digest: str, *, offset: int, size: int output_limit: int | None, instruction: bool, presentation: dict[str, Any] | None = None, next_page_bytes: int | None = None, - next_page_output_bytes: int | None = None) -> dict[str, Any]: + next_page_output_bytes: int | None = None, + outcome_ref: str | None = None, + selection_sha256: str | None = None) -> dict[str, Any]: end = min(offset + size, len(raw)) while end < len(raw) and raw[end] & 0xC0 == 0x80: end -= 1 @@ -48,11 +50,18 @@ def page(text: str) -> dict[str, Any]: result: dict[str, Any] = {**(presentation or {}), "ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, "offset": offset, "next_offset": cursor if cursor < len(raw) else None, "total_bytes": len(raw), "eof": cursor == len(raw), "content": text} + if outcome_ref is not None: + result["outcome_ref"] = outcome_ref or None + result["selection_sha256"] = selection_sha256 + result["read_instruction"] = ( + "Offsets and total_bytes address only this selected outcome/index JSON; sha256 binds the entire original capture. " + "This read does not establish full-capture retention or replace the capture's required-read obligations.") if instruction: - result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE + result["read_instruction"] = str(result.get("read_instruction", "")) + CONTEXT_CAPTURE_PAGE_GUIDANCE if cursor < len(raw): result["next_command"] = context_capture_read_command(path, digest, offset=cursor, page_bytes=next_page_bytes or size, - max_page_output_bytes=next_page_output_bytes or output_limit) + max_page_output_bytes=next_page_output_bytes or output_limit, outcome_ref=outcome_ref, + selection_sha256=selection_sha256) return result def fits(result: dict[str, Any]) -> bool: @@ -88,6 +97,7 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An in that exact capture, so a replacement cannot silently mix two decisions. EOF describes this page, never a model's complete-consumption receipt. """ + recovery_command = None try: digest = getattr(args, "capture_sha256", None) value = getattr(args, "capture_file", None) @@ -124,18 +134,50 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An if type(offset) is not int or offset < 0 or type(size) is not int or not 256 <= size <= maximum: raise ValueError("context-read requires a nonnegative byte offset and page-bytes between 256 and 8192; " "larger pages require a sufficient max-page-output-bytes within the capture limit") - path, raw, _ = _read_capture(value, digest) + path, raw, captured = _read_capture(value, digest) + outcome_ref = getattr(args, "outcome_ref", None) + selection_digest = None + expected_selection = getattr(args, "selection_sha256", None) + if expected_selection is not None and outcome_ref is None: + raise ValueError("selection-sha256 requires outcome-ref on the original context-read") + if outcome_ref is not None: + recovery_command = context_capture_read_command(path, digest) + # The typed owner shares exact lookup/index semantics with replan. + # IO and pagination remain here; no Goal/provider or guard is read. + from ..control_plane.quota.turn_envelope import _turn_envelope_result + + source_payload = None + if captured.get("schema_version") == "outcome_context_projection_v0": + source = captured.get("source_capture") + if not isinstance(source, dict): + raise ValueError("scoped context has no original source reference") + _, _, source_payload = _read_capture(source.get("path"), source.get("sha256")) + selected = _turn_envelope_result("captured_outcome", payload=captured, outcome_ref=outcome_ref, + **({"source_payload": source_payload} if source_payload is not None else {})) + raw = (json.dumps(selected, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8") + selection_digest = hashlib.sha256(raw).hexdigest() + # File integrity alone cannot bind cursors in a derived rendering: + # the same file may be read by an upgraded projection owner. Never + # mix selected JSON layouts under an unchanged original-file SHA. + if expected_selection is not None and expected_selection != selection_digest: + raise ValueError("selected JSON digest mismatch; recover the original complete capture") + if offset > 0 and expected_selection is None: + raise ValueError("outcome continuation requires the returned selection-sha256; follow next_command") if offset > len(raw) or (offset < len(raw) and raw[offset] & 0xC0 == 0x80): raise ValueError("capture offset is outside the file or not a UTF-8 boundary; use the returned next offset") return _capture_page(path, raw, digest, offset=offset, size=size, output_limit=output_limit, instruction=bool(context_dir), presentation=presentation, next_page_bytes=next_page_bytes, - next_page_output_bytes=next_page_output_bytes) - except (OSError, ValueError, RecursionError) as exc: + next_page_output_bytes=next_page_output_bytes, outcome_ref=outcome_ref, + selection_sha256=selection_digest) + except (OSError, ValueError, RecursionError, EffectRuntimeRemoteError, EffectRuntimeStartupError) as exc: # Recovery is presentation-only: keep failures separate from quota's # collection error path and retain the existing receipt/capture. return {"ok": False, "schema_version": "context_capture_page_v1", - "reason": str(exc) if isinstance(exc, ValueError) else "original capture could not be read", + "reason": str(exc) if isinstance(exc, ValueError) else ( + "captured outcome reader is unavailable; use the original full-capture reader" + if recovery_command else "original capture could not be read"), + **({"recovery_command": recovery_command} if recovery_command else {}), "recovery": "Restore the original digest-bound capture; no guard, admission or spend was performed."} @@ -158,6 +200,10 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, mode, target = "full", None if projected.get("schema_version") == "context_delivery_v1": mode, target = projected["mode"], projected["target"] + # Only the explicitly scoped artifact replaces the caller's original + # recovery route. Ordinary captures keep their exact historical bytes. + if projected["recovery"].get("presentation_schema") == "outcome_context_projection_v0": + metadata = {**(metadata or {}), "recovery": projected["recovery"]} if mode == "full": reference = {key: projected["recovery"][key] for key in ("path", "sha256")} page_bytes, output_bytes = native_context_page_budget() @@ -264,7 +310,8 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: # still owns when the following full/delta view can be published. bind_capture_selection_transport(payload, args, capture_root=root) projected = project_codex_context(payload, directory=directory, capture_directory=cache, - goal_id=args.goal_id, agent_id=args.agent_id, surface="quota_packet", + goal_id=args.goal_id, agent_id=args.agent_id, + surface="complete_turn" if payload.get("schema_version") == "loopx_turn_envelope_v1" else "quota_packet", capture_id=hashlib.sha256(raw).hexdigest(), continuity_generation=getattr(args, "_context_delivery_generation", None)) native_prepared = True @@ -490,6 +537,15 @@ def handoff_decision(directory: Path | None, socket_path: str | None, def handle_native_context_command(args: argparse.Namespace, *, print_payload: Callable[..., None], registry_path: Path | None = None, runtime_root_arg: str | None = None) -> int | None: """Dispatch native presentation/lifecycle IO before any quota authority read.""" + if getattr(args, "selection_sha256", None) is not None and (args.quota_command != "context-read" + or getattr(args, "outcome_ref", None) is None): + print_payload({"ok": False, "reason": "selection-sha256 requires context-read with outcome-ref"}, + args.format, lambda value: str(value["reason"])) + return 1 + if getattr(args, "outcome_ref", None) is not None and args.quota_command not in {"context-read", "replan-view"}: + print_payload({"ok": False, "reason": "outcome-ref is only supported by context-read and replan-view"}, + args.format, lambda value: str(value["reason"])) + return 1 has_output_limit = getattr(args, "max_page_output_bytes", None) is not None budget_scope_error = ("max-page-output-bytes is only supported by JSON context-read " "or enabled native should-run/context-project") @@ -606,6 +662,8 @@ def read_replan_view(args: argparse.Namespace) -> dict[str, Any]: if not args.decision_file or not args.source_hash: raise ValueError("replan-view requires --decision-file and --source-hash") + if args.outcome_ref == "": + raise ValueError("replan-view outcome-ref requires an exact reference; use context-read for the captured index") path = Path(args.decision_file).expanduser() with path.open(encoding="utf-8") as stream: payload = json.load(stream) diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index 7789a3f800..1560614154 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -43,7 +43,9 @@ def register_quota_command( help="Use status for all groups, plan for next-turn groups, should-run for one goal, context-read for bounded original-capture recovery without a guard, context-project to present an existing digest-bound quota capture with native full/delta and a full-read fallback, context-hook for opt-in native Codex lifecycle IO, context-config for native hook/recovery configuration without installation or trust changes, monitor-poll for no-spend quiet poll evidence, scheduler-ack for successful Codex App RRULE state, scheduler-fail-current to suppress a repeated failed host update pair, spend-slot for accounting, or void-slot for a non-destructive accounting correction.", ) quota_parser.add_argument("--decision-file", help="Original captured decision for read-only replan-view.") - quota_parser.add_argument("--outcome-ref", help="Exact captured acceptance outcome reference for replan-view.") + quota_parser.add_argument("--outcome-ref", nargs="?", const="", + help="For context-read: omit the value to list captured outcomes, or supply an exact reference to page its full detail. " + "For replan-view: exact captured acceptance outcome reference. Neither changes required-read obligations.") quota_parser.add_argument("--source-hash", help="Original source_decision_hash for replan-view.") quota_parser.add_argument("--replan-stage", choices=["review", "writeback", "acceptance", "explore"], default="review", help="Read decision context before opening its writeback format.") @@ -225,6 +227,9 @@ def register_quota_command( "Use for shared Codex App hook configuration; cannot combine with --codex-context-dir. " "context-config renders native configuration for the exact Goal/Agent; no host configuration, trust, guard or spend are changed.") quota_parser.add_argument("--capture-sha256", help="For context-read/context-project: exact SHA-256 supplied with the capture reference.") + quota_parser.add_argument("--selection-sha256", + help="For context-read with outcome-ref: selected JSON byte digest returned by the first page. " + "Required on continuation; follow next_command rather than computing it.") quota_parser.add_argument("--offset", type=int, default=0, help="For context-read: byte offset returned by the previous page.") quota_parser.add_argument("--page-bytes", type=int, help="For context-read: maximum UTF-8 content bytes per page (JSON default: 64000 under a 64000-byte stdout budget; Markdown: 4096). " diff --git a/loopx/cli_commands/turn.py b/loopx/cli_commands/turn.py index 3b4c750b0e..ec86c76afb 100644 --- a/loopx/cli_commands/turn.py +++ b/loopx/cli_commands/turn.py @@ -12,7 +12,7 @@ ) from ..control_plane.operator_provider import operator_provider_environ from ..control_plane.quota.effective_action import EffectiveAction -from ..control_plane.quota.turn_envelope import build_turn_envelope +from ..control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision from ..control_plane.runtime.status_projection_cache import ( resolve_status_projection_cache_runtime_root, ) @@ -248,7 +248,7 @@ def handle_turn_command( ) operator_inbox_urgency_projector = decision_owner.operator_inbox_urgency_projector scheduler_context = decision_owner.scheduler_execution_context - decision = decision_owner.resolve() + decision = compose_turn_decision(decision_owner.resolve(), scheduler_execution_context=scheduler_context) resume_requested, session_binding = resolve_turn_resume_session_binding(args) turn_envelope = build_turn_envelope( decision, @@ -270,19 +270,14 @@ def handle_turn_command( ): from ..control_plane.turn_driver.codex_cli import codex_cli_session_binding - session_binding = ( - codex_cli_session_binding( - runtime_root, - turn_envelope, - goal_admission=strict_goal_admission, - session_scope=session_scope, - host=args.host, - ) - if strict_goal_admission is not None - else codex_cli_session_binding(runtime_root, turn_envelope, session_scope=session_scope, host=args.host) + session_binding = codex_cli_session_binding( + runtime_root, turn_envelope, work_contract=decision, + goal_admission=strict_goal_admission, + session_scope=session_scope, host=args.host, ) payload = build_loopx_turn_plan( turn_envelope, + work_contract=decision, host=args.host, execution_mode=args.execution_mode, scheduler_owner=args.scheduler_owner, diff --git a/loopx/cli_commands/turn_decision.py b/loopx/cli_commands/turn_decision.py index 4e36ed4825..fe4a8661fb 100644 --- a/loopx/cli_commands/turn_decision.py +++ b/loopx/cli_commands/turn_decision.py @@ -33,7 +33,7 @@ from ..control_plane.quota.live_decision import build_live_quota_should_run_decision from ..control_plane.quota.effective_action import EffectiveAction from ..control_plane.quota.turn_envelope import ( - TurnControllerInput, build_turn_controller_input, build_turn_envelope, + TurnControllerInput, build_turn_controller_input, build_turn_envelope, compose_turn_decision, ) from ..control_plane.quota.settlement import SettlementIdentity, build_turn_scoped_cli_settlement_plan from ..control_plane.quota.heartbeat_receipt import ( @@ -319,7 +319,7 @@ def build_fresh_turn_decision_owner( def current_turn_host_launch_decision( args: argparse.Namespace, *, registry_path: Path, runtime_root: Path, - identity: SettlementIdentity, replan_obligation_id: str | None, + identity: SettlementIdentity, planned_effective_action: object, scheduler_execution_context: Mapping[str, Any], operator_inbox_urgency_projector: Callable[..., dict[str, Any]], @@ -332,6 +332,9 @@ def current_turn_host_launch_decision( cannot reconstruct it. This read neither creates another receipt nor substitutes a newly selected task into the already planned Host request. """ + # A replan observation may accompany a Todo-bound Turn. Only the recorded + # settlement identity can bind admission to an obligation; passing the + # observed obligation as well would erase the original Todo selection. current = build_live_quota_should_run_decision( collect_turn_status_payload(args, registry_path=registry_path, runtime_root_arg=str(runtime_root)), @@ -343,7 +346,7 @@ def current_turn_host_launch_decision( operator_inbox_urgency_projector=operator_inbox_urgency_projector, bounded_research_frontier_projector=project_live_explore_composition_frontier, receipt_bound_todo_id=identity.todo_id or None, - receipt_bound_replan_obligation_id=replan_obligation_id, + receipt_bound_replan_obligation_id=identity.replan_obligation_id, turn_instance_id=identity.turn_instance_id, turn_start_hook_dispatch=turn_start_hook_dispatch, workspace_path=Path(args.project).expanduser().resolve(), goal_ref=goal_ref, @@ -362,15 +365,18 @@ def current_turn_host_launch_decision( if (current.get("should_run") is not True or current.get("effective_action") != planned_effective_action): return launch - current_envelope = build_turn_envelope( + current_contract = compose_turn_decision( current, scheduler_execution_context=scheduler_execution_context, ) + current_envelope = build_turn_envelope( + current_contract, scheduler_execution_context=scheduler_execution_context, + ) # Reuse the existing side-effect-free Host route projection, including # delivery, user-gate and protocol checks. No plan/session is persisted. plan = build_loopx_turn_plan( current_envelope, host=args.host, execution_mode=args.execution_mode, scheduler_owner=args.scheduler_owner, turn_instance_id=identity.turn_instance_id, - goal_ref=goal_ref, + goal_ref=goal_ref, work_contract=current_contract, ) route = plan["route"] if route.get("would_invoke_host") is not True: diff --git a/loopx/cli_commands/turn_inspection.py b/loopx/cli_commands/turn_inspection.py index ed6c5105b6..694b6eb419 100644 --- a/loopx/cli_commands/turn_inspection.py +++ b/loopx/cli_commands/turn_inspection.py @@ -43,12 +43,14 @@ def handle_turn_journal_inspection( registry_path=registry_path, runtime_root_override=runtime_root_arg, ) + contract_fields = {} scope = "todo" session_host = "codex-cli" if args.retry_failed_turn: plan = load_loopx_turn_plan_from_journal( runtime_root, goal_id=args.goal_id, turn_key=args.turn_key, ) + contract_fields = {"work_contract": plan["work_contract"]} if "work_contract" in plan else {} scope = str(((plan.get("session") or {}).get("context_policy") or {}).get("binding_scope") or "todo") # The retained plan, not the operator's current default host, owns # the failed Turn's session namespace. Legacy plans remain Codex. @@ -85,6 +87,7 @@ def handle_turn_journal_inspection( turn_envelope, session_scope=scope, host=session_host, + **contract_fields, ) ), ) @@ -159,6 +162,7 @@ def handle_turn_journal_inspection( turn_envelope, session_scope=scope, host=session_host, + **contract_fields, ) ), ) diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index 58d97c8a6c..20e6c16c55 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -94,7 +94,8 @@ def execute_turn_run_once( ) selected_todo = selected_turn_todo(envelope) expected_todo_text_sha256 = captured_todo_text_sha256( - envelope, todo_id=str(selected_todo.get("todo_id") or "") + envelope, todo_id=str(selected_todo.get("todo_id") or ""), + **({"work_contract": payload["work_contract"]} if "work_contract" in payload else {}), ) transaction = ( payload.get("transaction") @@ -802,7 +803,6 @@ def scheduler(_spend_payload: dict[str, object]) -> dict[str, object]: def native_launch_decision(current_registry: Path) -> Mapping[str, Any]: return current_turn_host_launch_decision(args, registry_path=current_registry, runtime_root=runtime_root, identity=settlement_identity, - replan_obligation_id=replan_obligation_id, planned_effective_action=payload["turn_envelope"].get("effective_action"), scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, @@ -826,7 +826,7 @@ def current_launch_decision(current_registry: Path) -> Mapping[str, Any]: # when its mirror still advertises old quota or health. return current_turn_host_launch_decision( args, registry_path=current_registry, runtime_root=runtime_root, - identity=settlement_identity, replan_obligation_id=replan_obligation_id, + identity=settlement_identity, planned_effective_action=payload["turn_envelope"].get("effective_action"), scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, @@ -881,19 +881,10 @@ def resolve_built_in_session_binding( turn_envelope: Mapping[str, Any], ) -> dict[str, str] | None: session_scope = str((payload.get("session", {}).get("context_policy") or {}).get("binding_scope") or "todo") - return ( - codex_cli_session_binding( - runtime_root, - turn_envelope, - goal_admission=goal_admission, - session_scope=session_scope, - ) - if goal_admission is not None - else codex_cli_session_binding( - runtime_root, - turn_envelope, - session_scope=session_scope, - ) + return codex_cli_session_binding( + runtime_root, turn_envelope, goal_admission=goal_admission, + session_scope=session_scope, + **({"work_contract": payload["work_contract"]} if "work_contract" in payload else {}), ) session_binding_resolver = resolve_built_in_session_binding @@ -907,7 +898,8 @@ def resolve_built_in_session_binding( def resolve_dsh_operation_session(envelope: Mapping[str, Any]) -> dict[str, str] | None: return codex_cli_session_binding( - runtime_root, envelope, goal_admission=goal_admission, host="dsh" + runtime_root, envelope, goal_admission=goal_admission, host="dsh", + **({"work_contract": payload["work_contract"]} if "work_contract" in payload else {}), ) session_binding_resolver = resolve_dsh_operation_session diff --git a/loopx/cli_commands/turn_todo_writeback.py b/loopx/cli_commands/turn_todo_writeback.py index 1e0052d96a..0448e86519 100644 --- a/loopx/cli_commands/turn_todo_writeback.py +++ b/loopx/cli_commands/turn_todo_writeback.py @@ -4,7 +4,8 @@ from pathlib import Path from typing import Any, Mapping -from hashlib import sha256 +from ..control_plane.effect_runtime import EffectRuntimeRejected +from ..control_plane.quota.turn_envelope import _turn_envelope_result from ..control_plane.todos.mutation_api import complete_goal_todo from ..control_plane.coordination.local_authority import ( @@ -12,53 +13,25 @@ ) +_ABSENT_WORK_CONTRACT = object() + + def captured_todo_text_sha256( - envelope: Mapping[str, Any], *, todo_id: str + envelope: Mapping[str, Any], *, todo_id: str, + work_contract: object = _ABSENT_WORK_CONTRACT, ) -> str | None: - """Resolve admitted full-source references; hash bytes without rereading live state. - - Older journal envelopes have no verification marker and retain their - original completion request. New producers mark source validation before - body deduplication, so a display preview alone can never become the source. - """ - action = envelope.get("action") or {} - selected = action.get("selected_todo") or {} - if "text_source_verified" not in selected: - return None - if ( - selected["text_source_verified"] is not True - or selected.get("todo_id") != todo_id - ): - raise ValueError("Turn task source does not match its selected Todo") - context = envelope.get("work_context") or {} - if not isinstance(context, Mapping) or not isinstance( - context.get("sources", []), list - ): - raise ValueError("Turn completion requires its admitted full task source") - records = [] - for source in context.get("sources", []): - if not isinstance(source, Mapping) or source.get("source") != "selected_todo": - continue - content = source.get("content") - record = content.get("todo") if isinstance(content, Mapping) else None - if ( - isinstance(record, Mapping) - and record.get("todo_id") == todo_id - and isinstance(record.get("text"), str) - ): - records.append(record["text"]) - if len(records) > 1: - raise ValueError("Turn task source is ambiguous") - text = records[0] if records else None - if text is None and ( - not context or context.get("selected_todo_ref") == "selected_todo" - ): - text = selected.get("text") - if text is None and selected.get("text_ref") == "action.recommended_action": - text = action.get("recommended_action") - if not isinstance(text, str) or not text: - raise ValueError("Turn completion requires its admitted full task source") - return sha256(text.encode("utf-8")).hexdigest() + """Use the shared task-source owner; omitted contracts decode old journals.""" + try: + result = _turn_envelope_result( + "captured_todo_text", envelope=dict(envelope), todo_id=todo_id, + **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else {}), + ) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from exc + digest = result.get("sha256") + if digest is not None and not isinstance(digest, str): + raise RuntimeError("Turn captured task source hash must be a string or null") + return digest def write_turn_validated_completion( diff --git a/loopx/control_plane/goals/acceptance.py b/loopx/control_plane/goals/acceptance.py index cbc43b3ec7..8dbdb3501b 100644 --- a/loopx/control_plane/goals/acceptance.py +++ b/loopx/control_plane/goals/acceptance.py @@ -195,6 +195,7 @@ def inspect_goal_acceptance( agent_id: str | None = None, todo_id: str | None = None, include_work_details: bool = True, + context_agent_id: str | None = None, delivery_workspace: Mapping[str, Any] | None = None, validation_workspace_path: Path | None = None, ) -> dict[str, Any]: @@ -205,6 +206,7 @@ def inspect_goal_acceptance( {**route, "registered_agents": registered_agent_ids_for_goal(goal), **({"agent_id": agent_id} if agent_id is not None else {}), **({"todo_id": todo_id} if todo_id is not None else {}), + **({"context_agent_id": context_agent_id} if context_agent_id is not None else {}), **({"include_work_details": False} if not include_work_details else {})}, ) basis["goal_acceptance_contract"] = observe_goal_acceptance_oracles( diff --git a/loopx/control_plane/goals/acceptance_authority.ts b/loopx/control_plane/goals/acceptance_authority.ts index 9372dfc139..b0c56d7af3 100644 --- a/loopx/control_plane/goals/acceptance_authority.ts +++ b/loopx/control_plane/goals/acceptance_authority.ts @@ -1,4 +1,5 @@ -import {bindGoalOutcomesStateOwner, GoalOutcomeValidationError, goalOutcomeCheckBasis, refineGoalOutcome} from "./outcome_requirements.ts"; +import {bindGoalOutcomesStateOwner, GoalOutcomeValidationError, goalOutcomeCheckBasis, refineGoalOutcome, + outcomeContextReadPlan} from "./outcome_requirements.ts"; /** Acceptance effects use the existing canonical head, CAS and operation journal. * Only trusted local owner/host adapters may invoke these mutation exports. */ import {isAbsolute} from "node:path"; @@ -561,6 +562,7 @@ export async function inspectGoalAcceptance( includeWorkDetails = true, agentId?: string, registeredAgents?: unknown, + contextAgentId?: string, ): Promise { const head = await store.loadAuthority(); if (head.status !== "loaded") return source(store, {...head}); @@ -582,6 +584,14 @@ export async function inspectGoalAcceptance( ...(state ? acceptanceTask(key, todo, state) : {todo_id: key, state: "unbound", criterion_ids: [], applicable: false}), })); + const contract = projectGoalAcceptance(head.head, goalId, registeredAgents); + const context = !includeWorkDetails && contextAgentId !== undefined && todoId !== undefined + ? outcomeContextReadPlan(head.head, goalId, contextAgentId, todoId, contract) : null; + if (context && (!Array.isArray(registeredAgents) || !registeredAgents.includes(contextAgentId))) { + context.complete = false; + context.fallback_reasons = [...context.fallback_reasons as string[], "context_agent_unavailable"]; + context.required_outcome_ids = ((contract.outcome_requirements as JsonObject).outcomes as JsonObject[]).map(o => o.id); + } return source(store, {status: "loaded", provider_revision: head.provider_revision, revision: state?.revision ?? null, contract_digest: state?.digest ?? null, contract: state ? state.document : null, tasks, @@ -589,7 +599,8 @@ export async function inspectGoalAcceptance( completion_requirements: acceptanceCompletionRequirements(head.head, goalId, todoId)}), ...(agentId === undefined || currentGoalObjective(head.head) === null ? {} : {objective_work_digest: goalObjectiveWorkDigest(head.head, agentId), goal_objective_guard: goalObjectiveWorkGuard(head.head, agentId)}), - goal_acceptance_contract: projectGoalAcceptance(head.head, goalId, registeredAgents)}); + ...(context === null ? {} : {outcome_context: {...context, provider_revision: head.provider_revision}}), + goal_acceptance_contract: contract}); } async function local(value: unknown, kind: "inspect" | "configure" | "verify" | "lifecycle" | "refine" | "objective"): Promise { @@ -609,7 +620,8 @@ async function local(value: unknown, kind: "inspect" | "configure" | "verify" | request.goal_ref === undefined ? undefined : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref"), request.include_work_details !== false, request.agent_id === undefined ? undefined : requireAuthorityStoreId(request.agent_id, "agent_id"), - request.registered_agents); + request.registered_agents, + request.context_agent_id === undefined ? undefined : requireAuthorityStoreId(request.context_agent_id, "context_agent_id")); } if (kind === "configure") return configureGoalAcceptance(store, request); if (kind === "refine") return refineGoalAcceptanceOutcome(store, request); diff --git a/loopx/control_plane/goals/outcome_requirements.ts b/loopx/control_plane/goals/outcome_requirements.ts index 47db417fd6..2e88ce295d 100644 --- a/loopx/control_plane/goals/outcome_requirements.ts +++ b/loopx/control_plane/goals/outcome_requirements.ts @@ -7,8 +7,11 @@ import {readGoalAcceptanceLifecycle, sameExactGoalRef, parseWireExactGoalRef, ty import {BARE_SHA256_PATTERN} from "../content_digest.ts"; import {goalAcceptanceTodoDigest} from "./acceptance_work_basis.ts"; +import {projectRelations, todoRef} from "../work_items/planning_relations.ts"; +import {normalizeTodoResumeWhen, TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION} from "../todos/resume_condition.ts"; export const GOAL_OUTCOMES_SCHEMA = "loopx_goal_outcomes_v0"; +export const OUTCOME_CONTEXT_READ_PLAN_SCHEMA = "outcome_context_read_plan_v0"; interface Requirement extends JsonObject {id: string; description: string} interface ReviewItem extends JsonObject {id: string; conclusion: "met" | "unmet" | "uncertain"; evidence_refs: string[]; note: string} interface OutcomeReview extends JsonObject {basis_digest: string; items: ReviewItem[]} @@ -230,6 +233,106 @@ export function projectGoalOutcomes(head: JsonObject, goalId: string, acceptance execution_authority_granted: false, goal_completion_assessed: false}, acceptance); } +/** Scope only the reading plan, never the persisted requirements or admission. + * Use the complete canonical work snapshot and the existing relation owner; + * the bounded planning horizon cannot establish this registered-relation closure. + * Missing work, unresolved relations and unscoped checks retain a full read. + */ +export function outcomeContextReadPlan(head: JsonObject, goalId: string, agentId: string, + todoId: string, acceptance: JsonObject): JsonObject | null { + const declaration = acceptance.outcome_requirements as JsonObject | undefined; + if (!declaration || !Array.isArray(declaration.outcomes)) return null; + const outcomes = declaration.outcomes as JsonObject[]; + const work = indexCoordinationProjectionTodos(head, goalId).todos; + const required = new Set(outcomes.filter(o => o.owner_agent_id === agentId).map(o => String(o.id))); + const related = new Set([todoId]); + const reasons = new Set(); + if (declaration.current_lifetime !== true) reasons.add("outcome_lifetime_unavailable"); + if (!work.has(todoId) || !outcomes.some(o => (o.todo_ids as string[]).includes(todoId))) + reasons.add("selected_work_unbound"); + if (Array.isArray(acceptance.criteria) && acceptance.criteria.length && + (acceptance.scope as JsonObject | undefined)?.kind !== "selected_work") + reasons.add("goal_wide_acceptance"); + const adjacency = new Map>(); + const connect = (left: string, right: string) => { + for (const [from, to] of [[left, right], [right, left]]) { + const neighbors = adjacency.get(from) ?? new Set(); + neighbors.add(to); adjacency.set(from, neighbors); + } + }; + const rows = [...work.values()].map(todo => ({todo_id: String(todo.todo_id), + successor_todo_ids: (todo.successor_todo_ids ?? []) as string[], + ...Object.fromEntries(["unblocks_todo_id", "superseded_by", "resume_when"] + .filter(key => typeof todo[key] === "string" && todo[key] !== "") + .map(key => [key, todo[key]]))})); + const sharedRefs = new Map(); + for (const relation of projectRelations(rows)) { + const target = todoRef(relation.to_ref); + if (target !== null) { + if (!work.has(target)) reasons.add("work_relation_unavailable"); + else connect(relation.from_todo_id, target); + } else if (relation.relation === "resumes_when") { + // Exact external conditions may be shared without naming a Todo. + // No substring/prose matching or invented cross-Agent dependency. + const normalized = normalizeTodoResumeWhen({ + schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: relation.to_ref, + }); + if (normalized === null) { reasons.add("work_relation_unavailable"); continue; } + const key = `${relation.relation}:${normalized}`; + const previous = sharedRefs.get(key); + if (previous) connect(previous, relation.from_todo_id); + else sharedRefs.set(key, relation.from_todo_id); + } else reasons.add("work_relation_unavailable"); + } + const checkScope = acceptance.scope as JsonObject | undefined; + if (checkScope?.kind === "selected_work" && Array.isArray(checkScope.todo_ids)) { + const ids = checkScope.todo_ids as string[]; + for (const key of ids) { + if (!work.has(key)) reasons.add("acceptance_work_unavailable"); + else if (ids[0]) connect(ids[0], key); + } + } + const materialWork = new Map(); + for (const outcome of outcomes) { + const ids = outcome.todo_ids as string[]; + if (!Array.isArray(ids) || !ids.length || ids.some(key => !work.has(key))) { + reasons.add("outcome_work_unavailable"); continue; + } + // One outcome may legitimately span several Agents' work. Ownership is + // not a substitute for its actual linked tasks or their shared acceptance. + for (const key of ids) connect(ids[0], key); + // A common declared request or evidence item may carry shared obligations + // or opposing interpretations. Expand its actual work without deciding + // relevance from descriptions or promoting either review to acceptance. + const refs = [...(outcome.sources as JsonObject[]).map(source => String(source.ref)), + ...(outcome.requirements as JsonObject[]).flatMap(r => + ((r.agent_review as JsonObject | undefined)?.evidence_refs ?? []) as string[])]; + for (const ref of refs) { + const previous = materialWork.get(ref); + if (previous) connect(previous, ids[0]); + else materialWork.set(ref, ids[0]); + } + // A failed/stale current observation cannot be hidden solely by ownership; + // its related work and complete evidence also need assessment. + if ((outcome.requirements as JsonObject[]).some(r => r.status === "failed" || r.status === "stale")) + required.add(String(outcome.id)); + if (required.has(String(outcome.id))) for (const key of ids) related.add(key); + } + const queue = [...related]; + for (let cursor = 0; cursor < queue.length; cursor++) for (const next of adjacency.get(queue[cursor]) ?? []) { + if (!related.has(next)) { related.add(next); queue.push(next); } + } + for (const outcome of outcomes) { + if ((outcome.todo_ids as string[]).some(key => related.has(key))) required.add(String(outcome.id)); + } + if (reasons.size) for (const outcome of outcomes) required.add(String(outcome.id)); + return {schema_version: OUTCOME_CONTEXT_READ_PLAN_SCHEMA, goal_id: goalId, agent_id: agentId, todo_id: todoId, + outcome_revision: declaration.revision, + acceptance_digest: canonicalAuthoritySha256(acceptance), + complete: reasons.size === 0, fallback_reasons: [...reasons].sort(), + required_outcome_ids: outcomes.filter(o => required.has(String(o.id))).map(o => o.id)}; +} + /** Re-use after the host observes oracle freshness; raw stored evidence cannot * become a current passing result without those observations. No state writes. */ export function observeOutcomeRequirements(declaration: JsonObject, acceptance: JsonObject, diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 65e737b803..d8f3ff6c35 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -63,8 +63,7 @@ def bind_exact_turn_settlement_task_body( placeholder, render_turn_bootstrap(value=turn_instance_id), ) - plan_path = ("writeback.settlement_plan.ordered_steps" if captured_envelope - else "interaction_contract.cli_channel.settlement_plan.ordered_steps") + plan_path = "interaction_contract.cli_channel.settlement_plan.ordered_steps" return ( f"{bound}\n\nFor accountable delivery, execute " f"`{plan_path}` " @@ -537,10 +536,9 @@ def _render_goal_task_body( permission_rule=permission_rule, ) envelope_rule = ( - "For TurnEnvelope, honor action/boundary/required_reads; read " - "detail_ref.full_decision for the complete same-invocation contract " - "before selection, work or settlement. Retain transport flags on fresh guards.\n" - if captured_envelope else "" + "Read the complete captured contract before selection, work or settlement; " + "use interaction_contract for action, required reads and settlement. " + "Retain supplied transport flags on fresh guards.\n" ) if captured_context: # The opted-in native Goal has a 4000-character host limit, including @@ -548,10 +546,8 @@ def _render_goal_task_body( # intact and refer to the registered active state instead of repeating # its path. Ordinary/off Goal instructions remain byte-for-byte intact. envelope_rule = ( - "Read complete same-invocation detail_ref.full_decision before " - "selection/work/settlement; honor envelope action/boundary/required_reads; " - "keep fresh-guard flags.\n" - if captured_envelope else "" + "Read the complete captured contract before selection/work/settlement; " + "keep fresh-guard transport flags.\n" ) wait_rule = ( "\n`/goal` owns blocked state. Recheck quota at " @@ -569,7 +565,7 @@ def _render_goal_task_body( `{quota_guard_command}` {envelope_rule}Follow required selection_command/reentry; do/verify authorized work. Settle current settlement_plan.ordered_steps, else next_cli_actions. Exact identities/effect ids; actual outcomes. Never rebuild static refresh/spend; ambiguous effects: readback/recovery, no blind retry. Keep flags; truthful evidence. Authorized entrypoint repair; unavailable/incomplete contract: no work/spend/completion claim. -Read agent_channel.work_context+required_reads (or envelope) before work. Size by task/evidence/risk within authority/budget, not ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, not wake counts. Continue feasible integration/negatives/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: keep original/cumulative requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. +Read interaction_contract.agent_channel.work_context+required_reads before work. Size by task/evidence/risk within authority/budget, not ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, not wake counts. Continue feasible integration/negatives/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: keep original/cumulative requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. Notify affects output only; progress alone creates no host Goal. After settlement recheck quota; obey continuation/wait, no unchanged polls. Complete {completion_subject} only on should_run=false + terminal no-follow-up; else next action/wait.{wait_rule} {policy_tail}""" @@ -669,42 +665,8 @@ def render_thin_heartbeat_task_body( if pr_review_pre_quota_command else "" ) - if captured_envelope: - return f"""Advance `{goal_id}` from {active_state}. - -{HOST_LOOP_SAFETY_RULE} -{scope_sentence} -Use TurnEnvelope: `action.must_attempt` requires work; `user.notify` controls -output only. ok=false -> recovery, no delivery. Language=user. Honor `boundary`, `execution_policy`, `contract_capsule`, -`work_context`, remaining `required_reads`, `agent_context`, and `replan_action_packet` when present. -When present, read inline `agent_context` before planning. Read `detail_ref.full_decision` for -selection, replan, missing/truncated commands or an `agent_context.detail_ref`: -same-invocation observation, not fresh admission. -Selection uses the saved `interaction_contract.cli_channel.selection_command`. -Keep all supplied guard transport flags (--turn-envelope, --decision-output-root, -and --decision-handoff-socket when present) on every fresh selection/repair/replan -guard; each call saves its own immutable observation. -After a workspace or state change, re-enter the guard before continuing work. - -```sh -{render_turn_bootstrap()} -{pr_review_pre_quota_instruction}{quota_guard_command} -``` -{SCOPE_BOUNDED_WORK_RULE} -Observed capabilities -> --available-capability, never a user gate. -Apply `scheduler` as projected using inline ACK argv. Read saved scheduler detail -only for a needed missing/truncated/referenced command; failure argv only after host update failure. -Exact monitor settlement=no refresh/spend; auxiliary poll continues work. -Validate work; execute `writeback.settlement_plan` and `writeback.next_cli_actions` with -their exact identity. Do not guess commands from abbreviated previews. -Writeback needs outcome and vision; unchanged reason requires an existing -current evidence-linked vision. Missing vision after writeback -> same-Turn -checkpoint-context then evidence; stale -> reread. -{HOST_LOOP_TODO_CLOSEOUT_COMPACT_RULE} -P0 blocked -> admitted P1/P2; quiet polls do not spend. -`agent_read_required`: read/triage before work, settle/ACK. -{reward_memory_rule} -{policy_tail}""" + # New captures use one complete contract; the old caller option no longer + # chooses a separate execution view. Historical saved bodies remain intact. return f"""Advance `{goal_id}` from {active_state}. {RUNTIME_EXECUTION_ROUTING_RULE} @@ -712,6 +674,7 @@ def render_thin_heartbeat_task_body( {scope_sentence} {HOST_LOOP_QUOTA_DISPATCH_RULE} +Preserve the supplied capture/handoff flags on selection and same-Turn repair; read the complete captured contract before acting. {HEARTBEAT_TURN_BOOTSTRAP_RULE} ```sh {render_turn_bootstrap()} diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 45ef36f048..f7f304a0b9 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -2,6 +2,7 @@ import json from copy import deepcopy + from typing import Any from .decision_summary import compact_quota_decision @@ -241,93 +242,27 @@ def _compact_vision_continuation_audit( def _reference_inline_replan_authoring( packet: dict[str, Any], *, payload: dict[str, Any] ) -> dict[str, Any]: - """Reference an identical executable input already present in this response.""" + """Legacy display metadata around the shared complete authoring owner.""" + from .turn_envelope import _turn_envelope_result - writeback = packet.get("writeback_contract") - if not isinstance(writeback, dict) or not isinstance( - writeback.get("vision_authoring"), dict - ): - return packet - interaction = payload.get("interaction_contract") - cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None - plan = cli.get("settlement_plan") if isinstance(cli, dict) else None - steps = plan.get("ordered_steps") if isinstance(plan, dict) else None - if not isinstance(steps, list): + shared = _turn_envelope_result("reference_replan_authoring", + payload={**payload, "replan_action_packet": packet})["replan_action_packet"] + if shared == packet: return packet - index = next( - (index for index, step in enumerate(steps) - if isinstance(step, dict) and step.get("kind") == "durable_writeback" - and step.get("vision_authoring") == writeback["vision_authoring"]), - None, - ) - if index is None: - return packet - reference = ( - "$.interaction_contract.cli_channel.settlement_plan.ordered_steps" - f"[{index}].vision_authoring" - ) - compact_writeback = dict(writeback) - compact_writeback.pop("vision_authoring") - compact_writeback["vision_authoring_ref"] = reference - compact = dict(packet) - compact["writeback_contract"] = compact_writeback - compact["payload_compaction"] = { + reference = shared["writeback_contract"]["vision_authoring_ref"] + return {**shared, "payload_compaction": { "schema_version": QUOTA_CLI_REPLAN_ACTION_COMPACTION_SCHEMA_VERSION, "mode": "compact_hot_path", "compacted_fields": ["writeback_contract.vision_authoring"], "projection_ref": reference, - } - return compact + }} -def _vision_continuation_ref(audit: dict[str, Any]) -> dict[str, Any]: - compact = { - key: audit[key] - for key in ("schema_version", "required", "decision") - if key in audit - } - judge = audit.get("vision_gap_judge") - if isinstance(judge, dict): - compact["vision_gap_judge"] = { - key: judge[key] for key in ("done", "decision") if key in judge - } - compact["vision_gap_judge"]["projection_ref"] = ( - "$.vision_continuation_audit.vision_gap_judge" - ) - compact["projection_ref"] = "$.vision_continuation_audit" - return compact - - -def _replace_nested_vision_continuation_audits( - payload: dict[str, Any], - *, - source_audit: dict[str, Any], - audit_ref: dict[str, Any], -) -> None: - frontier = payload.get("goal_frontier_projection") - if ( - isinstance(frontier, dict) - and frontier.get("vision_continuation_audit") == source_audit - ): - compact_frontier = dict(frontier) - compact_frontier["vision_continuation_audit"] = dict(audit_ref) - payload["goal_frontier_projection"] = compact_frontier +def _reference_vision_continuation(payload: dict[str, Any]) -> dict[str, Any]: + """Legacy display delegates exact audit sharing to the complete owner.""" + from .turn_envelope import _turn_envelope_result - interaction = payload.get("interaction_contract") - if not isinstance(interaction, dict): - return - compact_interaction = dict(interaction) - for channel_name in ("agent_channel", "cli_channel"): - channel = compact_interaction.get(channel_name) - if ( - not isinstance(channel, dict) - or channel.get("vision_continuation_audit") != source_audit - ): - continue - compact_channel = dict(channel) - compact_channel["vision_continuation_audit"] = dict(audit_ref) - compact_interaction[channel_name] = compact_channel - payload["interaction_contract"] = compact_interaction + return _turn_envelope_result("reference_vision_continuation", payload=payload) def _deduplicate_next_action_warning( @@ -477,87 +412,25 @@ def _promote_interaction_contract(payload: dict[str, Any]) -> dict[str, Any]: return promoted -def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any]: - """Share exact JSON copies in this display only; never trim the contract. - - Work context is the preferred full carrier, then the planning horizon. - This Python CLI adapter only changes serialization after owner decisions; - canonical state, captures and replan context construction remain untouched. - """ - paths: list[tuple[str | int, ...]] = [] - interaction = payload.get("interaction_contract") - channel = interaction.get("agent_channel") if isinstance(interaction, dict) else None - context = channel.get("work_context") if isinstance(channel, dict) else None - sources = context.get("sources") if isinstance(context, dict) else None - for index, source in enumerate(sources if isinstance(sources, list) else []): - if (isinstance(source, dict) and source.get("source") == "goal_acceptance" - and isinstance(source.get("content"), dict) - and source["content"].get("status", "loaded") == "loaded" - and isinstance(source["content"].get("goal_acceptance_contract"), dict) - and source["content"]["goal_acceptance_contract"]): - paths.append(("interaction_contract", "agent_channel", "work_context", - "sources", index, "content", "goal_acceptance_contract")) - break - paths.extend([ - ("planning_horizon", "core_goal", "acceptance_contract"), - ("autonomous_replan_obligation", "replan_context", "core_goal", "acceptance_contract"), - ("agent_todo_summary", "goal_acceptance_contract"), - ]) - carriers: dict[str, str] = {} - result = payload - for path in paths: - parent: Any = result - for part in path[:-1]: - if isinstance(parent, dict) and isinstance(part, str): - parent = parent.get(part) - elif isinstance(parent, list) and isinstance(part, int) and part < len(parent): - parent = parent[part] - else: - parent = None - break - field = str(path[-1]) - value = parent.get(field) if isinstance(parent, dict) else None - if not isinstance(value, dict) or not value or field + "_ref" in parent: - continue - # Python equality conflates True/1 and False/0. Compare JSON types and - # the complete content, including revision, scope, failures and sources. - encoded = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False) - reference = carriers.get(encoded) - if reference is None: - carriers[encoded] = "$" + "".join( - f"[{part}]" if isinstance(part, int) else f".{part}" for part in path) - continue - if len(encoded) <= len(reference) + 8: - continue - # Copy only the affected spine, so callers retain their original facts. - result = dict(result) - target: Any = result - for part in path[:-1]: - child = target[part] - target[part] = list(child) if isinstance(child, list) else dict(child) - target = target[part] - del target[field] - target[field + "_ref"] = reference - return result - - def _reference_context( payload: dict[str, Any], *, task_title: bool = True, explore_guidance: bool = True, - diagnostics: dict[str, Any] | None = None, + goal_acceptance: bool = False, diagnostics: dict[str, Any] | None = None, ) -> dict[str, Any]: """One coarse transport call for the existing typed context owner.""" contract = payload.get("interaction_contract") agent = contract.get("agent_channel") if isinstance(contract, dict) else None context = agent.get("work_context") if isinstance(agent, dict) else None - # No source carrier means neither operation can act. Leave semantic matching - # and reference validation in TypeScript, rather than replicating its rules. - if diagnostics is None and (not (task_title or explore_guidance) or not isinstance(context, dict) or not context.get("sources")): + # Task/Explore sharing needs a source carrier; acceptance may use other root + # owners. Leave semantic matching and reference validation in TypeScript. + if diagnostics is None and not goal_acceptance and ( + not (task_title or explore_guidance) or not isinstance(context, dict) or not context.get("sources") + ): return payload from .turn_envelope import _turn_envelope_result result = _turn_envelope_result("reference_context", payload=payload, task_title=task_title, explore_guidance=explore_guidance, - **({"diagnostics": diagnostics} if diagnostics is not None else {})) + goal_acceptance=goal_acceptance, **({"diagnostics": diagnostics} if diagnostics is not None else {})) # Existing reference-only callers keep their historical wire contract. return result if diagnostics is None else _apply_context_projection_changes(payload, result) @@ -617,14 +490,8 @@ def compact_quota_should_run_cli_payload( compact["goal_boundary"] = compact_goal_boundary vision_audit = payload.get("vision_continuation_audit") if not include_vision_detail and isinstance(vision_audit, dict): - compact = dict(compact) - compact_vision_audit = _compact_vision_continuation_audit(vision_audit) - compact["vision_continuation_audit"] = compact_vision_audit - _replace_nested_vision_continuation_audits( - compact, - source_audit=vision_audit, - audit_ref=_vision_continuation_ref(compact_vision_audit), - ) + compact = _reference_vision_continuation(compact) + compact["vision_continuation_audit"] = _compact_vision_continuation_audit(vision_audit) replan_action = payload.get("replan_action_packet") if not include_vision_detail and isinstance(replan_action, dict): compact_replan_action = _reference_inline_replan_authoring( @@ -658,14 +525,9 @@ def compact_quota_should_run_cli_payload( agent_lane_next_action = dict(agent_lane_next_action) agent_lane_next_action.pop("content_revision", None) compact["agent_lane_next_action"] = agent_lane_next_action - compact = _reference_inline_goal_acceptance(compact) - diagnostics = None - if ((not include_todo_summary_detail and isinstance(payload.get("agent_todo_summary"), dict)) - or (not include_user_todo_summary_detail and isinstance(payload.get("user_todo_summary"), dict))): - diagnostics = {"agent_detail": include_todo_summary_detail, - "user_detail": include_user_todo_summary_detail} compact = _reference_context(compact, task_title=not include_todo_summary_detail, - explore_guidance=not include_vision_detail, diagnostics=diagnostics) + explore_guidance=not include_vision_detail, goal_acceptance=True, diagnostics={ + "agent_detail": include_todo_summary_detail, "user_detail": include_user_todo_summary_detail}) return _promote_interaction_contract( _promote_runtime_capability_reentry(compact) ) @@ -677,8 +539,6 @@ def compact_quota_plan_cli_payload( """Bound read-only CLI summaries after complete planning; retain full opt-ins.""" if payload.get("mode") not in {"status", "plan"} or not isinstance(payload.get("groups"), dict): return payload - if {"agent-todos", "user-todos"} <= detail_sections: - return payload return _reference_context(payload, task_title=False, explore_guidance=False, diagnostics={ "agent_detail": "agent-todos" in detail_sections, diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index c3a7c71ff7..06145786d2 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -9,6 +9,7 @@ import {reusableTurnContext} from "../capability_hooks.ts"; import {COORDINATION_STATE_CONTRACT} from "../coordination/coordination_state_contract.generated.ts"; const VERSION = "context_delivery_v1"; +export const OUTCOME_CONTEXT_PROJECTION_SCHEMA = "outcome_context_projection_v0"; const MAX_ANCESTRY = 32; type Path = (string | number)[]; type Split = {fresh: JsonObject; valid: boolean}; @@ -165,7 +166,7 @@ function splitContext(context: JsonObject, binding: JsonObject): Split { /** The projection surface is a transport binding, never a new admission mode. * Keep one exact codec for quota and Turn; source-only callers use the same codec. */ -const SURFACES = ["work_context", "quota_packet", "turn_envelope"] as const; +const SURFACES = ["work_context", "quota_packet", "turn_envelope", "complete_turn"] as const; type Surface = typeof SURFACES[number]; type Delta = {value: unknown} | {from: Path; edit?: Delta} | {changed?: Record; values?: Record; removed?: string[]} | {items: Record; values?: Record; length?: number}; @@ -191,6 +192,15 @@ const REUSABLE_PACKET_FIELDS = new Set([ * duplicating them in both a fresh frame and a patch. Static authoring guidance * remains binding via exact reuse; commands, conditions and identity stay fresh. */ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: readonly string[], binding: JsonObject): {paths: Path[]; valid: boolean} { + if (context.schema_version === OUTCOME_CONTEXT_PROJECTION_SCHEMA) { + const inner = jsonObject(context.context); + if (!inner) return {paths: [[]], valid: false}; + const projected = freshPaths(inner, surface, legacyQuotaFields, binding); + return {valid: projected.valid, paths: [ + ...Object.keys(context).filter(key => key !== "context").map(key => [key]), + ...projected.paths.map(path => ["context", ...path]), + ]}; + } const paths: Path[] = []; let valid = true; const leaves = (value: unknown, path: Path) => { diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 7fe75dc784..cc1086b8e3 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -103,6 +103,20 @@ def turn_envelope_replan_settlement_identity( return dict(identity) +def compose_turn_decision( + payload: Mapping[str, Any], *, scheduler_execution_context: object = None, +) -> dict[str, Any]: + """Transport complete source facts to the existing typed composition owner.""" + resolution = resolve_scheduler_execution_context(scheduler_execution_context) + args = render_scheduler_execution_args(scheduler_execution_context=resolution) if resolution.ok and resolution.context is not None else "" + return _turn_envelope_result("compose_complete", payload=dict(payload), scheduler_execution_args=args) + + +def assert_complete_turn_decision(payload: Mapping[str, Any]) -> None: + """Validate the original contract; keep its host binding and caller-owned bytes.""" + _turn_envelope_result("complete_consumer", payload=dict(payload)) + + def build_turn_envelope( payload: Mapping[str, Any], *, @@ -113,7 +127,8 @@ def build_turn_envelope( ) -> dict[str, Any]: """Project one full quota decision through the canonical TS transaction.""" - return _build_turn_projection(payload, scheduler_execution_context=scheduler_execution_context, + return _build_turn_projection(payload, + scheduler_execution_context=scheduler_execution_context, captured_decision_path=captured_decision_path) @@ -138,12 +153,19 @@ def _build_turn_projection( payload: Mapping[str, Any], *, scheduler_execution_context: object = None, captured_decision_path: str | None = None, operation: str = "build", ) -> dict[str, Any]: + resolution = resolve_scheduler_execution_context(scheduler_execution_context) scheduler_execution_args = "" if resolution.ok and resolution.context is not None: scheduler_execution_args = render_scheduler_execution_args( scheduler_execution_context=resolution, ) + # A captured complete contract already records the original host binding. + # Only transport that value when no caller override was supplied; TS checks + # its integrity and rejects an explicit conflicting binding. + if scheduler_execution_context is None and payload.get("schema_version") == "loopx_turn_envelope_v1": + binding = payload.get("contract_binding") + scheduler_execution_args = binding.get("scheduler_execution_args") if isinstance(binding, Mapping) else None return _turn_envelope_result( operation, payload=dict(payload), diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index bfcf7c5c9d..18db6bfc4e 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,11 +1,14 @@ import {contextProjectionChanges, projectQuotaDiagnostics} from "./diagnostic_projection.ts"; import type { LoopXTurnRouteValue } from "../turn_driver/turn_contract_generated.ts"; -import { EffectiveAction, type QuotaEffectiveActionValue } from "./effective_action.generated.ts"; +import { EFFECTIVE_ACTIONS, EffectiveAction, type QuotaEffectiveActionValue } from "./effective_action.generated.ts"; +import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../agents/agent_scope_frontier.generated.ts"; import { createHash } from "node:crypto"; -import { ENVELOPED_SHA256_PATTERN } from "../content_digest.ts"; +import { ENVELOPED_SHA256_PATTERN, BARE_SHA256_PATTERN } from "../content_digest.ts"; import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from "../capabilities/explore_turn_context.ts"; -import {prepareContextDelivery, restoreContextDelivery} from "./context_delivery.ts"; +import {prepareContextDelivery, restoreContextDelivery, OUTCOME_CONTEXT_PROJECTION_SCHEMA} from "./context_delivery.ts"; import {updateNativeContextLifecycle} from "./native_context_lifecycle.ts"; +import {OUTCOME_CONTEXT_READ_PLAN_SCHEMA} from "../goals/outcome_requirements.ts"; +import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; import { interpretQuotaShouldRunPacket, @@ -18,7 +21,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { projectTurnStartUnavailableContext } from "../capability_hooks.ts"; import { jsonObject, requireBoolean, requireJsonObject, requireNonEmptyString, requireStringLiteral } from "../runtime_decode.ts"; import { projectPendingCapabilityIntent } from "../work_items/pending_capability_intent.ts"; -import { projectInteractionRequiredReads, selectedWorkTodoId, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; +import { capturedTodoTextSha256, decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodoId, scopeInteractionOutcomeReadGuidance, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; import { measureTurnEnvelope, TURN_ENVELOPE_SECTION_TARGETS } from "./turn_envelope_budget.ts"; export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; @@ -221,6 +224,339 @@ function canonicalHash(value: unknown): string { return `sha256:${createHash("sha256").update(JSON.stringify(canonicalValue(value)), "utf8").digest("hex")}`; } +/** Existing acceptance carriers only: this is not a general JSON-path codec. + * The loaded work context is preferred, then the complete planning/replan facts. + */ +function acceptanceCarriers(payload: JsonObject) { + // Keep carrier parents attached to the cloned document. The general object() + // helper intentionally returns copies for projection callers. + const carrierObject = (value: unknown): JsonObject => + typeof value === "object" && value !== null && !Array.isArray(value) + ? value as JsonObject : {}; + const carriers: {parent: JsonObject; field: string; path: string}[] = []; + const sources = carrierObject(carrierObject(carrierObject(payload.interaction_contract).agent_channel).work_context).sources; + if (Array.isArray(sources)) { + const index = sources.findIndex(value => { + const source = carrierObject(value), content = carrierObject(source.content); + return source.source === "goal_acceptance" && + (content.status === undefined || content.status === "loaded") && + Object.keys(carrierObject(content.goal_acceptance_contract)).length > 0; + }); + if (index >= 0) carriers.push({parent: carrierObject(carrierObject(sources[index]).content), + field: "goal_acceptance_contract", + path: `$.interaction_contract.agent_channel.work_context.sources[${index}].content.goal_acceptance_contract`}); + } + carriers.push( + {parent: carrierObject(carrierObject(payload.planning_horizon).core_goal), field: "acceptance_contract", + path: "$.planning_horizon.core_goal.acceptance_contract"}, + {parent: carrierObject(carrierObject(carrierObject(payload.autonomous_replan_obligation).replan_context).core_goal), + field: "acceptance_contract", path: "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract"}, + {parent: carrierObject(payload.agent_todo_summary), field: "goal_acceptance_contract", + path: "$.agent_todo_summary.goal_acceptance_contract"}, + ); + return carriers; +} + +function inlineAcceptance(payload: JsonObject, reference: unknown): JsonObject { + // Older captures may name any loaded work-context carrier, not only the + // preferred first source chosen for new sharing. + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.goal_acceptance_contract$/.exec(reference) : null; + if (match) { + const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; + const source = object(Array.isArray(sources) ? sources[Number(match[1])] : null); + const content = object(source.content); + if (source.source === "goal_acceptance" && + (content.status == null || content.status === "loaded") && + Object.keys(object(content.goal_acceptance_contract)).length) { + return object(content.goal_acceptance_contract); + } + } + const carrier = acceptanceCarriers(payload).find(item => item.path === reference); + const value = object(carrier?.parent[carrier.field]); + if (!carrier || !Object.keys(value).length) { + throw new EffectRuntimeRequestError("Goal acceptance reference must resolve to complete inline work context or planning facts"); + } + return value; +} + +function referenceGoalAcceptance(payload: JsonObject): JsonObject { + const result = structuredClone(payload); + const owners = new Map(); + const relocated = new Map(); + const carriers = acceptanceCarriers(result); + for (const {parent, field, path} of carriers) { + const value = object(parent[field]); + if (!Object.keys(value).length || Object.hasOwn(parent, `${field}_ref`)) continue; + // Complete canonical JSON distinguishes booleans/numbers, revisions, + // provenance and negative evidence. Never deduplicate a prose summary. + const encoded = JSON.stringify(canonicalValue(value)); + const owner = owners.get(encoded); + if (!owner) owners.set(encoded, path); + else if (Buffer.byteLength(encoded, "utf8") > Buffer.byteLength(owner, "utf8") + 8) { + delete parent[field]; + parent[`${field}_ref`] = owner; + relocated.set(path, owner); + } + } + // A historical source can already reference a carrier that was just shared. + // Rebind that exact reference to the retained inline owner, never a chain. + for (const {parent, field} of carriers) { + const reference = parent[`${field}_ref`]; + if (typeof reference === "string" && relocated.has(reference)) { + parent[`${field}_ref`] = relocated.get(reference); + } + } + return result; +} + +/** One registry for ordinary captured reads and staged replan. Only registered + * acceptance carriers participate; unrelated objects named `outcomes` are not + * sources. Equal content shares a reference, while differing revisions of the + * same outcome retain their complete bodies, including unknown fields. + */ +function capturedAcceptanceOutcomes(payload: JsonObject): Map | null { + const outcomes = new Map(); + const carriers = acceptanceCarriers(payload); + // Sharing prefers the first source, but a diagnostic index must cover every + // loaded registered carrier, including different same-id revisions. + const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; + if (Array.isArray(sources)) for (const value of sources) { + const source = object(value), content = object(source.content); + if (source.source === "goal_acceptance" && (content.status == null || content.status === "loaded")) { + carriers.push({parent: content, field: "goal_acceptance_contract", path: ""}); + } + } + let hasSource = false; + for (const {parent, field} of carriers) { + const acceptance = Object.hasOwn(parent, `${field}_ref`) + ? inlineAcceptance(payload, parent[`${field}_ref`]) : object(parent[field]); + if (!Object.keys(acceptance).length) continue; + hasSource = true; + const requirements = object(acceptance.outcome_requirements); + if (!Object.hasOwn(acceptance, "outcome_requirements")) continue; + if (!Array.isArray(requirements.outcomes)) throw new EffectRuntimeRequestError("captured outcome source is unsupported"); + for (const value of requirements.outcomes) { + const outcome = requireJsonObject(value, "captured acceptance outcome"); + outcomes.set(canonicalHash(outcome), outcome); + } + } + return hasSource ? outcomes : null; +} + +/** Expose existing facts, not another acceptance verdict. Missing values stay + * absent; an Agent's `met` review is not a current independent check. + */ +function capturedOutcomeIndex(outcomes: ReadonlyMap): JsonObject[] { + return [...outcomes].map(([ref, outcome]) => { + const states = new Map(); + if (Array.isArray(outcome.requirements)) for (const value of outcome.requirements) { + const requirement = requireJsonObject(value, "captured outcome requirement"); + const review = jsonObject(requirement.agent_review); + const state = { + ...Object.fromEntries(["status", "planning_satisfied"].filter(key => Object.hasOwn(requirement, key)) + .map(key => [key, requirement[key]])), + ...(Object.hasOwn(requirement, "agent_review") ? {agent_review: review === null + ? requirement.agent_review : Object.fromEntries( + ["conclusion", "basis_current", "independently_verified"].filter(key => Object.hasOwn(review, key)) + .map(key => [key, review[key]]))} : {}), + }; + const key = JSON.stringify(canonicalValue(state)); + const group = states.get(key); + if (group) (group.requirement_ids as unknown[]).push(requirement.id); + else states.set(key, {requirement_ids: [requirement.id], ...state}); + } + return {outcome_ref: ref, id: outcome.id, objective: outcome.objective, revision: outcome.revision, + ...Object.fromEntries(["owner_agent_id", "work", ...(Object.hasOwn(outcome, "work") ? [] : ["todo_ids"])].filter(key => Object.hasOwn(outcome, key)) + .map(key => [key, outcome[key]])), + requirement_count: Array.isArray(outcome.requirements) ? outcome.requirements.length : null, + requirement_states: Array.isArray(outcome.requirements) ? [...states.values()] : null}; + }); +} + +/** A captured, source-owned reading plan can prioritize exact detail reads. + * This does not relax the existing complete-capture obligation. The eventual + * scope cutover must also migrate delivery and Host recovery together. + */ +function capturedOutcomeReadPlan(payload: JsonObject, outcomes: ReadonlyMap): { + required: ReadonlySet; facts: JsonObject; +} { + const agent = payload.agent_id ?? object(payload.agent_identity).agent_id; + const todo = selectedWorkTodoId(payload); + const all = new Set(outcomes.keys()); + const fallback = (reason: string) => ({required: all, + facts: {complete: false, fallback_reasons: [reason], scope_applied: false}}); + if (typeof agent !== "string" || typeof todo !== "string") return fallback("selected_work_unbound"); + if (payload.agent_id !== undefined && object(payload.agent_identity).agent_id !== undefined && + payload.agent_id !== object(payload.agent_identity).agent_id) return fallback("captured_scope_unavailable"); + if (Object.keys(object(payload.autonomous_replan_obligation)).length || + Object.keys(object(payload.replan_action_packet)).length) return fallback("broad_replan_read"); + const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; + const plans = (Array.isArray(sources) ? sources : []).map(object) + .filter(source => source.source === "goal_acceptance").map(source => object(source.content)) + .filter(content => content.status === "loaded" && object(content.outcome_context).complete === true); + const covered = new Set(), required = new Set(); + for (const content of plans) { + const plan = object(content.outcome_context), acceptance = object(content.goal_acceptance_contract); + const declaration = object(acceptance.outcome_requirements); + if (plan.schema_version !== OUTCOME_CONTEXT_READ_PLAN_SCHEMA || + plan.goal_id !== payload.goal_id || plan.agent_id !== agent || plan.todo_id !== todo || + typeof content.provider_revision !== "string" || plan.provider_revision !== content.provider_revision || + plan.outcome_revision !== declaration.revision || !Array.isArray(declaration.outcomes) || + !Array.isArray(plan.required_outcome_ids) || plan.required_outcome_ids.some(id => typeof id !== "string")) continue; + try { + // Bind the whole source, including unknown evidence and negative facts. + // Later oracle observations or an upgraded projection widen to full; + // the reader does not recompute scope from a different observation. + if (plan.acceptance_digest !== canonicalAuthoritySha256(acceptance)) continue; + } catch { continue; } + const ids = new Set(plan.required_outcome_ids); + const rows = declaration.outcomes.map(object); + if (rows.some(outcome => outcome.owner_agent_id === agent && !ids.has(outcome.id)) || + [...ids].some(id => !rows.some(outcome => outcome.id === id))) continue; + for (const outcome of rows) { + const ref = canonicalHash(outcome); + covered.add(ref); + if (ids.has(outcome.id)) required.add(ref); + } + } + if ([...all].some(ref => !covered.has(ref))) return fallback("captured_scope_unavailable"); + return {required, facts: {complete: true, fallback_reasons: [], scope_applied: false, + goal_id: payload.goal_id, agent_id: agent, todo_id: todo}}; +} + +/** A model presentation, never a partial canonical decision. The full source + * remains immutable; native baseline/recovery stores this exact presentation, + * so an unread optional body cannot become a retained requirement later. */ +function capturedOutcomeContext(request: JsonObject): JsonObject { + const payload = requireJsonObject(request.payload, "captured outcome source"); + const outcomes = capturedAcceptanceOutcomes(payload); + if (payload.ok !== true || outcomes === null || !outcomes.size) return payload; + const plan = capturedOutcomeReadPlan(payload, outcomes); + if (plan.facts.complete !== true || plan.required.size === outcomes.size) return payload; + // Unknown outcome-local fields may carry new obligations. Preserve full + // delivery until the existing projection owner explicitly supports them. + const known = (value: JsonObject, keys: string[]) => Object.keys(value).every(key => keys.includes(key)); + for (const [ref, row] of outcomes) { + if (plan.required.has(ref)) continue; + if (!known(row, ["id", "owner_agent_id", "objective", "revision", "requirements", "todo_ids", "sources", "basis_digest", "work"]) || + !Array.isArray(row.requirements) || row.requirements.some(value => { + const requirement = object(value), review = jsonObject(requirement.agent_review); + return !known(requirement, ["id", "description", "status", "planning_satisfied", "agent_review"]) || + (Object.hasOwn(requirement, "agent_review") && (!review || !known(review, + ["id", "conclusion", "evidence_refs", "note", "basis_current", "independently_verified"]))); + }) || !Array.isArray(row.sources) || row.sources.some(value => !known(object(value), ["ref", "summary"])) || + !Array.isArray(row.work) || row.work.some(value => !known(object(value), ["todo_id", "status"]))) return payload; + } + const source = requireJsonObject(request.source_capture, "original capture reference"); + requireNonEmptyString(source.path, "original capture path"); + if (typeof source.sha256 !== "string" || !BARE_SHA256_PATTERN.test(source.sha256)) + throw new EffectRuntimeRequestError("outcome context needs the original file digest"); + const context = structuredClone(payload), carriers = acceptanceCarriers(context); + const channel = jsonObject(jsonObject(context.interaction_contract)?.agent_channel); + if (!channel || !scopeInteractionOutcomeReadGuidance(channel)) return payload; + const sources = jsonObject(jsonObject(jsonObject(context.interaction_contract)?.agent_channel)?.work_context)?.sources; + if (Array.isArray(sources)) for (const value of sources) { + const entry = jsonObject(value), content = jsonObject(entry?.content); + if (entry?.source === "goal_acceptance" && content?.status === "loaded") + carriers.push({parent: content, field: "goal_acceptance_contract", path: ""}); + } + for (const {parent, field} of carriers) { + const acceptance = jsonObject(parent[field]), declaration = jsonObject(acceptance?.outcome_requirements); + if (Array.isArray(declaration?.outcomes)) declaration.outcomes = declaration.outcomes + .filter(row => plan.required.has(canonicalHash(row))); + } + return {schema_version: OUTCOME_CONTEXT_PROJECTION_SCHEMA, + source_capture: {path: source.path, sha256: source.sha256}, + read_scope: {...plan.facts, scope_applied: true, required_outcome_refs: [...plan.required]}, + outcome_index: capturedOutcomeIndex(outcomes).map(row => ({...row, + required_in_scope: plan.required.has(String(row.outcome_ref))})), + instruction: "This is the complete required model context, not a canonical decision or execution authority. " + + "Read this entire presentation before dependent work. The nested context expands every required outcome; " + + "other independent outcomes retain current index facts and exact references in source_capture. " + + "Use quota context-read on that original file/hash with --outcome-ref for complete detail before relying on it. " + + "Goal-wide acceptance or unproven relevance requires the complete original source. " + + "After context loss recover this exact presentation, not a fresh admission. Original requirements and evidence remain binding.", + context}; +} + +function capturedOutcomeRead(request: JsonObject): JsonObject { + let payload = requireJsonObject(request.payload, "captured outcome source"); + let scoped = false; + if (payload.schema_version === OUTCOME_CONTEXT_PROJECTION_SCHEMA) { + const original = requireJsonObject(request.source_payload, "original outcome source"); + const expected = capturedOutcomeContext({payload: original, source_capture: payload.source_capture}); + if (canonicalHash(expected) !== canonicalHash(payload)) + throw new EffectRuntimeRequestError("scoped context differs from its original source; recover the original complete capture"); + payload = original; scoped = true; + } + if (payload.ok !== true) throw new EffectRuntimeRequestError("outcome read requires an original successful capture"); + if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { + requireCompleteTurnDecision(payload); + } + const outcomes = capturedAcceptanceOutcomes(payload); + if (outcomes === null) throw new EffectRuntimeRequestError("capture has no registered acceptance source; recover the original complete capture"); + const common = {ok: true, goal_id: payload.goal_id, + agent_id: payload.agent_id ?? object(payload.agent_identity).agent_id}; + if (request.outcome_ref === "") { + const plan = capturedOutcomeReadPlan(payload, outcomes); + return {...common, read_plan: {...plan.facts, scope_applied: scoped}, outcomes: capturedOutcomeIndex(outcomes) + .map(row => ({...row, required_in_scope: plan.required.has(String(row.outcome_ref))})), + instruction: (scoped ? "Read the complete required presentation; other detail must be read before reliance. " : + "This index and reading plan do not replace full-read obligations; scope_applied is false. ") + + "Read required complete outcome detail before a dependent decision. Unknown scope retains all outcomes. " + + "Planning satisfaction, Agent review and independent checks remain distinct; no execution authority or fresh admission is granted."}; + } + const ref = request.outcome_ref; + const outcome = typeof ref === "string" && ENVELOPED_SHA256_PATTERN.test(ref) ? outcomes.get(ref) : null; + if (!outcome) throw new EffectRuntimeRequestError("acceptance read requires an exact captured outcome reference"); + return {...common, outcome_ref: ref, outcome}; +} + +/** Reuse the existing settlement authoring owner, never a summary or a chain. */ +function replanAuthoringContract(payload: JsonObject): JsonObject { + const contract = object(object(payload.replan_action_packet).writeback_contract); + if (!Object.hasOwn(contract, "vision_authoring_ref")) return contract; + const reference = contract.vision_authoring_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.cli_channel\.settlement_plan\.ordered_steps\[(0|[1-9]\d*)\]\.vision_authoring$/.exec(reference) : null; + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + const step = object(match && Array.isArray(steps) ? steps[Number(match[1])] : null); + if (Object.hasOwn(contract, "vision_authoring") || step.kind !== "durable_writeback" || + Object.hasOwn(step, "vision_authoring_ref") || !Object.keys(object(step.vision_authoring)).length) { + throw new EffectRuntimeRequestError("Replan authoring reference must resolve to one complete inline durable-writeback owner"); + } + delete contract.vision_authoring_ref; + return {...contract, vision_authoring: step.vision_authoring}; +} + +function referenceReplanAuthoring(payload: JsonObject): JsonObject { + const packet = object(payload.replan_action_packet); + const contract = object(packet.writeback_contract); + if (Object.hasOwn(contract, "vision_authoring_ref")) { + replanAuthoringContract(payload); + return payload; + } + const authoring = object(contract.vision_authoring); + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + if (!Object.keys(authoring).length || !Array.isArray(steps)) return payload; + const encoded = JSON.stringify(canonicalValue(authoring)); + const index = steps.findIndex(value => { + const step = object(value); + return step.kind === "durable_writeback" && !Object.hasOwn(step, "vision_authoring_ref") && + JSON.stringify(canonicalValue(step.vision_authoring)) === encoded; + }); + if (index < 0) return payload; + const reference = `$.interaction_contract.cli_channel.settlement_plan.ordered_steps[${index}].vision_authoring`; + if (Buffer.byteLength(encoded, "utf8") <= Buffer.byteLength(reference, "utf8") + 8) return payload; + delete contract.vision_authoring; + return {...payload, replan_action_packet: {...packet, writeback_contract: { + ...contract, vision_authoring_ref: reference}}}; +} + +/** Same-capture references have one typed settlement owner. Historical readers + * expand before moving work context, so a full-packet path never dangles in a + * staged envelope. Explore's independent read and domain guidance stay intact. */ function resolveExploreGuidance(payload: JsonObject, context: JsonObject): JsonObject { if (!Array.isArray(context.sources)) return context; const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; @@ -274,6 +610,69 @@ function referenceExploreGuidance(payload: JsonObject): JsonObject { work_context: {...context, sources}}}}; } +/** Keep the complete vision audit once. These existing projection references + * share identical facts only; a channel-specific obligation is never a summary + * of the root. The old display adapter delegates here before adding metadata. */ +function visionAuditReference(audit: JsonObject): JsonObject { + const reference: JsonObject = {projection_ref: "$.vision_continuation_audit"}; + for (const key of ["schema_version", "required", "decision"]) { + if (Object.hasOwn(audit, key)) reference[key] = audit[key]; + } + if (typeof audit.vision_gap_judge === "object" && audit.vision_gap_judge !== null && + !Array.isArray(audit.vision_gap_judge)) { + const judge = object(audit.vision_gap_judge); + reference.vision_gap_judge = {projection_ref: "$.vision_continuation_audit.vision_gap_judge"}; + for (const key of ["done", "decision"]) { + if (Object.hasOwn(judge, key)) (reference.vision_gap_judge as JsonObject)[key] = judge[key]; + } + } + return reference; +} + +function mapNestedVisionAudits(payload: JsonObject, visit: (audit: JsonObject) => JsonObject): JsonObject { + const map = (value: unknown) => { + const parent = object(value), audit = parent.vision_continuation_audit; + if (typeof audit !== "object" || audit === null || Array.isArray(audit)) return value; + return {...parent, vision_continuation_audit: visit(audit as JsonObject)}; + }; + const rawInteraction = payload.interaction_contract, interaction = object(rawInteraction); + const hasInteraction = typeof rawInteraction === "object" && rawInteraction !== null && !Array.isArray(rawInteraction); + return {...payload, + ...(Object.hasOwn(payload, "goal_frontier_projection") ? {goal_frontier_projection: map(payload.goal_frontier_projection)} : {}), + ...(hasInteraction ? {interaction_contract: {...interaction, + ...Object.fromEntries(["agent_channel", "cli_channel"].filter(key => Object.hasOwn(interaction, key)) + .map(key => [key, map(interaction[key])]))}} : {})}; +} + +function requireVisionAuditReferences(payload: JsonObject): void { + const owner = object(payload.vision_continuation_audit); + const chained = Object.hasOwn(owner, "projection_ref") || Object.hasOwn(object(owner.vision_gap_judge), "projection_ref"); + if (chained) throw new EffectRuntimeRequestError("Vision audit reference requires a complete inline root owner"); + mapNestedVisionAudits(payload, audit => { + if (Object.hasOwn(audit, "projection_ref") || Object.hasOwn(object(audit.vision_gap_judge), "projection_ref")) { + if (!Object.keys(owner).length || + JSON.stringify(canonicalValue(audit)) !== JSON.stringify(canonicalValue(visionAuditReference(owner)))) { + throw new EffectRuntimeRequestError("Vision audit reference must match its complete inline root owner without conflicting facts"); + } + } + return audit; + }); +} + +function referenceVisionContinuation(payload: JsonObject): JsonObject { + requireVisionAuditReferences(payload); + const owner = object(payload.vision_continuation_audit); + if (!Object.keys(owner).length) return payload; + const encoded = JSON.stringify(canonicalValue(owner)), reference = visionAuditReference(owner); + // A short audit is cheaper inline. Do not create a reference merely to count sharing. + if (Buffer.byteLength(encoded, "utf8") <= Buffer.byteLength(JSON.stringify(reference), "utf8")) return payload; + return mapNestedVisionAudits(payload, audit => + JSON.stringify(canonicalValue(audit)) === encoded ? structuredClone(reference) : audit); +} + +/** The verified current Task is the only owner of a shared lane title. Keep + * the original declaration inline and require exact identity, never a label or + * prefix comparison. The legacy display and complete writer use this owner. */ function currentTaskTitleOwner(payload: JsonObject): {index: number; todo: JsonObject} | null { const lane = object(payload.agent_lane_next_action), selected = object(payload.selected_todo); const context = object(object(object(payload.interaction_contract).agent_channel).work_context); @@ -290,6 +689,20 @@ function currentTaskTitleOwner(payload: JsonObject): {index: number; todo: JsonO return {index, todo}; } +function requireCurrentTaskTitleReference(payload: JsonObject): void { + const lane = object(payload.agent_lane_next_action); + if (!Object.hasOwn(lane, "title_ref")) return; + const reference = lane.title_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.todo\.(text|title)$/.exec(reference) : null; + const owner = currentTaskTitleOwner(payload); + if (Object.hasOwn(lane, "title") || !match || !owner || owner.index !== Number(match[1]) || + typeof owner.todo[match[2]] !== "string" || !owner.todo[match[2]] || + Object.hasOwn(owner.todo, `${match[2]}_ref`)) { + throw new EffectRuntimeRequestError("Current Task title reference must resolve to the complete verified selected Task"); + } +} + function referenceCurrentTaskTitle(payload: JsonObject): JsonObject { const lane = object(payload.agent_lane_next_action), title = lane.title; // Preserve historical caller-owned references; complete readers validate them @@ -390,22 +803,13 @@ function deferredReplanWriteback(writeback: JsonObject): JsonObject { */ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRef = "$.core_goal") { const context = object(object(payload.autonomous_replan_obligation).replan_context); - const outcomes = new Map(); + const outcomes = (reads ? capturedAcceptanceOutcomes(payload) : null) ?? new Map(); function indexedGoal(value: unknown): JsonObject { const core = {...object(value)}; if (!("acceptance_contract" in core) && "acceptance_contract_ref" in core) { - const match = typeof core.acceptance_contract_ref === "string" - ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.goal_acceptance_contract$/.exec(core.acceptance_contract_ref) : null; - const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; - const source = object(match && Array.isArray(sources) ? sources[Number(match[1])] : null); - const content = object(source.content); - if (!match || source.source !== "goal_acceptance" - || content.status != null && content.status !== "loaded" - || !Object.keys(object(content.goal_acceptance_contract)).length) { - throw new EffectRuntimeRequestError("Goal acceptance reference must resolve to complete inline work context"); - } + const acceptance = inlineAcceptance(payload, core.acceptance_contract_ref); delete core.acceptance_contract_ref; - core.acceptance_contract = content.goal_acceptance_contract; + core.acceptance_contract = acceptance; } const acceptance = {...object(core.acceptance_contract)}; const requirements = object(acceptance.outcome_requirements); @@ -413,7 +817,6 @@ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRe const refs = requirements.outcomes.map(value => { const outcome = requireJsonObject(value, "captured acceptance outcome"); const ref = canonicalHash(outcome); - outcomes.set(ref, outcome); return ref; }); const {outcomes: _outcomes, ...metadata} = requirements; @@ -432,13 +835,24 @@ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRe } const acceptanceIndex = outcomes.size ? { read_action_template: `${reads!.acceptance} --outcome-ref {outcome_ref}`, - outcomes: [...outcomes].map(([ref, outcome]) => ({outcome_ref: ref, id: outcome.id, - objective: outcome.objective, revision: outcome.revision, - requirement_count: Array.isArray(outcome.requirements) ? outcome.requirements.length : null})), + outcomes: capturedOutcomeIndex(outcomes), } : null; return {core, horizon, outcomes, acceptanceIndex}; } +/** Applicability of Explore's existing attachment owner, shared by complete + * composition and the historical staged projection. No authority is granted. */ +function unboundExploreReplan(payload: JsonObject, plan: JsonObject): boolean { + const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; + const attachments = object(steps.find(step => step.kind === "durable_writeback")).optional_attachments; + const explore = Array.isArray(attachments) ? attachments.map(object).filter(row => row.capability_id === "explore") : []; + const identity = object(plan.identity); + return payload.effective_action === EffectiveAction.AUTONOMOUS_REPLAN_REQUIRED && + selectedWorkTodoId(payload) === null && identity.binding_kind === "autonomous_replan" && + identity.replan_obligation_id === object(payload.replan_action_packet).obligation_id && + !identity.todo_id && explore.length === 0; +} + /** Move only an identical acceptance copy behind the existing captured read. * Different revisions, other source fields and unavailable context stay intact. */ @@ -452,11 +866,7 @@ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; const attachments = object(steps.find(step => step.kind === "durable_writeback")).optional_attachments; const explore = Array.isArray(attachments) ? attachments.map(object).filter(row => row.capability_id === "explore") : []; - const identity = object(plan.identity); - const unboundReplan = payload.effective_action === EffectiveAction.AUTONOMOUS_REPLAN_REQUIRED && - selectedWorkTodoId(payload) === null && identity.binding_kind === "autonomous_replan" && - identity.replan_obligation_id === object(payload.replan_action_packet).obligation_id && - !identity.todo_id && explore.length === 0; + const unboundReplan = unboundExploreReplan(payload, plan); return {...context, sources: context.sources.map(value => { const source = object(value), content = object(source.content); if (source.source === "turn_start_capability_hook" && source.hook_id === "explore.turn_context" && @@ -474,11 +884,36 @@ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject })}; } +/** Bind successor authoring to the already-decided Turn. This formatting + * owner is shared by complete composition and historical staged reads; it does + * not select work, create a successor or change the settlement plan. + */ +function bindReplanSuccessorContract(value: JsonObject, plan: JsonObject): JsonObject { + const contract = {...value}; + const identity = object(plan.identity); + if (typeof identity.turn_instance_id !== "string") return contract; + const goalRef = object(plan.goal_ref); + contract.successor_binding_args = `--turn-instance-id ${shellQuote(identity.turn_instance_id)}` + + (typeof goalRef.goal_instance_id === "string" ? ` --goal-instance-id ${shellQuote(goalRef.goal_instance_id)}` : ""); + contract.successor_settlement = "If creating a replan successor, include successor_binding_args with its obligation id, then follow the creation receipt's original Turn settlement plan before ending; do not run the successor."; + delete contract.successor_host_action; + if (typeof contract.successor_command === "string") { + contract.successor_command = `${contract.successor_command} ${contract.successor_binding_args}`; + } + return contract; +} + function replanView(request: JsonObject): JsonObject { const payload = requireJsonObject(request.payload, "captured replan decision"); if (request.source_hash !== canonicalHash(payload) || payload.ok !== true) { throw new EffectRuntimeRequestError("Replan read requires the original successful decision and matching source hash"); } + // A matching file hash proves which bytes were read, not whether a modern + // contract's version and inner binding are supported. Historical unversioned + // captures keep their decoder; modern captures must pass their shared owner. + if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { + requireCompleteTurnDecision(payload); + } const packet = object(payload.replan_action_packet); const obligation = object(payload.autonomous_replan_obligation); const context = object(obligation.replan_context); @@ -505,7 +940,7 @@ function replanView(request: JsonObject): JsonObject { // duplicate context are available in the unchanged full captured source. const stride = {...object(packet.stride_control)}; for (const key of ["decision_followthrough", "explore_basis", "decision_value_review", "minimal_example"]) delete stride[key]; - const contract = {...object(packet.writeback_contract)}; + const contract = replanAuthoringContract(payload); if (contract.vision_authoring) { const authoring = {...object(contract.vision_authoring)}; delete authoring.minimal_example; @@ -513,23 +948,16 @@ function replanView(request: JsonObject): JsonObject { } const writeback = actionProjection(payload, false).writeback; const plan = object(object(writeback).settlement_plan); - const identity = object(plan.identity); - if (typeof identity.turn_instance_id === "string") { - const goalRef = object(plan.goal_ref); - contract.successor_binding_args = `--turn-instance-id ${shellQuote(identity.turn_instance_id)}` + - (typeof goalRef.goal_instance_id === "string" ? ` --goal-instance-id ${shellQuote(goalRef.goal_instance_id)}` : ""); - contract.successor_settlement = "If creating a replan successor, include successor_binding_args with its obligation id, then follow the creation receipt's original Turn settlement plan before ending; do not run the successor."; - delete contract.successor_host_action; - if (typeof contract.successor_command === "string") { - contract.successor_command = `${contract.successor_command} ${contract.successor_binding_args}`; - // Creation returns the original settlement plan directly. A guard - // reentry here would repeat admission/context and can select new work. - object(writeback).next_cli_actions = [contract.successor_command]; - } + const boundContract = payload.schema_version === COMPLETE_TURN_DECISION_VERSION + ? contract : bindReplanSuccessorContract(contract, plan); + if (typeof boundContract.successor_command === "string" && typeof object(plan.identity).turn_instance_id === "string") { + // Creation returns the original settlement plan directly. A guard + // reentry would repeat admission/context and can select new work. + object(writeback).next_cli_actions = [boundContract.successor_command]; } const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; const refresh = steps.find(step => step.kind === "durable_writeback"); - return {...common, ...sourceRead, writeback_contract: contract, + return {...common, ...sourceRead, writeback_contract: boundContract, required_outcome: packet.required_outcome, allowed_terminal: packet.allowed_terminal, ...(packet.stride_control ? {stride_control: stride} : {}), writeback, @@ -594,7 +1022,7 @@ function replanActionPacket(payload: JsonObject, reads = replanReadCommands(payl // The typed replan owner supplies bounded instructions; do not truncate their // authority/stop qualifiers through generic diagnostic compaction. if (Array.isArray(source.planning_guidance)) compact.planning_guidance = source.planning_guidance; - const writeback = object(source.writeback_contract); + const writeback = replanAuthoringContract(payload); if (writeback.vision_authoring) compact.writeback_contract = writeback; return Object.keys(compact).length > 0 ? compact : null; } @@ -1229,6 +1657,156 @@ function coldPath( }; } +/** Complete decision composition precedes capture. Domain owners have already + * decided admission, work and settlement; this boundary does not decide again. + * The v0 envelope remains an internal executor adapter during reader migration. + */ +export const COMPLETE_TURN_DECISION_VERSION = "loopx_turn_envelope_v1"; + +function requireCompleteDecisionSource(source: JsonObject): void { + assertSupportedQuotaPacket(source); + // Validate executable semantics with their owner, retaining the original + // object (including optional facts) rather than its normalized decode. + decodeInteractionContract(source.interaction_contract); + replanAuthoringContract(source); + requireVisionAuditReferences(source); + requireCurrentTaskTitleReference(source); + resolveExploreGuidance(source, object(object(object(source.interaction_contract).agent_channel).work_context)); + // This root field selects execution, unlike optional evidence carried intact + // in the source. Reuse both registered owners; do not inherit the historical + // adapter's unknown-action fallback at the new version boundary. + requireStringLiteral(source.effective_action, + [...EFFECTIVE_ACTIONS, ...AGENT_SCOPE_FRONTIER_ACTIONS], "Turn effective_action"); + for (const {parent, field} of acceptanceCarriers(source)) { + if (!Object.hasOwn(parent, field) && Object.hasOwn(parent, `${field}_ref`)) { + inlineAcceptance(source, parent[`${field}_ref`]); + } + } +} + +export function requireCompleteTurnDecision(value: unknown): JsonObject { + const decision = requireJsonObject(value, "complete Turn decision"); + if (decision.schema_version !== COMPLETE_TURN_DECISION_VERSION) { + throw new EffectRuntimeRequestError("unsupported complete Turn decision version"); + } + const binding = requireJsonObject(decision.contract_binding, "Turn contract binding"); + if (typeof binding.scheduler_execution_args !== "string") { + throw new EffectRuntimeRequestError("Turn scheduler execution binding must be a string"); + } + const unsignedBinding = {...binding}; + delete unsignedBinding.content_hash; + const content = {...decision, contract_binding: unsignedBinding}; + if (binding.content_hash !== canonicalHash(content)) { + throw new EffectRuntimeRequestError("complete Turn decision content integrity mismatch"); + } + const source = {...decision}; + delete source.schema_version; + delete source.contract_binding; + if (binding.source_decision_hash !== canonicalHash(source)) { + throw new EffectRuntimeRequestError("complete Turn decision source integrity mismatch"); + } + requireCompleteDecisionSource(source); + return decision; +} + +/** One checked source for model input and transport session identity. The + * historical effect adapter may remain in the request during migration, but it + * cannot redirect the complete contract to another Goal, Agent or work item. */ +function completeHostDecision(request: JsonObject): JsonObject { + const decision = requireCompleteTurnDecision(request.payload); + const envelope = requireJsonObject(request.envelope, "complete Turn executor envelope"); + assertTurnEnvelopeConsumerVersion(envelope); + const signature = object(envelope.action_signature); + if (signature.source_decision_hash !== canonicalHash(decision) + || signature.envelope_hash !== canonicalHash(turnEnvelopeActionSignatureDocument(envelope)) + || signature.source_hash !== signature.envelope_hash || signature.matches !== true) { + throw new EffectRuntimeRequestError("complete Turn model input differs from captured executor source"); + } + if (decision.goal_id !== envelope.goal_id + || object(decision.agent_identity).agent_id !== envelope.agent_id + || selectedWorkTodoId(decision) !== selectedWorkTodoId({ + selected_todo: object(envelope.action).selected_todo, + task_orchestration_contract: envelope.task_orchestration_contract, + })) { + throw new EffectRuntimeRequestError("complete Turn session lineage differs from executor source"); + } + // An adapter's self-consistent hash does not prove that it carries the + // admitted decision. Reuse the producer's projection, including writeback + // and execution conditions, rather than maintaining a second field list. + if (signature.source_hash !== canonicalHash(quotaActionSignatureDocument(decision))) { + throw new EffectRuntimeRequestError("complete Turn executor projection differs from admitted source"); + } + return decision; +} + +export function composeCompleteTurnDecision(value: unknown): JsonObject { + const request = requireJsonObject(value, "complete Turn decision request"); + const payload = requireJsonObject(request.payload, "complete Turn decision source"); + if (typeof request.scheduler_execution_args !== "string") { + throw new EffectRuntimeRequestError("Turn scheduler execution binding must be a string"); + } + if (payload.schema_version === COMPLETE_TURN_DECISION_VERSION) { + const prior = requireCompleteTurnDecision(payload); + if (object(prior.contract_binding).scheduler_execution_args !== request.scheduler_execution_args) { + throw new EffectRuntimeRequestError("complete Turn decision cannot change its host binding"); + } + return prior; + } + if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { + throw new EffectRuntimeRequestError("complete Turn decision source contains reserved version/binding fields"); + } + requireCompleteDecisionSource(payload); + // A complete input must carry the original-Turn successor instructions that + // used to exist only in the staged writer. Compose them before hashing and + // capture; a later material read must not add a competing execution contract. + const packet = object(payload.replan_action_packet); + const interaction = object(payload.interaction_contract); + const cli = object(interaction.cli_channel); + const plan = object(cli.settlement_plan); + let source = payload; + if (packet.obligation_id && packet.settlement_only !== true && + payload.effective_action !== EffectiveAction.GOVERNED_CAPABILITY_INTENT && + packet.obligation_id === object(object(payload.autonomous_replan_obligation).replan_context).obligation_id && + typeof object(plan.identity).turn_instance_id === "string") { + const contract = bindReplanSuccessorContract(object(packet.writeback_contract), plan); + source = {...payload, replan_action_packet: {...packet, writeback_contract: contract}, + ...(typeof contract.successor_command === "string" ? { + interaction_contract: {...interaction, cli_channel: {...cli, + next_cli_actions: [contract.successor_command]}}, + } : {})}; + } + // Retain the existing Explore owner's pure-replan applicability correction + // before capture too. A complete contract must not advertise an attachment + // that requires a Todo when this admission has only an obligation. Reuse the + // exact known-guidance check; custom instructions and evidence stay intact. + // Do not run the historical compactor: complete authoring remains in this capture. + if (unboundExploreReplan(source, plan)) { + const currentInteraction = object(source.interaction_contract); + const agent = object(currentInteraction.agent_channel); + const context = object(agent.work_context); + if (Array.isArray(context.sources)) { + source = {...source, interaction_contract: {...currentInteraction, agent_channel: {...agent, + work_context: {...context, sources: context.sources.map(value => { + const item = object(value); + return item.source === "turn_start_capability_hook" && item.hook_id === "explore.turn_context" && + item.capability_id === "explore" + ? {...item, content: omitUnboundExploreResultWriteback(object(item.content))} : value; + })}}}}; + } + } + source = referenceGoalAcceptance(source); + source = referenceReplanAuthoring(source); + source = referenceExploreGuidance(source); + source = referenceCurrentTaskTitle(source); + source = referenceVisionContinuation(source); + requireCompleteDecisionSource(source); + const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, + contract_binding: {scheduler_execution_args: request.scheduler_execution_args, + source_decision_hash: canonicalHash(source)}}; + result.contract_binding = {...object(result.contract_binding), content_hash: canonicalHash(result)}; + return result; +} + export function buildTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const payload = requireJsonObject(request.payload, "turn envelope payload"); @@ -1237,6 +1815,12 @@ export function buildTurnEnvelope(value: unknown): JsonObject { "turn envelope scheduler_execution_args must be a string", ); } + if (payload.schema_version === COMPLETE_TURN_DECISION_VERSION) { + requireCompleteTurnDecision(payload); + if (object(payload.contract_binding).scheduler_execution_args !== request.scheduler_execution_args) { + throw new EffectRuntimeRequestError("Turn adapter host binding differs from captured contract"); + } + } const schedulerExecutionArgs = request.scheduler_execution_args; const capturedDecisionPath = request.captured_decision_path; if (capturedDecisionPath !== undefined && ( @@ -1406,6 +1990,16 @@ export function turnEnvelopeRoute(envelope: JsonObject, historicalText?: JsonObj export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "captured_outcome") return capturedOutcomeRead(request); + if (operation === "reference_vision_continuation") { + return referenceVisionContinuation(requireJsonObject(request.payload, "vision audit sharing source")); + } + if (operation === "reference_replan_authoring") { + return referenceReplanAuthoring(requireJsonObject(request.payload, "authoring sharing source")); + } + if (operation === "reference_goal_acceptance") { + return referenceGoalAcceptance(requireJsonObject(request.payload, "acceptance sharing source")); + } if (operation === "replan_view") return replanView(request); if (operation === "replan_cli_view") { const payload = requireJsonObject(request.payload, "replan CLI payload"); @@ -1439,13 +2033,49 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { autonomous_replan_obligation: {...obligation, context_read: reads.review}, replan_action_packet: replanActionPacket(payload)}; } + if (operation === "compose_complete") return composeCompleteTurnDecision(request); + if (operation === "complete_consumer") { + requireCompleteTurnDecision(request.payload); + return {ok: true}; + } + if (operation === "validate_complete" || operation === "complete_host_lineage") { + const decision = completeHostDecision(request); + if (operation === "validate_complete") return decision; + return {goal_id: scalarString(decision.goal_id, "Turn Goal identity").trim(), + agent_id: scalarString(object(decision.agent_identity).agent_id, "Turn Agent identity").trim(), + todo_id: (selectedWorkTodoId(decision) ?? "").trim()}; + } if (operation === "driver_route") return {route: turnEnvelopeRoute(requireJsonObject(request.envelope, "Turn route envelope"), request.historical_route_text === undefined ? undefined : requireJsonObject(request.historical_route_text, "historical route text"))}; if (operation === "driver_lineage") { const envelope = requireJsonObject(request.envelope, "Turn driver envelope"); - return {route: turnEnvelopeRoute(envelope, request.historical_route_text === undefined - ? undefined : requireJsonObject(request.historical_route_text, "historical route text")), - action_signature: turnEnvelopeActionSignatureDocument(envelope)}; + const decision = Object.hasOwn(request, "payload") ? completeHostDecision(request) : null; + return { + ...(decision ? {goal_id: decision.goal_id ?? null, + agent_id: object(decision.agent_identity).agent_id ?? null, + todo_id: selectedWorkTodoId(decision) ?? ""} : {}), + // Complete source validation above admits no caller-supplied scalar override. + route: turnEnvelopeRoute(envelope, decision || request.historical_route_text === undefined + ? undefined : requireJsonObject(request.historical_route_text, "historical route text")), + // Keep the historical Python JSON encoding of this preimage at the + // transport boundary: JavaScript number spelling can change Turn keys. + action_signature: turnEnvelopeActionSignatureDocument(request.envelope), + }; + } + if (operation === "captured_todo_text") { + const envelope = requireJsonObject(request.envelope, "Turn completion envelope"); + let action = object(envelope.action), context: unknown = envelope.work_context; + if (Object.hasOwn(request, "payload")) { + const decision = completeHostDecision(request); + const workContext = object(object(object(decision.interaction_contract).agent_channel).work_context); + action = {selected_todo: {...object(decision.selected_todo), + ...(Object.hasOwn(workContext, "selected_todo_text_verified") + ? {text_source_verified: workContext.selected_todo_text_verified} : {})}, + recommended_action: decision.recommended_action}; + context = workContext; + } + return {sha256: capturedTodoTextSha256(object(action.selected_todo), context, + action.recommended_action, request.todo_id)}; } if (operation === "build_controller_input") { const envelope = buildTurnEnvelope(request); @@ -1464,6 +2094,11 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { return jsonObject(request.diagnostics) ? contextProjectionChanges(source, payload) : payload; } if (operation === "build") return buildTurnEnvelope(request); + if (operation === "build_controller_input") { + const envelope = buildTurnEnvelope(request); + return {envelope, route: turnEnvelopeRoute(envelope)}; + } + if (operation === "outcome_context") return capturedOutcomeContext(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); if (operation === "context_recovery") return restoreContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index b36c8d2437..24674477c7 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -41,6 +41,7 @@ from ..capability_hooks import capability_result_fields from .execution_profile import require_supported_reasoning_effort from .host_failure import BuiltInHostError +from .host_candidate import execution_profile_present from .host_process_transport import HostOutputLines, run_host_process from .transaction import LOOPX_TURN_RESULT_SCHEMA_VERSION, TRANSACTION_PHASES @@ -276,10 +277,7 @@ def codex_cli_result_schema( "enum": ["codex-cli"], } properties["child_execution_receipts"] = child_receipts - envelope = _mapping(request.get("turn_envelope")) if request else {} - capsule = _mapping(envelope.get("contract_capsule")) - profile = capsule.get("execution_profile") - evidence_required = isinstance(profile, Mapping) and bool(profile) + evidence_required = execution_profile_present(request or {}) schema: dict[str, Any] = { "type": "object", "properties": properties, @@ -310,10 +308,23 @@ def _prompt(request: Mapping[str, Any]) -> str: from ..quota.turn_envelope import assert_turn_envelope_consumer_version envelope = _mapping(request.get("turn_envelope")) - assert_turn_envelope_consumer_version(envelope) staged = _mapping(_mapping(envelope.get("detail_ref")).get("replan_action_packet")).get("review") - model_request = request - if staged: + from .host_candidate import complete_host_input + + # The complete owner checks the adapter version, source and lineage in one + # call. Keep the standalone version gate only for historical requests. + try: + complete_input = complete_host_input(request) + except RuntimeError: + # Preserve the historical diagnostic precedence for doubly malformed + # input without imposing a second bridge call on valid requests. + assert_turn_envelope_consumer_version(envelope) + raise + complete = complete_input is not None + if not complete: + assert_turn_envelope_consumer_version(envelope) + model_request: Mapping[str, Any] = complete_input if complete_input is not None else request + if staged and not complete: from .host_candidate import extract_turn_authority, replan_prompt_instruction authority = extract_turn_authority(request) @@ -326,7 +337,7 @@ def _prompt(request: Mapping[str, Any]) -> str: ) instructions = [ "Execute exactly one bounded LoopX Turn in the current workspace.", - ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ") + ("Use work_contract as the complete source of truth. Its interaction_contract.agent_channel carries work_context, required_reads and primary_action; its cli_channel describes Core effects owned by the outer adapter. " if complete else ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ")) + "Read work_context and remaining required_reads before primary_action; do not repeat fulfilled reads; perform work only when its contract allows it. The selected Todo summary is not its full requirements, and completing that work does not prove Goal completion.", "Do not write LoopX state, spend quota, or apply scheduler changes; the adapter owns those effects.", "Return only the schema-constrained result. For validated_progress, repair_required, or replan_required, fill every material field with public-safe evidence.", @@ -337,8 +348,13 @@ def _prompt(request: Mapping[str, Any]) -> str: "For user_action_required, wait, or iteration_failed, leave material-only fields empty and explain the stop in summary. iteration_failed ends only this iteration and never requests a retry or successor.", 'completed_phases must be exactly ["host_execute","typed_result"], and turn_key must match the request.', ] - if staged: + if staged and not complete: instructions.append(replan_prompt_instruction(authority)) + elif staged: + from .host_candidate import replan_prompt_instruction + + instructions.append(replan_prompt_instruction( + _mapping(model_request.get("detail_ref")), packet_path="detail_ref.replan_action_packet")) instructions.extend(request.get("capability_instructions") or ()) boundary = _mapping(_mapping(request.get("turn_envelope")).get("boundary")) if boundary.get("checkpointed_boundary_authority"): diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index cd0ab3a5f7..4cfdae8095 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -493,17 +493,39 @@ def project_codex_context(payload: dict[str, Any], *, directory: str | None, cap recovery = capture_context_projection(payload, directory=capture_directory, read_guidance=CONTEXT_CAPTURE_PAGE_GUIDANCE) page_bytes, output_bytes = native_context_page_budget() - recovery["command"] = context_capture_read_command(Path(recovery["path"]), recovery["sha256"], - page_bytes=page_bytes, max_page_output_bytes=output_bytes) - state["recovery"] = {**{key: recovery[key] for key in ("path", "sha256", "command")}, - "goal_id": goal_id, "agent_id": agent_id, - "total_bytes": Path(recovery["path"]).stat().st_size} - # Block reuse before optional wire IO, not after a first page - # or EOF. A codec/read interruption keeps this incomplete. - state["pending"] = {"path": recovery["path"], "sha256": recovery["sha256"], - "total_bytes": state["recovery"]["total_bytes"], "ranges": {}} - _save(root, state) + + def publish_recovery(reference: dict[str, Any]) -> None: + reference["command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], + page_bytes=page_bytes, max_page_output_bytes=output_bytes) + state["recovery"] = {**{key: reference[key] for key in ("path", "sha256", "command")}, + "goal_id": goal_id, "agent_id": agent_id, + "total_bytes": Path(reference["path"]).stat().st_size} + state["pending"] = {"path": reference["path"], "sha256": reference["sha256"], + "total_bytes": state["recovery"]["total_bytes"], "ranges": {}} + _save(root, state) + + # Retire old reuse before *any* optional scope/codec call. A + # process interruption must recover this original, never its + # predecessor. Scoped publication can then replace this route. + publish_recovery(recovery) recovery_committed = True + # The canonical capture stays complete. A scoped model context + # owns a distinct immutable recovery/baseline artifact containing + # exactly what the model reads, never hidden optional bodies. + from ..quota.turn_envelope import _turn_envelope_result + from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError + + try: + projected = _turn_envelope_result("outcome_context", payload=payload, + source_capture={key: recovery[key] for key in ("path", "sha256")}) + except (ValueError, TypeError, EffectRuntimeRemoteError, EffectRuntimeStartupError): + projected = payload # optional presentation failure retains the complete source + if projected.get("schema_version") == "outcome_context_projection_v0": + payload = projected + recovery = capture_context_projection(payload, directory=capture_directory, + filename="required-context.json", read_guidance=CONTEXT_CAPTURE_PAGE_GUIDANCE) + recovery["presentation_schema"] = payload["schema_version"] + publish_recovery(recovery) result = project_captured_context(payload, directory=capture_directory, request=request, captured_recovery=recovery, diff --git a/loopx/control_plane/turn_driver/codex_sessions.py b/loopx/control_plane/turn_driver/codex_sessions.py index 741a2df4a2..81c3b1a89f 100644 --- a/loopx/control_plane/turn_driver/codex_sessions.py +++ b/loopx/control_plane/turn_driver/codex_sessions.py @@ -20,12 +20,13 @@ require_runtime_compatible_project_registry, ) from ..goals.first_party_host_admission import FirstPartyHostGoalAdmission -from .driver import selected_turn_todo, session_identity_fields +from .driver import session_identity_fields, turn_host_lineage CODEX_CLI_SESSION_SCHEMA_VERSION = "loopx_codex_cli_session_v1" MANAGED_CLI_SESSION_SCHEMA_VERSION = "loopx_managed_cli_session_v1" SESSION_ID_MAX_CHARS = 256 _UNCHECKED_SESSION_DOCUMENT = object() +_ABSENT_WORK_CONTRACT = object() def _host_session_schema(host: str) -> str: @@ -37,13 +38,7 @@ def _host_session_schema(host: str) -> str: def _lineage(request: Mapping[str, Any], *, session_scope: str | None = None) -> dict[str, str]: - envelope = request.get("turn_envelope") or {} - todo = selected_turn_todo(envelope) - lineage = { - "goal_id": str(envelope.get("goal_id") or "").strip(), - "agent_id": str(envelope.get("agent_id") or "").strip(), - "todo_id": str(todo.get("todo_id") or "").strip(), - } + lineage = turn_host_lineage(request) if session_scope is None: session = request.get("session") or {} policy = session.get("context_policy") or {} @@ -169,11 +164,15 @@ def codex_cli_session_binding( runtime_root: Path, turn_envelope: Mapping[str, Any], *, + work_contract: object = _ABSENT_WORK_CONTRACT, goal_admission: FirstPartyHostGoalAdmission | None = None, session_scope: str = "todo", host: str = "codex-cli", ) -> dict[str, str] | None: - lineage = _lineage({"turn_envelope": dict(turn_envelope)}, session_scope=session_scope) + request: dict[str, Any] = {"turn_envelope": dict(turn_envelope)} + if work_contract is not _ABSENT_WORK_CONTRACT: + request["work_contract"] = work_contract + lineage = _lineage(request, session_scope=session_scope) session = select_codex_cli_session( runtime_root, lineage=lineage, diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 8964e03824..9d23ee22fe 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -100,7 +100,14 @@ def stage_turn_decision_capture( # Legacy journals retain their recorded delivery/recovery route. reader_argv=current_loopx_cli_argv() if not existing_journal else None, ) - return StagedTurnDecisionCapture({**plan, "turn_envelope": bound}, path, observation, existed) + restored = {**plan, "turn_envelope": bound} + if observation.get("schema_version") == "loopx_turn_envelope_v1": + restored["work_contract"] = observation + else: + # Historical observations retain their original input contract. Never + # pair an old capture with a freshly observed complete decision. + restored.pop("work_contract", None) + return StagedTurnDecisionCapture(restored, path, observation, existed) def prepare_turn_decision_capture( @@ -185,7 +192,9 @@ def native_context_page_budget() -> tuple[int, int]: def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int | None = None, - max_page_output_bytes: int | None = None) -> str: + max_page_output_bytes: int | None = None, + outcome_ref: str | None = None, + selection_sha256: str | None = None) -> str: from ...command_invocation import loopx_cli_argv if page_bytes is None: @@ -195,7 +204,9 @@ def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, return shlex.join([*loopx_cli_argv(), "--format", "json", "quota", "context-read", "--capture-file", str(path), "--capture-sha256", digest, "--offset", str(offset), "--page-bytes", str(page_bytes), - *(["--max-page-output-bytes", str(max_page_output_bytes)] if max_page_output_bytes is not None else [])]) + *(["--max-page-output-bytes", str(max_page_output_bytes)] if max_page_output_bytes is not None else []), + *(["--selection-sha256", selection_sha256] if selection_sha256 is not None else []), + *(["--outcome-ref", *([outcome_ref] if outcome_ref else [])] if outcome_ref is not None else [])]) def read_context_capture_file(value: str, digest: str, *, private: bool = False) -> tuple[Path, bytes, dict[str, Any]]: @@ -228,7 +239,8 @@ def read_context_capture_file(value: str, digest: str, *, private: bool = False) def capture_page_range(page: dict[str, Any], *, raw: bytes, digest: str) -> tuple[int, int] | None: """Verify reported page bytes, independently of a host's retention claim.""" - if page.get("schema_version") != "context_capture_page_v1" or page.get("sha256") != digest: + if ("outcome_ref" in page or "selection_sha256" in page or page.get("schema_version") != "context_capture_page_v1" + or page.get("sha256") != digest): return None start, content = page.get("offset"), page.get("content") if type(start) is not int or start < 0 or not isinstance(content, str) or page.get("ok") is not True: @@ -245,9 +257,10 @@ def capture_page_range(page: dict[str, Any], *, raw: bytes, digest: str) -> tupl def capture_context_projection(payload: dict[str, Any], *, directory: Path, - read_guidance: str = CONTEXT_CAPTURE_READ_GUIDANCE) -> dict[str, Any]: + read_guidance: str = CONTEXT_CAPTURE_READ_GUIDANCE, + filename: str = "projection.json") -> dict[str, Any]: """Save the full projection independently of the optional presentation codec.""" - projection = directory / "projection.json" + projection = directory / filename write_decision_capture(projection, payload) digest = hashlib.sha256(projection.read_bytes()).hexdigest() return { diff --git a/loopx/control_plane/turn_driver/driver.py b/loopx/control_plane/turn_driver/driver.py index 0fc5fefeb6..8e21729562 100644 --- a/loopx/control_plane/turn_driver/driver.py +++ b/loopx/control_plane/turn_driver/driver.py @@ -34,6 +34,8 @@ "loopx_iteration_context_policy_v0" ) LOOPX_CHILD_HOST_OPERATION_SCHEMA_VERSION = "loopx_child_host_operation_v0" +_ABSENT_WORK_CONTRACT = object() + SUPPORTED_HOSTS = {"codex-cli", "codex-app-server", "claude-code", "dsh", "generic-cli"} SUPPORTED_EXECUTION_MODES = {"interactive-visible", "isolated-headless"} SUPPORTED_ITERATION_CONTEXT_POLICIES = {"fresh", "resume"} @@ -101,6 +103,24 @@ def selected_turn_todo(envelope: Mapping[str, Any]) -> dict[str, Any]: return _mapping(action.get("selected_todo")) +def turn_host_lineage(request: Mapping[str, Any]) -> dict[str, str]: + """Read identity from the complete owner, with historical request decoding.""" + if "work_contract" in request: + from ..quota.turn_envelope import _turn_envelope_result + + return _turn_envelope_result( + "complete_host_lineage", payload=request["work_contract"], + envelope=request.get("turn_envelope"), + ) + envelope = _mapping(request.get("turn_envelope")) + todo = selected_turn_todo(envelope) + return { + "goal_id": str(envelope.get("goal_id") or "").strip(), + "agent_id": str(envelope.get("agent_id") or "").strip(), + "todo_id": str(todo.get("todo_id") or "").strip(), + } + + def _receipt_bound_replan_identity( envelope: Mapping[str, Any], *, turn_instance_id: str | None, ) -> SettlementIdentity | None: @@ -120,35 +140,30 @@ def _turn_lineage( envelope: Mapping[str, Any], *, selected_todo: Mapping[str, Any], - settlement_identity: SettlementIdentity | None = None, + work_contract: object = _ABSENT_WORK_CONTRACT, projection: Mapping[str, Any] | None = None, ) -> dict[str, str]: resolved = projection if projection is not None else _turn_envelope_result( - "driver_lineage", envelope=dict(envelope), historical_route_text=_historical_route_text(envelope), + "driver_lineage", envelope=dict(envelope), + **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else + {"historical_route_text": _historical_route_text(envelope)}), ) - signature_document = resolved["action_signature"] - action = _mapping(signature_document.get("action")) - signature_document["action"] = { - **action, - "selected_todo": dict(selected_todo), - } - action_hash = "sha256:" + sha256( - json.dumps( - signature_document, - ensure_ascii=False, - sort_keys=True, - separators=(",", ":"), - ).encode("utf-8") - ).hexdigest() - lineage = { - "goal_id": str(envelope.get("goal_id") or ""), - "agent_id": str(envelope.get("agent_id") or ""), - "todo_id": str(selected_todo.get("todo_id") or ""), - "action_hash": action_hash, + if work_contract is _ABSENT_WORK_CONTRACT: + # Historical journals retain the exact decoder and JSON preimage. + identity = {"goal_id": envelope.get("goal_id"), "agent_id": envelope.get("agent_id"), + "todo_id": selected_todo.get("todo_id")} + else: + identity = resolved + signature = resolved["action_signature"] + signature["action"] = {**_mapping(signature.get("action")), "selected_todo": dict(selected_todo)} + # Persisted Turn keys depend on Python's JSON number encoding. Keep this + # transport codec while the TS owner selects and validates identity facts. + return { + **{field: str(identity.get(field) or "") for field in ("goal_id", "agent_id", "todo_id")}, + "action_hash": "sha256:" + sha256(json.dumps( + signature, ensure_ascii=False, sort_keys=True, separators=(",", ":"), + ).encode("utf-8")).hexdigest(), } - if settlement_identity is not None: - lineage["replan_obligation_id"] = str(settlement_identity.replan_obligation_id) - return lineage def _session_plan( @@ -236,13 +251,17 @@ def reconcile_failed_turn_session_request( envelope = _mapping(request.get("turn_envelope")) selected_todo = selected_turn_todo(envelope) try: + lineage = _turn_lineage( + envelope, selected_todo=selected_todo, + work_contract=request.get("work_contract", _ABSENT_WORK_CONTRACT), + ) settlement_identity = (None if selected_todo else _receipt_bound_replan_identity( envelope, turn_instance_id=None, )) + if settlement_identity is not None: + lineage["replan_obligation_id"] = str(settlement_identity.replan_obligation_id) except (ValueError, RuntimeError) as exc: raise FailedTurnSessionRecoveryError("turn_lineage_missing", str(exc)) from exc - lineage = _turn_lineage(envelope, selected_todo=selected_todo, - settlement_identity=settlement_identity) planned_session = _mapping(request.get("session")) session, session_error = _session_plan( route=LoopXTurnRoute.READY_FOR_HOST, @@ -373,8 +392,9 @@ def build_loopx_turn_plan( iteration_context_policy: str = "resume", session_scope: str | None = None, goal_ref: Mapping[str, object] | None = None, + work_contract: object = _ABSENT_WORK_CONTRACT, ) -> dict[str, Any]: - """Project a TurnEnvelope into a typed, side-effect-free host decision.""" + """Project a checked Turn input into a typed, side-effect-free host decision.""" if host not in SUPPORTED_HOSTS: raise ValueError(f"unsupported LoopX Turn host: {host}") @@ -401,14 +421,21 @@ def build_loopx_turn_plan( assert_turn_envelope_consumer_version(envelope) except (ValueError, RuntimeError) as exc: contract_error = str(exc) - projection = _turn_envelope_result("driver_lineage", envelope=envelope, - historical_route_text=_historical_route_text(envelope)) + selected_todo = selected_turn_todo(envelope) + # Check the complete source before route/session/transaction planning. Reuse + # the action-hash boundary rather than adding a separate identity call. + projection = _turn_envelope_result( + "driver_lineage", envelope=envelope, + **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else + {"historical_route_text": _historical_route_text(envelope)}), + ) + lineage = _turn_lineage(envelope, selected_todo=selected_todo, + work_contract=work_contract, projection=projection) route = ( LoopXTurnRoute(projection["route"]) if execution_context.ok and contract_error is None else LoopXTurnRoute.CONTRACT_ERROR ) - selected_todo = selected_turn_todo(envelope) settlement_identity = None identity_error = None if route is LoopXTurnRoute.REPLAN_REQUIRED: @@ -422,8 +449,8 @@ def build_loopx_turn_plan( if (not selected_todo and identity_error is None and (settlement_identity is None or session_scope != "agent")): route = LoopXTurnRoute.BLOCKED - lineage = _turn_lineage(envelope, selected_todo=selected_todo, - settlement_identity=settlement_identity, projection=projection) + if settlement_identity is not None: + lineage["replan_obligation_id"] = str(settlement_identity.replan_obligation_id) session, session_error = _session_plan( route=route, lineage=lineage, @@ -508,6 +535,8 @@ def build_loopx_turn_plan( else {} ), } + if work_contract is not _ABSENT_WORK_CONTRACT: + payload["work_contract"] = work_contract if child_operations: payload["child_operations"] = child_operations context = envelope_agent_context( diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index ad0f412862..3094679648 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -36,7 +36,7 @@ normalize_reward_memory_reflection_validation, reward_memory_reflection_digest, ) -from .driver import selected_turn_todo +from .driver import selected_turn_todo, turn_host_lineage from .decision_capture import stage_turn_decision_capture from .execution_readback import execution_payload from .host_process_transport import run_host_process @@ -170,6 +170,8 @@ def build_loopx_turn_host_request(plan: Mapping[str, Any]) -> dict[str, Any]: "stdout": "one public-safe JSON object", }, } + if isinstance(plan.get("work_contract"), Mapping): + request["work_contract"] = dict(plan["work_contract"]) goal_ref = plan.get("goal_ref") if isinstance(goal_ref, Mapping): request["goal_ref"] = dict(goal_ref) @@ -304,13 +306,11 @@ def _normalize_host_path_delta( packet = result["agent_vision"] if not isinstance(packet, dict): raise TypeError("cached agent_vision must be a JSON object") - envelope = plan.get("turn_envelope") or {} + lineage = turn_host_lineage(plan) agent_vision = normalize_goal_vision_packet( - packet, - goal_id=str(envelope.get("goal_id") or ""), - agent_id=str(envelope.get("agent_id") or "") or None, + packet, goal_id=lineage["goal_id"], agent_id=lineage["agent_id"] or None, ) - except (TypeError, ValueError) as exc: + except (TypeError, ValueError, RuntimeError) as exc: errors.append(f"invalid cached agent_vision: {exc}") elif agent_vision_json: if len(agent_vision_json) > HOST_AGENT_VISION_JSON_MAX_CHARS: @@ -323,17 +323,11 @@ def _normalize_host_path_delta( packet = json.loads(agent_vision_json) if not isinstance(packet, dict): raise TypeError("agent_vision_json must decode to a JSON object") - envelope = ( - plan.get("turn_envelope") - if isinstance(plan.get("turn_envelope"), dict) - else {} - ) + lineage = turn_host_lineage(plan) agent_vision = normalize_goal_vision_packet( - packet, - goal_id=str(envelope.get("goal_id") or ""), - agent_id=str(envelope.get("agent_id") or "") or None, + packet, goal_id=lineage["goal_id"], agent_id=lineage["agent_id"] or None, ) - except (json.JSONDecodeError, TypeError, ValueError) as exc: + except (json.JSONDecodeError, TypeError, ValueError, RuntimeError) as exc: errors.append(f"invalid agent_vision_json: {exc}") if path_delta_mode == "material_replan": @@ -1160,20 +1154,26 @@ def _task_validation_stage( def _ensure_turn_settlement_plan( plan: Mapping[str, Any], transaction_plan: dict[str, Any], -) -> None: +) -> dict[str, str] | None: + # Check the complete source once before either new execution or journal + # recovery. Reuse this call-local identity below; never cache it across a + # host attempt, source replacement or another invocation. + lineage = turn_host_lineage(plan) if "work_contract" in plan else None if isinstance(transaction_plan.get("settlement_plan"), Mapping): - return - envelope = plan.get("turn_envelope") - if not isinstance(envelope, Mapping): - return - selected_todo = selected_turn_todo(envelope) - lineage = { - "goal_id": str(envelope.get("goal_id") or ""), - "agent_id": str(envelope.get("agent_id") or ""), - "todo_id": str(selected_todo.get("todo_id") or ""), - } + return lineage + if lineage is None: + # Preserve the historical decoder, including its exact string handling. + envelope = plan.get("turn_envelope") + if not isinstance(envelope, Mapping): + return None + selected_todo = selected_turn_todo(envelope) + lineage = { + "goal_id": str(envelope.get("goal_id") or ""), + "agent_id": str(envelope.get("agent_id") or ""), + "todo_id": str(selected_todo.get("todo_id") or ""), + } if not all(lineage.values()): - return + return lineage host = plan.get("host") host_fields = host if isinstance(host, Mapping) else {} built = build_loopx_turn_transaction_plan( @@ -1194,6 +1194,7 @@ def _ensure_turn_settlement_plan( settlement_plan = built.get("settlement_plan") if isinstance(settlement_plan, Mapping): transaction_plan["settlement_plan"] = settlement_plan + return lineage def _typed_settlement_stage( @@ -1477,7 +1478,7 @@ def run_loopx_turn_once( transaction_plan = ( plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {} ) - _ensure_turn_settlement_plan(plan, transaction_plan) + settlement_lineage = _ensure_turn_settlement_plan(plan, transaction_plan) request = build_loopx_turn_host_request(plan) empty_effects = { "host_invoked": False, @@ -1560,12 +1561,15 @@ def persist_source_journal(source_admission: Mapping[str, Any]) -> None: operation="turn_recovery_lifetime", ): pass - envelope = ( - plan.get("turn_envelope") - if isinstance(plan.get("turn_envelope"), Mapping) - else {} - ) - agent_id = str(envelope.get("agent_id") or "") + if settlement_lineage is not None: + agent_id = settlement_lineage["agent_id"] + else: + envelope = ( + plan.get("turn_envelope") + if isinstance(plan.get("turn_envelope"), Mapping) + else {} + ) + agent_id = str(envelope.get("agent_id") or "") assessment = assess_existing_turn_recovery( journal, request, @@ -1645,6 +1649,10 @@ def persist_source_journal(source_admission: Mapping[str, Any]) -> None: if capture is not None: plan = capture.publish() request = {**request, "turn_envelope": plan["turn_envelope"]} + if isinstance(plan.get("work_contract"), Mapping): + request["work_contract"] = dict(plan["work_contract"]) + else: + request.pop("work_contract", None) if journal is not None and recovery_decision is not None: journal["recovery_audit"] = build_turn_recovery_audit( recovery_decision, diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index efdfbd7de4..120af332d1 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -63,31 +63,82 @@ def _canonical_hash(value: Any) -> str: return "sha256:" + sha256(encoded).hexdigest() -def _execution_profile_present(request: Mapping[str, Any]) -> bool: - envelope = _mapping(request.get("turn_envelope")) - capsule = _mapping(envelope.get("contract_capsule")) - profile = capsule.get("execution_profile") +def _complete_host_decision(request: Mapping[str, Any]) -> dict[str, Any] | None: + if "work_contract" not in request: + return None + from ..quota.turn_envelope import _turn_envelope_result + + return _turn_envelope_result("validate_complete", payload=request["work_contract"], + envelope=_mapping(request.get("turn_envelope"))) + + +def execution_profile_present(request: Mapping[str, Any]) -> bool: + """Read result evidence applicability from the checked admitted source. + + An omitted complete contract retains historical capsule decoding; an invalid + supplied contract never falls back. Schema and candidate adapters share this + read, while evidence content validation stays with the result executor. + """ + decision = _complete_host_decision(request) + if decision is None: + envelope = _mapping(request.get("turn_envelope")) + decision = _mapping(envelope.get("contract_capsule")) + profile = decision.get("execution_profile") return isinstance(profile, Mapping) and bool(profile) +def complete_host_input(request: Mapping[str, Any]) -> dict[str, Any] | None: + """Transport the complete contract plus original material-reader metadata. + + The TypeScript owner validates identity and integrity. This adapter never + compacts decision facts or derives a second model-facing authority view. + Historical requests without a complete contract retain their old decoder. + """ + if _complete_host_decision(request) is None: + return None + envelope = _mapping(request.get("turn_envelope")) + model_input = {key: value for key, value in request.items() if key != "turn_envelope"} + detail = _mapping(envelope.get("detail_ref")) + captured = _mapping(detail.get("captured_decision")) + if captured: + model_input["source_decision_hash"] = _mapping(envelope.get("action_signature"))["source_decision_hash"] + model_input["detail_ref"] = { + "full_decision": detail["full_decision"], + "captured_decision": {**captured, "source_hash_ref": "$.source_decision_hash"}, + } + readers = _mapping(detail.get("replan_action_packet")) + if readers.get("review"): + model_input["detail_ref"]["replan_action_packet"] = { + key: value for key, value in readers.items() if key in {"review", "writeback"} + } + return model_input + + def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: """Return the signed action and safety boundary exactly as projected.""" envelope = _mapping(request.get("turn_envelope")) - assert_turn_envelope_consumer_version(envelope) + try: + model_input = complete_host_input(request) + except RuntimeError as exc: + raise ValueError("complete Turn contract rejected") from exc signature = _mapping(envelope.get("action_signature")) - source_hash = str(signature.get("source_hash") or "") - envelope_hash = str(signature.get("envelope_hash") or "") - computed_envelope_hash = _canonical_hash( - turn_envelope_action_signature_document(envelope) - ) - if ( - signature.get("matches") is not True - or not source_hash - or source_hash != envelope_hash - or envelope_hash != computed_envelope_hash - ): - raise ValueError("TurnEnvelope action signature is missing or does not match") + if model_input is None: + # The complete owner checks version, signature and source binding in + # one call. Only historical requests need this compatibility decoder. + assert_turn_envelope_consumer_version(envelope) + source_hash = str(signature.get("source_hash") or "") + envelope_hash = str(signature.get("envelope_hash") or "") + computed_envelope_hash = _canonical_hash( + turn_envelope_action_signature_document(envelope) + ) + if ( + signature.get("matches") is not True + or not source_hash + or source_hash != envelope_hash + or envelope_hash != computed_envelope_hash + ): + raise ValueError("TurnEnvelope action signature is missing or does not match") action = _mapping(envelope.get("action")) detail = _mapping(envelope.get("detail_ref")) @@ -161,9 +212,9 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: # priorities. Keep the signed envelope intact for the executor; do not send # a second copy ahead of the mandatory immutable review read. for field in (() if staged else ("core_goal", "planning_horizon", "acceptance_index")): - value = detail.get(field, action.get(field)) - if value is not None: - authority[field] = value + detail_value = detail.get(field, action.get(field)) + if detail_value is not None: + authority[field] = detail_value horizon = _mapping(authority.get("planning_horizon")) if horizon: # Relocate only the envelope's registered aliases after flattening its @@ -186,6 +237,10 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: if key not in {"replan_action_packet", "replan_read_argv", "core_goal", "planning_horizon", "acceptance_index"}} authority["action_signature"] = signature authority.update(host_context_projection(request)) + if model_input is not None: + # Adapter-local transport only. Result qualification still uses the + # verified internal effect fields; rendering sends the complete input. + authority["_model_input"] = model_input return authority @@ -195,15 +250,17 @@ def extract_action_text(request: Mapping[str, Any]) -> str: return str(extract_turn_authority(request)["primary_action"]) -def replan_prompt_instruction(authority: Mapping[str, Any]) -> str: +def replan_prompt_instruction( + authority: Mapping[str, Any], *, packet_path: str = "replan_action_packet", +) -> str: """Reader routing shared by native Codex and structured Host transports.""" packet = _mapping(authority.get("replan_action_packet")) if not packet: return "" source = ( - "Read replan_action_packet.review and the applicable acceptance/evidence reads. " + f"Read {packet_path}.review and the applicable acceptance/evidence reads. " "Decide the next work and route from those facts before reading " - "replan_action_packet.writeback for the semantic result format. " + f"{packet_path}.writeback for the semantic result format. " "The readers verify the original source hash; check their Goal/Agent and " "obligation/context identity against this Turn. " if packet.get("review") else @@ -229,8 +286,9 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals adapter reads back as a typed candidate; it stays public-safe and bounded. """ + model_input = authority.get("_model_input") authority_json = json.dumps( - dict(authority), + model_input if model_input is not None else dict(authority), ensure_ascii=False, sort_keys=True, separators=(",", ":"), @@ -272,7 +330,7 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals "Turn executor. The profile grants no new permissions.\n" if authority.get("execution_profile") else "" ) - return ( + legacy_input = ( "You are executing one bounded LoopX-governed work segment.\n" "The JSON below is the complete host authority for this Turn. Execute " "primary_action only after reading work_context and any remaining required_reads. " @@ -293,7 +351,40 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals f"{acceptance_instruction}" f"{work_instruction}" f"Turn authority JSON:\n{authority_json}\n\n" - "When finished, return only one JSON object (no Markdown fence) with " + ) + if model_input is not None: + detail = _mapping(model_input.get("detail_ref")) + input_prompt = ( + "You are executing one bounded LoopX-governed work segment.\n" + "Use work_contract as the complete source of truth. Read its " + "interaction_contract.agent_channel.work_context and remaining required_reads " + "before primary_action; do not repeat fulfilled reads. Perform work only " + "when its contract allows it. Obey every supplied write scope, workspace " + "restriction, approval boundary and stop condition. Checkpointed approval " + "covers only active_write_scope; other approval gates remain.\n" + "Read agent_context before planning; unavailable_context is a failed read, " + "not empty context: discard affected cached context and hold dependent work " + "until recovery. Independent work retains its existing authority.\n" + "Before Goal closeout, preserve vision_continuation_audit's ordered " + "required_before_closeout, evidence requirements and vision_gap_judge. " + "The selected Todo summary is not its full requirements, and Todo " + "completion cannot close the Goal. Use execution_profile to size and " + "qualify work, including artifact, validation, outcome and degradation rules.\n" + "For required additional detail, use detail_ref.full_decision from the " + "saved original observation identified by detail_ref.captured_decision; " + "verify Goal/Agent/Turn and source_decision_hash. Never re-enter admission " + "for detail reads. Missing or invalid evidence holds dependent planning " + "and closure; independent work retains existing authority. Guidance, " + "evidence and reads grant no permissions. Do not write LoopX state, spend " + "quota or apply scheduler changes; the outer Turn executor owns those effects.\n" + + replan_prompt_instruction(detail, packet_path="detail_ref.replan_action_packet") + + "Turn request:\n" + authority_json + "\n\n" + ) + else: + input_prompt = legacy_input + return ( + input_prompt + + "When finished, return only one JSON object (no Markdown fence) with " "these public-safe fields:\n" "- result_kind: one of validated_progress | " + ("validated_completion | " if allow_completion else "") @@ -451,7 +542,7 @@ def build_result( limit=TEXT_LIMITS["vision_unchanged_reason"], ), } - if kind in material_kinds and _execution_profile_present(request): + if kind in material_kinds and execution_profile_present(request): missing_evidence = [ field for field in HOST_RESULT_EVIDENCE_LIMITS if field not in candidate diff --git a/loopx/control_plane/work_items/context_readback.py b/loopx/control_plane/work_items/context_readback.py index e110d6e627..8112b29201 100644 --- a/loopx/control_plane/work_items/context_readback.py +++ b/loopx/control_plane/work_items/context_readback.py @@ -21,7 +21,7 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id, - inline_goal_source=False): + inline_goal_source=False, agent_id=None): source = read.get("source") if source == "goal_state": _goal, _project, state_file = resolve_goal_state( @@ -73,7 +73,7 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id, # selected-Todo read owns its body; do not transport it a second time # through the bounded acceptance RPC before discarding it here. return inspect_goal_acceptance(registry_path=registry_path, goal_id=goal_id, - runtime_root=str(runtime_root), todo_id=todo_id, include_work_details=False) + runtime_root=str(runtime_root), todo_id=todo_id, context_agent_id=agent_id, include_work_details=False) if source == "selected_todo": if not todo_id: raise ValueError("selected Todo identity is unavailable") @@ -157,7 +157,7 @@ def attach_work_context(payload: dict[str, Any], *, registry_path: Path, try: result["content"] = _source_content(read, registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, todo_id=plan.get("todo_id"), - inline_goal_source=inline_goal_source) + inline_goal_source=inline_goal_source, agent_id=agent_id) except (OSError, ValueError, RuntimeError): result["error_code"] = "context_source_unavailable" results.append(result) diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index 70b85d9464..5e61eb7e5a 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -770,6 +770,38 @@ export function selectedWorkTodoId(packet: JsonObject): string | null { return typeof selected.todo_id === "string" && selected.todo_id.trim() ? selected.todo_id : null; } +const OBJECTIVE_READ_GUIDANCE = "Use the canonical goal_objective before the older source text. Only if your adoption is missing/stale, review your own work, reconcile Todo/Vision changes, then use adopt-objective with your Agent id, exact revision/digest, own objective_work_digest and review_summary. New execution is held until your adoption. Other peers adopt independently; existing Turn settlement/release remains available. Preserve root acceptance, permissions and stops. Adopt once per objective revision, never per Turn or work/peer update. Reuse complete inline context without extra reads/writes."; +function outcomeReadGuidance(scoped = false): string { + return (scoped + ? "Read the full global acceptance basis and every outcome required by this capture's read_scope, including cumulative requirements, work links and sources. Other outcomes retain current index facts and exact original-capture references; read their complete bodies before reliance. Unknown relevance or Goal-wide acceptance requires the complete original source. " + : "Read the full acceptance basis and any supporting outcomes with cumulative requirements, work links and sources. ") + + "Preserve the original Goal and earlier requirements when applying direct user refinements. Before switching work or declaring completion, reconcile unmet outcome requirements with subsequent work; batch changed requirements/evidence reviews through refresh-state --goal-outcome-update-json or goal-acceptance refine. Reuse complete inline context; unchanged work needs no extra inspect/write. Agent review is distinct from independent checks. Scoped acceptance never replaces Goal intent. Changed requirements need a fresh guard."; +} + +/** The caller has already verified the captured reading plan. Adapt only this + * owner's exact fulfilled acceptance instruction in the derived presentation; + * unknown guidance or an outstanding acceptance read keeps full delivery. + * This changes no canonical source, adoption rule or execution authority. */ +export function scopeInteractionOutcomeReadGuidance(channel: JsonObject): boolean { + const pending = Array.isArray(channel.required_reads) ? channel.required_reads : []; + if (pending.some(value => jsonObject(value)?.source === "goal_acceptance")) return false; + const context = jsonObject(channel.work_context); + const sources = Array.isArray(context?.sources) ? context.sources : []; + const changes: {source: JsonObject; reason: string}[] = []; + for (const value of sources) { + const source = jsonObject(value), content = jsonObject(source?.content); + if (source?.source !== "goal_acceptance") continue; + const acceptance = jsonObject(content?.goal_acceptance_contract); + if (content?.status !== "loaded" || content.read_complete === false || !acceptance?.outcome_requirements) return false; + const prefix = acceptance.goal_objective ? `${OBJECTIVE_READ_GUIDANCE} ` : ""; + if (source.reason !== prefix + outcomeReadGuidance()) return false; + changes.push({source, reason: prefix + outcomeReadGuidance(true)}); + } + if (!changes.length) return false; + for (const {source, reason} of changes) source.reason = reason; + return true; +} + /** Generate the shared pre-work reads after final admission. Hosts consume this * list; transport projections must not independently infer requirements. * Python supplies registered source routing, never a second admission rule. @@ -814,9 +846,9 @@ function interactionRequiredReads(request: JsonObject): JsonObject { if (stateFile) add(`cat -- ${shellQuote(stateFile)}`, "goal_state", "Read full Goal intent, acceptance, stops. No summaries; failed/changed sources need a fresh guard."); if (request.goal_objective_present === true) add(`${prefix} --format json goal-acceptance inspect --goal-id ${goalArg}`, - "goal_acceptance", "Use the canonical goal_objective before the older source text. Only if your adoption is missing/stale, review your own work, reconcile Todo/Vision changes, then use adopt-objective with your Agent id, exact revision/digest, own objective_work_digest and review_summary. New execution is held until your adoption. Other peers adopt independently; existing Turn settlement/release remains available. Preserve root acceptance, permissions and stops. Adopt once per objective revision, never per Turn or work/peer update. Reuse complete inline context without extra reads/writes."); + "goal_acceptance", OBJECTIVE_READ_GUIDANCE); if (checksPresent || request.goal_outcomes_present === true) add(`${prefix} --format json goal-acceptance inspect --goal-id ${goalArg}`, - "goal_acceptance", request.goal_outcomes_present === true ? "Read the full acceptance basis and any supporting outcomes with cumulative requirements, work links and sources. Preserve the original Goal and earlier requirements when applying direct user refinements. Before switching work or declaring completion, reconcile unmet outcome requirements with subsequent work; batch changed requirements/evidence reviews through refresh-state --goal-outcome-update-json or goal-acceptance refine. Reuse complete inline context; unchanged work needs no extra inspect/write. Agent review is distinct from independent checks. Scoped acceptance never replaces Goal intent. Changed requirements need a fresh guard." : "Read full configured objective, non-goals and criteria with scope/revision. Scoped acceptance does not replace Goal intent or prove completion; Removed or changed checks require a fresh guard."); + "goal_acceptance", request.goal_outcomes_present === true ? outcomeReadGuidance() : "Read full configured objective, non-goals and criteria with scope/revision. Scoped acceptance does not replace Goal intent or prove completion; Removed or changed checks require a fresh guard."); const todoId = hasReplan ? null : selectedWorkTodoId(request); if (todoId) add(`${prefix} --format json todo list --goal-id ${goalArg} --todo-id ${shellQuote(todoId)}`, "selected_todo", "Read full current requirements and status/claim. Require one matching active Todo; missing, ambiguous or changed work requires a fresh guard. A summary cannot replace this read."); @@ -859,8 +891,38 @@ export function assertGoalContextDelivered(channel: JsonObject): void { } } -/** Deliver full registered sources once. Ordinary mixed Goals retain progressive - * reads; native delivery can require the complete original. */ +/** Completion binds to the admitted body, including whitespace, never a live + * reread or display prefix. Both complete contracts and historical journals + * use this resolver; absence of the old witness retains receipt recovery. */ +export function capturedTodoTextSha256( + selected: JsonObject, contextValue: unknown, recommendedAction: unknown, todoId: unknown, +): string | null { + if (!Object.hasOwn(selected, "text_source_verified")) return null; + if (selected.text_source_verified !== true || selected.todo_id !== todoId) { + throw new EffectRuntimeRequestError("Turn task source does not match its selected Todo"); + } + const context = contextValue == null ? {} : jsonObject(contextValue); + if (context === null || (context.sources !== undefined && !Array.isArray(context.sources))) { + throw new EffectRuntimeRequestError("Turn completion requires its admitted full task source"); + } + const records = (Array.isArray(context.sources) ? context.sources : []) + .map(value => jsonObject(value)).filter(source => source?.source === "selected_todo") + .map(source => jsonObject(jsonObject(source!.content)?.todo)) + .filter(record => record?.todo_id === todoId && typeof record?.text === "string"); + if (records.length > 1) throw new EffectRuntimeRequestError("Turn task source is ambiguous"); + let text = records.length ? records[0]!.text : undefined; + if (text === undefined && (!Object.keys(context).length || context.selected_todo_ref === "selected_todo")) { + text = selected.text; + if (text == null && selected.text_ref === "action.recommended_action") text = recommendedAction; + } + if (typeof text !== "string" || !text) { + throw new EffectRuntimeRequestError("Turn completion requires its admitted full task source"); + } + return createHash("sha256").update(text, "utf8").digest("hex"); +} + +/** Deliver full registered sources once. Unowned mixed Goal documents remain + * progressive reads; failed/changed sources forbid dependent work. */ export function projectInteractionWorkContext(request: JsonObject): JsonObject { if (request.require_full_goal_source !== undefined && typeof request.require_full_goal_source !== "boolean") { throw new EffectRuntimeRequestError("Goal source delivery requirement must be boolean"); diff --git a/loopx/presentation/renderers/quota_markdown.py b/loopx/presentation/renderers/quota_markdown.py index 81afc46986..97ff029c64 100644 --- a/loopx/presentation/renderers/quota_markdown.py +++ b/loopx/presentation/renderers/quota_markdown.py @@ -351,6 +351,10 @@ def render_quota_should_run_markdown(payload: dict[str, Any]) -> str: from ...control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if payload.get("schema_version") == "loopx_turn_envelope_v1": + from .turn_envelope_markdown import render_turn_envelope_markdown + + return render_turn_envelope_markdown(payload) quota = as_dict(payload.get("quota")) lines = [ "# LoopX Quota Should Run", diff --git a/loopx/presentation/renderers/turn_envelope_markdown.py b/loopx/presentation/renderers/turn_envelope_markdown.py index 6edc454532..ca301d10ed 100644 --- a/loopx/presentation/renderers/turn_envelope_markdown.py +++ b/loopx/presentation/renderers/turn_envelope_markdown.py @@ -46,6 +46,10 @@ def render_turn_envelope_markdown(payload: dict[str, Any]) -> str: from ...control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if payload.get("schema_version") == "loopx_turn_envelope_v1": + # The complete contract has no separately selected Markdown facts. + # JSON escaping also keeps arbitrary source text inside this one block. + return "# LoopX Turn\n\n```json\n" + json.dumps(payload, ensure_ascii=False, indent=2) + "\n```" action_value = payload.get("action") user_value = payload.get("user") writeback_value = payload.get("writeback") diff --git a/packages/loopx-ark-turn/tests/test_host.py b/packages/loopx-ark-turn/tests/test_host.py index 5db02b46e1..61c686b671 100644 --- a/packages/loopx-ark-turn/tests/test_host.py +++ b/packages/loopx-ark-turn/tests/test_host.py @@ -12,6 +12,8 @@ import pytest from arkruntime import AsyncArk +from tests.control_plane.test_turn_host_decision_capture import complete_host_request as complete_host_request + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document from loopx_ark_turn.config import AdapterError, Config from loopx_ark_turn.host import run @@ -336,3 +338,60 @@ async def exercise(): def test_unsafe_configuration_is_rejected(tmp_path, override): with pytest.raises(AdapterError): replace(config(tmp_path, tools=False), **override) + + +@pytest.mark.parametrize("checkpoint", ["completed", "running"]) +def test_complete_cli_input_reaches_sdk_and_replays_original_receipt(tmp_path, monkeypatch, complete_host_request, checkpoint): + req, contract = complete_host_request + cfg = config(tmp_path, tools=False) + provider = Provider([]) + checkpoints = [] + save = Receipt.save + + def capture(receipt): + save(receipt) + if receipt.data["stage"] == "running": + checkpoints.append(copy.deepcopy(receipt.data)) + + monkeypatch.setattr(Receipt, "save", capture) + result = asyncio.run(execute(provider, cfg, req)) + sent = [body["events"][0] for method, path, body in provider.calls + if method == "POST" and path.endswith("/events")] + assert len(sent) == 1 and sent[0]["type"] == "user.message" + prompt = sent[0]["content"][0]["text"] + model_input, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + assert model_input["work_contract"] == contract + assert "turn_envelope" not in model_input and "turn_authority" not in model_input + assert result["turn_key"] == req["turn_key"] + if checkpoint == "running": + # Replay a real persisted checkpoint; the provider retains the original + # input and result. Recovery must observe it instead of submitting again. + assert checkpoints + Receipt(cfg.state_dir, req["turn_key"]).path.write_text(json.dumps(checkpoints[0])) + provider.deleted.clear() + calls = len(provider.calls) + assert asyncio.run(execute(provider, cfg, req)) == result + if checkpoint == "completed": + assert len(provider.calls) == calls + else: + assert not [row for row in provider.calls[calls:] if row[0] == "POST"] + + +@pytest.mark.parametrize("mutation", ["source", "version", "binding", "missing"]) +def test_complete_cli_input_rejects_corruption_before_provider(tmp_path, complete_host_request, mutation): + original, _ = complete_host_request + req = copy.deepcopy(original) + if mutation == "source": + req["work_contract"]["goal_id"] = "different-goal" + elif mutation == "version": + req["work_contract"]["schema_version"] = "unsupported-contract" + elif mutation == "binding": + req["work_contract"]["contract_binding"]["scheduler_execution_args"] = "different-host" + else: + req["work_contract"] = None + cfg = config(tmp_path, tools=False) + provider = Provider([]) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + asyncio.run(execute(provider, cfg, req)) + assert provider.calls == [] + assert not cfg.state_dir.exists() diff --git a/packages/loopx-memt-turn/README.md b/packages/loopx-memt-turn/README.md index 043b3ff95c..3391675269 100644 --- a/packages/loopx-memt-turn/README.md +++ b/packages/loopx-memt-turn/README.md @@ -96,8 +96,17 @@ authentication. Provider credentials and arbitrary parent environment values are not forwarded. This adapter is not an OS sandbox. Do not select a server that grants unrestricted effects based on model arguments or remembered text. -For an existing registered Goal, Agent and eligible claimed Todo, use the -normal Turn arguments and independent validator, with these explicit options: +The integrated launch path is currently unqualified: executable legacy Goal +registries do not supply the exact `goal_ref` required by this adapter, while +`source_session_v1` is lifecycle-only and the generic Turn entrypoint rejects it. +The adapter fails before provider IO when the reference is absent. Do not inject +an instance identifier, relax that check or change registry profiles to work +around it. The shared Goal/Turn owner must first provide admitted exact-lifetime +identity through planning and recovery; see the +[controller migration checkpoint](../../docs/architecture/rfcs/controller-executor-unified-contract-v0.md). + +The intended command shape, once that prerequisite is delivered and qualified, +uses a registered Goal, Agent, eligible claimed Todo and independent validator: ```sh loopx turn run-once --goal-id demo --agent-id analyst --todo-id todo_demo \ @@ -106,9 +115,10 @@ loopx turn run-once --goal-id demo --agent-id analyst --todo-id todo_demo \ --validation-command-json '["python3","/srv/operator/check_artifact.py"]' ``` -This previews the Turn. Supply an operator-authored validator that independently -checks the real artifact, then add `--execute` to the reviewed command. Existing -admission and permission requirements still apply. The adapter returns one +This is a preview command, not evidence that the current integrated path works. +After the exact-lifetime prerequisite is qualified, supply an operator-authored +validator that independently checks the real artifact before adding `--execute`. +Existing admission and permission requirements still apply. The adapter returns one `loopx_turn_result_v0` candidate; a successful model reply is not acceptance. Execution must start in the configured workspace; the normal Turn launcher sets that working directory. A mismatch fails before any provider calls. diff --git a/packages/loopx-memt-turn/tests/test_host.py b/packages/loopx-memt-turn/tests/test_host.py index f70764e7fe..1f55e27f78 100644 --- a/packages/loopx-memt-turn/tests/test_host.py +++ b/packages/loopx-memt-turn/tests/test_host.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import copy from dataclasses import replace import json from pathlib import Path @@ -13,6 +14,7 @@ from loopx_memt_turn.host import run from memt_fixtures import turn, with_tools +from tests.control_plane.test_turn_host_decision_capture import complete_host_request as complete_host_request def execute(config, request=None): @@ -194,3 +196,54 @@ def test_receipt_is_not_a_transcript_or_credential_log(config, provider): execute(config) receipts = [json.loads(path.read_text()) for path in Path(config.state_dir).rglob("*.json")] assert not any("input_ids" in receipt or "responses" in receipt for receipt in receipts) + + +def test_complete_cli_input_without_exact_lifetime_cannot_open_memory(config, provider, complete_host_request): + request, _ = complete_host_request + assert "goal_ref" not in request + with pytest.raises(AdapterError, match="exact_goal_instance_required"): + execute(config, request) + assert provider.requests == [] + assert not Path(config.state_dir).exists() + + +def test_lifecycle_only_profile_cannot_supply_an_executable_turn(tmp_path): + from tests.control_plane.test_turn_host_decision_capture import _fixture_module + from loopx.control_plane.projects.registry_codec import source_session_registry_transaction + + fixture = _fixture_module() + _, runtime, _, legacy = fixture._write_fixture(tmp_path, turn_count=2) + registration = json.loads(legacy.read_text()) + registration.update(schema_version="0.2", registry_role="project-local", profile_id="source_session_v1", + projects=[], session_bindings=[], session_receipts=[], lifetime_receipts=[], retired_goal_instances=[]) + registration["goals"][0].update(goal_instance_id="ginst_" + "a" * 32, execution_authority=False) + registry = legacy.with_name("source-registry.json") + with source_session_registry_transaction(registry, operation="optional_host_fixture", create=lambda: registration) as tx: + tx.commit(registration) + before = registry.read_bytes() + code, rejected = fixture._run_cli(["--registry", str(registry), "--runtime-root", str(runtime), + "--format", "json", "turn", "plan", "--goal-id", fixture.GOAL_ID, + "--agent-id", fixture.AGENT_ID, "--host", "generic-cli", "--iteration-context", "fresh"]) + assert code == 1 and rejected["ok"] is False + assert "lifecycle-only profile" in rejected["error"] + assert registry.read_bytes() == before + assert not list(runtime.rglob("decisions/*.json")) + assert fixture._quota_spend_count(runtime) == 0 + + +@pytest.mark.parametrize("mutation", ["source", "version", "binding", "missing"]) +def test_complete_cli_input_rejects_corruption_before_memory(config, provider, complete_host_request, mutation): + original, _ = complete_host_request + request = copy.deepcopy(original) + if mutation == "source": + request["work_contract"]["goal_id"] = "different-goal" + elif mutation == "version": + request["work_contract"]["schema_version"] = "unsupported-contract" + elif mutation == "binding": + request["work_contract"]["contract_binding"]["scheduler_execution_args"] = "different-host" + else: + request["work_contract"] = None + with pytest.raises(ValueError, match="complete Turn contract rejected"): + execute(config, request) + assert provider.requests == [] + assert not Path(config.state_dir).exists() diff --git a/scripts/external_scheduler_worker.py b/scripts/external_scheduler_worker.py index 5f6822e25c..481ff91058 100644 --- a/scripts/external_scheduler_worker.py +++ b/scripts/external_scheduler_worker.py @@ -109,6 +109,14 @@ def _local_scheduler_directive(hint: dict[str, Any]) -> str: def parse_tick(payload: dict[str, Any]) -> TickDecision: + # Versioned source must pass the same semantic/integrity owner as managed + # hosts before scheduler or wake facts are used. Never reinterpret an + # unsupported or damaged complete contract as an unversioned legacy packet. + # Historical ordinary quota packets retain their existing compatibility path. + if "schema_version" in payload or "contract_binding" in payload: + from loopx.control_plane.quota.turn_envelope import assert_complete_turn_decision + + assert_complete_turn_decision(payload) hint = _mapping(payload.get("scheduler_hint")) action = str(hint.get("action") or "").strip() cadence_class = str(hint.get("cadence_class") or "").strip() diff --git a/scripts/qualify-native-goal-release.py b/scripts/qualify-native-goal-release.py index 8b80d58711..17bc5397e4 100644 --- a/scripts/qualify-native-goal-release.py +++ b/scripts/qualify-native-goal-release.py @@ -112,6 +112,7 @@ def setup( ) fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) + # Finish the release source before its one canonical cold import. project, runtime, registry = fixture.write_fixture(root, initialize=False) config = json.loads(registry.read_text()) goal = config["goals"][0] diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index eb84be30dd..71e26db36d 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -18,7 +18,7 @@ from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery -from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _spend_run_count, _write_fixture +from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _run_cli, _spend_run_count, _write_fixture from tests.control_plane.codex_checkpoint_fixture import ( local_inference_provider, delivered_context as _delivered, last_context_page as _last_page, ) @@ -37,7 +37,7 @@ + [(host, surface, False, True, False) for host in ("benchmark", "code_mode") for surface in ("quota_packet", "turn_envelope")] + [("code_mode", surface, False, True, True) for surface in ("quota_packet", "turn_envelope")]) -def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False, ssh_goal=False, reader_output_bytes=None, recovery_resume=False): +def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False, ssh_goal=False, reader_output_bytes=None, recovery_resume=False, scoped_outcomes=False): # Real worker qualification follows returned cursors; output budgets can # shrink a page, so precomputed fixed offsets are not its production path. bounded_pages = bounded_pages or host == "benchmark" @@ -57,9 +57,31 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho body += '\nMixed UTF-8 and JSON escaping: 汉🙂"\\\t' * 100 todos = read_fixture_todos(state) todos[0].update(text=body, title=body) + if scoped_outcomes: + config = json.loads(registry.read_text()) + config["goals"][0].update(status="active", adapter={"kind": "generic_project_goal_v0", "status": "connected"}) + config["goals"][0]["coordination"]["registered_agents"].append("independent-peer") + registry.write_text(json.dumps(config)) + state.write_text(state.read_text().replace("status: active-read-only", "status: active")) + todos.append({**todos[0], "todo_id": "todo_independent_peer", "text": "Independent peer deliverable.", + "title": "Independent peer deliverable.", "claimed_by": "independent-peer"}) initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim"), state_path=state, provider="sqlite") + if scoped_outcomes: + for owner, todo_id, outcome_id in ((AGENT_ID, TODO_ID, "own-result"), + ("independent-peer", "todo_independent_peer", "peer-result")): + document = workspace / f"{outcome_id}.json" + document.write_text(json.dumps({"outcome_id": outcome_id, "owner_agent_id": owner, + "objective": f"Deliver {outcome_id}.", "todo_ids": [todo_id], + "requirements": [{"id": "complete", "description": f"Complete {outcome_id} requirement tail."}], + "source": {"ref": f"message:{outcome_id}", "summary": f"Independent origin for {outcome_id}."}})) + rc, observed_acceptance = _run_cli(registry, runtime, "goal-acceptance", "inspect", "--goal-id", GOAL_ID) + assert rc == 0, observed_acceptance + rc, refined = _run_cli(registry, runtime, "goal-acceptance", "refine", "--goal-id", GOAL_ID, + "--agent-id", owner, "--document", str(document), "--operation-id", f"declare-{outcome_id}", + "--expected-provider-revision", observed_acceptance["provider_revision"], "--execute") + assert rc == 0, refined original_state = state.read_bytes() root = workspace / "captures" root.mkdir() @@ -80,7 +102,7 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho capture_output=True, text=True, timeout=15) assert prepared.returncode == 0, prepared.stderr native_configuration = json.loads(prepared.stdout)["native_config"] - automatic_output_budget = host == "benchmark" or native_configuration is not None + automatic_output_budget = host == "benchmark" or native_configuration is not None or scoped_outcomes native_guard_output_bytes = (7900 if not automatic_output_budget or ( native_history_tokens is not None and native_configuration is not None and native_history_tokens < native_configuration["config_overrides"]["tool_output_token_limit"]) @@ -454,6 +476,9 @@ def observed_response(request, number, message): with local_inference_provider(observed_response, usage) as (url, calls, observed): if host != "benchmark": _configure_provider(home, url) + if scoped_outcomes: + config = home / "config.toml" + config.write_text("tool_output_token_limit=20000\n" + config.read_text()) if host == "code_mode": config = home / "config.toml" config.write_text(config.read_text().replace("[features]\n", "[features]\ncode_mode=true\n")) @@ -543,7 +568,6 @@ def held(*args, **kwargs): # checked first readback through the real CLI, rather than # reusing a stale guard/Turn from resumed history to obtain an # artificially eligible warm packet. - from test_quota_settlement_cli import _run_cli expected = hashlib.sha256(read_paths["cold"].read_text().rstrip("\n").encode()).hexdigest() assert stages["cold_readback_sha256"] == expected artifact = project / "native-context-readback.txt" @@ -643,7 +667,8 @@ def held(*args, **kwargs): projection = Path(wires["warm"]["recovery"]["path"]) assert hashlib.sha256(projection.read_bytes()).hexdigest() == wires["warm"]["recovery"]["sha256"] current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, - capture_id=wires["warm"]["target"]["capture_id"], surface=surface) + capture_id=wires["warm"]["target"]["capture_id"], surface="complete_turn") + assert current["target"] == wires["warm"]["target"] baseline = wires["cold"] if resumed and qualified_resume and not resume_loss: assert wires["warm"]["base"] == baseline["target"] @@ -655,13 +680,34 @@ def held(*args, **kwargs): assert current["target"]["context_epoch"] != baseline["target"]["context_epoch"] baseline = prepare_context_delivery(baseline["context"], scope={ k: current["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, - capture_id=baseline["target"]["capture_id"], surface=surface) + capture_id=baseline["target"]["capture_id"], surface="complete_turn") assert baseline["target"] == wires["warm"]["base"] restored = restore_context_delivery(wires["warm"], baseline=baseline, current=current) assert restored["context"] == current["context"] and restored["mode"] == wires["warm"]["mode"] - for document in (wires["cold"]["context"], current["context"]): - context = (document["work_context"] if surface == "turn_envelope" else - document["interaction_contract"]["agent_channel"]["work_context"]) + # Both CLI spellings now emit the same complete contract. Compare every + # reconstructed fact with its original capture, not two compact views. + for document, (_, raw) in zip((wires["cold"]["context"], current["context"]), snapshots, strict=True): + original = json.loads(raw) + if scoped_outcomes: + assert document["schema_version"] == "outcome_context_projection_v0" + assert {row["id"]: row["required_in_scope"] for row in document["outcome_index"]} == { + "own-result": True, "peer-result": False} + assert "Complete own-result requirement tail." in json.dumps(document) + assert "Complete peer-result requirement tail." not in json.dumps(document) + assert json.loads(Path(document["source_capture"]["path"]).read_bytes()) == original + # Independent full-source oracle: only the declared reading scope + # and its owner-generated instruction differ in model presentation. + document = json.loads(json.dumps(document["context"])) + actual_sources = document["interaction_contract"]["agent_channel"]["work_context"]["sources"] + original_sources = original["interaction_contract"]["agent_channel"]["work_context"]["sources"] + for actual_source, original_source in zip(actual_sources, original_sources, strict=True): + if actual_source.get("source") == "goal_acceptance": + assert "read_scope" in actual_source["reason"] + actual_source["reason"] = original_source["reason"] + actual_source["content"]["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] = original_source["content"]["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] + assert document == original + assert document["schema_version"] == "loopx_turn_envelope_v1" + context = document["interaction_contract"]["agent_channel"]["work_context"] selected = next(source["content"]["todo"] for source in context["sources"] if source["source"] == "selected_todo") assert selected["todo_id"] == TODO_ID and selected["text"] == body @@ -700,6 +746,12 @@ def held(*args, **kwargs): assert bounded_sizes and max(bounded_sizes) > 3072 +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_scoped_outcome_input_and_same_capture_recovery(tmp_path, monkeypatch, surface): + test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, "code_mode", surface, + False, False, False, bounded_pages=True, scoped_outcomes=True) + + @pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) def test_native_compact_recovery_survives_process_resume(tmp_path, monkeypatch, surface): test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index fe0ce53e01..fda0a1e04f 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -155,6 +155,39 @@ def deliver(root, event, pages): "tool_use_id": f"page-{page.get('offset')}", "tool_response": json.dumps(page)}) == {} +def test_scope_projection_interruption_keeps_current_original_recovery(tmp_path, monkeypatch): + from loopx.control_plane.quota import turn_envelope + + event = setup(tmp_path, monkeypatch) + _, old = project(tmp_path, "old") + deliver(tmp_path, event, read_pages(tmp_path, old)) + assert "candidate" in context._state(tmp_path) and "pending" not in context._state(tmp_path) + operation = turn_envelope._turn_envelope_result + observed = [] + + class ScopeInterrupted(BaseException): + pass + + def interrupt(name, **request): + if name == "outcome_context": + observed.append(context._state(tmp_path)) + raise ScopeInterrupted() + return operation(name, **request) + + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", interrupt) + current = {"ok": True, "goal_id": "fixture", "requirements": "New current requirements after the prior delivery."} + with pytest.raises(ScopeInterrupted): + project(tmp_path, "current", payload=current) + expected = tmp_path / "current" / "projection.json" + assert observed[0]["recovery"]["path"] == str(expected) + assert observed[0]["pending"]["path"] == str(expected) + stored = context._state(tmp_path) + assert stored["recovery"]["path"] == str(expected) + assert stored["pending"]["path"] == str(expected) + assert json.loads(expected.read_bytes()) == current + assert stored["recovery"]["sha256"] != old["recovery"]["sha256"] + + def test_complete_reader_delivery_allows_delta_without_diagnostics_or_ack(tmp_path, monkeypatch): event = setup(tmp_path, monkeypatch) payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py new file mode 100644 index 0000000000..bdfe235629 --- /dev/null +++ b/tests/control_plane/test_complete_turn_decision.py @@ -0,0 +1,713 @@ +"""Candidate complete source through actual producers; no model is invoked.""" +import copy +from hashlib import sha256 +import json +from pathlib import Path +import subprocess +import sys + +import pytest + +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, compose_turn_decision, + prepare_context_delivery, restore_context_delivery, +) +from loopx.control_plane.turn_driver.codex_cli import _prompt +from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request +from tests.control_plane.test_quota_settlement_cli import ( + _write_fixture, _configure_read_only_todo, _run_cli, _spend_run_count, + GOAL_ID, AGENT_ID, TODO_ID, +) +from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + + +def _valid_interaction(): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + return _quota_payload()["interaction_contract"] + + +def _canonical_digest(value): + return "sha256:" + sha256(json.dumps(value, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + + +def _resign_contract(contract): + source = {key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}} + contract["contract_binding"].pop("content_hash") + contract["contract_binding"]["source_decision_hash"] = _canonical_digest(source) + contract["contract_binding"]["content_hash"] = _canonical_digest(contract) + return contract, source + + +def _future_producer_contract(contract): + contract = copy.deepcopy(contract) + contract["effective_action"] = "future_execution" + return _resign_contract(contract) + + +@pytest.mark.parametrize("mutation, error", [ + ("version", "schema_version"), + ("boolean", "must_attempt"), + ("notification", "notify"), + ("contradiction", "quiet"), +]) +def test_complete_readers_reject_invalid_interaction_with_valid_integrity(mutation, error): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + interaction = source["interaction_contract"] + if mutation == "version": + interaction["schema_version"] = "future_interaction_version" + elif mutation == "boolean": + interaction["agent_channel"]["must_attempt"] = "true" + elif mutation == "notification": + interaction["user_channel"]["notify"] = "FUTURE_NOTIFY" + else: + interaction["agent_channel"]["quiet_noop_allowed"] = True + # Obtain a valid current contract, then emulate a producer with different + # required semantics and correct integrity hashes. Every reader must refuse. + contract = compose_turn_decision(_quota_payload()) + contract["interaction_contract"] = interaction + contract, _ = _resign_contract(contract) + before = copy.deepcopy(source) + for read in ( + lambda: compose_turn_decision(source), + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review"), + ): + with pytest.raises(RuntimeError, match=error): + read() + assert source == before + + +def test_complete_source_rejects_tampering_and_binding_substitution(): + source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "effective_action": "normal_run", + "interaction_contract": {**_valid_interaction(), "future_optional": {"value": None}}, "future_evidence": {"original": "unchanged"}} + before = copy.deepcopy(source) + result = compose_turn_decision(source) + assert {k: v for k, v in result.items() if k not in {"schema_version", "contract_binding"}} == before + assert source == before + assert compose_turn_decision(result) == result + tampered = copy.deepcopy(result) + tampered["future_evidence"]["original"] = "changed" + with pytest.raises(RuntimeError, match="integrity mismatch"): + compose_turn_decision(tampered) + with pytest.raises(RuntimeError, match="host binding"): + _turn_envelope_result("compose_complete", payload=result, scheduler_execution_args="different") + with pytest.raises(RuntimeError, match="reserved"): + compose_turn_decision({**source, "schema_version": "unknown"}) + + +@pytest.mark.parametrize("action", ["future_execution", "", None, 0]) +def test_complete_source_refuses_unknown_required_action_before_capture(action): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + source["effective_action"] = action + original = copy.deepcopy(source) + with pytest.raises(RuntimeError, match="effective_action"): + compose_turn_decision(source) + assert source == original + + +def test_complete_readers_refuse_unknown_action_even_with_valid_integrity(): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + contract, _ = _future_producer_contract(compose_turn_decision(_quota_payload())) + # A future producer can supply perfectly valid hashes for a semantic value + # this consumer cannot execute. Integrity alone must not admit that action. + with pytest.raises(RuntimeError, match="effective_action"): + compose_turn_decision(contract) + with pytest.raises(RuntimeError, match="effective_action"): + build_turn_envelope(contract) + with pytest.raises(RuntimeError, match="effective_action"): + _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review") + + +def test_complete_action_validation_preserves_registered_union_and_optional_facts(): + from loopx.control_plane.quota.effective_action import EffectiveAction + from loopx.control_plane.agents.agent_scope_frontier import AgentScopeFrontierAction + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + for action in {*EffectiveAction, *AgentScopeFrontierAction}: + source = _quota_payload() + source["effective_action"] = action.value + source["future_evidence"] = {"schema_version": "future_optional_evidence", + "effective_action": "uninterpreted evidence", "result": None} + contract = compose_turn_decision(source) + assert contract["effective_action"] == action.value + assert contract["future_evidence"] == source["future_evidence"] + assert compose_turn_decision(contract) == contract + + +@pytest.mark.parametrize("mutation", ["version", "content", "binding"]) +def test_replan_reader_refuses_invalid_complete_contract_even_with_matching_file_hash(mutation): + from hashlib import sha256 + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + contract = compose_turn_decision(_quota_payload()) + if mutation == "version": + contract["schema_version"] = "unsupported-future-version" + elif mutation == "content": + contract["replan_action_packet"]["planning_guidance"] = ["Altered obligation"] + else: + contract.pop("contract_binding") + # A caller can truthfully hash the bytes it read without proving that their + # version or inner source binding is valid. Both checks are required. + digest = "sha256:" + sha256(json.dumps(contract, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + with pytest.raises(RuntimeError, match="version|integrity|binding"): + _turn_envelope_result("replan_view", payload=contract, source_hash=digest, stage="review") + + +@pytest.mark.parametrize("successor", [False, True]) +def test_complete_replan_carries_original_turn_successor_binding_before_capture(successor): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + plan = {"identity": {"turn_instance_id": "original-turn"}, + "goal_ref": {"goal_instance_id": "original-goal-instance"}} + source["interaction_contract"]["cli_channel"]["settlement_plan"] = plan + if successor: + source["replan_action_packet"]["writeback_contract"] = { + "successor_command": "loopx todo add --goal-id fixture", + "successor_host_action": "end_current_heartbeat"} + before = copy.deepcopy(source) + contract = compose_turn_decision(source) + writer = contract["replan_action_packet"]["writeback_contract"] + assert writer["successor_binding_args"] == ( + "--turn-instance-id original-turn --goal-instance-id original-goal-instance") + assert "original Turn settlement plan before ending" in writer["successor_settlement"] + assert "do not run the successor" in writer["successor_settlement"] + assert "successor_host_action" not in writer + assert contract["interaction_contract"]["cli_channel"]["settlement_plan"] == plan + if successor: + assert writer["successor_command"].endswith(writer["successor_binding_args"]) + assert contract["interaction_contract"]["cli_channel"]["next_cli_actions"] == [writer["successor_command"]] + assert source == before + assert compose_turn_decision(contract) == contract + + +@pytest.mark.parametrize("inactive", ["settlement_only", "capability_intent", "different_context", "unbound"]) +def test_complete_composition_preserves_inactive_replan_authoring(inactive): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}} + source["replan_action_packet"]["writeback_contract"] = { + "successor_command": "loopx todo add --goal-id fixture", + "successor_host_action": "end_current_heartbeat"} + if inactive == "settlement_only": + source["replan_action_packet"]["settlement_only"] = True + elif inactive == "capability_intent": + source["effective_action"] = "governed_capability_intent" + elif inactive == "different_context": + source["autonomous_replan_obligation"]["replan_context"]["obligation_id"] = "different" + else: + source["interaction_contract"]["cli_channel"].pop("settlement_plan") + contract = compose_turn_decision(source) + assert {k: v for k, v in contract.items() if k not in {"schema_version", "contract_binding"}} == source + + +def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): + from loopx.presentation.renderers.quota_markdown import render_quota_should_run_markdown + from loopx.presentation.renderers.turn_envelope_markdown import render_turn_envelope_markdown + + source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "effective_action": "normal_run", + "interaction_contract": _valid_interaction(), + "future_evidence": {"missing": None, "denied": False, "attempts": 0, "text": "0"}} + source["interaction_contract"]["agent_channel"]["required_reads"] = [ + {"instruction": "Read the original constraints.\n```\nStop before publication."}] + contract = compose_turn_decision(source) + before = copy.deepcopy(contract) + for render in (render_quota_should_run_markdown, render_turn_envelope_markdown): + rendered = render(contract) + # The complete JSON remains one source even for a human-readable CLI. + actual = json.loads(rendered.split("```json\n", 1)[1].rsplit("\n```", 1)[0]) + assert actual == before + assert contract == before + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("host_args", [("--codex-app",), ("--runtime-profile", "generic_cli")]) +def test_complete_quota_successor_settles_original_turn_without_staged_reader(tmp_path, monkeypatch, provider, host_args): + import shlex + from tests.control_plane.test_replan_successor_guard_reentry import _fixture, GOAL, AGENT, TURN + + call, runtime, index = _fixture(tmp_path, monkeypatch, provider, future_monitor=False) + capture = tmp_path / "capture" + guard = call("quota", "should-run", *host_args, "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", TURN, "--decision-output-dir", str(capture)) + original = (capture / "decision.json").read_bytes() + assert json.loads(original) == guard + identity = guard["heartbeat_receipt"]["settlement_identity"] + writer = guard["replan_action_packet"]["writeback_contract"] + binding = shlex.split(writer["successor_binding_args"]) + assert binding[binding.index("--turn-instance-id") + 1] == TURN + added = call("todo", "add", "--goal-id", GOAL, "--role", "agent", "--claimed-by", AGENT, + "--text", "Independently verify the source artifact", "--task-class", "advancement_task", + "--action-kind", "validate", "--target-key", "independent-source-artifact", + "--operation-id", "complete-source-successor", "--replan-obligation-id", identity["replan_obligation_id"], + *binding) + assert added["settlement_identity"] == identity + assert added["replan_transition"]["turn_boundary"] == "settle_original_turn_before_end" + steps = added["settlement_plan"]["ordered_steps"] + refresh = next(row["command_template"] for row in steps if row["kind"] == "durable_writeback") + spend = next(row["command_template"] for row in steps if row["kind"] == "quota_spend") + refused = call(*shlex.split(spend)[1:], expected_code=1) + assert refused.get("appended") is not True + for placeholder, value in {"": "successor_created", + "": "single_surface", "": "outcome_progress", + "": "in_flight_continuation"}.items(): + refresh = refresh.replace(placeholder, value) + refreshed = call(*shlex.split(refresh)[1:], "--no-global-sync", "--suppress-external-sinks", + "--vision-unchanged-reason", "The accepted source outcome is unchanged; the successor supplies its next validation.") + assert refreshed["settlement_progress"]["state"] == "spend_required" + spent = call(*shlex.split(spend)[1:]) + assert spent["settlement_progress"]["state"] == "settled" + assert call(*shlex.split(spend)[1:])["appended"] is False + assert sum(json.loads(line).get("classification") == "quota_slot_spent" for line in index.read_text().splitlines()) == 1 + todo = call("todo", "list", "--goal-id", GOAL, "--todo-id", added["todo_id"])["todo"] + assert todo["status"] == "open" # Creating/settling is not executing the successor. + assert (capture / "decision.json").read_bytes() == original + + # After inline execution/settlement, check historical material-reader parity. + from hashlib import sha256 + digest = "sha256:" + sha256(json.dumps(guard, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + staged = _turn_envelope_result("replan_view", payload=guard, source_hash=digest, stage="writeback") + assert staged["writeback_contract"]["successor_binding_args"] == writer["successor_binding_args"] + assert staged["writeback_contract"]["successor_settlement"] == writer["successor_settlement"] + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("format_name,envelope_flag", [("json", True), ("markdown", True), ("markdown", False)]) +def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeypatch, provider, format_name, envelope_flag): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = _configure_read_only_todo(project) + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, read_fixture_todos + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state)), + state_path=state, provider=provider) + capture = tmp_path / "capture" + command = [sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), + "--runtime-root", str(runtime), "--format", format_name, "quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "complete-source", + "--codex-app", "--available-capability", "shell", "--available-capability", "filesystem_read", + "--decision-output-dir", str(capture)] + if envelope_flag: + command.append("--turn-envelope") + process = subprocess.run(command, cwd=Path(__file__).resolve().parents[2], + capture_output=True, text=True, check=False) + assert process.returncode == 0, process.stdout + process.stderr + text = process.stdout if format_name == "json" else process.stdout.split("```json\n", 1)[1].rsplit("\n```", 1)[0] + output = json.loads(text) + original = (capture / "decision.json").read_bytes() + contract = json.loads(original) + assert contract["schema_version"] == "loopx_turn_envelope_v1" + assert output["interaction_contract"] == contract["interaction_contract"] + assert output["contract_binding"] == contract["contract_binding"] + assert output == contract # Every original fact and instruction survives rendering. + assert "action" not in output and "contract_capsule" not in output + scope = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="fixture", context_epoch="one") + first = prepare_context_delivery(contract, scope=scope, capture_id="one", surface="complete_turn") + current = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn") + warm = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn", + baseline=first, retained={"complete": True, "target": first["target"]}) + assert restore_context_delivery(warm, baseline=first, current=current)["context"] == contract + assert restore_context_delivery(warm, baseline=None, current=current)["context"] == contract + old = prepare_context_delivery(contract, scope=scope, capture_id="one", surface="quota_packet") + changed_surface = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn", + baseline=old, retained={"complete": True, "target": old["target"]}) + assert changed_surface["mode"] == "full" + assert (capture / "decision.json").read_bytes() == original + assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("first_attempt_fails", [False, True]) +def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, provider, first_attempt_fails): + from tests.control_plane.test_turn_host_decision_capture import _fixture_module + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + isolate_sqlite_runtime(tmp_path, monkeypatch) + f = _fixture_module() + project, runtime, workspace, registry = f._write_fixture(tmp_path, turn_count=2) + state = project / ".codex/goals" / f.GOAL_ID / "ACTIVE_GOAL_STATE.md" + summary = parse_active_state_todos(state.read_text(), state_path=state, item_limit=None) + initialize_canonical_authority(runtime, f.GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=f.GOAL_ID, handoff_mode="soft_claim", + todos=summary["agent_todos"]["items"]), state_path=state, provider=provider) + # Add acceptance through the real authority API, so native execution and + # failed-turn recovery exercise sharing rather than a hand-built contract. + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + document = tmp_path / "acceptance.json" + document.write_text(json.dumps({"outcome_id": "complete-input", "owner_agent_id": f.AGENT_ID, + "objective": "Deliver the complete requested artifact", "todo_ids": [f.TODO_ID], + "requirements": [{"id": "complete", "description": "Preserve the original content and tail constraint."}], + "source": {"ref": "message:owner", "summary": "Original artifact requirement"}})) + code, inspected = f._run_cli([*prefix, "goal-acceptance", "inspect", "--goal-id", f.GOAL_ID]) + assert code == 0, inspected + code, refined = f._run_cli([*prefix, "goal-acceptance", "refine", "--goal-id", f.GOAL_ID, + "--agent-id", f.AGENT_ID, "--document", str(document), "--expected-provider-revision", + inspected["provider_revision"], "--operation-id", "original-acceptance", "--execute"]) + assert code == 0, refined + code, inspected = f._run_cli([*prefix, "goal-acceptance", "inspect", "--goal-id", f.GOAL_ID]) + assert code == 0, inspected + expected_acceptance = inspected["goal_acceptance_contract"] + args = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", + "turn", "plan", "--goal-id", f.GOAL_ID, "--agent-id", f.AGENT_ID, + "--turn-instance-id", "complete-source", "--host", "codex-cli", + "--execution-mode", "isolated-headless", "--include-transaction-detail"] + code, plan = f._run_cli(args) + assert code == 0, plan + assert "work_contract" in plan, json.dumps(plan) + contract = plan["work_contract"] + assert contract["schema_version"] == "loopx_turn_envelope_v1" + request = build_loopx_turn_host_request(plan) + prompt = _prompt(request) + actual = json.loads(prompt.split("Turn request:\n", 1)[1]) + assert actual["work_contract"] == contract + assert "turn_envelope" not in actual and "turn_authority" not in actual + from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt + structured = render_prompt(extract_turn_authority(request)) + structured_input, _ = json.JSONDecoder().raw_decode(structured.split("Turn request:\n", 1)[1]) + assert structured_input == actual + assert "_model_input" not in structured_input + tampered = copy.deepcopy(request) + tampered["work_contract"]["reason"] = "different facts" + with pytest.raises(RuntimeError, match="integrity mismatch"): + _prompt(tampered) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(tampered) + from loopx.dsh_goal_mode.turn_host_adapter import DshHostConfig, run_dsh_host + from loopx.control_plane.turn_driver.host_failure import BuiltInHostError + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(tampered, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + + assert not (workspace / ".dsh").exists() + assert not list(runtime.rglob("decisions/*.json")) + assert f._quota_spend_count(runtime) == 0 + + # An old journal must not inherit a new admission's complete source. + from loopx.control_plane.turn_driver.decision_capture import prepare_turn_decision_capture + historical = {key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}} + historical_envelope = _turn_envelope_result("build", payload=historical, + scheduler_execution_args=contract["contract_binding"]["scheduler_execution_args"]) + historical_plan = {**plan, "turn_envelope": historical_envelope} + historical_journal = tmp_path / "historical" / "journal.json" + prepared = prepare_turn_decision_capture(historical_plan, journal_path=historical_journal, + decision=historical, existing_journal=False) + restored = prepare_turn_decision_capture({**prepared, "work_contract": contract}, + journal_path=historical_journal, decision=None, existing_journal=True) + assert "work_contract" not in restored + assert "work_contract" not in build_loopx_turn_host_request(restored) + + executable = f._write_fake_codex(tmp_path) + script = executable.read_text().replace("#!/usr/bin/env python3", "#!" + sys.executable) + script = script.replace("prompt = sys.stdin.read()", 'prompt = sys.stdin.read()\npathlib.Path("received.txt").write_text(prompt)') + if first_attempt_fails: + script = script.replace("marker = pathlib.Path(", + 'failed = pathlib.Path("failed-once")\n' + 'if not failed.exists():\n' + ' failed.touch()\n' + ' print(json.dumps({"type":"thread.started","thread_id":"session-fixture-0001"}), flush=True)\n' + ' print("Selected model is at capacity. Please try a different model.", file=sys.stderr)\n' + ' raise SystemExit(9)\n' + 'marker = pathlib.Path(') + executable.write_text(script) + run_args = f._base_argv(registry=registry, runtime=runtime, workspace=workspace, + codex_bin=executable, model=None, timeout_seconds=20, + expected_marker=f._marker_value(1), turn_instance_id="complete-execution") + from loopx.cli_commands import turn as native_turn + real_compose = native_turn.compose_turn_decision + def future_action(payload, **kwargs): + return real_compose({**payload, "effective_action": "future_execution"}, **kwargs) + with monkeypatch.context() as patch: + # Fault only the producer's semantic value. Admission, provider, + # composition, CLI refusal and capture/host boundaries remain real. + patch.setattr(native_turn, "compose_turn_decision", future_action) + code, refused = f._run_cli([*run_args, "--execute"]) + assert code == 1 and "effective_action" in json.dumps(refused), refused + assert not (workspace / "received.txt").exists() + assert not list(runtime.rglob("decisions/*.json")) + assert f._quota_spend_count(runtime) == 0 + # Preserve the historical decoder's existing route, but refuse the same + # unknown execution semantics in a validly hashed complete contract before + # either native host can load a runner or produce a captured execution. + from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan + future_contract, future_source = _future_producer_contract(contract) + legacy_envelope = _turn_envelope_result("build", payload=future_source, + scheduler_execution_args=contract["contract_binding"]["scheduler_execution_args"]) + legacy_plan = build_loopx_turn_plan(legacy_envelope, host="codex-cli", + execution_mode="isolated-headless", turn_instance_id="future-source") + assert legacy_plan["route"]["would_invoke_host"] is True + future_request = build_loopx_turn_host_request({**legacy_plan, "work_contract": future_contract}) + with pytest.raises(RuntimeError, match="effective_action"): + _prompt(future_request) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(future_request) + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(future_request, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + + invalid_interaction = copy.deepcopy(request) + invalid_interaction["work_contract"]["interaction_contract"]["schema_version"] = "future_interaction_version" + invalid_interaction["work_contract"], _ = _resign_contract(invalid_interaction["work_contract"]) + with pytest.raises(RuntimeError, match="interaction_contract.schema_version"): + _prompt(invalid_interaction) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(invalid_interaction) + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(invalid_interaction, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + assert not (workspace / "received.txt").exists() + assert not (workspace / ".dsh").exists() + assert f._quota_spend_count(runtime) == 0 + + code, executed = f._run_cli([*run_args, "--execute"]) + if first_attempt_fails: + assert code == 1 and executed["status"] == "failed", executed + assert f._quota_spend_count(runtime) == 0 + else: + assert code == 0 and executed["status"] == "committed", executed + observed = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) + assert observed["work_contract"]["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in observed + acceptance_sources = observed["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] + acceptance_index = next(i for i, row in enumerate(acceptance_sources) if row["source"] == "goal_acceptance") + assert acceptance_sources[acceptance_index]["content"]["goal_acceptance_contract"] == expected_acceptance + assert observed["work_contract"]["agent_todo_summary"]["goal_acceptance_contract_ref"] == ( + f"$.interaction_contract.agent_channel.work_context.sources[{acceptance_index}].content.goal_acceptance_contract") + saved = list(runtime.rglob("decisions/*.json")) + assert len(saved) == 1 + saved_bytes = saved[0].read_bytes() + assert json.loads(saved_bytes) == observed["work_contract"] + replay_args = list(run_args) + index = replay_args.index("--turn-instance-id") + del replay_args[index:index + 2] + if first_attempt_fails: + retry_args = [*replay_args, "--resume-turn-key", observed["turn_key"], "--retry-failed-turn", "--execute"] + received = (workspace / "received.txt").read_bytes() + for corrupt in (True, False): + if corrupt: + saved[0].write_text(json.dumps({**observed["work_contract"], "reason": "tampered source"})) + else: + saved[0].unlink() + code, rejected = f._run_cli(retry_args) + assert code == 1, rejected + assert (workspace / "received.txt").read_bytes() == received + assert f._quota_spend_count(runtime) == 0 + saved[0].write_bytes(saved_bytes) + code, recovered = f._run_cli(retry_args) + assert code == 0 and recovered["status"] == "committed", recovered + restored = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) + assert restored["work_contract"] == observed["work_contract"] + code, replay = f._run_cli([*replay_args, "--resume-turn-key", observed["turn_key"], "--execute"]) + assert code == 0 and replay["replayed"] is True, replay + assert saved[0].read_bytes() == saved_bytes + assert f._quota_spend_count(runtime) == 1 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("prior_todo_binding", [False, True]) +def test_native_replan_complete_input_retains_verified_material_readers(tmp_path, monkeypatch, provider, prior_todo_binding): + from tests.control_plane.test_turn_replan_writeback import _fixture + from tests.control_plane.test_quota_settlement_cli import _classification_count, SELECTED_REPLAN_TODO_ID + project, runtime, registry, _state, _prior_turn = _fixture(tmp_path, monkeypatch, provider) + configuration = json.loads(registry.read_text()) + configuration["goals"][0]["explore_graph"] = {"enabled": True} + registry.write_text(json.dumps(configuration)) + turn = _prior_turn if prior_todo_binding else "complete-replan-readers" + executable = tmp_path / "replan-host" + executable.write_text("#!" + sys.executable + ''' +import json, pathlib, shlex, subprocess, sys +print(json.dumps({"type":"thread.started", "thread_id":"session-fixture-replan"}), flush=True) +prompt = sys.stdin.read() +root = pathlib.Path.cwd() +(root / "received.txt").write_text(prompt) +request = json.loads(prompt.split("Turn request:\\n", 1)[1]) +packet = request["detail_ref"]["replan_action_packet"] +for stage in ("review", "writeback"): + read = subprocess.run(shlex.split(packet[stage]), text=True, capture_output=True, check=True) + (root / (stage + ".json")).write_text(read.stdout) +output = pathlib.Path(sys.argv[sys.argv.index("--output-last-message") + 1]) +output.write_text(json.dumps({"schema_version":"loopx_turn_result_v0", "turn_key":request["turn_key"], + "result_kind":"wait", "completed_phases":["host_execute","typed_result"], + "summary":"Original material readers verified; no task outcome claimed."})) +''') + executable.chmod(0o700) + code, result = _run_cli(registry, runtime, "turn", "run-once", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--turn-instance-id", turn, "--host", "codex-cli", + "--execution-mode", "isolated-headless", "--project", str(project), + "--codex-bin", str(executable), "--timeout-seconds", "20", "--no-global-sync", "--execute", cwd=project) + assert code == 0, json.dumps(result, indent=2) + prompt = (project / "received.txt").read_text() + request = json.loads(prompt.split("Turn request:\n", 1)[1]) + source = request["work_contract"] + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in request and "turn_authority" not in request + identity = result["receipt"]["lineage"] + if prior_todo_binding: + assert identity["todo_id"] == SELECTED_REPLAN_TODO_ID + assert not identity.get("replan_obligation_id") + else: + assert not identity["todo_id"] + assert identity["replan_obligation_id"] == source["autonomous_replan_obligation"]["obligation_id"] + graph = next(row["content"]["graph"] for row in + source["interaction_contract"]["agent_channel"]["work_context"]["sources"] + if row.get("hook_id") == "explore.turn_context") + assert ("result_attachment_template" in graph) is prior_todo_binding + assert ("unavailable for this pure replan" in graph["result_writeback_guidance"]) is not prior_todo_binding + assert "Read measurement_feedback before repeating a measured route" in graph["result_writeback_guidance"] + assert graph["record_finding_template"] and graph["record_node_template"] + context = source["autonomous_replan_obligation"]["replan_context"] + review = json.loads((project / "review.json").read_text()) + writeback = json.loads((project / "writeback.json").read_text()) + assert review["context_id"] == writeback["context_id"] == context["context_id"] + # The native outer controller owns settlement; its model input has no + # inline settlement owner to reference. Preserve all authoring here. + contract = source["replan_action_packet"]["writeback_contract"] + steps = source["interaction_contract"]["cli_channel"].get("settlement_plan", {}).get("ordered_steps", []) + assert not any("vision_authoring" in step for step in steps) + assert "vision_authoring_ref" not in contract + if "vision_authoring" in contract: + assert writeback["writeback_contract"]["vision_authoring"] == { + key: value for key, value in contract["vision_authoring"].items() if key != "minimal_example"} + assert review["source_hash"] == writeback["source_hash"] == request["source_decision_hash"] + assert "Read detail_ref.replan_action_packet.review" in prompt + assert "before reading detail_ref.replan_action_packet.writeback" in prompt + assert "Never re-enter quota admission" in prompt and "Do not execute writeback commands" in prompt + from pathlib import Path + import shlex + import subprocess + captured = Path(request["detail_ref"]["captured_decision"]["path"]) + original = captured.read_bytes() + assert json.loads(original) == source + captured.write_text(json.dumps({**source, "reason": "Changed original facts"})) + rejected = subprocess.run(shlex.split(request["detail_ref"]["replan_action_packet"]["review"]), + capture_output=True, text=True) + assert rejected.returncode != 0 + captured.write_bytes(original) + assert _classification_count(runtime, "quota_slot_spent") == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("prior_todo_binding", [False, True]) +def test_real_structured_host_reads_same_complete_replan_source(tmp_path, monkeypatch, provider, prior_todo_binding): + from tests.control_plane.test_turn_replan_writeback import _fixture, _runner, _run_turn + project, runtime, registry, _state, prior_turn = _fixture(tmp_path, monkeypatch, provider) + runner = _runner(tmp_path, {"result_kind": "wait", "summary": "Original evidence read; no outcome claimed."}) + script = runner.read_text().replace("import json\n", "import json, shlex, subprocess\n") + script = script.replace(" return json.dumps(candidate)", + " candidate.pop('work_evidence', None)\n candidate.pop('validation_evidence', None)\n return json.dumps(candidate)") + script = script.replace(" counter = root / 'host-count.txt'", """ prompt = kwargs["prompt"] + (root / "received.txt").write_text(prompt) + request, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + for stage in ("review", "writeback"): + result = subprocess.run(shlex.split(request["detail_ref"]["replan_action_packet"][stage]), + text=True, capture_output=True, check=True) + (root / (stage + ".json")).write_text(result.stdout) + counter = root / 'host-count.txt'""") + # The runner is a separate Python module; preserve its literal newline escape. + script = script.replace('split("Turn request:\n",', 'split("Turn request:\\n",') + runner.write_text(script) + turn = prior_turn if prior_todo_binding else "structured-complete-replan" + rc, result = _run_turn(project, runtime, registry, runner, turn=turn) + if not prior_todo_binding: + # Structured DSH still has Todo-scoped sessions: an obligation-only + # replan remains blocked rather than inventing a Todo or widening scope. + assert rc == 1 and result["error"] == "LoopX Turn route is not host executable" + assert not (project / "received.txt").exists() + assert _spend_run_count(runtime) == 0 + return + assert rc == 0, json.dumps(result, indent=2) + prompt = (project / "received.txt").read_text() + request, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + source = request["work_contract"] + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in request and "turn_authority" not in request + saved = list(runtime.rglob("decisions/*.json")) + assert len(saved) == 1 and json.loads(saved[0].read_text()) == source + for stage in ("review", "writeback"): + material = json.loads((project / (stage + ".json")).read_text()) + assert material["ok"] is True + assert material["source_hash"] == request["source_decision_hash"] + assert "before reading detail_ref.replan_action_packet.writeback" in prompt + assert "outer Turn executor owns those effects" in prompt + assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_external_worker_validates_complete_source_before_wake(tmp_path, monkeypatch, provider): + import shlex + from tests.test_external_scheduler_worker import _args, _write_executable, worker + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, read_fixture_todos + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = _configure_read_only_todo(project) + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state)), + state_path=state, provider=provider) + rc, contract = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "external-source", + "--runtime-profile", "generic_cli", "--include-detail", "scheduler", + "--available-capability", "shell", "--available-capability", "filesystem_read", cwd=project) + assert rc == 0, contract + assert contract["schema_version"] == "loopx_turn_envelope_v1" + assert contract["interaction_contract"]["agent_channel"]["must_attempt"] is True + original = copy.deepcopy(contract) + before_state = state.read_bytes() + for mutation in ("valid", "version", "missing_version", "missing_binding", "content", + "terminal", "action", "interaction", "reference"): + candidate = copy.deepcopy(contract) + if mutation == "version": + candidate["schema_version"] = "unsupported-future-version" + elif mutation == "missing_version": + candidate.pop("schema_version") + elif mutation == "missing_binding": + candidate.pop("contract_binding") + elif mutation == "content": + candidate["scheduler_hint"]["reason"] = "Changed after capture" + elif mutation == "terminal": + candidate["scheduler_hint"]["unchanged_poll"] = {"local_scheduler": "stop"} + elif mutation == "action": + candidate["effective_action"] = "future_execution" + candidate, _ = _resign_contract(candidate) + elif mutation == "interaction": + candidate["interaction_contract"]["schema_version"] = "future_interaction_version" + candidate, _ = _resign_contract(candidate) + elif mutation == "reference": + candidate.setdefault("agent_lane_next_action", {}).pop("title", None) + candidate["agent_lane_next_action"]["title_ref"] = "$.missing_original_requirement" + candidate, _ = _resign_contract(candidate) + case = tmp_path / mutation + cli, marker = case / "quota", case / "wake-called" + _write_executable(cli, f"#!{sys.executable}\nprint({json.dumps(candidate)!r})\n") + args = _args(fake_cli=cli, state_file=case / "state.json", + wake_cmd=shlex.join([sys.executable, "-c", + f"from pathlib import Path; Path({str(marker)!r}).touch()"]), wake_timeout_seconds=5) + args.goal_id, args.agent_id = GOAL_ID, AGENT_ID + result = worker.run_worker(args) + assert result == (0 if mutation == "valid" else 2), mutation + assert marker.exists() is (mutation == "valid"), mutation + assert contract == original + assert state.read_bytes() == before_state + assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_context_capture_read.py b/tests/control_plane/test_context_capture_read.py index d52ff22097..501de76be7 100644 --- a/tests/control_plane/test_context_capture_read.py +++ b/tests/control_plane/test_context_capture_read.py @@ -9,6 +9,7 @@ from loopx.cli_commands.quota_capture import read_context_capture from test_quota_settlement_cli import _run_cli +from test_goal_acceptance_cli import acceptance_goal as acceptance_goal def _args(path, raw, **changes): @@ -120,3 +121,208 @@ def test_recovery_page_keeps_invocation_executable_and_resets_afterwards(tmp_pat assert shlex.split(page["next_command"])[:3] == [executable, "--cli-bin", executable] plain = read_context_capture(_args(path, raw)) assert shlex.split(plain["next_command"])[:3] == ["loopx", "--format", "json"] + + +def _outcome_capture(tmp_path): + outcome = {"id": "own", "owner_agent_id": "worker", "objective": "Keep every original requirement.", + "revision": 2, "requirements": [{"id": "r1", "description": "Exact 原文🙂 " * 70, + "planning_satisfied": False, "status": "unverified", + "agent_review": {"conclusion": "met", "basis_current": False, + "independently_verified": False, "note": "Historical only", "evidence_refs": ["evidence:original"]}}], + "todo_ids": ["work"], "work": [{"todo_id": "work", "status": "blocked"}], + "sources": [{"ref": "user:original", "summary": "Keep the complete declaration."}], + "future_field": {"permission": False, "unknown": None}} + acceptance = {"outcome_requirements": {"current_lifetime": True, "outcomes": [outcome]}} + source = {"ok": True, "goal_id": "goal", "agent_identity": {"agent_id": "worker"}, + "interaction_contract": {"agent_channel": {"work_context": {"sources": [ + {"source": "goal_acceptance", "content": {"status": "loaded", "goal_acceptance_contract": acceptance}}]}}}, + "planning_horizon": {"core_goal": {"acceptance_contract_ref": + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract"}}} + raw = (json.dumps(source, ensure_ascii=False, indent=2) + "\n").encode() + path = tmp_path / "captured ' ordinary.json" + path.write_bytes(raw) + ref = "sha256:" + hashlib.sha256(json.dumps(outcome, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + return path, raw, outcome, ref + + +def test_outcome_index_and_detail_share_original_capture_without_authority(tmp_path): + path, raw, outcome, ref = _outcome_capture(tmp_path) + digest = hashlib.sha256(raw).hexdigest() + registry, runtime = tmp_path / "absent-registry", tmp_path / "absent-runtime" + base = ["quota", "context-read", "--capture-file", str(path), "--capture-sha256", digest, + "--max-page-output-bytes", "5000", "--page-bytes", "4096", "--outcome-ref"] + code, page = _run_cli(registry, runtime, *base, cwd=tmp_path) + assert code == 0 and page["ok"], page + assert page["outcome_ref"] is None and page["eof"] + index = json.loads(page["content"]) + assert len(index["outcomes"]) == 1 + row = index["outcomes"][0] + assert row["outcome_ref"] == ref and row["owner_agent_id"] == "worker" + assert row["work"] == outcome["work"] + assert row["requirement_states"] == [{"requirement_ids": ["r1"], "planning_satisfied": False, "status": "unverified", + "agent_review": {"conclusion": "met", "basis_current": False, "independently_verified": False}}] + assert "description" not in row["requirement_states"][0] + assert "full-read obligations" in index["instruction"] + args, chunks, offset = [*base, ref, "--page-bytes", "256"], [], 0 + while True: + code, page = _run_cli(registry, runtime, *args, cwd=tmp_path) + assert code == 0 and page["ok"], page + assert page["sha256"] == digest and page["outcome_ref"] == ref and page["offset"] == offset + chunks.append(page["content"].encode()) + offset += len(chunks[-1]) + if page["eof"]: + assert page["total_bytes"] == offset and page["next_offset"] is None + break + command = shlex.split(page["next_command"]) + assert command[command.index("--outcome-ref") + 1] == ref + assert command[command.index("--selection-sha256") + 1] == page["selection_sha256"] + args = command[3:] + detail = json.loads(b"".join(chunks)) + assert detail["outcome"] == outcome and detail["outcome_ref"] == ref + assert path.read_bytes() == raw and not registry.exists() and not runtime.exists() + + +def test_outcome_pages_cannot_count_as_full_capture_retention(tmp_path): + from loopx.control_plane.turn_driver.decision_capture import capture_page_range + path, raw, _, ref = _outcome_capture(tmp_path) + page = read_context_capture(_args(path, raw, outcome_ref=ref)) + assert page["ok"] + assert capture_page_range(page, raw=raw, digest=hashlib.sha256(raw).hexdigest()) is None + # Even a coincidentally identical byte range is a scoped read, never a + # full-context observation a native Host may use to establish continuity. + ordinary = read_context_capture(_args(path, raw)) + assert capture_page_range(ordinary, raw=raw, digest=hashlib.sha256(raw).hexdigest()) is not None + assert capture_page_range({**ordinary, "outcome_ref": None}, raw=raw, + digest=hashlib.sha256(raw).hexdigest()) is None + + +@pytest.mark.parametrize("ref", ["missing", "sha256:" + "0" * 64]) +def test_outcome_unknown_reference_is_not_a_full_capture_fallback(tmp_path, ref): + path, raw, _, _ = _outcome_capture(tmp_path) + result = read_context_capture(_args(path, raw, outcome_ref=ref)) + assert result["ok"] is False and "content" not in result and "next_command" not in result + + +def test_outcome_detail_rechecks_source_before_next_page(tmp_path): + path, raw, _, ref = _outcome_capture(tmp_path) + args = _args(path, raw, outcome_ref=ref) + page = read_context_capture(args) + assert page["ok"] and not page["eof"] + args.offset = page["next_offset"] + args.selection_sha256 = page["selection_sha256"] + path.write_bytes(raw + b" ") + rejected = read_context_capture(args) + assert not rejected["ok"] and "digest mismatch" in rejected["reason"] + + +def test_outcome_transport_failure_keeps_effect_free_exact_full_recovery(tmp_path, monkeypatch): + from loopx.control_plane.effect_runtime import EffectRuntimeStartupError + from loopx.control_plane.quota import turn_envelope + path, raw, _, ref = _outcome_capture(tmp_path) + + def unavailable(*args, **kwargs): + raise EffectRuntimeStartupError("synthetic unavailable transport", diagnostic_code="runtime_unavailable") + + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", unavailable) + result = read_context_capture(_args(path, raw, outcome_ref=ref)) + assert not result["ok"] and "unavailable" in result["reason"] and "content" not in result + recovery = shlex.split(result["recovery_command"]) + assert recovery[recovery.index("--capture-file") + 1] == str(path) + assert recovery[recovery.index("--capture-sha256") + 1] == hashlib.sha256(raw).hexdigest() + assert "--outcome-ref" not in recovery + assert read_context_capture(_args(path, raw))["ok"], "full recovery does not require the failed typed transport" + assert path.read_bytes() == raw + + +def test_outcome_index_pagination_retains_selector_budget_and_pinned_cli(tmp_path): + from loopx.command_invocation import loopx_cli_route + + path, raw, _, _ = _outcome_capture(tmp_path) + args = _args(path, raw, outcome_ref="", format="json", max_page_output_bytes=1600) + with loopx_cli_route(str(tmp_path / "pinned release" / "loopx")): + page = read_context_capture(args) + assert page["ok"] and not page["eof"] and page["outcome_ref"] is None + command = shlex.split(page["next_command"]) + assert command[:3] == [str(tmp_path / "pinned release" / "loopx"), "--cli-bin", + str(tmp_path / "pinned release" / "loopx")] + assert command[-1] == "--outcome-ref" + assert command[command.index("--selection-sha256") + 1] == page["selection_sha256"] + assert command[command.index("--max-page-output-bytes") + 1] == "1600" + chunks = [page["content"].encode()] + while not page["eof"]: + args.offset = page["next_offset"] + args.selection_sha256 = page["selection_sha256"] + page = read_context_capture(args) + assert page["ok"] and page["outcome_ref"] is None + chunks.append(page["content"].encode()) + assert len(json.loads(b"".join(chunks))["outcomes"]) == 1 + + +@pytest.mark.parametrize("change", ["missing_digest", "wrong_digest", "changed_renderer"]) +def test_outcome_cursor_cannot_mix_derived_layouts_under_one_capture_hash(tmp_path, monkeypatch, change): + from loopx.control_plane.quota import turn_envelope + + path, raw, _, ref = _outcome_capture(tmp_path) + args = _args(path, raw, outcome_ref=ref) + page = read_context_capture(args) + assert page["ok"] and not page["eof"] + args.offset = page["next_offset"] + if change != "missing_digest": + args.selection_sha256 = "0" * 64 if change == "wrong_digest" else page["selection_sha256"] + if change == "changed_renderer": + original = turn_envelope._turn_envelope_result + + def next_renderer(*arguments, **kwargs): + return {**original(*arguments, **kwargs), "future_reader_metadata": "layout changed"} + + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", next_renderer) + rejected = read_context_capture(args) + assert not rejected["ok"] and "content" not in rejected and "next_command" not in rejected + assert ("selection-sha256" if change == "missing_digest" else "digest mismatch") in rejected["reason"] + assert path.read_bytes() == raw + + +def test_real_guard_outcomes_remain_bound_after_file_sqlite_authority_changes(acceptance_goal): + from test_goal_outcome_refinement import record, refinement + + project, _, cli, run = acceptance_goal + assert record(project, cli, refinement([{"id": "original", "description": "Keep the original demand"}], + "message:first"), "captured-outcome-first")[0][0] == 0 + directory = project / "capture" + code, decision = run("quota", "should-run", "--goal-id", "goal-acceptance", "--agent-id", "agent-a", + "--turn-instance-id", "outcome-reader-turn", "--decision-output-dir", str(directory)) + assert code == 0 and decision["ok"], decision + path = directory / "decision.json" + raw = path.read_bytes() + digest = hashlib.sha256(raw).hexdigest() + inspect_before = cli("inspect")[1] + original = inspect_before["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0] + + def read(ref): + code, page = run("quota", "context-read", "--capture-file", str(path), "--capture-sha256", digest, + "--page-bytes", "8192", "--max-page-output-bytes", "10000", "--outcome-ref", ref) + assert code == 0 and page["ok"] and page["eof"], page + return json.loads(page["content"]) + + index = read("") + ref = index["outcomes"][0]["outcome_ref"] + assert index["outcomes"][0]["owner_agent_id"] == "agent-a" + assert read(ref)["outcome"] == original + assert cli("inspect")[1] == inspect_before, "reader cannot revise authority or append receipts" + assert record(project, cli, {"outcome_id": "tour", "requirements": [ + {"id": "new", "description": "An actual later demand must not enter the old capture"}], + "source": {"ref": "message:later", "summary": "Later owner refinement"}}, + "captured-outcome-later")[0][0] == 0 + inspect_after = cli("inspect")[1] + assert len(inspect_after["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0]["requirements"]) == 2 + assert read(ref)["outcome"] == original and read("") == index + assert cli("inspect")[1] == inspect_after and path.read_bytes() == raw + + +@pytest.mark.parametrize("command", ["should-run", "spend-slot", "status"]) +def test_outcome_selector_cannot_be_silently_ignored_by_live_commands(tmp_path, command): + registry, runtime = tmp_path / "absent-registry", tmp_path / "absent-runtime" + code, result = _run_cli(registry, runtime, "quota", command, "--outcome-ref", cwd=tmp_path) + assert code == 1 and result["ok"] is False and "only supported" in result["reason"] + assert not registry.exists() and not runtime.exists() diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 3407911f7e..e70877d5c6 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -318,8 +318,12 @@ def release(): assert warm["mode"] == "delta" projection = json.loads((second / "projection.json").read_bytes()) current = prepare_context_delivery(projection, scope=scope, - capture_id=warm["target"]["capture_id"], surface=surface) - assert restore_context_delivery(warm, baseline=cold, current=current)["context"] == projection + capture_id=warm["target"]["capture_id"], surface="complete_turn") + assert projection == json.loads((second / "decision.json").read_bytes()) + assert projection["schema_version"] == "loopx_turn_envelope_v1" + assert current["target"] == warm["target"] + restored = restore_context_delivery(warm, baseline=cold, current=current) + assert restored["mode"] == "delta" and restored["context"] == projection assert len(json.dumps(warm)) < len(json.dumps(current)) assert warm["recovery"]["path"] == str(second / "projection.json") assert warm["target"]["capture_id"] == hashlib.sha256((second / "decision.json").read_bytes()).hexdigest() diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 47a322f4f2..3b8b998e0d 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -273,8 +273,13 @@ def periodic(): ) assert rc == 0, envelope assert envelope["replan_action_packet"]["planning_guidance"] == selected["replan_action_packet"]["planning_guidance"] - assert envelope["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring - assert envelope["writeback"]["settlement_plan"] == plan + # Both CLI spellings now emit the same complete contract. Historical + # adapters still reconstruct the authoring body from that capture. + assert envelope["schema_version"] == selected["schema_version"] == "loopx_turn_envelope_v1" + assert envelope["replan_action_packet"]["writeback_contract"] == contract + assert envelope["interaction_contract"]["cli_channel"]["settlement_plan"] == plan + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + assert build_turn_envelope(envelope)["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring vision_path = tmp_path / "periodic-vision.json" vision = json.loads(json.dumps(authoring["minimal_example"])) vision["vision_patch"] = {"acceptance_summary": "Original acceptance remains open."} diff --git a/tests/control_plane/test_quota_acceptance_references.py b/tests/control_plane/test_quota_acceptance_references.py index 1aa0f17eaf..d5533dc7b7 100644 --- a/tests/control_plane/test_quota_acceptance_references.py +++ b/tests/control_plane/test_quota_acceptance_references.py @@ -1,4 +1,4 @@ -"""Display sharing preserves the complete acceptance and executable decisions.""" +"""One complete acceptance survives composition, display and actual admission.""" from copy import deepcopy import json @@ -8,12 +8,18 @@ _reference_inline_goal_acceptance, compact_quota_should_run_cli_payload, ) from loopx.control_plane.quota.turn_envelope import ( - build_turn_envelope, quota_action_signature_document, + build_turn_envelope, compose_turn_decision, quota_action_signature_document, turn_envelope_action_signature_document, ) from test_replan_host_context_projection import _quota_payload +@pytest.fixture(params=[_reference_inline_goal_acceptance, compose_turn_decision], + ids=["legacy-display", "complete-composition"]) +def share(request): + return request.param + + def acceptance(): return {"revision": 3, "scope": {"kind": "all_advancement"}, "status": "failed", "requirements": [{"id": "original", "text": "Keep the complete original requirement. " * 20}, @@ -38,12 +44,12 @@ def packet(with_context=True): @pytest.mark.parametrize("with_context", [False, True]) -def test_exact_copies_share_one_complete_source_without_changing_signed_action(with_context): +def test_exact_copies_share_one_complete_source_without_changing_signed_action(with_context, share): source = packet(with_context) original = deepcopy(source) - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert source == original - assert _reference_inline_goal_acceptance(shared) == shared + assert share(shared) == shared ref = ("$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract" if with_context else "$.planning_horizon.core_goal.acceptance_contract") assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == ref @@ -64,7 +70,7 @@ def test_exact_copies_share_one_complete_source_without_changing_signed_action(w @pytest.mark.parametrize("change", ["revision", "scope", "failure", "type", "source"]) -def test_different_contracts_are_retained_in_full(change): +def test_different_contracts_are_retained_in_full(change, share): source = packet(False) changed = source["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] if change == "revision": @@ -77,27 +83,27 @@ def test_different_contracts_are_retained_in_full(change): changed["verification"]["passed"] = 0 # False == 0 in Python if change == "source": changed["sources"][0]["ref"] = "message:corrected" - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] == changed assert "acceptance_contract_ref" not in shared["autonomous_replan_obligation"]["replan_context"]["core_goal"] @pytest.mark.parametrize("missing", [None, {}, [], "unavailable"]) -def test_missing_or_malformed_carriers_never_create_dangling_references(missing): +def test_missing_or_malformed_carriers_never_create_dangling_references(missing, share): source = packet() source["interaction_contract"]["agent_channel"]["work_context"] = missing source["planning_horizon"] = missing source["autonomous_replan_obligation"] = missing - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["agent_todo_summary"]["goal_acceptance_contract"] == acceptance() assert "goal_acceptance_contract_ref" not in shared["agent_todo_summary"] -def test_unavailable_context_is_not_used_even_if_it_retains_old_content(): +def test_unavailable_context_is_not_used_even_if_it_retains_old_content(share): source = packet() content = source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"] content["status"] = "unavailable" - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["planning_horizon"]["core_goal"]["acceptance_contract"] == acceptance() assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == "$.planning_horizon.core_goal.acceptance_contract" assert build_turn_envelope(shared)["action"]["planning_horizon"]["core_goal"]["acceptance_contract"] == acceptance() @@ -165,3 +171,100 @@ def test_real_cli_reuses_complete_acceptance_for_selection_and_replan(tmp_path, envelope = build_turn_envelope(selected) assert envelope["work_context"]["sources"][source_index]["content"]["goal_acceptance_contract"] == expected assert envelope["action_signature"]["matches"] is True + + +@pytest.mark.parametrize("mutation", ["dangling", "cycle", "self", "unavailable"]) +def test_complete_readers_reject_dangling_or_cyclic_acceptance_even_with_valid_hash(mutation): + from test_complete_turn_decision import _resign_contract, _canonical_digest + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + from loopx.control_plane.turn_driver.codex_cli import _prompt + contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) + summary = contract["agent_todo_summary"] + if mutation == "dangling": + summary["goal_acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[9].content.goal_acceptance_contract" + elif mutation == "self": + summary["goal_acceptance_contract_ref"] = "$.agent_todo_summary.goal_acceptance_contract" + elif mutation == "cycle": + summary["goal_acceptance_contract_ref"] = "$.planning_horizon.core_goal.acceptance_contract" + contract["planning_horizon"]["core_goal"]["acceptance_contract_ref"] = "$.agent_todo_summary.goal_acceptance_contract" + else: + sources = contract["interaction_contract"]["agent_channel"]["work_context"]["sources"] + sources.append(deepcopy(sources[0])) + sources[1]["content"]["status"] = "unavailable" + summary["goal_acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[1].content.goal_acceptance_contract" + contract, _ = _resign_contract(contract) + for read in ( + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review"), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + ): + with pytest.raises(RuntimeError, match="acceptance reference"): + read() + + +@pytest.mark.parametrize("status", [None, "loaded"]) +def test_historical_reference_to_later_loaded_source_keeps_full_material(status): + from test_complete_turn_decision import _canonical_digest + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + source = packet() + sources = source["interaction_contract"]["agent_channel"]["work_context"]["sources"] + sources.append(deepcopy(sources[0])) + sources[1]["content"]["status"] = status + sources[1]["content"]["goal_acceptance_contract"]["revision"] = 7 + expected = deepcopy(sources[1]["content"]["goal_acceptance_contract"]) + core = source["autonomous_replan_obligation"]["replan_context"]["core_goal"] + del core["acceptance_contract"] + core["acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[1].content.goal_acceptance_contract" + contract = compose_turn_decision(source) + view = _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review") + # The staged review indexes requirements, retaining the complete captured + # requirement source, revision and negative verification for exact readback. + assert view["core_goal"]["acceptance_contract"]["revision"] == 7 + assert contract["interaction_contract"]["agent_channel"]["work_context"]["sources"][1]["content"]["goal_acceptance_contract"] == expected + + +def test_shared_contract_survives_warm_delta_and_missing_baseline_recovery(): + from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery + initial = compose_turn_decision(packet()) + changed = packet() + changed["reason"] = "New feedback retains the original requirements and stops." + current = compose_turn_decision(changed) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(initial, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(current, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(current, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == "delta" + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == current + assert compose_turn_decision(restored) == current + assert restored["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() + + +@pytest.mark.parametrize("value", [{"revision": 1}, {"note": "短"}, {"passed": False}]) +def test_small_identical_contracts_stay_inline_when_a_reference_would_cost_more(value, share): + source = packet(False) + source["planning_horizon"]["core_goal"]["acceptance_contract"] = deepcopy(value) + source["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] = deepcopy(value) + source["agent_todo_summary"]["goal_acceptance_contract"] = deepcopy(value) + shared = share(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract"] == value + assert shared["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] == value + + +@pytest.mark.parametrize("existing_owner", ["planning", "replan"]) +def test_existing_reference_follows_a_relocated_complete_owner(existing_owner, share): + source = packet() + del source["agent_todo_summary"]["goal_acceptance_contract"] + owner = ("$.planning_horizon.core_goal.acceptance_contract" if existing_owner == "planning" + else "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract") + source["agent_todo_summary"]["goal_acceptance_contract_ref"] = owner + shared = share(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == ( + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract") + assert compose_turn_decision(shared)["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() diff --git a/tests/control_plane/test_quota_authoring_references.py b/tests/control_plane/test_quota_authoring_references.py new file mode 100644 index 0000000000..007c4652cf --- /dev/null +++ b/tests/control_plane/test_quota_authoring_references.py @@ -0,0 +1,173 @@ +"""Replan authoring shares complete settlement instructions without losing stops.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_inline_replan_authoring +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + quota_action_signature_document, prepare_context_delivery, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + authoring = {"schema_version": "goal_vision_replan_contract_v0", + "fields": {"vision_patch": {"acceptance_summary": 420}}, + "rules": ["Keep all original requirements and cite source evidence. " * 12, + "A result does not grant publication or spending authority."], + "minimal_example": {"state": "vision_patch_proposed"}, + "provenance": {"ref": "message:original", "revision": 3}, "required": False} + source["replan_action_packet"]["writeback_contract"]["vision_authoring"] = deepcopy(authoring) + steps = [{"kind": "validation"}, {"kind": "durable_writeback"}] + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}, "ordered_steps": steps} + index = next(i for i, step in enumerate(steps) if step["kind"] == "durable_writeback") + steps[index]["vision_authoring"] = authoring + return source, authoring, index + + +def legacy(source): + return {**source, "replan_action_packet": _reference_inline_replan_authoring( + source["replan_action_packet"], payload=source)} + + +@pytest.fixture(params=[legacy, compose_turn_decision], ids=["legacy-display", "complete"]) +def share(request): + return request.param + + +def test_exact_authoring_keeps_single_complete_owner_and_original_settlement(share): + source, authoring, index = packet() + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + writeback = shared["replan_action_packet"]["writeback_contract"] + assert "vision_authoring" not in writeback + assert writeback["vision_authoring_ref"] == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{index}].vision_authoring") + assert shared["interaction_contract"]["cli_channel"]["settlement_plan"] == original["interaction_contract"]["cli_channel"]["settlement_plan"] + if share is legacy: + assert quota_action_signature_document(shared) == quota_action_signature_document(source) + envelope = build_turn_envelope(shared) + assert envelope["action_signature"]["matches"] is True + assert envelope["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring + writer = _turn_envelope_result("replan_view", payload=shared, + source_hash=envelope["action_signature"]["source_decision_hash"], stage="writeback") + expected = {k: v for k, v in authoring.items() if k != "minimal_example"} + assert writer["writeback_contract"]["vision_authoring"] == expected + assert "vision_authoring_ref" not in writer["writeback_contract"] + + +@pytest.mark.parametrize("mutation", ["different-rule", "revision", "type", "wrong-step", "short"]) +def test_distinct_or_unavailable_authoring_is_not_replaced(mutation, share): + source, _, index = packet() + owner = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][index] + if mutation == "different-rule": + owner["vision_authoring"]["rules"].append("Retain an additional stop.") + elif mutation == "revision": + owner["vision_authoring"]["provenance"]["revision"] = 4 + elif mutation == "type": + owner["vision_authoring"]["required"] = 0 + elif mutation == "wrong-step": + owner["kind"] = "validation" + else: + owner["vision_authoring"] = {"required": True} + source["replan_action_packet"]["writeback_contract"]["vision_authoring"] = {"required": True} + original = deepcopy(source["replan_action_packet"]["writeback_contract"]["vision_authoring"]) + shared = share(source) + assert shared["replan_action_packet"]["writeback_contract"]["vision_authoring"] == original + assert "vision_authoring_ref" not in shared["replan_action_packet"]["writeback_contract"] + + +@pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "chain", "both", "malformed"]) +def test_invalid_authoring_reference_is_rejected_even_with_valid_content_hash(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + source, _, index = packet() + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + writeback = contract["replan_action_packet"]["writeback_contract"] + writeback.pop("vision_authoring", None) + reference = f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{index}].vision_authoring" + writeback["vision_authoring_ref"] = reference + owner = contract["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][index] + if mutation == "dangling": + writeback["vision_authoring_ref"] = reference.replace(f"[{index}]", "[999]") + elif mutation == "wrong-step": + owner["kind"] = "validation" + elif mutation == "chain": + owner.pop("vision_authoring") + owner["vision_authoring_ref"] = "$.replan_action_packet.writeback_contract.vision_authoring" + elif mutation == "both": + writeback["vision_authoring"] = {"rules": ["Conflicting authority"]} + else: + writeback["vision_authoring_ref"] = {"path": reference} + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="writeback")): + with pytest.raises(RuntimeError, match="authoring reference"): + read() + + +def test_full_delta_and_lost_baseline_restore_authoring_without_guard_reentry(): + # Use the production delivery/restore owner; independent material is checked + # after reconstruction rather than only comparing signatures. + source, authoring, _ = packet() + before = compose_turn_decision(source) + source["replan_action_packet"]["uncovered_frontier"] = ["A newly observed gap"] + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == "delta" + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert build_turn_envelope(restored)["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_capture_shares_authoring_and_refuses_invalid_recovery_without_effects(tmp_path, monkeypatch, provider): + import json + import subprocess + import sys + from pathlib import Path + from test_turn_replan_writeback import _fixture + from test_quota_settlement_cli import _run_cli, _spend_run_count, GOAL_ID, AGENT_ID, SELECTED_REPLAN_TODO_ID + project, runtime, registry, state, turn = _fixture(tmp_path, monkeypatch, provider) + capture = tmp_path / "captured" + code, source = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", SELECTED_REPLAN_TODO_ID, + "--turn-instance-id", turn, "--decision-output-dir", str(capture), cwd=project) + assert code == 0, source + assert json.loads((capture / "decision.json").read_text()) == source + contract = source["replan_action_packet"]["writeback_contract"] + assert "vision_authoring" not in contract + assert contract["vision_authoring_ref"] == "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[1].vision_authoring" + original = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["vision_authoring"] + envelope = build_turn_envelope(source) + import shlex + read = subprocess.run(shlex.split(envelope["replan_action_packet"]["writeback"]), + capture_output=True, text=True, check=True) + assert json.loads(read.stdout)["writeback_contract"]["vision_authoring"] == { + key: value for key, value in original.items() if key != "minimal_example"} + state_before = state.read_bytes() + invalid = deepcopy(source) + invalid["replan_action_packet"]["writeback_contract"]["vision_authoring_ref"] = "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[99].vision_authoring" + invalid, _ = _resign_contract(invalid) + saved = tmp_path / "invalid.json" + saved.write_text(json.dumps(invalid)) + result = subprocess.run([str(Path(sys.executable).with_name("loopx")), "--format", "json", "quota", "replan-view", + "--decision-file", str(saved), "--source-hash", _canonical_digest(invalid), "--replan-stage", "writeback"], + capture_output=True, text=True) + assert result.returncode != 0 + assert "authoring reference" in result.stdout + result.stderr + assert state.read_bytes() == state_before + assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_quota_cli_projection.py b/tests/control_plane/test_quota_cli_projection.py index 09463544e1..a004d9dbc0 100644 --- a/tests/control_plane/test_quota_cli_projection.py +++ b/tests/control_plane/test_quota_cli_projection.py @@ -19,10 +19,12 @@ @pytest.mark.parametrize("variant", ["text", "title", "different", "short", "incomplete", "unverified", "unmatched", "ambiguous", "partial", "failed", "wrong-id", "wrong-selection", "duplicate", "existing-ref", - "archived", "done", "boolean-id"]) + "archived", "done", "boolean-id", "short-unicode"]) def test_lane_title_reuses_only_exact_complete_current_task_in_same_packet(variant): body = "Preserve every requirement and source. 完整要求🙂\n" * 100 + "Stop before publication." title = body if variant != "short" else "Short label" + if variant == "short-unicode": + title = body = "完整🙂" * 20 todo = {"todo_id": "todo_current", "text": body, "status": "open"} if variant == "title": todo.update(text="[P1] " + body, title=body) @@ -78,9 +80,23 @@ def test_lane_title_reuses_only_exact_complete_current_task_in_same_packet(varia else: assert result == lane + from loopx.control_plane.quota.turn_envelope import compose_turn_decision + from test_replan_host_context_projection import _quota_payload + complete_source = _quota_payload() + complete_source.update(selected_todo=payload["selected_todo"], agent_lane_next_action=lane) + complete_source["interaction_contract"]["agent_channel"]["work_context"] = context + if variant == "existing-ref": + with pytest.raises(RuntimeError, match="Current Task title reference"): + compose_turn_decision(complete_source) + else: + complete = compose_turn_decision(complete_source) + assert complete["agent_lane_next_action"] == result + assert complete["interaction_contract"] == complete_source["interaction_contract"] + @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("captured", [False, True]) +def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_path, monkeypatch, provider, captured): from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _run_cli, _write_fixture @@ -96,8 +112,11 @@ def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_pa build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim"), state_path=state, provider=provider) original_state = state.read_bytes() + capture = tmp_path / "captured" + capture_args = ("--codex-app", "--turn-instance-id", "task-title-read", + "--decision-output-dir", str(capture)) if captured else () code, full = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, - "--agent-id", AGENT_ID, cwd=project) + "--agent-id", AGENT_ID, *capture_args, cwd=project) assert code == 0 and full["should_run"], full context = full["interaction_contract"]["agent_channel"]["work_context"] assert context["complete"] and context["selected_todo_text_verified"] @@ -115,6 +134,27 @@ def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_pa "--todo-id", TODO_ID, cwd=project) assert code == 0 and detail["todo"]["text"] == body and detail["todo"]["title"] == body + if captured: + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from test_complete_turn_decision import _canonical_digest, _resign_contract + from test_quota_settlement_cli import _spend_run_count + saved = json.loads((capture / "decision.json").read_text()) + assert saved == full + historical = build_turn_envelope(saved) + restored_task = next(source["content"]["todo"] for source in historical["work_context"]["sources"] + if source.get("source") == "selected_todo") + assert restored_task["text"] == body + invalid = deepcopy(saved) + invalid["agent_lane_next_action"]["title_ref"] = lane["title_ref"].replace(f"[{index}]", "[999]") + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + rc, refused = _run_cli(registry, runtime, "quota", "replan-view", + "--decision-file", str(invalid_path), "--source-hash", _canonical_digest(invalid), + "--replan-stage", "review", cwd=project) + assert rc != 0 and "Current Task title reference" in json.dumps(refused) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + @pytest.mark.parametrize("mirror", ["shared", "identical", "different", "boolean-number", "missing-root"]) def test_fallback_reference_preserves_complete_same_packet_facts(mirror: str) -> None: @@ -234,8 +274,10 @@ def test_real_cli_explore_reference_resolves_to_standalone_guidance(tmp_path, mo registry.write_text(json.dumps(config)) fixture._initialize_fixture_authority(project, runtime, provider=provider) binding = ("--goal-id", fixture.GOAL_ID, "--agent-id", fixture.AGENT_ID) + capture = tmp_path / "captured" rc, guard = fixture._run_cli(registry, runtime, "quota", "should-run", "--codex-app", - *binding, "--todo-id", fixture.TODO_ID, "--turn-instance-id", "guidance-read", cwd=project) + *binding, "--todo-id", fixture.TODO_ID, "--turn-instance-id", "guidance-read", + "--decision-output-dir", str(capture), cwd=project) assert rc == 0, guard sources = guard["interaction_contract"]["agent_channel"]["work_context"]["sources"] graph = next(source["content"]["graph"] for source in sources if source.get("kind") == "explore_turn_context") @@ -246,6 +288,31 @@ def test_real_cli_explore_reference_resolves_to_standalone_guidance(tmp_path, mo rc, standalone = fixture._run_cli(registry, runtime, "explore", "turn-context", *binding, cwd=project) assert rc == 0, standalone assert owner == standalone["graph"]["result_writeback_guidance"] + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from test_complete_turn_decision import _canonical_digest, _resign_contract + saved = json.loads((capture / "decision.json").read_text()) + assert saved == guard + envelope = build_turn_envelope(saved) + recovered = next(source["content"]["graph"] for source in envelope["work_context"]["sources"] + if source.get("kind") == "explore_turn_context") + assert recovered["result_writeback_guidance"] == owner + assert "result_writeback_guidance_ref" not in recovered + invalid = deepcopy(saved) + invalid_graph = next(source["content"]["graph"] for source in invalid["interaction_contract"][ + "agent_channel"]["work_context"]["sources"] if source.get("kind") == "explore_turn_context") + invalid_graph["result_writeback_guidance_ref"] = ( + "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[999].optional_attachments[0].guidance") + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + state = project / f".codex/goals/{fixture.GOAL_ID}/ACTIVE_GOAL_STATE.md" + before = state.read_bytes() + rc, refused = fixture._run_cli(registry, runtime, "quota", "replan-view", + "--decision-file", str(invalid_path), "--source-hash", _canonical_digest(invalid), + "--replan-stage", "writeback", cwd=project) + assert rc != 0 + assert "Explore guidance reference" in json.dumps(refused) + assert state.read_bytes() == before assert fixture._spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_quota_context_reference_owner.py b/tests/control_plane/test_quota_context_reference_owner.py index 76ef472719..55848733fb 100644 --- a/tests/control_plane/test_quota_context_reference_owner.py +++ b/tests/control_plane/test_quota_context_reference_owner.py @@ -158,9 +158,14 @@ def observed(operation, **params): task_detail, vision_detail = detail in {"task", "both"}, detail in {"vision", "both"} result = compact_quota_should_run_cli_payload(source, include_todo_summary_detail=task_detail, include_vision_detail=vision_detail) - # Agent/vision detail leaves the User summary on its default compact path. - # That formerly local Python work now shares this single typed request. - assert calls == ["reference_context"] + # Acceptance still shares its exact source when both display selectors are + # explicit; it now uses the same request as task/Explore sharing. + assert calls.count("reference_context") == 1 + assert "reference_goal_acceptance" not in calls + assert "reference_current_task_title" not in calls and "reference_explore_guidance" not in calls + # Replan authoring has its own applicable lifecycle; this assertion measures + # the merged acceptance/task/Explore operation, not unrelated projections. + assert calls.count("reference_replan_authoring") == (0 if vision_detail else 1) if task_detail: assert result["agent_lane_next_action"]["title"] == title else: @@ -173,18 +178,6 @@ def observed(operation, **params): assert result["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] -def test_all_explicit_details_need_no_diagnostic_transport(monkeypatch): - from loopx.control_plane.quota.cli_projection import compact_quota_should_run_cli_payload - from loopx.control_plane.quota import turn_envelope - source, _ = packet() - def unavailable(*args, **kwargs): - raise AssertionError("explicit full summaries and guidance need no projection") - monkeypatch.setattr(turn_envelope, "_turn_envelope_result", unavailable) - result = compact_quota_should_run_cli_payload(source, include_todo_summary_detail=True, - include_user_todo_summary_detail=True, include_vision_detail=True) - assert result == source - - def test_no_context_sources_needs_no_transport(monkeypatch): from loopx.control_plane.quota import turn_envelope def unavailable(*args, **kwargs): diff --git a/tests/control_plane/test_quota_decision_capture.py b/tests/control_plane/test_quota_decision_capture.py index 081f91cec4..8838f41568 100644 --- a/tests/control_plane/test_quota_decision_capture.py +++ b/tests/control_plane/test_quota_decision_capture.py @@ -103,8 +103,9 @@ def test_real_cli_fallback_reference_keeps_capture_and_turn_effects(tmp_path, mo fallback = full['blocked_priority_fallback'] assert full['heartbeat_recommendation']['blocked_priority_fallback'] == fallback assert ordinary['blocked_priority_fallback'] == fallback - assert ordinary['heartbeat_recommendation']['blocked_priority_fallback_ref'] == '$.blocked_priority_fallback' - assert 'blocked_priority_fallback' not in ordinary['heartbeat_recommendation'] + assert ordinary == full + assert ordinary['schema_version'] == 'loopx_turn_envelope_v1' + assert ordinary['heartbeat_recommendation']['blocked_priority_fallback'] == fallback assert ordinary['heartbeat_receipt'] == full['heartbeat_receipt'] assert ordinary['interaction_contract']['cli_channel']['settlement_plan'] == full[ 'interaction_contract']['cli_channel']['settlement_plan'] @@ -156,20 +157,23 @@ def test_real_cli_boundary_clauses_reach_shared_host_without_loss(tmp_path, monk full = json.loads(original_bytes) assert full['goal_boundary']['write_scope'] == scopes assert full['goal_boundary']['guards'] == guards + assert envelope == full + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full) for field in ('write_scope', 'guards', 'requires_parent_approval'): assert envelope['boundary'][field] == full['goal_boundary'][field], field - authority = extract_turn_authority({'turn_envelope': envelope}) + authority = extract_turn_authority({'work_contract': full, 'turn_envelope': envelope}) assert authority['write_scope'] == scopes assert authority['boundary']['guards'] == guards assert authority['boundary']['requires_parent_approval'] == ['publish', 'production-action'] assert 'write_scope' not in authority['boundary'] # Existing aliases carry these facts once. prompt = render_prompt(authority) - transported, _ = json.JSONDecoder().raw_decode(prompt.split('Turn authority JSON:\n', 1)[1]) - assert transported == authority + transported, _ = json.JSONDecoder().raw_decode(prompt.split('Turn request:\n', 1)[1]) + assert transported == {'work_contract': full} altered = json.loads(json.dumps(envelope)) altered['boundary']['guards'][-1] = 'Continue without the final restriction.' with pytest.raises(ValueError, match='action signature'): - extract_turn_authority({'turn_envelope': altered}) + extract_turn_authority({'work_contract': full, 'turn_envelope': altered}) before = _heartbeat_receipt_count(runtime, TURN_ID) code, replay = _run_cli(registry, runtime, *args) assert code == 0, replay @@ -209,9 +213,14 @@ def test_real_guard_saved_before_envelope_and_read_without_reexecution(tmp_path, code, envelope = _run_cli(registry, runtime, *args, '--turn-envelope', '--decision-output-dir', str(capture)) assert code == 0, envelope - assert envelope['schema_version'] == 'loopx_turn_envelope_v0' + assert envelope['schema_version'] == 'loopx_turn_envelope_v1' saved = capture / 'decision.json' full = json.loads(saved.read_text()) + assert envelope == full + # Retain historical long-route capture recovery coverage explicitly; + # the current producer no longer selects this view with --turn-envelope. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full, captured_decision_path=str(saved)) app = envelope['scheduler']['app_automation'] assert 'ack_cli_args' not in app ref = app['ack_cli_args_detail_ref'] @@ -262,6 +271,8 @@ def test_real_guard_saved_before_envelope_and_read_without_reexecution(tmp_path, assert json.loads((second / 'decision.json').read_text())['heartbeat_receipt']['status'] == 'replayed' assert json.loads(saved.read_text()) == full + assert fresh == json.loads((second / 'decision.json').read_text()) + fresh = build_turn_envelope(fresh, captured_decision_path=str(second / 'decision.json')) assert shlex.split(fresh['detail_ref']['full_decision']) == ['cat', '--', str(second / 'decision.json')] # Missing old evidence fails locally; it never substitutes a new guard observation. saved.unlink() @@ -278,7 +289,8 @@ def test_full_display_is_unchanged_by_capture_location(): from loopx.cli_commands.quota import _project_quota_cli_payload from tests.test_turn_envelope import _full_decision args = argparse.Namespace(turn_envelope=False, quota_command='should-run') - source = _full_decision() + from loopx.control_plane.quota.turn_envelope import compose_turn_decision + source = compose_turn_decision(_full_decision()) baseline = _project_quota_cli_payload(source, args, frozenset(), None) captured = _project_quota_cli_payload(source, args, frozenset(), None, captured_decision_path='/tmp/observation/decision.json') @@ -589,7 +601,8 @@ def typed_host(observed): 'execution_authority_granted': False, } assert code == 0 and selected['ok'] is True, selected - assert selected['schema_version'] == 'loopx_turn_envelope_v0' + assert selected['schema_version'] == 'loopx_turn_envelope_v1' + assert selected == observed['decision'] assert len(saved) == 1 and json.loads(saved[0].read_text()) == observed['decision'] @@ -632,9 +645,9 @@ def test_native_recovery_failure_keeps_original_receipt_and_capture( def refuse_recovery(payload, **kwargs): saved = kwargs['capture_directory'] / 'decision.json' attempts.append(saved.read_bytes()) - if turn_envelope: - assert payload['schema_version'] == 'loopx_turn_envelope_v0' - assert 'heartbeat_receipt' not in payload + assert payload['schema_version'] == 'loopx_turn_envelope_v1' + assert payload == json.loads(attempts[-1]) + assert payload['heartbeat_receipt']['turn_instance_id'] == TURN_ID raise QuotaDecisionTransportError( 'Current context recovery could not be published or safely invalidated. ' 'Read the saved original decision/receipt; do not rerun admission.' diff --git a/tests/control_plane/test_quota_explore_guidance_references.py b/tests/control_plane/test_quota_explore_guidance_references.py new file mode 100644 index 0000000000..ba44ed26e8 --- /dev/null +++ b/tests/control_plane/test_quota_explore_guidance_references.py @@ -0,0 +1,135 @@ +"""Explore instructions survive sharing, historical views and captured recovery.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_context +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + prepare_context_delivery, quota_action_signature_document, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + guidance = "Keep original requirements, lease ownership, negative evidence and stop conditions. " * 20 + source["interaction_contract"]["agent_channel"]["work_context"] = {"complete": True, "sources": [{ + "kind": "explore_turn_context", "source": "turn_start_capability_hook", + "hook_id": "explore.turn_context", "capability_id": "explore", + "content": {"graph": {"result_writeback_guidance": guidance, + "writeback_results": [{"status": "refuted", "evidence": "fixture:negative"}]}}}]} + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}, "ordered_steps": [ + {"kind": "validation"}, {"kind": "durable_writeback", "optional_attachments": [ + {"capability_id": "explore", "guidance": guidance, "required": False}]}]} + return source, guidance + + +def graph(source): + return source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["graph"] + + +def attachment(source): + return source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0] + + +@pytest.fixture(params=[_reference_context, compose_turn_decision], ids=["legacy-display", "complete"]) +def share(request): + return request.param + + +@pytest.mark.parametrize("captured", [False, True]) +def test_shared_guidance_preserves_material_and_settlement_across_readers(share, captured): + source, guidance = packet() + if captured: + source["replan_read_path"] = "/synthetic/decision.json" + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + assert graph(shared) == {"writeback_results": graph(source)["writeback_results"], + "result_writeback_guidance_ref": "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[1].optional_attachments[0].guidance"} + assert shared["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] + if share is _reference_context and not captured: + assert quota_action_signature_document(shared) == quota_action_signature_document(source) + envelope = build_turn_envelope(shared) + material = envelope["work_context"]["sources"][0]["content"]["graph"] + assert material == graph(source) + assert material["result_writeback_guidance"] == guidance + assert envelope["action_signature"]["matches"] is True + if captured: + projected = _turn_envelope_result("replan_cli_view", payload=shared) + assert graph(projected) == graph(source) + writer = _turn_envelope_result("replan_view", payload=shared, + source_hash=_canonical_digest(shared), stage="writeback") + assert writer["writeback"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0] == attachment(source) + + +@pytest.mark.parametrize("mutation", ["different", "wrong-step", "wrong-capability", "missing-owner", "short", "wrong-source"]) +def test_nonidentical_or_unavailable_guidance_stays_inline(share, mutation): + source, _ = packet() + if mutation == "different": + attachment(source)["guidance"] += " Extra stop." + elif mutation == "wrong-step": + source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(source)["capability_id"] = "another" + elif mutation == "missing-owner": + attachment(source).pop("guidance") + elif mutation == "short": + attachment(source)["guidance"] = graph(source)["result_writeback_guidance"] = "Keep evidence." + else: + source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["kind"] = "other_context" + assert graph(share(source)) == graph(source) + + +@pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "wrong-capability", "chain", "both", "malformed"]) +def test_invalid_guidance_reference_is_refused_despite_valid_integrity(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + source, _ = packet() + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + if mutation == "dangling": + graph(contract)["result_writeback_guidance_ref"] = graph(contract)["result_writeback_guidance_ref"].replace("[1]", "[99]") + elif mutation == "wrong-step": + contract["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(contract)["capability_id"] = "another" + elif mutation == "chain": + attachment(contract)["guidance_ref"] = "$.unknown" + elif mutation == "both": + graph(contract)["result_writeback_guidance"] = "Conflicting instruction." + else: + graph(contract)["result_writeback_guidance_ref"] = {"path": "$.unknown"} + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="writeback")): + with pytest.raises(RuntimeError, match="Explore guidance reference"): + read() + + +@pytest.mark.parametrize("change_guidance, complete", [(False, True), (True, True), (True, False)]) +def test_guidance_restores_after_delta_or_full_fallback(change_guidance, complete): + source, guidance = packet() + source["interaction_contract"]["agent_channel"]["work_context"]["complete"] = complete + before = compose_turn_decision(source) + if change_guidance: + guidance += " New authorization boundary." + graph(source)["result_writeback_guidance"] = attachment(source)["guidance"] = guidance + else: + source["replan_action_packet"]["uncovered_frontier"] = ["A new observed gap"] + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert build_turn_envelope(restored)["work_context"]["sources"][0]["content"]["graph"]["result_writeback_guidance"] == guidance diff --git a/tests/control_plane/test_quota_identity_precondition.py b/tests/control_plane/test_quota_identity_precondition.py index 23f7f8cd6d..7313615f89 100644 --- a/tests/control_plane/test_quota_identity_precondition.py +++ b/tests/control_plane/test_quota_identity_precondition.py @@ -5,7 +5,7 @@ import pytest -from canonical_authority_fixture import initialize_canonical_source_fixture +from canonical_authority_fixture import initialize_canonical_source_fixture, isolate_sqlite_runtime from loopx.cli import main from loopx.control_plane.agents.identity import build_quota_agent_identity from loopx.control_plane.quota.error_codes import ( @@ -38,9 +38,12 @@ def _write_state(project: Path) -> None: ) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("envelope_flag", [False, True]) def test_selected_registry_missing_roster_returns_typed_public_recovery( tmp_path: Path, capsys: pytest.CaptureFixture[str], + monkeypatch, provider: str, envelope_flag: bool, ) -> None: project = tmp_path / "project" global_project = tmp_path / "global-project" @@ -69,9 +72,9 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( registered_agents=[AGENT_ID], ) - initialize_canonical_source_fixture( - runtime_root, GOAL_ID, state_path=project_state_path(project, GOAL_ID), provider="sqlite", - ) + isolate_sqlite_runtime(tmp_path, monkeypatch) + initialize_canonical_source_fixture(runtime_root, GOAL_ID, + state_path=project_state_path(project, GOAL_ID), provider=provider) exit_code = main( [ @@ -83,6 +86,7 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( str(runtime_root), "quota", "should-run", + *(["--turn-envelope"] if envelope_flag else []), "--goal-id", GOAL_ID, "--agent-id", @@ -104,6 +108,8 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( assert "selected registry" in payload["recommended_action"] assert "--agent-id" in payload["recommended_action"] assert "verbose_debug" not in payload + assert "contract_binding" not in payload and "interaction_contract" not in payload + assert "turn_envelope_skipped" not in payload @pytest.mark.parametrize( diff --git a/tests/control_plane/test_quota_rejected_decision_contract.py b/tests/control_plane/test_quota_rejected_decision_contract.py new file mode 100644 index 0000000000..4f2d36fdd4 --- /dev/null +++ b/tests/control_plane/test_quota_rejected_decision_contract.py @@ -0,0 +1,318 @@ +"""Rejected decisions share the complete transport without acquiring authority.""" + +from argparse import Namespace +from copy import deepcopy +import json + +import pytest + +from loopx.cli_commands.quota import _project_quota_cli_payload +from loopx.control_plane.quota.turn_envelope import ( + build_turn_envelope, + compose_turn_decision, +) +from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan +from canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, + read_fixture_todos, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from test_quota_settlement_cli import ( + AGENT_ID, + GOAL_ID, + TODO_ID, + _run_cli, + _write_fixture, + _heartbeat_receipt_count, + _spend_run_count, + _configure_selectable_alternative, +) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) +@pytest.mark.parametrize("rejection", ["missing", "capability", "reentry"]) +def test_rejected_cli_decision_is_complete_and_keeps_original_effects( + tmp_path, monkeypatch, provider, profile, rejection +): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture( + tmp_path, required_capability="network" if rejection == "capability" else None + ) + if rejection == "reentry": + _configure_selectable_alternative(project) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state) + ), + state_path=state, + provider=provider, + ) + before = state.read_bytes() + turn = "rejected-complete-contract" + args = [ + "quota", + "should-run", + "--goal-id", + GOAL_ID, + "--agent-id", + AGENT_ID, + "--runtime-profile", + profile, + "--turn-instance-id", + turn, + ] + original = None + if rejection == "reentry": + rc, original = _run_cli(registry, runtime, *args, cwd=project) + assert rc == 0, original + receipt_count = _heartbeat_receipt_count(runtime, turn) + assert receipt_count == (1 if original else 0) + for flag in ([], ["--turn-envelope"]): + capture = tmp_path / ("flag" if flag else "ordinary") + context = tmp_path / "context" + rc, denied = _run_cli( + registry, + runtime, + *args, + *flag, + "--todo-id", + TODO_ID if rejection == "capability" else "todo_unavailable", + "--decision-output-dir", + str(capture), + *( + ["--context-delivery", "on", "--codex-context-root", str(context)] + if profile == "codex_app_heartbeat" + else [] + ), + cwd=project, + ) + assert rc == 1 and denied["ok"] is False and denied["should_run"] is False, ( + denied + ) + assert denied["error_code"] == "quota_action_selection_rejected" + assert denied["schema_version"] == "loopx_turn_envelope_v1" + assert denied == json.loads((capture / "decision.json").read_text()) + assert "context_delivery" not in denied + assert ( + not context.exists() + ) # Refusal does not advance a host delivery baseline. + assert denied["interaction_contract"]["agent_channel"]["must_attempt"] is False + assert ( + denied["interaction_contract"]["agent_channel"]["delivery_allowed"] is False + ) + cli = denied["interaction_contract"]["cli_channel"] + assert ( + cli["spend_allowed_now"] is False and cli["spend_after_validation"] is False + ) + assert "settlement_plan" not in cli + assert "selected_todo" not in denied + assert ( + "selection_rejection" in denied + or "action_selection_qualification" in denied + ) + binding = denied["contract_binding"]["scheduler_execution_args"] + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + + assert ( + _turn_envelope_result( + "compose_complete", payload=denied, scheduler_execution_args=binding + ) + == denied + ) + envelope = build_turn_envelope(denied) + plan = build_loopx_turn_plan( + envelope, + host="codex-cli", + execution_mode="isolated-headless", + turn_instance_id=turn, + ) + assert plan["route"]["would_invoke_host"] is False + if original: + assert denied["heartbeat_receipt"]["status"] == "replayed" + assert ( + denied["heartbeat_receipt"]["event_id"] + == original["heartbeat_receipt"]["event_id"] + ) + assert denied["heartbeat_receipt"].get("settlement_identity") == original[ + "heartbeat_receipt" + ].get("settlement_identity") + else: + assert denied["heartbeat_receipt"]["status"] == "not_committed" + assert _heartbeat_receipt_count(runtime, turn) == receipt_count + assert _spend_run_count(runtime) == 0 and state.read_bytes() == before + + +@pytest.mark.parametrize("flag", [False, True]) +@pytest.mark.parametrize( + "code", + [ + "QUOTA_VALIDATION_FAILED", + "quota_unexpected_collection_error", + "quota_agent_registry_roster_missing", + ], +) +def test_predecision_failure_remains_diagnostic_without_legacy_view(flag, code): + failure = { + "ok": False, + "mode": "should-run", + "decision": "skip", + "should_run": False, + "error_code": code, + "reason": "Original diagnosis", + "recommended_action": "Original recovery", + "diagnostic_evidence": {"unknown_fact": ["preserve", "ordered"]}, + } + original = deepcopy(failure) + result = _project_quota_cli_payload( + failure, + Namespace(quota_command="should-run", turn_envelope=flag), + frozenset(), + None, + ) + assert result == original and failure == original + assert "contract_binding" not in result and "interaction_contract" not in result + with pytest.raises(RuntimeError, match="interaction_contract"): + compose_turn_decision(result) + + +@pytest.mark.parametrize("flag", [False, True]) +@pytest.mark.parametrize("fault", [None, "action", "interaction"]) +def test_unhealthy_paused_decision_keeps_stop_policy_in_complete_capture( + tmp_path, flag, fault +): + from loopx.cli_commands.quota import _emit_quota_result + from loopx.quota import build_quota_should_run + from test_quota_paused_precedence import ( + _paused_status, + _assert_authoritatively_paused, + ) + from test_quota_paused_precedence import ( + AGENT_ID as PAUSED_AGENT, + GOAL_ID as PAUSED_GOAL, + ) + from loopx.control_plane.scheduler.execution_context import ( + GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, + ) + + status = _paused_status() + status["contract"] = {"error_diagnostics": []} + status["global_registry"] = {"ok": False} + source = build_quota_should_run( + status, + goal_id=PAUSED_GOAL, + agent_id=PAUSED_AGENT, + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, + ) + assert source["ok"] is False and source["status_health_ok"] is False + capture = tmp_path / "capture" + capture.mkdir() + emitted = [] + args = Namespace( + quota_command="should-run", + turn_envelope=flag, + format="json", + agent_id=PAUSED_AGENT, + ) + + def emit(): + return _emit_quota_result( + source, + args, + context=None, + registry_path=tmp_path / "registry.json", + heartbeat_turn_id="paused-unhealthy", + usage_quota_started=0, + capture_directory=capture, + detail_sections=frozenset(), + print_payload=lambda value, *_: emitted.append(value), + ) + + if fault: + if fault == "action": + source["effective_action"] = "future_execution" + else: + source["interaction_contract"]["schema_version"] = "future_interaction" + with pytest.raises( + RuntimeError, match="effective_action|interaction_contract.schema_version" + ): + emit() + assert emitted == [] and not (capture / "decision.json").exists() + return + rc = emit() + assert rc == 1 + result = emitted[0] + _assert_authoritatively_paused(result) + assert result["schema_version"] == "loopx_turn_envelope_v1" + assert result["ok"] is False and result["status_health_ok"] is False + assert result == json.loads((capture / "decision.json").read_text()) + # Complete transport must not discard any health, authority or stop fact. + assert { + k: v + for k, v in result.items() + if k not in {"schema_version", "contract_binding"} + } == source + + +@pytest.mark.parametrize("flag", [False, True]) +def test_new_emission_cannot_fall_back_to_historical_views(monkeypatch, flag): + from loopx.cli_commands import quota + from loopx.control_plane.effect_runtime import EffectRuntimeRejected + from tests.test_turn_envelope import _full_decision + + source = _full_decision() + original = deepcopy(source) + complete = compose_turn_decision(source) + args = Namespace(quota_command="should-run", turn_envelope=flag) + + def forbidden(*args, **kwargs): + pytest.fail("new should-run emission must not call a historical renderer") + + monkeypatch.setattr(quota, "build_turn_envelope", forbidden) + # Source and historical view can only enter through composition/recovery, + # never silently become an alternative new model-facing contract. + historical = build_turn_envelope(source) + for uncomposed in (source, historical): + with pytest.raises(EffectRuntimeRejected, match="requires a composed complete decision"): + _project_quota_cli_payload(uncomposed, args, frozenset(), None) + for detail in (frozenset(), frozenset({"vision", "agent-todos"})): + result = _project_quota_cli_payload(complete, args, detail, None, + captured_decision_path="/synthetic/decision.json") + assert result is complete + assert result["interaction_contract"] == original["interaction_contract"] + assert source == original + + +@pytest.mark.parametrize("flag", [False, True]) +@pytest.mark.parametrize("schema", [None, "loopx_turn_envelope_v1"]) +def test_success_without_contract_is_rejected_before_capture_or_handoff( + tmp_path, monkeypatch, flag, schema +): + from loopx.cli_commands import quota + from loopx.control_plane.effect_runtime import EffectRuntimeRejected + + def forbidden(*args, **kwargs): + pytest.fail("invalid decision must not reach capture, handoff or output") + + monkeypatch.setattr(quota, "capture_decision", forbidden) + monkeypatch.setattr(quota, "handoff_decision", forbidden) + with pytest.raises(EffectRuntimeRejected, match="requires a composed complete decision"): + quota._emit_quota_result( + {"ok": True, "should_run": True, "effective_action": "run", + "schema_version": schema}, + Namespace(quota_command="should-run", turn_envelope=flag, + format="json", agent_id="synthetic-agent"), + context=None, + registry_path=tmp_path / "registry.json", + heartbeat_turn_id="missing-contract", + usage_quota_started=0, + capture_directory=tmp_path / "capture", + detail_sections=frozenset(), + print_payload=forbidden, + ) diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index a8dda173d8..3439b0c40e 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -4166,8 +4166,8 @@ def test_visible_goal_capability_reentry_preserves_turn_through_selection( assert first["interaction_contract"]["cli_channel"]["selection_required"] is True assert first["selected_todo"]["todo_id"] == TODO_ID assert first["interaction_contract"]["agent_channel"]["next_task_action"]["kind"] == "capability_verification" - assert first["interaction_contract"]["cli_channel"]["next_cli_actions"][0] == first["runtime_capability_reentry"]["candidates"][0]["command"] - reentry_command = first["runtime_capability_reentry"]["candidates"][0][ + assert first["interaction_contract"]["cli_channel"]["next_cli_actions"][0] == first["interaction_contract"]["cli_channel"]["runtime_capability_reentry"]["candidates"][0]["command"] + reentry_command = first["interaction_contract"]["cli_channel"]["runtime_capability_reentry"]["candidates"][0][ "command" ] assert f"--turn-instance-id {turn_instance_id}" in reentry_command diff --git a/tests/control_plane/test_quota_task_title_references.py b/tests/control_plane/test_quota_task_title_references.py new file mode 100644 index 0000000000..653942c87e --- /dev/null +++ b/tests/control_plane/test_quota_task_title_references.py @@ -0,0 +1,122 @@ +"""Full current-Task requirements survive composition, reference validation and recovery.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_context +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + prepare_context_delivery, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + body = "Keep the original acceptance, evidence and authorization. 完整要求🙂\n" * 100 + "Stop before publication." + source["selected_todo"] = {"todo_id": "current-task"} + source["agent_lane_next_action"] = {"todo_id": "current-task", "text": "Bounded label…", "title": body} + source["interaction_contract"]["agent_channel"]["work_context"] = { + "complete": True, "selected_todo_text_verified": True, "sources": [ + {"source": "selected_todo", "content": {"ok": True, "matched": True, + "read_complete": True, "todo": {"todo_id": "current-task", "status": "open", "text": body}}}]} + return source + + +def context(source): + return source["interaction_contract"]["agent_channel"]["work_context"] + + +@pytest.mark.parametrize("field", ["text", "title"]) +def test_exact_title_sharing_preserves_entire_task_in_complete_and_historical_readers(field): + source = packet() + body = source["agent_lane_next_action"]["title"] + if field == "title": + context(source)["sources"][0]["content"]["todo"].update(text="[P0] " + body, title=body) + original = deepcopy(source) + contract = compose_turn_decision(source) + expected = {"todo_id": "current-task", "text": "Bounded label…", "title_ref": + f"$.interaction_contract.agent_channel.work_context.sources[0].content.todo.{field}"} + assert contract["agent_lane_next_action"] == expected + assert _reference_context(source)["agent_lane_next_action"] == expected + assert compose_turn_decision(contract) == contract and source == original + assert contract["interaction_contract"] == source["interaction_contract"] + assert contract["selected_todo"] == source["selected_todo"] + envelope = build_turn_envelope(contract) + assert envelope["work_context"]["sources"] == context(source)["sources"] + assert envelope["action_signature"]["matches"] is True + assert "agent_lane_next_action" not in envelope # Historical view retains the full declaration in work context. + + +@pytest.mark.parametrize("mutation", ["dangling", "malformed", "both", "chain", "wrong-source", "duplicate", + "wrong-id", "wrong-selection", "boolean-id", "incomplete", "unverified", "unmatched", "ambiguous", + "partial", "failed", "archived", "done", "nonstring", "empty"]) +def test_invalid_task_reference_is_refused_before_consumption_even_with_valid_integrity(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) + lane = contract["agent_lane_next_action"] + ctx = context(contract) + src = ctx["sources"][0] + content = src["content"] + todo = content["todo"] + if mutation == "dangling": + lane["title_ref"] = lane["title_ref"].replace("[0]", "[99]") + elif mutation == "malformed": + lane["title_ref"] = {"path": lane["title_ref"]} + elif mutation == "both": + lane["title"] = todo["text"] + elif mutation == "chain": + todo["text_ref"] = "$.another" + elif mutation == "wrong-source": + src["source"] = "historical_todo" + elif mutation == "duplicate": + ctx["sources"].append(deepcopy(src)) + elif mutation == "wrong-id": + todo["todo_id"] = "other-task" + elif mutation == "wrong-selection": + contract["selected_todo"]["todo_id"] = "other-task" + elif mutation == "boolean-id": + lane["todo_id"] = todo["todo_id"] = contract["selected_todo"]["todo_id"] = True + elif mutation in {"incomplete", "unverified"}: + ctx["complete" if mutation == "incomplete" else "selected_todo_text_verified"] = False + elif mutation in {"unmatched", "partial", "failed"}: + content[{"unmatched": "matched", "partial": "read_complete", "failed": "ok"}[mutation]] = False + elif mutation == "ambiguous": + content["ambiguous"] = True + elif mutation == "archived": + todo["archive_state"] = "archive" + elif mutation == "done": + todo["status"] = "done" + else: + todo["text"] = "" if mutation == "empty" else True + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review")): + with pytest.raises(RuntimeError, match="Current Task title reference"): + read() + + +@pytest.mark.parametrize("complete", [False, True]) +def test_new_requirements_survive_delta_and_lost_baseline_recovery(complete): + source = packet() + before = compose_turn_decision(source) + body = source["agent_lane_next_action"]["title"] + " New requirement: preserve failed evidence." + source["agent_lane_next_action"]["title"] = body + context(source)["sources"][0]["content"]["todo"]["text"] = body + context(source)["complete"] = complete + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert context(restored)["sources"][0]["content"]["todo"]["text"] == body + assert build_turn_envelope(restored)["work_context"]["sources"] == context(source)["sources"] diff --git a/tests/control_plane/test_quota_vision_audit_references.py b/tests/control_plane/test_quota_vision_audit_references.py new file mode 100644 index 0000000000..d68e6b7ada --- /dev/null +++ b/tests/control_plane/test_quota_vision_audit_references.py @@ -0,0 +1,370 @@ +"""Whole audit sharing preserves evidence, decisions and recovery obligations.""" + +from copy import deepcopy +import json + +import pytest + +from loopx.control_plane.quota.cli_projection import ( + compact_quota_should_run_cli_payload, +) +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, + build_turn_envelope, + compose_turn_decision, + prepare_context_delivery, + restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + audit = { + "schema_version": "vision_continuation_audit_v0", + "required": True, + "decision": "continue", + "agent_id": "worker", + "trigger_count": 1, + "required_before_closeout": [ + "inspect_original_evidence", + "preserve_unmet_requirements", + ], + "vision_gap_judge": { + "done": False, + "decision": "continue", + "agent_judge_instruction": "Keep original requirements, negative evidence and stop conditions. " + * 20, + "done_only_when": ["authoritative_evidence_satisfies_acceptance"], + "continue_when": ["evidence_is_missing_weak_or_stale"], + }, + "acceptance_gaps": [ + {"requirement": "Original acceptance", "evidence": "fixture:negative"} + ], + "optional_future_fact": {"revision": 2, "negative": True}, + } + source["interaction_contract"]["agent_channel"]["work_context"] = { + "complete": True, + "sources": [], + } + source["vision_continuation_audit"] = audit + source["goal_frontier_projection"] = {"vision_continuation_audit": deepcopy(audit)} + for channel in ("agent_channel", "cli_channel"): + source["interaction_contract"][channel]["vision_continuation_audit"] = deepcopy( + audit + ) + return source + + +def nested(source): + return [ + source["goal_frontier_projection"], + source["interaction_contract"]["agent_channel"], + source["interaction_contract"]["cli_channel"], + ] + + +@pytest.fixture( + params=[compact_quota_should_run_cli_payload, compose_turn_decision], + ids=["legacy", "complete"], +) +def share(request): + return request.param + + +def test_exact_audit_is_retained_once_with_complete_judge_and_history(share): + source = packet() + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + audit = shared["vision_continuation_audit"] + assert {k: v for k, v in audit.items() if k != "payload_compaction"} == source[ + "vision_continuation_audit" + ] + for carrier in nested(shared): + assert carrier["vision_continuation_audit"] == { + "schema_version": "vision_continuation_audit_v0", + "required": True, + "decision": "continue", + "vision_gap_judge": { + "done": False, + "decision": "continue", + "projection_ref": "$.vision_continuation_audit.vision_gap_judge", + }, + "projection_ref": "$.vision_continuation_audit", + } + assert ( + json.dumps(shared).count(audit["vision_gap_judge"]["agent_judge_instruction"]) + == 1 + ) + historical = build_turn_envelope(shared) + assert ( + historical["contract_capsule"]["vision_continuation_audit"]["vision_gap_judge"] + == audit["vision_gap_judge"] + ) + assert historical["action_signature"]["matches"] is True + assert "projection_ref" not in json.dumps( + historical["contract_capsule"]["vision_continuation_audit"] + ) + + +@pytest.mark.parametrize( + "difference", + ["requirement", "type", "version", "negative", "unknown", "missing-owner"], +) +def test_different_audit_is_never_replaced_by_root_summary(share, difference): + source = packet() + for carrier in nested(source): + audit = carrier["vision_continuation_audit"] + if difference == "requirement": + audit["required_before_closeout"].append("wait_for_review") + elif difference == "type": + audit["required"] = 1 + elif difference == "version": + audit["schema_version"] = "future_audit_v1" + elif difference == "negative": + audit["acceptance_gaps"][0]["evidence"] = "fixture:other" + elif difference == "unknown": + audit["optional_future_fact"]["revision"] = 3 + if difference == "missing-owner": + source.pop("vision_continuation_audit") + before = [deepcopy(c["vision_continuation_audit"]) for c in nested(source)] + assert [c["vision_continuation_audit"] for c in nested(share(source))] == before + + +@pytest.mark.parametrize( + "mutation", + [ + "missing", + "empty", + "malformed", + "wrong-owner", + "chain", + "judge-chain", + "conflict", + "type-conflict", + "extra-clause", + "judge-only", + ], +) +def test_dangling_or_conflicting_audit_reference_is_rejected_with_valid_hash(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + + contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) + audit = nested(contract)[1]["vision_continuation_audit"] + if mutation == "missing": + contract.pop("vision_continuation_audit") + elif mutation == "empty": + contract["vision_continuation_audit"] = {} + elif mutation == "malformed": + audit["projection_ref"] = {"path": "$.vision_continuation_audit"} + elif mutation == "wrong-owner": + audit["projection_ref"] = "$.another_audit" + elif mutation == "chain": + contract["vision_continuation_audit"]["projection_ref"] = "$.another_audit" + elif mutation == "judge-chain": + contract["vision_continuation_audit"]["vision_gap_judge"]["projection_ref"] = ( + "$.another_judge" + ) + elif mutation == "conflict": + audit["vision_gap_judge"]["done"] = True + elif mutation == "type-conflict": + audit["required"] = 1 + elif mutation == "extra-clause": + audit["required_before_closeout"] = ["skip_review"] + else: + audit.pop("projection_ref") + contract, _ = _resign_contract(contract) + for read in ( + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result( + "replan_view", + payload=contract, + source_hash=_canonical_digest(contract), + stage="review", + ), + ): + with pytest.raises(RuntimeError, match="Vision audit reference"): + read() + + +@pytest.mark.parametrize( + "changed, complete", [(False, True), (True, True), (True, False)] +) +def test_audit_survives_delta_full_and_lost_baseline_recovery(changed, complete): + source = packet() + source["interaction_contract"]["agent_channel"]["work_context"]["complete"] = ( + complete + ) + before = compose_turn_decision(source) + if changed: + source["vision_continuation_audit"]["vision_gap_judge"][ + "agent_judge_instruction" + ] += " Wait for new evidence." + for carrier in nested(source): + carrier["vision_continuation_audit"] = deepcopy( + source["vision_continuation_audit"] + ) + else: + source["reason"] = "A new observation" + after = compose_turn_decision(source) + scope = dict( + goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one" + ) + baseline = prepare_context_delivery( + before, scope=scope, capture_id="one", surface="complete_turn" + ) + full = prepare_context_delivery( + after, scope=scope, capture_id="two", surface="complete_turn" + ) + delta = prepare_context_delivery( + after, + scope=scope, + capture_id="two", + surface="complete_turn", + baseline=baseline, + retained={"complete": True, "target": baseline["target"]}, + ) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)[ + "context" + ] + assert restored == after + assert ( + restored["vision_continuation_audit"] == source["vision_continuation_audit"] + ) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_guard_capture_and_recovery_preserve_vision_before_effects( + tmp_path, monkeypatch, provider +): + from canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, + read_fixture_todos, + ) + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from test_quota_settlement_cli import ( + AGENT_ID, + GOAL_ID, + _run_cli, + _spend_run_count, + _write_fixture, + _configure_autonomous_replan_fixture, + ) + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + _configure_autonomous_replan_fixture(project, runtime, registry) + runs = runtime / "goals" / GOAL_ID / "runs" / "index.jsonl" + prior = { + "generated_at": "2026-01-01T00:03:00+00:00", + "goal_id": GOAL_ID, + "agent_id": AGENT_ID, + "classification": "prior_progress", + "agent_vision": { + "agent_id": AGENT_ID, + "state": "vision_active", + "vision_patch": { + "vision_summary": "Verify the original outcome.", + "acceptance_summary": "Keep evidence for all original requirements.", + }, + "path_delta": {"outcome": "continue", "evidence_refs": ["evidence:prior"]}, + }, + } + runs.write_text(runs.read_text() + json.dumps(prior) + "\n") + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=read_fixture_todos(state), handoff_mode="soft_claim" + ), + state_path=state, + provider=provider, + ) + original_state = state.read_bytes() + capture = tmp_path / "captured" + code, full = _run_cli( + registry, + runtime, + "quota", + "should-run", + "--goal-id", + GOAL_ID, + "--agent-id", + AGENT_ID, + "--codex-app", + "--turn-instance-id", + "vision-read", + "--decision-output-dir", + str(capture), + cwd=project, + ) + assert code == 0 and full["should_run"], full + assert "context_delivery" not in full + saved = json.loads((capture / "decision.json").read_text()) + assert saved == full + audit = full["vision_continuation_audit"] + assert "projection_ref" not in audit + assert ( + full["goal_frontier_projection"]["vision_continuation_audit"]["projection_ref"] + == "$.vision_continuation_audit" + ) + assert ( + build_turn_envelope(saved)["contract_capsule"]["vision_continuation_audit"][ + "vision_gap_judge" + ] + == audit["vision_gap_judge"] + ) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + invalid = deepcopy(saved) + invalid["goal_frontier_projection"]["vision_continuation_audit"][ + "required" + ] = not audit["required"] + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + rc, refused = _run_cli( + registry, + runtime, + "quota", + "replan-view", + "--decision-file", + str(invalid_path), + "--source-hash", + _canonical_digest(invalid), + "--replan-stage", + "review", + cwd=project, + ) + assert rc != 0 and "Vision audit reference" in json.dumps(refused) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + + +def test_short_audit_remains_inline_without_reference_overhead(share): + source = packet() + source["vision_continuation_audit"] = {"required": False} + for carrier in nested(source): + carrier["vision_continuation_audit"] = {"required": False} + shared = share(source) + assert [c["vision_continuation_audit"] for c in nested(shared)] == [ + {"required": False} + ] * 3 + + +@pytest.mark.parametrize("interaction", [None, [], "historical-unavailable"]) +def test_legacy_sharing_does_not_manufacture_absent_interaction(interaction): + source = packet() + source["interaction_contract"] = interaction + shared = compact_quota_should_run_cli_payload(source) + assert shared["interaction_contract"] == interaction diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index 780a14138c..729a496adc 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -14,7 +14,7 @@ from canonical_authority_fixture import isolate_sqlite_runtime from loopx.control_plane.effect_runtime import EffectRuntimeRejected, restart_effect_runtime from loopx.control_plane.quota.turn_envelope import ( - _turn_envelope_result, build_turn_envelope, quota_action_signature_document, + _turn_envelope_result, build_turn_envelope, compose_turn_decision, quota_action_signature_document, turn_envelope_action_signature_document, prepare_context_delivery, restore_context_delivery, ) from test_replan_host_context_projection import _quota_payload @@ -107,8 +107,11 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, entry = call("quota", "should-run", "--codex-app", "--goal-id", goal, "--agent-id", agent, "--turn-instance-id", turn, "--decision-output-dir", str(capture)) source = json.loads((capture / "decision.json").read_bytes()) - original = explore_source(source)["content"]["graph"] + raw_context = call("explore", "turn-context", "--goal-id", goal, "--agent-id", agent) + original = raw_context["graph"] graph = explore_source(entry)["content"]["graph"] + assert entry == source # Applicability is composed before immutable capture. + assert entry["schema_version"] == "loopx_turn_envelope_v1" assert entry["heartbeat_receipt"]["settlement_identity"]["binding_kind"] == "autonomous_replan" assert not entry.get("selected_todo") assert "result_attachment_template" not in graph and "result_writeback_read" not in graph @@ -120,7 +123,7 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, "record_node_template", "record_finding_template", "guidance"): assert graph.get(key) == original.get(key) assert json_bytes(original) - json_bytes(graph) > 2000 - writer_run = subprocess.run(shlex.split(entry["replan_action_packet"]["writeback"]), + writer_run = subprocess.run(shlex.split(build_turn_envelope(entry)["replan_action_packet"]["writeback"]), capture_output=True, text=True, check=True) writer = json.loads(writer_run.stdout) assert all(not row.get("optional_attachments") @@ -137,6 +140,9 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, # identity keep the old payload rather than guessing applicability. for mutation in ("capture", "custom", "identity", "attachment"): changed = copy.deepcopy(source) + changed.pop("schema_version") + changed.pop("contract_binding") + explore_source(changed)["content"]["graph"] = copy.deepcopy(original) expected = explore_source(changed)["content"] if mutation == "capture": changed.pop("replan_read_path") @@ -150,6 +156,30 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, step["optional_attachments"] = [{"capability_id": "explore", "guidance": "Custom authoring"}] projected = _turn_envelope_result("replan_cli_view", payload=changed) assert explore_source(projected)["content"] == expected + # Complete composition uses the same conservative owner before capture, + # without depending on a staged-reader path. Unsupported/custom source + # semantics remain inline, rather than being silently normalized away. + for mutation in ("custom", "template", "identity", "attachment", "bound"): + changed = {key: copy.deepcopy(value) for key, value in source.items() + if key not in {"schema_version", "contract_binding"}} + changed.pop("replan_read_path") + expected = explore_source(changed)["content"] + expected["graph"] = copy.deepcopy(original) + if mutation == "custom": + expected["graph"]["result_writeback_guidance"] += " Preserve a custom obligation." + elif mutation == "template": + expected["graph"]["result_attachment_template"]["input_revision"] = "custom-revision" + elif mutation == "identity": + changed["interaction_contract"]["cli_channel"]["settlement_plan"]["identity"].pop("binding_kind") + elif mutation == "bound": + changed["selected_todo"] = {"todo_id": "todo_synthetic_bound"} + else: + step = next(row for row in changed["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] + if row["kind"] == "durable_writeback") + step["optional_attachments"] = [{"capability_id": "explore", "guidance": "Custom authoring"}] + before = copy.deepcopy(changed) + assert explore_source(compose_turn_decision(changed))["content"] == expected + assert changed == before assert json.loads((capture / "decision.json").read_bytes()) == source finally: restart_effect_runtime(wait_for_exit=True) @@ -185,8 +215,8 @@ def assert_explore_entry_and_writer(source, entry, writer): def test_checkpoint_preflight_delivers_the_real_cli_view_and_retains_full_source(tmp_path, monkeypatch, provider, replan, context_delivery): """Exercise the production preflight without a model or evaluator. - A full-source fixture alone missed the original bug: the model must be sent - the captured reader, while checkpoint selection still sees full context. + The production preflight must deliver the complete immutable decision to + both model transport and checkpoint selection without a second admission. """ from benchmark.runtime import checkpoint @@ -231,8 +261,8 @@ def observe(command, **kwargs): assert str(view_path) in body and str(source_path) not in body assert body.endswith("original heartbeat instructions") if context_delivery: - # The optional full/delta transport must present the staged CLI - # view too. Its full recovery must not restore old inline hints. + # Optional full/delta transport must recover this exact complete + # contract, including facts outside the historical action subset. command = body.split("```sh\n", 1)[1].split("\n```", 1)[0] projected = run(shlex.split(command), cwd=fixture["project"], env=env, capture_output=True, text=True, timeout=60) @@ -240,6 +270,9 @@ def observe(command, **kwargs): presentation = json.loads(projected.stdout) assert presentation["ok"] and presentation["mode"] == "full" assert presentation["recovery"]["path"] == str(view_path) + assert json.loads(Path(presentation["path"]).read_bytes()) == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert view == source # The checkpoint sends one complete source, not a second fact lens. assert source["heartbeat_receipt"]["turn_instance_id"] == TURN assert source["heartbeat_receipt"] == view["heartbeat_receipt"] assert receipt["checkpoint"]["reason"] == "synthetic_no_native_capture" @@ -255,9 +288,12 @@ def observe(command, **kwargs): assert "replan_action_packet" not in view return assert source["autonomous_replan_obligation"]["replan_context"]["context_id"] - assert "replan_context" not in view["autonomous_replan_obligation"] - packet = view["replan_action_packet"] - assert "planning_guidance" not in packet + assert view["autonomous_replan_obligation"]["replan_context"] == source["autonomous_replan_obligation"]["replan_context"] + assert view["replan_action_packet"]["planning_guidance"] == source["replan_action_packet"]["planning_guidance"] + assert view["replan_action_packet"]["writeback_contract"] == source["replan_action_packet"]["writeback_contract"] + # Historical staged consumers still use the original hash-bound reads. + # This does not qualify their discoverability in the complete CLI input. + packet = build_turn_envelope(source)["replan_action_packet"] capture = Path(source["replan_read_path"]) assert capture.read_bytes() == source_path.read_bytes() before = {key: fixture[key].read_bytes() for key in ("registry", "state", "index")} @@ -268,7 +304,33 @@ def observe(command, **kwargs): assert read["obligation_id"] == selector["obligation_id"] assert read["stage"] == stage if stage == "writeback": - assert_explore_entry_and_writer(source, view, read) + # Complete delivery preserves original capability authoring; + # the old writer still supplies the exact applicable attachment. + original = explore_source(source)["content"]["graph"] + assert explore_source(view)["content"]["graph"] == original + plan = read["writeback"].get("settlement_plan", {}) + steps = plan.get("ordered_steps", []) + attachments = [attachment for step in steps + for attachment in step.get("optional_attachments", []) + if attachment.get("capability_id") == "explore"] + if "settlement_plan" in read["writeback"]: + assert len(attachments) == 1 + assert attachments[0]["attachment_template"] == original["result_attachment_template"] + assert attachments[0]["guidance"] == original["result_writeback_guidance"] + # Correct outer bytes cannot rescue unsupported inner version semantics. + malformed = copy.deepcopy(source) + malformed["schema_version"] = "unsupported-future-version" + from hashlib import sha256 + bad_path = tmp_path / "unsupported.json" + bad_path.write_text(json.dumps(malformed)) + digest = "sha256:" + sha256(json.dumps(malformed, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + command = shlex.split(packet["review"]) + command[command.index("--decision-file") + 1] = str(bad_path) + command[command.index("--source-hash") + 1] = digest + refused = run(command, cwd=fixture["project"], env=env, capture_output=True, text=True, timeout=60) + assert refused.returncode != 0 + assert "unsupported complete Turn decision version" in refused.stdout + refused.stderr assert {key: fixture[key].read_bytes() for key in before} == before assert capture.read_bytes() == source_path.read_bytes() assert len(commands) == 1 # Reads did not re-enter checkpoint preflight. @@ -317,20 +379,31 @@ def test_real_guard_capture_read_and_writeback_without_another_guard(tmp_path, m "--goal-id", GOAL, "--agent-id", AGENT, "--turn-instance-id", TURN, "--todo-id", TODO, "--decision-output-dir", str(capture), *(["--turn-envelope"] if envelope else [])) - packet = guard["replan_action_packet"] - if not envelope: - assert "settlement_plan" not in guard["interaction_contract"]["cli_channel"] - assert guard["interaction_contract"]["cli_channel"]["settlement_read_ref"] == "$.replan_action_packet.writeback" + # Exercise the retained historical material-reader codec against the + # actual new capture. Complete input itself needs no deferred body. + packet = build_turn_envelope(guard)["replan_action_packet"] + assert guard["schema_version"] == "loopx_turn_envelope_v1" + assert "settlement_plan" in guard["interaction_contract"]["cli_channel"] + assert "settlement_read_ref" not in guard["interaction_contract"]["cli_channel"] saved = (capture / "decision.json").read_bytes() source = json.loads(saved) + assert guard == source # Unknown old/custom prose, different templates, ambiguous/missing # writers and missing capture must remain fully inline. for mutation in ("custom", "template", "duplicate", "missing", "capture"): changed = copy.deepcopy(source) + changed.pop("schema_version") + changed.pop("contract_binding") graph = explore_source(changed)["content"]["graph"] step = next(row for row in changed["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] if row["kind"] == "durable_writeback") attachment = next(row for row in step["optional_attachments"] if row["capability_id"] == "explore") + # Reconstruct the historical inline fixture before mutating it. + # The complete producer now shares this exact same-capture owner. + assert graph.pop("result_writeback_guidance_ref") == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{changed['interaction_contract']['cli_channel']['settlement_plan']['ordered_steps'].index(step)}]" + f".optional_attachments[{step['optional_attachments'].index(attachment)}].guidance") + graph["result_writeback_guidance"] = attachment["guidance"] if mutation == "custom": graph["result_writeback_guidance"] += " Stop before a new operation." attachment["guidance"] = graph["result_writeback_guidance"] @@ -344,9 +417,8 @@ def test_real_guard_capture_read_and_writeback_without_another_guard(tmp_path, m changed.pop("replan_read_path") projected = _turn_envelope_result("replan_cli_view", payload=changed) assert explore_source(projected)["content"]["graph"] == graph - entry_writeback = guard["writeback"] if envelope else guard["interaction_contract"]["cli_channel"] - assert "settlement_plan" not in entry_writeback and "next_cli_actions" not in entry_writeback - assert entry_writeback["settlement_identity"] == source["heartbeat_receipt"]["settlement_identity"] + entry_writeback = guard["interaction_contract"]["cli_channel"] + assert entry_writeback["settlement_plan"]["identity"] == source["heartbeat_receipt"]["settlement_identity"] original_files = {key: fixture[key].read_bytes() for key in ("registry", "state", "index")} # Executable commands come from the production packet. Deliberately use # an absent registry: the reader must never collect state or re-admit. @@ -364,25 +436,32 @@ def read(stage, expected_code=0): assert result.returncode == expected_code, (result.stdout, result.stderr) return json.loads(result.stdout) review, writer = read("review"), read("writeback") - if not envelope: - assert_explore_entry_and_writer(source, guard, writer) - else: - assert_explore_entry_and_writer(source, { - "interaction_contract": {"agent_channel": {"work_context": guard["work_context"]}}, - "replan_action_packet": packet}, writer) + # Complete input keeps supported authoring inline. Historical staged + # reads remain exact material reads, not a second execution contract. + assert explore_source(guard)["content"] == explore_source(source)["content"] + original_graph = explore_source(source)["content"]["graph"] + writer_step = next(row for row in writer["writeback"]["settlement_plan"]["ordered_steps"] + if row["kind"] == "durable_writeback") + attachment = next(row for row in writer_step["optional_attachments"] + if row["capability_id"] == "explore") + assert "result_writeback_guidance" not in original_graph + assert original_graph["result_writeback_guidance_ref"] == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{writer['writeback']['settlement_plan']['ordered_steps'].index(writer_step)}]" + f".optional_attachments[{writer_step['optional_attachments'].index(attachment)}].guidance") + assert attachment["attachment_template"] == original_graph["result_attachment_template"] assert writer["writeback"]["settlement_plan"] == source["interaction_contract"]["cli_channel"]["settlement_plan"] assert writer["settlement_command"].endswith(" --settle-turn") - # Real CLI/default-off and native readers both deliver the original - # planning facts after admission, without a second body in the entry. + # Both retired flag values deliver complete planning facts. The + # original captured material reads cannot replace them with fresh state. expected_goal = source["autonomous_replan_obligation"]["replan_context"]["core_goal"] assert review["core_goal"] == expected_goal assert review["action_portfolio"] == source.get("action_portfolio") - if not envelope: - assert "core_goal" not in guard and "planning_horizon" not in guard - assert guard.get("action_portfolio") == source.get("action_portfolio") + assert guard.get("core_goal") == source.get("core_goal") + assert guard.get("planning_horizon") == source.get("planning_horizon") + assert guard.get("action_portfolio") == source.get("action_portfolio") assert review["context_id"] == writer["context_id"] assert review["obligation_id"] == source["replan_action_packet"]["obligation_id"] - assert {k: v for k, v in writer["writeback_contract"].items() if k not in {"successor_binding_args", "successor_settlement"}} == without_example(source["replan_action_packet"]["writeback_contract"]) + assert writer["writeback_contract"] == without_example(source["replan_action_packet"]["writeback_contract"]) assert (capture / "decision.json").read_bytes() == saved assert {key: fixture[key].read_bytes() for key in original_files} == original_files assert not reader_fixture["registry"].exists() @@ -458,13 +537,18 @@ def test_populated_history_and_cumulative_acceptance_have_separate_budgets(tmp_p assert expanded["planning_horizon"]["core_goal_ref"] == "$.core_goal" index = expanded["acceptance_index"] assert len(index["outcomes"]) == 24 - # Cumulative acceptance earns its own index allocation; it cannot spend the - # generic review allowance by duplicating entire historical reviews. - assert 0 < json_bytes(expanded) - json_bytes(review) <= 12 * 1024 + # Same-source count-only allocation: 8,190 bytes; current owner/work and + # exact-state requirement groups: 18,979 bytes (route lengths vary). The + # original 12 KiB failure is retained in the RFC. Keep this explicit 20 KiB + # index budget; historical descriptions/review prose still stay in detail, + # and the separate history, horizon and whole-envelope budgets do not grow. + assert 0 < json_bytes(expanded) - json_bytes(review) <= 20 * 1024 originals = original["acceptance_contract"]["outcome_requirements"]["outcomes"] for row, outcome in zip(index["outcomes"], originals, strict=True): read = _turn_envelope_result("replan_view", **params, stage="acceptance", outcome_ref=row["outcome_ref"]) assert read["outcome"] == outcome + assert _turn_envelope_result("captured_outcome", payload=large, + outcome_ref=row["outcome_ref"])["outcome"] == outcome assert row["requirement_count"] == len(outcome["requirements"]) # A different source revision with the same outcome id must remain visible. large["planning_horizon"]["core_goal"]["acceptance_contract"]["outcome_requirements"]["outcomes"][0]["revision"] = 3 diff --git a/tests/control_plane/test_reward_memory_host_isolation.py b/tests/control_plane/test_reward_memory_host_isolation.py index 5d31e72fff..5edb80be2e 100644 --- a/tests/control_plane/test_reward_memory_host_isolation.py +++ b/tests/control_plane/test_reward_memory_host_isolation.py @@ -394,8 +394,8 @@ def test_real_turn_cli_memory_projection_and_settlement(tmp_path: Path, monkeypa runner = tmp_path / "fixture_dsh_runner.py" runner.write_text('''import json, os, pathlib def run_dsh_turn(*, prompt, **kwargs): - authority = json.loads(prompt.split('Turn authority JSON:\\n', 1)[1].split('\\n\\nWhen finished', 1)[0]) - pathlib.Path(os.environ['FIXTURE_HOST_CAPTURE']).write_text(json.dumps({'prompt': prompt, 'authority': authority})) + request, _ = json.JSONDecoder().raw_decode(prompt.split('Turn request:\\n', 1)[1]) + pathlib.Path(os.environ['FIXTURE_HOST_CAPTURE']).write_text(json.dumps({'prompt': prompt, 'request': request})) return json.dumps({'result_kind': 'validated_progress', 'classification': 'fixture_progress', 'next_action': 'Run the next bounded fixture', 'path_delta_mode': 'unchanged', 'vision_unchanged_reason': 'The original fixture remains open.', @@ -435,17 +435,23 @@ def run_dsh_turn(*, prompt, **kwargs): assert ("Set reward_memory_reflection_json" in observed["prompt"]) is ingest assert ("When reward_memory_recall contains guidance" in observed["prompt"]) is (mode == "recall_context") if mode in {"recall_context", "oversized_recall"}: - memory = observed["authority" if host_kind == "dsh" else "request"]["reward_memory_recall"] + memory = observed["request"]["reward_memory_recall"] assert memory["status"] == "applied" if mode == "recall_context": assert "integration branch" in memory["context"]["guidance"][0]["content_summary"] else: assert memory["context"] is None if mode in {"unconfigured", "disabled", "stale", "both_off"}: - if host_kind == "generic-cli": - assert "reward_memory_recall" not in observed["request"] - else: + request = observed["request"] + assert "reward_memory_recall" not in request + assert "reward_memory_reflection_json" not in request.get("capability_result_fields", {}) + assert not any("reward_memory" in instruction + for instruction in request.get("capability_instructions", [])) + if host_kind != "generic-cli" and "work_contract" not in request: assert "reward_memory" not in observed["prompt"] + # A complete captured source retains inactive configuration/diagnostics. + # Those facts must not activate recall instructions or reflection output; + # deleting them merely to pass a substring check would lose source facts. assert result["effects"]["quota_spent"] is True assert result.get("post_settlement", {}).get("external_writes_performed", False) is False if ingest: diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index b8b817bdeb..7c4c7eb030 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -19,7 +19,6 @@ from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source from loopx.control_plane.todos.goal_todo_projection import retained_todo_summary_fields from loopx.control_plane.turn_driver.codex_cli import _prompt -from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt @@ -111,7 +110,7 @@ def guard(*extra, expected_exit=0): assert packet["scoped_user_gate_fallback"]["selected_executable"]["todo_id"] == "todo_inspect" assert channel["work_context"]["complete"] is True envelope = guard("--turn-envelope") - assert envelope["work_context"]["user_todos"] == channel["work_context"]["user_todos"] + assert envelope["interaction_contract"]["agent_channel"]["work_context"]["user_todos"] == channel["work_context"]["user_todos"] assert state.read_text() == state_text # Canonical tasks still exist: the full Goal source must fail closed, # rather than silently substituting a Todo summary for missing intent. @@ -214,15 +213,12 @@ def guard(*extra: str) -> dict: assert "_context_text_sha256" not in full["selected_todo"] assert reads[-1]["content"]["todo"]["text"] == expected assert json.dumps(context).count(expected) == 1 - assert envelope["required_reads"] == channel["required_reads"] + envelope_channel = envelope["interaction_contract"]["agent_channel"] + assert envelope_channel["required_reads"] == channel["required_reads"] assert context["selected_todo_text_verified"] is True - assert envelope["work_context"] == { - key: value - for key, value in context.items() - if key != "selected_todo_text_verified" - } - assert envelope["action"]["selected_todo"]["text_source_verified"] is True - selected = envelope["action"]["selected_todo"] + assert envelope_channel["work_context"] == context + assert envelope_channel["work_context"]["selected_todo_text_verified"] is True + selected = envelope["selected_todo"] assert selected["todo_id"] == "todo_cursor_work" read = reads[-1] tokens = shlex.split(read["command"]) @@ -242,18 +238,35 @@ def read_detail() -> dict: assert not {"todos", "agent_todos", "user_todos"}.intersection(detail) assert json.dumps(detail).count(expected) == 1 assert detail["todo"]["claimed_by"] == "agent-a" - assert envelope["action_signature"]["matches"] is True assert state.read_bytes() == before - plan = build_loopx_turn_plan(envelope, host="codex-cli", execution_mode="isolated-headless", - turn_instance_id="requirements-synthetic-turn") + def native_plan(turn): + # Use the production native entrypoint and its own host binding rather + # than silently rebinding an App heartbeat capture to a Codex executor. + code, plan = run_json_cli_result("--cli-bin", str(cli_bin), "turn", "plan", + "--goal-id", "requirements-goal", "--agent-id", "agent-a", + "--host", "codex-cli", "--execution-mode", "isolated-headless", + "--turn-instance-id", turn, "--include-transaction-detail", + registry_path=registry, runtime_root=runtime) + assert code == 0 and plan["ok"], plan + assert plan["route"]["would_invoke_host"] is True + assert plan["route"]["host_invocation_allowed"] is False + return plan + + plan = native_plan("requirements-synthetic-turn") request = build_loopx_turn_host_request(plan) assert read["command"] in _prompt(request) assert "work_context" in _prompt(request) + model_input = json.loads(_prompt(request).split("Turn request:\n", 1)[1]) + assert model_input["work_contract"] == plan["work_contract"] + assert "turn_envelope" not in model_input and "turn_authority" not in model_input + assert expected in _prompt(request) authority = extract_turn_authority(request) assert authority["selected_todo"]["todo_id"] == "todo_cursor_work" assert authority["required_reads"] == channel["required_reads"] - assert authority["work_context"] == envelope["work_context"] + assert authority["work_context"] == { + key: value for key, value in context.items() if key != "selected_todo_text_verified" + } assert authority["selected_todo"]["text_source_verified"] is True assert read["command"] in render_prompt(authority) @@ -270,8 +283,8 @@ def read_detail() -> dict: fresh_reads = fresh["interaction_contract"]["agent_channel"]["work_context"]["sources"] assert fresh_reads[-1]["content"]["todo"]["text"] == replacement fresh_envelope = guard("--turn-envelope") - fresh_plan = build_loopx_turn_plan(fresh_envelope, host="codex-cli", execution_mode="isolated-headless", - turn_instance_id="requirements-next-turn") + assert fresh_envelope["interaction_contract"]["agent_channel"]["work_context"]["sources"][-1]["content"]["todo"]["text"] == replacement + fresh_plan = native_plan("requirements-next-turn") fresh_prompt = _prompt(build_loopx_turn_host_request(fresh_plan)) assert replacement in fresh_prompt assert "unknown, not unchanged" in fresh_prompt @@ -441,7 +454,7 @@ def test_short_selected_todo_deduplicates_body_but_keeps_canonical_note( "--agent-id", "agent-a", "--turn-envelope", registry_path=registry, runtime_root=runtime) assert code == 0 and turn["ok"], turn - turn_context = turn["work_context"] + turn_context = turn["interaction_contract"]["agent_channel"]["work_context"] assert turn_context["complete"] is True assert turn_context["selected_todo_ref"] == "selected_todo" turn_source = next(source for source in turn_context["sources"] if source["source"] == "selected_todo") diff --git a/tests/control_plane/test_turn_completion_source.py b/tests/control_plane/test_turn_completion_source.py index 69732e7d34..2656e53f52 100644 --- a/tests/control_plane/test_turn_completion_source.py +++ b/tests/control_plane/test_turn_completion_source.py @@ -220,9 +220,25 @@ def recover_old_writer(**kwargs): assert not any(replay["effects"].values()) assert len(results) == 1 assert _turn_journal(runtime) == recovered + _assert_recovered_completion(runtime, registry, journal) assert _todo(prefix)["todo"]["status"] == "done" +def _assert_recovered_completion(runtime: Path, registry: Path, original_journal: dict) -> None: + from loopx.control_plane.quota.settlement import read_heartbeat_settlement + + turn_id = original_journal["turn_key"] # This CLI fixture supplies no explicit Turn id. + readback = read_heartbeat_settlement(runtime, registry_path=registry, + goal_id="loopx-turn-fixture", agent_id="codex-fixture", + todo_id="todo_fixture0001", turn_instance_id=turn_id) + assert readback is not None + assert readback.completion_event is not None and readback.spend_event is not None + recovered = _turn_journal(runtime) + assert recovered["turn_key"] == original_journal["turn_key"] + assert recovered["host_attempt_count"] == 1 + assert recovered["writeback"]["completion"]["todo_id"] == "todo_fixture0001" + + def _cli(prefix: list[str], *args: str) -> tuple[int, dict]: output = io.StringIO() with contextlib.redirect_stdout(output): @@ -515,4 +531,5 @@ def lose_response_after_commit(**kwargs): assert not any(replay["effects"].values()) assert len(completions) == 1 assert _turn_journal(runtime) == recovered + _assert_recovered_completion(runtime, registry, journal) assert _todo(prefix)["todo"]["text"] == "[P0] A later requirement after acceptance." diff --git a/tests/control_plane/test_turn_envelope_capability_facts.py b/tests/control_plane/test_turn_envelope_capability_facts.py index daec89b1e2..576ac760e0 100644 --- a/tests/control_plane/test_turn_envelope_capability_facts.py +++ b/tests/control_plane/test_turn_envelope_capability_facts.py @@ -34,6 +34,10 @@ def test_real_captured_guard_preserves_required_and_missing_capabilities( state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("action_kind=validate ", "action_kind=validate continuation_policy=same_agent_non_delivery ")) + # Initialize only this disposable fixture; ordinary reads must refuse + # unregistered projections instead of restoring canonical work from them. + code, refused = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) + assert code == 1 and refused["error_code"] == "canonical_authority_required" projection = build_todo_runtime_shadow_projection( goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state), ) @@ -52,6 +56,11 @@ def test_real_captured_guard_preserves_required_and_missing_capabilities( "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) assert code == (0 if network_available else 1), envelope full = json.loads((capture / "decision.json").read_text()) + # Both admission and refusal carry the same complete source. Keep the + # historical decoder checks below without choosing a second producer path. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" + envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) gate = full["capability_gate"] assert "network" in gate["required"] assert ("network" in gate["missing"]) is (not network_available) @@ -86,6 +95,10 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("action_kind=validate ", "action_kind=validate continuation_policy=same_agent_non_delivery ")) + # Initialize only this disposable fixture; ordinary reads must refuse + # unregistered projections instead of restoring canonical work from them. + code, refused = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) + assert code == 1 and refused["error_code"] == "canonical_authority_required" initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection( goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state), ), state_path=state, provider=provider) @@ -122,6 +135,11 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( "--decision-output-dir", str(capture), cwd=project) assert code == 0, envelope full = json.loads((capture / "decision.json").read_text()) + assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" + # The real CLI now emits the complete contract. Retain the historical + # decoder and its independent signature/refusal checks below. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) audit = full["vision_continuation_audit"] assert audit["required"] and audit["required_before_closeout"] assert audit["vision_gap_judge"]["done"] is False @@ -181,6 +199,7 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", turn_instance_id="vision-contract-turn") assert plan["route"]["would_invoke_host"] is True + # A replan route permits planning, not ordinary delivery. assert envelope["action"]["delivery_allowed"] is (not replan) request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": plan["transaction"]["turn_key"], "route": "replan_required" if replan else "primary_delivery", "turn_envelope": envelope} diff --git a/tests/control_plane/test_turn_envelope_settlement.py b/tests/control_plane/test_turn_envelope_settlement.py index 904293e690..351b8b44b9 100644 --- a/tests/control_plane/test_turn_envelope_settlement.py +++ b/tests/control_plane/test_turn_envelope_settlement.py @@ -102,7 +102,8 @@ def test_generated_json_commands_settle_original_turn_once(tmp_path, profile_arg assert rc == 0, full rc, envelope = _run_cli(registry, runtime, *args, "--turn-envelope") assert rc == 0, envelope - plan = envelope["writeback"]["settlement_plan"] + assert envelope["schema_version"] == full["schema_version"] == "loopx_turn_envelope_v1" + plan = envelope["interaction_contract"]["cli_channel"]["settlement_plan"] assert plan == full["interaction_contract"]["cli_channel"]["settlement_plan"] assert plan["identity"] == full["heartbeat_receipt"]["settlement_identity"] assert [step["kind"] for step in plan["ordered_steps"]] == [ diff --git a/tests/control_plane/test_turn_host_context_delivery.py b/tests/control_plane/test_turn_host_context_delivery.py index 973d1b8c6f..772e44375a 100644 --- a/tests/control_plane/test_turn_host_context_delivery.py +++ b/tests/control_plane/test_turn_host_context_delivery.py @@ -10,7 +10,9 @@ import pytest from loopx.control_plane.quota.turn_envelope import build_turn_envelope, turn_envelope_action_signature_document +from loopx.control_plane.turn_driver.decision_capture import prepare_turn_decision_capture from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan +from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import _canonical_hash from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_agent_context import POLICY @@ -47,6 +49,9 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk assert code == 0, envelope original = (capture / "decision.json").read_bytes() source = json.loads(original) + assert envelope == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + envelope = build_turn_envelope(source, captured_decision_path=str(capture / "decision.json")) context = source["interaction_contract"].get("agent_context") assert bool(context) is (mode != "off") if not context: @@ -70,53 +75,63 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk ) command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(project), "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] - request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": "sha256:" + "0" * 64, - "route": "primary_delivery", "turn_envelope": envelope} + plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", + turn_instance_id="host-context-turn") + if mode != "legacy_reference": + plan["work_contract"] = source + request = build_loopx_turn_host_request(plan) before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert json.loads(result.stdout)["result_kind"] == "wait" prompt_path = project / "received.txt" prompt = prompt_path.read_text() - authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) - # Capability context can be off or complete inline while the admitted Turn - # still needs its original evidence route and signed required reads. - assert authority["action_signature"] == envelope["action_signature"] - assert authority["required_reads"] == envelope["required_reads"] - detail = authority["detail_ref"] - assert detail == envelope["detail_ref"] - captured = detail["captured_decision"] - assert (captured["goal_id"], captured["agent_id"], captured["turn_instance_id"]) == (GOAL_ID, AGENT_ID, "host-context-turn") - read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout - assert read == original - # Full decision JSON includes numeric values whose canonical wire is owned - # by TS (e.g. Python 1.0 and JSON.stringify's 1 differ). - assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] - if context: - # Independent source oracle after the actual subprocess boundary: two - # agreeing reduced projections cannot prove delivery of the full source. + if mode == "legacy_reference": + # Historical decoder remains explicitly qualified until its reader exits. + authority, _ = json.JSONDecoder().raw_decode(prompt.split("Turn authority JSON:\n", 1)[1]) assert authority["agent_context"] == envelope["agent_context"] - if mode == "legacy_reference": - assert _canonical_hash(json.loads(read)["interaction_contract"]["agent_context"]) == authority["agent_context"]["content_hash"] - else: - assert envelope["agent_context"] == context - assert "detail_ref" not in envelope["agent_context"] - assert envelope["compaction"]["within_budget"] is False + assert authority["action_signature"] == envelope["action_signature"] + assert authority["required_reads"] == envelope["required_reads"] + assert authority["required_reads"] + assert authority["detail_ref"] == envelope["detail_ref"] + detail = authority["detail_ref"] + read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout + assert read == original + assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] + assert _canonical_hash(json.loads(read)["interaction_contract"]["agent_context"]) == authority["agent_context"]["content_hash"] invalid = deepcopy(request) invalid["turn_envelope"]["agent_context"]["scope"]["agent_id"] = "wrong-agent" rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) assert rejected.returncode != 0 and "action signature" in rejected.stderr - assert prompt_path.read_text() == prompt # Refusal precedes the provider seam. + assert prompt_path.read_text() == prompt + invalid = deepcopy(request) + invalid["turn_envelope"]["required_reads"] = [] + rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) + assert rejected.returncode != 0 and "action signature" in rejected.stderr + assert prompt_path.read_text() == prompt else: - assert "agent_context" not in authority - # Even with capability context off, dropping signed reads must be refused - # before the provider seam, without another admission or state mutation. - assert envelope["required_reads"] - invalid = deepcopy(request) - invalid["turn_envelope"]["required_reads"] = [] - rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) - assert rejected.returncode != 0 and "action signature" in rejected.stderr - assert prompt_path.read_text() == prompt + actual, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + # Compare at the real subprocess/provider seam with the independent + # immutable capture, including full acceptance, context and stop facts. + assert actual["work_contract"] == source + assert "turn_envelope" not in actual and "turn_authority" not in actual + delivered_context = actual["work_contract"]["interaction_contract"].get("agent_context") + assert delivered_context == context + if context: + assert "detail_ref" not in delivered_context + for field, value in (("reason", "Changed original facts"), + ("schema_version", "loopx_turn_envelope_v999")): + invalid = deepcopy(request) + invalid["work_contract"][field] = value + rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) + assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr + assert prompt_path.read_text() == prompt # Refusal precedes the provider seam. + assert source["interaction_contract"]["agent_channel"]["required_reads"] + invalid = deepcopy(request) + invalid["work_contract"]["interaction_contract"]["agent_channel"]["required_reads"] = [] + rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) + assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr + assert prompt_path.read_text() == prompt assert (capture / "decision.json").read_bytes() == original assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == before @@ -146,14 +161,20 @@ def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monke "--todo-id", SELECTED_REPLAN_TODO_ID, "--turn-instance-id", turn, "--codex-app", "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) assert code == 0, envelope - assert envelope["action"]["delivery_allowed"] is False - assert envelope["action"]["must_attempt"] is True + original = (capture / "decision.json").read_bytes() + source = json.loads(original) + assert envelope == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + agent_channel = source["interaction_contract"]["agent_channel"] + assert agent_channel["delivery_allowed"] is False + assert agent_channel["must_attempt"] is True + envelope = build_turn_envelope(source) plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", turn_instance_id=turn) + plan = prepare_turn_decision_capture(plan, journal_path=tmp_path / "turn" / "journal.json", + decision=source, existing_journal=False) assert plan["route"]["kind"] == "replan_required" assert plan["route"]["would_invoke_host"] is True - original = (capture / "decision.json").read_bytes() - source = json.loads(original) assert source["heartbeat_receipt"]["settlement_identity"]["todo_id"] == SELECTED_REPLAN_TODO_ID runner = tmp_path / "runner.py" @@ -165,44 +186,40 @@ def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monke ) command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(project), "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] - request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": plan["transaction"]["turn_key"], - "route": "replan_required", "turn_envelope": envelope} + request = build_loopx_turn_host_request({**plan, "work_contract": source}) before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert json.loads(result.stdout)["result_kind"] == "wait" prompt_path = project / "received.txt" prompt = prompt_path.read_text() - authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) - # Compare preserved choices with the complete source and execute the reader - # actually delivered to the provider. No unconditional full-source detour. - packet = authority["replan_action_packet"] - for field in ("obligation_id", "required_outcome", "allowed_terminal"): - assert packet[field] == source["replan_action_packet"][field] + actual, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + assert actual["work_contract"] == source + assert "turn_envelope" not in actual and "turn_authority" not in actual + # Execute the original observation readers actually delivered to the provider. + detail = actual["detail_ref"] + packet = detail["replan_action_packet"] review = subprocess.run(shlex.split(packet["review"]), capture_output=True, text=True, check=True) - assert json.loads(review.stdout)["context_id"] == source["autonomous_replan_obligation"]["replan_context"]["context_id"] - assert authority["action_signature"] == envelope["action_signature"] - detail = authority["detail_ref"] - assert detail == envelope["detail_ref"] + context = source["autonomous_replan_obligation"]["replan_context"] + assert json.loads(review.stdout)["context_id"] == context["context_id"] captured = detail["captured_decision"] assert (captured["goal_id"], captured["agent_id"], captured["turn_instance_id"]) == (GOAL_ID, AGENT_ID, turn) read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout assert read == original - assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] - context = json.loads(read)["autonomous_replan_obligation"]["replan_context"] - assert context["obligation_id"] == authority["replan_action_packet"]["obligation_id"] - goal_read = next(r for r in authority["required_reads"] if r["source"] == "goal_state") + assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == actual["source_decision_hash"] + assert context["obligation_id"] == source["replan_action_packet"]["obligation_id"] + goal_read = next(r for r in agent_channel["required_reads"] if r["source"] == "goal_state") delivered_goal = subprocess.run(shlex.split(goal_read["command"]), capture_output=True, check=True).stdout assert delivered_goal == goal_bytes and stop.encode() in delivered_goal - assert "Read replan_action_packet.review" in prompt - assert "read autonomous_replan_obligation.replan_context" not in prompt + assert "Read detail_ref.replan_action_packet.review" in prompt + assert "before reading detail_ref.replan_action_packet.writeback" in prompt assert "Never re-enter quota admission" in prompt assert "Do not execute writeback commands" in prompt for field in ("obligation_id", "planning_guidance", "uncovered_frontier"): invalid = deepcopy(request) - invalid["turn_envelope"]["replan_action_packet"][field] = "changed after signing" + invalid["work_contract"]["replan_action_packet"][field] = "changed after signing" rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) - assert rejected.returncode != 0 and "action signature" in rejected.stderr + assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr assert prompt_path.read_text() == prompt assert (capture / "decision.json").read_bytes() == original assert state.read_bytes() == goal_bytes diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 804ee99515..0a2cf73bc7 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -27,12 +27,53 @@ def _fixture_module(): return module +@pytest.fixture(params=["file", "sqlite"]) +def complete_host_request(tmp_path, monkeypatch, request): + """Use the real generic host entrypoint and its selected authority store.""" + from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request + + isolate_sqlite_runtime(tmp_path, monkeypatch) + fixture = _fixture_module() + project, runtime, workspace, registry = fixture._write_fixture(tmp_path, turn_count=2) + state = project / ".codex/goals" / fixture.GOAL_ID / "ACTIVE_GOAL_STATE.md" + summary = parse_active_state_todos(state.read_text(), state_path=state, item_limit=None) + initialize_canonical_authority(runtime, fixture.GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=fixture.GOAL_ID, handoff_mode="soft_claim", + todos=summary["agent_todos"]["items"]), state_path=state, provider=request.param) + code, plan = fixture._run_cli(["--registry", str(registry), "--runtime-root", str(runtime), + "--format", "json", "turn", "plan", "--goal-id", fixture.GOAL_ID, + "--agent-id", fixture.AGENT_ID, "--turn-instance-id", "optional-host-source", + "--host", "generic-cli", "--iteration-context", "fresh", + "--execution-mode", "isolated-headless", "--include-transaction-detail"]) + assert code == 0, plan + contract = plan["work_contract"] + assert contract["schema_version"] == "loopx_turn_envelope_v1" + original_state = state.read_bytes() + yield build_loopx_turn_host_request(plan), contract + assert state.read_bytes() == original_state + assert fixture._quota_spend_count(runtime) == 0 # Adapter results are not Core settlement. + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("first_attempt_fails", [False, True]) @pytest.mark.parametrize("legacy_pretty", [False, True]) def test_real_host_reads_original_capture_after_source_change( tmp_path, monkeypatch, provider, first_attempt_fails, legacy_pretty, ): + from loopx.cli_commands import turn_decision + + launch_plans = [] + original_planner = turn_decision.build_loopx_turn_plan + + def observe_launch_plan(envelope, **kwargs): + # Observe the real live reader and planner; neither is replaced by a + # fixture result. Every launch, including retry, uses the same contract. + assert kwargs["work_contract"]["schema_version"] == "loopx_turn_envelope_v1" + planned = original_planner(envelope, **kwargs) + launch_plans.append(planned) + return planned + + monkeypatch.setattr(turn_decision, "build_loopx_turn_plan", observe_launch_plan) isolate_sqlite_runtime(tmp_path, monkeypatch) f = _fixture_module() project, runtime, workspace, registry = f._write_fixture(tmp_path, turn_count=2) @@ -71,13 +112,16 @@ def test_real_host_reads_original_capture_after_source_change( assert not list(runtime.rglob("decisions/*.json")) assert not (workspace / "received.txt").exists() code, result = f._run_cli([*base, "--execute"]) + assert len(launch_plans) == 1 + assert launch_plans[0]["transaction"]["settlement_plan"]["identity"]["turn_instance_id"] == "original-observation" if first_attempt_fails: assert code == 1 and result["status"] == "failed", result assert f._quota_spend_count(runtime) == 0 else: assert code == 0 and result["status"] == "committed", result request = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) - detail = request["turn_envelope"]["detail_ref"] + assert "turn_envelope" not in request + detail = request["detail_ref"] capture = detail["captured_decision"] assert capture["turn_instance_id"] == "original-observation" assert capture["turn_key"] == request["turn_key"] @@ -92,13 +136,14 @@ def test_real_host_reads_original_capture_after_source_change( else: assert original.endswith(b"\n") and b"\n" not in original[:-1] source_a = json.loads(original) + assert request["work_contract"] == source_a assert source_a["goal_id"] == f.GOAL_ID assert source_a["agent_identity"]["agent_id"] == f.AGENT_ID # Direct `turn` bypasses quota CLI emission. Check its actual subprocess # prompt against the same original contracts, including recovery below. for field in ("work_lane_contract", "execution_obligation", "automation_liveness"): assert source_a[field] - assert request["turn_envelope"]["contract_capsule"][field] == source_a[field] + assert request["work_contract"][field] == source_a[field] # Change the real provider through its native CAS CLI, not the projection. prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] code, current = f._run_cli([*prefix, "todo", "list", "--goal-id", f.GOAL_ID, "--todo-id", f.TODO_ID]) @@ -116,9 +161,33 @@ def test_real_host_reads_original_capture_after_source_change( assert code == 0 and "SOURCE_B" in current["todo"]["text"] plan = load_loopx_turn_plan_from_journal(runtime, goal_id=f.GOAL_ID, turn_key=request["turn_key"]) journal_path = turn_journal_path(runtime, goal_id=f.GOAL_ID, turn_key=request["turn_key"]) + # An existing receipt must not let a malformed complete request bypass the + # source gate on recovery. Reuse the real CLI journal and provider state; + # none of these rejected calls may invoke a host or settlement callback. + from copy import deepcopy + from loopx.control_plane.turn_driver.executor import run_loopx_turn_once + + def unexpected_effect(*_args, **_kwargs): + raise AssertionError("rejected recovery invoked an effect") + + original_journal = journal_path.read_bytes() + spent_before = f._quota_spend_count(runtime) + for bad_contract in (None, {**plan["work_contract"], "reason": "changed after capture"}): + malformed = deepcopy(plan) + malformed["work_contract"] = bad_contract + with pytest.raises(RuntimeError): + run_loopx_turn_once(malformed, project=project, runtime_root=runtime, + goal_id=f.GOAL_ID, timeout_seconds=10, execute=True, + host_runner=unexpected_effect, writeback=unexpected_effect, + spend=unexpected_effect, scheduler=unexpected_effect) + assert journal_path.read_bytes() == original_journal + assert f._quota_spend_count(runtime) == spent_before rebound = prepare_turn_decision_capture(plan, journal_path=journal_path, decision=None, existing_journal=True) - assert rebound["turn_envelope"]["detail_ref"] == detail + from loopx.control_plane.turn_driver.host_candidate import complete_host_input + rebound_input = complete_host_input(rebound) + assert rebound_input["detail_ref"] == detail + assert rebound_input["work_contract"] == source_a replay = list(base) index = replay.index("--turn-instance-id") del replay[index:index + 2] @@ -139,7 +208,8 @@ def test_real_host_reads_original_capture_after_source_change( "--retry-failed-turn", "--execute"]) assert code == 0 and recovered["status"] == "committed", json.dumps(recovered) retried = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) - assert retried["turn_envelope"] == request["turn_envelope"] + assert retried["work_contract"] == request["work_contract"] + assert retried["detail_ref"] == detail assert path.read_bytes() == original code, repeated = f._run_cli([*replay, "--resume-turn-key", request["turn_key"], "--execute"]) assert code == 0, repeated @@ -205,3 +275,430 @@ def test_capture_compacts_layout_without_changing_values(tmp_path, caller): assert "保留".encode() in raw assert b"two spaces" in raw and b"\\nnew line\\t" in raw assert len(raw) < len((json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode()) + + +@pytest.mark.parametrize("adaptive", [False, True]) +def test_complete_session_lineage_preserves_saved_host_sessions(complete_host_request, tmp_path, adaptive): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision + from loopx.control_plane.turn_driver.codex_sessions import ( + _lineage, _store_codex_cli_session, load_codex_cli_session, codex_cli_session_binding, + ) + + request, contract = complete_host_request + if adaptive: + source = {key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}} + source["task_orchestration_contract"] = { + "schema_version": "task_orchestration_contract_v2", "mode": "adaptive", + "primary_todo_id": "todo_adaptive_owner", + } + contract = compose_turn_decision(source) + request = {**request, "work_contract": contract, "turn_envelope": build_turn_envelope(contract)} + fixture = _fixture_module() + expected = {"goal_id": fixture.GOAL_ID, "agent_id": fixture.AGENT_ID, + "todo_id": "todo_adaptive_owner" if adaptive else fixture.TODO_ID} + historical = {key: value for key, value in request.items() if key != "work_contract"} + for scope in ("todo", "agent"): + assert _lineage(request, session_scope=scope) == expected + assert _lineage(historical, session_scope=scope) == expected + for host in ("codex-cli", "dsh"): + runtime = tmp_path / host / scope + _store_codex_cli_session(runtime, lineage=expected, session_scope=scope, + host=host, session_id="existing-native-session") + saved = next(runtime.rglob("turn-sessions/*.json")) + before = saved.read_bytes() + restored = load_codex_cli_session(runtime, lineage=_lineage(request, session_scope=scope), + session_scope=scope, host=host) + assert restored["session_id"] == "existing-native-session" + for fields in ({}, {"work_contract": contract}): + binding = codex_cli_session_binding(runtime, request["turn_envelope"], + session_scope=scope, host=host, **fields) + assert binding == {"schema_version": "loopx_turn_session_binding_v0", **expected} + with pytest.raises(RuntimeError): + codex_cli_session_binding(runtime, request["turn_envelope"], + session_scope=scope, host=host, work_contract=None) + assert saved.read_bytes() == before + + +def test_complete_host_rejects_self_consistent_wrong_effect_identity(complete_host_request): + from copy import deepcopy + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document + from loopx.control_plane.turn_driver.codex_sessions import _lineage + from loopx.control_plane.turn_driver.host_candidate import complete_host_input + + original, _ = complete_host_request + for field in ("goal_id", "agent_id", "todo_id"): + request = deepcopy(original) + envelope = request["turn_envelope"] + if field == "todo_id": + envelope["action"]["selected_todo"][field] = "different-work" + else: + envelope[field] = "different-owner" + # Both digests are individually valid. The original complete input must + # still reject a compatibility adapter redirected to another identity. + digest = "sha256:" + sha256(json.dumps(turn_envelope_action_signature_document(envelope), + sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + envelope["action_signature"].update(source_hash=digest, envelope_hash=digest, matches=True) + for consume in (_lineage, complete_host_input): + with pytest.raises(RuntimeError, match="lineage differs"): + consume(request) + + +@pytest.mark.parametrize("from_journal", [False, True]) +def test_complete_result_vision_uses_original_identity(complete_host_request, from_journal): + from copy import deepcopy + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document + from loopx.control_plane.turn_driver.executor import validate_loopx_turn_host_result + from loopx.control_plane.turn_driver.host_candidate import build_result + + request, _ = complete_host_request + vision = {"schema_version": "goal_vision_replan_contract_v0", "state": "active", + "vision_patch": {"acceptance_summary": "Validate the preserved work."}, + "path_delta": {"outcome": "replan", "prior_assumption": "Existing work remains useful.", + "observed_reality": "The artifact is independently checked.", + "changed": ["Use the verified artifact for the next work step."], "evidence_refs": ["fixture:checked-work"]}} + candidate = build_result(request, {"result_kind": "replan_required", + "classification": "validated_fixture_replan", "summary": "Original work remains useful.", + "next_action": "Continue the original work.", "path_delta_mode": "material_replan", + "agent_vision_json": json.dumps(vision), "work_evidence": ["fixture:artifact"], + "validation_evidence": ["fixture:checked-work"]}) + plan = {**request, "transaction": {"turn_key": request["turn_key"]}} + accepted = validate_loopx_turn_host_result(plan, candidate) + assert accepted["ok"], accepted + original_vision = accepted["result"]["agent_vision"] + fixture = _fixture_module() + assert (original_vision["goal_id"], original_vision["agent_id"]) == (fixture.GOAL_ID, fixture.AGENT_ID) + if from_journal: + candidate = accepted["result"] + assert validate_loopx_turn_host_result(plan, candidate, from_journal=from_journal)["ok"] + changed = deepcopy(plan) + envelope = changed["turn_envelope"] + envelope["agent_id"] = "another-agent" + digest = "sha256:" + sha256(json.dumps(turn_envelope_action_signature_document(envelope), + sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + envelope["action_signature"].update(source_hash=digest, envelope_hash=digest, matches=True) + rejected = validate_loopx_turn_host_result(changed, candidate, from_journal=from_journal) + assert not rejected["ok"] + assert any("lineage differs" in error for error in rejected["errors"]) + + +@pytest.mark.parametrize("carrier", ["canonical_detail", "deduplicated_text", "missing_source", "ambiguous_source"]) +def test_complete_completion_source_uses_admitted_contract(complete_host_request, carrier): + from copy import deepcopy + from hashlib import sha256 + from loopx.cli_commands.turn_todo_writeback import captured_todo_text_sha256 + from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision + + _, contract = complete_host_request + source = deepcopy({key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}}) + todo_id = source["selected_todo"]["todo_id"] + text = "Preserve every requirement. 保留全部要求。\n" * 50 + " final whitespace\n" + source["selected_todo"]["text"] = text if carrier == "deduplicated_text" else "Display prefix" + context = {"complete": True, "selected_todo_text_verified": True} + if carrier == "deduplicated_text": + context["selected_todo_ref"] = "selected_todo" + elif carrier != "missing_source": + record = {"source": "selected_todo", "content": {"todo": {"todo_id": todo_id, "text": text}}} + context["sources"] = [record, record] if carrier == "ambiguous_source" else [record] + source["interaction_contract"]["agent_channel"]["work_context"] = context + # This synthetic source replaces the task record, so do not retain the old + # lane title reference to its previous source index. + source["agent_lane_next_action"].pop("title_ref", None) + source["agent_lane_next_action"]["title"] = "Completion source qualification" + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + if carrier in {"missing_source", "ambiguous_source"}: + with pytest.raises(ValueError, match="full task source|ambiguous"): + captured_todo_text_sha256(envelope, todo_id=todo_id, work_contract=contract) + return + assert captured_todo_text_sha256(envelope, todo_id=todo_id, work_contract=contract) == sha256(text.encode()).hexdigest() + assert captured_todo_text_sha256(envelope, todo_id=todo_id) == sha256(text.encode()).hexdigest() + for bad in (None, {**contract, "reason": "changed after capture"}): + with pytest.raises(ValueError): + captured_todo_text_sha256(envelope, todo_id=todo_id, work_contract=bad) + with pytest.raises(ValueError, match="selected Todo"): + captured_todo_text_sha256(envelope, todo_id="foreign_todo", work_contract=contract) + altered = deepcopy(envelope) + altered["action"]["selected_todo"]["text"] = "changed internal effect body" + with pytest.raises(ValueError, match="captured executor source"): + captured_todo_text_sha256(altered, todo_id=todo_id, work_contract=contract) + + +def test_complete_result_evidence_applicability_uses_admitted_profile(complete_host_request): + from copy import deepcopy + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import ( + build_turn_envelope, compose_turn_decision, turn_envelope_action_signature_document, + ) + from loopx.control_plane.turn_driver.codex_cli import codex_cli_result_schema + from loopx.control_plane.turn_driver.host_candidate import build_result + + request, original = complete_host_request + candidate = {"result_kind": "validated_progress", "next_action": "Continue checked work."} + # The complete source and historical projection agree for every applicability + # case. Profile content remains owned by admission, not this result adapter. + for profile in ("omitted", None, {}, "not a profile", {"work_requirements": ["Check the artifact."]}): + source = deepcopy({key: value for key, value in original.items() + if key not in {"schema_version", "contract_binding"}}) + if profile == "omitted": + source.pop("execution_profile", None) + else: + source["execution_profile"] = profile + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + complete = {**request, "work_contract": contract, "turn_envelope": envelope} + historical = {key: value for key, value in complete.items() if key != "work_contract"} + expected = isinstance(profile, dict) and bool(profile) + for consumer_request in (complete, historical): + schema = codex_cli_result_schema(consumer_request) + assert bool(schema.get("allOf")) is expected + result = build_result(consumer_request, candidate) + assert (result["classification"] == "missing_host_execution_evidence") is expected + for field in ("work_evidence", "validation_evidence"): + partial = build_result(consumer_request, {**candidate, field: ["fixture:checked"]}) + assert (partial["classification"] == "missing_host_execution_evidence") is expected + completion = build_result(consumer_request, {"result_kind": "validated_completion"}, allow_completion=True) + assert (completion["classification"] == "missing_host_execution_evidence") is expected + assert build_result(consumer_request, {"result_kind": "wait"})["result_kind"] == "wait" + # Supplied evidence is still validated by the executor; conversion + # must not manufacture evidence or silently sanitize invalid values. + supplied = {**candidate, "work_evidence": None, "validation_evidence": []} + result = build_result(consumer_request, supplied) + assert result["result_kind"] == "validated_progress" + assert result["work_evidence"] is None and result["validation_evidence"] == [] + + # A separately signed effect representation cannot become the profile + # authority of a complete request. Historical-only requests still read it. + altered = deepcopy(envelope) + altered["contract_capsule"]["execution_profile"] = {} if expected else {"work_requirements": ["Legacy"]} + digest = "sha256:" + sha256(json.dumps(turn_envelope_action_signature_document(altered), + ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + altered["action_signature"].update(source_hash=digest, envelope_hash=digest, matches=True) + changed = {**complete, "turn_envelope": altered} + with pytest.raises(RuntimeError, match="projection differs"): + codex_cli_result_schema(changed) + with pytest.raises(RuntimeError, match="projection differs"): + build_result(changed, candidate) + historical_changed = {key: value for key, value in changed.items() if key != "work_contract"} + assert bool(codex_cli_result_schema(historical_changed).get("allOf")) is not expected + assert (build_result(historical_changed, candidate)["classification"] == "missing_host_execution_evidence") is not expected + + for bad_contract in (None, {**original, "reason": "changed after admission"}): + malformed = {**request, "work_contract": bad_contract} + with pytest.raises(RuntimeError): + codex_cli_result_schema(malformed) + with pytest.raises(RuntimeError): + build_result(malformed, candidate) + malformed = deepcopy(request) + malformed["turn_envelope"]["contract_capsule"]["execution_profile"] = {"changed": True} + with pytest.raises(RuntimeError): + codex_cli_result_schema(malformed) + with pytest.raises(RuntimeError): + build_result(malformed, candidate) + + +def test_complete_settlement_fallback_keeps_original_identity(complete_host_request): + from copy import deepcopy + from loopx.control_plane.turn_driver.executor import _ensure_turn_settlement_plan + + request, contract = complete_host_request + plan = {**request, "host": {"kind": "generic-cli", "execution_mode": "isolated-headless"}} + transaction = {"turn_instance_id": "original-settlement", "turn_key": request["turn_key"]} + lineage = _ensure_turn_settlement_plan(plan, transaction) + identity = transaction["settlement_plan"]["identity"] + assert (identity["goal_id"], identity["agent_id"], identity["todo_id"]) == ( + contract["goal_id"], contract["agent_identity"]["agent_id"], contract["selected_todo"]["todo_id"]) + assert identity["turn_instance_id"] == "original-settlement" + before = deepcopy(transaction) + assert _ensure_turn_settlement_plan(plan, transaction) == lineage + assert transaction == before # A recorded settlement is never rebuilt. + historical = {key: value for key, value in plan.items() if key != "work_contract"} + legacy_transaction = {"turn_instance_id": "original-settlement", "turn_key": request["turn_key"]} + _ensure_turn_settlement_plan(historical, legacy_transaction) + assert legacy_transaction == transaction + for existing in (False, True): + for bad_contract in (None, {**contract, "reason": "changed source"}): + target = deepcopy(transaction) if existing else {"turn_instance_id": "original-settlement"} + original = deepcopy(target) + with pytest.raises(RuntimeError): + _ensure_turn_settlement_plan({**plan, "work_contract": bad_contract}, target) + assert target == original + + +def test_codex_prompt_preserves_complete_source_and_historical_version_gate(complete_host_request): + from copy import deepcopy + from loopx.control_plane.turn_driver.codex_cli import _prompt + from loopx.control_plane.turn_driver.host_candidate import complete_host_input + + request, contract = complete_host_request + prompt = _prompt(request) + payload = json.loads(prompt.split("Turn request:\n", 1)[1]) + assert payload == complete_host_input(request) + assert payload["work_contract"] == contract and "turn_envelope" not in payload + historical = {key: value for key, value in request.items() if key != "work_contract"} + assert "TurnEnvelope as the source of truth" in _prompt(historical) + for source in (request, historical): + broken = deepcopy(source) + broken["turn_envelope"]["schema_version"] = "unsupported_fixture_version" + with pytest.raises(RuntimeError): + _prompt(broken) + for invalid in (None, {**contract, "reason": "changed source"}, + {**contract, "schema_version": "unsupported_fixture_version"}): + with pytest.raises(RuntimeError): + _prompt({**request, "work_contract": invalid}) + broken = deepcopy(request) + broken["turn_envelope"]["agent_id"] = "another-agent" + with pytest.raises(RuntimeError): + _prompt(broken) + broken["work_contract"] = None + broken["turn_envelope"]["schema_version"] = "unsupported_fixture_version" + with pytest.raises(RuntimeError, match="unsupported TurnEnvelope schema_version"): + _prompt(broken) + + +def test_complete_planning_and_failed_recovery_share_lineage(complete_host_request): + from copy import deepcopy + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision + from loopx.control_plane.turn_driver.driver import ( + FailedTurnSessionRecoveryError, _turn_lineage, build_loopx_turn_plan, reconcile_failed_turn_session_request, + ) + + request, original = complete_host_request + source = {k: v for k, v in original.items() if k not in {"schema_version", "contract_binding"}} + # Hash parity protects existing journals, including numeric encodings and + # Unicode in retained source facts; a new hash silently forks the Turn. + source["execution_profile"] = {"probe": [1e-7, 1e-6, 1e20, -0.0, "原文", "\U00010000", "\ue000"]} + for adaptive in (False, True): + if adaptive: + source["task_orchestration_contract"] = { + "schema_version": "task_orchestration_contract_v2", "mode": "adaptive", + "primary_todo_id": "todo_adaptive_owner", + } + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + kwargs = dict(host="codex-cli", execution_mode="isolated-headless", turn_instance_id="planning-parity") + historical = build_loopx_turn_plan(envelope, **kwargs) + complete = build_loopx_turn_plan(envelope, work_contract=contract, **kwargs) + assert complete.pop("work_contract") == contract + assert complete == historical + expected_todo = "todo_adaptive_owner" if adaptive else original["selected_todo"]["todo_id"] + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document + signature = turn_envelope_action_signature_document(envelope) + signature["action"]["selected_todo"] = complete["route"]["selected_todo"] + expected_hash = "sha256:" + sha256(json.dumps(signature, ensure_ascii=False, + sort_keys=True, separators=(",", ":")).encode()).hexdigest() + lineage = _turn_lineage(envelope, selected_todo=complete["route"]["selected_todo"], work_contract=contract) + assert lineage["action_hash"] == expected_hash + assert lineage["todo_id"] == expected_todo + binding = {"schema_version": "loopx_turn_session_binding_v0", "host_session_id": "fixture-session", + "goal_id": original["goal_id"], "agent_id": original["agent_identity"]["agent_id"], + "todo_id": expected_todo} + recovery = {**request, "turn_envelope": envelope, "work_contract": contract, + "session": complete["session"]} + recovered = reconcile_failed_turn_session_request(recovery, session_binding=binding) + assert recovered["session"]["action"] == "resume" + assert recovered["session"]["binding_status"] == "failed_turn_recovery" + assert recovered["work_contract"] == contract and recovered["turn_envelope"] == envelope + assert reconcile_failed_turn_session_request(recovered, session_binding=binding) == recovered + for bad in (None, {**contract, "reason": "changed source"}, + {**contract, "schema_version": "unsupported_fixture_version"}): + with pytest.raises(RuntimeError): + build_loopx_turn_plan(envelope, work_contract=bad, **kwargs) + for previous in (recovery, recovered): + with pytest.raises(FailedTurnSessionRecoveryError, match="complete Turn"): + reconcile_failed_turn_session_request({**previous, "work_contract": bad}, session_binding=binding) + altered = deepcopy(envelope) + altered["agent_id"] = "wrong-agent" + with pytest.raises(RuntimeError): + build_loopx_turn_plan(altered, work_contract=contract, **kwargs) + assert envelope["agent_id"] == original["agent_identity"]["agent_id"] + + +def test_complete_planning_keeps_receipt_bound_todoless_replan(complete_host_request): + from loopx.control_plane.quota.settlement import build_turn_scoped_cli_settlement_plan + from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision + from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan, reconcile_failed_turn_session_request + + _, original = complete_host_request + goal, agent, obligation = original["goal_id"], original["agent_identity"]["agent_id"], "replan-" + "a" * 16 + source = {"ok": True, "goal_id": goal, "agent_identity": {"agent_id": agent}, "decision": "run", + "interaction_contract": {"schema_version": "loopx_interaction_contract_v0", "mode": "autonomous_replan", + "user_channel": {"action_required": False, "notify": "DONT_NOTIFY"}, + "agent_channel": {}, "cli_channel": {"spend_after_validation": True}}} + settlement = build_turn_scoped_cli_settlement_plan(goal_id=goal, agent_id=agent, + replan_obligation_id=obligation, turn_instance_id="fixture.replan", command_prefix="loopx", + scoped_cli_args="", lifecycle_actor_args="").as_dict() + source.update(selected_todo=None, should_run=True, effective_action="autonomous_replan_required", + replan_action_packet={"schema_version": "replan_action_packet_v0", "decision": "replan_required", + "obligation_id": obligation}) + source.pop("task_orchestration_contract", None) + interaction = source["interaction_contract"] + interaction["mode"] = "autonomous_replan" + interaction["agent_channel"].update(must_attempt=True, delivery_allowed=False, quiet_noop_allowed=False) + interaction["cli_channel"]["settlement_plan"] = settlement + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + kwargs = dict(host="codex-cli", execution_mode="isolated-headless", session_scope="agent", + turn_instance_id="fixture.replan") + historical = build_loopx_turn_plan(envelope, **kwargs) + plan = build_loopx_turn_plan(envelope, work_contract=contract, **kwargs) + assert {k: v for k, v in plan.items() if k != "work_contract"} == historical + assert plan["route"]["kind"] == "replan_required" and plan["route"]["selected_todo"] is None + assert plan["transaction"]["settlement_plan"]["identity"] == settlement["identity"] + binding = {"schema_version": "loopx_turn_session_binding_v0", "goal_id": goal, "agent_id": agent, + "host_session_id": "fixture-session"} + assert reconcile_failed_turn_session_request(plan, session_binding=binding)["session"]["action"] == "resume" + wrong_turn = build_loopx_turn_plan(envelope, work_contract=contract, **{**kwargs, "turn_instance_id": "other"}) + assert wrong_turn["route"]["would_invoke_host"] is False + assert not any(wrong_turn["effects"].values()) + + +def test_complete_plan_rejects_rehashed_adapter_decision_drift(complete_host_request, tmp_path): + from copy import deepcopy + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document + from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan + from loopx.control_plane.turn_driver.executor import run_loopx_turn_once + + request, contract = complete_host_request + envelope = request['turn_envelope'] + options = dict(host='generic-cli', execution_mode='isolated-headless', + turn_instance_id='adapter-drift') + plan = build_loopx_turn_plan(envelope, work_contract=contract, **options) + assert plan['route']['would_invoke_host'] is True + + def unexpected_effect(*_args, **_kwargs): + raise AssertionError('divergent adapter invoked an effect') + + # Each payload retains its Goal/Agent/Todo and correctly recomputes its own + # integrity hash. The independent invariant is derivation from the admitted + # decision, not self-consistency of a second decision carrier. + for field, replacement in ( + ('should_run', False), ('effective_action', 'wait'), + ('action', {**envelope['action'], 'delivery_allowed': False}), + ('user', {**envelope['user'], 'action_required': True}), + ('execution_policy', {**envelope['execution_policy'], 'normal_delivery_allowed': False}), + ('writeback', {**envelope['writeback'], 'spend_allowed_now': True}), + ): + changed = deepcopy(envelope) + changed[field] = replacement + assert changed != envelope + digest = 'sha256:' + sha256(json.dumps(turn_envelope_action_signature_document(changed), + sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode()).hexdigest() + changed['action_signature'].update(source_hash=digest, envelope_hash=digest, matches=True) + with pytest.raises(RuntimeError, match='projection differs'): + build_loopx_turn_plan(changed, work_contract=contract, **options) + malformed = {**plan, 'turn_envelope': changed} + with pytest.raises(RuntimeError, match='projection differs'): + run_loopx_turn_once(malformed, project=tmp_path, runtime_root=tmp_path / 'rejected', + goal_id=contract['goal_id'], timeout_seconds=10, execute=True, + host_runner=unexpected_effect, writeback=unexpected_effect, + spend=unexpected_effect, scheduler=unexpected_effect) + # The existing lane lock may be allocated before the checked execution + # entry. No Turn journal or settlement receipt may be produced. + assert not list((tmp_path / 'rejected').rglob('*.json')) + assert not list((tmp_path / 'rejected').rglob('*.jsonl')) diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index 417040fc03..1205be5d33 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -5,6 +5,7 @@ import contextlib import io import json +from pathlib import Path import shlex import subprocess import sys @@ -57,6 +58,7 @@ def test_selected_acceptance_read_keeps_global_requirements_and_exact_binding( def cli(*arguments): code, result = run_json_cli_result(*arguments, registry_path=registry, runtime_root=runtime) assert code == 0, result + result.pop("_returncode", None) # harness metadata is not captured source content return result # Two owners' cumulative outcomes remain Goal requirements. Selection must @@ -122,8 +124,156 @@ def cli(*arguments): } observed = next(row["content"] for row in channel["work_context"]["sources"] if row.get("source") == "goal_acceptance") - assert observed == exact + scoped = _source_content(read, registry_path=registry, runtime_root=runtime, + goal_id=goal, todo_id=selected, agent_id="agent-a") + assert observed == scoped + assert scoped["goal_acceptance_contract"] == exact["goal_acceptance_contract"], "reading plans never filter canonical requirements" + assert scoped["outcome_context"]["required_outcome_ids"] == ["export-agent-a"] + assert scoped["outcome_context"]["complete"] is True + assert scoped["outcome_context"]["provider_revision"] == scoped["provider_revision"] + unknown = _source_content(read, registry_path=registry, runtime_root=runtime, + goal_id=goal, todo_id=selected, agent_id="not-registered") + assert unknown["status"] == "loaded" and unknown["goal_acceptance_contract"] == exact["goal_acceptance_contract"] + assert unknown["outcome_context"]["complete"] is False + assert unknown["outcome_context"]["required_outcome_ids"] == ["export-agent-a", "export-agent-b"] + with pytest.raises(ValueError, match="registered Agent"): + inspect_goal_acceptance(registry_path=registry, runtime_root=str(runtime), + goal_id=goal, agent_id="not-registered", todo_id=selected, include_work_details=False) + # The actual original-capture reader uses that plan without another guard + # or provider read. Default delivery still retains both complete outcomes. + from loopx.control_plane.turn_driver.decision_capture import write_decision_capture + import hashlib + + capture = tmp_path / "original-decision.json" + write_decision_capture(capture, packet) + captured = capture.read_bytes() + index = cli("quota", "context-read", "--capture-file", str(capture), + "--capture-sha256", hashlib.sha256(captured).hexdigest(), "--outcome-ref", + "--page-bytes", "8192") + assert index["eof"] is True + indexed = json.loads(index["content"]) + assert indexed["read_plan"]["complete"] is True + assert indexed["read_plan"]["scope_applied"] is False + assert {row["id"]: row["required_in_scope"] for row in indexed["outcomes"]} == { + "export-agent-a": True, "export-agent-b": False, + } + assert capture.read_bytes() == captured assert any(row.get("source") == "goal_state" for row in channel["required_reads"]) + + # The native presentation, its retained baseline and same-capture recovery + # all use the actual required input. The canonical capture stays complete. + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture + from loopx.control_plane.turn_driver.codex_context import codex_context_hook, project_codex_context + + with monkeypatch.context() as native: + native.setenv("CODEX_THREAD_ID", "outcome-scope-session") + native.chdir(tmp_path) + native_root = tmp_path / "native-context" + native_root.mkdir(mode=0o700) + event = {"session_id": "outcome-scope-session", "cwd": str(tmp_path)} + codex_context_hook(str(native_root), {**event, "hook_event_name": "SessionStart", "source": "startup"}) + + def consume(page): + observed = bytearray() + while True: + assert page["offset"] == len(observed) + observed.extend(page["content"].encode()) + codex_context_hook(str(native_root), {**event, "hook_event_name": "PostToolUse", + "tool_response": page}) + if page["eof"]: + break + page = read_context_capture(build_parser().parse_args(shlex.split(page["next_command"])[1:])) + assert page["ok"] + return json.loads(observed) + + source_before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} + assert project_codex_context(packet, directory=None, capture_directory=tmp_path / "disabled", + goal_id=goal, agent_id="agent-a", surface="complete_turn", capture_id="disabled") is packet + assert not (tmp_path / "disabled").exists() + first = tmp_path / "native-first" + first.mkdir(mode=0o700) + projected = project_codex_context(packet, directory=str(native_root), capture_directory=first, + goal_id=goal, agent_id="agent-a", surface="complete_turn", capture_id=hashlib.sha256(captured).hexdigest()) + assert projected["mode"] == "full" + view = native_context_capture_view(projected, first, context_directory=str(native_root), + metadata={"recovery": {"path": str(capture), "sha256": hashlib.sha256(captured).hexdigest()}}) + assert view["recovery"]["path"] == projected["recovery"]["path"] + assert view["recovery"]["presentation_schema"] == "outcome_context_projection_v0" + delivered = consume(view) + assert delivered["schema_version"] == "outcome_context_projection_v0" + assert delivered["read_scope"]["scope_applied"] is True + assert {row["id"]: row["required_in_scope"] for row in delivered["outcome_index"]} == { + "export-agent-a": True, "export-agent-b": False, + } + basis = next(row["content"] for row in delivered["context"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] + if row.get("source") == "goal_acceptance") + reason = next(row["reason"] for row in delivered["context"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] + if row.get("source") == "goal_acceptance") + assert "every outcome required by this capture's read_scope" in reason + assert "complete bodies before reliance" in reason + assert "Preserve the original Goal and earlier requirements" in reason + assert [row["id"] for row in basis["goal_acceptance_contract"]["outcome_requirements"]["outcomes"]] == ["export-agent-a"] + assert json.loads(Path(delivered["source_capture"]["path"]).read_bytes()) == packet + candidate = json.loads((native_root / "session.json").read_text())["candidate"] + assert candidate["capture_file"] == projected["recovery"]["path"] + assert json.loads(Path(candidate["capture_file"]).read_bytes()) == delivered + + optional_index = cli("quota", "context-read", "--capture-file", view["path"], + "--capture-sha256", view["sha256"], "--outcome-ref", "--page-bytes", "8192") + indexed = json.loads(optional_index["content"]) + assert indexed["read_plan"]["scope_applied"] is True + peer = next(row for row in indexed["outcomes"] if row["id"] == "export-agent-b") + detail = cli("quota", "context-read", "--capture-file", view["path"], + "--capture-sha256", view["sha256"], "--outcome-ref", peer["outcome_ref"], "--page-bytes", "8192") + assert json.loads(detail["content"])["outcome"]["id"] == "export-agent-b" + # A wrapper's own hash cannot substitute a missing/corrupt original. + broken = deepcopy(delivered) + broken["source_capture"]["sha256"] = "0" * 64 + invalid = tmp_path / "invalid-required-context.json" + write_decision_capture(invalid, broken) + rejected = read_context_capture(build_parser().parse_args(["--format", "json", "quota", "context-read", + "--capture-file", str(invalid), "--capture-sha256", hashlib.sha256(invalid.read_bytes()).hexdigest(), + "--outcome-ref", peer["outcome_ref"]])) + assert rejected["ok"] is False and "digest mismatch" in rejected["reason"] + + second = tmp_path / "native-second" + second.mkdir(mode=0o700) + warm = project_codex_context(packet, directory=str(native_root), capture_directory=second, + goal_id=goal, agent_id="agent-a", surface="complete_turn", capture_id=hashlib.sha256(captured).hexdigest()) + assert warm["mode"] in {"full", "delta"} + warm_view = native_context_capture_view(warm, second, context_directory=str(native_root)) + consume(warm_view) + codex_context_hook(str(native_root), {**event, "hook_event_name": "PreCompact"}) + assert "candidate" not in json.loads((native_root / "session.json").read_text()) + recovery = read_context_capture(build_parser().parse_args(shlex.split(warm["recovery"]["command"])[1:])) + restored = consume(recovery) + assert restored["read_scope"]["required_outcome_refs"] == delivered["read_scope"]["required_outcome_refs"] + assert restored["context"] == delivered["context"] + assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == source_before + assert capture.read_bytes() == captured + # Optional scope/codec failure recovers the complete original source, + # without weakening admission or inventing another user gate. + from loopx.control_plane.quota import turn_envelope + + original_operation = turn_envelope._turn_envelope_result + + def unavailable(operation, **request): + if operation == "outcome_context": + raise ValueError("synthetic optional scope projection failure") + return original_operation(operation, **request) + + with native.context() as failed: + failed.setattr(turn_envelope, "_turn_envelope_result", unavailable) + codex_context_hook(str(native_root), {**event, "hook_event_name": "PreCompact"}) + fallback_dir = tmp_path / "native-fallback" + fallback_dir.mkdir(mode=0o700) + fallback = project_codex_context(packet, directory=str(native_root), capture_directory=fallback_dir, + goal_id=goal, agent_id="agent-a", surface="complete_turn", capture_id="fallback") + assert fallback["mode"] == "full" + assert consume(native_context_capture_view(fallback, fallback_dir, context_directory=str(native_root))) == packet + assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == source_before + assert capture.read_bytes() == captured # The normal guard's complete captured basis is the envelope input; no # second guard may substitute a different observation for the host. from loopx.control_plane.quota.turn_envelope import build_turn_envelope @@ -210,6 +360,7 @@ def test_captured_acceptance_does_not_replace_current_work_admission(acceptance_ def cli(*arguments): code, result = run(*arguments) assert code == 0, result + result.pop("_returncode", None) return result def configure(*arguments): @@ -232,23 +383,28 @@ def configure(*arguments): original = configure("inspect")["goal_acceptance_contract"]["outcome_requirements"] capture = tmp_path / "decision" guard = ("quota", "should-run", "--goal-id", goal, "--agent-id", agent, "--codex-app") - first = cli(*guard, "--todo-id", selected, "--turn-instance-id", "original-delivery", + cli(*guard, "--todo-id", selected, "--turn-instance-id", "original-delivery", "--turn-envelope", "--decision-output-dir", str(capture)) saved = (capture / "decision.json").read_bytes() full = json.loads(saved) + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from loopx.control_plane.scheduler.execution_context import scheduler_execution_context_for_runtime_profile + + first = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json"), + scheduler_execution_context=scheduler_execution_context_for_runtime_profile("codex_app_heartbeat")) basis = next(row["content"] for row in first["work_context"]["sources"] if row.get("source") == "goal_acceptance") assert first["action"]["delivery_allowed"] is True assert basis["tasks"][0]["state"] == "ready" assert basis["goal_acceptance_contract"]["outcome_requirements"] == original request = build_loopx_turn_host_request(build_loopx_turn_plan(first, - host="codex-cli", execution_mode="isolated-headless", turn_instance_id="original-delivery")) + host="codex-cli", execution_mode="isolated-headless", turn_instance_id="original-delivery", work_contract=full)) result = subprocess.run(_recording_host(project), input=json.dumps(request), capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr prompt = (project / "received.txt").read_text() - authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) - assert authority["work_context"] == first["work_context"] + authority = json.loads(prompt.split("Turn request:\n", 1)[1].split("\n\nWhen finished", 1)[0]) + assert authority["work_contract"] == full # Finish the original synthetic work before attempting another managed # Turn. An unsettled receipt is its own legitimate recovery boundary. diff --git a/tests/control_plane/test_work_context_host_delivery.py b/tests/control_plane/test_work_context_host_delivery.py index c913e2eef1..57eb3ae0e0 100644 --- a/tests/control_plane/test_work_context_host_delivery.py +++ b/tests/control_plane/test_work_context_host_delivery.py @@ -9,6 +9,8 @@ import pytest from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.quota.turn_envelope import build_turn_envelope +from loopx.control_plane.scheduler.execution_context import scheduler_execution_context_for_runtime_profile from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt from tests.control_plane.canonical_authority_fixture import ( initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos, @@ -48,12 +50,16 @@ def test_authored_requirements_survive_capture_projection_and_host(tmp_path, mon '--todo-id', TODO_ID, '--turn-instance-id', TURN_ID, '--codex-app', '--available-capability', 'shell', '--available-capability', 'filesystem_read'] capture = tmp_path / 'original-context' - code, envelope = _run_cli(registry, runtime, *args, '--turn-envelope', + code, emitted = _run_cli(registry, runtime, *args, '--turn-envelope', '--decision-output-dir', str(capture)) - assert code == 0, envelope + assert code == 0, emitted saved = capture / 'decision.json' original_bytes = saved.read_bytes() full = json.loads(original_bytes) + # New CLI emission is the complete contract. Exercise the retained internal + # adapter explicitly; it is no longer a competing CLI output format. + envelope = build_turn_envelope(full, captured_decision_path=str(saved), + scheduler_execution_context=scheduler_execution_context_for_runtime_profile('codex_app_heartbeat')) source_context = full['interaction_contract']['agent_channel']['work_context'] assert source_context['complete'] is True assert source_context['instruction'] # Non-vacuous instruction transport. diff --git a/tests/control_plane_ts/goal_outcome_refinement.test.ts b/tests/control_plane_ts/goal_outcome_refinement.test.ts index f610d7cd1f..7db79edf4a 100644 --- a/tests/control_plane_ts/goal_outcome_refinement.test.ts +++ b/tests/control_plane_ts/goal_outcome_refinement.test.ts @@ -8,11 +8,14 @@ import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_au import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts"; import {readCanonicalSnapshotFromStore, CANONICAL_SNAPSHOT_PAGE_REQUEST} from "../../loopx/control_plane/coordination/canonical_snapshot_page.ts"; import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; -import {refineGoalOutcome, projectGoalOutcomes} from "../../loopx/control_plane/goals/outcome_requirements.ts"; +import {refineGoalOutcome, projectGoalOutcomes, outcomeContextReadPlan} from "../../loopx/control_plane/goals/outcome_requirements.ts"; import {refineGoalAcceptanceOutcome, inspectGoalAcceptance, configureGoalAcceptance, transitionGoalAcceptanceLifecycle} from "../../loopx/control_plane/goals/acceptance_authority.ts"; import {observeAcceptanceOracles} from "../../loopx/control_plane/goals/acceptance_oracle.ts"; import {projectGoalAcceptance} from "../../loopx/control_plane/goals/acceptance_contract.ts"; import {projectReplanSemantics} from "../../loopx/control_plane/work_items/replan_semantics.ts"; +import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts"; +import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts"; +import {projectInteractionRequiredReads} from "../../loopx/control_plane/work_items/interaction_contract.ts"; const goal = "tour-goal"; const goalRef = {goal_id: goal, goal_instance_id: "ginst_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}; @@ -35,6 +38,234 @@ function outcomeAck(gaps: JsonObject[]): JsonObject { trigger_checkpoints: gaps.slice(0, 3).map(({kind, frontier_revision}) => ({kind, frontier_revision}))}}; } +function scopedOutcomes(changes: Record = {}) { + const items = ["own", "peer", "dependency", "independent"].map(name => ({ + todo_id: `todo_${name}`, role: "agent", status: "open", done: false, + archive_state: "active", text: `Deliver ${name}`, task_class: "advancement_task", + claimed_by: name === "own" ? "author" : "peer", ...changes[name], + })); + const head: JsonObject = authorityProjectionFixture(goal, items, [], "native"); + for (const name of ["own", "peer", "dependency", "independent"]) { + const owner = name === "own" ? "author" : "peer"; + head.goal_outcomes = refineGoalOutcome(head, goal, null, owner, + patch({outcome_id: name, owner_agent_id: owner, todo_ids: [`todo_${name}`], + source: {ref: `message:${name}`, summary: "Independently requested work"}})); + } + return head; +} + +test("outcome reading plan follows canonical cross-Agent closure without dropping any source", () => { + const head = scopedOutcomes({peer: {unblocks_todo_id: "todo_own"}, + dependency: {unblocks_todo_id: "todo_peer"}}); + const acceptance = projectGoalAcceptance(head, goal), before = structuredClone(head); + const plan = outcomeContextReadPlan(head, goal, "author", "todo_own", acceptance)!; + assert.equal(plan.complete, true); + assert.deepEqual(plan.required_outcome_ids, ["own", "peer", "dependency"]); + assert.deepEqual(head, before); + assert.equal((acceptance.outcome_requirements as JsonObject).outcomes instanceof Array, true); + const separate = scopedOutcomes(); + assert.deepEqual(outcomeContextReadPlan(separate, goal, "author", "todo_own", + projectGoalAcceptance(separate, goal))!.required_outcome_ids, ["own"]); + for (const relation of [{successor_todo_ids: ["todo_own"]}, {superseded_by: "todo_own"}, + {resume_when: "todo_done:todo_own"}]) { + const linked = scopedOutcomes({peer: relation}); + assert.deepEqual(outcomeContextReadPlan(linked, goal, "author", "todo_own", + projectGoalAcceptance(linked, goal))!.required_outcome_ids, ["own", "peer"]); + } + const sharedWait = scopedOutcomes({own: {resume_when: "pr_merged:example/repo#123"}, + peer: {resume_when: "pr_merged:example/repo#123"}}); + assert.deepEqual(outcomeContextReadPlan(sharedWait, goal, "author", "todo_own", + projectGoalAcceptance(sharedWait, goal))!.required_outcome_ids, ["own", "peer"]); + const sharedSource = structuredClone(projectGoalAcceptance(separate, goal)); + const rows = (sharedSource.outcome_requirements as JsonObject).outcomes as JsonObject[]; + rows[1].sources = rows[0].sources; + assert.deepEqual(outcomeContextReadPlan(separate, goal, "author", "todo_own", sharedSource)! + .required_outcome_ids, ["own", "peer"]); + const sharedEvidence = structuredClone(projectGoalAcceptance(separate, goal)); + const evidenceRows = (sharedEvidence.outcome_requirements as JsonObject).outcomes as JsonObject[]; + for (const [index, conclusion] of ["met", "unmet"].entries()) { + (evidenceRows[index].requirements as JsonObject[])[0].agent_review = { + conclusion, evidence_refs: ["artifact:shared"], independently_verified: false, + }; + } + assert.deepEqual(outcomeContextReadPlan(separate, goal, "author", "todo_own", sharedEvidence)! + .required_outcome_ids, ["own", "peer"], "opposing reviews of the same evidence remain visible"); +}); + +test("shared acceptance and failed evidence widen reading; missing relationships keep the full source", () => { + const head = scopedOutcomes({dependency: {unblocks_todo_id: "todo_peer"}}); + const acceptance = projectGoalAcceptance(head, goal); + const read = (checks: JsonObject, todo = "todo_own") => outcomeContextReadPlan(head, goal, "author", todo, + {...acceptance, ...checks})!; + assert.deepEqual(read({scope: {kind: "selected_work", todo_ids: ["todo_own", "todo_peer"]}}) + .required_outcome_ids, ["own", "peer", "dependency"]); + const conflicting = structuredClone(acceptance); + const rows = (conflicting.outcome_requirements as JsonObject).outcomes as JsonObject[]; + (rows[1].requirements as JsonObject[])[0].status = "failed"; + assert.deepEqual(read(conflicting).required_outcome_ids, ["own", "peer", "dependency"]); + for (const plan of [read({criteria: [{id: "global"}]}), read({}, "todo_unknown"), + read({outcome_requirements: {...acceptance.outcome_requirements as JsonObject, current_lifetime: false}}), + (() => {const missing = scopedOutcomes({peer: {unblocks_todo_id: "todo_absent"}}); + return outcomeContextReadPlan(missing, goal, "author", "todo_own", projectGoalAcceptance(missing, goal))!;})()]) { + assert.equal(plan.complete, false); + assert.deepEqual(plan.required_outcome_ids, ["own", "peer", "dependency", "independent"]); + assert.ok((plan.fallback_reasons as string[]).length > 0); + } +}); + +test("the captured index consumes the same reading plan and refuses stale or narrower coverage", () => { + const head = scopedOutcomes({peer: {unblocks_todo_id: "todo_own"}}); + const acceptance = projectGoalAcceptance(head, goal); + const plan = {...outcomeContextReadPlan(head, goal, "author", "todo_own", acceptance), provider_revision: "file:original"}; + const content = {status: "loaded", provider_revision: "file:original", outcome_context: plan, + goal_acceptance_contract: acceptance}; + const source: JsonObject = {ok: true, goal_id: goal, agent_identity: {agent_id: "author"}, + selected_todo: {todo_id: "todo_own"}, interaction_contract: {agent_channel: {work_context: { + complete: true, sources: [{source: "goal_acceptance", content}], + }}}}; + const read = (payload: JsonObject) => evaluateTurnEnvelope({operation: "captured_outcome", payload, outcome_ref: ""}); + const index = read(source); + assert.equal((index.read_plan as JsonObject).complete, true); + assert.equal((index.read_plan as JsonObject).scope_applied, false, "the ordinary full-read obligation remains unchanged"); + assert.deepEqual((index.outcomes as JsonObject[]).filter(o => o.required_in_scope).map(o => o.id), ["own", "peer"]); + const adaptive = read({...source, selected_todo: {todo_id: "todo_independent"}, + task_orchestration_contract: {schema_version: "task_orchestration_contract_v2", mode: "adaptive", primary_todo_id: "todo_own"}}); + assert.equal((adaptive.read_plan as JsonObject).complete, true, "use the existing admitted-work identity owner"); + assert.deepEqual((adaptive.outcomes as JsonObject[]).filter(o => o.required_in_scope).map(o => o.id), ["own", "peer"]); + for (const altered of [ + {...source, autonomous_replan_obligation: {obligation_id: "wide-review"}}, + {...source, selected_todo: {todo_id: "todo_independent"}}, + {...source, agent_identity: {agent_id: "other"}}, + {...source, agent_todo_summary: {goal_acceptance_contract: { + outcome_requirements: {outcomes: [{id: "another-version", requirements: []}]}}}}, + ]) { + const fallback = read(altered); + assert.equal((fallback.read_plan as JsonObject).complete, false); + assert.ok((fallback.outcomes as JsonObject[]).every(o => o.required_in_scope)); + } + for (const stale of [{provider_revision: "file:changed"}, + {acceptance_digest: "0".repeat(64)}, {required_outcome_ids: ["peer"]}, + {outcome_revision: 999}, {complete: false}, {schema_version: "future"}]) { + const fallback = read({...source, interaction_contract: {agent_channel: {work_context: {sources: [ + {source: "goal_acceptance", content: {...content, outcome_context: {...plan, ...stale}}}, + ]}}}}); + assert.equal((fallback.read_plan as JsonObject).complete, false); + assert.ok((fallback.outcomes as JsonObject[]).every(o => o.required_in_scope)); + } + for (const change of [{future_dependency: {known: false}}, {revision: 999}, {global_check: true}]) { + const changed = structuredClone(acceptance); + if (change.global_check) changed.scope = {kind: "all_advancement"}; + else Object.assign(((changed.outcome_requirements as JsonObject).outcomes as JsonObject[])[1], change); + const fallback = read({...source, interaction_contract: {agent_channel: {work_context: {sources: [ + {source: "goal_acceptance", content: {...content, goal_acceptance_contract: changed}}, + ]}}}}); + assert.equal((fallback.read_plan as JsonObject).complete, false); + assert.ok((fallback.outcomes as JsonObject[]).every(o => o.required_in_scope)); + } +}); + +function outcomeContextPacket(linkPeer = false): JsonObject { + const head = scopedOutcomes(linkPeer ? {peer: {unblocks_todo_id: "todo_own"}} : {}); + const acceptance = projectGoalAcceptance(head, goal); + const rows = (acceptance.outcome_requirements as JsonObject).outcomes as JsonObject[]; + ((rows[0].requirements as JsonObject[])[0]).description = "Complete own requirement. ".repeat(800); + ((rows[1].requirements as JsonObject[])[0]).description = "Unique peer requirement that was not delivered earlier."; + const reads = projectInteractionRequiredReads({arbitration_facts: { + schema_version: "loopx_interaction_contract_v0", mode: "bounded_delivery", + user_channel: {present: true, action_required: false}, + agent_channel: {present: true, must_attempt: true, delivery_allowed: true, quiet_noop_allowed: false}, + cli_channel: {}, agent_scope_frontier_actions: []}, required_reads: [], command_prefix: "loopx", + goal_id: goal, goal_acceptance_checks_present: false, goal_outcomes_present: true, + should_run: true, delivery_allowed: true, selection_required: false, has_replan: false, + settlement_only: false}).required_reads as JsonObject[]; + return {ok: true, goal_id: goal, agent_identity: {agent_id: "author"}, selected_todo: {todo_id: "todo_own"}, + goal_boundary: {stop: "Preserve the global stop."}, interaction_contract: {agent_channel: {work_context: { + complete: true, sources: [{...reads[0], content: {status: "loaded", provider_revision: "file:original", + outcome_context: {...outcomeContextReadPlan(head, goal, "author", "todo_own", acceptance), provider_revision: "file:original"}, + goal_acceptance_contract: acceptance}}], + }}}}; +} + +test("scoped model context retains canonical source, global controls and exact optional detail", () => { + const payload = outcomeContextPacket(), before = structuredClone(payload); + const source_capture = {path: "/capture/original.json", sha256: "a".repeat(64)}; + const context = evaluateTurnEnvelope({operation: "outcome_context", payload, source_capture}); + assert.equal(context.schema_version, "outcome_context_projection_v0"); + assert.equal((context.read_scope as JsonObject).scope_applied, true); + assert.deepEqual((context.context as JsonObject).goal_boundary, payload.goal_boundary); + const sources = ((((context.context as JsonObject).interaction_contract as JsonObject).agent_channel as JsonObject).work_context as JsonObject).sources as JsonObject[]; + const rows = (((sources[0].content as JsonObject).goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject).outcomes as JsonObject[]; + assert.deepEqual(rows.map(row => row.id), ["own"]); + assert.match(String(sources[0].reason), /every outcome required by this capture's read_scope/); + assert.match(String(sources[0].reason), /read their complete bodies before reliance/); + assert.match(String(sources[0].reason), /Preserve the original Goal and earlier requirements/); + assert.equal(JSON.stringify(context).includes("Unique peer requirement"), false); + const index = evaluateTurnEnvelope({operation: "captured_outcome", payload: context, source_payload: payload, outcome_ref: ""}); + assert.equal((index.read_plan as JsonObject).scope_applied, true); + const peer = (index.outcomes as JsonObject[]).find(row => row.id === "peer")!; + const detail = evaluateTurnEnvelope({operation: "captured_outcome", payload: context, source_payload: payload, outcome_ref: peer.outcome_ref}); + assert.ok(JSON.stringify(detail).includes("Unique peer requirement")); + assert.deepEqual(payload, before, "the canonical decision and all provenance stay intact"); + const altered = structuredClone(context); (altered.context as JsonObject).goal_boundary = {stop: "changed"}; + assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: altered, source_payload: payload, outcome_ref: ""}), /differs from its original source/); + assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: context, outcome_ref: ""}), /original outcome source/); + const broad = {...payload, autonomous_replan_obligation: {id: "whole-goal"}}; + assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: broad, source_capture}), broad); + const unknown = outcomeContextPacket(); + const content = (((((unknown.interaction_contract as JsonObject).agent_channel as JsonObject).work_context as JsonObject).sources as JsonObject[])[0]).content as JsonObject; + const acceptance = content.goal_acceptance_contract as JsonObject; + (((acceptance.outcome_requirements as JsonObject).outcomes as JsonObject[])[1]).future_obligation = "Unknown cross-Agent constraint"; + // Even a plan bound to that complete source must not hide unknown semantics. + (content.outcome_context as JsonObject).acceptance_digest = canonicalAuthoritySha256(acceptance); + assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: unknown, source_capture}), unknown); + const unread = outcomeContextPacket(); + (unread.interaction_contract as JsonObject).agent_channel.required_reads = [{source: "goal_acceptance", command: "pending-read"}]; + assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: unread, source_capture}), unread); + const future = outcomeContextPacket(); + future.interaction_contract.agent_channel.work_context.sources[0].reason += " Retain a future instruction."; + assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: future, source_capture}), future); +}); + +test("warm scope expansion delivers previously unread requirements; lost baseline recovers the same required input", () => { + const project = (payload: JsonObject, letter: string) => evaluateTurnEnvelope({operation: "outcome_context", payload, + source_capture: {path: `/capture/${letter}.json`, sha256: letter.repeat(64)}}); + const before = project(outcomeContextPacket(), "a"), after = project(outcomeContextPacket(true), "b"); + const scope = {goal_id: goal, agent_id: "author", session_id: "same-session", context_epoch: "same-epoch"}; + const baseline = evaluateTurnEnvelope({operation: "context_delivery", context: before, scope, capture_id: "first", surface: "quota_packet"}); + const delivery = evaluateTurnEnvelope({operation: "context_delivery", context: after, scope, capture_id: "second", surface: "quota_packet", + baseline, retained: {complete: true, target: baseline.target}}); + assert.equal(delivery.mode, "delta"); + assert.ok(JSON.stringify(delivery).includes("Unique peer requirement"), "a machine-only canonical source is not retained model context"); + const current = evaluateTurnEnvelope({operation: "context_delivery", context: after, scope, capture_id: "second", surface: "quota_packet"}); + const restored = evaluateTurnEnvelope({operation: "context_recovery", delivery, baseline, current}); + assert.deepEqual(restored.context, after); + const lost = evaluateTurnEnvelope({operation: "context_recovery", delivery, current}); + assert.equal(lost.mode, "full"); assert.deepEqual(lost.context, after); +}); + +for (const provider of ["file", "sqlite"] as const) test(`${provider}: reading scope and full acceptance share one canonical observation`, async t => { + const root = await mkdtemp(join(tmpdir(), "outcome-reading-scope-")); + t.after(() => rm(root, {recursive: true, force: true})); + const store = provider === "file" ? new FileAuthorityStore(root, goal) : new SqliteAuthorityStore(root, goal); + const head = scopedOutcomes({peer: {unblocks_todo_id: "todo_own"}}); + await store.commitAuthority({operation_id: "seed", expected_provider_revision: null, + next_projection: head, events: [], receipts: []}); + const load = store.loadAuthority.bind(store); let reads = 0; + store.loadAuthority = async () => {reads++; return load();}; + const scoped = await inspectGoalAcceptance(store, goal, "todo_own", undefined, false, undefined, ["author", "peer"], "author"); + assert.equal(reads, 1); + assert.deepEqual(scoped.goal_acceptance_contract, projectGoalAcceptance(head, goal)); + assert.deepEqual((scoped.outcome_context as JsonObject).required_outcome_ids, ["own", "peer"]); + assert.equal((scoped.outcome_context as JsonObject).provider_revision, scoped.provider_revision); + const ordinary = await inspectGoalAcceptance(store, goal); + assert.equal(ordinary.outcome_context, undefined, "ordinary inspect keeps the full existing format"); + assert.deepEqual(ordinary.goal_acceptance_contract, scoped.goal_acceptance_contract); + const unknown = await inspectGoalAcceptance(store, goal, "todo_own", undefined, false, undefined, ["author", "peer"], "unknown"); + assert.equal((unknown.outcome_context as JsonObject).complete, false); + assert.deepEqual((unknown.outcome_context as JsonObject).required_outcome_ids, ["own", "peer", "dependency", "independent"]); + assert.deepEqual(unknown.goal_acceptance_contract, scoped.goal_acceptance_contract); +}); + test("refinement retains original intent, accumulates requirements and cannot invent success", () => { const original = fixture(); assert.deepEqual(projectGoalAcceptance(original, goal), {}); diff --git a/tests/control_plane_ts/interaction_contract.test.ts b/tests/control_plane_ts/interaction_contract.test.ts index 5f7212d2f3..be1d8012fc 100644 --- a/tests/control_plane_ts/interaction_contract.test.ts +++ b/tests/control_plane_ts/interaction_contract.test.ts @@ -14,6 +14,7 @@ import { projectInteractionRequiredReads, projectInteractionWorkContext, assertGoalContextDelivered, + scopeInteractionOutcomeReadGuidance, projectBlockedSuccessorWaitObservation, type AgentInteractionChannel, } from "../../loopx/control_plane/work_items/interaction_contract.ts"; @@ -642,6 +643,31 @@ test("declared outcomes remain required reads without enabled checks after a wor assert.ok(!off.some(read => read.source === "goal_acceptance")); }); +test("scoped presentation keeps the original objective, refinement and stop obligations", () => { + for (const objective of [false, true]) { + const reads = (projectInteractionRequiredReads({...readFacts(), required_reads: [], + goal_objective_present: objective, goal_outcomes_present: true}).required_reads as JsonObject[]) + .filter(read => read.source === "goal_acceptance"); + const source: JsonObject = {...reads[0], content: {status: "loaded", goal_acceptance_contract: { + outcome_requirements: {outcomes: []}, ...(objective ? {goal_objective: {revision: 1}} : {})}}}; + const channel = {required_reads: [], work_context: {sources: [source]}}; + const original = String(source.reason); + assert.equal(scopeInteractionOutcomeReadGuidance(channel), true); + const reason = String(source.reason); + for (const clause of ["read_scope", "complete bodies before reliance", "Unknown relevance", + "original Goal and earlier requirements", "Before switching work or declaring completion", + "refresh-state --goal-outcome-update-json", "unchanged work needs no extra inspect/write", + "Agent review is distinct from independent checks", "Scoped acceptance never replaces Goal intent", + "Changed requirements need a fresh guard"]) + assert.ok(reason.includes(clause), clause); + if (objective) assert.ok(reason.startsWith(original.slice(0, original.indexOf(" Read the full acceptance basis"))), + "objective adoption, settlement/release, peer independence and stops retain their exact instruction"); + const unknown = {required_reads: [], work_context: {sources: [{...source, reason: "Unknown future obligations"}]}}; + assert.equal(scopeInteractionOutcomeReadGuidance(unknown), false); + assert.equal(unknown.work_context.sources[0].reason, "Unknown future obligations"); + } +}); + test("full original Goal delivery fulfills its read without excluding legacy text", () => { const text = "Original intent\nUnknown section\n停止条件:不得发布。\n"; diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index f40b64b7b4..6cc7c67668 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -1,7 +1,11 @@ +import { EFFECTIVE_ACTIONS } from "../../loopx/control_plane/quota/effective_action.generated.ts"; +import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../../loopx/control_plane/agents/agent_scope_frontier.generated.ts"; import assert from "node:assert/strict"; import test from "node:test"; import { + TURN_ACTION_ROUTES, + turnEnvelopeRoute, ACTION_SIGNATURE_COVERAGE_V0, ACTION_SIGNATURE_COVERAGE_V3, buildTurnEnvelope, @@ -16,6 +20,59 @@ import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; import { TURN_ENVELOPE_SECTION_TARGETS } from "../../loopx/control_plane/quota/turn_envelope_budget.ts"; import { projectPeerOrchestration } from "../../loopx/control_plane/quota/peer_orchestration.ts"; +test("captured outcome lookup shares exact content across registered carriers and retains revisions", () => { + const one = {id: "shared", owner_agent_id: "peer", objective: "Original", revision: 1, + requirements: [{id: "a", planning_satisfied: false, status: "unverified", description: "Full a"}, + {id: "b", planning_satisfied: false, status: "unverified", description: "Full b"}, + {id: "c", planning_satisfied: true, status: "unverified", description: "Full c", + agent_review: {conclusion: "met", basis_current: true, independently_verified: false}}], + work: [{todo_id: "dependency", status: "blocked"}], future: {negative: false, unknown: null}}; + const two = {...one, revision: 2, objective: "Changed"}; + const typedDifference = {...one, future: {negative: 0, unknown: null}}; + const contract = (outcome: unknown) => ({outcome_requirements: {outcomes: [outcome]}}); + const source = {ok: true, goal_id: "goal", agent_identity: {agent_id: "controller"}, + interaction_contract: {agent_channel: {work_context: {sources: [ + {source: "goal_acceptance", content: {status: "loaded", goal_acceptance_contract: contract(one)}}, + {source: "goal_acceptance", content: {status: "loaded", goal_acceptance_contract: contract(two)}}, + {source: "unregistered", content: {goal_acceptance_contract: contract({...one, id: "not-a-source"})}}, + ]}}}, planning_horizon: {core_goal: {acceptance_contract_ref: + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract"}}, + agent_todo_summary: {goal_acceptance_contract: contract(typedDifference)}}; + const original = structuredClone(source); + const index = evaluateTurnEnvelope({operation: "captured_outcome", payload: source, outcome_ref: ""}); + const rows = index.outcomes as JsonObject[]; + assert.equal(rows.length, 3); + assert.notEqual(rows[0].outcome_ref, rows[1].outcome_ref); + assert.deepEqual(rows[0].requirement_states, [ + {requirement_ids: ["a", "b"], planning_satisfied: false, status: "unverified"}, + {requirement_ids: ["c"], planning_satisfied: true, status: "unverified", + agent_review: {conclusion: "met", basis_current: true, independently_verified: false}}, + ]); + // The preferred work carrier appears first, followed by the registered + // summary carrier and the second loaded work source. + for (const [i, outcome] of [one, typedDifference, two].entries()) { + const detail = evaluateTurnEnvelope({operation: "captured_outcome", payload: source, outcome_ref: rows[i].outcome_ref}); + assert.deepEqual(detail.outcome, outcome); + } + assert.deepEqual(source, original); +}); + +test("captured outcome index refuses unknown sources and keeps missing facts unknown", () => { + const read = (payload: JsonObject) => evaluateTurnEnvelope({operation: "captured_outcome", payload, outcome_ref: ""}); + for (const payload of [{ok: false}, {ok: true}, {ok: true, schema_version: "unknown"}, + {ok: true, agent_todo_summary: {goal_acceptance_contract: {outcome_requirements: {outcomes: null}}}}]) { + assert.throws(() => read(payload)); + } + const source = {ok: true, agent_todo_summary: {goal_acceptance_contract: {outcome_requirements: { + outcomes: [{id: "unknown", requirements: [{id: "r"}, {id: "null", agent_review: null}]}]}}}}; + const row = (read(source).outcomes as JsonObject[])[0]; + assert.deepEqual(row.requirement_states, [{requirement_ids: ["r"]}, + {requirement_ids: ["null"], agent_review: null}]); + assert.equal(row.work, undefined); + assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: source, + outcome_ref: "sha256:" + "0".repeat(64)}), /exact captured outcome reference/); +}); + test("task text commitment has one selected-Todo carrier without changing legacy context elision", () => { for (const retainDetail of [false, true]) { const source = payload(); @@ -87,6 +144,31 @@ function payload(): Record { }; } +test("managed controller builds its unchanged envelope and typed route in one projection", () => { + for (const [action, delivery, shouldRun, quiet, expected] of [ + ["normal_run", true, true, false, "ready_for_host"], + ["control_plane_repair", true, true, false, "repair_required"], + ["autonomous_replan_required", false, true, false, "replan_required"], + ["normal_run", false, true, false, "blocked"], + ["normal_run", false, false, true, "wait"], + ["future_repair", true, true, false, "ready_for_host"], + ] as const) { + const source = payload(); + source.effective_action = action; + source.should_run = shouldRun; + const channel = (source.interaction_contract as JsonObject).agent_channel as JsonObject; + channel.delivery_allowed = delivery; + channel.must_attempt = shouldRun; + channel.quiet_noop_allowed = quiet; + const request = {payload: source, scheduler_execution_args: ""}; + const before = structuredClone(source); + const result = evaluateTurnEnvelope({operation: "build_controller_input", ...request}); + assert.deepEqual(result.envelope, buildTurnEnvelope(request)); + assert.equal(result.route, expected); + assert.deepEqual(source, before); + } +}); + test("execution contracts retain complete clauses, nested data and empty values", () => { const source = payload(); const clause = "Preserve original identity / 保留原身份. ".repeat(40) + "Stop on source mismatch."; @@ -1225,3 +1307,34 @@ test("consumer version gate rejects explicit future/null declarations without re assertTurnEnvelopeConsumerVersion(legacy); assert.deepEqual(envelope, before); }); + + +test("registered route coverage and historical actions share the runtime classifier", () => { + const historical = ["autonomous_replan", "successor_replan_required"]; + assert.deepEqual(new Set(Object.keys(TURN_ACTION_ROUTES)), + new Set([...EFFECTIVE_ACTIONS, ...AGENT_SCOPE_FRONTIER_ACTIONS, ...historical])); + for (const [name, route] of Object.entries(TURN_ACTION_ROUTES)) { + const envelope = {schema_version: "loopx_turn_envelope_v0", should_run: true, + effective_action: name, goal_id: "g", agent_id: "a", + action_signature: {matches: true, source_hash: "same", envelope_hash: "same"}, + action: {must_attempt: true, delivery_allowed: true, selected_todo: {todo_id: "t"}, + capability_intent: {schema_version: "pending_capability_intent_projection_v0", + goal_id: "g", agent_id: "a", command: "work"}}}; + assert.equal(turnEnvelopeRoute(envelope), route, name); + } + for (const name of ["future_repair", "constructor", "toString", "__proto__"]) { + assert.equal(turnEnvelopeRoute({should_run: true, effective_action: name, + action_signature: {matches: true, source_hash: "same", envelope_hash: "same"}, + action: {must_attempt: true, delivery_allowed: true}}), "ready_for_host"); + } +}); + +test("complete driver projection ignores historical scalar decoding hints", () => { + const contract = evaluateTurnEnvelope({operation: "compose_complete", payload: payload(), scheduler_execution_args: ""}); + const envelope = buildTurnEnvelope({payload: contract, scheduler_execution_args: ""}); + const normal = evaluateTurnEnvelope({operation: "driver_lineage", payload: contract, envelope}); + const forged = evaluateTurnEnvelope({operation: "driver_lineage", payload: contract, envelope, + historical_route_text: {source_hash: "", envelope_hash: "different", effective_action: "autonomous_replan"}}); + assert.equal(normal.route, "ready_for_host"); + assert.deepEqual(forged, normal); +}); diff --git a/tests/test_ark_managed_agent_host.py b/tests/test_ark_managed_agent_host.py index 605b5851cc..36a19017ed 100644 --- a/tests/test_ark_managed_agent_host.py +++ b/tests/test_ark_managed_agent_host.py @@ -126,7 +126,7 @@ def test_goal_prompt_projects_goal_only_host_contract() -> None: "quota_should_run.interaction_contract.cli_channel." "runtime_capability_reentry" ), - "cli_projection_ref": "quota_should_run.runtime_capability_reentry", + "cli_projection_ref": "quota_should_run.interaction_contract.cli_channel.runtime_capability_reentry", "packet_schema_version": "runtime_capability_reentry_v0", "delivery_channel": "quota_tool_result", "goal_prompt_mutated": False, diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 21716da259..cf651103f1 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -3439,12 +3439,15 @@ def test_turn_run_once_cli_rejects_unproven_host_claim_before_writeback( @pytest.mark.parametrize( "result_kind", ["validated_progress", "repair_required", "replan_required"] ) +@pytest.mark.parametrize("verified_source", [False, True]) def test_turn_run_once_cli_uses_built_in_codex_host_and_typed_writeback( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, result_kind: str, + verified_source: bool, ) -> None: - from loopx.cli_commands.turn import build_turn_envelope as real_build_turn_envelope + from copy import deepcopy + from loopx.cli_commands.turn import compose_turn_decision as real_compose_turn_decision from loopx.cli_commands.turn_run_once import ( refresh_state_run as real_refresh_state_run, spend_quota_slot as real_spend_quota_slot, @@ -3489,19 +3492,28 @@ def test_turn_run_once_cli_uses_built_in_codex_host_and_typed_writeback( spend_workspace_paths: list[Path | None] = [] refresh_todo_ids: list[str] = [] - def adaptive_turn_envelope(*args: object, **kwargs: object) -> dict[str, object]: - envelope = real_build_turn_envelope(*args, **kwargs) - envelope["action"]["selected_todo"] = { + def adaptive_turn_decision(source: Mapping[str, Any], **kwargs: Any) -> dict[str, Any]: + # Build the stale-selection scenario before signing either projection. + # The adaptive primary, not the provisional selection, owns writeback. + decision = deepcopy(source) + # This pre-orchestration selection is not a verified completion source. + # Preserve the historical unverified-selection case; claiming verified + # text for a different Todo must still reject in captured_todo_text_sha256. + if not verified_source: + decision["interaction_contract"]["agent_channel"]["work_context"].pop( + "selected_todo_text_verified", None + ) + decision["selected_todo"] = { "todo_id": "todo_stale_selection", "text": "A stale pre-orchestration selection", } - envelope["task_orchestration_contract"] = { + decision["task_orchestration_contract"] = { "schema_version": "task_orchestration_contract_v2", "mode": "adaptive", "primary_todo_id": "todo_fixture0001", "eligible_child_lanes": [], } - return envelope + return real_compose_turn_decision(decision, **kwargs) def recording_refresh_state_run(*args: object, **kwargs: object) -> dict[str, object]: refresh_workspace_paths.append(kwargs.get("delivery_workspace_path")) @@ -3512,7 +3524,10 @@ def recording_spend_quota_slot(*args: object, **kwargs: object) -> dict[str, obj spend_workspace_paths.append(kwargs.get("workspace_path")) return real_spend_quota_slot(*args, **kwargs) + host_calls: list[str] = [] + def fake_codex_host(request: dict[str, object], **_kwargs: object) -> dict[str, object]: + host_calls.append(str(request["turn_key"])) return { "schema_version": "loopx_turn_result_v0", "turn_key": request["turn_key"], @@ -3532,8 +3547,8 @@ def fake_codex_host(request: dict[str, object], **_kwargs: object) -> dict[str, fake_codex_host, ) monkeypatch.setattr( - "loopx.cli_commands.turn.build_turn_envelope", - adaptive_turn_envelope, + "loopx.cli_commands.turn.compose_turn_decision", + adaptive_turn_decision, ) monkeypatch.setattr( "loopx.cli_commands.turn_run_once.refresh_state_run", @@ -3577,6 +3592,12 @@ def fake_codex_host(request: dict[str, object], **_kwargs: object) -> dict[str, ) payload = json.loads(output.getvalue()) + if verified_source: + assert exit_code == 1, payload + assert "Turn task source does not match" in payload["error"] + assert not host_calls and not refresh_workspace_paths and not spend_workspace_paths + return + assert len(host_calls) == 1 assert exit_code == 0, payload assert payload["host"] == {"executable": "built-in", "kind": "codex-cli"} assert payload["effects"]["state_written"] is True diff --git a/tests/test_turn_envelope.py b/tests/test_turn_envelope.py index 7934a25aef..b950538b0f 100644 --- a/tests/test_turn_envelope.py +++ b/tests/test_turn_envelope.py @@ -1007,7 +1007,7 @@ def test_modern_envelope_does_not_bypass_host_signature_check(corruption: str) - @pytest.mark.parametrize("has_packet", [False, True]) -def test_envelope_fallback_preserves_typed_failure_with_or_without_packet(has_packet: bool) -> None: +def test_predecision_diagnostic_preserved_without_historical_envelope(has_packet: bool) -> None: from loopx.cli_commands.quota import _project_quota_cli_payload from argparse import Namespace @@ -1023,11 +1023,11 @@ def test_envelope_fallback_preserves_typed_failure_with_or_without_packet(has_pa if has_packet: from loopx.control_plane.effect_runtime import EffectRuntimeRejected with pytest.raises(EffectRuntimeRejected, match="unsupported format: protocol_action_packet"): - _project_quota_cli_payload(failure, Namespace(turn_envelope=True), frozenset(), None) + _project_quota_cli_payload(failure, Namespace(turn_envelope=True, quota_command="should-run"), frozenset(), None) assert failure == before return - rendered = _project_quota_cli_payload(failure, Namespace(turn_envelope=True), frozenset(), None) - assert "interaction_contract must be an object" in rendered.pop("turn_envelope_skipped") + rendered = _project_quota_cli_payload(failure, Namespace(turn_envelope=True, quota_command="should-run"), frozenset(), None) + assert "turn_envelope_skipped" not in rendered assert rendered == before assert failure == before From 9ca580bed7245dd8c50548a6637592026886b065 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:26:29 +0800 Subject: [PATCH 02/37] refactor(turn): isolate original captured Host context restoration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/executor.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index 3094679648..5c1264e93d 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -1434,6 +1434,34 @@ def persist_tail_checkpoint(*, release: bool) -> None: @single_executor_per_turn_lane(execution_payload) +def _prepare_host_decision_capture( + plan: Mapping[str, Any], + request: Mapping[str, Any], + *, + journal_path: Path, + decision: Mapping[str, Any] | None, + journal: Mapping[str, Any] | None, +) -> tuple[dict[str, Any], dict[str, Any]]: + """Restore the original captured input without replacing session recovery. + + Capture validation and binding stay with their existing owner. This IO + adapter only installs that owner's original context into the Host request. + """ + from .decision_capture import prepare_turn_decision_capture + + original_plan = journal["plan"] if journal is not None else plan + captured = prepare_turn_decision_capture( + original_plan, journal_path=journal_path, + decision=decision, existing_journal=journal is not None, + ) + restored = {**request, "turn_envelope": captured["turn_envelope"]} + if isinstance(captured.get("work_contract"), Mapping): + restored["work_contract"] = dict(captured["work_contract"]) + else: + restored.pop("work_contract", None) + return captured, restored + + def run_loopx_turn_once( plan: Mapping[str, Any], *, From 49573d825482606d8b02cd46211aa285fffad63d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:28:48 +0800 Subject: [PATCH 03/37] docs(controller): reconcile independent delivery and historical qualification Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 26 ++++++++++++++++--- ...ller-executor-unified-contract-v0.zh-CN.md | 19 ++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index a9635ed296..34eacc4b84 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -498,11 +498,16 @@ repaired by replacing metadata only for explicitly scoped artifacts, preserving ordinary original-file byte identity. An interruption regression first failed with the preceding capture still selected; publishing current recovery before the optional scope call fixes it. -Standard premerge still fails two checks also reproduced on `8b37000b3`: +At runtime `e1c69289a`, standard premerge failed two checks also reproduced on `8b37000b3`: the existing oversized `run_loopx_turn_once` decision function and stale `turn_route` producer coverage after the earlier owner convergence. The native -increment does not change those files; they remain C1 qualification gaps, -not passing checks or exceptions. +increment did not change those files; that historical result is not a pass or +an exception. The current integration, rebased onto `b69021498`, incorporates +the actual TypeScript producer/scanner repair from [#7520](https://github.com/loopx-project/loopx/pull/7520). +It also isolates original-capture restoration from run-once orchestration at +`fec9cd3af`, preserving the existing capture owner and session recovery. The +size thresholds remain unchanged. Current combined qualification belongs to +the exact integration head's PR evidence, not the old native-slice test counts. Final measurements include the recovery-publication fix at runtime commit `e1c69289a`. Against immutable `8b37000b3` (the preceding C1 plus the same target-main large @@ -1432,6 +1437,21 @@ and retirement, and adoption/full-cost qualification where each boundary is independently reviewable. Record dependencies in the existing tasks and PRs. Splitting delivery does not close C1 or waive §9's original quota-baseline test. +The current author split keeps [#7517](https://github.com/loopx-project/loopx/pull/7517) +as an independent bilingual design refinement and +[#7520](https://github.com/loopx-project/loopx/pull/7520) as an independently +usable route-owner/caller refactor on main. Existing independent proposals +[#7457](https://github.com/loopx-project/loopx/pull/7457) and +[#7468](https://github.com/loopx-project/loopx/pull/7468) retain context-reference +and observation-diagnostic ownership; the cache-clock fix +[#7464](https://github.com/loopx-project/loopx/pull/7464) is already in the +named target main. These boundaries have local rollback; the design change +has no runtime dependency. [#7430](https://github.com/loopx-project/loopx/pull/7430) +remains an unfinished integration candidate and includes prerequisite content +for combination testing. After prerequisites enter main, rebase it again to +remove overlapping diff before cutover handoff. Do not merge two implementations +or present this staging arrangement as complete C1 delivery. + The first **cutover** slice pairs the complete field/clause migration with a real single-session producer and consumer replacement. Start at the shared quota emission boundary and trace both attached heartbeat delivery and diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 4374ab4310..264a2d2f5d 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -370,9 +370,13 @@ TypeScript 和四项真实 Codex `0.163.0-alpha.11.1` 静态 loopback 检查。 四个新增 File/SQLite 范围断言在 `1ac29089f` 失败。集成中发现的恢复引用回归已修复: 只为显式 scoped 产物替换 metadata,普通恢复保留原文件字节身份。 中断回归最初因仍选中前次 capture 而失败;将当前恢复发布前移到可选 scope 调用之前 -后通过。标准 premerge 仍有两项在 `8b37000b3` 同样复现的失败:既有 +后通过。在运行源码 `e1c69289a`,标准 premerge 有两项在 `8b37000b3` 同样复现的失败:既有 `run_loopx_turn_once` 决策函数过大,以及前序 owner 收敛后 `turn_route` 的 producer -覆盖过期。本 native 增量未修改这些文件;它们仍是 C1 资格缺口,不是通过或例外。 +覆盖过期。本 native 增量未修改这些文件;历史结果不是通过或例外。当前集成已 rebase +到 `b69021498`,采用 [#7520](https://github.com/loopx-project/loopx/pull/7520) +对真实 TypeScript producer/scanner 的修复,并在 `fec9cd3af` 将原 capture 恢复从 +run-once 编排中分离,保留既有 capture owner 与 session 恢复。大小阈值不变。当前 +完整组合的资格归精确集成 head 的 PR 证据,不能沿用旧 native 切片的测试计数。 最终运行源码 `e1c69289a` 的成本含上述恢复发布顺序修复;对照不可变 `8b37000b3`(前序 C1 与相同 target-main 大页配置),24 成果/253 要求 来源上,每侧三组温态 cohort、每 provider/阶段 21 样本。温态新增真实 typed 依赖并核 @@ -1047,6 +1051,17 @@ owner。列明后续切换消费者;不能添加无人使用的基础设施或 可按独立可审查的边界拆分,在既有任务和 PR 中记录依赖。拆分交付不代表 C1 完成, 也不豁免第 9 节相对原 quota 基线的验证。 +当前作者拆分把 [#7517](https://github.com/loopx-project/loopx/pull/7517) 作为独立 +双语设计细化,把 [#7520](https://github.com/loopx-project/loopx/pull/7520) 作为可在 +main 独立使用的 route owner/caller 重构。已有独立提案 +[#7457](https://github.com/loopx-project/loopx/pull/7457) 与 +[#7468](https://github.com/loopx-project/loopx/pull/7468) 分别保留上下文引用和观察诊断 +归属;cache 时钟修复 [#7464](https://github.com/loopx-project/loopx/pull/7464) 已进入 +上述目标 main。这些边界可以局部回退,设计改动没有 runtime 依赖。 +[#7430](https://github.com/loopx-project/loopx/pull/7430) 仍是未完成的集成候选,为组合 +测试保留前置内容;前置进入 main 后须再次 rebase,移除重叠 diff,再交付切换评审。 +不能合入两套实现,也不能把这种分阶段安排说成 C1 整体完成。 + 首个**切换**片必须把完整字段/条款迁移与真实单会话 producer、consumer 替换一起交付。 从共享 quota emission 边界开始,同时追踪 attached heartbeat 交付与 Turn adapter, 保留各自真实宿主归属。不能新增无人使用的第三个 serializer,而两个活跃 writer 继续 From 04c6ae33bc2ec0f466ac6db90a7908f7de4a8c3c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:42:27 +0800 Subject: [PATCH 04/37] docs(controller): record target native budget integration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/controller-executor-unified-contract-v0.md | 3 ++- .../rfcs/controller-executor-unified-contract-v0.zh-CN.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 34eacc4b84..9bb22a7b97 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -502,7 +502,8 @@ At runtime `e1c69289a`, standard premerge failed two checks also reproduced on ` the existing oversized `run_loopx_turn_once` decision function and stale `turn_route` producer coverage after the earlier owner convergence. The native increment did not change those files; that historical result is not a pass or -an exception. The current integration, rebased onto `b69021498`, incorporates +an exception. The integration was rebased onto `b69021498`, then onto +`f63647e2e` with the target-main native output-budget behavior retained. It incorporates the actual TypeScript producer/scanner repair from [#7520](https://github.com/loopx-project/loopx/pull/7520). It also isolates original-capture restoration from run-once orchestration at `fec9cd3af`, preserving the existing capture owner and session recovery. The diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 264a2d2f5d..d94cd60c2b 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -372,8 +372,8 @@ TypeScript 和四项真实 Codex `0.163.0-alpha.11.1` 静态 loopback 检查。 中断回归最初因仍选中前次 capture 而失败;将当前恢复发布前移到可选 scope 调用之前 后通过。在运行源码 `e1c69289a`,标准 premerge 有两项在 `8b37000b3` 同样复现的失败:既有 `run_loopx_turn_once` 决策函数过大,以及前序 owner 收敛后 `turn_route` 的 producer -覆盖过期。本 native 增量未修改这些文件;历史结果不是通过或例外。当前集成已 rebase -到 `b69021498`,采用 [#7520](https://github.com/loopx-project/loopx/pull/7520) +覆盖过期。本 native 增量未修改这些文件;历史结果不是通过或例外。集成已先 rebase +到 `b69021498`,再到 `f63647e2e`,保留目标主干的原生输出预算行为,并采用 [#7520](https://github.com/loopx-project/loopx/pull/7520) 对真实 TypeScript producer/scanner 的修复,并在 `fec9cd3af` 将原 capture 恢复从 run-once 编排中分离,保留既有 capture owner 与 session 恢复。大小阈值不变。当前 完整组合的资格归精确集成 head 的 PR 证据,不能沿用旧 native 切片的测试计数。 From 9de0d26f6df07156563bd2b2ecbddf70b7a1e29f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:29:45 +0800 Subject: [PATCH 05/37] refactor(context): compose native scope and encoding in one call Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/quota_capture.py | 10 ++- loopx/control_plane/quota/turn_envelope.ts | 20 ++++- .../turn_driver/codex_context.py | 21 +++-- .../control_plane/codex_checkpoint_fixture.py | 23 +++++ .../test_codex_context_capture.py | 13 ++- .../test_codex_context_lifecycle.py | 30 ++++++- .../test_saved_context_project.py | 87 ++++++++++++------- .../test_work_acceptance_context.py | 2 +- .../goal_outcome_refinement.test.ts | 39 ++++++--- 9 files changed, 185 insertions(+), 60 deletions(-) diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 72eb366d36..5822e41a2d 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -194,9 +194,7 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, if max_page_output_bytes is not None and max_page_output_bytes <= 0: raise QuotaDecisionTransportError("Native context output limit must be positive.") - wire = directory / "delivery.json" - write_decision_capture(wire, projected) - reference = {"path": str(wire), "sha256": hashlib.sha256(wire.read_bytes()).hexdigest()} + reference = None mode, target = "full", None if projected.get("schema_version") == "context_delivery_v1": mode, target = projected["mode"], projected["target"] @@ -206,6 +204,12 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, metadata = {**(metadata or {}), "recovery": projected["recovery"]} if mode == "full": reference = {key: projected["recovery"][key] for key in ("path", "sha256")} + if reference is None: + # Full delivery already pages its immutable recovery. Do not serialize + # a duplicate wire file that no reader or lifecycle receipt consumes. + wire = directory / "delivery.json" + write_decision_capture(wire, projected) + reference = {"path": str(wire), "sha256": hashlib.sha256(wire.read_bytes()).hexdigest()} page_bytes, output_bytes = native_context_page_budget() if max_page_output_bytes is not None: output_bytes = min(output_bytes, max_page_output_bytes) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 18db6bfc4e..412fbf36d9 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -513,6 +513,24 @@ function capturedOutcomeRead(request: JsonObject): JsonObject { return {...common, outcome_ref: ref, outcome}; } +/** One native projection call, after IO has published the current full recovery. + * Reuse both owners without another model wire or retention claim. Full codec + * results already carry the projection; delta/fallback results return it only + * for the caller's immutable required-context capture, never as hidden input. + */ +function nativeContextDelivery(request: JsonObject): JsonObject { + let projection = requireJsonObject(request.payload, "native context source"); + try { projection = capturedOutcomeContext(request); } + catch { /* Optional scope failure retains the original complete source. */ } + try { + const delivery = prepareContextDelivery({...request, context: projection}, LEGACY_QUOTA_CONTEXT_FIELDS); + return delivery.mode === "full" ? {delivery} : {projection, delivery}; + } catch { + // Preserve a successful scope projection when the optional codec fails. + return {projection}; + } +} + /** Reuse the existing settlement authoring owner, never a summary or a chain. */ function replanAuthoringContract(payload: JsonObject): JsonObject { const contract = object(object(payload.replan_action_packet).writeback_contract); @@ -2098,7 +2116,7 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { const envelope = buildTurnEnvelope(request); return {envelope, route: turnEnvelopeRoute(envelope)}; } - if (operation === "outcome_context") return capturedOutcomeContext(request); + if (operation === "native_context_delivery") return nativeContextDelivery(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); if (operation === "context_recovery") return restoreContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 4cfdae8095..0750d647ea 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -516,10 +516,20 @@ def publish_recovery(reference: dict[str, Any]) -> None: from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError try: - projected = _turn_envelope_result("outcome_context", payload=payload, - source_capture={key: recovery[key] for key in ("path", "sha256")}) + prepared = _turn_envelope_result("native_context_delivery", payload=payload, + source_capture={key: recovery[key] for key in ("path", "sha256")}, + **request, surface=surface, capture_id=capture_id) + result = prepared.get("delivery") + projected = prepared.get("projection", result.get("context") if isinstance(result, dict) else None) + if not isinstance(projected, dict): + raise ValueError("native projection result has no complete context") except (ValueError, TypeError, EffectRuntimeRemoteError, EffectRuntimeStartupError): - projected = payload # optional presentation failure retains the complete source + projected = payload + # A failed optional composition can still use the ordinary + # codec; its own failure returns the original full source. + result = project_captured_context(payload, directory=capture_directory, + request=request, captured_recovery=recovery, + goal_id=goal_id, agent_id=agent_id, surface=surface, capture_id=capture_id) if projected.get("schema_version") == "outcome_context_projection_v0": payload = projected recovery = capture_context_projection(payload, directory=capture_directory, @@ -527,10 +537,9 @@ def publish_recovery(reference: dict[str, Any]) -> None: recovery["presentation_schema"] = payload["schema_version"] publish_recovery(recovery) - result = project_captured_context(payload, directory=capture_directory, - request=request, captured_recovery=recovery, - goal_id=goal_id, agent_id=agent_id, surface=surface, capture_id=capture_id) + result = result if isinstance(result, dict) else payload if result.get("schema_version") == "context_delivery_v1": + result["recovery"] = recovery state["recovery"]["target"] = result["target"] state["pending"].update(target=result["target"], ancestry=result["ancestry"]) _save(root, state) diff --git a/tests/control_plane/codex_checkpoint_fixture.py b/tests/control_plane/codex_checkpoint_fixture.py index daeade632b..c58281dbc9 100644 --- a/tests/control_plane/codex_checkpoint_fixture.py +++ b/tests/control_plane/codex_checkpoint_fixture.py @@ -137,6 +137,29 @@ def native_context_command(request, *, todo_id=None): argv += ["--todo-id", todo_id] return shlex.join(argv) +def native_capture_wire(descriptor, *, recovery=None): + """Independent saved-byte oracle; full delivery needs no duplicate wire file. + + Native requests must still consume every returned page. This fixture reads + the exact capture separately to verify those actual model-visible bytes. + """ + import hashlib + from pathlib import Path + from loopx.control_plane.quota.turn_envelope import prepare_context_delivery + + raw = Path(descriptor["path"]).read_bytes() + assert hashlib.sha256(raw).hexdigest() == descriptor["sha256"] + if descriptor["mode"] != "full": + return json.loads(raw) + target = descriptor["target"] + wire = prepare_context_delivery(json.loads(raw), + scope={key: target[key] for key in ("goal_id", "agent_id", "session_id", "context_epoch", "continuity_generation") + if key in target}, capture_id=target["capture_id"], surface=target["surface"]) + assert wire["target"] == target + wire["recovery"] = recovery if recovery is not None else descriptor["recovery"] + return wire + + @contextmanager def local_inference_provider(response_builder=None, usage_builder=None): """Static loopback Responses stream, retaining the actual native model input.""" diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 71e26db36d..2c7f1b4dce 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -21,6 +21,7 @@ from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _run_cli, _spend_run_count, _write_fixture from tests.control_plane.codex_checkpoint_fixture import ( local_inference_provider, delivered_context as _delivered, last_context_page as _last_page, + native_capture_wire, ) from tests.control_plane.test_heartbeat_envelope_transport import private_handoff # noqa: F401 -- shared private socket fixture from tests.control_plane.test_codex_context_boundary import _app_server, _configure_provider, _input_text, _native_fixture @@ -325,16 +326,20 @@ def response(request, number, message): assert len(json.dumps(descriptor, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= (native_guard_output_bytes or 64000) read_commands[name] = descriptor["command"] # The public native CLI now shares context-project's paged reader. - # Keep the independent full-wire oracle separate from its page view. - wire_file = Path(descriptor["path"]).parent / "delivery.json" - wires[name] = json.loads(wire_file.read_text()) + # Keep saved-byte/state oracles separate from actual page input. + # Full views page their existing capture; only deltas need a wire. + context_root = home / "loopx-context" if host == "benchmark" else lifecycle + if shared_root: + context_root /= hashlib.sha256(descriptor["target"]["session_id"].encode()).hexdigest() + recovery = json.loads((context_root / "session.json").read_bytes())["recovery"] + wires[name] = native_capture_wire(descriptor, recovery=recovery) assert descriptor["mode"] == wires[name]["mode"] == ("full" if name == "cold" or resume_loss else "delta") assert "Native input observation independently verifies" not in descriptor["instruction"] if name == "warm" and not resume_loss: assert json.dumps(wires[name]["instruction"]).encode() in descriptor["content"].encode() capture = Path(wires[name]["recovery"]["path"]).parent / "decision.json" snapshots.append((capture, capture.read_bytes())) - read_paths[name] = Path(wires[name]["recovery"]["path"]) if wires[name]["mode"] == "full" else wire_file + read_paths[name] = Path(descriptor["path"]) stages["pages"] = ([descriptor["next_command"]] if not descriptor["eof"] else []) if bounded_pages else pages(name, read_paths[name]) page_digests[name] = descriptor["sha256"] stages["phase"] = name + "_read" diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index fda0a1e04f..6ea235d9d8 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -155,6 +155,34 @@ def deliver(root, event, pages): "tool_use_id": f"page-{page.get('offset')}", "tool_response": json.dumps(page)}) == {} +def test_native_projection_uses_one_boundary_and_pages_its_existing_full_capture(tmp_path, monkeypatch): + from loopx.cli_commands.quota_capture import native_context_capture_view + from loopx.control_plane.quota import turn_envelope + + setup(tmp_path, monkeypatch) + operation = turn_envelope._turn_envelope_result + calls = [] + + def counted(name, **request): + calls.append(name) + return operation(name, **request) + + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", counted) + payload, wire = project(tmp_path) + # One source/projection boundary, rather than a scope request followed by + # another encoding request for the same current capture. + assert len(calls) == 1 + assert wire["mode"] == "full" and wire["context"] == payload + recovery = wire["recovery"] + original = Path(recovery["path"]).read_bytes() + view = native_context_capture_view(wire, Path(recovery["path"]).parent, + context_directory=str(tmp_path)) + assert view["path"] == recovery["path"] and view["sha256"] == recovery["sha256"] + assert not (Path(view["path"]).parent / "delivery.json").exists() + assert Path(view["path"]).read_bytes() == original + assert json.loads(view["content"]) == payload and view["eof"] is True + + def test_scope_projection_interruption_keeps_current_original_recovery(tmp_path, monkeypatch): from loopx.control_plane.quota import turn_envelope @@ -169,7 +197,7 @@ class ScopeInterrupted(BaseException): pass def interrupt(name, **request): - if name == "outcome_context": + if name == "native_context_delivery": observed.append(context._state(tmp_path)) raise ScopeInterrupted() return operation(name, **request) diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py index c155e068e2..b3c3e2d690 100644 --- a/tests/control_plane/test_saved_context_project.py +++ b/tests/control_plane/test_saved_context_project.py @@ -197,12 +197,16 @@ def test_saved_projection_first_page_continues_exact_bytes_and_keeps_restart(tmp @pytest.mark.parametrize("failure", ["wire", "inline_read"]) def test_saved_projection_wire_failure_keeps_committed_current_recovery(tmp_path, monkeypatch, failure): from loopx.cli import build_parser - from loopx.cli_commands.quota_capture import project_saved_context_capture + from loopx.cli_commands.quota_capture import project_saved_context_capture, native_context_capture_view, read_context_capture from loopx.control_plane.turn_driver import codex_context, decision_capture path, root, context = tmp_path / "original.json", tmp_path / "cache", tmp_path / "native" original = {"ok": True, "goal_id": "saved-goal", "agent_identity": {"agent_id": "saved-agent"}, "heartbeat_receipt": {"turn_instance_id": "saved-turn"}, "requirement": "Current complete requirements."} + if failure == "wire": + # Keep current controls inline; a large reusable source makes this a + # genuine delta-wire failure rather than a full-view fixture. + original["plan_summary"] = {"requirements": original["requirement"] * 800} raw = json.dumps(original).encode() path.write_bytes(raw) root.mkdir(mode=0o700) @@ -211,10 +215,25 @@ def test_saved_projection_wire_failure_keeps_committed_current_recovery(tmp_path monkeypatch.chdir(tmp_path) codex_context.codex_context_hook(str(context), {"hook_event_name": "SessionStart", "source": "startup", "session_id": "native-session", "cwd": str(tmp_path)}) + if failure == "wire": + previous = root / "previous" + previous.mkdir(mode=0o700) + prior = codex_context.project_codex_context({**original, "heartbeat_receipt": {"turn_instance_id": "prior-turn"}}, + directory=str(context), capture_directory=previous, goal_id="saved-goal", agent_id="saved-agent", + surface="quota_packet", capture_id="prior-capture") + page = native_context_capture_view(prior, previous, context_directory=str(context)) + while True: + codex_context.codex_context_hook(str(context), {"hook_event_name": "PostToolUse", + "session_id": "native-session", "cwd": str(tmp_path), "tool_response": page}) + if page["eof"]: + break + page = read_context_capture(build_parser().parse_args(shlex.split(page["next_command"])[1:])) write_capture = decision_capture.write_decision_capture + failures = [] def fail_optional_wire(path, payload, **kwargs): if failure == "wire" and path.name == "delivery.json": + failures.append(path) raise OSError("optional wire unavailable") return write_capture(path, payload, **kwargs) @@ -230,6 +249,7 @@ def fail_optional_wire(path, payload, **kwargs): result = project_saved_context_capture(args) assert result["ok"] and result["mode"] == "full" if failure == "wire": + assert len(failures) == 1 # Exercise the remaining delta-wire IO failure. assert result["path"] == str(path) and result["sha256"] == digest else: assert "content" not in result and result["recovery"]["sha256"] == digest @@ -422,9 +442,9 @@ def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, mo from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery - from tests.control_plane.codex_checkpoint_fixture import local_inference_provider + from tests.control_plane.codex_checkpoint_fixture import local_inference_provider, native_capture_wire from tests.control_plane.test_codex_context_boundary import _app_server, _configure_provider, _native_fixture, _input_text - from tests.control_plane.test_codex_context_capture import _delivered + from tests.control_plane.test_codex_context_capture import _delivered, _last_page from tests.control_plane.test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, TODO_ID, _configure_selectable_alternative, _write_fixture, _spend_run_count, _heartbeat_receipt_count, @@ -505,18 +525,15 @@ def response(request, number, message): assert descriptor["ok"] and descriptor["mode"] == ("full" if name == "cold" else "delta") if host == "benchmark": assert len(json.dumps(descriptor, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 32000 - wire = json.loads((Path(descriptor["path"]).parent / "delivery.json").read_text()) + wire = native_capture_wire(descriptor) assert descriptor["target"] == wire["target"] observed[name] = (wire, descriptor) - data = Path(descriptor["path"]).read_bytes() # The existing page protocol also admits the projector's first # page. Old installed projectors still start with command. - offset = descriptor["next_offset"] if "content" in descriptor else 0 - for offset in range(len(data) if offset is None else offset, len(data), 4096): - # Synthetic captures are ASCII JSON; public reader owns every byte. - queue.append(shlex.join([*cli, "--format", "json", "quota", "context-read", - "--capture-file", descriptor["path"], "--capture-sha256", descriptor["sha256"], - "--offset", str(offset)])) + if "content" not in descriptor: + queue.append(descriptor["command"]) + elif not descriptor["eof"]: + queue.append(descriptor["next_command"]) phase = name + "_read" if not queue: return response(request, number, message) @@ -524,27 +541,32 @@ def response(request, number, message): else: name = phase.removesuffix("_read") descriptor = observed[name][1] - assert _delivered(request, descriptor["sha256"]) == Path(descriptor["path"]).read_text().rstrip("\n") - if host == "benchmark" and name in ("cold", "warm"): - # Consume the command actually delivered to the native - # worker, rather than manually supplying missing flags. - presented = json.loads(Path(observed[name][0]["recovery"]["path"]).read_text()) - selection = presented["interaction_contract"]["cli_channel"]["selection_command"] - command = selection["route_prefix"] + " " + selection["command_args_template"].replace("{todo_id}", TODO_ID) - argv = shlex.split(command) - assert argv[argv.index("--context-delivery") + 1] == "on" - assert argv[argv.index("--codex-context-dir") + 1] == str(home / "loopx-context") - assert argv.count("--decision-output-root") == 1 - assert argv[argv.index("--max-page-output-bytes") + 1] == "32000" - phase = name + "_bound_ready" - elif name in ("warm", "warm_bound"): - phase = "done" - return message + page = _last_page(request) + assert page and page["ok"] and page["sha256"] == descriptor["sha256"] + if not page["eof"]: + command = page["next_command"] else: - # A new worker wake owns its own original guard. - phase = "warm" - if host == "benchmark": + assert _delivered(request, descriptor["sha256"]) == Path(descriptor["path"]).read_text().rstrip("\n") + if host == "benchmark" and name in ("cold", "warm"): + # Consume the command actually delivered to the native + # worker, rather than manually supplying missing flags. + presented = json.loads(Path(observed[name][0]["recovery"]["path"]).read_text()) + selection = presented["interaction_contract"]["cli_channel"]["selection_command"] + command = selection["route_prefix"] + " " + selection["command_args_template"].replace("{todo_id}", TODO_ID) + argv = shlex.split(command) + assert argv[argv.index("--context-delivery") + 1] == "on" + assert argv[argv.index("--codex-context-dir") + 1] == str(home / "loopx-context") + assert argv.count("--decision-output-root") == 1 + assert argv[argv.index("--max-page-output-bytes") + 1] == "32000" + phase = name + "_bound_ready" + elif name in ("warm", "warm_bound"): + phase = "done" return message + else: + # A new worker wake owns its own original guard. + phase = "warm" + if host == "benchmark": + return message return response(request, number, message) if host == "app_code_mode": return {"type": "custom_tool_call", "id": f"fc_{number}", "call_id": f"call_{number}", @@ -596,9 +618,10 @@ def response(request, number, message): else: _configure_provider(home, url) config = home / "config.toml" - overrides = "".join(f"{key}={json.dumps(value)}\n" + config.write_text("".join(key + "=" + json.dumps(value) + "\n" for key, value in configuration["config_overrides"].items()) - config.write_text(overrides + config.read_text()) + + "developer_instructions=" + json.dumps(configuration["developer_instructions"]) + + "\n" + config.read_text()) with (home / "config.toml").open("a") as config: config.write("[sandbox_workspace_write]\nnetwork_access=true\n") if host == "app_code_mode": diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index 1205be5d33..13b059ba73 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -259,7 +259,7 @@ def consume(page): original_operation = turn_envelope._turn_envelope_result def unavailable(operation, **request): - if operation == "outcome_context": + if operation == "native_context_delivery": raise ValueError("synthetic optional scope projection failure") return original_operation(operation, **request) diff --git a/tests/control_plane_ts/goal_outcome_refinement.test.ts b/tests/control_plane_ts/goal_outcome_refinement.test.ts index 7db79edf4a..19f222536a 100644 --- a/tests/control_plane_ts/goal_outcome_refinement.test.ts +++ b/tests/control_plane_ts/goal_outcome_refinement.test.ts @@ -186,10 +186,17 @@ function outcomeContextPacket(linkPeer = false): JsonObject { }}}}; } +function nativeProjection(payload: JsonObject, source_capture: JsonObject): JsonObject { + const prepared = evaluateTurnEnvelope({operation: "native_context_delivery", payload, source_capture, + scope: {goal_id: goal, agent_id: "author", session_id: "session", context_epoch: "epoch"}, + capture_id: "reading", surface: "quota_packet"}); + return (prepared.projection ?? (prepared.delivery as JsonObject).context) as JsonObject; +} + test("scoped model context retains canonical source, global controls and exact optional detail", () => { const payload = outcomeContextPacket(), before = structuredClone(payload); const source_capture = {path: "/capture/original.json", sha256: "a".repeat(64)}; - const context = evaluateTurnEnvelope({operation: "outcome_context", payload, source_capture}); + const context = nativeProjection(payload, source_capture); assert.equal(context.schema_version, "outcome_context_projection_v0"); assert.equal((context.read_scope as JsonObject).scope_applied, true); assert.deepEqual((context.context as JsonObject).goal_boundary, payload.goal_boundary); @@ -210,33 +217,41 @@ test("scoped model context retains canonical source, global controls and exact o assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: altered, source_payload: payload, outcome_ref: ""}), /differs from its original source/); assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: context, outcome_ref: ""}), /original outcome source/); const broad = {...payload, autonomous_replan_obligation: {id: "whole-goal"}}; - assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: broad, source_capture}), broad); + assert.deepEqual(nativeProjection(broad, source_capture), broad); const unknown = outcomeContextPacket(); const content = (((((unknown.interaction_contract as JsonObject).agent_channel as JsonObject).work_context as JsonObject).sources as JsonObject[])[0]).content as JsonObject; const acceptance = content.goal_acceptance_contract as JsonObject; (((acceptance.outcome_requirements as JsonObject).outcomes as JsonObject[])[1]).future_obligation = "Unknown cross-Agent constraint"; // Even a plan bound to that complete source must not hide unknown semantics. (content.outcome_context as JsonObject).acceptance_digest = canonicalAuthoritySha256(acceptance); - assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: unknown, source_capture}), unknown); + assert.deepEqual(nativeProjection(unknown, source_capture), unknown); const unread = outcomeContextPacket(); (unread.interaction_contract as JsonObject).agent_channel.required_reads = [{source: "goal_acceptance", command: "pending-read"}]; - assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: unread, source_capture}), unread); + assert.deepEqual(nativeProjection(unread, source_capture), unread); const future = outcomeContextPacket(); future.interaction_contract.agent_channel.work_context.sources[0].reason += " Retain a future instruction."; - assert.deepEqual(evaluateTurnEnvelope({operation: "outcome_context", payload: future, source_capture}), future); + assert.deepEqual(nativeProjection(future, source_capture), future); + assert.deepEqual(nativeProjection(payload, {...source_capture, sha256: "invalid"}), payload, + "scope failure retains the original source before the ordinary codec"); + const failedCodec = evaluateTurnEnvelope({operation: "native_context_delivery", payload, source_capture, + scope: {goal_id: goal, agent_id: "author", session_id: "session", context_epoch: "epoch"}, + capture_id: "reading", surface: "unsupported"}); + assert.deepEqual(failedCodec.projection, context, "codec failure retains the complete required projection"); + assert.equal(failedCodec.delivery, undefined); }); test("warm scope expansion delivers previously unread requirements; lost baseline recovers the same required input", () => { - const project = (payload: JsonObject, letter: string) => evaluateTurnEnvelope({operation: "outcome_context", payload, - source_capture: {path: `/capture/${letter}.json`, sha256: letter.repeat(64)}}); - const before = project(outcomeContextPacket(), "a"), after = project(outcomeContextPacket(true), "b"); const scope = {goal_id: goal, agent_id: "author", session_id: "same-session", context_epoch: "same-epoch"}; - const baseline = evaluateTurnEnvelope({operation: "context_delivery", context: before, scope, capture_id: "first", surface: "quota_packet"}); - const delivery = evaluateTurnEnvelope({operation: "context_delivery", context: after, scope, capture_id: "second", surface: "quota_packet", - baseline, retained: {complete: true, target: baseline.target}}); + const project = (payload: JsonObject, letter: string, extra: JsonObject = {}) => evaluateTurnEnvelope({ + operation: "native_context_delivery", payload, scope, capture_id: letter, surface: "quota_packet", + source_capture: {path: `/capture/${letter}.json`, sha256: letter.repeat(64)}, ...extra}); + const first = project(outcomeContextPacket(), "a"), baseline = first.delivery as JsonObject; + assert.equal(first.projection, undefined, "full delivery already contains the exact projection"); + const next = project(outcomeContextPacket(true), "b", {baseline, retained: {complete: true, target: baseline.target}}); + const after = next.projection as JsonObject, delivery = next.delivery as JsonObject; assert.equal(delivery.mode, "delta"); assert.ok(JSON.stringify(delivery).includes("Unique peer requirement"), "a machine-only canonical source is not retained model context"); - const current = evaluateTurnEnvelope({operation: "context_delivery", context: after, scope, capture_id: "second", surface: "quota_packet"}); + const current = evaluateTurnEnvelope({operation: "context_delivery", context: after, scope, capture_id: "b", surface: "quota_packet"}); const restored = evaluateTurnEnvelope({operation: "context_recovery", delivery, baseline, current}); assert.deepEqual(restored.context, after); const lost = evaluateTurnEnvelope({operation: "context_recovery", delivery, current}); From 56c3cbe4b2c93b2505f7050db5aa809f7f5a7241 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:36:21 +0800 Subject: [PATCH 06/37] docs(context): record native composition cost and recovery boundaries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 33 ++++++++++++++++--- ...ller-executor-unified-contract-v0.zh-CN.md | 27 ++++++++++++--- docs/heartbeat-automation-prompt.md | 4 +++ docs/reference/protocols/turn-envelope-v0.md | 12 +++++++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 9bb22a7b97..6fbefb937f 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -465,8 +465,9 @@ The native candidate and compaction recovery point to the exact required model artifact, rather than the machine's full canonical source. The existing delta codec therefore delivers a newly required body's actual text; it cannot borrow an unread optional body from a hidden baseline. The current full-source recovery -reference is published before optional scope composition, then replaced with -the exact required artifact before encoding. An interruption during composition +reference is published before optional scope/codec composition. The shared typed +owner computes both in one call; before exposing any result, IO commits the exact +required artifact and switches recovery to it. An interruption during composition therefore cannot leave the preceding capture selected for recovery. Full page coverage remains an explicit native-hook continuity assumption, not model-retention proof. Optional @@ -483,8 +484,8 @@ real-provider owner calls against the prior implementation measured File median metadata added 405/415 serialized bytes. This isolates the acceptance owner in one TypeScript process, excluding bridge, CLI, cold startup and model costs. That foundation added cost before the native scope cutover; it was not an -efficiency win. The native presentation additionally allocates an exact required -artifact and calls the same typed owner once before the existing codec. These +efficiency win. At the scope-cutover checkpoint, native presentation additionally +allocates an exact required artifact and calls the same typed owner once before the existing codec. These costs must be included with page, detail and recovery costs; a smaller first input does not establish a net benefit. @@ -525,7 +526,7 @@ serialized page output for File cold/warm/recovery is | Lost-context full recovery | 109.126→61.425 | 109.725→56.777 | | Index plus one optional complete detail | 96.918→108.557 | 92.460→109.251 | -The additional scope RPC remains: warm bridge calls rise from two to three. +In that measured version, the additional scope RPC remains: warm bridge calls rise from two to three. File warm artifact files total 131,020→160,192 serialized bytes; the index/detail operation still uses two calls and now revalidates the derived presentation. Cold and recovery use one page instead of two. These measurements cover Python @@ -535,6 +536,28 @@ child CPU, model/cache tokens, billing, human/setup work and accepted outcomes. Cold/recovery transport improves while warm and detail medians regress. This is not the original-quota comparison or a positive complete-cost verdict. +At `85c235d29`, the native caller combines scope and encoding through the same +owners in one request and removes the unused full-wire copy. The former standalone +scope operation is retired; ordinary codecs and captured-detail readers remain. +Against `307d000a3`, the same frozen File/SQLite observations and three interleaved +warm cohorts per arm (21 samples per provider/phase) measure: + +| Phase | File median/p95, before→after ms | SQLite median/p95, before→after ms | +| --- | --- | --- | +| Cold full input | 37.438/60.718→28.516/38.941 | 34.263/39.924→26.966/39.054 | +| Warm scope expansion | 42.065/58.640→34.813/45.922 | 39.869/49.047→33.856/45.593 | + +Including page-lifecycle observation, these phases now use two rather than three +bridge calls. File cold artifact bytes fall 162,877→129,299; warm artifacts remain +160,192. Complete page output is unchanged. Six same-path ordinary/scoped/broad +File/SQLite comparisons preserve complete public pages and recovery values; +the one-call regression assertion fails on the baseline. Warm response payload +grows by 27 bytes despite lower request traffic. Detail/recovery keep their call +counts; SQLite detail p95 worsens 138.131→175.541 ms without a changed code path. +Machine load remains uncontrolled and the earlier exclusions still apply. +This removes demonstrated local transport/IO cost, not the original-quota +outcome/full-cost gap, short-source growth or all warm artifact overhead. + Small sources provide a counterexample to size savings: four real-entry File/SQLite fixtures with two outcomes and four requirements grow from 36,293–40,548 to 37,948–42,203 bytes, exactly 1,655 bytes each. Index and scope diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index d94cd60c2b..e97698a1fc 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -349,8 +349,9 @@ acceptance read 或不认识的来源指导均保持完整交付。既有 intera native candidate 与压缩恢复指向真正交付的必读模型产物,而不是机器保存的完整 canonical 来源。既有 delta codec 因而会交付新增必读正文,不能从隐藏 baseline 借用 -模型未读过的可选正文。当前完整来源的恢复引用先于可选 scope 组合发布,再在编码前 -替换为精确必读产物;组合期间中断不能让恢复仍选中前一次 capture。完整分页覆盖仍只是 +模型未读过的可选正文。当前完整来源的恢复引用先于可选 scope/codec 组合发布;共享 +typed owner 在一次调用中计算两者,IO 在暴露任何结果前落盘精确必读产物并切换恢复 +引用。组合期间中断不能让恢复仍选中前一次 capture。完整分页覆盖仍只是 显式 native-hook 连续性假设,不证明模型 保留。可选详情复用同一原来源 registry,重验文件和派生展示,不确认完整 capture 保留。transport 关闭和旧 capture 保持原行为;其他 Host adapter、正式 App 采用、 @@ -361,7 +362,7 @@ canonical 来源。既有 delta codec 因而会交付新增必读正文,不能 15.267→17.580 和 6.504→9.412 ms。原响应字段全等,计划元数据增加 405/415 个 序列化字节。此对照仅隔离同一 TypeScript 进程的 acceptance owner,不含 bridge、 CLI、冷启动或模型成本;此基础在 native 范围切换之前增加成本,不是效率提升。 -native 展示还增加精确必读产物及既有 codec 之前的一次 typed owner 调用,必须与 +在范围切换检查点,native 展示还增加精确必读产物及既有 codec 之前的一次 typed owner 调用,必须与 分页、补读和恢复成本一起计入;首包缩小不证明净收益。 native 切片通过 406 项真实入口 Python(五项显式 native/SDK 案例未资格)、169 项 @@ -390,12 +391,30 @@ run-once 编排中分离,保留既有 capture owner 与 session 恢复。大 | 上下文丢失后全文恢复 | 109.126→61.425 | 109.725→56.777 | | 索引加一项可选全文 | 96.918→108.557 | 92.460→109.251 | -新增 scope RPC 仍在,温态 bridge 调用由二增三;File 温态产物文件共 +在该次测量版本中,新增 scope RPC 仍在,温态 bridge 调用由二增三;File 温态产物文件共 131,020→160,192 个序列化字节。索引/详情仍为两调用,但增加派生展示重验;冷态和恢复由两页 变一页。测量含温态 runtime 下 Python native 投影、分页与生命周期,未控制机器负载; 不含首次验收收集、进程启动、子进程 CPU、模型/缓存 tokens、账单、人工/实施及验收 成果。冷态/恢复传输改善,温态及详情中位数退化;这不是原 quota 对照或完整成本正向结论。 +在 `85c235d29`,native 调用方经同一批 owner 在一次请求中组合 scope 与编码,并删除 +未被使用的全文 wire 副本;原独立 scope operation 已退役,普通 codec 和 capture +详情 reader 保留。相对 `307d000a3`,使用相同冻结 File/SQLite 观察、每侧三组交错 +温态 cohort(每 provider/阶段 21 样本)测得: + +| 阶段 | File 中位/p95,原→新 ms | SQLite 中位/p95,原→新 ms | +| --- | --- | --- | +| 冷态全文输入 | 37.438/60.718→28.516/38.941 | 34.263/39.924→26.966/39.054 | +| 温态范围扩大 | 42.065/58.640→34.813/45.922 | 39.869/49.047→33.856/45.593 | + +计入分页生命周期观察,两阶段 bridge 调用均由三减二。File 冷态产物字节由 +162,877 降至 129,299,温态仍为 160,192;完整分页输出不变。六组同路径的 +普通/scoped/广域 File/SQLite 对照保留完整公开分页与恢复值,单调用回归断言在 +基线失败。温态请求流量降低,但响应 payload 增加 27 字节。详情/恢复调用数不变; +SQLite 详情 p95 在未改代码路径下由 138.131 退化到 175.541 ms。机器负载仍未控制, +前述成本排除项仍适用。这去除了已证明的局部传输/IO 成本,不闭合原 quota 的成果/ +完整成本缺口、短来源增长或全部温态产物开销。 + 小来源给出体积节省的反例:四个真实入口 File/SQLite 夹具各含两成果、四要求, 由 36,293–40,548 增至 37,948–42,203 字节,每项恰好增加 1,655 字节,索引和范围 元数据超过短可选正文。提案不从大夹具推导语义省略规则或生产体积阈值。 diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 0b03335271..978239642c 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -453,6 +453,10 @@ In a supported run mode, a missing or invalid continuity basis still delivers the same original full capture. The current recovery reference is saved before optional delta computation, so an oversized warm request or failed candidate update cannot select an older capture after compaction or resume. +The unified-contract C1 candidate combines outcome scope and encoding in that +same typed call, commits the exact required artifact before publication, and +pages existing full captures without writing an unused duplicate wire file. +This candidate optimization does not qualify installed adoption or outcome gains. If the current recovery cannot be saved, the old session lookup is invalidated; use the explicit original capture reference. If preparation fails before that can be established, or invalidation also fails, the CLI reports diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 3a10b0bd83..a750a8735d 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -18,6 +18,13 @@ also covers the direct Turn producer and final host prompts. Quota CLI output parity alone does not qualify those consumers. Historical v0 decoding remains separate from switching new admissions to a future unified version. +The C1 native candidate composes outcome-reading scope and full/delta encoding +in one call to the existing typed owner. IO publishes current original recovery +before that call and commits any scoped required artifact before publishing the +result. Full views page their existing immutable capture; only deltas need a +separate encoded wire file. This changes neither the model-facing page contract +nor the historical reader, default activation, execution or settlement authority. + 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; 历史捕获保留原 schema 与签名。本次文档更新不新增 CLI 开关。 @@ -27,6 +34,11 @@ RFC 的 [C1 消费链替换](../../architecture/rfcs/controller-executor-unified 还覆盖直接生产契约的 Turn 入口和最终 host prompt;仅 quota CLI 输出等价不能验收 这些消费者。历史 v0 解码与新准入切换到未来统一版本分别处理。 +C1 的 native 候选在既有 typed owner 的一次调用中组合成果读取范围与 full/delta +编码。IO 在调用前发布当前原始恢复引用,并在发布结果前落盘所需的 scoped 必读 +产物。全文视图直接分页已有不可变 capture,只有 delta 需要额外编码文件;模型侧 +分页契约、历史 reader、默认激活、执行及结算权限均不改变。 + Preview it explicitly: ```bash From e8c6d6b098a6964ceba9afbf3dc9edd63010fc1d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:12:23 +0800 Subject: [PATCH 07/37] refactor(turn): read execution facts from the complete source Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 27 +++++---- ...ller-executor-unified-contract-v0.zh-CN.md | 18 +++--- docs/reference/protocols/turn-envelope-v0.md | 11 ++++ loopx/cli_commands/turn_run_once.py | 30 ++++------ loopx/cli_commands/turn_todo_writeback.py | 29 +++++++++- loopx/control_plane/quota/turn_envelope.ts | 47 +++++++++++----- .../work_items/interaction_contract.ts | 12 +++- .../test_complete_turn_decision.py | 21 +++++++ tests/control_plane_ts/turn_envelope.test.ts | 56 +++++++++++++++++++ 9 files changed, 196 insertions(+), 55 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 6fbefb937f..39b4beea5f 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -849,17 +849,24 @@ also failed on the prior baseline; their repaired oracle retains changed-task-te and historical pre-source acceptance cases on real File/SQLite stores. Completion task-body resolution now lives with the existing TypeScript interaction -source owner. New `run-once` requests validate the complete capture against the -internal adapter, then resolve and hash the complete contract's admitted source; -they no longer reconstruct that source from the adapter. Historical journals use -the same resolver through a thin Python transport. Exact Unicode/whitespace bytes, -missing/ambiguous-source refusals and accepted-receipt recovery remain covered. -This retires one Python semantic algorithm without introducing a second packet -view. The old 48-case File/SQLite capture/completion selection passes at +source owner. The same coarse read now supplies `run-once` with the original +selected work, task-body commitment, admitted action, replan and writeback +conditions. Complete-source validation reuses its already-checked effect +projection within that call; it adds neither another RPC nor a model-facing +contract. Adaptive primary selection and ordinary selection share the existing +interaction owner. The CLI no longer reads these execution facts from the +historical adapter; it still transports that adapter for source/signature checks +and consumers not yet migrated. Omitted complete contracts retain the old journal +decoder; supplied null or invalid sources never fall back. Exact Unicode and +whitespace bytes, missing/ambiguous-source refusals, original settlement identity +and accepted-receipt recovery remain protected. The old 48-case File/SQLite +capture/completion selection passes at `599cf1f72`; candidate complete-source checks also reject altered bindings and -explicit null inputs. This adds a TypeScript boundary call where the historical -resolver was local Python; net full-path cost remains unmeasured. Result profiles, -evidence interpretation and other settlement consumers still need migration. +explicit null inputs. The initial task-hash migration added a TypeScript boundary +where the historical resolver was local Python; this broader read reuses that +boundary rather than canceling its cost. Full adapter retirement, remaining +effect/material consumers and original-quota outcome/full-cost qualification +remain open. The Codex result schema and shared candidate adapter now use one checked complete-contract profile read for evidence applicability. They no longer decide diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index e97698a1fc..a95fa3c181 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -625,14 +625,16 @@ profile 判定与结算仍须迁移。 未带来源凭据但已经接受完成的恢复场景。 完成任务正文的解析现由既有 TypeScript interaction 来源 owner 统一负责。 -新 `run-once` 请求先校验完整 capture 与内部 adapter 的绑定,再解析并计算完整 -契约中已准入正文的摘要,不再从 adapter 重建该来源。历史 journal 经 Python -薄 transport 使用同一解析规则,保留 Unicode/空白原始字节、缺失/重复来源拒绝 -及已接受完成回执的恢复。此次删除一套 Python 语义算法,没有新增第二种 packet -视图。迁移前 `599cf1f72` 的 48 项 File/SQLite 捕获及完成检查通过;候选完整来源 -检查也拒绝被修改的 binding 和显式 null 输入。历史解析原为本地 Python,现在 -增加一次 TypeScript 边界调用;完整路径净成本尚未测量。结果 profile、证据解释 -和其余结算消费者仍须继续迁移。 +同一次粗粒度读取现在向 `run-once` 提供原始选定工作、正文摘要、已准入动作、replan +与写回条件。完整来源校验在本次调用内复用已核对的 effect 投影,不新增 RPC 或面向 +模型的契约。自适应 primary 与普通选择共用既有 interaction owner。CLI 不再从历史 +adapter 读取这组执行事实,仍传递该 adapter 供来源/签名校验及尚未迁移的消费者使用。 +缺省完整契约保留旧 journal decoder;显式 null 或无效来源不回退。Unicode/空白原始 +字节、缺失/重复来源拒绝、原结算身份及已接受完成回执恢复继续受保护。 +迁移前 `599cf1f72` 的 48 项 File/SQLite 捕获及完成检查通过;候选完整来源检查也 +拒绝被修改的 binding 和显式 null 输入。最初正文摘要迁移将本地 Python 解析变为 +一次 TypeScript 边界调用;本次扩展读取复用该边界,不抵销原新增成本。 +完整 adapter 退役、其余 effect/material 消费者及原 quota 成果/完整成本资格仍开放。 Codex 结果 schema 与共享 candidate adapter 现通过同一经过校验的完整契约 profile 读取判断证据要求是否适用;新请求不再从内部 capsule 决定该要求。 diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index a750a8735d..63e2bd9903 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -24,6 +24,13 @@ before that call and commits any scoped required artifact before publishing the result. Full views page their existing immutable capture; only deltas need a separate encoded wire file. This changes neither the model-facing page contract nor the historical reader, default activation, execution or settlement authority. +The candidate's `turn run-once` also reads selection, task commitment, admitted +action and replan/writeback conditions together from the checked complete source. +It reuses the task-source boundary call and existing effect projection. The old +adapter remains for integrity checks and unmigrated consumers, with its execution +decoder used only for requests that omit the complete contract. Invalid supplied +contracts never fall back. This does not qualify full adapter retirement or +outcome/full-cost benefit over original quota. 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; @@ -38,6 +45,10 @@ C1 的 native 候选在既有 typed owner 的一次调用中组合成果读取 编码。IO 在调用前发布当前原始恢复引用,并在发布结果前落盘所需的 scoped 必读 产物。全文视图直接分页已有不可变 capture,只有 delta 需要额外编码文件;模型侧 分页契约、历史 reader、默认激活、执行及结算权限均不改变。 +候选 `turn run-once` 也在同一次经过校验的完整来源读取中获得选定工作、正文摘要、 +已准入动作及 replan/写回条件,复用正文来源的边界调用与既有效果投影。旧 adapter +仍供完整性校验和未迁移消费者使用;只有缺省完整契约的请求使用旧执行 decoder, +显式无效契约不回退。完整 adapter 退役及优于原 quota 的成果/完整成本收益仍未资格。 Preview it explicitly: diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index 20e6c16c55..3ad467d22a 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -41,7 +41,6 @@ inspect_loopx_turn_journal, run_codex_cli_host, run_loopx_turn_once, - selected_turn_todo, ) from ..quota import spend_quota_slot from ..state_refresh import refresh_state_run @@ -52,7 +51,7 @@ from .turn_dsh_host import build_dsh_host_runner, build_dsh_operation_host_runner from .turn_rendering import build_turn_error_payload from .turn_post_writeback import drain_committed_turn_post_writeback_hooks -from .turn_todo_writeback import captured_todo_text_sha256, write_turn_validated_completion +from .turn_todo_writeback import read_turn_execution_context, write_turn_validated_completion EXACT_SETTLEMENT_READBACK_NOT_FOUND = ( "exact settlement readback unexpectedly returned not-found" @@ -92,11 +91,9 @@ def execute_turn_run_once( native_task_validator=native_task_validator, validator_builder=build_loopx_turn_command_validator, ) - selected_todo = selected_turn_todo(envelope) - expected_todo_text_sha256 = captured_todo_text_sha256( - envelope, todo_id=str(selected_todo.get("todo_id") or ""), - **({"work_contract": payload["work_contract"]} if "work_contract" in payload else {}), - ) + execution_context = read_turn_execution_context(payload, envelope) + selected_todo = execution_context["selected_todo"] + expected_todo_text_sha256 = execution_context["sha256"] transaction = ( payload.get("transaction") if isinstance(payload.get("transaction"), Mapping) @@ -133,13 +130,10 @@ def execute_turn_run_once( and persisted_effect_id != settlement_identity.effect_id ): raise ValueError("Turn settlement identity effect_id is inconsistent") - stable_envelope: Mapping[str, Any] = ( - envelope if isinstance(envelope, Mapping) else {} - ) - replan_guard_scoped = "replan_action_packet" in stable_envelope + replan_guard_scoped = execution_context["replan_guard_scoped"] replan_obligation_id = ( replan_obligation_id_from_packet( - stable_envelope.get("replan_action_packet") + execution_context["replan_action_packet"] ) if replan_guard_scoped else None @@ -198,11 +192,7 @@ def append_settlement_event( if event_payload.get("rollout_event_log_error"): raise OSError(f"failed to persist {event_kind} settlement receipt") - writeback_contract = ( - envelope.get("writeback") - if isinstance(envelope.get("writeback"), dict) - else {} - ) + writeback_contract = execution_context["writeback"] delivery_workspace_path = ( project if isinstance( @@ -803,7 +793,7 @@ def scheduler(_spend_payload: dict[str, object]) -> dict[str, object]: def native_launch_decision(current_registry: Path) -> Mapping[str, Any]: return current_turn_host_launch_decision(args, registry_path=current_registry, runtime_root=runtime_root, identity=settlement_identity, - planned_effective_action=payload["turn_envelope"].get("effective_action"), + planned_effective_action=execution_context["effective_action"], scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, turn_start_hook_dispatch=turn_start_hook_dispatch, goal_ref=goal_ref) @@ -827,7 +817,7 @@ def current_launch_decision(current_registry: Path) -> Mapping[str, Any]: return current_turn_host_launch_decision( args, registry_path=current_registry, runtime_root=runtime_root, identity=settlement_identity, - planned_effective_action=payload["turn_envelope"].get("effective_action"), + planned_effective_action=execution_context["effective_action"], scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, # Keep this Turn's already dispatched required reads at launch. @@ -909,7 +899,7 @@ def resolve_dsh_operation_session(envelope: Mapping[str, Any]) -> dict[str, str] ) def on_managed_start_admitted() -> None: - replan_packet = stable_envelope.get("replan_action_packet") + replan_packet = execution_context["replan_action_packet"] capability_guard = ( replan_packet.get("capability_guard") if isinstance(replan_packet, Mapping) diff --git a/loopx/cli_commands/turn_todo_writeback.py b/loopx/cli_commands/turn_todo_writeback.py index 0448e86519..5863187eef 100644 --- a/loopx/cli_commands/turn_todo_writeback.py +++ b/loopx/cli_commands/turn_todo_writeback.py @@ -1,4 +1,4 @@ -"""Todo writeback adapters that preserve the Turn's effective runtime root.""" +"""Captured Turn execution context and Todo writeback transport.""" from __future__ import annotations @@ -34,6 +34,33 @@ def captured_todo_text_sha256( return digest +def read_turn_execution_context( + plan: Mapping[str, Any], envelope: Mapping[str, Any], +) -> dict[str, Any]: + """Read complete execution facts in one call; decode old journals as before.""" + if "work_contract" in plan: + try: + result = _turn_envelope_result( + "execution_context", envelope=dict(envelope), payload=plan["work_contract"], + ) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from exc + if result.get("sha256") is not None and not isinstance(result["sha256"], str): + raise RuntimeError("Turn captured task source hash must be a string or null") + return result + from ..control_plane.turn_driver.driver import selected_turn_todo + + selected = selected_turn_todo(envelope) + return { + "selected_todo": selected, + "sha256": captured_todo_text_sha256(envelope, todo_id=str(selected.get("todo_id") or "")), + "effective_action": envelope.get("effective_action"), + "writeback": envelope.get("writeback") if isinstance(envelope.get("writeback"), dict) else {}, + "replan_guard_scoped": "replan_action_packet" in envelope, + "replan_action_packet": envelope.get("replan_action_packet"), + } + + def write_turn_validated_completion( *, registry_path: Path, diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 412fbf36d9..7a8f3e1c1d 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -21,7 +21,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { projectTurnStartUnavailableContext } from "../capability_hooks.ts"; import { jsonObject, requireBoolean, requireJsonObject, requireNonEmptyString, requireStringLiteral } from "../runtime_decode.ts"; import { projectPendingCapabilityIntent } from "../work_items/pending_capability_intent.ts"; -import { capturedTodoTextSha256, decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodoId, scopeInteractionOutcomeReadGuidance, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; +import { capturedTodoTextSha256, decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodo, selectedWorkTodoId, scopeInteractionOutcomeReadGuidance, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; import { measureTurnEnvelope, TURN_ENVELOPE_SECTION_TARGETS } from "./turn_envelope_budget.ts"; export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; @@ -1730,7 +1730,7 @@ export function requireCompleteTurnDecision(value: unknown): JsonObject { /** One checked source for model input and transport session identity. The * historical effect adapter may remain in the request during migration, but it * cannot redirect the complete contract to another Goal, Agent or work item. */ -function completeHostDecision(request: JsonObject): JsonObject { +function completeHostSource(request: JsonObject): {decision: JsonObject; projection: JsonObject} { const decision = requireCompleteTurnDecision(request.payload); const envelope = requireJsonObject(request.envelope, "complete Turn executor envelope"); assertTurnEnvelopeConsumerVersion(envelope); @@ -1751,10 +1751,23 @@ function completeHostDecision(request: JsonObject): JsonObject { // An adapter's self-consistent hash does not prove that it carries the // admitted decision. Reuse the producer's projection, including writeback // and execution conditions, rather than maintaining a second field list. - if (signature.source_hash !== canonicalHash(quotaActionSignatureDocument(decision))) { + const projection = turnActionProjection(decision); + if (signature.source_hash !== canonicalHash(turnEnvelopeActionSignatureDocument(projection))) { throw new EffectRuntimeRequestError("complete Turn executor projection differs from admitted source"); } - return decision; + return {decision, projection}; +} + +function completeHostDecision(request: JsonObject): JsonObject { + return completeHostSource(request).decision; +} + +function completeTaskTextSha256(decision: JsonObject, todoId: unknown): string | null { + const context = object(object(object(decision.interaction_contract).agent_channel).work_context); + const declared = {...object(decision.selected_todo), + ...(Object.hasOwn(context, "selected_todo_text_verified") + ? {text_source_verified: context.selected_todo_text_verified} : {})}; + return capturedTodoTextSha256(declared, context, decision.recommended_action, todoId); } export function composeCompleteTurnDecision(value: unknown): JsonObject { @@ -2080,19 +2093,27 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { action_signature: turnEnvelopeActionSignatureDocument(request.envelope), }; } + if (operation === "execution_context") { + // The CLI consumes one checked source for execution, not the historical + // adapter's layout. Reuse the projection already checked against that + // source for effect conditions; this is not another model-facing contract. + const {decision, projection} = completeHostSource(request); + const work = {selected_todo: object(projection.action).selected_todo, + task_orchestration_contract: projection.task_orchestration_contract}; + return {selected_todo: selectedWorkTodo(work), + sha256: completeTaskTextSha256(decision, selectedWorkTodoId(work) ?? ""), + effective_action: decision.effective_action ?? null, + writeback: object(projection.writeback), + replan_guard_scoped: Object.hasOwn(projection, "replan_action_packet"), + replan_action_packet: projection.replan_action_packet ?? null}; + } if (operation === "captured_todo_text") { const envelope = requireJsonObject(request.envelope, "Turn completion envelope"); - let action = object(envelope.action), context: unknown = envelope.work_context; if (Object.hasOwn(request, "payload")) { - const decision = completeHostDecision(request); - const workContext = object(object(object(decision.interaction_contract).agent_channel).work_context); - action = {selected_todo: {...object(decision.selected_todo), - ...(Object.hasOwn(workContext, "selected_todo_text_verified") - ? {text_source_verified: workContext.selected_todo_text_verified} : {})}, - recommended_action: decision.recommended_action}; - context = workContext; + return {sha256: completeTaskTextSha256(completeHostDecision(request), request.todo_id)}; } - return {sha256: capturedTodoTextSha256(object(action.selected_todo), context, + const action = object(envelope.action); + return {sha256: capturedTodoTextSha256(object(action.selected_todo), envelope.work_context, action.recommended_action, request.todo_id)}; } if (operation === "build_controller_input") { diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index 5e61eb7e5a..9656e2f62a 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -760,13 +760,19 @@ function shellQuote(value: string): string { } /** The adaptive primary is the work identity for both reads and settlement. */ -export function selectedWorkTodoId(packet: JsonObject): string | null { +export function selectedWorkTodo(packet: JsonObject): JsonObject { const orchestration = jsonObject(packet.task_orchestration_contract) ?? {}; const primaryTodoId = typeof orchestration.primary_todo_id === "string" ? orchestration.primary_todo_id.trim() : ""; if (orchestration.schema_version === "task_orchestration_contract_v2" - && orchestration.mode === "adaptive" && primaryTodoId) return primaryTodoId; - const selected = jsonObject(packet.selected_todo) ?? {}; + && orchestration.mode === "adaptive" && primaryTodoId) { + return {todo_id: primaryTodoId, source: "task_orchestration_contract.primary_todo_id"}; + } + return jsonObject(packet.selected_todo) ?? {}; +} + +export function selectedWorkTodoId(packet: JsonObject): string | null { + const selected = selectedWorkTodo(packet); return typeof selected.todo_id === "string" && selected.todo_id.trim() ? selected.todo_id : null; } diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index bdfe235629..cf0cab3770 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -344,6 +344,27 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority isolate_sqlite_runtime(tmp_path, monkeypatch) + from loopx.cli_commands import turn_run_once + + prepare_request = turn_run_once.prepare_turn_host_request + + class SignatureOnlyEnvelope(dict): + def get(self, key, default=None): + if key in {"action", "task_orchestration_contract", "writeback", + "effective_action", "replan_action_packet"}: + raise AssertionError("complete execution read the historical adapter: " + key) + return super().get(key, default) + + def require_complete_execution(*args, **kwargs): + project, argv, validator, envelope = prepare_request(*args, **kwargs) + # Keep the real adapter for source/signature verification, but forbid + # CLI execution policy from reading its obsolete field layout. The + # persisted plan, native host, provider and settlement remain real. + if "work_contract" in args[1]: + envelope = SignatureOnlyEnvelope(envelope) + return project, argv, validator, envelope + + monkeypatch.setattr(turn_run_once, "prepare_turn_host_request", require_complete_execution) f = _fixture_module() project, runtime, workspace, registry = f._write_fixture(tmp_path, turn_count=2) state = project / ".codex/goals" / f.GOAL_ID / "ACTIVE_GOAL_STATE.md" diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 6cc7c67668..36e4702d3c 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -1,6 +1,7 @@ import { EFFECTIVE_ACTIONS } from "../../loopx/control_plane/quota/effective_action.generated.ts"; import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../../loopx/control_plane/agents/agent_scope_frontier.generated.ts"; import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; import test from "node:test"; import { @@ -1338,3 +1339,58 @@ test("complete driver projection ignores historical scalar decoding hints", () = assert.equal(normal.route, "ready_for_host"); assert.deepEqual(forged, normal); }); + +test("complete execution reads one original selection, task commitment and effect conditions", () => { + const source = payload(); + const selected = source.selected_todo as JsonObject; + selected.text = "Full task declaration. ".repeat(40) + "Retain the final requirement.\n"; + selected.future_constraint = {unknown: null, allowed: false}; + const interaction = source.interaction_contract as JsonObject; + (interaction.agent_channel as JsonObject).work_context = { + complete: true, selected_todo_ref: "selected_todo", selected_todo_text_verified: true}; + (interaction.cli_channel as JsonObject).delivery_workspace_causality = { + requirement: "required", future: {empty: [], approved: false}}; + const contract = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + const envelope = buildTurnEnvelope({payload: contract, scheduler_execution_args: ""}); + const before = structuredClone({contract, envelope}); + const result = evaluateTurnEnvelope({operation: "execution_context", payload: contract, envelope}); + assert.equal((result.selected_todo as JsonObject).todo_id, selected.todo_id); + assert.equal((result.selected_todo as JsonObject).task_repository, selected.task_repository); + assert.equal((result.selected_todo as JsonObject).text, selected.text); + assert.equal(result.sha256, createHash("sha256").update(String(selected.text), "utf8").digest("hex")); + assert.equal(result.effective_action, "normal_run"); + assert.deepEqual((result.writeback as JsonObject).delivery_workspace_causality, + (interaction.cli_channel as JsonObject).delivery_workspace_causality); + assert.equal(result.replan_guard_scoped, true); + assert.equal(result.replan_action_packet, null); + assert.deepEqual({contract, envelope}, before); + for (const payload of [null, {...contract, reason: "different source"}, {...contract, schema_version: "future"}]) { + assert.throws(() => evaluateTurnEnvelope({operation: "execution_context", payload, envelope})); + } + assert.throws(() => evaluateTurnEnvelope({operation: "execution_context", payload: contract, + envelope: {...envelope, writeback: {spend_allowed_now: true}}}), /captured executor source/); + for (const todoId of [42, true]) { + const malformed = structuredClone(source); + (malformed.selected_todo as JsonObject).todo_id = todoId; + const invalid = evaluateTurnEnvelope({operation: "compose_complete", payload: malformed, scheduler_execution_args: ""}); + const adapter = buildTurnEnvelope({payload: invalid, scheduler_execution_args: ""}); + assert.throws(() => evaluateTurnEnvelope({operation: "execution_context", payload: invalid, + envelope: adapter}), /selected Todo/); + } +}); + +test("complete execution keeps the adaptive primary and unbound selection distinct", () => { + for (const adaptive of [true, false]) { + const source = payload(); + source.selected_todo = adaptive ? {todo_id: "child", text: "Child work"} + : {future_evidence: {unknown: null}}; + if (adaptive) source.task_orchestration_contract = { + schema_version: "task_orchestration_contract_v2", mode: "adaptive", primary_todo_id: " primary "}; + const contract = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + const envelope = buildTurnEnvelope({payload: contract, scheduler_execution_args: ""}); + const result = evaluateTurnEnvelope({operation: "execution_context", payload: contract, envelope}); + assert.deepEqual(result.selected_todo, adaptive + ? {todo_id: "primary", source: "task_orchestration_contract.primary_todo_id"} : {}); + assert.equal(result.sha256, null); + } +}); From c47fb1e3973c396358243031b10f37baf2ef5d24 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:00:31 +0800 Subject: [PATCH 08/37] refactor(turn): consume complete Host source before legacy decoding Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 15 +- ...ller-executor-unified-contract-v0.zh-CN.md | 11 +- docs/reference/protocols/turn-envelope-v0.md | 9 + loopx/control_plane/quota/turn_envelope.ts | 9 + loopx/control_plane/turn_driver/codex_cli.py | 13 +- .../turn_driver/host_candidate.py | 183 +++++++++--------- .../test_turn_host_decision_capture.py | 62 ++++++ tests/control_plane_ts/turn_envelope.test.ts | 12 ++ 8 files changed, 220 insertions(+), 94 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 39b4beea5f..6e16fcdddd 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -868,6 +868,19 @@ boundary rather than canceling its cost. Full adapter retirement, remaining effect/material consumers and original-quota outcome/full-cost qualification remain open. +Structured Host preparation now consumes the checked complete source directly; +it no longer flattens the old action, boundary, capsule and planning carriers for +new requests. The same typed source check supplies the executable-action alias +and admitted-replan fact, preserving capability-before-replan ordering without +reimplementing it in Python. Codex prompt construction also uses the complete +source for boundary guidance and the original model input's staged readers. +Original capture metadata still travels through the existing transport; its +source and adapter integrity checks remain. Historical-only requests retain the +old decoder and renderer. This is an internal consumer replacement, not another +model-facing view or a cache across attempts; the final complete model input and +its instruction obligations must remain unchanged. Other effect/session +consumers, packaged adoption and full-cost qualification remain open. + The Codex result schema and shared candidate adapter now use one checked complete-contract profile read for evidence applicability. They no longer decide that requirement from the internal capsule on new requests. The existing @@ -1046,7 +1059,7 @@ design, not a claim that these paths have changed. | `quota.py::_emit_quota_result` / `_project_quota_cli_payload` | Compose the complete contract before capture; replace the ordinary/envelope execution-format branch. Retain admission, capability hooks, capture IO and host poll receipts with their current owners. A diagnostic status view is not an executable contract. | | `turn.py` after `build_fresh_turn_decision_owner().resolve()` | Call the same composition owner with the actual scheduler execution context. This entry calls `build_turn_envelope` directly; changing quota CLI emission alone does not migrate Turn. Retain the Turn plan, journal and receipt executor. | | `heartbeat/task_body.py::render_thin_heartbeat_task_body` | Replace the `captured_envelope` instruction split with one contract-reading instruction. Preserve exact selection/reentry transport, notification independence, required reads and original settlement. The saved bootstrap still resolves current instructions on wake. | -| `turn_driver/codex_cli.py::_prompt` and `host_candidate.py::extract_turn_authority` / `render_prompt` | Replace format-specific model inputs at the actual prompt boundary. Today ordinary Codex receives the envelope, staged replan uses extracted authority, and structured hosts use extracted authority. Shared source bytes or signatures do not prove these final inputs preserve the same obligations. Keep provider result encoding and outer effect ownership. | +| `turn_driver/codex_cli.py::_prompt` and `host_candidate.py::extract_turn_authority` / `render_prompt` | The C1 candidate sends the complete source at both final prompt boundaries and reads new Host metadata from its checked owner. The old authority layout is decoded only for historical input. Qualify complete final inputs and their obligations, captured-reader recovery and result applicability; shared source bytes alone do not prove them. Keep provider result encoding and outer effect ownership. | | `turn_driver/decision_capture.py` and `cli_commands/quota_capture.py` | Reuse immutable capture, validated readers and full/delta delivery. Fresh delivery and saved-capture recovery must reconstruct the same contract; do not reuse a continuity baseline for a different projection merely because its Turn identity matches. | | `benchmark/runtime/worker.py`, `benchmark/runtime/sessions.py` and `scripts/external_scheduler_worker.py` | Migrate the product entry they actually invoke. Keep evaluation setup and outer wake ownership; do not add benchmark-only conversion or count a managed-host test as attached-host acceptance. | diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index a95fa3c181..e0bc04403f 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -636,6 +636,15 @@ adapter 读取这组执行事实,仍传递该 adapter 供来源/签名校验 一次 TypeScript 边界调用;本次扩展读取复用该边界,不抵销原新增成本。 完整 adapter 退役、其余 effect/material 消费者及原 quota 成果/完整成本资格仍开放。 +结构化 Host 准备现在直接消费经过校验的完整来源,不再为新请求展开旧 action、 +boundary、capsule 与 planning 载体。同一次 typed 来源校验提供可执行行动别名和 +已准入 replan 事实,保留能力动作优先于 replan 的顺序,不在 Python 重写判断。 +Codex prompt 的边界指导也读取完整来源,分阶段 reader 读取原始模型输入。 +原 capture 元数据仍沿既有 transport 传递,来源及 adapter 完整性校验保持。 +只有历史请求继续使用旧 decoder 与 renderer。这是内部消费者替换,不是另一份 +面向模型的视图或跨 attempt 缓存;完整最终模型输入及其全部指令义务须保持一致。 +其他 effect/session 消费者、打包采用及完整成本资格仍开放。 + Codex 结果 schema 与共享 candidate adapter 现通过同一经过校验的完整契约 profile 读取判断证据要求是否适用;新请求不再从内部 capsule 决定该要求。 该读取与模型输入校验复用既有 TypeScript 完整性/来源/身份校验器,没有新增 @@ -766,7 +775,7 @@ TypeScript 采样保持合法身份输出一致;校验 median 从 4.864 增至 | `quota.py::_emit_quota_result` / `_project_quota_cli_payload` | 在 capture 前组合完整契约,替换 ordinary/envelope 执行格式分支。准入、capability hook、capture IO 与 host poll 回执保留原 owner。诊断 status 视图不是可执行契约。 | | `turn.py` 的 `build_fresh_turn_decision_owner().resolve()` 之后 | 携带实际 scheduler execution context 调用同一组合 owner。此入口直接调用 `build_turn_envelope`;仅修改 quota CLI emission 不会迁移 Turn。保留 Turn plan、journal 和回执执行器。 | | `heartbeat/task_body.py::render_thin_heartbeat_task_body` | 用统一契约读取指令替换 `captured_envelope` 指令分支,保留精确 selection/reentry transport、通知独立性、必读与原结算身份。保存的 bootstrap 仍在唤醒时解析当前指令。 | -| `turn_driver/codex_cli.py::_prompt` 与 `host_candidate.py::extract_turn_authority` / `render_prompt` | 在实际 prompt 边界替换格式特定输入。当前普通 Codex 接收 envelope,分阶段 replan 使用提取的 authority,结构化 host 也使用提取的 authority。共享源字节或签名不能证明这些最终输入保留相同义务。保留 provider 结果编码与外层 effect 归属。 | +| `turn_driver/codex_cli.py::_prompt` 与 `host_candidate.py::extract_turn_authority` / `render_prompt` | C1 候选在两个最终 prompt 边界发送完整来源,新 Host 元数据由其已校验 owner 提供;旧 authority 布局仅为历史输入解码。验收完整最终输入与义务、原 capture reader 恢复及结果适用性,不能仅凭共享源字节证明。保留 provider 结果编码与外层 effect 归属。 | | `turn_driver/decision_capture.py` 与 `cli_commands/quota_capture.py` | 复用不可变 capture、校验读取器和 full/delta 交付。首次交付和保存后恢复必须重建同一契约;不能仅因 Turn 身份相同,就把某投影的 continuity baseline 用于另一投影。 | | `benchmark/runtime/worker.py`、`benchmark/runtime/sessions.py` 与 `scripts/external_scheduler_worker.py` | 迁移它们实际调用的产品入口。保留评估配置与外层唤醒归属,不新增 benchmark 专用转换,也不把 managed-host 测试算作 attached-host 验收。 | diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 63e2bd9903..a42ce62d44 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -31,6 +31,11 @@ adapter remains for integrity checks and unmigrated consumers, with its executio decoder used only for requests that omit the complete contract. Invalid supplied contracts never fall back. This does not qualify full adapter retirement or outcome/full-cost benefit over original quota. +New structured Host preparation also consumes that complete source instead of +flattening the legacy authority layout. Executable-action and admitted-replan +facts retain their typed owner; Codex boundary guidance uses the complete source. +The original capture metadata and historical-only decoder remain supported. +This changes no complete model input, effect owner or cross-attempt freshness rule. 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; @@ -49,6 +54,10 @@ C1 的 native 候选在既有 typed owner 的一次调用中组合成果读取 已准入动作及 replan/写回条件,复用正文来源的边界调用与既有效果投影。旧 adapter 仍供完整性校验和未迁移消费者使用;只有缺省完整契约的请求使用旧执行 decoder, 显式无效契约不回退。完整 adapter 退役及优于原 quota 的成果/完整成本收益仍未资格。 +新结构化 Host 准备也直接消费完整来源,不再展开旧 authority 布局;可执行行动与 +已准入 replan 事实仍由既有 typed owner 提供,Codex 边界指导读取完整来源。 +原 capture 元数据和仅用于历史请求的 decoder 保留;完整模型输入、effect 归属及 +跨 attempt 新鲜度规则不变。 Preview it explicitly: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 7a8f3e1c1d..ca015d93c6 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2076,6 +2076,15 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { agent_id: scalarString(object(decision.agent_identity).agent_id, "Turn Agent identity").trim(), todo_id: (selectedWorkTodoId(decision) ?? "").trim()}; } + if (operation === "host_context") { + const {decision, projection} = completeHostSource(request); + // The structured Host consumes the complete source. Only the existing + // action and resolved-control facts come from its checked projection; + // Python must not recreate replan/capability or write-approval rules. + return {decision, primary_action: object(projection.action).primary_action ?? null, + replan_admitted: Object.keys(object(projection.replan_action_packet)).length > 0, + checkpointed_write_approval: Object.hasOwn(object(projection.boundary), "checkpointed_boundary_authority")}; + } if (operation === "driver_route") return {route: turnEnvelopeRoute(requireJsonObject(request.envelope, "Turn route envelope"), request.historical_route_text === undefined ? undefined : requireJsonObject(request.historical_route_text, "historical route text"))}; if (operation === "driver_lineage") { diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index 24674477c7..afaab07a9e 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -308,13 +308,13 @@ def _prompt(request: Mapping[str, Any]) -> str: from ..quota.turn_envelope import assert_turn_envelope_consumer_version envelope = _mapping(request.get("turn_envelope")) - staged = _mapping(_mapping(envelope.get("detail_ref")).get("replan_action_packet")).get("review") - from .host_candidate import complete_host_input + from .host_candidate import _captured_model_input, _complete_host_source # The complete owner checks the adapter version, source and lineage in one # call. Keep the standalone version gate only for historical requests. try: - complete_input = complete_host_input(request) + checked = _complete_host_source(request, operation="host_context") + complete_input = _captured_model_input(request) if checked is not None else None except RuntimeError: # Preserve the historical diagnostic precedence for doubly malformed # input without imposing a second bridge call on valid requests. @@ -324,6 +324,8 @@ def _prompt(request: Mapping[str, Any]) -> str: if not complete: assert_turn_envelope_consumer_version(envelope) model_request: Mapping[str, Any] = complete_input if complete_input is not None else request + detail = _mapping((complete_input if complete else envelope).get("detail_ref")) + staged = _mapping(detail.get("replan_action_packet")).get("review") if staged and not complete: from .host_candidate import extract_turn_authority, replan_prompt_instruction @@ -356,8 +358,9 @@ def _prompt(request: Mapping[str, Any]) -> str: instructions.append(replan_prompt_instruction( _mapping(model_request.get("detail_ref")), packet_path="detail_ref.replan_action_packet")) instructions.extend(request.get("capability_instructions") or ()) - boundary = _mapping(_mapping(request.get("turn_envelope")).get("boundary")) - if boundary.get("checkpointed_boundary_authority"): + checkpointed_approval = (checked["checkpointed_write_approval"] if checked is not None + else _mapping(envelope.get("boundary")).get("checkpointed_boundary_authority")) + if checkpointed_approval: instructions.append( "The boundary's checkpointed_boundary_authority records existing write approval " "only within its active_write_scope. It satisfies the write approval requirement " diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index 120af332d1..cc01095f0e 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -63,12 +63,14 @@ def _canonical_hash(value: Any) -> str: return "sha256:" + sha256(encoded).hexdigest() -def _complete_host_decision(request: Mapping[str, Any]) -> dict[str, Any] | None: +def _complete_host_source( + request: Mapping[str, Any], *, operation: str = "validate_complete", +) -> dict[str, Any] | None: if "work_contract" not in request: return None from ..quota.turn_envelope import _turn_envelope_result - return _turn_envelope_result("validate_complete", payload=request["work_contract"], + return _turn_envelope_result(operation, payload=request["work_contract"], envelope=_mapping(request.get("turn_envelope"))) @@ -79,7 +81,7 @@ def execution_profile_present(request: Mapping[str, Any]) -> bool: supplied contract never falls back. Schema and candidate adapters share this read, while evidence content validation stays with the result executor. """ - decision = _complete_host_decision(request) + decision = _complete_host_source(request) if decision is None: envelope = _mapping(request.get("turn_envelope")) decision = _mapping(envelope.get("contract_capsule")) @@ -94,8 +96,13 @@ def complete_host_input(request: Mapping[str, Any]) -> dict[str, Any] | None: compacts decision facts or derives a second model-facing authority view. Historical requests without a complete contract retain their old decoder. """ - if _complete_host_decision(request) is None: + if _complete_host_source(request) is None: return None + return _captured_model_input(request) + + +def _captured_model_input(request: Mapping[str, Any]) -> dict[str, Any]: + """Transport original-reader metadata after complete-source validation.""" envelope = _mapping(request.get("turn_envelope")) model_input = {key: value for key, value in request.items() if key != "turn_envelope"} detail = _mapping(envelope.get("detail_ref")) @@ -115,30 +122,37 @@ def complete_host_input(request: Mapping[str, Any]) -> dict[str, Any] | None: def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: - """Return the signed action and safety boundary exactly as projected.""" + """Use the complete source; decode the old layout for historical input only.""" - envelope = _mapping(request.get("turn_envelope")) try: - model_input = complete_host_input(request) + checked = _complete_host_source(request, operation="host_context") except RuntimeError as exc: raise ValueError("complete Turn contract rejected") from exc + if checked is not None: + primary_action = checked["primary_action"] + if not isinstance(primary_action, str) or not primary_action.strip(): + raise ValueError("signed TurnEnvelope has no primary_action") + decision = checked["decision"] + return {**decision, "primary_action": primary_action, + "execution_profile": _mapping(decision.get("execution_profile")), + "replan_action_packet": decision.get("replan_action_packet") if checked["replan_admitted"] else None, + "_model_input": _captured_model_input(request)} + + envelope = _mapping(request.get("turn_envelope")) signature = _mapping(envelope.get("action_signature")) - if model_input is None: - # The complete owner checks version, signature and source binding in - # one call. Only historical requests need this compatibility decoder. - assert_turn_envelope_consumer_version(envelope) - source_hash = str(signature.get("source_hash") or "") - envelope_hash = str(signature.get("envelope_hash") or "") - computed_envelope_hash = _canonical_hash( - turn_envelope_action_signature_document(envelope) - ) - if ( - signature.get("matches") is not True - or not source_hash - or source_hash != envelope_hash - or envelope_hash != computed_envelope_hash - ): - raise ValueError("TurnEnvelope action signature is missing or does not match") + assert_turn_envelope_consumer_version(envelope) + source_hash = str(signature.get("source_hash") or "") + envelope_hash = str(signature.get("envelope_hash") or "") + computed_envelope_hash = _canonical_hash( + turn_envelope_action_signature_document(envelope) + ) + if ( + signature.get("matches") is not True + or not source_hash + or source_hash != envelope_hash + or envelope_hash != computed_envelope_hash + ): + raise ValueError("TurnEnvelope action signature is missing or does not match") action = _mapping(envelope.get("action")) detail = _mapping(envelope.get("detail_ref")) @@ -237,10 +251,6 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: if key not in {"replan_action_packet", "replan_read_argv", "core_goal", "planning_horizon", "acceptance_index"}} authority["action_signature"] = signature authority.update(host_context_projection(request)) - if model_input is not None: - # Adapter-local transport only. Result qualification still uses the - # verified internal effect fields; rendering sends the complete input. - authority["_model_input"] = model_input return authority @@ -293,65 +303,6 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals sort_keys=True, separators=(",", ":"), ) - context_instruction = ( - "For unavailable_context, discard cached context from the affected hooks; " - "do not interpret failure as empty context. Hold actions that require that " - "missing context until it is read successfully. Independent work may " - "continue under its existing authority; this grants no new permissions.\n" - if authority.get("unavailable_context") else "" - ) - planning_context_instruction = ( - "Read agent_context before planning; its guidance and facts grant no permissions. " - "Only an agent_context.detail_ref requires capability detail: use detail_ref.full_decision " - "only for the saved original observation identified by detail_ref.captured_decision, " - "checking its Goal/Agent/Turn and source hash. Never re-enter quota admission for a detail read. " - "Missing or invalid original evidence holds dependent planning until recovery; " - "independent work retains its existing authority.\n" - if authority.get("agent_context") else "" - ) - replan_instruction = replan_prompt_instruction(authority) - acceptance_instruction = ( - "Read vision_continuation_audit before judging Goal acceptance or proposing " - "closeout; preserve its ordered required_before_closeout, evidence requirements " - "and current vision_gap_judge. Selected Todo completion alone cannot close the Goal. " - "For additional acceptance or coverage evidence, use detail_ref.full_decision " - "only for the saved original observation identified by detail_ref.captured_decision; " - "check its Goal/Agent/Turn and action_signature.source_decision_hash. " - "Never re-enter quota admission for a detail read. Missing or invalid evidence " - "holds dependent closure; independent work retains its existing authority. " - "Do not execute its state or scheduler commands; report the semantic result " - "for the outer Turn executor. The audit grants no new authority.\n" - if authority.get("vision_continuation_audit") else "" - ) - work_instruction = ( - "Use execution_profile to size and qualify the work, including its required " - "artifact, validation, outcome and degradation rules. Return the work and " - "validation evidence; state writeback and quota effects belong to the outer " - "Turn executor. The profile grants no new permissions.\n" - if authority.get("execution_profile") else "" - ) - legacy_input = ( - "You are executing one bounded LoopX-governed work segment.\n" - "The JSON below is the complete host authority for this Turn. Execute " - "primary_action only after reading work_context and any remaining required_reads. " - "Do not repeat reads already fulfilled in work_context. Write only inside write_scope, " - "and obey workspace_guard and all supplied boundary restrictions and stop conditions. " - "Checkpointed approval covers only its active_write_scope; other approval gates remain. " - "Do not infer authority from other prose. " - "User notification policy does not grant or cancel execution authority. " - "Respect action.must_attempt, action.delivery_allowed and action.quiet_noop_allowed " - "when present; alternatives and capability intents do not override admission or scope. " - "Obey execution_policy and task_orchestration_contract when supplied. " - "Scheduler and response plans describe the surrounding Turn: do not execute " - "state writeback, quota or scheduler commands from this work segment. " - "Return the semantic result; the outer Turn executor owns those effects.\n\n" - f"{context_instruction}" - f"{planning_context_instruction}" - f"{replan_instruction}" - f"{acceptance_instruction}" - f"{work_instruction}" - f"Turn authority JSON:\n{authority_json}\n\n" - ) if model_input is not None: detail = _mapping(model_input.get("detail_ref")) input_prompt = ( @@ -381,7 +332,65 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals + "Turn request:\n" + authority_json + "\n\n" ) else: - input_prompt = legacy_input + context_instruction = ( + "For unavailable_context, discard cached context from the affected hooks; " + "do not interpret failure as empty context. Hold actions that require that " + "missing context until it is read successfully. Independent work may " + "continue under its existing authority; this grants no new permissions.\n" + if authority.get("unavailable_context") else "" + ) + planning_context_instruction = ( + "Read agent_context before planning; its guidance and facts grant no permissions. " + "Only an agent_context.detail_ref requires capability detail: use detail_ref.full_decision " + "only for the saved original observation identified by detail_ref.captured_decision, " + "checking its Goal/Agent/Turn and source hash. Never re-enter quota admission for a detail read. " + "Missing or invalid original evidence holds dependent planning until recovery; " + "independent work retains its existing authority.\n" + if authority.get("agent_context") else "" + ) + replan_instruction = replan_prompt_instruction(authority) + acceptance_instruction = ( + "Read vision_continuation_audit before judging Goal acceptance or proposing " + "closeout; preserve its ordered required_before_closeout, evidence requirements " + "and current vision_gap_judge. Selected Todo completion alone cannot close the Goal. " + "For additional acceptance or coverage evidence, use detail_ref.full_decision " + "only for the saved original observation identified by detail_ref.captured_decision; " + "check its Goal/Agent/Turn and action_signature.source_decision_hash. " + "Never re-enter quota admission for a detail read. Missing or invalid evidence " + "holds dependent closure; independent work retains its existing authority. " + "Do not execute its state or scheduler commands; report the semantic result " + "for the outer Turn executor. The audit grants no new authority.\n" + if authority.get("vision_continuation_audit") else "" + ) + work_instruction = ( + "Use execution_profile to size and qualify the work, including its required " + "artifact, validation, outcome and degradation rules. Return the work and " + "validation evidence; state writeback and quota effects belong to the outer " + "Turn executor. The profile grants no new permissions.\n" + if authority.get("execution_profile") else "" + ) + input_prompt = ( + "You are executing one bounded LoopX-governed work segment.\n" + "The JSON below is the complete host authority for this Turn. Execute " + "primary_action only after reading work_context and any remaining required_reads. " + "Do not repeat reads already fulfilled in work_context. Write only inside write_scope, " + "and obey workspace_guard and all supplied boundary restrictions and stop conditions. " + "Checkpointed approval covers only its active_write_scope; other approval gates remain. " + "Do not infer authority from other prose. " + "User notification policy does not grant or cancel execution authority. " + "Respect action.must_attempt, action.delivery_allowed and action.quiet_noop_allowed " + "when present; alternatives and capability intents do not override admission or scope. " + "Obey execution_policy and task_orchestration_contract when supplied. " + "Scheduler and response plans describe the surrounding Turn: do not execute " + "state writeback, quota or scheduler commands from this work segment. " + "Return the semantic result; the outer Turn executor owns those effects.\n\n" + f"{context_instruction}" + f"{planning_context_instruction}" + f"{replan_instruction}" + f"{acceptance_instruction}" + f"{work_instruction}" + f"Turn authority JSON:\n{authority_json}\n\n" + ) return ( input_prompt + "When finished, return only one JSON object (no Markdown fence) with " diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 0a2cf73bc7..146ca2d030 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -559,6 +559,68 @@ def test_codex_prompt_preserves_complete_source_and_historical_version_gate(comp _prompt(broken) +@pytest.mark.parametrize("host", ["codex", "structured"]) +def test_complete_host_prompt_uses_source_without_decoding_effect_layout(complete_host_request, host, monkeypatch): + from loopx.control_plane.turn_driver import codex_cli, host_candidate + from loopx.control_plane.turn_driver.codex_cli import _prompt + from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt + + request, contract = complete_host_request + + class OpaqueEffectEnvelope(dict): + def get(self, key, default=None): + # The real typed owner still receives the entire envelope for its + # signature/source fence. Only original-capture transport metadata + # may be read here; Host policy must use the complete source. + if key not in {"detail_ref", "action_signature"}: + raise AssertionError("Host decoded the historical effect layout: " + key) + return super().get(key, default) + + original = json.dumps(request, ensure_ascii=False, sort_keys=True) + guarded = {**request, "turn_envelope": OpaqueEffectEnvelope(request["turn_envelope"])} + for adapter in (codex_cli, host_candidate): + original_mapping = adapter._mapping + + def observed_mapping(value, *, convert=original_mapping): + return value if isinstance(value, OpaqueEffectEnvelope) else convert(value) + + # Keep the observation wrapper across the adapters' shallow defensive + # copies; otherwise the assertion would never see their policy reads. + monkeypatch.setattr(adapter, "_mapping", observed_mapping) + if host == "codex": + prompt = _prompt(guarded) + else: + authority = extract_turn_authority(guarded) + assert authority["primary_action"] == request["turn_envelope"]["action"]["primary_action"] + prompt = render_prompt(authority) + model_input, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + assert model_input["work_contract"] == contract + assert "turn_envelope" not in model_input and "turn_authority" not in model_input + assert json.dumps(request, ensure_ascii=False, sort_keys=True) == original + + +def test_complete_codex_approval_guidance_uses_resolved_boundary(complete_host_request): + from copy import deepcopy + from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision + from loopx.control_plane.turn_driver.codex_cli import _prompt + + request, original = complete_host_request + active = {"schema_version": "checkpointed_boundary_authority_v0", "active_count": 1, + "active_write_scope": ["artifacts/**"]} + for approval, expected in ( + (None, False), ({}, False), ({**active, "schema_version": "future"}, False), + ({**active, "active_count": 0}, False), ({**active, "active_write_scope": []}, False), + ({**active, "active_write_scope": [None, ""]}, False), (active, True), + ): + source = deepcopy({key: value for key, value in original.items() + if key not in {"schema_version", "contract_binding"}}) + source.setdefault("goal_boundary", {})["checkpointed_boundary_authority"] = approval + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + prompt = _prompt({**request, "work_contract": contract, "turn_envelope": envelope}) + assert ("It satisfies the write approval requirement" in prompt) is expected + + def test_complete_planning_and_failed_recovery_share_lineage(complete_host_request): from copy import deepcopy from hashlib import sha256 diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 36e4702d3c..7159da2ca2 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -632,6 +632,18 @@ test("pending capability action outranks stale replan commands and remains signe assert.equal((envelope.required_reads as JsonObject[]).some(read => read.source === "selected_todo"), false); const signed = JSON.parse(JSON.stringify(turnEnvelopeActionSignatureDocument(envelope))); assert.deepEqual((signed.action as JsonObject).capability_intent, source.pending_capability_intent); + const contract = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + const completeEnvelope = buildTurnEnvelope({payload: contract, scheduler_execution_args: ""}); + const host = evaluateTurnEnvelope({operation: "host_context", payload: contract, envelope: completeEnvelope}); + assert.deepEqual(host.decision, contract); // Preserve the stale observation as data, not admitted work. + assert.equal(host.primary_action, "advance one bounded segment"); + assert.equal(host.replan_admitted, false); + const replanContract = evaluateTurnEnvelope({operation: "compose_complete", + payload: {...source, effective_action: "autonomous_replan_required"}, scheduler_execution_args: ""}); + const replanHost = evaluateTurnEnvelope({operation: "host_context", payload: replanContract, + envelope: buildTurnEnvelope({payload: replanContract, scheduler_execution_args: ""})}); + assert.equal(replanHost.primary_action, "produce one required semantic outcome"); + assert.equal(replanHost.replan_admitted, true); (source.pending_capability_intent as JsonObject).command = "untrusted replacement"; assert.throws(() => buildTurnEnvelope({payload: source, scheduler_execution_args: ""}), /action is unsupported/); From 9805817d5a97d1c008c53bddd1e2022d187df6f4 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:28:41 +0800 Subject: [PATCH 09/37] test(turn): qualify report replans against complete Host input Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 21 +++++++++++++++++++ ...ller-executor-unified-contract-v0.zh-CN.md | 17 +++++++++++++++ tests/test_native_steward_reports.py | 9 +++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 6e16fcdddd..f223ef41a2 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -881,6 +881,27 @@ model-facing view or a cache across attempts; the final complete model input and its instruction obligations must remain unchanged. Other effect/session consumers, packaged adoption and full-cost qualification remain open. +Against rebased predecessor `4c4e9b346`, frozen real File/SQLite CLI requests +produce identical complete prompts, result contracts, historical-only outputs +and invalid-source diagnostics. Host preparation still makes two RPCs; total +request bytes decrease by five and responses grow by 190 bytes per group. Six +sequential cohorts, with separate verified runtime namespaces and 93 warm +samples per arm/case, observe these median/p95 values in milliseconds: + +| Request provenance / Host | Predecessor | Complete-source consumer | +| --- | --- | --- | +| File / Codex | 27.544 / 32.366 | 31.258 / 40.408 | +| File / structured | 25.948 / 32.642 | 30.007 / 39.242 | +| SQLite / Codex | 25.695 / 31.070 | 31.889 / 37.696 | +| SQLite / structured | 24.409 / 30.258 | 33.242 / 42.287 | + +These observations are negative local costs, not a speedup. Provider labels +identify request provenance; the measured groups perform no authority-store IO. +Machine/background load remains uncontrolled. Startup, other calls, complete +Turn/model/cache/billing/human costs and accepted outcomes are excluded. Earlier +overlapping, non-isolated exploratory timing is not qualified; neither source +convergence nor these samples establish original-quota net benefit. + The Codex result schema and shared candidate adapter now use one checked complete-contract profile read for evidence applicability. They no longer decide that requirement from the internal capsule on new requests. The existing diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index e0bc04403f..e4efefa0da 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -645,6 +645,23 @@ Codex prompt 的边界指导也读取完整来源,分阶段 reader 读取原 面向模型的视图或跨 attempt 缓存;完整最终模型输入及其全部指令义务须保持一致。 其他 effect/session 消费者、打包采用及完整成本资格仍开放。 +相对重基后的前序 `4c4e9b346`,冻结的真实 File/SQLite CLI 请求产生完全一致的 +完整提示、结果契约、仅历史输入输出与无效来源诊断。Host 准备仍为两次 RPC;每组 +请求合计减少五字节,响应增加 190 字节。六组顺序 cohort 使用各自经过核验的独立 +runtime namespace,每个 arm/case 93 次温态采样,median/p95 毫秒值为: + +| 请求来源 / Host | 前序 | 完整来源消费者 | +| --- | --- | --- | +| File / Codex | 27.544 / 32.366 | 31.258 / 40.408 | +| File / structured | 25.948 / 32.642 | 30.007 / 39.242 | +| SQLite / Codex | 25.695 / 31.070 | 31.889 / 37.696 | +| SQLite / structured | 24.409 / 30.258 | 33.242 / 42.287 | + +这些是局部负向成本观察,不是加速。provider 标签只说明请求来源,计量组不执行 +authority-store IO;机器及后台负载仍未控制。不含启动、其他调用、完整 Turn/模型/ +缓存/账单/人工成本及验收成果。早期与验证重叠且未独立隔离的探索计时不作资格; +源码收敛和这些样本均不能证明相对原 quota 的净收益。 + Codex 结果 schema 与共享 candidate adapter 现通过同一经过校验的完整契约 profile 读取判断证据要求是否适用;新请求不再从内部 capsule 决定该要求。 该读取与模型输入校验复用既有 TypeScript 完整性/来源/身份校验器,没有新增 diff --git a/tests/test_native_steward_reports.py b/tests/test_native_steward_reports.py index a5af79e9db..d34694a915 100644 --- a/tests/test_native_steward_reports.py +++ b/tests/test_native_steward_reports.py @@ -693,7 +693,10 @@ def now(cls, tz=None): "\ndef fake_report_model(params):\n" " import json\n" " prompt = '\\n'.join(row.get('text', '') for row in params.get('input', []) if isinstance(row, dict))\n" - " authority = json.JSONDecoder().raw_decode(prompt.split('Turn authority JSON:', 1)[1].lstrip())[0]\n" + " marker = 'Turn request:' if 'Turn request:' in prompt else 'Turn authority JSON:'\n" + " authority = json.JSONDecoder().raw_decode(prompt.split(marker, 1)[1].lstrip())[0]\n" + " if marker == 'Turn request:':\n" + " authority = authority['work_contract']\n" " if authority.get('replan_action_packet'):\n" " return " + repr(json.dumps(replan)) + "\n" " return report_only(params)\n") @@ -728,6 +731,10 @@ def legacy_selection(host, original, **kwargs): assert len(replans) == (0 if replan_outcome == "retained_probe" else 1) if replan_outcome not in {"settled", "already_admitted", "retained_probe"}: assert replans[0].get("status") != "committed" + # An oracle parser/transport failure must not stand in for the intended + # wait or rejected semantic candidate. + assert replans[0]["result_kind"] == ("wait" if replan_outcome == "wait" else "validation_failed") + assert replans[0]["effects"]["host_invoked"] is True assert replans[0]["effects"]["state_written"] is False and replans[0]["effects"]["quota_spent"] is False assert not (attempt_directory(reports.root, second_window) / "private-generation.json").exists() assert bot.writes == writes and transport.steward_reports.reconcile() == 0 From b3b21350a87a4416e060d48557182f5f551a6f90 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:06:33 +0800 Subject: [PATCH 10/37] refactor(turn): prepare Codex input with one source check Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 10 +-- loopx/control_plane/turn_driver/codex_cli.py | 43 ++++++++-- .../turn_driver/codex_operation_host.py | 8 +- .../turn_driver/host_candidate.py | 14 ++-- .../test_codex_operation_launch.py | 24 +++--- .../test_turn_host_decision_capture.py | 80 ++++++++++++++++++- tests/control_plane_ts/turn_envelope.test.ts | 3 +- tests/test_loopx_turn_codex_cli.py | 24 +++--- 8 files changed, 164 insertions(+), 42 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index ca015d93c6..f544d9c045 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2078,12 +2078,12 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { } if (operation === "host_context") { const {decision, projection} = completeHostSource(request); - // The structured Host consumes the complete source. Only the existing - // action and resolved-control facts come from its checked projection; - // Python must not recreate replan/capability or write-approval rules. - return {decision, primary_action: object(projection.action).primary_action ?? null, + // The caller already holds this exact checked source. Return only the + // resolved facts it needs, not another full copy over the transport. + return {primary_action: object(projection.action).primary_action ?? null, replan_admitted: Object.keys(object(projection.replan_action_packet)).length > 0, - checkpointed_write_approval: Object.hasOwn(object(projection.boundary), "checkpointed_boundary_authority")}; + checkpointed_write_approval: Object.hasOwn(object(projection.boundary), "checkpointed_boundary_authority"), + execution_profile_present: Object.keys(object(decision.execution_profile)).length > 0}; } if (operation === "driver_route") return {route: turnEnvelopeRoute(requireJsonObject(request.envelope, "Turn route envelope"), request.historical_route_text === undefined ? undefined : requireJsonObject(request.historical_route_text, "historical route text"))}; diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index afaab07a9e..d8d114b47d 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -195,6 +195,12 @@ def _has_subagent_topology(request: Mapping[str, Any] | None) -> bool: def codex_cli_result_schema( request: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + return _result_schema(request) + + +def _result_schema( + request: Mapping[str, Any] | None, *, checked: Mapping[str, Any] | None = None, ) -> dict[str, Any]: text_limits = dict(HOST_RESULT_TEXT_LIMITS) properties: dict[str, Any] = { @@ -277,7 +283,8 @@ def codex_cli_result_schema( "enum": ["codex-cli"], } properties["child_execution_receipts"] = child_receipts - evidence_required = execution_profile_present(request or {}) + evidence_required = (checked["execution_profile_present"] if checked is not None + else execution_profile_present(request or {})) schema: dict[str, Any] = { "type": "object", "properties": properties, @@ -305,21 +312,47 @@ def codex_cli_result_schema( def _prompt(request: Mapping[str, Any]) -> str: + return _render_prompt(request, checked=_checked_prompt_context(request)) + + +def _checked_prompt_context(request: Mapping[str, Any]) -> dict[str, Any] | None: from ..quota.turn_envelope import assert_turn_envelope_consumer_version envelope = _mapping(request.get("turn_envelope")) - from .host_candidate import _captured_model_input, _complete_host_source + from .host_candidate import _complete_host_source # The complete owner checks the adapter version, source and lineage in one # call. Keep the standalone version gate only for historical requests. try: checked = _complete_host_source(request, operation="host_context") - complete_input = _captured_model_input(request) if checked is not None else None except RuntimeError: # Preserve the historical diagnostic precedence for doubly malformed # input without imposing a second bridge call on valid requests. assert_turn_envelope_consumer_version(envelope) raise + return checked + + +def prepare_codex_cli_input(request: Mapping[str, Any]) -> tuple[str, dict[str, Any]]: + """Prepare one launch's prompt and schema from one complete-source check. + + This value never crosses an attempt/result boundary or enters a cache. + Standalone consumers still validate independently; historical requests keep + their existing prompt/schema readers. Launch and result authority stay with + the original Host and outer executor. + """ + checked = _checked_prompt_context(request) + if checked is None: + return _prompt(request), codex_cli_result_schema(request) + return _render_prompt(request, checked=checked), _result_schema(request, checked=checked) + + +def _render_prompt(request: Mapping[str, Any], *, checked: Mapping[str, Any] | None) -> str: + from ..quota.turn_envelope import assert_turn_envelope_consumer_version + from .host_candidate import _captured_model_input + + envelope = _mapping(request.get("turn_envelope")) + complete_input = _captured_model_input(request) if checked is not None else None complete = complete_input is not None if not complete: assert_turn_envelope_consumer_version(envelope) @@ -863,9 +896,10 @@ def commit() -> None: temporary = Path(directory) schema_path = temporary / "result-schema.json" output_path = temporary / "last-message.json" + input_text, result_schema = prepare_codex_cli_input(request) schema_path.write_text( json.dumps( - codex_cli_result_schema(request), + result_schema, ensure_ascii=False, separators=(",", ":"), ), @@ -911,7 +945,6 @@ def observe_stderr(line: str) -> None: events = HostOutputLines(observe_event) diagnostics = HostOutputLines(observe_stderr) - input_text = _prompt(request) with capture_codex_input_source(project, input_text, lineage["goal_id"], observed_session): observed = run_host_process(command, project=project, input_text=input_text, timeout_seconds=timeout_seconds, drain_timeout_seconds=OUTPUT_DRAIN_TIMEOUT_SECONDS, diff --git a/loopx/control_plane/turn_driver/codex_operation_host.py b/loopx/control_plane/turn_driver/codex_operation_host.py index 1869ee1635..9e0fddf5f7 100644 --- a/loopx/control_plane/turn_driver/codex_operation_host.py +++ b/loopx/control_plane/turn_driver/codex_operation_host.py @@ -20,9 +20,8 @@ from ..goals.first_party_host_admission import FirstPartyHostGoalAdmission from . import managed_operation_host as operations from .codex_cli import ( - _prompt, normalize_codex_stdio_mcp_server, - codex_cli_result_schema, + prepare_codex_cli_input, ) from .codex_sessions import _lineage, select_managed_operation_session from .executor import LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION @@ -198,8 +197,9 @@ def record_child() -> None: else: goal_admission.accept_result(record_child) + input_text, result_schema = prepare_codex_cli_input(request) result = session.send( - _prompt(request) + input_text + "\nUse loopx_operation for context/pending/prepare/inspect/consume/report. " "Source conversations are not executor identity. context/pending/inspect do not require consumption. " "When the admitted task authorizes proposal preparation and supplies its terms, prepare may run before human confirmation or consume; " @@ -213,7 +213,7 @@ def record_child() -> None: + json.dumps(continuations, sort_keys=True) + ". Inspect these locators through loopx_operation and consume once before any effect." if continuations else ""), - output_schema=codex_cli_result_schema(request), + output_schema=result_schema, on_event=on_event, turn_start_guard=lambda: launch_scope("codex_operation_turn_start"), on_native_item=observe_child if child_observer is not None else None, diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index cc01095f0e..e695b60d81 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -7,6 +7,7 @@ from __future__ import annotations from collections.abc import Mapping +from copy import deepcopy from hashlib import sha256 import json from typing import Any @@ -81,10 +82,11 @@ def execution_profile_present(request: Mapping[str, Any]) -> bool: supplied contract never falls back. Schema and candidate adapters share this read, while evidence content validation stays with the result executor. """ - decision = _complete_host_source(request) - if decision is None: - envelope = _mapping(request.get("turn_envelope")) - decision = _mapping(envelope.get("contract_capsule")) + checked = _complete_host_source(request, operation="host_context") + if checked is not None: + return checked["execution_profile_present"] + envelope = _mapping(request.get("turn_envelope")) + decision = _mapping(envelope.get("contract_capsule")) profile = decision.get("execution_profile") return isinstance(profile, Mapping) and bool(profile) @@ -132,7 +134,9 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: primary_action = checked["primary_action"] if not isinstance(primary_action, str) or not primary_action.strip(): raise ValueError("signed TurnEnvelope has no primary_action") - decision = checked["decision"] + # Keep the old detached authority value without echoing its complete + # bytes through the checked-source RPC. + decision = deepcopy(_mapping(request["work_contract"])) return {**decision, "primary_action": primary_action, "execution_profile": _mapping(decision.get("execution_profile")), "replan_action_packet": decision.get("replan_action_packet") if checked["replan_admitted"] else None, diff --git a/tests/control_plane/test_codex_operation_launch.py b/tests/control_plane/test_codex_operation_launch.py index 3e8acbf9ca..d18994370e 100644 --- a/tests/control_plane/test_codex_operation_launch.py +++ b/tests/control_plane/test_codex_operation_launch.py @@ -124,13 +124,13 @@ def stop_before_start(**kwargs): monkeypatch.setattr(CodexChatAgentSession, "start", stop_before_start) else: - original_prompt = codex_operation_host._prompt + original_input = codex_operation_host.prepare_codex_cli_input def stop_before_turn(request): stop_goal() - return original_prompt(request) + return original_input(request) - monkeypatch.setattr(codex_operation_host, "_prompt", stop_before_turn) + monkeypatch.setattr(codex_operation_host, "prepare_codex_cli_input", stop_before_turn) code, payload = _cli([*arguments, "--execute"]) assert len(stopped) == 1, payload assert (turns.read_bytes() if turns.exists() else None) == before_turns, payload @@ -246,9 +246,12 @@ def test_native_turn_dispatch_failure_drains_before_stop_writer_is_released( 1, )) arguments[arguments.index("--timeout-seconds") + 1] = "1" - original_prompt = codex_operation_host._prompt - monkeypatch.setattr(codex_operation_host, "_prompt", - lambda request: original_prompt(request) + "\n" + "x" * (8 * 1024 * 1024)) + original_input = codex_operation_host.prepare_codex_cli_input + def supplemented_input(request): + prompt, schema = original_input(request) + return prompt + "\n" + "x" * (8 * 1024 * 1024), schema + + monkeypatch.setattr(codex_operation_host, "prepare_codex_cli_input", supplemented_input) sessions, drained_before_release = [], [] entered = threading.Event() original_start = CodexChatAgentSession.start @@ -314,10 +317,13 @@ def test_guarded_native_dispatch_preserves_large_unicode_input(tmp_path, monkeyp _, _, _, turns, arguments = _fixture(tmp_path, monkeypatch, "sqlite") observed = tmp_path / "accepted-native-input" monkeypatch.setenv("FAKE_OPERATION_PROMPT_FILE", str(observed)) - original_prompt = codex_operation_host._prompt + original_input = codex_operation_host.prepare_codex_cli_input supplement = "完整输入☃" * (1024 * 1024) - monkeypatch.setattr(codex_operation_host, "_prompt", - lambda request: original_prompt(request) + "\n" + supplement) + def supplemented_input(request): + prompt, schema = original_input(request) + return prompt + "\n" + supplement, schema + + monkeypatch.setattr(codex_operation_host, "prepare_codex_cli_input", supplemented_input) code, payload = _cli([*arguments, "--execute"]) assert code == 0 and payload["effects"]["host_invoked"], payload assert turns.read_text().splitlines() == ["owned-app-server-thread"] diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 146ca2d030..3e32eda04d 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -433,7 +433,7 @@ def test_complete_result_evidence_applicability_uses_admitted_profile(complete_h from loopx.control_plane.quota.turn_envelope import ( build_turn_envelope, compose_turn_decision, turn_envelope_action_signature_document, ) - from loopx.control_plane.turn_driver.codex_cli import codex_cli_result_schema + from loopx.control_plane.turn_driver.codex_cli import codex_cli_result_schema, prepare_codex_cli_input from loopx.control_plane.turn_driver.host_candidate import build_result request, original = complete_host_request @@ -455,6 +455,7 @@ def test_complete_result_evidence_applicability_uses_admitted_profile(complete_h for consumer_request in (complete, historical): schema = codex_cli_result_schema(consumer_request) assert bool(schema.get("allOf")) is expected + assert bool(prepare_codex_cli_input(consumer_request)[1].get("allOf")) is expected result = build_result(consumer_request, candidate) assert (result["classification"] == "missing_host_execution_evidence") is expected for field in ("work_evidence", "validation_evidence"): @@ -559,6 +560,83 @@ def test_codex_prompt_preserves_complete_source_and_historical_version_gate(comp _prompt(broken) +def test_complete_structured_authority_keeps_detached_source(complete_host_request): + from copy import deepcopy + from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority + + request, contract = complete_host_request + before = deepcopy(contract) + authority = extract_turn_authority(request) + authority["contract_binding"]["source_decision_hash"] = "changed local authority" + authority["selected_todo"]["text"] = "changed local work" + assert contract == before and request["work_contract"] == before + + +@pytest.mark.parametrize("operation_tools", [False, True]) +def test_complete_codex_launch_shares_input_check_only_within_attempt( + complete_host_request, tmp_path, monkeypatch, operation_tools, +): + from copy import deepcopy + from loopx.control_plane.quota import turn_envelope + from loopx.control_plane.turn_driver.codex_cli import _prompt, codex_cli_result_schema, run_codex_cli_host + from loopx.control_plane.turn_driver.codex_operation_host import run_codex_operation_host + from tests.test_codex_operation_host import FAKE_SERVER + + request, _ = complete_host_request + expected_prompt, expected_schema = _prompt(request), codex_cli_result_schema(request) + prompt_file, schema_file = tmp_path / "prompt.txt", tmp_path / "schema.json" + if operation_tools: + executable = tmp_path / "fake-operation-host" + executable.write_text(FAKE_SERVER.replace( + 'assert row["params"]["outputSchema"]["type"] == "object"', + 'import pathlib\n ' + f'pathlib.Path({str(schema_file)!r}).write_text(json.dumps(row["params"]["outputSchema"]))', + )) + executable.chmod(0o700) + monkeypatch.setenv("FAKE_OPERATION_PROMPT_FILE", str(prompt_file)) + else: + executable = _fixture_module()._write_fake_codex(tmp_path) + executable.write_text(executable.read_text().replace( + "prompt = sys.stdin.read()", "prompt = sys.stdin.read()\n" + f"pathlib.Path({str(prompt_file)!r}).write_text(prompt)\n" + f"pathlib.Path({str(schema_file)!r}).write_text(pathlib.Path(args[args.index('--output-schema') + 1]).read_text())", + )) + observed = [] + original = turn_envelope.effect_runtime_result + + def observe(method, arguments, **kwargs): + if arguments.get("operation") in {"host_context", "validate_complete"}: + observed.append(arguments["operation"]) + return original(method, arguments, **kwargs) + + monkeypatch.setattr(turn_envelope, "effect_runtime_result", observe) + options = dict(runtime_root=tmp_path / "runtime", project=tmp_path / "project", + codex_bin=str(executable), timeout_seconds=10) + run = run_codex_cli_host + if operation_tools: + run = run_codex_operation_host + options.update(registry_path=tmp_path / "project/.loopx/registry.json", + model="test-model", reasoning_effort="xhigh") + for _ in range(2): + observed.clear() + result = run(request, **options) + assert result["turn_key"] == request["turn_key"] + # The existing app-server client folds prompt whitespace before send; + # CLI stdin retains the original bytes. Neither adapter changes here. + assert prompt_file.read_text().startswith( + " ".join(expected_prompt.split()) if operation_tools else expected_prompt) + assert json.loads(schema_file.read_text()) == expected_schema + # A fresh attempt checks again; preparing its prompt and schema shares + # the one check. Result validation and outer settlement remain separate. + assert observed == ["host_context"] + changed = deepcopy(request) + changed["work_contract"]["reason"] = "source changed after a successful launch" + before = prompt_file.read_bytes(), schema_file.read_bytes() + with pytest.raises((RuntimeError, ValueError)): + run(changed, **options) + assert before == (prompt_file.read_bytes(), schema_file.read_bytes()) + + @pytest.mark.parametrize("host", ["codex", "structured"]) def test_complete_host_prompt_uses_source_without_decoding_effect_layout(complete_host_request, host, monkeypatch): from loopx.control_plane.turn_driver import codex_cli, host_candidate diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 7159da2ca2..1922c407e7 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -635,7 +635,8 @@ test("pending capability action outranks stale replan commands and remains signe const contract = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); const completeEnvelope = buildTurnEnvelope({payload: contract, scheduler_execution_args: ""}); const host = evaluateTurnEnvelope({operation: "host_context", payload: contract, envelope: completeEnvelope}); - assert.deepEqual(host.decision, contract); // Preserve the stale observation as data, not admitted work. + assert.equal(Object.hasOwn(host, "decision"), false); // Do not echo the checked source over the bridge. + assert.equal(host.execution_profile_present, false); assert.equal(host.primary_action, "advance one bounded segment"); assert.equal(host.replan_admitted, false); const replanContract = evaluateTurnEnvelope({operation: "compose_complete", diff --git a/tests/test_loopx_turn_codex_cli.py b/tests/test_loopx_turn_codex_cli.py index 4846eb346c..9bf3ade4d2 100644 --- a/tests/test_loopx_turn_codex_cli.py +++ b/tests/test_loopx_turn_codex_cli.py @@ -828,7 +828,7 @@ def test_executable_legacy_goal_stop_after_selection_prevents_host_launch( run_codex_cli_host(request, **options) binding = _session_files(tmp_path / "runtime")[0] original_binding = binding.read_bytes() - original_prompt = codex_cli._prompt + original_input = codex_cli.prepare_codex_cli_input def stop_before_dispatch(current): with project_registry_transaction(registry_path, operation="fixture_stop_before_launch") as transaction: @@ -838,9 +838,9 @@ def stop_before_dispatch(current): "updated_at": "2026-10-10T12:00:00Z", "reason": "Owner stopped this Goal", } transaction.commit(registry) - return original_prompt(current) + return original_input(current) - monkeypatch.setattr(codex_cli, "_prompt", stop_before_dispatch) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", stop_before_dispatch) request["session"]["context_policy"]["mode"] = context request["session"]["action"] = "resume" if context == "resume" else "start_new" with pytest.raises(FirstPartyHostRuntimeRejected, match="goal_stopped"): @@ -867,7 +867,7 @@ def test_default_turn_cli_blocks_native_launch_after_canonical_goal_stop( project, runtime, registry_path = _write_live_fixture(tmp_path, provider=provider) executable, log = _fake_codex(tmp_path) monkeypatch.setenv("FAKE_CODEX_LOG", str(log)) - original_prompt = codex_cli._prompt + original_input = codex_cli.prepare_codex_cli_input reached_host = [] def stop_before_dispatch(current): @@ -879,9 +879,9 @@ def stop_before_dispatch(current): "updated_at": "2026-10-10T12:00:00Z", "reason": "Owner stopped this Goal", } transaction.commit(registry) - return original_prompt(current) + return original_input(current) - monkeypatch.setattr(codex_cli, "_prompt", stop_before_dispatch) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", stop_before_dispatch) output = io.StringIO() with contextlib.redirect_stdout(output): code = cli_main([ @@ -919,7 +919,7 @@ def test_source_goal_change_after_session_selection_prevents_host_launch( run_codex_cli_host(request, **options) binding = _session_files(tmp_path / "runtime")[0] original_binding = binding.read_bytes() - original_prompt = codex_cli._prompt + original_input = codex_cli.prepare_codex_cli_input def change_before_dispatch(current): # Session selection has succeeded, but no new Host request exists yet. @@ -935,9 +935,9 @@ def change_before_dispatch(current): "updated_at": "2026-10-10T12:00:00Z", "reason": "Owner stopped this Goal", } transaction.commit(registry) - return original_prompt(current) + return original_input(current) - monkeypatch.setattr(codex_cli, "_prompt", change_before_dispatch) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", change_before_dispatch) request["session"]["context_policy"]["mode"] = context request["session"]["action"] = "resume" if context == "resume" else "start_new" with pytest.raises(FirstPartyHostRuntimeRejected) as error: @@ -1650,7 +1650,7 @@ def test_default_turn_cli_rejects_stopped_source_behind_stale_global_mirror( 1, )) monkeypatch.setenv("FAKE_CODEX_LOG", str(native_log)) - original_prompt = codex_cli._prompt + original_input = codex_cli.prepare_codex_cli_input stopped = [] def stop_source_before_dispatch(request): @@ -1667,9 +1667,9 @@ def stop_source_before_dispatch(request): assert json.loads(source_registry.read_text())["goals"][0]["activation"]["state"] == "stopped" assert mirror.read_bytes() == original_mirror stopped.append(result) - return original_prompt(request) + return original_input(request) - monkeypatch.setattr(codex_cli, "_prompt", stop_source_before_dispatch) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", stop_source_before_dispatch) output = io.StringIO() with contextlib.redirect_stdout(output): code = cli_main([ From c02aeaa55385976e41e6afd2df24b522a2168ef3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:06:44 +0800 Subject: [PATCH 11/37] docs(controller): qualify input preparation cost and remaining gaps Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 44 ++++++++++++++++++- ...ller-executor-unified-contract-v0.zh-CN.md | 33 +++++++++++++- docs/reference/protocols/turn-envelope-v0.md | 8 ++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index f223ef41a2..ea9ad662ac 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -902,11 +902,53 @@ Turn/model/cache/billing/human costs and accepted outcomes are excluded. Earlier overlapping, non-isolated exploratory timing is not qualified; neither source convergence nor these samples establish original-quota net benefit. +The next input-preparation increment shares one complete-source check between +prompt and result-schema construction within a single Codex launch. Both the +CLI process and operation-equipped app-server use this existing adapter boundary. +Standalone prompt/schema calls still check independently; structured result +conversion checks its source again after execution. No checked value is cached +across attempts, model work or result handling. The typed owner now returns only +action/replan/approval/profile applicability facts instead of echoing the full +source over the bridge. Python consumes its original checked input, retaining +the structured authority's detached values. Complete model input, original +readers, launch guards, result validation and outer settlement remain unchanged. + +An owned V8 capture of 500 baseline source checks locates substantial CPU work +in canonical hashing and Unicode key ordering. It excludes Python, transport and +provider IO; its inclusive weights are not elapsed-time savings. This motivates +removing duplicate checks at one preparation boundary, not weakening integrity +checks or sharing validation across execution. Earlier negative measurements +remain historical evidence; this increment does not close C1's original-quota +outcome/full-cost or adoption requirements. + +Against rebased predecessor `75bd69bf6` on target `ff9e97f8e`, six sequential interleaved cohorts +(93 warm samples per arm/case) retain identical complete prompt/schema/result, +historical output and invalid-source diagnostics. Codex preparation drops from +two RPCs to one; structured prompt plus post-execution candidate conversion +retains two. Codex request bytes are 86,293/86,399 → 43,144/43,197 for File/SQLite; +responses are 62,554/62,606 → 211. Structured requests decrease by five bytes and +responses become 422 bytes. These are internal JSON payloads, excluding framing. + +| Request provenance / Host | `75bd69bf6` median / p95 ms | Prepared input median / p95 ms | +| --- | --- | --- | +| File / Codex | 22.147 / 26.204 | 12.003 / 13.653 | +| File / structured | 22.269 / 26.310 | 23.124 / 27.843 | +| SQLite / Codex | 21.656 / 26.511 | 11.734 / 17.183 | +| SQLite / structured | 21.324 / 23.608 | 23.716 / 26.079 | + +Codex's local preparation improves; both structured timings remain negative +observations despite lower transport volume. The workload performs no store IO +or real model work. Separate owned runtimes and sequential arms avoid this +Agent's validation overlap, but background load remains uncontrolled. Startup, +other calls, full Turn, tokens/cache, billing, implementation/human cost and +accepted outcomes remain outside this measurement; it is not the original-quota +comparison or proof of overall benefit. + The Codex result schema and shared candidate adapter now use one checked complete-contract profile read for evidence applicability. They no longer decide that requirement from the internal capsule on new requests. The existing TypeScript integrity/source/lineage validator serves both this read and model -input validation; no new packet view, wire field or per-evidence-field call is +input validation; no new model-facing packet view or per-evidence-field call is introduced. Omitted contracts retain historical capsule decoding; explicit null or altered contracts reject. Empty/non-object profiles preserve their previous non-applicability, stop results still need no evidence, and supplied evidence is diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index e4efefa0da..08d20d8063 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -662,10 +662,41 @@ authority-store IO;机器及后台负载仍未控制。不含启动、其他 缓存/账单/人工成本及验收成果。早期与验证重叠且未独立隔离的探索计时不作资格; 源码收敛和这些样本均不能证明相对原 quota 的净收益。 +后续输入准备增量在同一次 Codex 启动内,由提示词和结果 schema 共用一次完整 +来源校验。CLI 进程与启用 operation tools 的 app-server 都消费这一既有 adapter +边界。独立调用提示词/schema 仍各自校验,结构化结果转换在执行后重新校验来源; +不跨 attempt、模型执行或结果处理缓存校验值。typed owner 只返回行动、replan、 +approval 和 profile 适用性事实,不再通过 bridge 原样回传完整来源。Python 消费 +自己持有的原始已校验输入,并保留结构化 authority 值的独立副本。完整模型输入、 +原 reader、启动 guard、结果验证和外层结算不变。 + +自有 V8 进程捕获 500 次基线来源校验,显示 canonical hash 与 Unicode key +排序占用明显 CPU;未覆盖 Python、transport 或 provider IO,inclusive 权重不是 +节省时延。它支持移除同一次准备中的重复校验,不支持弱化完整性或跨执行复用。 +原负面测量按历史保留;本增量不关闭 C1 相对原 quota 的成果/完整成本及采用要求。 + +相对重基到目标 `ff9e97f8e` 的前序 `75bd69bf6`,六组顺序交错 cohort、每 arm/case 93 次温态样本, +完整 prompt/schema/result、历史输出和无效来源诊断完全一致。Codex 输入准备从 +两次 RPC 降为一次;结构化提示与执行后 candidate 转换仍保留两次。File/SQLite 的 +Codex 请求字节为 86,293/86,399 → 43,144/43,197,响应为 62,554/62,606 → 211; +结构化请求各减少五字节,响应变为 422 字节。这里是内部 JSON payload,不含 framing。 + +| 请求来源 / Host | `75bd69bf6` median / p95 毫秒 | 输入准备增量 median / p95 毫秒 | +| --- | --- | --- | +| File / Codex | 22.147 / 26.204 | 12.003 / 13.653 | +| File / structured | 22.269 / 26.310 | 23.124 / 27.843 | +| SQLite / Codex | 21.656 / 26.511 | 11.734 / 17.183 | +| SQLite / structured | 21.324 / 23.608 | 23.716 / 26.079 | + +Codex 局部准备成本改善,两个结构化时延仍是负向观察,不能被传输量下降抵销。 +负载没有 store IO 或真实模型工作;各自有独立 runtime、顺序执行避免本 Agent 的 +验证重叠,但后台负载仍未控制。不含启动、其他调用、整轮、tokens/缓存、账单、 +实施/人工与验收成果;不是原 quota 对照,也不证明整体净收益。 + Codex 结果 schema 与共享 candidate adapter 现通过同一经过校验的完整契约 profile 读取判断证据要求是否适用;新请求不再从内部 capsule 决定该要求。 该读取与模型输入校验复用既有 TypeScript 完整性/来源/身份校验器,没有新增 -packet 视图、wire 字段或按证据字段分别调用。缺省契约仍解码历史 capsule; +模型侧 packet 视图或按证据字段分别调用。缺省契约仍解码历史 capsule; 显式 null 或被修改的契约会拒绝。空/非对象 profile 保持原来的不适用行为, 停止结果仍不要求证据,已提供证据原样交给既有结果验证器。此次迁移的是适用性 判断,未改写证据内容校验,也尚未迁移全部执行路由和结算消费者。 diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index a42ce62d44..ce3a4a06ce 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -36,6 +36,11 @@ flattening the legacy authority layout. Executable-action and admitted-replan facts retain their typed owner; Codex boundary guidance uses the complete source. The original capture metadata and historical-only decoder remain supported. This changes no complete model input, effect owner or cross-attempt freshness rule. +Codex CLI and operation-equipped app-server input preparation share one complete +source check for prompt and result schema within that launch. The internal reply +returns resolved metadata without echoing the source. Standalone consumers and +post-execution result conversion still validate independently; no cross-attempt +cache or new model-facing contract is introduced. 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; @@ -58,6 +63,9 @@ C1 的 native 候选在既有 typed owner 的一次调用中组合成果读取 已准入 replan 事实仍由既有 typed owner 提供,Codex 边界指导读取完整来源。 原 capture 元数据和仅用于历史请求的 decoder 保留;完整模型输入、effect 归属及 跨 attempt 新鲜度规则不变。 +Codex CLI 与启用 operation tools 的 app-server 在一次启动的输入准备中,让提示词 +与结果 schema 共用完整来源校验;内部响应只返回已解析元数据,不原样回传来源。 +独立消费者与执行后的结果转换仍各自校验,不引入跨 attempt 缓存或新的模型侧契约。 Preview it explicitly: From f23105d6ef31bd6c0849cc54bda9859c00b332f9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:54:18 +0800 Subject: [PATCH 12/37] refactor(turn): share selected work in planning and session recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 49 +++++++++++++++++++ ...ller-executor-unified-contract-v0.zh-CN.md | 37 ++++++++++++++ docs/reference/protocols/turn-envelope-v0.md | 10 ++++ loopx/control_plane/quota/turn_envelope.ts | 20 ++++++-- loopx/control_plane/turn_driver/driver.py | 23 +++++---- .../test_turn_host_decision_capture.py | 21 ++++++-- tests/control_plane_ts/turn_envelope.test.ts | 24 +++++++++ tests/test_loopx_turn_driver.py | 43 ++++++++++++++++ 8 files changed, 212 insertions(+), 15 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index ea9ad662ac..346263e50d 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1110,6 +1110,55 @@ from 4.864 to 5.163 ms and p95 from 5.570 to 6.010 ms. These are added local cos excluding bridge transport, live composition, cold startup, models and full Turn cost. They do not qualify the original-quota outcome comparison or C1 cutover. +The candidate's ordinary planner and failed-session reconciliation now also +consume the selected work from the existing `driver_lineage` call. The shared +`selectedWorkTodo` owner places ordinary or adaptive selection into that call's +existing action-hash preimage; Python no longer selects it again or rewrites the +preimage. Python retains the persisted JSON/number codec and supplies scalar-text +compatibility hints only for historical non-string primary IDs. Complete inputs +ignore those hints and retain full source/projection integrity checks. There is +no new bridge call, model-facing field, persisted schema or cross-attempt cache. +The public action-signature operation still describes the original envelope. + +This replaces selection in the actual plan and failed-session recovery callers, +including the native CLI's original-capture retry. The remaining v0 Host, +settlement and controller readers still use their historical selection decoder; +they retire only after their callers and outstanding receipts migrate and pass +equivalent recovery qualification. This increment does not qualify complete +adapter retirement, App/SDK/benchmark adoption or original-quota net benefit. + +Qualification against immutable `334c1a772` retains complete plan/recovery outputs +on frozen File/SQLite CLI inputs. Planning still uses two bridge calls and +reconciliation one, with equal request/response bytes on these ordinary inputs. +The actual CLI/provider/Host, completion and settlement modules pass 251 cases; +ten migration assertions fail on that predecessor while nine historical-codec +cases pass there. The latter cover omitted/false/zero, whitespace, numeric, +boolean and collection-valued historical primary IDs, version/mode applicability, +original Turn keys and resumed bindings. The candidate passes all of them. + +There is no qualified latency improvement. Three balanced sequential cohorts +give 93 warm samples per arm/case. The initial source-worktree versus archived +baseline comparison has higher candidate medians in all eight ordinary and +historical plan/recovery rows. Its generated-cache inventories differ. A second +control uses clean sibling source copies, identical inventories and disabled +bytecode generation in both arms; it still has mixed or slower observations: + +| Complete-source input / operation | Initial baseline → candidate median / p95, ms | Clean control baseline → candidate median / p95, ms | +| --- | --- | --- | +| File / plan | 35.129 / 40.776 → 46.227 / 57.355 | 24.174 / 30.183 → 25.330 / 30.443 | +| File / failed-session recovery | 14.941 / 17.957 → 19.440 / 24.477 | 11.259 / 13.196 → 12.582 / 15.495 | +| SQLite / plan | 36.979 / 46.065 → 46.461 / 54.795 | 23.820 / 30.178 → 26.408 / 30.523 | +| SQLite / failed-session recovery | 17.404 / 22.606 → 18.759 / 22.139 | 12.586 / 16.141 → 11.503 / 14.124 | + +The initial negative results are retained; the control is not a historical pass +or proof of their cause. These are raw-helper observations without store IO, +not full native CLI/Turn timings, cold startup, model cost or accepted outcomes; +background host load is uncontrolled. Separate owned V8 captures identify +existing Unicode key ordering/hash work, without proving the latency cause. +Python wait/IO stacks remain unprofiled because the optional profiler is absent. +Shared ownership and byte parity do not cancel these cost gaps or earlier +negative C1 measurements. + ### C1 caller replacement and cutover unit The cutover unit is a supported entrypoint **and its downstream consumers**, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 08d20d8063..2652f99c0e 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -812,6 +812,43 @@ TypeScript 采样保持合法身份输出一致;校验 median 从 4.864 增至 5.570 增至 6.010 ms。这是局部新增成本,不含桥接、实时 composition、冷启动、模型 或完整 Turn 成本,不证明相对原 quota 的成果收益或 C1 切换资格。 +候选的普通 planner 与失败会话恢复现在也从既有 `driver_lineage` 调用消费选中工作。 +共享 `selectedWorkTodo` owner 将普通或自适应主任务选择写入该调用已有的动作 hash +preimage;Python 不再重选或重写 preimage。Python 保留已持久化 JSON/数字编码, +仅对历史非字符串 primary ID 提供 scalar-text 兼容提示;完整输入忽略该提示,并保留 +完整来源/投影一致性校验。不新增 bridge 调用、模型侧字段、持久化 schema 或跨 +attempt 缓存;公开动作签名操作仍描述原 envelope。 + +这替换了实际规划和失败会话恢复 caller 中的选择解释,包括原生 CLI 使用原 capture +重试的路径。剩余 v0 Host、结算和 controller reader 仍使用历史选择 decoder,只有其 +caller 与未结 receipt 已迁移且通过等价恢复资格后才可退役。本片不验收完整 adapter +退役、App/SDK/benchmark 采用或优于原 quota 的净收益。 + +对不可变 `334c1a772` 的资格保留了冻结 File/SQLite CLI 输入上的完整规划/恢复 +输出;规划仍为两次 bridge 调用,恢复为一次,普通输入的请求/响应字节量相同。 +实际 CLI/provider/Host、完成及结算模块 251 项通过;十个迁移断言在前序失败, +九个历史编码用例在前序通过,候选全部通过。后者覆盖缺省/false/zero、空白、 +数字、布尔与 collection-valued 历史 primary ID、版本/mode 适用性、原 Turn key +及恢复绑定。 + +尚无延迟改善资格。三组平衡顺序对照使每 arm/case 有 93 个温态样本。最初以 +实际 source worktree 对比归档基线,八个普通/历史规划和恢复行的候选 median +均升高,但两侧生成缓存清单不同。第二组控制使用干净同级来源副本、相同清单并在 +两侧关闭 bytecode 生成,仍观察到混合或更慢结果: + +| 完整来源输入/操作 | 初始基线 → 候选 median / p95,ms | 干净控制基线 → 候选 median / p95,ms | +| --- | --- | --- | +| File/规划 | 35.129 / 40.776 → 46.227 / 57.355 | 24.174 / 30.183 → 25.330 / 30.443 | +| File/失败会话恢复 | 14.941 / 17.957 → 19.440 / 24.477 | 11.259 / 13.196 → 12.582 / 15.495 | +| SQLite/规划 | 36.979 / 46.065 → 46.461 / 54.795 | 23.820 / 30.178 → 26.408 / 30.523 | +| SQLite/失败会话恢复 | 17.404 / 22.606 → 18.759 / 22.139 | 12.586 / 16.141 → 11.503 / 14.124 | + +初始负结果保留;控制对照不追认历史通过,也不证明负结果的原因。这些是不含 store +IO 的直接 helper 观察,不是完整原生 CLI/Turn、冷启动、模型成本或验收成果;后台 +宿主负载未控。另对自有进程取得的 V8 capture 指向既有 Unicode key ordering/hash +计算,但未证明延迟根因。可选 profiler 未安装,Python wait/IO stack 仍未观察。 +归属统一和字节相同不能抵销这些成本缺口或此前 C1 的负测量。 + ### C1 消费链替换与切换单元 切换单元是一个受支持入口**及其下游消费者**,不是一个输出开关。对 diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index ce3a4a06ce..71fdc78f31 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -41,6 +41,12 @@ source check for prompt and result schema within that launch. The internal reply returns resolved metadata without echoing the source. Standalone consumers and post-execution result conversion still validate independently; no cross-attempt cache or new model-facing contract is introduced. +Within the C1 candidate, planning and failed-session recovery consume ordinary +and adaptive selection in the existing checked action-hash preimage. They reuse +the typed work owner; Python preserves historical JSON/number encoding without +selecting work again. Historical non-string primary-ID hints cannot override a +complete source. Other v0 readers remain until their own migration and recovery +qualification; no extra model-facing field or bridge call is introduced. 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; @@ -66,6 +72,10 @@ C1 的 native 候选在既有 typed owner 的一次调用中组合成果读取 Codex CLI 与启用 operation tools 的 app-server 在一次启动的输入准备中,让提示词 与结果 schema 共用完整来源校验;内部响应只返回已解析元数据,不原样回传来源。 独立消费者与执行后的结果转换仍各自校验,不引入跨 attempt 缓存或新的模型侧契约。 +在 C1 候选中,规划与失败会话恢复从已有、经校验的动作 hash preimage 消费普通及 +自适应主任务选择,复用 typed 工作 owner;Python 只保留历史 JSON/数字编码, +不再重新选择。历史非字符串 primary-ID 提示不能覆盖完整来源;其他 v0 reader +须各自完成迁移及恢复资格后退役,不新增模型侧字段或 bridge 调用。 Preview it explicitly: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index f544d9c045..c910905c58 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2090,16 +2090,30 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { if (operation === "driver_lineage") { const envelope = requireJsonObject(request.envelope, "Turn driver envelope"); const decision = Object.hasOwn(request, "payload") ? completeHostDecision(request) : null; + const historical = !decision && request.historical_route_text !== undefined + ? requireJsonObject(request.historical_route_text, "historical route text") : undefined; + const orchestration = object(envelope.task_orchestration_contract); + // Old journals retain Python's scalar-text codec; selection itself belongs + // to the same owner as complete reads and settlement. A complete source + // never accepts a caller-provided historical identity override. + const selected = selectedWorkTodo({selected_todo: object(envelope.action).selected_todo, + task_orchestration_contract: historical && Object.hasOwn(historical, "primary_todo_id") + ? {...orchestration, primary_todo_id: scalarString(historical.primary_todo_id, "historical primary Todo")} + : orchestration}); + // Bind selection into the existing hash preimage. Python hashes this exact + // document with its historical number codec; no extra work view or copy is + // returned across the bridge. + const actionSignature = turnEnvelopeActionSignatureDocument(envelope); + actionSignature.action = {...object(actionSignature.action), selected_todo: selected}; return { ...(decision ? {goal_id: decision.goal_id ?? null, agent_id: object(decision.agent_identity).agent_id ?? null, todo_id: selectedWorkTodoId(decision) ?? ""} : {}), // Complete source validation above admits no caller-supplied scalar override. - route: turnEnvelopeRoute(envelope, decision || request.historical_route_text === undefined - ? undefined : requireJsonObject(request.historical_route_text, "historical route text")), + route: turnEnvelopeRoute(envelope, historical), // Keep the historical Python JSON encoding of this preimage at the // transport boundary: JavaScript number spelling can change Turn keys. - action_signature: turnEnvelopeActionSignatureDocument(request.envelope), + action_signature: actionSignature, }; } if (operation === "execution_context") { diff --git a/loopx/control_plane/turn_driver/driver.py b/loopx/control_plane/turn_driver/driver.py index 8e21729562..2919741bb2 100644 --- a/loopx/control_plane/turn_driver/driver.py +++ b/loopx/control_plane/turn_driver/driver.py @@ -73,9 +73,11 @@ def _historical_route_text(envelope: Mapping[str, Any]) -> dict[str, str]: Complete contracts do not accept these historical decoding hints. """ signature = _mapping(envelope.get("action_signature")) + primary = _mapping(envelope.get("task_orchestration_contract")).get("primary_todo_id") return {"source_hash": str(signature.get("source_hash") or ""), "envelope_hash": str(signature.get("envelope_hash") or ""), - "effective_action": str(envelope.get("effective_action") or "")} + "effective_action": str(envelope.get("effective_action") or ""), + **({"primary_todo_id": str(primary).strip()} if primary and not isinstance(primary, str) else {})} def _typed_route(envelope: Mapping[str, Any]) -> LoopXTurnRoute: @@ -139,7 +141,6 @@ def _receipt_bound_replan_identity( def _turn_lineage( envelope: Mapping[str, Any], *, - selected_todo: Mapping[str, Any], work_contract: object = _ABSENT_WORK_CONTRACT, projection: Mapping[str, Any] | None = None, ) -> dict[str, str]: @@ -148,14 +149,14 @@ def _turn_lineage( **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else {"historical_route_text": _historical_route_text(envelope)}), ) + signature = resolved["action_signature"] + selected_todo = _mapping(_mapping(signature.get("action")).get("selected_todo")) if work_contract is _ABSENT_WORK_CONTRACT: # Historical journals retain the exact decoder and JSON preimage. identity = {"goal_id": envelope.get("goal_id"), "agent_id": envelope.get("agent_id"), "todo_id": selected_todo.get("todo_id")} else: identity = resolved - signature = resolved["action_signature"] - signature["action"] = {**_mapping(signature.get("action")), "selected_todo": dict(selected_todo)} # Persisted Turn keys depend on Python's JSON number encoding. Keep this # transport codec while the TS owner selects and validates identity facts. return { @@ -249,10 +250,15 @@ def reconcile_failed_turn_session_request( """Re-authorize a journaled host request against one current binding.""" envelope = _mapping(request.get("turn_envelope")) - selected_todo = selected_turn_todo(envelope) try: + projection = _turn_envelope_result( + "driver_lineage", envelope=envelope, + **({"payload": request["work_contract"]} if "work_contract" in request else + {"historical_route_text": _historical_route_text(envelope)}), + ) + selected_todo = _mapping(_mapping(projection["action_signature"].get("action")).get("selected_todo")) lineage = _turn_lineage( - envelope, selected_todo=selected_todo, + envelope, projection=projection, work_contract=request.get("work_contract", _ABSENT_WORK_CONTRACT), ) settlement_identity = (None if selected_todo else _receipt_bound_replan_identity( @@ -421,7 +427,6 @@ def build_loopx_turn_plan( assert_turn_envelope_consumer_version(envelope) except (ValueError, RuntimeError) as exc: contract_error = str(exc) - selected_todo = selected_turn_todo(envelope) # Check the complete source before route/session/transaction planning. Reuse # the action-hash boundary rather than adding a separate identity call. projection = _turn_envelope_result( @@ -429,8 +434,8 @@ def build_loopx_turn_plan( **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else {"historical_route_text": _historical_route_text(envelope)}), ) - lineage = _turn_lineage(envelope, selected_todo=selected_todo, - work_contract=work_contract, projection=projection) + selected_todo = _mapping(_mapping(projection["action_signature"].get("action")).get("selected_todo")) + lineage = _turn_lineage(envelope, work_contract=work_contract, projection=projection) route = ( LoopXTurnRoute(projection["route"]) if execution_context.ok and contract_error is None diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 3e32eda04d..5630b08de4 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -69,7 +69,16 @@ def observe_launch_plan(envelope, **kwargs): # Observe the real live reader and planner; neither is replaced by a # fixture result. Every launch, including retry, uses the same contract. assert kwargs["work_contract"]["schema_version"] == "loopx_turn_envelope_v1" - planned = original_planner(envelope, **kwargs) + # The actual native caller must consume the typed owner's selected + # work, not independently decode the historical adapter again. + from loopx.control_plane.turn_driver import driver + + def legacy_selection(*_args, **_kwargs): + raise AssertionError("native planning reinterpreted historical selection") + + with monkeypatch.context() as planning: + planning.setattr(driver, "selected_turn_todo", legacy_selection) + planned = original_planner(envelope, **kwargs) launch_plans.append(planned) return planned @@ -699,7 +708,7 @@ def test_complete_codex_approval_guidance_uses_resolved_boundary(complete_host_r assert ("It satisfies the write approval requirement" in prompt) is expected -def test_complete_planning_and_failed_recovery_share_lineage(complete_host_request): +def test_complete_planning_and_failed_recovery_share_lineage(complete_host_request, monkeypatch): from copy import deepcopy from hashlib import sha256 from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision @@ -708,6 +717,12 @@ def test_complete_planning_and_failed_recovery_share_lineage(complete_host_reque ) request, original = complete_host_request + from loopx.control_plane.turn_driver import driver + + def legacy_selection(*_args, **_kwargs): + raise AssertionError("planning/recovery reinterpreted historical selection") + + monkeypatch.setattr(driver, "selected_turn_todo", legacy_selection) source = {k: v for k, v in original.items() if k not in {"schema_version", "contract_binding"}} # Hash parity protects existing journals, including numeric encodings and # Unicode in retained source facts; a new hash silently forks the Turn. @@ -731,7 +746,7 @@ def test_complete_planning_and_failed_recovery_share_lineage(complete_host_reque signature["action"]["selected_todo"] = complete["route"]["selected_todo"] expected_hash = "sha256:" + sha256(json.dumps(signature, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest() - lineage = _turn_lineage(envelope, selected_todo=complete["route"]["selected_todo"], work_contract=contract) + lineage = _turn_lineage(envelope, work_contract=contract) assert lineage["action_hash"] == expected_hash assert lineage["todo_id"] == expected_todo binding = {"schema_version": "loopx_turn_session_binding_v0", "host_session_id": "fixture-session", diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 1922c407e7..330fddcdec 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -145,6 +145,30 @@ function payload(): Record { }; } +test("planning selection shares the work owner and complete sources ignore historical identity hints", () => { + for (const adaptive of [false, true]) { + const source = payload(); + if (adaptive) source.task_orchestration_contract = { + schema_version: "task_orchestration_contract_v2", mode: "adaptive", primary_todo_id: "todo-primary", + }; + const complete = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + const envelope = buildTurnEnvelope({payload: complete, scheduler_execution_args: ""}); + const expected = adaptive ? {todo_id: "todo-primary", source: "task_orchestration_contract.primary_todo_id"} + : (envelope.action as JsonObject).selected_todo; + const snapshot = structuredClone({complete, envelope}); + const projection = evaluateTurnEnvelope({operation: "driver_lineage", envelope, payload: complete, + historical_route_text: {primary_todo_id: "foreign", effective_action: "foreign"}}); + assert.deepEqual((projection.action_signature as JsonObject).action, + {...(turnEnvelopeActionSignatureDocument(envelope).action as JsonObject), selected_todo: expected}); + assert.equal(projection.todo_id, adaptive ? "todo-primary" : "todo-1"); + assert.deepEqual({complete, envelope}, snapshot); + // An omitted source keeps the old decoder; supplied null remains invalid. + const historical = evaluateTurnEnvelope({operation: "driver_lineage", envelope}); + assert.deepEqual(((historical.action_signature as JsonObject).action as JsonObject).selected_todo, expected); + assert.throws(() => evaluateTurnEnvelope({operation: "driver_lineage", envelope, payload: null})); + } +}); + test("managed controller builds its unchanged envelope and typed route in one projection", () => { for (const [action, delivery, shouldRun, quiet, expected] of [ ["normal_run", true, true, false, "ready_for_host"], diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index cf651103f1..50f8015dd0 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -84,6 +84,49 @@ def _envelope( } +@pytest.mark.parametrize("primary, expected", [ + (None, None), (False, None), (0, None), (" \t", None), + (" todo_other ", "todo_other"), (True, "True"), (1e-7, "1e-07"), + (["owner"], "['owner']"), ({"id": "owner"}, "{'id': 'owner'}"), +]) +def test_planning_and_recovery_retain_historical_selected_work_codec(primary, expected): + """Old journal identity/hash encoding survives migration to the typed selector.""" + from hashlib import sha256 + from loopx.control_plane.quota.turn_envelope import turn_envelope_action_signature_document + from loopx.control_plane.turn_driver.driver import reconcile_failed_turn_session_request + + for schema, mode in (("task_orchestration_contract_v2", "adaptive"), + ("task_orchestration_contract_v1", "adaptive"), + ("task_orchestration_contract_v2", "parallel")): + envelope = _envelope() + envelope["task_orchestration_contract"] = { + "schema_version": schema, "mode": mode, "primary_todo_id": primary, + } + selected = ({"todo_id": expected, "source": "task_orchestration_contract.primary_todo_id"} + if expected is not None and schema == "task_orchestration_contract_v2" and mode == "adaptive" + else envelope["action"]["selected_todo"]) + signature = turn_envelope_action_signature_document(envelope) + signature["action"]["selected_todo"] = selected + action_hash = "sha256:" + sha256(json.dumps(signature, ensure_ascii=False, + sort_keys=True, separators=(",", ":")).encode()).hexdigest() + plan = build_loopx_turn_plan(envelope, host="codex-cli", execution_mode="isolated-headless", + session_scope="todo", turn_instance_id="legacy-selected-work") + assert plan["route"]["selected_todo"] == selected + identity = {"lineage": {"goal_id": "fixture-goal", "agent_id": "codex-fixture", + "todo_id": selected["todo_id"], "action_hash": action_hash}, + "host": "codex-cli", "execution_mode": "isolated-headless", + "scheduler_owner": plan["host"]["scheduler_owner"], "session_action": "start_new", + "turn_instance_id": "legacy-selected-work"} + assert plan["transaction"]["turn_key"] == "sha256:" + sha256(json.dumps(identity, ensure_ascii=False, + sort_keys=True, separators=(",", ":")).encode()).hexdigest() + binding = {"schema_version": LOOPX_TURN_SESSION_BINDING_SCHEMA_VERSION, + "goal_id": "fixture-goal", "agent_id": "codex-fixture", "todo_id": selected["todo_id"]} + recovered = reconcile_failed_turn_session_request({"turn_envelope": envelope, "session": plan["session"]}, + session_binding=binding) + assert recovered["session"]["action"] == "resume" + assert recovered["session"]["binding_status"] == "failed_turn_recovery" + + def test_turn_plan_projects_ready_route_without_side_effects() -> None: envelope = _envelope() From 49e69f2ea4d45f402b7ba7b91d133d0986fd1bef Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:07:25 +0800 Subject: [PATCH 13/37] fix(turn): retain historical whitespace in selected work identity Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 33 ++++++++++++++++--- ...ller-executor-unified-contract-v0.zh-CN.md | 30 ++++++++++++++--- docs/reference/protocols/turn-envelope-v0.md | 4 +-- loopx/control_plane/quota/turn_envelope.ts | 9 +++-- loopx/control_plane/turn_driver/driver.py | 5 +-- .../work_items/interaction_contract.ts | 11 ++++--- tests/test_loopx_turn_driver.py | 2 ++ 7 files changed, 72 insertions(+), 22 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 346263e50d..28eb5261fa 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1114,8 +1114,9 @@ The candidate's ordinary planner and failed-session reconciliation now also consume the selected work from the existing `driver_lineage` call. The shared `selectedWorkTodo` owner places ordinary or adaptive selection into that call's existing action-hash preimage; Python no longer selects it again or rewrites the -preimage. Python retains the persisted JSON/number codec and supplies scalar-text -compatibility hints only for historical non-string primary IDs. Complete inputs +preimage. Python retains the persisted JSON/number and primary-ID scalar/whitespace codec. +The shared owner consumes already-decoded historical text without applying JS +trim again. Complete inputs ignore those hints and retain full source/projection integrity checks. There is no new bridge call, model-facing field, persisted schema or cross-attempt cache. The public action-signature operation still describes the original envelope. @@ -1130,12 +1131,17 @@ adapter retirement, App/SDK/benchmark adoption or original-quota net benefit. Qualification against immutable `334c1a772` retains complete plan/recovery outputs on frozen File/SQLite CLI inputs. Planning still uses two bridge calls and reconciliation one, with equal request/response bytes on these ordinary inputs. -The actual CLI/provider/Host, completion and settlement modules pass 251 cases; -ten migration assertions fail on that predecessor while nine historical-codec +The actual CLI/provider/Host, completion and settlement modules pass 254 cases; +ten migration assertions fail on that predecessor while twelve historical-codec cases pass there. The latter cover omitted/false/zero, whitespace, numeric, -boolean and collection-valued historical primary IDs, version/mode applicability, +boolean and collection-valued historical primary IDs, Unicode whitespace/BOM, version/mode applicability, original Turn keys and resumed bindings. The candidate passes all of them. +A temporary `ef479d2b5` trial changed three Unicode whitespace/BOM identities; +those semantic counterexamples fail there and pass on the corrected owner, +while the original predecessor preserves them. Its measurements below remain +historical trial evidence, not qualification of the corrected source. + There is no qualified latency improvement. Three balanced sequential cohorts give 93 warm samples per arm/case. The initial source-worktree versus archived baseline comparison has higher candidate medians in all eight ordinary and @@ -1159,6 +1165,23 @@ Python wait/IO stacks remain unprofiled because the optional profiler is absent. Shared ownership and byte parity do not cancel these cost gaps or earlier negative C1 measurements. +The corrected three-file source has a separate clean-source comparison, again +93 warm samples per arm/case with all eight complete outputs equal and unchanged +RPC/ordinary-input traffic. It retains mixed results, including a worse SQLite +planning tail. The same exclusions and uncontrolled-load limits apply: + +| Input / operation | Baseline median / p95, ms | Corrected source median / p95, ms | +| --- | --- | --- | +| File / complete plan | 38.529 / 48.381 | 38.504 / 47.134 | +| File / complete recovery | 17.821 / 22.029 | 18.244 / 21.857 | +| File / historical plan | 32.787 / 44.413 | 30.871 / 36.733 | +| File / historical recovery | 10.635 / 14.620 | 10.292 / 12.201 | +| SQLite / complete plan | 39.600 / 46.547 | 40.036 / 77.714 | +| SQLite / complete recovery | 18.034 / 28.685 | 18.552 / 25.223 | +| SQLite / historical plan | 32.061 / 42.789 | 32.447 / 38.962 | +| SQLite / historical recovery | 10.444 / 14.258 | 10.270 / 12.891 | + + ### C1 caller replacement and cutover unit The cutover unit is a supported entrypoint **and its downstream consumers**, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 2652f99c0e..42d8f9abbf 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -815,7 +815,8 @@ TypeScript 采样保持合法身份输出一致;校验 median 从 4.864 增至 候选的普通 planner 与失败会话恢复现在也从既有 `driver_lineage` 调用消费选中工作。 共享 `selectedWorkTodo` owner 将普通或自适应主任务选择写入该调用已有的动作 hash preimage;Python 不再重选或重写 preimage。Python 保留已持久化 JSON/数字编码, -仅对历史非字符串 primary ID 提供 scalar-text 兼容提示;完整输入忽略该提示,并保留 +同时运输历史 primary-ID scalar/空白编码;共享 owner 消费已解码历史文本,不再重复 +执行 JS trim。完整输入忽略该提示,并保留 完整来源/投影一致性校验。不新增 bridge 调用、模型侧字段、持久化 schema 或跨 attempt 缓存;公开动作签名操作仍描述原 envelope。 @@ -826,11 +827,16 @@ caller 与未结 receipt 已迁移且通过等价恢复资格后才可退役。 对不可变 `334c1a772` 的资格保留了冻结 File/SQLite CLI 输入上的完整规划/恢复 输出;规划仍为两次 bridge 调用,恢复为一次,普通输入的请求/响应字节量相同。 -实际 CLI/provider/Host、完成及结算模块 251 项通过;十个迁移断言在前序失败, -九个历史编码用例在前序通过,候选全部通过。后者覆盖缺省/false/zero、空白、 -数字、布尔与 collection-valued 历史 primary ID、版本/mode 适用性、原 Turn key +实际 CLI/provider/Host、完成及结算模块 254 项通过;十个迁移断言在前序失败, +十二个历史编码用例在前序通过,候选全部通过。后者覆盖缺省/false/zero、空白、 +数字、布尔与 collection-valued 历史 primary ID、Unicode 空白/BOM、版本/mode +适用性、原 Turn key 及恢复绑定。 +临时 `ef479d2b5` 试验改变了三种 Unicode 空白/BOM 身份:语义反例在该版本失败, +在修正 owner 后通过,而原始前序保留这些身份。下述该版本测量仍是历史试验, +不作为修正源码的资格。 + 尚无延迟改善资格。三组平衡顺序对照使每 arm/case 有 93 个温态样本。最初以 实际 source worktree 对比归档基线,八个普通/历史规划和恢复行的候选 median 均升高,但两侧生成缓存清单不同。第二组控制使用干净同级来源副本、相同清单并在 @@ -849,6 +855,22 @@ IO 的直接 helper 观察,不是完整原生 CLI/Turn、冷启动、模型 计算,但未证明延迟根因。可选 profiler 未安装,Python wait/IO stack 仍未观察。 归属统一和字节相同不能抵销这些成本缺口或此前 C1 的负测量。 +修正后的三个源码文件另有干净来源对照,同样每 arm/case 93 个温态样本,八项 +完整输出相同,RPC 与普通输入传输量不变。结果仍混合,SQLite 规划尾延迟更差; +同样保留上述未测范围和后台负载未控限制: + +| 输入/操作 | 基线 median / p95,ms | 修正源码 median / p95,ms | +| --- | --- | --- | +| File/完整规划 | 38.529 / 48.381 | 38.504 / 47.134 | +| File/完整恢复 | 17.821 / 22.029 | 18.244 / 21.857 | +| File/历史规划 | 32.787 / 44.413 | 30.871 / 36.733 | +| File/历史恢复 | 10.635 / 14.620 | 10.292 / 12.201 | +| SQLite/完整规划 | 39.600 / 46.547 | 40.036 / 77.714 | +| SQLite/完整恢复 | 18.034 / 28.685 | 18.552 / 25.223 | +| SQLite/历史规划 | 32.061 / 42.789 | 32.447 / 38.962 | +| SQLite/历史恢复 | 10.444 / 14.258 | 10.270 / 12.891 | + + ### C1 消费链替换与切换单元 切换单元是一个受支持入口**及其下游消费者**,不是一个输出开关。对 diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 71fdc78f31..e93b226269 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -44,7 +44,7 @@ cache or new model-facing contract is introduced. Within the C1 candidate, planning and failed-session recovery consume ordinary and adaptive selection in the existing checked action-hash preimage. They reuse the typed work owner; Python preserves historical JSON/number encoding without -selecting work again. Historical non-string primary-ID hints cannot override a +selecting work again. Historical primary-ID scalar/whitespace hints are not trimmed again and cannot override a complete source. Other v0 readers remain until their own migration and recovery qualification; no extra model-facing field or bridge call is introduced. @@ -74,7 +74,7 @@ Codex CLI 与启用 operation tools 的 app-server 在一次启动的输入准 独立消费者与执行后的结果转换仍各自校验,不引入跨 attempt 缓存或新的模型侧契约。 在 C1 候选中,规划与失败会话恢复从已有、经校验的动作 hash preimage 消费普通及 自适应主任务选择,复用 typed 工作 owner;Python 只保留历史 JSON/数字编码, -不再重新选择。历史非字符串 primary-ID 提示不能覆盖完整来源;其他 v0 reader +不再重新选择。历史 primary-ID scalar/空白提示不再重复 trim,也不能覆盖完整来源;其他 v0 reader 须各自完成迁移及恢复资格后退役,不新增模型侧字段或 bridge 调用。 Preview it explicitly: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index c910905c58..9447227178 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2092,14 +2092,13 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { const decision = Object.hasOwn(request, "payload") ? completeHostDecision(request) : null; const historical = !decision && request.historical_route_text !== undefined ? requireJsonObject(request.historical_route_text, "historical route text") : undefined; - const orchestration = object(envelope.task_orchestration_contract); - // Old journals retain Python's scalar-text codec; selection itself belongs + // Old journals retain Python's scalar/whitespace codec; selection belongs // to the same owner as complete reads and settlement. A complete source // never accepts a caller-provided historical identity override. const selected = selectedWorkTodo({selected_todo: object(envelope.action).selected_todo, - task_orchestration_contract: historical && Object.hasOwn(historical, "primary_todo_id") - ? {...orchestration, primary_todo_id: scalarString(historical.primary_todo_id, "historical primary Todo")} - : orchestration}); + task_orchestration_contract: envelope.task_orchestration_contract}, + historical && Object.hasOwn(historical, "primary_todo_id") + ? scalarString(historical.primary_todo_id, "historical primary Todo") : undefined); // Bind selection into the existing hash preimage. Python hashes this exact // document with its historical number codec; no extra work view or copy is // returned across the bridge. diff --git a/loopx/control_plane/turn_driver/driver.py b/loopx/control_plane/turn_driver/driver.py index 2919741bb2..feb55fec8d 100644 --- a/loopx/control_plane/turn_driver/driver.py +++ b/loopx/control_plane/turn_driver/driver.py @@ -73,11 +73,12 @@ def _historical_route_text(envelope: Mapping[str, Any]) -> dict[str, str]: Complete contracts do not accept these historical decoding hints. """ signature = _mapping(envelope.get("action_signature")) - primary = _mapping(envelope.get("task_orchestration_contract")).get("primary_todo_id") + orchestration = _mapping(envelope.get("task_orchestration_contract")) return {"source_hash": str(signature.get("source_hash") or ""), "envelope_hash": str(signature.get("envelope_hash") or ""), "effective_action": str(envelope.get("effective_action") or ""), - **({"primary_todo_id": str(primary).strip()} if primary and not isinstance(primary, str) else {})} + **({"primary_todo_id": str(orchestration.get("primary_todo_id") or "").strip()} + if "primary_todo_id" in orchestration else {})} def _typed_route(envelope: Mapping[str, Any]) -> LoopXTurnRoute: diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index 9656e2f62a..d51fbbd266 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -759,11 +759,14 @@ function shellQuote(value: string): string { return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`; } -/** The adaptive primary is the work identity for both reads and settlement. */ -export function selectedWorkTodo(packet: JsonObject): JsonObject { +/** The adaptive primary is the work identity for both reads and settlement. + * Historical callers may supply their already-decoded text: reapplying JS + * trim would change Python whitespace/BOM identities in persisted journals. + */ +export function selectedWorkTodo(packet: JsonObject, historicalPrimaryText?: string): JsonObject { const orchestration = jsonObject(packet.task_orchestration_contract) ?? {}; - const primaryTodoId = typeof orchestration.primary_todo_id === "string" - ? orchestration.primary_todo_id.trim() : ""; + const primaryTodoId = historicalPrimaryText ?? (typeof orchestration.primary_todo_id === "string" + ? orchestration.primary_todo_id.trim() : ""); if (orchestration.schema_version === "task_orchestration_contract_v2" && orchestration.mode === "adaptive" && primaryTodoId) { return {todo_id: primaryTodoId, source: "task_orchestration_contract.primary_todo_id"}; diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 50f8015dd0..472e22e6c1 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -88,6 +88,8 @@ def _envelope( (None, None), (False, None), (0, None), (" \t", None), (" todo_other ", "todo_other"), (True, "True"), (1e-7, "1e-07"), (["owner"], "['owner']"), ({"id": "owner"}, "{'id': 'owner'}"), + ("\x1c todo_other \x1f", "todo_other"), ("\u0085todo_other\u0085", "todo_other"), + ("\ufefftodo_other\ufeff", "\ufefftodo_other\ufeff"), ]) def test_planning_and_recovery_retain_historical_selected_work_codec(primary, expected): """Old journal identity/hash encoding survives migration to the typed selector.""" From 5b2e91d373272b51f3dbee5ad113d9fcc016bd15 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:49:25 +0800 Subject: [PATCH 14/37] fix(turn): bind completion classification to the captured source Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 67 ++++++++++++++++++ ...ller-executor-unified-contract-v0.zh-CN.md | 50 ++++++++++++++ docs/reference/protocols/turn-envelope-v0.md | 17 +++++ loopx/cli_commands/turn_decision.py | 4 +- loopx/cli_commands/turn_run_once.py | 3 +- loopx/cli_commands/turn_todo_writeback.py | 52 +------------- loopx/control_plane/turn_driver/driver.py | 44 ++++++++++++ loopx/control_plane/turn_driver/settlement.py | 13 +++- .../test_turn_completion_source.py | 68 ++++++++++++++++++- .../test_turn_host_decision_capture.py | 43 +++++++++++- 10 files changed, 303 insertions(+), 58 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 28eb5261fa..b72ccb1e00 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1182,6 +1182,73 @@ planning tail. The same exclusions and uncontrolled-load limits apply: | SQLite / historical recovery | 10.444 / 14.258 | 10.270 / 12.891 | +#### Native execution and completion source + +The native launch guard now consumes the selected Todo already returned by the +shared planner, rather than running the historical Python selector again. +The existing execution-context/task-commitment reader lives with the Turn +driver and serves both CLI preparation and completion-intent classification; +its CLI copy is removed. The semantic owner remains the existing TypeScript +`execution_context` operation, not a new view, schema or Python decision owner. + +After Host execution and independent task validation, a complete request checks +that source again before choosing terminal versus ordinary completion. A saved +writeback or terminal outcome does not bypass this check. Malformed complete +input and adapter facts inconsistent with its source reject before writeback or spend; +the original journal can then recover the already validated result without +another Host invocation and settle once. Omitted historical inputs retain their +exact decoder and receipt recovery. Completion-outcome validation and the +typed settlement/effect owner are unchanged; this does not retire every +historical completion or controller reader. + +The launch guard adds no bridge call. Complete validated-completion +classification adds one coarse source check at this later boundary, including +unfinished journal recovery; historical classification is unchanged. That is +an explicit additional cost, not a speedup or permission to cache a pre-execution +check across execution, attempts or recovery. Original-quota decisions, +accepted outcomes, complete costs and remaining consumer adoption stay open. + +The four affected capture/driver/completion/settlement modules pass 264 cases +with real isolated File/SQLite CLI consumers and synthetic Host/validator +processes; the two typed-owner modules pass 79 cases. Twenty-two migration +assertions fail on immutable `2a170247ba3` and pass on the candidate. Eight +additional native replacement cases commit on that baseline but reject before +writeback/spend on the candidate, then recover the original journal result +without rerunning the Host and debit once. These cover ordinary and final +completion on both backends; they do not establish live model or installed +consumer adoption. + +Three balanced sequential cohorts use clean sibling source copies and the same +fixtures. Each warm helper row has 93 samples per arm. Complete intent/recovery +classification changes from about 0.002 ms with no bridge call to these +median/p95 values: File 19.813/26.454 and 20.770/25.287 ms; SQLite +20.243/23.964 and 19.005/22.458 ms. Each adds one logical request with +43,200/43,253 request bytes respectively and 1,693 result bytes. Historical +classification retains zero requests and equivalent outcomes. Logical JSON +counts exclude framing and physical transport retries. + +The same cohorts also execute real CLI admission, capture, synthetic Host work, +independent validation and settlement, followed by native Todo readback and +committed replay. Each row has only three samples per arm; the table reports +median and observed range, not a p95 or causal speedup: + +| CLI completion | Baseline median [range], ms | Candidate median [range], ms | Logical requests, base → candidate | +| --- | --- | --- | --- | +| File / ordinary | 2303.0 [2001.9–3646.5] | 2173.5 [2051.4–2196.6] | 282 → 283 | +| File / final | 2306.0 [2283.4–2389.4] | 2818.4 [2338.2–2973.9] | 294 → 295 | +| SQLite / ordinary | 1888.5 [1758.5–2061.7] | 2513.5 [2086.5–2955.7] | 278 → 279 | +| SQLite / final | 2549.1 [2240.7–2681.2] | 2282.7 [2276.8–4064.8] | 294 → 295 | + +Native outcome observations agree, including the original settlement identity, +done readback and one debit. Committed replay retains five logical requests and +zero new effects in both arms. Readback/replay observations are retained +separately from the timed execution. Background host load is uncontrolled; +no other owned validation runs alongside these cohorts. The added source check +is a demonstrated cost, while whole-CLI timing remains mixed and default SQLite +performance is not qualified. Models/cache tokens, billing, evaluation, +implementation/human costs, accepted real-work outcomes and original-quota net +benefit remain unqualified; earlier negative observations remain valid. + ### C1 caller replacement and cutover unit The cutover unit is a supported entrypoint **and its downstream consumers**, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 42d8f9abbf..a7d6444bc7 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -871,6 +871,56 @@ IO 的直接 helper 观察,不是完整原生 CLI/Turn、冷启动、模型 | SQLite/历史恢复 | 10.444 / 14.258 | 10.270 / 12.891 | +#### 原生执行与完成来源 + +原生启动 guard 现在消费共享 planner 已返回的选中 Todo,不再调用历史 Python +selector 重选。既有执行上下文/任务承诺 reader 归 Turn driver 所有,同时供 CLI +准备和完成意图分类使用;CLI 副本已删除。语义 owner 仍为既有 TypeScript +`execution_context` 操作,没有新增视图、schema 或 Python 决策 owner。 + +Host 执行和独立任务验证之后,完整请求在判断终态或普通完成前重新检查该来源。 +已保存的 writeback 或 terminal outcome 不能跳过检查。无效完整输入或 +与来源冲突的 adapter 事实在写回/扣额前拒绝;原 journal 随后可以恢复已有验证结果, +不再次调用 Host,并只结算一次。省略完整输入的历史请求保留精确 decoder 和 +回执恢复。完成 outcome 验证及 typed 结算/effect owner 不变;本片没有退役所有 +历史完成或 controller reader。 + +启动 guard 不增加 bridge 调用。完整 validated-completion 分类在此后续边界增加 +一次粗粒度来源检查,包括未完成 journal 恢复;历史分类不变。这是明确新增成本, +不代表提速,也不能据此跨执行、attempt 或恢复缓存执行前检查。原 quota 的实际 +决策、验收成果、完整成本及剩余消费者采用仍保持开放。 + +四个受影响的 capture/driver/completion/settlement 模块共 264 项通过,使用 +真实隔离 File/SQLite CLI 消费者及合成 Host/validator 进程;两个 typed owner +模块 79 项通过。22 个迁移断言在不可变 `2a170247ba3` 失败、候选通过。新增的 +8 个原生替换反例在基线会提交完成结果,候选在写回/扣额前拒绝,随后从原 +journal 恢复已有结果,不重跑 Host 并只扣额一次;覆盖两后端的普通/最终完成。 +这些不证明真实模型或已安装消费者采用。 + +三组平衡顺序对照使用干净同级来源副本与相同夹具,每个温态 helper 行各有 +93 个样本。完整 intent/recovery 分类从约 0.002 ms、零 bridge 请求变为: +File median/p95 分别 19.813/26.454 和 20.770/25.287 ms;SQLite 分别 +20.243/23.964 和 19.005/22.458 ms。每次新增一个逻辑请求,File/SQLite +请求分别 43,200/43,253 字节,结果均 1,693 字节。历史分类仍零请求、结果等价。 +逻辑 JSON 计量不含 framing 或物理传输重试。 + +同组对照还运行真实 CLI 准入、捕获、合成 Host 工作、独立验证与结算,再通过 +原生入口回读 Todo 并重放已提交结果。每行每 arm 仅三个样本;下表为 median +和实际范围,不推 p95 或因果提速: + +| CLI 完成 | 基线 median [范围],ms | 候选 median [范围],ms | 逻辑请求,基线 → 候选 | +| --- | --- | --- | --- | +| File/普通 | 2303.0 [2001.9–3646.5] | 2173.5 [2051.4–2196.6] | 282 → 283 | +| File/最终 | 2306.0 [2283.4–2389.4] | 2818.4 [2338.2–2973.9] | 294 → 295 | +| SQLite/普通 | 1888.5 [1758.5–2061.7] | 2513.5 [2086.5–2955.7] | 278 → 279 | +| SQLite/最终 | 2549.1 [2240.7–2681.2] | 2282.7 [2276.8–4064.8] | 294 → 295 | + +原生结果观察一致,包括原结算身份、done 回读和一次扣额;两侧已提交重放均为 +五次逻辑请求、零新增 effect。回读/重放观察与执行计时分别保留。后台宿主负载 +未控,这些对照未与其他自有验证并行。新增来源检查是实测成本;完整 CLI 时延 +仍混合,默认 SQLite 性能尚未资格。模型/缓存 tokens、账单、评测、实现/人工 +成本、真实工作验收成果和优于原 quota 的净益仍未知,所有既有负结果保持。 + ### C1 消费链替换与切换单元 切换单元是一个受支持入口**及其下游消费者**,不是一个输出开关。对 diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index e93b226269..2bf9e60477 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -48,6 +48,16 @@ selecting work again. Historical primary-ID scalar/whitespace hints are not trim complete source. Other v0 readers remain until their own migration and recovery qualification; no extra model-facing field or bridge call is introduced. +The native launch check now reuses the planner's selected work. CLI preparation +and post-validation completion classification share the existing execution-context +reader in `turn_driver/driver.py`, backed by the typed source owner. Completion +classification rechecks complete source/projection integrity even when its outcome +is already saved in the journal; malformed supplied input cannot use historical +fallback. Historical omission retains its decoder and accepted-receipt recovery. +This adds one coarse request per complete completion classification, with no +additional launch request. It preserves original Turn identity, effect order and +once-only recovery; it does not establish a net efficiency improvement. + 中文:本页记录当前版本。[统一契约提案](../../architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md) 的终态是一份包含差分交付能力的完整契约,取消线上 quota/envelope 格式选择; 历史捕获保留原 schema 与签名。本次文档更新不新增 CLI 开关。 @@ -77,6 +87,13 @@ Codex CLI 与启用 operation tools 的 app-server 在一次启动的输入准 不再重新选择。历史 primary-ID scalar/空白提示不再重复 trim,也不能覆盖完整来源;其他 v0 reader 须各自完成迁移及恢复资格后退役,不新增模型侧字段或 bridge 调用。 +原生启动检查现在复用 planner 已返回的选中工作。CLI 准备与任务验证后的完成分类 +共用 `turn_driver/driver.py` 中的既有 execution-context reader,由 typed 来源 +owner 校验。即使 journal 已保存完成 outcome,仍重新检查完整来源/投影一致性; +显式无效输入不能走历史回退。省略完整输入的历史请求保留 decoder 和已接受回执 +恢复。完整完成分类每次新增一次粗粒度请求,启动检查不新增请求;原 Turn 身份、 +effect 顺序和一次恢复语义保持,不据此认定净效率提升。 + Preview it explicitly: ```bash diff --git a/loopx/cli_commands/turn_decision.py b/loopx/cli_commands/turn_decision.py index fe4a8661fb..08c762a063 100644 --- a/loopx/cli_commands/turn_decision.py +++ b/loopx/cli_commands/turn_decision.py @@ -42,7 +42,7 @@ heartbeat_receipt_settlement_todo_id, ) from ..control_plane.goals.first_party_host_admission import FirstPartyHostRuntimeRejected -from ..control_plane.turn_driver.driver import build_loopx_turn_plan, selected_turn_todo +from ..control_plane.turn_driver.driver import build_loopx_turn_plan from ..control_plane.scheduler.execution_context import ( SchedulerExecutionContextResolution, scheduler_execution_context_for_turn, @@ -381,7 +381,7 @@ def current_turn_host_launch_decision( route = plan["route"] if route.get("would_invoke_host") is not True: raise FirstPartyHostRuntimeRejected("goal_launch_not_allowed") - if selected_turn_todo(current_envelope).get("todo_id") != identity.todo_id: + if (plan["route"].get("selected_todo") or {}).get("todo_id") != identity.todo_id: raise FirstPartyHostRuntimeRejected("current_turn_selection_changed") if identity.replan_obligation_id and plan["transaction"]["settlement_plan"]["identity"] != identity.as_dict(): raise FirstPartyHostRuntimeRejected("current_turn_selection_changed") diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index 3ad467d22a..2f33df0721 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -51,7 +51,8 @@ from .turn_dsh_host import build_dsh_host_runner, build_dsh_operation_host_runner from .turn_rendering import build_turn_error_payload from .turn_post_writeback import drain_committed_turn_post_writeback_hooks -from .turn_todo_writeback import read_turn_execution_context, write_turn_validated_completion +from ..control_plane.turn_driver.driver import read_turn_execution_context +from .turn_todo_writeback import write_turn_validated_completion EXACT_SETTLEMENT_READBACK_NOT_FOUND = ( "exact settlement readback unexpectedly returned not-found" diff --git a/loopx/cli_commands/turn_todo_writeback.py b/loopx/cli_commands/turn_todo_writeback.py index 5863187eef..ab547b1b6e 100644 --- a/loopx/cli_commands/turn_todo_writeback.py +++ b/loopx/cli_commands/turn_todo_writeback.py @@ -1,11 +1,9 @@ -"""Captured Turn execution context and Todo writeback transport.""" +"""Native Todo completion writeback transport.""" from __future__ import annotations from pathlib import Path from typing import Any, Mapping -from ..control_plane.effect_runtime import EffectRuntimeRejected -from ..control_plane.quota.turn_envelope import _turn_envelope_result from ..control_plane.todos.mutation_api import complete_goal_todo from ..control_plane.coordination.local_authority import ( @@ -13,54 +11,6 @@ ) -_ABSENT_WORK_CONTRACT = object() - - -def captured_todo_text_sha256( - envelope: Mapping[str, Any], *, todo_id: str, - work_contract: object = _ABSENT_WORK_CONTRACT, -) -> str | None: - """Use the shared task-source owner; omitted contracts decode old journals.""" - try: - result = _turn_envelope_result( - "captured_todo_text", envelope=dict(envelope), todo_id=todo_id, - **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else {}), - ) - except EffectRuntimeRejected as exc: - raise ValueError(str(exc)) from exc - digest = result.get("sha256") - if digest is not None and not isinstance(digest, str): - raise RuntimeError("Turn captured task source hash must be a string or null") - return digest - - -def read_turn_execution_context( - plan: Mapping[str, Any], envelope: Mapping[str, Any], -) -> dict[str, Any]: - """Read complete execution facts in one call; decode old journals as before.""" - if "work_contract" in plan: - try: - result = _turn_envelope_result( - "execution_context", envelope=dict(envelope), payload=plan["work_contract"], - ) - except EffectRuntimeRejected as exc: - raise ValueError(str(exc)) from exc - if result.get("sha256") is not None and not isinstance(result["sha256"], str): - raise RuntimeError("Turn captured task source hash must be a string or null") - return result - from ..control_plane.turn_driver.driver import selected_turn_todo - - selected = selected_turn_todo(envelope) - return { - "selected_todo": selected, - "sha256": captured_todo_text_sha256(envelope, todo_id=str(selected.get("todo_id") or "")), - "effective_action": envelope.get("effective_action"), - "writeback": envelope.get("writeback") if isinstance(envelope.get("writeback"), dict) else {}, - "replan_guard_scoped": "replan_action_packet" in envelope, - "replan_action_packet": envelope.get("replan_action_packet"), - } - - def write_turn_validated_completion( *, registry_path: Path, diff --git a/loopx/control_plane/turn_driver/driver.py b/loopx/control_plane/turn_driver/driver.py index feb55fec8d..f468df33f0 100644 --- a/loopx/control_plane/turn_driver/driver.py +++ b/loopx/control_plane/turn_driver/driver.py @@ -9,6 +9,7 @@ from ..agent_context import envelope_agent_context from ..effect_program import SettlementIdentity +from ..effect_runtime import EffectRuntimeRejected from .subagent_host_adapter import ( project_child_context_adapter, supported_child_context_modes, @@ -106,6 +107,49 @@ def selected_turn_todo(envelope: Mapping[str, Any]) -> dict[str, Any]: return _mapping(action.get("selected_todo")) +def captured_todo_text_sha256( + envelope: Mapping[str, Any], *, todo_id: str, + work_contract: object = _ABSENT_WORK_CONTRACT, +) -> str | None: + """Use the shared task-source owner; omitted contracts decode old journals.""" + try: + result = _turn_envelope_result( + "captured_todo_text", envelope=dict(envelope), todo_id=todo_id, + **({"payload": work_contract} if work_contract is not _ABSENT_WORK_CONTRACT else {}), + ) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from exc + digest = result.get("sha256") + if digest is not None and not isinstance(digest, str): + raise RuntimeError("Turn captured task source hash must be a string or null") + return digest + + +def read_turn_execution_context( + plan: Mapping[str, Any], envelope: Mapping[str, Any], +) -> dict[str, Any]: + """Read complete execution facts in one call; decode old journals as before.""" + if "work_contract" in plan: + try: + result = _turn_envelope_result( + "execution_context", envelope=dict(envelope), payload=plan["work_contract"], + ) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from exc + if result.get("sha256") is not None and not isinstance(result["sha256"], str): + raise RuntimeError("Turn captured task source hash must be a string or null") + return result + selected = selected_turn_todo(envelope) + return { + "selected_todo": selected, + "sha256": captured_todo_text_sha256(envelope, todo_id=str(selected.get("todo_id") or "")), + "effective_action": envelope.get("effective_action"), + "writeback": envelope.get("writeback") if isinstance(envelope.get("writeback"), dict) else {}, + "replan_guard_scoped": "replan_action_packet" in envelope, + "replan_action_packet": envelope.get("replan_action_packet"), + } + + def turn_host_lineage(request: Mapping[str, Any]) -> dict[str, str]: """Read identity from the complete owner, with historical request decoding.""" if "work_contract" in request: diff --git a/loopx/control_plane/turn_driver/settlement.py b/loopx/control_plane/turn_driver/settlement.py index ee68b9cf2d..ee46bd3e06 100644 --- a/loopx/control_plane/turn_driver/settlement.py +++ b/loopx/control_plane/turn_driver/settlement.py @@ -19,7 +19,7 @@ SOURCE_TURN_EFFECT_HOLD_SCHEMA_VERSION, ) from ..settlement_driver import decode_settlement_result -from .driver import selected_turn_todo +from .driver import read_turn_execution_context, selected_turn_todo from .transaction import ( LoopXTurnResultKind, require_loopx_turn_completion_outcome, @@ -396,7 +396,16 @@ def terminal_closeout_requirement( if not isinstance(observed_completion, Mapping): observed_completion = completion_intent(result) envelope = plan.get("turn_envelope") - selected = selected_turn_todo(envelope) if isinstance(envelope, Mapping) else {} + if "work_contract" in plan: + # This is a new execution boundary, including journal recovery. + # Recheck the captured source after execution/intent; a pre-launch + # check must not authorize a later replacement of the adapter. + selected = read_turn_execution_context( + plan, envelope if isinstance(envelope, Mapping) else {}, + )["selected_todo"] + else: + # Outstanding journals retain their exact historical decoder. + selected = selected_turn_todo(envelope) if isinstance(envelope, Mapping) else {} outcome = require_loopx_turn_completion_outcome( observed_completion, expected_todo_id=str(selected.get("todo_id") or ""), diff --git a/tests/control_plane/test_turn_completion_source.py b/tests/control_plane/test_turn_completion_source.py index 2656e53f52..78b6b673fb 100644 --- a/tests/control_plane/test_turn_completion_source.py +++ b/tests/control_plane/test_turn_completion_source.py @@ -13,7 +13,7 @@ from loopx.cli import main from loopx.control_plane.turn_driver import executor -from loopx.cli_commands.turn_todo_writeback import captured_todo_text_sha256 +from loopx.control_plane.turn_driver.driver import captured_todo_text_sha256 from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane import canonical_authority_fixture from tests.test_loopx_turn_driver import ( @@ -293,6 +293,12 @@ def test_native_turn_completion_checks_full_source_and_preserves_unrelated_chang change: str, no_followup: bool, ) -> None: + from loopx.control_plane.turn_driver import settlement + + def legacy_selection(*_args, **_kwargs): + raise AssertionError("native completion reinterpreted historical selection") + + monkeypatch.setattr(settlement, "selected_turn_todo", legacy_selection) isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_live_fixture( tmp_path, @@ -533,3 +539,63 @@ def lose_response_after_commit(**kwargs): assert _turn_journal(runtime) == recovered _assert_recovered_completion(runtime, registry, journal) assert _todo(prefix)["todo"]["text"] == "[P0] A later requirement after acceptance." + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("no_followup", [False, True]) +@pytest.mark.parametrize("replacement", ["source", "adapter"]) +def test_native_completion_refuses_replaced_source_before_effects_then_recovers( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + provider: str, no_followup: bool, replacement: str, +) -> None: + from copy import deepcopy + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_live_fixture(tmp_path, provider=provider, + todo_metadata_extra="no_followup=true" if no_followup else "") + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + host, validator = _completion_host_and_validation_scripts() + host = host.replace("request = json.load(sys.stdin)", + 'request = json.load(sys.stdin)\ncalls = pathlib.Path("host-calls.txt")\n' + 'calls.write_text(calls.read_text() + "x" if calls.exists() else "x")') + argv = _turn_run_once_completion_argv(project, runtime, registry, host, validator) + actual_stage = executor._typed_settlement_stage + + def replace_after_execution(plan, *args, **kwargs): + # Challenge the real post-execution boundary, after initial source and + # task validation. The durable journal retains its original capture. + changed = deepcopy(plan) + if replacement == "source": + changed["work_contract"]["reason"] = "Replaced after independent validation." + else: + changed["turn_envelope"]["writeback"]["next_cli_actions"] = ["foreign effect"] + return actual_stage(changed, *args, **kwargs) + + monkeypatch.setattr(executor, "_typed_settlement_stage", replace_after_execution) + code, refused = _cli([], *argv) + assert code == 1 and refused["result_kind"] == "writeback_failed", refused + assert refused["effects"] == {"host_invoked": True, "state_written": False, + "quota_spent": False, "scheduler_acknowledged": False} + assert _todo(prefix)["todo"]["status"] == "open" + journal = _turn_journal(runtime) + identity = journal["plan"]["transaction"]["settlement_plan"]["identity"] + original = deepcopy(journal["plan"]["work_contract"]) + assert (identity["goal_id"], identity["agent_id"], identity["todo_id"]) == ( + "loopx-turn-fixture", "codex-fixture", "todo_fixture0001") + assert (project / "completion-artifact.txt").read_text() == "completed" + + monkeypatch.setattr(executor, "_typed_settlement_stage", actual_stage) + retry = [*argv[:-1], "--resume-turn-key", str(journal["turn_key"]), + "--retry-failed-turn", "--execute"] + code, recovered = _cli([], *retry) + assert code == 0 and recovered["status"] == "committed", recovered + assert recovered["effects"]["host_invoked"] is False + assert recovered["effects"]["quota_spent"] is True + assert _todo(prefix)["todo"]["status"] == "done" + assert _turn_journal(runtime)["plan"]["work_contract"] == original + assert (project / "host-calls.txt").read_text() == "x" + code, replay = _cli([], *argv[:-1], "--resume-turn-key", str(journal["turn_key"]), "--execute") + assert code == 0 and replay["replayed"] is True, replay + assert not any(replay["effects"].values()) + rows = (runtime / "goals/loopx-turn-fixture/runs/index.jsonl").read_text().splitlines() + assert sum(json.loads(row).get("classification") == "quota_slot_spent" for row in rows) == 1 diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 5630b08de4..aafb32b363 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -83,6 +83,10 @@ def legacy_selection(*_args, **_kwargs): return planned monkeypatch.setattr(turn_decision, "build_loopx_turn_plan", observe_launch_plan) + def legacy_launch_selection(*_args, **_kwargs): + raise AssertionError("native launch reinterpreted historical selection") + + monkeypatch.setattr(turn_decision, "selected_turn_todo", legacy_launch_selection, raising=False) isolate_sqlite_runtime(tmp_path, monkeypatch) f = _fixture_module() project, runtime, workspace, registry = f._write_fixture(tmp_path, turn_count=2) @@ -397,7 +401,7 @@ def test_complete_result_vision_uses_original_identity(complete_host_request, fr def test_complete_completion_source_uses_admitted_contract(complete_host_request, carrier): from copy import deepcopy from hashlib import sha256 - from loopx.cli_commands.turn_todo_writeback import captured_todo_text_sha256 + from loopx.control_plane.turn_driver.driver import captured_todo_text_sha256 from loopx.control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision _, contract = complete_host_request @@ -538,6 +542,43 @@ def test_complete_settlement_fallback_keeps_original_identity(complete_host_requ assert target == original +def test_complete_terminal_intent_rechecks_original_source(complete_host_request, monkeypatch): + from copy import deepcopy + from loopx.control_plane.turn_driver import settlement + + request, contract = complete_host_request + todo_id = contract["selected_todo"]["todo_id"] + + def legacy_selection(*_args, **_kwargs): + raise AssertionError("complete terminal intent reinterpreted historical selection") + + monkeypatch.setattr(settlement, "selected_turn_todo", legacy_selection) + before = deepcopy(request) + for continuation in ("active_goal", "successor", "no_followup"): + outcome = {"todo_id": todo_id, "continuation": continuation} + if continuation == "successor": + outcome["successor_todo_ids"] = ["todo_checked_successor"] + for phase in (None, "writeback", "terminal_closeout"): + journal = {phase: {"completion": outcome}} if phase else {} + def intent(_result): + assert phase is None, "stored completion must not repeat lifecycle intent" + return outcome + + actual = settlement.terminal_closeout_requirement(plan=request, + result={"result_kind": "validated_completion"}, journal=journal, + completion_intent=intent) + assert actual == (continuation == "no_followup", None) + # Another attempt or journal recovery must recheck its source; + # prior successful consumption cannot become a retained proof. + for malformed in (None, {**contract, "reason": "replaced after execution"}): + rejected = settlement.terminal_closeout_requirement( + plan={**request, "work_contract": malformed}, + result={"result_kind": "validated_completion"}, journal=journal, + completion_intent=intent) + assert rejected[0] is False and rejected[1] + assert request == before + + def test_codex_prompt_preserves_complete_source_and_historical_version_gate(complete_host_request): from copy import deepcopy from loopx.control_plane.turn_driver.codex_cli import _prompt From 5e3d86fb3b5310c365d74e066c5b3983aacad231 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:42:28 +0800 Subject: [PATCH 15/37] refactor(quota): share acceptance and context projection transport Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota-cli-hot-path-compaction-v0.md | 18 ++++++++++++------ loopx/control_plane/quota/turn_envelope.ts | 8 +------- .../test_quota_acceptance_references.py | 6 ++++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index fc468363f9..3bc4771d4c 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -87,9 +87,13 @@ neither enables native tracing nor changes the default-off transport switch. During the C1 migration, exact current-Task title sharing is owned by the TypeScript decision composer in `turn_envelope.ts`. The legacy Python display -helper shares one coarse transport call with Explore-guidance sharing; each -explicit detail selector is preserved. No source carrier or both detail selectors -means no sharing call. Its label-prefix omission stays display-only. +helper shares one coarse transport call with Explore-guidance and Goal-acceptance +sharing; each explicit task/Explore detail selector is preserved. Acceptance +still shares exact duplicates when both selectors request inline detail. A call +limited to task/Explore sharing needs no transport without a source carrier or +with both selectors disabled. Replan-authoring projection keeps its separate +lifecycle; it is not another acceptance/task/Explore call. Label-prefix omission +stays display-only. Complete `loopx_turn_envelope_v1` decisions share before hashing/capture and validate the same-capture target on recovery. Conflicting inline/reference values, chains, malformed paths or a no-longer-verified selected Task reject even with @@ -99,9 +103,11 @@ it does not select an alternative complete contract. Retire its delegate with th legacy display callers while retaining supported historical capture recovery. C1 迁移中,当前 Task 标题的精确共享归 `turn_envelope.ts` 的 TypeScript decision -composer 所有;旧 Python 展示 helper 与 Explore 指引共享合用一次粗粒度调用, -分别保留 detail 选择;无源载体或两项 detail 均开启时无需共享调用。按 label 前缀 -省略仍仅属于展示。 +composer 所有;旧 Python 展示 helper 与 Explore 指引、Goal acceptance 共享合用 +一次粗粒度调用,分别保留任务/Explore detail 选择;两项均要求内联详情时,验收 +仍共享精确重复。仅请求任务/Explore 共享时,无源载体或两项均停用才无需传输。 +Replan authoring 投影保留独立生命周期,不构成另一轮验收/任务/Explore 调用。 +按 label 前缀省略仍仅属于展示。 完整 `loopx_turn_envelope_v1` 在 hash/capture 前共享,恢复时验证同一 capture 的目标。 原文/引用冲突、引用链、非法路径或当前选中 Task 无法继续核验时,即使完整性 hash 有效也拒绝。该来源共享不要求开启差分。上文 `--include-detail` 描述历史展示投影, diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 9447227178..cecf885c45 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2028,9 +2028,6 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { if (operation === "reference_replan_authoring") { return referenceReplanAuthoring(requireJsonObject(request.payload, "authoring sharing source")); } - if (operation === "reference_goal_acceptance") { - return referenceGoalAcceptance(requireJsonObject(request.payload, "acceptance sharing source")); - } if (operation === "replan_view") return replanView(request); if (operation === "replan_cli_view") { const payload = requireJsonObject(request.payload, "replan CLI payload"); @@ -2138,10 +2135,6 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { return {sha256: capturedTodoTextSha256(object(action.selected_todo), envelope.work_context, action.recommended_action, request.todo_id)}; } - if (operation === "build_controller_input") { - const envelope = buildTurnEnvelope(request); - return {envelope, route: turnEnvelopeRoute(envelope)}; - } if (operation === "consumer_version") { assertTurnEnvelopeConsumerVersion(requireJsonObject(request.envelope, "turn_envelope")); return {ok: true}; @@ -2150,6 +2143,7 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { const source = requireJsonObject(request.payload, "quota decision"); let payload = source; if (jsonObject(request.diagnostics)) payload = projectQuotaDiagnostics(payload, request.diagnostics as JsonObject); + if (request.goal_acceptance === true) payload = referenceGoalAcceptance(payload); if (request.task_title === true) payload = referenceCurrentTaskTitle(payload); if (request.explore_guidance === true) payload = referenceExploreGuidance(payload); return jsonObject(request.diagnostics) ? contextProjectionChanges(source, payload) : payload; diff --git a/tests/control_plane/test_quota_acceptance_references.py b/tests/control_plane/test_quota_acceptance_references.py index d5533dc7b7..d8708cfffb 100644 --- a/tests/control_plane/test_quota_acceptance_references.py +++ b/tests/control_plane/test_quota_acceptance_references.py @@ -1,11 +1,12 @@ """One complete acceptance survives composition, display and actual admission.""" from copy import deepcopy +from functools import partial import json import pytest from loopx.control_plane.quota.cli_projection import ( - _reference_inline_goal_acceptance, compact_quota_should_run_cli_payload, + _reference_context, compact_quota_should_run_cli_payload, ) from loopx.control_plane.quota.turn_envelope import ( build_turn_envelope, compose_turn_decision, quota_action_signature_document, @@ -14,7 +15,8 @@ from test_replan_host_context_projection import _quota_payload -@pytest.fixture(params=[_reference_inline_goal_acceptance, compose_turn_decision], +@pytest.fixture(params=[partial(_reference_context, task_title=False, + explore_guidance=False, goal_acceptance=True), compose_turn_decision], ids=["legacy-display", "complete-composition"]) def share(request): return request.param From 65ea4fa616b6d35671d5029336487a6281e8db24 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:56:11 +0800 Subject: [PATCH 16/37] docs(controller): record packaged quota comparison and compaction gap Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/architecture/rfcs/STATUS.md | 2 +- docs/architecture/rfcs/STATUS.zh-CN.md | 2 +- ...controller-executor-unified-contract-v0.md | 10 +- ...ller-executor-unified-contract-v0.zh-CN.md | 8 +- .../2026-10-28-packaged-heartbeat.md | 105 ++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 81 ++++++++++++++ .../rfcs/loopx-overall-roadmap-v0.md | 7 ++ .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 5 + 8 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md create mode 100644 docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md diff --git a/docs/architecture/rfcs/STATUS.md b/docs/architecture/rfcs/STATUS.md index 67a340c3ce..24f907b374 100644 --- a/docs/architecture/rfcs/STATUS.md +++ b/docs/architecture/rfcs/STATUS.md @@ -31,7 +31,7 @@ appendix may keep dated history, but no dated log heading may precede it. | [RFC: Capable Agent Manager and Semantic Work Handoff (v0)](capable-manager-semantic-handoff-v0.md) | Accepted | [Personal Follow-through independent proposal](personal-follow-through-v0.md) | [2 entries](ledger/capable-manager-semantic-handoff-v0/) | | [RFC: Complete State Recovery and Controlled Reactivation (v0)](complete-state-recovery-v0.md) | Accepted | none | — | | [RFC: Composable State Machines and Recovery Verification (v0)](composable-state-machines-recovery-verification-v0.md) | Accepted | none | — | -| [RFC: Controller / Executor Sessions and One Governed Work Contract (v0)](controller-executor-unified-contract-v0.md) | Accepted | none | [1 entry](ledger/controller-executor-unified-contract-v0/) | +| [RFC: Controller / Executor Sessions and One Governed Work Contract (v0)](controller-executor-unified-contract-v0.md) | Accepted | none | [2 entries](ledger/controller-executor-unified-contract-v0/) | | [Explicit Todo continuation: Stage A](cross-session-memory-substrate-v0.md) | Accepted | none | — | | [RFC: LoopX Desktop Execution Frontends v0](desktop-execution-frontends-v0.md) | Accepted | none | — | | [RFC: External Evidence Research Capability v0](external-evidence-research-capability-v0.md) | Accepted | none | [3 entries](ledger/external-evidence-research-capability-v0/) | diff --git a/docs/architecture/rfcs/STATUS.zh-CN.md b/docs/architecture/rfcs/STATUS.zh-CN.md index 7059783a1e..039db58cdb 100644 --- a/docs/architecture/rfcs/STATUS.zh-CN.md +++ b/docs/architecture/rfcs/STATUS.zh-CN.md @@ -28,7 +28,7 @@ | [RFC:强能力 Agent 管家与语义工作交接(v0)](capable-manager-semantic-handoff-v0.zh-CN.md) | 已接受 | [个人事项持续跟进独立提案](personal-follow-through-v0.zh-CN.md) | [2 条](ledger/capable-manager-semantic-handoff-v0/) | | [RFC:完整状态恢复与受控重新激活(v0)](complete-state-recovery-v0.zh-CN.md) | 已接受 | 无 | — | | [RFC:可组合状态机与恢复验证(v0)](composable-state-machines-recovery-verification-v0.zh-CN.md) | 已接受 | 无 | — | -| [RFC:Controller / Executor 会话与统一受治理工作契约(v0)](controller-executor-unified-contract-v0.zh-CN.md) | 已接受 | 无 | [1 条](ledger/controller-executor-unified-contract-v0/) | +| [RFC:Controller / Executor 会话与统一受治理工作契约(v0)](controller-executor-unified-contract-v0.zh-CN.md) | 已接受 | 无 | [2 条](ledger/controller-executor-unified-contract-v0/) | | [显式 Todo 接续:阶段 A](cross-session-memory-substrate-v0.zh-CN.md) | 已接受 | 无 | — | | [RFC:LoopX 桌面执行前端 v0](desktop-execution-frontends-v0.zh-CN.md) | 已接受 | 无 | — | | [RFC:外部证据研究能力 v0](external-evidence-research-capability-v0.zh-CN.md) | 已接受 | 无 | [3 条](ledger/external-evidence-research-capability-v0/) | diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index b72ccb1e00..6eb1919507 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1590,7 +1590,7 @@ For an unhealthy-paused synthetic source, the original CLI projection frozen at win. Tokens/cache, latency, extra reads, framing and accepted outcomes remain unmeasured for this case. No live model experiment is implied by these checks. -The shared-owner prerequisite is independently reviewable in #7457. Its +The shared reference owner is merged in #7457. Its Task-title and Explore operations use one coarse legacy projection call; complete composition reuses those same functions inside its existing call. The integration branch absorbs the prerequisite commits rather than carrying a copied owner. @@ -1600,6 +1600,14 @@ projection. Historical capture recovery remains explicit. Component measurements show reduced bridge cost versus two calls, but higher cost than the original Python projection; this is not evidence of a positive full-path quota comparison. +The [packaged heartbeat comparison](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md) +now exercises real isolated File/SQLite CLI cold, warm and same-capture recovery +against original quota. Necessary source/replay checks pass, but default context +and total stdout grow; expanding all peer outcomes adds reads and cost. No positive +complete-cost diff or model benefit is qualified. The existing C1 continuation is +shared typed diagnostic compaction with unknown-field and obligation preservation, +followed by the same comparison. This does not promote the candidate by default. + Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, latency, failures/retries, verification and human intervention. A smaller first diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index a7d6444bc7..a33fe01555 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -1169,13 +1169,19 @@ capture 前验证失败。工作契约形成前的错误则在两种 CLI flag 这不是效率收益;该样例的 tokens/cache、延迟、追加读取、请求包装及验收成果仍未 测量。这些检查不代表启动过真实模型实验。 -共享 owner 前置通过 #7457 独立评审。Task 标题与 Explore 操作在旧投影中合用 +共享 reference owner 已通过 #7457 合入。Task 标题与 Explore 操作在旧投影中合用 一次粗粒度调用;完整 composition 在既有调用内部复用相同函数。集成分支吸收 前置提交,不复制一份 owner。新的 `should-run` emission 只接受已组合契约或 原始 predecision 诊断,不再回退旧 packet/envelope 投影;历史 capture 恢复 仍为显式路径。组件测量显示桥接成本比两次调用更低,但高于原 Python 投影, 不能作为完整 quota 路径净收益的证据。 +[打包 heartbeat 对照](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md) +已使用真实隔离 File/SQLite CLI,相对原 quota 核对冷态、温态和同 capture 恢复。 +必要来源/重放检查通过,但默认上下文及总 stdout 增加,展开全部 peer 成果另增补读 +和成本;完整成本正向 diff 及模型收益均未资格化。原 C1 继续由共享 typed owner 消除 +诊断重复,保留未知字段和义务,再跑相同对照;这不提升候选默认。 + 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 只有必要语义保留且同等验收成果的完整成本降低,或可比预算下验收成果改善,才判正向。 diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md new file mode 100644 index 0000000000..82007e1f70 --- /dev/null +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -0,0 +1,105 @@ +# Packaged single-session heartbeat comparison + +This is a measurement checkpoint for the [controller/executor RFC](../../controller-executor-unified-contract-v0.md#9-validation), +not acceptance of C1 or a default promotion. The [Chinese mirror](2026-10-28-packaged-heartbeat.zh-CN.md) +records the same scope and results. + +## Source and workload + +Compare original quota at main `843be494266774b63b3de61d0adf5dd7f4f877a2` +with the integration candidate at runtime commit +`1199cbe0b8e92ddf845e345483041486fe808024` in [#7430](https://github.com/loopx-project/loopx/pull/7430). +Each wheel was built with its own source-dependent frontend fingerprint; +1,884 packaged product members matched their respective source checkout. +No global runtime was installed or active Goal state used as a test fixture. + +Both arms use the same synthetic workload and real, isolated File and SQLite +authorities: one active Goal, a complete selected task, one current-Agent outcome +and five independent peer outcomes, with four requirements each. Global intent, +authority and stop conditions remain available. The candidate's existing native +context opt-in expands the current outcome and indexes the peers. Every peer +body remains readable from the original capture; all 24 requirement descriptions, +owners, linked work and unverified states were checked after expansion. + +The packaged CLI path runs thin heartbeat bootstrap, guarded quota/native context, +every required context page, mandatory Goal-source reads, and the real context-hook +command. Synthetic native lifecycle events provide warm continuity and then +PostCompact/SessionStart invalidation. Loss recovery reads the **same original +capture**, without replacing admission. Tampered sources and early spending are +rejected; refresh and spend replays preserve one effect and one debit. The +unfinished selected task remains open. Warm reconstruction and missing-baseline +full fallback were also checked. + +These are packaged engineering observations with simulated host continuity. +They do not establish installed Codex App adoption, model retention or decisions, +SDK coverage, accepted task outcomes, or a controller/executor experiment. + +## Observations + +One matched cohort was run, one observation per arm/backend/phase. Background +host load was uncontrolled. Elapsed time sums actual CLI calls, including startup, +required Goal reads and lifecycle observations. Counts are logical Python-to-runtime +requests; physical framing, retries and provider-internal IO are not inferred. +Artifact size is delivered UTF-8 context content, not tokens or the full stdout. + +| Backend / phase | Context bytes, original → candidate | Logical requests | Elapsed ms | +| --- | ---: | ---: | ---: | +| File / cold full | 75,387 → 76,162 | 103 → 102 | 5,052.49 → 4,867.53 | +| File / warm delta | 40,991 → 43,558 | 99 → 98 | 3,240.32 → 3,399.05 | +| File / loss recovery | 74,619 → 75,390 | 6 → 6 | 4,045.91 → 4,213.20 | +| SQLite / cold full | 75,483 → 76,270 | 103 → 102 | 4,566.19 → 4,428.81 | +| SQLite / warm delta | 41,085 → 43,666 | 99 → 98 | 3,236.50 → 3,416.50 | +| SQLite / loss recovery | 74,713 → 75,498 | 6 → 6 | 4,149.99 → 3,957.72 | + +Both arms use 13/9/13 CLI calls for cold/warm/recovery. Invalidation itself is +separate: two hook calls, 683–738 ms across these observations. Expanding all five +peers in the candidate adds 13,940 content bytes, five reader pages and five +observation hooks: **10 CLI calls / 10 logical requests**, 17,620 stdout bytes, +and 3,416–3,713 ms per phase. The original arm already carries these bodies inline. + +Total phase stdout also grows: File cold 94,662 → 96,082 bytes, warm +58,077 → 60,649, recovery 89,807 → 91,144; SQLite 94,776 → 96,208, +58,185 → 60,771 and 89,911 → 91,262. The seven settlement/negative/readback calls +per backend use 226 logical requests in each arm, with identical stdout totals +within each backend. Setup, builds, failed authoring probes and optional reads +are separate costs, not removed from the experiment history. + +Coalescing acceptance/title/Explore projection removes one logical request and +reduces bridge JSON. For example, File cold request/response content falls from +965,261/585,535 to 902,963/494,206 bytes. That local reduction does **not** offset +the larger delivered artifact or optional expansion. Warm elapsed time rises in +both backends; recovery observations are mixed. This sample supports no causal +speedup, default SQLite performance qualification or positive complete-cost diff. +Tokens, cache use, billing, verification outcomes, human attention and long-horizon +net benefit remain unqualified. Earlier negative and mixed measurements stand. + +## Owning gap and continuation + +Compared with original quota, scoping removes about 13 KB of peer bodies, but the +candidate retains about 6.6 KB of repeated Todo diagnostic lanes and adds about +3.9 KB of scope metadata. Candidate lists and other diagnostic repetitions consume +the remaining savings. This is a composition/compaction gap, not evidence that +peer scope itself lowers total cost. + +The next C1 slice must characterize necessary quota diagnostic compaction and +move it into the existing shared typed composer, retiring the replaced Python +caller algorithm. Preserve complete selected work, portfolio/horizon, +claim/lease facts, global obligations, unknown fields, exact references and +historical recovery. The old Python compactor drops unknown list fields and +shortens some prose; copying it unchanged would not meet that contract. Reuse +the same packaged comparison for default reads and all-peer expansion before +claiming improvement. No new packet format or per-field RPC is justified. + +The integration candidate adopts the merged route owner (#7520) and reference +owner (#7457). Acceptance, Task-title and Explore legacy references now share +one existing `reference_context` operation; the redundant acceptance operation +and duplicate controller-input dispatch branch are removed. Complete composition +reuses those same semantic functions. This is a bounded convergence change, +not full writer cutover, historical-reader retirement or C1 completion. + +The final affected suite has 469 passes and five optional native/SDK skips; +116 typed tests and control-plane typechecking pass. Earlier build fingerprint, +missing-dependency, harness-carrier and outdated call-count failures are retained +as authoring/validation costs, not silently converted into product passes. +No model, paid evaluation, managed benchmark, live install, release or merge was +performed for this comparison. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md new file mode 100644 index 0000000000..0c0f7e98ba --- /dev/null +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -0,0 +1,81 @@ +# 打包单会话 heartbeat 对照 + +本文是 [controller/executor RFC](../../controller-executor-unified-contract-v0.zh-CN.md#9-验证与验收) +的测量检查点,不代表 C1 验收或提升默认。[英文镜像](2026-10-28-packaged-heartbeat.md) +记录相同范围和结果。 + +## 来源与负载 + +以 main `843be494266774b63b3de61d0adf5dd7f4f877a2` 的原 quota 对照 +[#7430](https://github.com/loopx-project/loopx/pull/7430) 集成候选,候选运行时代码 +固定为 `1199cbe0b8e92ddf845e345483041486fe808024`。两份 wheel 分别按自身源码构建 +前端指纹,各自 1,884 个打包产品成员与来源工作树一致。未安装全局 runtime,也未拿 +活动 Goal 状态做实验夹具。 + +两臂使用同一合成负载及真实隔离 File、SQLite authority:一个活动 Goal、完整选中任务、 +一项当前 Agent 成果与五项独立 peer 成果,每项四条要求。全局意图、权限及停止条件 +仍可读取。候选在已有 native context opt-in 中展开自身成果并索引其他成果;正文仍可 +从原 capture 精确补读,展开后核对全部 24 条完整要求、owner、关联工作和未验证状态。 + +打包 CLI 路径执行 thin heartbeat bootstrap、quota/native context guard、全部必读 +上下文分页、强制 Goal 来源读取和真实 context-hook 命令。以合成 native 生命周期 +事件建立温态连续性,再通过 PostCompact/SessionStart 失效。丢失后读取**同一原始 +capture**,不替换准入;来源篡改和提前记账被拒绝,refresh/spend 重放保持一次 effect +及一次扣额,未完成的选中任务仍开放。另核对温态重建与缺 baseline 的完整回退。 + +这属于模拟宿主连续性下的打包工程观察,不证明正式 Codex App 采用、模型保留或决策、 +SDK 覆盖、任务验收成果,也不是 controller/executor 双会话实验。 + +## 观测 + +只有一组匹配 cohort,每臂、每后端、每阶段一次;宿主后台负载未受控。耗时为实际 CLI +调用之和,包含启动、必读 Goal 和生命周期观察。请求数是 Python 到 runtime 的逻辑 +请求,不推断物理 framing、重试和 provider 内部 IO。产物字节指已交付 UTF-8 上下文, +不是 tokens 或完整 stdout。 + +| 后端/阶段 | 上下文字节,原实现 → 候选 | 逻辑请求 | 耗时 ms | +| --- | ---: | ---: | ---: | +| File/冷态完整 | 75,387 → 76,162 | 103 → 102 | 5,052.49 → 4,867.53 | +| File/温态差分 | 40,991 → 43,558 | 99 → 98 | 3,240.32 → 3,399.05 | +| File/丢失恢复 | 74,619 → 75,390 | 6 → 6 | 4,045.91 → 4,213.20 | +| SQLite/冷态完整 | 75,483 → 76,270 | 103 → 102 | 4,566.19 → 4,428.81 | +| SQLite/温态差分 | 41,085 → 43,666 | 99 → 98 | 3,236.50 → 3,416.50 | +| SQLite/丢失恢复 | 74,713 → 75,498 | 6 → 6 | 4,149.99 → 3,957.72 | + +两臂冷态/温态/恢复均为 13/9/13 次 CLI。失效本身另计两次 hook 调用,本组耗时 +683–738 ms。候选展开全部五项 peer 成果每阶段增加 13,940 字节正文、五页 reader 和 +五次观察 hook:**10 次 CLI/10 次逻辑请求**、17,620 字节 stdout、3,416–3,713 ms。 +原实现已经内联这些正文。 + +完整阶段 stdout 也增加:File 冷态 94,662 → 96,082 字节、温态 58,077 → 60,649、 +恢复 89,807 → 91,144;SQLite 分别为 94,776 → 96,208、58,185 → 60,771、 +89,911 → 91,262。每后端另有七次结算/负例/读回调用,两臂均为 226 次逻辑请求, +同后端 stdout 合计相同。初始化、构建、失败作者探针与可选补读单独计量,不从历史抹去。 + +合并 acceptance/title/Explore 投影减少一次逻辑请求及 bridge JSON。例如 File 冷态 +请求/响应正文从 965,261/585,535 降到 902,963/494,206 字节。这一局部下降**没有** +抵消更大的交付产物和可选补读;两后端温态耗时均上升,恢复结果混合。本样本不支持 +因果提速、默认 SQLite 性能资格或完整成本正向 diff。Tokens、cache、账单、验收成果、 +人工注意力与长程净益仍未资格化;此前负向和混合测量继续保留。 + +## 归属缺口与接续 + +相对原 quota,scope 去掉约 13 KB 的 peer 正文,但候选仍保留约 6.6 KB 的重复 Todo +诊断列表,并新增约 3.9 KB scope 元数据;候选列表及其他诊断重复消耗了剩余节省。 +这是组合/压缩缺口,不能据此认定 peer scope 已降低总成本。 + +下一片 C1 应刻画原 quota 中必要的诊断压缩,迁入既有共享 typed composer,并删除 +被替代的 Python caller 算法。保留完整选中工作、portfolio/horizon、claim/lease、全局 +义务、未知字段、精确引用与历史恢复。旧 Python compactor 会丢弃未知列表和缩短部分 +正文,原样复制不满足此合同。继续用同一打包对照分别测默认读取与全 peer 补读,再判断 +是否改善;没有理由新增 packet 格式或逐字段 RPC。 + +集成候选采用已合入的 route owner(#7520)及 reference owner(#7457)。旧投影的 +acceptance、Task 标题和 Explore 引用合用既有 `reference_context` 操作,删除独立 +acceptance 操作和重复 controller-input dispatch;完整组合复用相同语义函数。这只是 +有界代码收敛,不是全部 writer 切换、历史 reader 退役或 C1 完成。 + +最终受影响测试 469 通过、五项可选 native/SDK 跳过;116 项 typed 测试及 control-plane +类型检查通过。此前构建指纹、缺依赖、harness carrier 和过时调用数断言失败保留为作者/ +验证成本,不静默改成产品通过。本对照未调用模型、付费评测、managed benchmark, +未执行 live 安装、发布或合并。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 423fc6b6f3..3d043d70d8 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -202,6 +202,13 @@ host prompts. A common capture does not unify those consumers. Qualification checks source obligations through composition, reconstruction and actual final input; new-admission cutover stays separate from historical receipt replay. +The [packaged C1 heartbeat checkpoint](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md) +preserves required source and same-capture settlement semantics on isolated +File/SQLite, but delivered bytes grow and all-peer expansion adds cost. Continue +within C1 by converging diagnostic compaction in its shared typed owner, preserving +unknowns and obligations, then repeat the quota-baseline comparison. This is no +default promotion or positive complete-cost qualification. + Use that RFC’s C1–C9 task/deletion map within the existing work streams: first qualify unified single-session inputs, then role binding and shared lifecycle, replan/result authoring, executor exchange, benchmark reuse, packaged App diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 4c640f6009..51cba9a56f 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -155,6 +155,11 @@ C1 的[消费链切换表](controller-executor-unified-contract-v0.zh-CN.md#c1- 共享 capture 不表示这些消费者已统一。资格检查须追踪来源义务经过组合、重建到实际 最终输入;新准入切换与历史回执重放分别处理。 +[C1 打包 heartbeat 检查点](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md) +在隔离 File/SQLite 保留必读来源及同 capture 结算语义,但交付字节增加,全部 peer +补读另增成本。继续在原 C1 的共享 typed owner 收敛诊断压缩,保留未知字段与义务, +再做原 quota 基线对照;这不是默认提升或完整成本正向资格。 + 把该 RFC 的 C1–C9 任务/删除表放在既有工作流内:先验收统一单会话输入,再做角色绑定 与共享生命周期、replan/结果编写、executor 交流、benchmark 复用、打包 App readback 及退役。App heartbeat 流程和实际 benchmark 消费者分别验收;managed Codex 可以作为 From 3090dc7891c61ffffba3c30254697be0ce4b72cb Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:55:49 +0800 Subject: [PATCH 17/37] refactor(quota): share typed Todo diagnostic projection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/protocols/turn-envelope-v0.md | 56 ++++--- loopx/control_plane/quota/turn_envelope.ts | 6 +- .../test_quota_cli_projection.py | 153 +++++++----------- .../test_work_acceptance_context.py | 18 ++- .../goal_outcome_refinement.test.ts | 45 ++++++ 5 files changed, 156 insertions(+), 122 deletions(-) diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 2bf9e60477..51f7160bac 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -605,37 +605,43 @@ work context 中已加载的来源,其次保留 planning horizon 或第一份 签名覆盖的 work context,拒绝缺失或不可用来源。这只证明展示成本下降,尚不证明 上游读取减少、模型采用或完整任务效率提升。 -Known Todo diagnostic lanes share `quota/diagnostic_projection.ts` through the -existing coarse context call. Read-only status/plan projects all rows in one -call, rather than a call per Goal or lane. Unknown lists, fields, versions and -complex nested item content remain full; field recognition reuses the canonical -Todo contract. Retained items, capability candidates, portfolio text, planning -horizon and selected-work sources are not shortened. Explicit detail returns the -original summaries. Upstream compaction remains visible as `source_projection`. -Status/plan adds one projection request; Agent/vision detail still needs one -when the User summary remains compact. Explicit full Todo roles bypass that -diagnostic request. These transport costs must be included in comparisons. +Known Todo diagnostic lanes use `quota/diagnostic_projection.ts` in the same +coarse legacy context call and in scoped native model-context composition. +Read-only `quota status`/`plan` projects its entire row collection in one call. +Unknown lists, nested fields and complete retained items survive; no suffix +heuristic removes future obligations. A known lane with unknown item fields, +an unknown item version or complex nested content stays full; field recognition +reuses the canonical Todo contract. Capability candidates, portfolio text, +planning horizon and full selected-work sources remain complete. Explicit +legacy detail preserves the original summaries. Legacy detail commands observe +current state; they cannot recover an earlier decision. A scoped native +summary's `full_detail_capture_ref` resolves to `source_capture`; read that +immutable original file with its digest, without repeating a command per lane. Upstream summary-compaction metadata remains available +as `source_projection`; this reader restores captured diagnostics, not omitted +canonical records. Full/delta baselines and context-loss recovery contain the +exact delivered presentation. Unscoped native/failure fallback retains the +original complete input. This is presentation reuse, not default promotion or +proof of model retention, reduced total cost or better accepted outcomes. + Diagnostic replies carry only display edits and references to the immutable input. The Python adapter applies them to its original facts, preserving float spelling, negative zero, large-integer precision and relocated upstream metadata. Reference-only callers keep their existing transport; no Python compaction decision is reintroduced. The read codec's `content_revision` is recognized as known display metadata and remains on retained items. -The legacy detail command observes current state and cannot recover an earlier -decision. The scoped native consumer and original-capture reference are a separate -[controller integration proposal](https://github.com/loopx-project/loopx/pull/7430), -which reuses this same owner. This does not change admission, claims, leases, -settlement or native context defaults; it does not qualify net efficiency gains. - -中文:已知 Todo 诊断列表通过既有粗粒度上下文调用复用同一 typed owner;只读 -status/plan 整批行只调用一次,不逐 Goal 或列表调用。未知列表、字段、版本和 -复杂嵌套条目保留全文,字段识别复用规范 Todo 契约;保留条目、候选、portfolio -正文、规划视野和当前工作来源不截短,详情返回原摘要,上游压缩信息保留于 -`source_projection`。旧详情命令读取当前状态,不能恢复历史决定。 -status/plan 新增一次投影请求;Agent/vision 详情若仍压缩 User 摘要,也需一次; -显式展开两类 Todo 则跳过诊断投影。这些传输成本须计入比较。scoped 原生 -消费者及同 capture 引用属于单独的 controller 集成提案 #7430,复用同一 owner。 -此处不改准入、claim、lease、结算或原生默认,也不代表完整效率收益通过。 + +中文:已登记的 Todo 诊断列表由 `quota/diagnostic_projection.ts` 统一投影,旧 CLI +复用同一次上下文调用,scoped 原生输入在同一组合调用中采用;只读 status/plan +整批行只调用一次。未知列表、嵌套字段和保留条目的完整字段不删除,不按名称后缀 +推测新义务。已知列表含未知条目字段、未知版本或复杂嵌套内容时保留全文,字段识别 +复用规范 Todo 契约。候选、portfolio 正文、规划视野和完整当前工作来源保持;旧详情选项 +保留完整原摘要。旧详情命令读取当前状态,不能恢复过去的决定;scoped 原生摘要 +的 `full_detail_capture_ref` 指向已有 `source_capture`,据原文件和 digest 补读, +不逐列表重复完整命令。上游压缩信息保留于 +`source_projection`,恢复的是已捕获诊断,不是来源本已省略的规范记录。full/delta +基线及上下文丢失恢复都使用实际交付的完整 presentation;未 scoped 或失败回退仍 +保留原完整输入。此处只证明展示代码复用,不代表默认转正、模型保持、完整成本 +下降或验收成果提高。 诊断回复只传展示变更及不可变输入的引用;Python adapter 在原始事实上应用, 保留浮点表示、负零、大整数精度及迁移后的上游元数据。仅引用的旧调用方保持 原传输行为,不重新引入 Python 裁剪决定。读取 codec 已有的 `content_revision` diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index cecf885c45..ab5867578d 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -8,6 +8,7 @@ import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from import {prepareContextDelivery, restoreContextDelivery, OUTCOME_CONTEXT_PROJECTION_SCHEMA} from "./context_delivery.ts"; import {updateNativeContextLifecycle} from "./native_context_lifecycle.ts"; import {OUTCOME_CONTEXT_READ_PLAN_SCHEMA} from "../goals/outcome_requirements.ts"; +import {projectQuotaDiagnostics} from "./diagnostic_projection.ts"; import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; import { @@ -452,7 +453,8 @@ function capturedOutcomeContext(request: JsonObject): JsonObject { requireNonEmptyString(source.path, "original capture path"); if (typeof source.sha256 !== "string" || !BARE_SHA256_PATTERN.test(source.sha256)) throw new EffectRuntimeRequestError("outcome context needs the original file digest"); - const context = structuredClone(payload), carriers = acceptanceCarriers(context); + const context = projectQuotaDiagnostics(structuredClone(payload), {capture_ref: "$.source_capture"}); + const carriers = acceptanceCarriers(context); const channel = jsonObject(jsonObject(context.interaction_contract)?.agent_channel); if (!channel || !scopeInteractionOutcomeReadGuidance(channel)) return payload; const sources = jsonObject(jsonObject(jsonObject(context.interaction_contract)?.agent_channel)?.work_context)?.sources; @@ -475,6 +477,8 @@ function capturedOutcomeContext(request: JsonObject): JsonObject { "Read this entire presentation before dependent work. The nested context expands every required outcome; " + "other independent outcomes retain current index facts and exact references in source_capture. " + "Use quota context-read on that original file/hash with --outcome-ref for complete detail before relying on it. " + + "Known Todo diagnostic lists are bounded; their full_detail_capture_ref resolves to source_capture. " + + "Read that original file/hash with quota context-read for captured diagnostics, without a new guard. " + "Goal-wide acceptance or unproven relevance requires the complete original source. " + "After context loss recover this exact presentation, not a fresh admission. Original requirements and evidence remain binding.", context}; diff --git a/tests/control_plane/test_quota_cli_projection.py b/tests/control_plane/test_quota_cli_projection.py index a004d9dbc0..ae3ecbc942 100644 --- a/tests/control_plane/test_quota_cli_projection.py +++ b/tests/control_plane/test_quota_cli_projection.py @@ -334,6 +334,62 @@ def _items(count: int, *, prefix: str) -> list[dict[str, object]]: ] +@pytest.mark.parametrize("role", ["agent", "user"]) +def test_shared_diagnostics_preserve_unknown_lists_nested_fields_and_complete_items(role): + lane = "first_executable_items" if role == "agent" else "first_open_items" + item = {**_items(1, prefix="retained")[0], "text": "Original constraint. " * 200, + "future_requirements": ["Keep the tail stop."], "lease": {"owner": "peer", "epoch": 7}} + unknown = [None, False, 0, {"requirement": "A new obligation"}] + summary = {"schema_version": "todo_summary_v0", lane: [item], "backlog_items": _items(5, prefix="diagnostic"), + "future_items": unknown, "future_rules": unknown, + "deferred_items": [{"todo_id": "later", "future_global_stop": "Do not continue."}], + "recent_completed_advancement_items": [{"todo_id": "old", "completion_result": {"future_stop": True}}], + "claim_scope": {"owner": "peer", "future_items": unknown, "blocked_claimed_items": []}, + "future_object": {"items": unknown}, + "todo_succession_warning": {"items": _items(4, prefix="old"), "todo_ids": ["original-id"], "future_items": unknown}} + payload = {f"{role}_todo_summary": summary, "capability_gate": {"runnable_candidates": [item], "future": unknown}, + "action_portfolio": {"suggested_actions": [item]}, "planning_horizon": {"work_items": [item]}} + original = deepcopy(payload) + projected = compact_quota_should_run_cli_payload(payload) + result = projected[f"{role}_todo_summary"] + assert result[lane] == [item] + assert result["future_items"] == result["future_rules"] == unknown + assert result["future_object"] == {"items": unknown} + assert result["deferred_items"] == summary["deferred_items"] + assert result["recent_completed_advancement_items"] == summary["recent_completed_advancement_items"] + assert result["claim_scope"] == {"owner": "peer", "future_items": unknown} + assert result["todo_succession_warning"]["todo_ids"] == ["original-id"] + assert result["todo_succession_warning"]["future_items"] == unknown + for key in ("capability_gate", "action_portfolio", "planning_horizon"): + assert projected[key] == payload[key] + assert payload == original + assert compact_quota_should_run_cli_payload(projected) == projected + detailed = compact_quota_should_run_cli_payload(payload, + include_todo_summary_detail=True, include_user_todo_summary_detail=True) + assert detailed == payload + + +def test_readonly_plan_projects_all_rows_in_one_typed_call(monkeypatch): + from loopx.control_plane.quota.cli_projection import compact_quota_plan_cli_payload + from loopx.control_plane.quota import turn_envelope + summary = {"schema_version": "todo_summary_v0", "backlog_items": _items(3, prefix="cold"), + "first_executable_items": _items(1, prefix="work"), "future_items": [False, None]} + row = {"agent_todos": summary, "user_todos": summary} + calls, real = [], turn_envelope._turn_envelope_result + def observed(operation, **params): + calls.append(operation) + return real(operation, **params) + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", observed) + payload = {"mode": "plan", "groups": {"ready": [row] * 8}, "next_automatic_turn": row} + projected = compact_quota_plan_cli_payload(payload, detail_sections=frozenset({"user-todos"})) + assert calls == ["reference_context"] + for result in [*projected["groups"]["ready"], projected["next_automatic_turn"]]: + assert result["user_todos"] == summary + assert "backlog_items" not in result["agent_todos"] + assert result["agent_todos"]["future_items"] == [False, None] + assert result["agent_todos"]["payload_compaction"]["full_detail_cold_path"] == "quota plan --include-detail agent-todos" + + def test_markdown_distinguishes_peer_blockers_from_native_admission() -> None: peer = {"mode": "task_scoped_peer", "execution_scope": "peer_agent_activation", "execution_state": "blocked", "task_selection": "canonical_claimed_candidates", @@ -802,7 +858,8 @@ def test_compact_quota_deduplicates_vision_without_losing_decision_rules() -> No ) assert len(json.dumps(payload, indent=2)) > 40_000 - # The old fixture hid selectable candidates. Retain that context and measure the cost. + # The historical 40KB fixture hid 28 candidate/binding records. Retaining + # that decision context is an intentional cost, not a compaction success. assert len(json.dumps(compact, indent=2)) < len(json.dumps(payload, indent=2)) assert compact["selected_todo"] == payload["selected_todo"] assert compact["interaction_contract"]["mode"] == "bounded_delivery" @@ -1039,62 +1096,6 @@ def test_compact_quota_should_run_cli_payload_keeps_hot_path_identity_in_markdow assert "- interaction_contract: " in markdown -@pytest.mark.parametrize("role", ["agent", "user"]) -def test_shared_diagnostics_preserve_unknown_lists_nested_fields_and_complete_items(role): - lane = "first_executable_items" if role == "agent" else "first_open_items" - item = {**_items(1, prefix="retained")[0], "text": "Original constraint. " * 200, - "future_requirements": ["Keep the tail stop."], "lease": {"owner": "peer", "epoch": 7}} - unknown = [None, False, 0, {"requirement": "A new obligation"}] - summary = {"schema_version": "todo_summary_v0", lane: [item], "backlog_items": _items(5, prefix="diagnostic"), - "future_items": unknown, "future_rules": unknown, - "deferred_items": [{"todo_id": "later", "future_global_stop": "Do not continue."}], - "recent_completed_advancement_items": [{"todo_id": "old", "completion_result": {"future_stop": True}}], - "claim_scope": {"owner": "peer", "future_items": unknown, "blocked_claimed_items": []}, - "future_object": {"items": unknown}, - "todo_succession_warning": {"items": _items(4, prefix="old"), "todo_ids": ["original-id"], "future_items": unknown}} - payload = {f"{role}_todo_summary": summary, "capability_gate": {"runnable_candidates": [item], "future": unknown}, - "action_portfolio": {"suggested_actions": [item]}, "planning_horizon": {"work_items": [item]}} - original = deepcopy(payload) - projected = compact_quota_should_run_cli_payload(payload) - result = projected[f"{role}_todo_summary"] - assert result[lane] == [item] - assert result["future_items"] == result["future_rules"] == unknown - assert result["future_object"] == {"items": unknown} - assert result["deferred_items"] == summary["deferred_items"] - assert result["recent_completed_advancement_items"] == summary["recent_completed_advancement_items"] - assert result["claim_scope"] == {"owner": "peer", "future_items": unknown} - assert result["todo_succession_warning"]["todo_ids"] == ["original-id"] - assert result["todo_succession_warning"]["future_items"] == unknown - for key in ("capability_gate", "action_portfolio", "planning_horizon"): - assert projected[key] == payload[key] - assert payload == original - assert compact_quota_should_run_cli_payload(projected) == projected - detailed = compact_quota_should_run_cli_payload(payload, - include_todo_summary_detail=True, include_user_todo_summary_detail=True) - assert detailed == payload - - -def test_readonly_plan_projects_all_rows_in_one_typed_call(monkeypatch): - from loopx.control_plane.quota.cli_projection import compact_quota_plan_cli_payload - from loopx.control_plane.quota import turn_envelope - summary = {"schema_version": "todo_summary_v0", "backlog_items": _items(3, prefix="cold"), - "first_executable_items": _items(1, prefix="work"), "future_items": [False, None]} - row = {"agent_todos": summary, "user_todos": summary} - calls, real = [], turn_envelope._turn_envelope_result - def observed(operation, **params): - calls.append(operation) - return real(operation, **params) - monkeypatch.setattr(turn_envelope, "_turn_envelope_result", observed) - payload = {"mode": "plan", "groups": {"ready": [row] * 8}, "next_automatic_turn": row} - projected = compact_quota_plan_cli_payload(payload, detail_sections=frozenset({"user-todos"})) - assert calls == ["reference_context"] - for result in [*projected["groups"]["ready"], projected["next_automatic_turn"]]: - assert result["user_todos"] == summary - assert "backlog_items" not in result["agent_todos"] - assert result["agent_todos"]["future_items"] == [False, None] - assert result["agent_todos"]["payload_compaction"]["full_detail_cold_path"] == "quota plan --include-detail agent-todos" - - @pytest.mark.parametrize("mode", ["should-run", "status", "plan"]) @pytest.mark.parametrize("expanded", [None, "agent", "user"]) def test_diagnostic_transport_preserves_source_number_spelling_and_precision(mode, expanded): @@ -1134,41 +1135,3 @@ def project(value): assert json.dumps(value["payload_compaction"]["source_projection"], ensure_ascii=False) == json.dumps(summary["payload_compaction"], ensure_ascii=False) assert json.dumps(payload, ensure_ascii=False) == encoded assert json.dumps(project(result), ensure_ascii=False) == json.dumps(result, ensure_ascii=False) - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("mode", ["status", "plan"]) -def test_public_readonly_projection_preserves_canonical_work_and_detail(tmp_path, monkeypatch, provider, mode): - from tests.control_plane import test_quota_settlement_cli as fixture - from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime - if provider == "sqlite": - isolate_sqlite_runtime(tmp_path, monkeypatch) - project, runtime, registry = fixture._write_fixture(tmp_path) - state = fixture._configure_read_only_todo(project) - fixture._initialize_fixture_authority(project, runtime, provider=provider) - def canonical_work(): - rc, value = fixture._run_cli(registry, runtime, "todo", "list", "--goal-id", fixture.GOAL_ID, - "--todo-id", fixture.TODO_ID, cwd=project) - assert rc == 0 and value["matched"] is True, value - assert value["authority_read"]["source_authority"] == f"{provider}_v0" - assert value["authority_read"]["legacy_fallback_used"] is False - return value["todo"], value["authority_read"]["provider_revision"] - before = canonical_work() - original = state.read_bytes() - def call(*detail): - rc, value = fixture._run_cli(registry, runtime, "quota", mode, *detail, cwd=project) - assert rc == 0 and value["ok"] is True, value - return {row["goal_id"]: row for rows in value["groups"].values() for row in rows} - projected = call()[fixture.GOAL_ID] - detailed = call("--include-detail", "agent-todos", "--include-detail", "user-todos")[fixture.GOAL_ID] - for role in ("agent", "user"): - one, full = projected[f"{role}_todos"], detailed[f"{role}_todos"] - assert one["open_count"] == full["open_count"] - assert one["work_counts"] == full["work_counts"] - assert one["payload_compaction"]["full_detail_cold_path"] == f"quota {mode} --include-detail {role}-todos" - lane = "first_executable_items" if role == "agent" else "first_open_items" - assert one[lane] == full[lane] - assert canonical_work() == before - assert state.read_bytes() == original - assert fixture._spend_run_count(runtime) == 0 - assert fixture._heartbeat_receipt_count(runtime, "readonly-projection") == 0 diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index 13b059ba73..a0c14ade16 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -215,6 +215,17 @@ def consume(page): assert "Preserve the original Goal and earlier requirements" in reason assert [row["id"] for row in basis["goal_acceptance_contract"]["outcome_requirements"]["outcomes"]] == ["export-agent-a"] assert json.loads(Path(delivered["source_capture"]["path"]).read_bytes()) == packet + # Bounded diagnostics reference the original immutable decision, never + # a new guard. Read every byte through the public recovery command. + diagnostics = delivered["context"]["agent_todo_summary"]["payload_compaction"] + assert diagnostics["full_detail_capture_ref"] == "$.source_capture" + source_ref = delivered["source_capture"] + original_page = read_context_capture(build_parser().parse_args(["--format", "json", "quota", "context-read", + "--capture-file", source_ref["path"], "--capture-sha256", source_ref["sha256"]])) + assert consume(original_page) == packet + for key in ("selected_todo", "planning_horizon", "capability_gate", "action_portfolio"): + if key in packet: + assert delivered["context"][key] == packet[key] candidate = json.loads((native_root / "session.json").read_text())["candidate"] assert candidate["capture_file"] == projected["recovery"]["path"] assert json.loads(Path(candidate["capture_file"]).read_bytes()) == delivered @@ -249,7 +260,12 @@ def consume(page): recovery = read_context_capture(build_parser().parse_args(shlex.split(warm["recovery"]["command"])[1:])) restored = consume(recovery) assert restored["read_scope"]["required_outcome_refs"] == delivered["read_scope"]["required_outcome_refs"] - assert restored["context"] == delivered["context"] + assert restored == json.loads(Path(warm["recovery"]["path"]).read_bytes()) + restored_diagnostics = restored["context"]["agent_todo_summary"]["payload_compaction"] + assert restored_diagnostics["full_detail_capture_ref"] == "$.source_capture" + source_ref = restored["source_capture"] + assert consume(read_context_capture(build_parser().parse_args(["--format", "json", "quota", "context-read", + "--capture-file", source_ref["path"], "--capture-sha256", source_ref["sha256"]]))) == packet assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == source_before assert capture.read_bytes() == captured # Optional scope/codec failure recovers the complete original source, diff --git a/tests/control_plane_ts/goal_outcome_refinement.test.ts b/tests/control_plane_ts/goal_outcome_refinement.test.ts index 19f222536a..66f4eb153b 100644 --- a/tests/control_plane_ts/goal_outcome_refinement.test.ts +++ b/tests/control_plane_ts/goal_outcome_refinement.test.ts @@ -193,6 +193,51 @@ function nativeProjection(payload: JsonObject, source_capture: JsonObject): Json return (prepared.projection ?? (prepared.delivery as JsonObject).context) as JsonObject; } +test("native and legacy diagnostics share one owner without hiding decision context", () => { + const payload = outcomeContextPacket(); + const task = {todo_id: "todo_own", text: "Original requirement and final stop. ".repeat(80), + future_requirements: ["Retain this new obligation"], lease: {owner: "author", epoch: 2}}; + const future = [null, false, 0, {constraint: "Unknown relevance"}]; + const diagnostic = {todo_id: "known", text: "Known diagnostic", status: "open"}; + const unknownTail = {todo_id: "later", future_global_stop: "Retain this new stop"}; + payload.agent_todo_summary = {schema_version: "todo_summary_v0", open_count: 1, + first_executable_items: [task], backlog_items: [diagnostic], future_items: future, + deferred_items: [unknownTail], recent_completed_advancement_items: [{todo_id: "old", completion_result: {future_stop: true}}], + future_object: {items: future}, claim_scope: {blocked_claimed_items: [diagnostic], future_items: future}}; + payload.capability_gate = {runnable_candidates: [task], available: ["shell"], future}; + payload.action_portfolio = {suggested_actions: [task]}; + payload.planning_horizon = {work_items: [task]}; + const before = structuredClone(payload), source_capture = {path: "/capture/one ' quoted.json", sha256: "a".repeat(64)}; + const native = nativeProjection(payload, source_capture); + const scoped = native.context as JsonObject; + const legacy = evaluateTurnEnvelope({operation: "reference_context", payload, diagnostics: {}}); + const one = scoped.agent_todo_summary as JsonObject, two = legacy.agent_todo_summary as JsonObject; + assert.equal(one.backlog_items, undefined); + assert.deepEqual(one.first_executable_items, [task]); + assert.deepEqual(one.future_items, future); + assert.deepEqual(one.future_object, {items: future}); + assert.deepEqual(one.deferred_items, [unknownTail]); + assert.deepEqual(one.recent_completed_advancement_items, [{todo_id: "old", completion_result: {future_stop: true}}]); + assert.deepEqual(one.claim_scope, {future_items: future}); + assert.deepEqual({...one, payload_compaction: null}, {...two, payload_compaction: null}); + assert.equal((one.payload_compaction as JsonObject).full_detail_capture_ref, "$.source_capture"); + assert.equal((one.payload_compaction as JsonObject).full_detail_cold_path, undefined); + assert.equal((two.payload_compaction as JsonObject).full_detail_cold_path, "quota should-run --include-detail agent-todos"); + for (const key of ["capability_gate", "action_portfolio", "planning_horizon"]) { + assert.deepEqual(scoped[key], before[key]); assert.deepEqual(legacy[key], before[key]); + } + const detail = evaluateTurnEnvelope({operation: "reference_context", payload, + diagnostics: {agent_detail: true, user_detail: true}}); + assert.deepEqual(detail, payload); + assert.deepEqual(payload, before); + // The derived wrapper must still be checked against the exact original. + evaluateTurnEnvelope({operation: "captured_outcome", payload: native, source_payload: payload, outcome_ref: ""}); + const altered = structuredClone(native); + ((altered.context as JsonObject).agent_todo_summary as JsonObject).future_items = []; + assert.throws(() => evaluateTurnEnvelope({operation: "captured_outcome", payload: altered, + source_payload: payload, outcome_ref: ""}), /differs from its original source/); +}); + test("scoped model context retains canonical source, global controls and exact optional detail", () => { const payload = outcomeContextPacket(), before = structuredClone(payload); const source_capture = {path: "/capture/original.json", sha256: "a".repeat(64)}; From ac3b2bc3934af7d51a0a614f1dba6662e375efe8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:34:35 +0800 Subject: [PATCH 18/37] docs(controller): record shared diagnostic costs and independent slice Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 17 ++-- ...ller-executor-unified-contract-v0.zh-CN.md | 11 ++- .../2026-10-28-packaged-heartbeat.md | 79 ++++++++++++++++++- .../2026-10-28-packaged-heartbeat.zh-CN.md | 60 +++++++++++++- .../rfcs/loopx-overall-roadmap-v0.md | 12 ++- .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 9 ++- 6 files changed, 171 insertions(+), 17 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 6eb1919507..ca113799c5 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1602,11 +1602,18 @@ Python projection; this is not evidence of a positive full-path quota comparison The [packaged heartbeat comparison](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md) now exercises real isolated File/SQLite CLI cold, warm and same-capture recovery -against original quota. Necessary source/replay checks pass, but default context -and total stdout grow; expanding all peer outcomes adds reads and cost. No positive -complete-cost diff or model benefit is qualified. The existing C1 continuation is -shared typed diagnostic compaction with unknown-field and obligation preservation, -followed by the same comparison. This does not promote the candidate by default. +against original quota. The first result grew default context and stdout. The +follow-through shares one typed diagnostic owner across legacy and scoped native +callers and removes the replaced Python algorithms. Its independently usable +legacy-reader slice is proposed in [#7557](https://github.com/loopx-project/loopx/pull/7557); +the integration uses the identical module. Unknown fields/lists, complete retained +records, candidates and portfolio remain intact. The repeated comparison reduces +cold/recovery context by about 5.3 KB, but warm context still grows about 2.8 KB; +all-peer expansion still adds 13,940 bytes and ten CLI calls. The public v4 budget +probe still fails on both preceding and current integration wheels. No positive +complete-cost diff, default performance or model benefit is qualified. Continue +within C1 on warm reconstruction/metadata cost and remaining consumers; this does +not promote the candidate by default or qualify the separate controller experiment. Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index a33fe01555..22a81bd2a5 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -1178,9 +1178,14 @@ capture 前验证失败。工作契约形成前的错误则在两种 CLI flag [打包 heartbeat 对照](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md) 已使用真实隔离 File/SQLite CLI,相对原 quota 核对冷态、温态和同 capture 恢复。 -必要来源/重放检查通过,但默认上下文及总 stdout 增加,展开全部 peer 成果另增补读 -和成本;完整成本正向 diff 及模型收益均未资格化。原 C1 继续由共享 typed owner 消除 -诊断重复,保留未知字段和义务,再跑相同对照;这不提升候选默认。 +首组默认上下文和 stdout 增长。后续让旧调用方和 scoped native 复用同一个 typed +诊断 owner,删除对应 Python 算法;可独立使用的旧读取方切片已在 +[#7557](https://github.com/loopx-project/loopx/pull/7557) 提案,集成版本使用相同模块。 +未知字段/列表、完整保留条目、候选和 portfolio 不变。重跑后冷态/恢复约少 5.3 KB, +温态仍多约 2.8 KB;全部 peer 补读仍增加 13,940 字节和十次 CLI。公开 v4 预算探针 +在前一版及当前集成 wheel 上仍失败。完整成本正向、默认性能及模型收益均未资格化。 +继续在原 C1 诊断温态重建/元数据成本及剩余消费者,不提升默认,也不验收独立的 +controller 实验。 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index 82007e1f70..977c3ef99d 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -81,7 +81,7 @@ candidate retains about 6.6 KB of repeated Todo diagnostic lanes and adds about the remaining savings. This is a composition/compaction gap, not evidence that peer scope itself lowers total cost. -The next C1 slice must characterize necessary quota diagnostic compaction and +That checkpoint selected a C1 slice to characterize necessary diagnostic compaction and move it into the existing shared typed composer, retiring the replaced Python caller algorithm. Preserve complete selected work, portfolio/horizon, claim/lease facts, global obligations, unknown fields, exact references and @@ -103,3 +103,80 @@ missing-dependency, harness-carrier and outdated call-count failures are retaine as authoring/validation costs, not silently converted into product passes. No model, paid evaluation, managed benchmark, live install, release or merge was performed for this comparison. + +## Shared diagnostic owner follow-through + +The follow-through at `d33c1a19d91738c66073d9e71586f14466f36d89` replaces the +Python Todo-list algorithms with `quota/diagnostic_projection.ts`. Existing +`reference_context` callers share it for should-run and the whole status/plan row +collection; scoped native composition reuses it inside its existing request. +[#7557](https://github.com/loopx-project/loopx/pull/7557), at +`5b5d9dd3b0a816aae9d5160b849cff4a3c38da13` on main +`19c213324fbb5990288bd374c1b79096f22bb152`, proposes the independently usable +legacy-reader slice as a normal PR. Its typed module is identical to the +integration module. Neither proposal is merged or installed by this work. + +Only explicitly known diagnostic lanes with recognized canonical Todo records may be +bounded. An unknown item field/version, a non-record item or complex nested +content preserves that entire list. Unknown lists survive. Retained records stay +complete; portfolio prose, capability candidates, planning horizon, selected +work and existing succession IDs are not shortened or reconstructed. Original +capture/recovery remains complete. Projection metadata points to the existing +source capture instead of repeating long recovery commands in each lane. + +The same frozen packaged harness and original quota commit were rerun in one +matched cohort, with the same path lengths, synthetic task and 24 clauses. The +base wheel has 1,884 source-matching product members; the candidate has 1,885. +These measurements belong to the recorded pre-rebase commit above. The later +rebase onto `19c213324` adds the unrelated merged Lark-return change; it does not +turn the frozen experiment into a measurement of that later head. + +| Backend / phase | Context bytes, original → candidate | Stdout bytes | Logical requests | Elapsed ms | +| --- | ---: | ---: | ---: | ---: | +| File / cold full | 75,648 → 70,388 | 95,004 → 89,875 | 103 → 102 | 4,959.03 → 6,475.69 | +| File / warm delta | 41,252 → 44,029 | 58,401 → 61,183 | 99 → 98 | 3,465.08 → 4,538.62 | +| File / loss recovery | 74,880 → 69,618 | 90,113 → 84,903 | 6 → 6 | 5,108.18 → 5,762.51 | +| SQLite / cold full | 75,740 → 70,492 | 95,114 → 89,997 | 103 → 102 | 5,771.16 → 5,856.03 | +| SQLite / warm delta | 41,346 → 44,133 | 58,509 → 61,301 | 99 → 98 | 3,772.95 → 3,794.99 | +| SQLite / loss recovery | 74,974 → 69,722 | 90,217 → 85,017 | 6 → 6 | 4,226.47 → 5,113.92 | + +Both arms still use 13/9/13 CLI calls. Expanding all five peers still adds 13,940 +context bytes, 17,620 stdout bytes and 10 CLI/logical requests to each candidate +phase; measured optional-read time is 4,107–4,875 ms. The original already has +those bodies inline. The seven settlement/negative/readback calls still use 226 +logical requests with identical per-backend stdout; one effect/debit and the +open task are preserved. Builds, setup, failed probes, two invalidation hooks +and optional reads remain separate recorded costs. + +Cold/recovery context is about 5.3 KB smaller, but warm context is about 2.8 KB +larger and all-peer consumption remains more expensive. Every observed candidate +phase is slower in this cohort; with one observation and uncontrolled background +load this is not a causal latency estimate. An intermediate version repeated +capture commands and was larger still (File warm 45,187; SQLite 45,303 bytes); +the metadata fix does not erase that experiment or establish net benefit. + +Four cold/warm File/SQLite presentations were independently compared with their +original source captures for complete selected work, boundary, horizon, +capability gate and portfolio. An intentionally mutated QA wheel that deletes +the capability gate is rejected through the packaged guard/reader path. This +checks oracle sensitivity, not model adherence. Source tampering, early spend, +same-capture loss recovery and once-only settlement retain their negative and +positive checks. + +The final affected integration selection passes 144 Python and 117 typed tests; +an earlier broader selection had 533 passes and five optional skips, before the +unknown-item fallback was strengthened. The independent #7557 slice passes 264 +Python/70 typed cases, including real File/SQLite status/plan/detail and canonical +Todo readback, and all 14 selected premerge checks. Status/plan now pays one coarse +typed request; explicit full Todo summaries skip it. A crowded diagnostic unit +fixture grows to 43,862 characters because complete candidates are retained; this +is an intentional information delta, not a speedup. The unchanged public v4 +multi-Agent budget probe passes on original quota but fails at 27,934 > 13,000 +characters on both the preceding and current integration wheels. That integration +failure remains open; no public budget was raised. + +The next existing C1 work is to diagnose the warm reconstruction/metadata cost +without losing obligations, then qualify remaining consumers and writer cutover. +No positive complete-cost diff, default SQLite performance, installed App/model +retention, token/cache/billing, accepted-outcome or long-horizon benefit is claimed. +C1 and the separate C2/controller experiment remain open. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index 0c0f7e98ba..51b593f43a 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -64,7 +64,7 @@ SDK 覆盖、任务验收成果,也不是 controller/executor 双会话实验 诊断列表,并新增约 3.9 KB scope 元数据;候选列表及其他诊断重复消耗了剩余节省。 这是组合/压缩缺口,不能据此认定 peer scope 已降低总成本。 -下一片 C1 应刻画原 quota 中必要的诊断压缩,迁入既有共享 typed composer,并删除 +该检查点选择的后继 C1 切片是刻画原 quota 中必要的诊断压缩,迁入既有共享 typed composer,并删除 被替代的 Python caller 算法。保留完整选中工作、portfolio/horizon、claim/lease、全局 义务、未知字段、精确引用与历史恢复。旧 Python compactor 会丢弃未知列表和缩短部分 正文,原样复制不满足此合同。继续用同一打包对照分别测默认读取与全 peer 补读,再判断 @@ -79,3 +79,61 @@ acceptance 操作和重复 controller-input dispatch;完整组合复用相同 类型检查通过。此前构建指纹、缺依赖、harness carrier 和过时调用数断言失败保留为作者/ 验证成本,不静默改成产品通过。本对照未调用模型、付费评测、managed benchmark, 未执行 live 安装、发布或合并。 + +## 共享诊断 owner 的接续结果 + +后续 `d33c1a19d91738c66073d9e71586f14466f36d89` 以 +`quota/diagnostic_projection.ts` 替换 Python Todo 列表算法。现有 +`reference_context` 调用方让 should-run 和整组 status/plan 行复用它;scoped +native composition 在已有请求内部调用相同函数。 +[#7557](https://github.com/loopx-project/loopx/pull/7557) 在 main +`19c213324fbb5990288bd374c1b79096f22bb152` 上以 +`5b5d9dd3b0a816aae9d5160b849cff4a3c38da13` 提交可独立使用的旧读取方切片, +为正常 PR;其 typed 模块与集成版本完全相同。本次未合并或安装任一提案。 + +仅明确已知的诊断 lane 且全部为已知规范 Todo 记录时可以有界投影。条目字段/版本未知、 +非记录条目或复杂嵌套内容会保留整个列表;未知列表保留。留下的记录全文不变,不缩短 +或重建 portfolio 正文、能力候选、planning horizon、选中工作和原 succession IDs。 +原 capture/恢复仍完整。投影元数据指向已有 source capture,不在各 lane 重复长恢复命令。 + +复用同一冻结打包 harness 和原 quota commit,按相同路径长度、合成任务及 24 条完整 +要求重跑一组匹配 cohort。基线 wheel 的 1,884 个产品成员、候选的 1,885 个成员分别 +与来源一致。测量绑定上述 rebase 前 commit;随后 rebase 到 `19c213324` 只带入 +无关的已合入 Lark 返回修复,不把冻结实验改称后续 head 的测量。 + +| 后端/阶段 | 上下文字节,原实现 → 候选 | Stdout 字节 | 逻辑请求 | 耗时 ms | +| --- | ---: | ---: | ---: | ---: | +| File/冷态完整 | 75,648 → 70,388 | 95,004 → 89,875 | 103 → 102 | 4,959.03 → 6,475.69 | +| File/温态差分 | 41,252 → 44,029 | 58,401 → 61,183 | 99 → 98 | 3,465.08 → 4,538.62 | +| File/丢失恢复 | 74,880 → 69,618 | 90,113 → 84,903 | 6 → 6 | 5,108.18 → 5,762.51 | +| SQLite/冷态完整 | 75,740 → 70,492 | 95,114 → 89,997 | 103 → 102 | 5,771.16 → 5,856.03 | +| SQLite/温态差分 | 41,346 → 44,133 | 58,509 → 61,301 | 99 → 98 | 3,772.95 → 3,794.99 | +| SQLite/丢失恢复 | 74,974 → 69,722 | 90,217 → 85,017 | 6 → 6 | 4,226.47 → 5,113.92 | + +两臂 CLI 次数仍为 13/9/13。候选各阶段展开全部五项 peer 仍增加 13,940 字节正文、 +17,620 字节 stdout 及 10 次 CLI/逻辑请求;补读耗时 4,107–4,875 ms,原实现已 +内联这些正文。七次结算/负例/读回仍为 226 次逻辑请求,同后端 stdout 相同, +保持一次 effect/扣额和开放任务。构建、初始化、失败探针、两次失效 hook 和补读 +各自保留成本记录。 + +冷态/恢复正文约少 5.3 KB,但温态约多 2.8 KB,全部 peer 消费仍更贵。本组每个 +候选阶段的观测耗时均更长;单次观测和未控制的后台负载不能支持因果延迟估计。 +中间版本重复 capture 命令,更大(File 温态 45,187、SQLite 45,303 字节); +修元数据不抹去该实验,也不证明净收益。 + +四组冷/温 File/SQLite 展示与各自原始 capture 独立核对完整选中工作、boundary、 +horizon、capability gate 和 portfolio。故意删除 capability gate 的 QA wheel +经实际打包 guard/reader 路径被拒绝;这验证 oracle 敏感性,不证明模型遵从。 +来源篡改、提前 spend、同 capture 丢失恢复及一次结算的正负例保持。 + +最终集成受影响集合 144 项 Python、117 项 typed 通过;较早的扩大集合为 533 通过、 +五项可选跳过,发生在加强未知条目回退之前。独立 #7557 为 264 项 Python/70 项 typed +通过,含真实 File/SQLite status/plan/detail 和规范 Todo 回读,14 项选中 premerge +检查均过。Status/plan 新增一次粗粒度 typed 请求;显式完整 Todo 摘要跳过它。 +拥挤诊断单元夹具因保留完整候选增至 43,862 字符,是有意的信息增量,不是提速。 +未修改的公开 v4 多 Agent 预算探针在原 quota 通过,在前一版及当前集成 wheel 均为 +27,934 > 13,000 字符而失败;该集成失败保持开放,未扩大公开预算。 + +下一步沿现有 C1 诊断温态重建/元数据成本,保留义务,再验收剩余消费者和 writer +切换。不宣称完整成本正向、默认 SQLite 性能、正式 App/模型保留、token/cache/账单、 +验收成果或长程收益。C1 及独立的 C2/controller 实验继续开放。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 3d043d70d8..0b6b9c0a67 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -204,10 +204,14 @@ input; new-admission cutover stays separate from historical receipt replay. The [packaged C1 heartbeat checkpoint](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md) preserves required source and same-capture settlement semantics on isolated -File/SQLite, but delivered bytes grow and all-peer expansion adds cost. Continue -within C1 by converging diagnostic compaction in its shared typed owner, preserving -unknowns and obligations, then repeat the quota-baseline comparison. This is no -default promotion or positive complete-cost qualification. +File/SQLite. Its diagnostic follow-through reuses one typed owner and removes the +Python duplicates; [#7557](https://github.com/loopx-project/loopx/pull/7557) proposes +the independently usable legacy-reader slice, shared unchanged by #7430. Repeated +quota-baseline measurements reduce cold/recovery bytes but still grow warm input +and add all-peer read cost; the integration's existing v4 budget failure remains. +Continue C1 on warm composition cost and remaining real consumers with full +unknown-field/obligation preservation. This is no default promotion or positive +complete-cost qualification; controller-model and outcome acceptance stay open. Use that RFC’s C1–C9 task/deletion map within the existing work streams: first qualify unified single-session inputs, then role binding and shared lifecycle, diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 51cba9a56f..05cfba4232 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -156,9 +156,12 @@ C1 的[消费链切换表](controller-executor-unified-contract-v0.zh-CN.md#c1- 最终输入;新准入切换与历史回执重放分别处理。 [C1 打包 heartbeat 检查点](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md) -在隔离 File/SQLite 保留必读来源及同 capture 结算语义,但交付字节增加,全部 peer -补读另增成本。继续在原 C1 的共享 typed owner 收敛诊断压缩,保留未知字段与义务, -再做原 quota 基线对照;这不是默认提升或完整成本正向资格。 +在隔离 File/SQLite 保留必读来源及同 capture 结算语义。后续诊断投影复用同一 typed +owner 并删除 Python 重复算法;[#7557](https://github.com/loopx-project/loopx/pull/7557) +提案可独立使用的旧读取方切片,#7430 原样复用相同模块。重跑原 quota 基线对照后 +冷态/恢复字节下降,但温态输入仍增长,全部 peer 补读另增成本,集成原有 v4 预算 +失败仍保留。原 C1 继续诊断温态组合成本及剩余真实消费者,保留未知字段和完整义务; +这不是默认提升或完整成本正向资格,controller 模型与成果验收保持开放。 把该 RFC 的 C1–C9 任务/删除表放在既有工作流内:先验收统一单会话输入,再做角色绑定 与共享生命周期、replan/结果编写、executor 交流、benchmark 复用、打包 App readback From ae13554ada6d5530d934da493564761422bec820 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:18:05 +0800 Subject: [PATCH 19/37] test(context): qualify scoped overlay adoption and quota costs Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 8 +++ ...ller-executor-unified-contract-v0.zh-CN.md | 6 ++ .../2026-10-28-packaged-heartbeat.md | 64 +++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 50 +++++++++++++++ .../rfcs/loopx-overall-roadmap-v0.md | 6 ++ .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 4 ++ .../control_plane_ts/context_delivery.test.ts | 15 +++-- 7 files changed, 148 insertions(+), 5 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index ca113799c5..8538fc5373 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1615,6 +1615,14 @@ complete-cost diff, default performance or model benefit is qualified. Continue within C1 on warm reconstruction/metadata cost and remaining consumers; this does not promote the candidate by default or qualify the separate controller experiment. +The integration also adopts the existing [#7528](https://github.com/loopx-project/loopx/pull/7528) +overlay in that same context codec. Relocated exact evidence can share one base +subtree while current and unknown controls remain literal, including through the +scoped C1 wrapper. The ledger separates the positive relocation-only byte result +from the repeated packaged comparison: ordinary warm input still grows about +2.8 KB and the v4 budget failure remains. This removes a demonstrated transport +redundancy without establishing a positive full-cycle or outcome result. + Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, latency, failures/retries, verification and human intervention. A smaller first diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 22a81bd2a5..55078f597d 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -1187,6 +1187,12 @@ capture 前验证失败。工作契约形成前的错误则在两种 CLI flag 继续在原 C1 诊断温态重建/元数据成本及剩余消费者,不提升默认,也不验收独立的 controller 实验。 +集成版本也在相同 context codec 内采用现有 +[#7528](https://github.com/loopx-project/loopx/pull/7528) overlay:搬移的精确证据可共用 +一份基线子树,当前及未知控制仍字面交付,包含 C1 scoped wrapper。Ledger 将局部 +搬移的正向字节结果与重跑的打包对照分开:普通温态输入仍多约 2.8 KB,v4 预算仍失败。 +这移除了已证实的传输冗余,尚不能证明完整周期或成果收益正向。 + 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 只有必要语义保留且同等验收成果的完整成本降低,或可比预算下验收成果改善,才判正向。 diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index 977c3ef99d..e9c140898f 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -180,3 +180,67 @@ without losing obligations, then qualify remaining consumers and writer cutover. No positive complete-cost diff, default SQLite performance, installed App/model retention, token/cache/billing, accepted-outcome or long-horizon benefit is claimed. C1 and the separate C2/controller experiment remain open. + +## Relocated-context integration + +Runtime `167b31e5d078193b9fd73ad6609804e268390972`, rebased onto main +`f205c02a37d5a668c956733a721af911dcac7fab`, adopts the existing +[#7528](https://github.com/loopx-project/loopx/pull/7528) codec change at +`759c16a9cf430bb7442b47dd3a5c3336ffb93666`. The integration keeps main's scoped +preference binding, native checkpoint fixtures and intelligent-mediation design. +It does not create another encoder. One exact baseline subtree plus literal +current overrides replaces repeated paths to unchanged evidence. Unknown and +current controls remain inline; original capture, identity and recovery stay +unchanged. A scoped-wrapper regression exercises the same implementation. + +A 24-row synthetic relocation compares that runtime with its immutable parent +`87e216f24f966671d35e035df7ba285248b48bad`. Complete ordinary delta bytes fall +13,199 → 6,090; the C1 scoped-wrapper delta falls 13,809 → 6,230. Cold/current +full encodings remain 20,060/20,188 bytes and 20,167/20,295 bytes respectively. +Both reconstruct exactly; the actual old decoder recovers the same full current +capture for the new overlay. The original missing-overlay case fails before +adoption. These are codec observations, excluding model retention, tokens, +framing, billing and task outcomes. + +The unchanged packaged harness is rerun in a new matched cohort against original +quota `843be494266774b63b3de61d0adf5dd7f4f877a2`. Both arms use the same fixture +and equal path lengths; their 1,884/1,885 product members match their sources. +These results belong to this runtime and cohort; the earlier tables remain +historical observations of their own sources. + +| Backend / phase | Context bytes, original → candidate | Stdout bytes | Logical requests | Elapsed ms | +| --- | ---: | ---: | ---: | ---: | +| File / cold full | 75,211 → 69,934 | 94,432 → 89,286 | 103 → 102 | 5,961.28 → 5,183.22 | +| File / warm delta | 40,817 → 43,575 | 57,861 → 60,624 | 99 → 98 | 3,981.29 → 3,592.23 | +| File / loss recovery | 74,445 → 69,164 | 89,603 → 84,374 | 6 → 6 | 5,052.05 → 5,156.02 | +| SQLite / cold full | 75,311 → 70,044 | 94,550 → 89,414 | 103 → 102 | 5,205.45 → 6,393.69 | +| SQLite / warm delta | 40,913 → 43,683 | 57,971 → 60,746 | 99 → 98 | 3,482.77 → 4,216.01 | +| SQLite / loss recovery | 74,541 → 69,272 | 89,709 → 84,492 | 6 → 6 | 4,649.12 → 5,590.37 | + +CLI counts remain 13/9/13. All-five-peer expansion still adds 13,940 context +bytes, 17,620 stdout bytes and ten CLI/logical requests per candidate phase; +observed optional-read time ranges from 4,046 to 9,431 ms. Seven negative, +settlement and readback calls retain 226 logical requests and equal per-backend +stdout in both arms. Tamper/early-spend rejection, all 24 clauses, same-capture +recovery, one effect/debit and the open task pass. Four source comparisons keep +selected work, boundary, horizon, capability gate and portfolio complete; a new +mutated QA wheel dropping the capability gate is rejected by the real guard and +reader. No other owned build or test overlaps the timed cohort; host background +load remains uncontrolled. One observation does not qualify latency or p95. + +The final typed selection passes 147 cases and typecheck. The affected Python +run records 299 passes, 51 optional native/SDK skips and two recovery-text size +failures in a long temporary namespace. A controlled identical-path comparison +on immutable main and this runtime reproduces both failures at 2,015 characters; +neither hook embeds the requirement body, and both retain the original recovery +command. The normal-path lifecycle suite separately passes all 145 cases. No limit, +assertion or fixture body was relaxed, and the original failing run is retained. +The new wheel required rebuilding main's stale frontend bundle before successful +packaging. The unchanged v4 probe still fails at 27,934 > 13,000 characters. + +Relocation redundancy is reduced, while ordinary warm input still grows about +2.8 KB and all-peer reading remains more expensive. The latter is a different +remaining C1 composition/metadata gap. Default SQLite performance, installed App +and model retention, token/cache/billing, accepted outcomes and full long-horizon +benefit remain unqualified. Continue the existing C1 on that gap and remaining +consumers; this adoption does not close C1, C2 or the controller experiment. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index 51b593f43a..a76dbc6a6b 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -137,3 +137,53 @@ horizon、capability gate 和 portfolio。故意删除 capability gate 的 QA wh 下一步沿现有 C1 诊断温态重建/元数据成本,保留义务,再验收剩余消费者和 writer 切换。不宣称完整成本正向、默认 SQLite 性能、正式 App/模型保留、token/cache/账单、 验收成果或长程收益。C1 及独立的 C2/controller 实验继续开放。 + +## 搬移上下文的集成 + +Runtime `167b31e5d078193b9fd73ad6609804e268390972` 已 rebase 到 main +`f205c02a37d5a668c956733a721af911dcac7fab`,采用现有 +[#7528](https://github.com/loopx-project/loopx/pull/7528) 的 codec 变更 +`759c16a9cf430bb7442b47dd3a5c3336ffb93666`,保留主干的偏好作用域绑定、原生 checkpoint +夹具及智能中介设计。没有新增第二编码器;精确基线子树加字面当前覆盖,替代对未变证据 +反复引用长路径。未知及当前控制仍内联,原 capture、身份和恢复保持,scoped wrapper +回归覆盖相同实现。 + +24 条合成记录搬移,对比该 runtime 的不可变父版本 +`87e216f24f966671d35e035df7ba285248b48bad`:普通完整差分 13,199 → 6,090 字节,C1 +scoped wrapper 差分 13,809 → 6,230。冷态/当前完整编码分别保持 20,060/20,188 及 +20,167/20,295 字节。两者精确重建,实际旧 decoder 对新 overlay 恢复相同当前全文; +原缺失 overlay 用例在采用前失败。这只是 codec 观察,不含模型保留、tokens、framing、 +账单或任务成果。 + +未改打包 harness 再以原 quota `843be494266774b63b3de61d0adf5dd7f4f877a2` 为基线运行 +一组匹配 cohort;两臂夹具和路径长度相同,1,884/1,885 个产品成员与各自来源一致。 +本组结果绑定本 runtime,之前表格仍属于各自原版本,不重新归属。 + +| 后端/阶段 | 上下文字节,原实现 → 候选 | Stdout 字节 | 逻辑请求 | 耗时 ms | +| --- | ---: | ---: | ---: | ---: | +| File/冷态完整 | 75,211 → 69,934 | 94,432 → 89,286 | 103 → 102 | 5,961.28 → 5,183.22 | +| File/温态差分 | 40,817 → 43,575 | 57,861 → 60,624 | 99 → 98 | 3,981.29 → 3,592.23 | +| File/丢失恢复 | 74,445 → 69,164 | 89,603 → 84,374 | 6 → 6 | 5,052.05 → 5,156.02 | +| SQLite/冷态完整 | 75,311 → 70,044 | 94,550 → 89,414 | 103 → 102 | 5,205.45 → 6,393.69 | +| SQLite/温态差分 | 40,913 → 43,683 | 57,971 → 60,746 | 99 → 98 | 3,482.77 → 4,216.01 | +| SQLite/丢失恢复 | 74,541 → 69,272 | 89,709 → 84,492 | 6 → 6 | 4,649.12 → 5,590.37 | + +CLI 次数仍为 13/9/13;全部五项 peer 补读在候选每阶段仍增加 13,940 字节正文、17,620 +字节 stdout 和十次 CLI/逻辑请求,观测耗时 4,046–9,431 ms。七次负例、结算及回读 +仍为 226 次逻辑请求,同后端两臂 stdout 相同。篡改/提前 spend 拒绝、24 条全文、同 +capture 恢复、一次 effect/扣额及开放任务通过。四组来源核对完整保留 selected work、 +boundary、horizon、capability gate 和 portfolio;新 QA mutant wheel 删除能力候选后被 +真实 guard/reader 拒绝。计时 cohort 没有其他自有构建或测试并发,宿主后台负载未控; +单次观察不认证延迟或 p95。 + +最终 typed 集合 147 项及 typecheck 通过。受影响 Python 运行保留 299 通过、51 项可选 +native/SDK 跳过,以及长临时 namespace 下两项恢复文本尺寸失败。同路径受控比较在 +不可变 main 和本 runtime 均复现两项 2,015 字符失败;两者钩子正文不内嵌原要求全文, +均保留原恢复命令。正常路径的 lifecycle 套件另有全部 145 项通过,没有放宽预算、断言或夹具正文, +原失败记录保留。新 wheel 先遇到主干前端 bundle 过期,重建后打包成功。未改 v4 探针 +仍为 27,934 > 13,000 字符而失败。 + +搬移冗余已减少,普通温态输入仍多约 2.8 KB,全部 peer 阅读仍更贵;后者是原 C1 +组合/元数据的另一未解缺口。默认 SQLite 性能、正式 App/模型保留、token/cache/ +账单、验收成果和完整长程净收益均未资格。沿原 C1 继续该缺口及剩余消费者,不以采用 +本修复结清 C1、C2 或 controller 实验。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 0b6b9c0a67..8808c5e02d 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -213,6 +213,12 @@ Continue C1 on warm composition cost and remaining real consumers with full unknown-field/obligation preservation. This is no default promotion or positive complete-cost qualification; controller-model and outcome acceptance stay open. +The same integration codec now adopts [#7528](https://github.com/loopx-project/loopx/pull/7528) +for exact relocated evidence with literal current overrides, including scoped +context. The ledger records smaller relocation-only deltas alongside the still +negative ordinary-warm byte comparison; this is shared-owner adoption, not a +second encoder or default promotion. + Use that RFC’s C1–C9 task/deletion map within the existing work streams: first qualify unified single-session inputs, then role binding and shared lifecycle, replan/result authoring, executor exchange, benchmark reuse, packaged App diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 05cfba4232..c4a5375c99 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -163,6 +163,10 @@ owner 并删除 Python 重复算法;[#7557](https://github.com/loopx-project/l 失败仍保留。原 C1 继续诊断温态组合成本及剩余真实消费者,保留未知字段和完整义务; 这不是默认提升或完整成本正向资格,controller 模型与成果验收保持开放。 +同一集成 codec 现采用 [#7528](https://github.com/loopx-project/loopx/pull/7528),在精确 +搬移证据上叠加字面当前控制,覆盖 scoped context。Ledger 同时记录局部搬移差分变小 +和普通温态字节对照仍负向;这是采用共享 owner,没有新增第二编码器或提升默认。 + 把该 RFC 的 C1–C9 任务/删除表放在既有工作流内:先验收统一单会话输入,再做角色绑定 与共享生命周期、replan/结果编写、executor 交流、benchmark 复用、打包 App readback 及退役。App heartbeat 流程和实际 benchmark 消费者分别验收;managed Codex 可以作为 diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index 9c8d2b52db..4ee158935f 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -2,7 +2,7 @@ import {createHash} from "node:crypto"; import assert from "node:assert/strict"; import test from "node:test"; import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; -import {compactTodoContextSource, prepareContextDelivery, restoreContextDelivery} from "../../loopx/control_plane/quota/context_delivery.ts"; +import {compactTodoContextSource, OUTCOME_CONTEXT_PROJECTION_SCHEMA, prepareContextDelivery, restoreContextDelivery} from "../../loopx/control_plane/quota/context_delivery.ts"; import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts"; import {updateNativeContextLifecycle} from "../../loopx/control_plane/quota/native_context_lifecycle.ts"; @@ -386,7 +386,7 @@ test("relocated immutable records reuse complete subtrees without hiding fresh d current: prepareContextDelivery(request)}).context, request.context); }); -test("relocated evidence overlays keep current controls literal without repeating reference paths", () => { +for (const projected of [false, true]) test(`relocated evidence overlays preserve literal controls with scoped projection=${projected}`, () => { const requirements = Array.from({length: 24}, (_, i) => ({id: `r${i}`, description: `Requirement ${i}: ${long}`, status: "unverified", planning_satisfied: false, unknown_control: {may_publish: false}, @@ -397,8 +397,12 @@ test("relocated evidence overlays keep current controls literal without repeatin const work = {complete: true, sources: [{source: "goal_acceptance", content: { provider_revision: "current", goal_acceptance_contract: acceptance}}]}; // Actual callers relocate acceptance from planning into the admitted source. - const original = {planning_horizon: {core_goal: {objective: long, acceptance_contract: acceptance}}}; - const current = {interaction_contract: {agent_channel: {work_context: work}}}; + const wrap = (value: JsonObject): JsonObject => projected ? { + schema_version: OUTCOME_CONTEXT_PROJECTION_SCHEMA, context: value, + unknown_scope_control: {may_publish: false}, + } : value; + const original = wrap({planning_horizon: {core_goal: {objective: long, acceptance_contract: acceptance}}}); + const current = wrap({interaction_contract: {agent_channel: {work_context: work}}}); const initial = prepareContextDelivery({context: original, scope, capture_id: "a", surface: "quota_packet"}); const args = {context: current, scope, capture_id: "b", surface: "quota_packet"}; const delta = prepareContextDelivery({...args, baseline: initial, retained: {complete: true, target: initial.target}}); @@ -412,7 +416,8 @@ test("relocated evidence overlays keep current controls literal without repeatin }; walk(delta.changes); assert.ok(overlays.length > 0, "relocated evidence should share a base with literal current overrides"); - const path = ["interaction_contract", "agent_channel", "work_context", "sources", 0, "content", + if (projected) assert.deepEqual(inline(delta, ["unknown_scope_control"]), {may_publish: false}); + const path = [...(projected ? ["context"] : []), "interaction_contract", "agent_channel", "work_context", "sources", 0, "content", "goal_acceptance_contract", "outcome_requirements"]; assert.equal(inline(delta, [...path, "revision"]), 1); requirements.forEach((row, i) => { From 9be5256279191a3ff4f47d90e28b1928e4ec94a1 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:13:12 +0800 Subject: [PATCH 20/37] fix(context): deliver complete registered Goal material before business work Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> (cherry picked from commit 1198fa85ca06f83591f0dcfaf7889e9cc97541ae) (cherry picked from commit 734e66af90736ee68cd56d71c5e767a52666c8d2) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 20 +++++++++++++ ...ller-executor-unified-contract-v0.zh-CN.md | 15 ++++++++++ docs/reference/required-work-context.md | 13 +++++++- loopx/cli_commands/turn_decision.py | 1 + loopx/control_plane/quota/turn_envelope.ts | 7 +++-- .../turn_driver/host_candidate.py | 2 +- .../test_complete_turn_decision.py | 21 +++++++++++++ .../test_turn_host_context_delivery.py | 30 +++++++++++++++---- 8 files changed, 99 insertions(+), 10 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 8538fc5373..41f21652ef 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1834,3 +1834,23 @@ The directory is the index. A planning/source-inspection entry does not establis runtime delivery; live priorities and claims remain in the roadmap and canonical work. Keep historical entries append-only and refine the current normative plan here rather than treating a dated audit as a permanent execution queue. + + +### Local registered Goal material checkpoint + +The complete Turn writer and existing native context-delivery opt-in now request +full registered Goal material before capture. The source adapter returns original +UTF-8 text for unmarked or mixed files; the existing TypeScript context owner +checks the full-file revision and fulfills that read. Existing marked-region +lineage and canonical Todo authority remain. Ordinary quota with delivery off +keeps progressive reads. The bridge requires a `complete_goal_v1` acknowledgement; +an older runtime cannot silently ignore the request. + +Automatic business-work Host context uses `host_dispatch_context`, which refuses pending or +invalid Goal material before provider loading. Historical `host_context` metadata +reads remain available for original-version interpretation and necessary result +finalization; stored captures are not rewritten. This is a bounded material +delivery prerequisite. General capability-read interlocks, current-condition +revalidation, packaged adoption, real-model adherence and complete costs still +need their own evidence. Replan material-reading invocations retain their staged +reader and work restrictions. C1 remains unqualified. diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 55078f597d..ca3c3f5cad 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -1353,3 +1353,18 @@ C1/C2 组合/capture/最终输入工作与这些精简同属一个候选前沿 目录即索引。规划/源码检查记录不证明 runtime 交付;当前优先级与认领仍在 roadmap 和 canonical 工作中维护。历史记录保持 append-only,当前规范性计划在本文修订,不能把 某日审计当作永久执行队列。 + + +### 本地注册 Goal 材料交付检查点 + +完整 Turn writer 和现有 native context-delivery 显式启用路径,现在在 capture 前 +请求完整的注册 Goal 材料。源 adapter 为未标记或混合文件返回原始 UTF-8 全文; +现有 TypeScript context owner 核对完整文件 revision 后完成该项读取。已标记区域 +的 lineage 和 canonical Todo 权威保留。普通 quota 在 delivery 关闭时继续渐进读取。 +桥接要求 `complete_goal_v1` 回执;旧 runtime 不能静默忽略这个要求。 + +自动业务执行的 Host 上下文使用 `host_dispatch_context`,在加载 provider 前拒绝未完成或无效的 +Goal 材料。历史 `host_context` 元数据读取继续用于按原版本解释与必要结果收尾; +已保存的 capture 不被重写。本项是范围有限的材料交付前置。通用 capability 必读 +强制、当前条件复核、安装采用、真实模型遵守和完整成本,仍各需独立证据。C1 未获 +完整资格。Replan 材料读取调用仍按原有阶段与工作限制执行。 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index adde9d0631..d24c265887 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -57,7 +57,8 @@ narrative is never classified by headings or clipped to a size budget. Read all delivered context and remaining reads before dependent work; changed sources need a fresh guard. Enabled scoped acceptance does not replace original Goal intent. -Explicitly enabled native context delivery requests complete Goal material before +The complete Turn writer and explicitly enabled native context delivery request +complete Goal material before capture. For an unmarked or mixed Goal, the registered-source adapter supplies the entire original UTF-8 text with its full-file revision and `full_source: true`. The existing TypeScript interaction owner checks that digest before fulfilling @@ -72,6 +73,16 @@ context-loss recovery retain the complete original text, including arbitrary sections, comments and final stops. This does not establish model retention, execution authority, token savings or accepted outcome gains. +The internal `complete_goal_v1` acknowledgement prevents an older runtime from +silently ignoring this requirement. Automatic complete-contract business-work Host dispatch +uses `host_dispatch_context`: pending or invalid Goal material rejects before +loading the Host. The `host_context` metadata read remains available for +historical interpretation and result finalization. Saved contracts are not +rewritten; a historical pending Goal read cannot become fulfilled merely by +restoring its capture. This change neither grants execution permission nor +establishes enforcement for other pending capability reads. Replan material-reading +invocations retain their existing staged reader and work restrictions. + The Goal availability adapter still reads and UTF-8-validates the complete file and hashes all bytes for its revision. With native context delivery off, only the path and revision of unmarked or mixed sources cross the internal context-projection bridge, preserving the complete progressive read diff --git a/loopx/cli_commands/turn_decision.py b/loopx/cli_commands/turn_decision.py index 08c762a063..9169420968 100644 --- a/loopx/cli_commands/turn_decision.py +++ b/loopx/cli_commands/turn_decision.py @@ -170,6 +170,7 @@ def build_turn_decision( ) -> dict[str, Any]: decision = build_live_quota_should_run_decision( status_payload, + inline_goal_source=True, goal_id=args.goal_id, agent_id=args.agent_id, available_capabilities=args.available_capabilities, diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index ab5867578d..6f0e854581 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -22,7 +22,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { projectTurnStartUnavailableContext } from "../capability_hooks.ts"; import { jsonObject, requireBoolean, requireJsonObject, requireNonEmptyString, requireStringLiteral } from "../runtime_decode.ts"; import { projectPendingCapabilityIntent } from "../work_items/pending_capability_intent.ts"; -import { capturedTodoTextSha256, decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodo, selectedWorkTodoId, scopeInteractionOutcomeReadGuidance, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; +import { assertGoalContextDelivered, capturedTodoTextSha256, decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodo, selectedWorkTodoId, scopeInteractionOutcomeReadGuidance, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; import { measureTurnEnvelope, TURN_ENVELOPE_SECTION_TARGETS } from "./turn_envelope_budget.ts"; export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; @@ -2077,8 +2077,11 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { agent_id: scalarString(object(decision.agent_identity).agent_id, "Turn Agent identity").trim(), todo_id: (selectedWorkTodoId(decision) ?? "").trim()}; } - if (operation === "host_context") { + if (operation === "host_context" || operation === "host_dispatch_context") { const {decision, projection} = completeHostSource(request); + if (operation === "host_dispatch_context" && Object.keys(object(projection.replan_action_packet)).length === 0) { + assertGoalContextDelivered(object(object(decision.interaction_contract).agent_channel)); + } // The caller already holds this exact checked source. Return only the // resolved facts it needs, not another full copy over the transport. return {primary_action: object(projection.action).primary_action ?? null, diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index e695b60d81..04be995533 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -127,7 +127,7 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: """Use the complete source; decode the old layout for historical input only.""" try: - checked = _complete_host_source(request, operation="host_context") + checked = _complete_host_source(request, operation="host_dispatch_context") except RuntimeError as exc: raise ValueError("complete Turn contract rejected") from exc if checked is not None: diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index cf0cab3770..4d3206f060 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -104,6 +104,27 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): compose_turn_decision({**source, "schema_version": "unknown"}) +def test_pending_goal_capture_remains_readable_but_cannot_dispatch_business_work(): + from loopx.control_plane.quota.turn_envelope import build_turn_controller_input + from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan + from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, execution_profile_present + from tests.test_turn_envelope import _full_decision + + source = _full_decision() + source["interaction_contract"]["agent_channel"]["required_reads"] = [ + {"source": "goal_state", "command": "cat -- original-goal.md", "ordering": "before_work"}] + contract = compose_turn_decision(source) + before = copy.deepcopy(contract) + plan = build_loopx_turn_plan(build_turn_controller_input(contract).envelope(), work_contract=contract, + host="dsh", execution_mode="isolated-headless", turn_instance_id="original-turn") + request = build_loopx_turn_host_request(plan) + assert isinstance(execution_profile_present(request), bool) + with pytest.raises(ValueError, match="complete Turn contract rejected") as rejected: + extract_turn_authority(request) + assert "Goal source" in str(rejected.value.__cause__) + assert contract == before + + @pytest.mark.parametrize("action", ["future_execution", "", None, 0]) def test_complete_source_refuses_unknown_required_action_before_capture(action): from tests.control_plane.test_replan_host_context_projection import _quota_payload diff --git a/tests/control_plane/test_turn_host_context_delivery.py b/tests/control_plane/test_turn_host_context_delivery.py index 772e44375a..61738e0101 100644 --- a/tests/control_plane/test_turn_host_context_delivery.py +++ b/tests/control_plane/test_turn_host_context_delivery.py @@ -3,6 +3,7 @@ from copy import deepcopy import json +from pathlib import Path import shlex import subprocess import sys @@ -14,6 +15,7 @@ from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import _canonical_hash +from loopx.control_plane.turn_driver import codex_context from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_agent_context import POLICY from tests.control_plane.test_quota_settlement_cli import ( @@ -41,15 +43,28 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk _initialize_fixture_authority(project, runtime, provider=provider) capture = tmp_path / "capture" - code, envelope = _run_cli(registry, runtime, "quota", "should-run", + if mode != "legacy_reference": + native_directory = tmp_path / "context" + native_directory.mkdir(mode=0o700) + codex_context.codex_context_hook(str(native_directory), { + "session_id": "host-context-fixture", "cwd": str(project), + "hook_event_name": "SessionStart", "source": "startup", + }) + delivery = (["--context-delivery", "on", "--codex-context-dir", str(tmp_path / "context")] + if mode != "legacy_reference" else []) + binary = Path(sys.executable).with_name("loopx.exe" if sys.platform == "win32" else "loopx") + guard = [str(binary), "--registry", str(registry), + "--runtime-root", str(runtime), "--format", "json", "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "host-context-turn", "--codex-app", "--available-capability", "filesystem_read", "--available-capability", "subagent_spawn", - "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) - assert code == 0, envelope + "--turn-envelope", "--decision-output-dir", str(capture), *delivery] + produced = subprocess.run(guard, cwd=project, text=True, capture_output=True, timeout=30) + assert produced.returncode == 0, produced.stdout + produced.stderr original = (capture / "decision.json").read_bytes() source = json.loads(original) - assert envelope == source + if mode == "legacy_reference": + assert json.loads(produced.stdout) == source assert source["schema_version"] == "loopx_turn_envelope_v1" envelope = build_turn_envelope(source, captured_decision_path=str(capture / "decision.json")) context = source["interaction_contract"].get("agent_context") @@ -126,9 +141,12 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr assert prompt_path.read_text() == prompt # Refusal precedes the provider seam. - assert source["interaction_contract"]["agent_channel"]["required_reads"] + channel = source["interaction_contract"]["agent_channel"] + assert not any(read.get("source") == "goal_state" for read in channel["required_reads"]) + assert any(row.get("source") == "goal_state" + for row in channel["work_context"]["sources"]) invalid = deepcopy(request) - invalid["work_contract"]["interaction_contract"]["agent_channel"]["required_reads"] = [] + invalid["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] = [] rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr assert prompt_path.read_text() == prompt From 99b2f184609930f320f9db79777259e75a50607e Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:47:47 +0800 Subject: [PATCH 21/37] fix(context): enforce Goal readiness in Codex prompt preparation Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> (cherry picked from commit 1b0f14e181f0dc432c2071c637dd91398cf2b426) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 62 +++++++++---------- loopx/control_plane/turn_driver/codex_cli.py | 2 +- .../test_complete_turn_decision.py | 3 + .../test_turn_host_context_delivery.py | 5 ++ 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index d24c265887..307827930b 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -58,20 +58,12 @@ delivered context and remaining reads before dependent work; changed sources nee a fresh guard. Enabled scoped acceptance does not replace original Goal intent. The complete Turn writer and explicitly enabled native context delivery request -complete Goal material before -capture. For an unmarked or mixed Goal, the registered-source adapter supplies -the entire original UTF-8 text with its full-file revision and `full_source: true`. -The existing TypeScript interaction owner checks that digest before fulfilling -the read; `complete_goal_v1` acknowledges support so an older runtime cannot -silently retain a pending Goal read. Missing or invalid sources still reject. -Marked sources retain their generated-region lineage and canonical Todo authority. - -Native delta delivery and source fulfillment share the same integrity predicate. -Only exact verified Goal text can be inherited from the retained baseline; -current source witnesses and unknown fields remain literal. Cold delivery and -context-loss recovery retain the complete original text, including arbitrary -sections, comments and final stops. This does not establish model retention, -execution authority, token savings or accepted outcome gains. +complete Goal material before capture. For an unmarked or mixed Goal, the +registered-source adapter supplies the entire original UTF-8 text with its +full-file revision and `full_source: true`. The existing TypeScript context owner +checks that revision before fulfilling the Goal read. Marked sources retain +their existing excluded-region lineage and canonical Todo authority. Ordinary +quota with context delivery off retains the progressive behavior above. The internal `complete_goal_v1` acknowledgement prevents an older runtime from silently ignoring this requirement. Automatic complete-contract business-work Host dispatch @@ -84,9 +76,10 @@ establishes enforcement for other pending capability reads. Replan material-read invocations retain their existing staged reader and work restrictions. The Goal availability adapter still reads and UTF-8-validates the complete file -and hashes all bytes for its revision. With native context delivery off, only the path and revision of unmarked or mixed sources cross the -internal context-projection bridge, preserving the complete progressive read -without an unused body copy. Explicitly owned sources send the complete +and hashes all bytes for its revision. In ordinary progressive delivery with +context delivery off, unmarked or mixed sources send only their path and revision +across the internal context-projection bridge, without an unused body copy. +Explicitly owned sources send the complete non-generated narrative and region lineage to the typed consumer. Missing files and invalid UTF-8 still block dependent delivery. Byte reduction alone does not establish model-token, latency or outcome gains. @@ -150,6 +143,17 @@ source for unknown relevance or Goal-wide acceptance. Unknown optional fields, unsupported guidance or a pending acceptance read keep full delivery. The canonical decision and ordinary inspection still retain all requirements. +Warm native delivery may reuse the wrapper's exact reading instruction and +outcome titles. Current source references, scope, owners, revisions, requirement +states and unknown controls remain literal on every input, including reordered +indexes. Changed prose is delivered exactly; cold delivery and context-loss +recovery retain the complete current presentation. + +The complete-file Goal source uses the same integrity predicate for dispatch +and native reuse: exact text digest, explicit full-source marker, no conflicting +exclusions and no incomplete-read flag. Its text may be inherited from the +exact baseline; current source witnesses and unknown fields remain inline. + Native full/delta baselines and compaction recovery bind the exact required presentation, including its original-source digest. A newly required outcome must deliver its body before reuse; machine storage of optional text is not @@ -504,20 +508,8 @@ Todo 区域相交的区间均拒绝。 普通注释、代码块中的示例、损坏或未知回执、其他 Goal 的回执和附带其他文字的行均原样 保留。这样首轮全文读取不再携带累积的恢复日志;不改变回执重放校验,也不据此宣称 模型输入或 token 成本已经降低。 -显式开启的 native context delivery 在捕获前请求完整 Goal。未标记或混合归属的来源 -由原 registered-source adapter 提供全部原始 UTF-8 正文、完整修订与 `full_source: true`。 -既有 TypeScript interaction owner 核对正文 digest 后才履行读取;`complete_goal_v1` -确认 runtime 支持,防止旧版静默保留未完成 Goal 读取。来源缺失或无效仍拒绝;已有标记 -来源保留生成区域血缘和 canonical Todo 权限。 - -native 差分与来源履行共享同一完整性判断;只有精确核验的 Goal 正文可从保留的 -baseline 继承,当前来源凭据和未知字段仍明文提供。冷态及上下文丢失恢复保留完整 -原文,包括任意章节、注释和末尾停止条件。不据此证明模型保留、执行权限、token 节省 -或已验收成果收益。 - -Goal adapter 仍完整读取文件、校验 UTF-8,并以全部字节计算修订摘要;关闭 native -context delivery 时,未标记或混合归属仅在内部桥接传路径与修订,避免未被消费的正文 -副本,Agent 仍须渐进式全文读取。显式 +Goal adapter 仍完整读取文件、校验 UTF-8,并以全部字节计算修订摘要;未标记或混合归属 +仅在内部桥接传路径与修订,避免未被消费的正文副本,Agent 仍须渐进式全文读取。显式 归属的来源才向 typed owner 传完整非生成正文和区域血缘。字节减少不能证明模型费用、 延迟或任务效果改善。 已附带的内容不要求重复读取;未提供的 provider 内容继续保留精确命令与顺序。 @@ -542,6 +534,14 @@ interaction owner 仅调整自己的已履行成果读取指导。依赖可选 相关性未知或 Goal 整体验收时读原完整来源。未知可选字段、不支持的指导或尚未完成的 acceptance read 均保留全量交付;canonical decision 和普通 inspect 不删减要求。 +温态 native 交付可复用 wrapper 中完全相同的阅读说明和成果标题。当前来源引用、 +范围、owner、修订、要求状态及未知控制字段每轮仍明文提供,包括索引重排;变化后的 +正文精确交付。冷态及上下文丢失恢复仍保留当前完整展示。 + +完整文件 Goal 来源的派发和 native 复用共享同一完整性判断:正文 digest 精确匹配、 +明确标记 full source、无矛盾的排除区域且读取完整。正文可继承精确 baseline;当前 +来源凭据与未知字段每轮仍明文提供。 + native 全量/差分 baseline 与压缩恢复绑定真正交付的必读展示及其原来源 digest。 新增必读成果须先交付正文,机器保存可选文字不代表模型已读。 可选 scope 组合前先发布当前完整来源的恢复引用,编码前再发布精确必读产物,避免 diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index d8d114b47d..c6cd7ffb24 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -324,7 +324,7 @@ def _checked_prompt_context(request: Mapping[str, Any]) -> dict[str, Any] | None # The complete owner checks the adapter version, source and lineage in one # call. Keep the standalone version gate only for historical requests. try: - checked = _complete_host_source(request, operation="host_context") + checked = _complete_host_source(request, operation="host_dispatch_context") except RuntimeError: # Preserve the historical diagnostic precedence for doubly malformed # input without imposing a second bridge call on valid requests. diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 4d3206f060..7c399f68f5 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -106,6 +106,7 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): def test_pending_goal_capture_remains_readable_but_cannot_dispatch_business_work(): from loopx.control_plane.quota.turn_envelope import build_turn_controller_input + from loopx.control_plane.turn_driver.codex_cli import prepare_codex_cli_input from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, execution_profile_present from tests.test_turn_envelope import _full_decision @@ -119,6 +120,8 @@ def test_pending_goal_capture_remains_readable_but_cannot_dispatch_business_work host="dsh", execution_mode="isolated-headless", turn_instance_id="original-turn") request = build_loopx_turn_host_request(plan) assert isinstance(execution_profile_present(request), bool) + with pytest.raises(RuntimeError, match="Goal source"): + prepare_codex_cli_input(request) with pytest.raises(ValueError, match="complete Turn contract rejected") as rejected: extract_turn_authority(request) assert "Goal source" in str(rejected.value.__cause__) diff --git a/tests/control_plane/test_turn_host_context_delivery.py b/tests/control_plane/test_turn_host_context_delivery.py index 61738e0101..3ff470579e 100644 --- a/tests/control_plane/test_turn_host_context_delivery.py +++ b/tests/control_plane/test_turn_host_context_delivery.py @@ -156,6 +156,8 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monkeypatch, provider): + from loopx.control_plane.turn_driver.codex_cli import prepare_codex_cli_input + isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) _configure_selected_todo_replan_fixture(project, registry) @@ -205,6 +207,9 @@ def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monke command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(project), "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] request = build_loopx_turn_host_request({**plan, "work_contract": source}) + codex_prompt, _ = prepare_codex_cli_input(request) + assert "Read detail_ref.replan_action_packet.review" in codex_prompt + assert "before reading detail_ref.replan_action_packet.writeback" in codex_prompt before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr From 2bdf24243bd6b0664b1126bcad0e13cdc575ded5 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:33:32 +0800 Subject: [PATCH 22/37] fix(turn): retain lane fencing around staged capture execution Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/executor.py | 28 --------------------- 1 file changed, 28 deletions(-) diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index 5c1264e93d..3094679648 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -1434,34 +1434,6 @@ def persist_tail_checkpoint(*, release: bool) -> None: @single_executor_per_turn_lane(execution_payload) -def _prepare_host_decision_capture( - plan: Mapping[str, Any], - request: Mapping[str, Any], - *, - journal_path: Path, - decision: Mapping[str, Any] | None, - journal: Mapping[str, Any] | None, -) -> tuple[dict[str, Any], dict[str, Any]]: - """Restore the original captured input without replacing session recovery. - - Capture validation and binding stay with their existing owner. This IO - adapter only installs that owner's original context into the Host request. - """ - from .decision_capture import prepare_turn_decision_capture - - original_plan = journal["plan"] if journal is not None else plan - captured = prepare_turn_decision_capture( - original_plan, journal_path=journal_path, - decision=decision, existing_journal=journal is not None, - ) - restored = {**request, "turn_envelope": captured["turn_envelope"]} - if isinstance(captured.get("work_contract"), Mapping): - restored["work_contract"] = dict(captured["work_contract"]) - else: - restored.pop("work_contract", None) - return captured, restored - - def run_loopx_turn_once( plan: Mapping[str, Any], *, From 0e2a01b906993a149a79448afb5468035727ac51 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:42:19 +0800 Subject: [PATCH 23/37] test(turn): qualify staged capture and inherited Goal readiness Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 2 +- ...ller-executor-unified-contract-v0.zh-CN.md | 2 +- .../2026-10-28-packaged-heartbeat.md | 27 +++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 19 +++++++++++++ .../rfcs/loopx-overall-roadmap-v0.md | 2 +- .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 2 +- .../test_turn_host_decision_capture.py | 8 +++--- 7 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 41f21652ef..f56b849b32 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1615,7 +1615,7 @@ complete-cost diff, default performance or model benefit is qualified. Continue within C1 on warm reconstruction/metadata cost and remaining consumers; this does not promote the candidate by default or qualify the separate controller experiment. -The integration also adopts the existing [#7528](https://github.com/loopx-project/loopx/pull/7528) +The integration also adopts the merged [#7528](https://github.com/loopx-project/loopx/pull/7528) overlay in that same context codec. Relocated exact evidence can share one base subtree while current and unknown controls remain literal, including through the scoped C1 wrapper. The ledger separates the positive relocation-only byte result diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index ca3c3f5cad..896b99a8c0 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -1187,7 +1187,7 @@ capture 前验证失败。工作契约形成前的错误则在两种 CLI flag 继续在原 C1 诊断温态重建/元数据成本及剩余消费者,不提升默认,也不验收独立的 controller 实验。 -集成版本也在相同 context codec 内采用现有 +集成版本也在相同 context codec 内采用已合入的 [#7528](https://github.com/loopx-project/loopx/pull/7528) overlay:搬移的精确证据可共用 一份基线子树,当前及未知控制仍字面交付,包含 C1 scoped wrapper。Ledger 将局部 搬移的正向字节结果与重跑的打包对照分开:普通温态输入仍多约 2.8 KB,v4 预算仍失败。 diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index e9c140898f..ff76c88847 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -244,3 +244,30 @@ remaining C1 composition/metadata gap. Default SQLite performance, installed App and model retention, token/cache/billing, accepted outcomes and full long-horizon benefit remain unqualified. Continue the existing C1 on that gap and remaining consumers; this adoption does not close C1, C2 or the controller experiment. + +Subsequent integration is rebased onto main +`0cfbf11dcea4ba7bc8fb13f06f1c915b63ce8fad`, which contains #7528; its duplicate +commit drops out. Both Goal-source commits from [#7547](https://github.com/loopx-project/loopx/pull/7547) +retain their authorship and all 12 contributed product/test blobs match the +accepted integration head. Main's capture owner still stages before admission +and publishes afterward. The original complete contract is installed only after +publication. Removing an unused capture helper restores the process-level fence +around the actual Turn executor; the existing held-lane test fails before this +repair and passes afterward. + +Runtime `6f30cc454f2dea7e77f0821a14a1deaf268efe2e` has a source-matching +1,885-member wheel. The post-rebase typed selection passes 148 cases and +typecheck. The integration Python selection initially records 185 passes, two +explicit live-host skips and 12 failures: eight new staged-readback assertions +compare adapter metadata with the actual model projection, and four observers +still name the pre-#7547 metadata-only operation. Retaining their complete +comparisons against the real Host input and observing the stricter dispatch +operation makes all 54 Host-capture cases pass; no product gate is relaxed. +The earlier run and its other passed cases remain recorded separately. + +The packaged File/SQLite bootstrap, complete Goal delivery, all clause/detail +reads, same-capture recovery, rejection and once-only settlement pass again on +this wheel. This functional run overlaps other validation and is not a new +matched timing cohort. The older cost tables remain attached to runtime +`167b31e5d`, not relabeled as this later source. Current-version complete-cost +and outcome qualification remain open. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index a76dbc6a6b..53ed9390bf 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -187,3 +187,22 @@ native/SDK 跳过,以及长临时 namespace 下两项恢复文本尺寸失 组合/元数据的另一未解缺口。默认 SQLite 性能、正式 App/模型保留、token/cache/ 账单、验收成果和完整长程净收益均未资格。沿原 C1 继续该缺口及剩余消费者,不以采用 本修复结清 C1、C2 或 controller 实验。 + +随后集成 rebase 到 main `0cfbf11dcea4ba7bc8fb13f06f1c915b63ce8fad`,其中已含 #7528, +重复 commit 因而退出。[#7547](https://github.com/loopx-project/loopx/pull/7547) 两项 Goal +来源提交保留原作者,12 个贡献的产品/测试 blob 与已接收的集成 head 完全相同。 +主干 capture owner 仍在准入前 stage、准入后发布,发布后才安装原完整契约。删除未用 +capture helper 后,进程级 fence 重新包住实际 Turn executor;既有占用 lane 用例在 +修复前失败、修复后通过。 + +Runtime `6f30cc454f2dea7e77f0821a14a1deaf268efe2e` 的 wheel 含 1,885 个与来源一致的 +产品成员。重基后的 typed 集合 148 项及 typecheck 通过。集成 Python 首次记录 +185 通过、两项明确的 live-host 跳过及 12 失败:八项新增 staged 回读断言把 adapter +元数据与实际模型投影混比,四项 observer 仍记录 #7547 之前的 metadata-only 操作。 +保留对真实 Host 输入的完整比较,观测更严格的 dispatch 操作后,54 项 Host-capture +全部通过,没有放宽产品门;前次失败和其余已通过案例分别保留。 + +该 wheel 再通过打包 File/SQLite bootstrap、完整 Goal 交付、全部条款/详情读取、同 +capture 恢复、拒绝及一次结算。本次功能验证与其他检查并行,不是新的匹配计时 cohort; +旧成本表仍绑定 `167b31e5d`,不改称后续源码的测量。当前版本完整成本与成果资格保持 +开放。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 8808c5e02d..f8236e9408 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -213,7 +213,7 @@ Continue C1 on warm composition cost and remaining real consumers with full unknown-field/obligation preservation. This is no default promotion or positive complete-cost qualification; controller-model and outcome acceptance stay open. -The same integration codec now adopts [#7528](https://github.com/loopx-project/loopx/pull/7528) +The same integration codec now adopts merged [#7528](https://github.com/loopx-project/loopx/pull/7528) for exact relocated evidence with literal current overrides, including scoped context. The ledger records smaller relocation-only deltas alongside the still negative ordinary-warm byte comparison; this is shared-owner adoption, not a diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index c4a5375c99..2b38e5a472 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -163,7 +163,7 @@ owner 并删除 Python 重复算法;[#7557](https://github.com/loopx-project/l 失败仍保留。原 C1 继续诊断温态组合成本及剩余真实消费者,保留未知字段和完整义务; 这不是默认提升或完整成本正向资格,controller 模型与成果验收保持开放。 -同一集成 codec 现采用 [#7528](https://github.com/loopx-project/loopx/pull/7528),在精确 +同一集成 codec 现采用已合入的 [#7528](https://github.com/loopx-project/loopx/pull/7528),在精确 搬移证据上叠加字面当前控制,覆盖 scoped context。Ledger 同时记录局部搬移差分变小 和普通温态字节对照仍负向;这是采用共享 owner,没有新增第二编码器或提升默认。 diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index aafb32b363..47b1a6ce52 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -242,7 +242,7 @@ def unexpected_effect(*_args, **_kwargs): staged.publish() assert path.read_bytes() == changed path.write_text(json.dumps(dict(reversed(source_a.items())))) - assert staged.publish()["turn_envelope"]["detail_ref"] == detail + assert complete_host_input(staged.publish())["detail_ref"] == detail path.write_bytes(original) path.write_text(json.dumps({**source_a, "reason": "changed during admission"})) changed = path.read_bytes() @@ -254,7 +254,7 @@ def unexpected_effect(*_args, **_kwargs): staged.publish() assert not path.exists() path.write_bytes(original) - assert staged.publish()["turn_envelope"]["detail_ref"] == detail + assert complete_host_input(staged.publish())["detail_ref"] == detail assert path.read_bytes() == original # Corrupt/missing evidence cannot be replaced by the caller's new guard. path.write_text(json.dumps({**source_a, "reason": "wrong original"})) @@ -655,7 +655,7 @@ def test_complete_codex_launch_shares_input_check_only_within_attempt( original = turn_envelope.effect_runtime_result def observe(method, arguments, **kwargs): - if arguments.get("operation") in {"host_context", "validate_complete"}: + if arguments.get("operation") in {"host_context", "host_dispatch_context", "validate_complete"}: observed.append(arguments["operation"]) return original(method, arguments, **kwargs) @@ -678,7 +678,7 @@ def observe(method, arguments, **kwargs): assert json.loads(schema_file.read_text()) == expected_schema # A fresh attempt checks again; preparing its prompt and schema shares # the one check. Result validation and outer settlement remain separate. - assert observed == ["host_context"] + assert observed == ["host_dispatch_context"] changed = deepcopy(request) changed["work_contract"]["reason"] = "source changed after a successful launch" before = prompt_file.read_bytes(), schema_file.read_bytes() From e902d23871e397f9f1a12aa6e0e32626da84c352 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:53:59 +0800 Subject: [PATCH 24/37] refactor(turn): bind published input in the capture owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/decision_capture.py | 12 ++++++++++++ loopx/control_plane/turn_driver/executor.py | 7 +------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 9d23ee22fe..16a41fba07 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -60,6 +60,18 @@ def publish(self) -> dict[str, Any]: write_decision_capture(self.path, self.observation) return self.plan + def publish_for_host( + self, request: Mapping[str, Any], + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Install the published original input without changing recovery bindings.""" + plan = self.publish() + bound_request = {**request, "turn_envelope": plan["turn_envelope"]} + if isinstance(plan.get("work_contract"), Mapping): + bound_request["work_contract"] = dict(plan["work_contract"]) + else: + bound_request.pop("work_contract", None) + return plan, bound_request + def stage_turn_decision_capture( plan: Mapping[str, Any], *, journal_path: Path, diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index 3094679648..ccca6aed1d 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -1647,12 +1647,7 @@ def persist_source_journal(source_admission: Mapping[str, Any]) -> None: plan, waiting, execute=True, replayed=False, effects=empty_effects ) if capture is not None: - plan = capture.publish() - request = {**request, "turn_envelope": plan["turn_envelope"]} - if isinstance(plan.get("work_contract"), Mapping): - request["work_contract"] = dict(plan["work_contract"]) - else: - request.pop("work_contract", None) + plan, request = capture.publish_for_host(request) if journal is not None and recovery_decision is not None: journal["recovery_audit"] = build_turn_recovery_audit( recovery_decision, From 614167f361fa0cea1db5674503bb5f166059d29a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:54:41 +0800 Subject: [PATCH 25/37] docs(rfc): retain capture-owner repair qualification Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../2026-10-28-packaged-heartbeat.md | 9 +++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index ff76c88847..d7d31a5fff 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -271,3 +271,12 @@ this wheel. This functional run overlaps other validation and is not a new matched timing cohort. The older cost tables remain attached to runtime `167b31e5d`, not relabeled as this later source. Current-version complete-cost and outcome qualification remain open. + +The committed premerge then exposes a maintainability failure: the executor +contains 94 statements against the existing 90-statement limit. Runtime +`82f93ab76a403c2633efedb2f8bf02f85be8e3c6` moves published-input installation +into the existing staged-capture owner, preserving recovery bindings and +publication ordering. Its source-matching 1,885-member wheel, all 54 Host-capture +cases and the unchanged maintainability ratchet pass. The original failure is +retained; no exception or threshold is added. This owner consolidation does not +turn the historical warm-input or v4 budget failures into passes. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index 53ed9390bf..7092334d2d 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -206,3 +206,9 @@ Runtime `6f30cc454f2dea7e77f0821a14a1deaf268efe2e` 的 wheel 含 1,885 个与来 capture 恢复、拒绝及一次结算。本次功能验证与其他检查并行,不是新的匹配计时 cohort; 旧成本表仍绑定 `167b31e5d`,不改称后续源码的测量。当前版本完整成本与成果资格保持 开放。 + +随后已提交版本的 premerge 暴露维护性失败:executor 有 94 条语句,超过既有 90 条 +限制。Runtime `82f93ab76a403c2633efedb2f8bf02f85be8e3c6` 将已发布输入的安装收回 +现有 staged-capture owner,保留恢复绑定与发布顺序。其 1,885 个产品成员与来源一致, +54 项 Host-capture 及未改动的维护性 ratchet 均通过。原失败保留,没有新增例外或改 +阈值;本次 owner 收敛不把历史温态输入及 v4 预算失败改判通过。 From 78937d76d15f56055cef38e65cf45b8636035ea8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:28:03 +0800 Subject: [PATCH 26/37] fix(quota): preserve original facts across diagnostic projection (cherry picked from commit eb4e9b970aeb1ffb83e46705d6386cc3a4679ead) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 6f0e854581..847746a565 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -8,7 +8,7 @@ import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from import {prepareContextDelivery, restoreContextDelivery, OUTCOME_CONTEXT_PROJECTION_SCHEMA} from "./context_delivery.ts"; import {updateNativeContextLifecycle} from "./native_context_lifecycle.ts"; import {OUTCOME_CONTEXT_READ_PLAN_SCHEMA} from "../goals/outcome_requirements.ts"; -import {projectQuotaDiagnostics} from "./diagnostic_projection.ts"; +import {contextProjectionChanges, projectQuotaDiagnostics} from "./diagnostic_projection.ts"; import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; import { From 81b3d4a9f161f9b168772dbf4c35c28673429a4c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:00:08 +0800 Subject: [PATCH 27/37] fix(context): preserve source identity during acceptance sharing Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 43 +++++++++++++------ .../test_quota_decision_capture.py | 21 ++++++--- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 847746a565..f4cf1a6035 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -229,12 +229,12 @@ function canonicalHash(value: unknown): string { * The loaded work context is preferred, then the complete planning/replan facts. */ function acceptanceCarriers(payload: JsonObject) { - // Keep carrier parents attached to the cloned document. The general object() - // helper intentionally returns copies for projection callers. + // Keep read-only carrier parents attached to their source. The general + // object() helper intentionally returns copies for projection callers. const carrierObject = (value: unknown): JsonObject => typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonObject : {}; - const carriers: {parent: JsonObject; field: string; path: string}[] = []; + const carriers: {parent: JsonObject; field: string; path: string; parentPath: (string | number)[]}[] = []; const sources = carrierObject(carrierObject(carrierObject(payload.interaction_contract).agent_channel).work_context).sources; if (Array.isArray(sources)) { const index = sources.findIndex(value => { @@ -245,14 +245,18 @@ function acceptanceCarriers(payload: JsonObject) { }); if (index >= 0) carriers.push({parent: carrierObject(carrierObject(sources[index]).content), field: "goal_acceptance_contract", + parentPath: ["interaction_contract", "agent_channel", "work_context", "sources", index, "content"], path: `$.interaction_contract.agent_channel.work_context.sources[${index}].content.goal_acceptance_contract`}); } carriers.push( {parent: carrierObject(carrierObject(payload.planning_horizon).core_goal), field: "acceptance_contract", + parentPath: ["planning_horizon", "core_goal"], path: "$.planning_horizon.core_goal.acceptance_contract"}, {parent: carrierObject(carrierObject(carrierObject(payload.autonomous_replan_obligation).replan_context).core_goal), - field: "acceptance_contract", path: "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract"}, + field: "acceptance_contract", path: "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract", + parentPath: ["autonomous_replan_obligation", "replan_context", "core_goal"]}, {parent: carrierObject(payload.agent_todo_summary), field: "goal_acceptance_contract", + parentPath: ["agent_todo_summary"], path: "$.agent_todo_summary.goal_acceptance_contract"}, ); return carriers; @@ -282,11 +286,23 @@ function inlineAcceptance(payload: JsonObject, reference: unknown): JsonObject { } function referenceGoalAcceptance(payload: JsonObject): JsonObject { - const result = structuredClone(payload); + let result = payload; + // Copy only a changed registered carrier's spine. Retained source objects + // keep their identity for the display-edit transport and original numbers. + const writableParent = (path: (string | number)[]): JsonObject => { + result = {...result}; + let target: Record = result; + for (const key of path) { + const value = target[key]; + target[key] = Array.isArray(value) ? [...value] : {...requireJsonObject(value, "acceptance carrier")}; + target = target[key] as Record; + } + return target; + }; const owners = new Map(); const relocated = new Map(); - const carriers = acceptanceCarriers(result); - for (const {parent, field, path} of carriers) { + const carriers = acceptanceCarriers(payload); + for (const {parent, field, path, parentPath} of carriers) { const value = object(parent[field]); if (!Object.keys(value).length || Object.hasOwn(parent, `${field}_ref`)) continue; // Complete canonical JSON distinguishes booleans/numbers, revisions, @@ -295,17 +311,18 @@ function referenceGoalAcceptance(payload: JsonObject): JsonObject { const owner = owners.get(encoded); if (!owner) owners.set(encoded, path); else if (Buffer.byteLength(encoded, "utf8") > Buffer.byteLength(owner, "utf8") + 8) { - delete parent[field]; - parent[`${field}_ref`] = owner; + const target = writableParent(parentPath); + delete target[field]; + target[`${field}_ref`] = owner; relocated.set(path, owner); } } // A historical source can already reference a carrier that was just shared. // Rebind that exact reference to the retained inline owner, never a chain. - for (const {parent, field} of carriers) { + for (const {parent, field, parentPath} of carriers) { const reference = parent[`${field}_ref`]; if (typeof reference === "string" && relocated.has(reference)) { - parent[`${field}_ref`] = relocated.get(reference); + writableParent(parentPath)[`${field}_ref`] = relocated.get(reference); } } return result; @@ -325,7 +342,7 @@ function capturedAcceptanceOutcomes(payload: JsonObject): Map Date: Wed, 28 Oct 2026 20:01:16 +0800 Subject: [PATCH 28/37] docs(rfc): record diagnostic preservation and C1 integration evidence Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../2026-10-28-packaged-heartbeat.md | 28 +++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 21 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index d7d31a5fff..fed99c7e40 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -280,3 +280,31 @@ publication ordering. Its source-matching 1,885-member wheel, all 54 Host-captur cases and the unchanged maintainability ratchet pass. The original failure is retained; no exception or threshold is added. This owner consolidation does not turn the historical warm-input or v4 budget failures into passes. + + +The next integration rebases the retained C1 history onto main +`6f8f476388645fcea2269329d32974b559d490e6` and adopts the repaired diagnostic +slice [#7557](https://github.com/loopx-project/loopx/pull/7557) at +`eb4e9b970aeb1ffb83e46705d6386cc3a4679ead`. That slice originally transported +the whole Python input through JavaScript JSON: `1.0`, negative zero and integers +above the safe range could change outside the display fields. Nine independent +source-number cases fail before repair. The shared typed owner now returns only +display edits and immutable source references; Python applies them to its original +values without owning compaction policy. The standalone slice passes 305 Python +cases, 99 typed cases, typecheck, Ruff and its standard premerge. These are source +and real File/SQLite CLI checks, not installation or independent acceptance. + +C1 runtime `7f35454b392e09ae574c5065c473ee27b5eee853` uses the byte-identical +shared diagnostic module. Its acceptance-sharing stage formerly cloned every +source object, defeating references for relocated numeric facts. It now copies +only changed registered carrier paths. The first integration selection retains +253 passes and five failures: three expose that clone and two old boundary +fixtures fail the complete-Goal dispatch requirement, also on the previous C1 +head. The boundary fixtures now keep work in its canonical provider and deliver +complete non-Todo Goal material; they check both the current original-contract +Host input and historical scope aliases, including rejection, without relaxing +dispatch. The final affected selection passes all 258 Python cases; the 110-case +typed selection, typecheck and Ruff pass. The older wheel and matched timing +cohorts remain attached to their actual versions. This source follow-through +does not qualify a new packaged cohort, cure the warm-input or v4 budget failures, +or close remaining consumers, reader retirement or actual model/outcome costs. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index 7092334d2d..a888dcc3b3 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -212,3 +212,24 @@ capture 恢复、拒绝及一次结算。本次功能验证与其他检查并行 现有 staged-capture owner,保留恢复绑定与发布顺序。其 1,885 个产品成员与来源一致, 54 项 Host-capture 及未改动的维护性 ratchet 均通过。原失败保留,没有新增例外或改 阈值;本次 owner 收敛不把历史温态输入及 v4 预算失败改判通过。 + + +下一次集成将保留的 C1 历史 rebase 到 main +`6f8f476388645fcea2269329d32974b559d490e6`,采用 +[#7557](https://github.com/loopx-project/loopx/pull/7557) 的修复版本 +`eb4e9b970aeb1ffb83e46705d6386cc3a4679ead`。该独立切片原先把整个 Python 输入经 +JavaScript JSON 往返,展示字段之外的 `1.0`、负零及安全整数范围外的值可能改变。 +九项独立的原数值案例在修复前失败。现在共享 typed owner 仅返回展示变更及不可变 +来源引用,Python 在原值上应用,不拥有裁剪规则。独立切片通过 305 项 Python、99 项 +typed、typecheck、Ruff 及其标准 premerge;这些是源码及真实 File/SQLite CLI 检查, +不是安装或独立验收。 + +C1 runtime `7f35454b392e09ae574c5065c473ee27b5eee853` 使用逐字节相同的共享诊断 +模块。其验收共享阶段原先克隆所有来源对象,导致搬移数值事实无法沿用来源引用;现在 +只复制发生变化的已注册 carrier 路径。首次集成保留 253 通过、五项失败:三项暴露 +上述克隆问题,两项旧边界夹具不满足完整 Goal 的 dispatch 要求,后两项在前一 C1 head +也失败。夹具现在将任务保留于规范 provider,完整交付非 Todo 的 Goal 内容,同时 +验证当前原始契约 Host 输入与历史作用域别名及拒绝,没有放宽 dispatch。最终受影响 +集合 258 项 Python 全过;110 项 typed、typecheck 和 Ruff 通过。旧 wheel 与匹配计时 +cohort 仍绑定各自实际版本。本次源码接续不认证新的打包 cohort,不消除温态输入或 v4 +预算失败,也不结清剩余消费者、reader 退役及实际模型/成果的完整成本。 From 094c657355edfbba8c7fe5ccf3a5a6cbcc3a8f0c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:05:53 +0800 Subject: [PATCH 29/37] test(turn): check native prompt bytes after main material repair Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_turn_host_decision_capture.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index 47b1a6ce52..079ff9c2c6 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -671,10 +671,9 @@ def observe(method, arguments, **kwargs): observed.clear() result = run(request, **options) assert result["turn_key"] == request["turn_key"] - # The existing app-server client folds prompt whitespace before send; - # CLI stdin retains the original bytes. Neither adapter changes here. - assert prompt_file.read_text().startswith( - " ".join(expected_prompt.split()) if operation_tools else expected_prompt) + # Both transports preserve the original complete input. Native operation + # guidance may follow it, but must not fold its whitespace or JSON prose. + assert prompt_file.read_text().startswith(expected_prompt) assert json.loads(schema_file.read_text()) == expected_schema # A fresh attempt checks again; preparing its prompt and schema shares # the one check. Result validation and outer settlement remain separate. From 19042b8f317e5ab47655a37c0215c16752573a1b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:08:51 +0800 Subject: [PATCH 30/37] docs(rfc): retain main transport integration and merge boundaries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../2026-10-28-packaged-heartbeat.md | 17 +++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index fed99c7e40..9c4de238e2 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -308,3 +308,20 @@ typed selection, typecheck and Ruff pass. The older wheel and matched timing cohorts remain attached to their actual versions. This source follow-through does not qualify a new packaged cohort, cure the warm-input or v4 budget failures, or close remaining consumers, reader retirement or actual model/outcome costs. + + +The additional complete-Host selection retains 200 passes and two failures +against the later main integration: its operation-host oracle still expected +whitespace folding, removed by main's [#7549](https://github.com/loopx-project/loopx/pull/7549). +The actual `chat_agent.py` blob equals the verified main base. Checking original +prompt bytes on both transports, while retaining schema, retry and tamper +assertions, makes all 54 Host-capture cases pass. This changes the test oracle, +not the production transport. The initial failure remains recorded. + +GitHub subsequently reports #7557 merged as +`4b2b97b7b726d902a20969e3911fd15d15b4bb2e`; all seven contributed files match +its repaired head. Merge occurrence does not supply a missing new-head +independent review or protected-action receipt. The C1 qualification above +remains bound to its frozen main base; later main changes are not implicitly +qualified. C1 stays an unfinished author integration with its existing cost, +consumer and retirement gaps. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index a888dcc3b3..8efadc3773 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -233,3 +233,15 @@ C1 runtime `7f35454b392e09ae574c5065c473ee27b5eee853` 使用逐字节相同的 集合 258 项 Python 全过;110 项 typed、typecheck 和 Ruff 通过。旧 wheel 与匹配计时 cohort 仍绑定各自实际版本。本次源码接续不认证新的打包 cohort,不消除温态输入或 v4 预算失败,也不结清剩余消费者、reader 退役及实际模型/成果的完整成本。 + + +额外的完整 Host 集合在后续 main 集成上保留 200 通过、两项失败:operation-host +oracle 仍预期折叠空白,而主干 [#7549](https://github.com/loopx-project/loopx/pull/7549) +已经去掉该行为。实际 `chat_agent.py` blob 与已核 main 基线完全相同。两类 transport +都检查原 prompt 字节,并保留 schema、重试和篡改断言后,54 项 Host-capture 全过。 +此处修正测试 oracle,没有改变生产 transport,首次失败记录保留。 + +GitHub 随后报告 #7557 已合并为 `4b2b97b7b726d902a20969e3911fd15d15b4bb2e`,七个 +贡献文件均与修复 head 相同。发生合并不能补足尚缺的新 head 独立评审或受保护操作 +回执。上述 C1 资格仍绑定冻结的 main 基线,不隐含认证后续 main 变化;C1 仍是尚未 +完成的作者集成,原有成本、消费者及退役缺口保持。 From 2ca00cbdd710762ba146c53f772e3c9ae9ed69da Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:36:42 +0800 Subject: [PATCH 31/37] fix(turn): keep one shared diagnostic import after integration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index f4cf1a6035..c31956144a 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,4 +1,3 @@ -import {contextProjectionChanges, projectQuotaDiagnostics} from "./diagnostic_projection.ts"; import type { LoopXTurnRouteValue } from "../turn_driver/turn_contract_generated.ts"; import { EFFECTIVE_ACTIONS, EffectiveAction, type QuotaEffectiveActionValue } from "./effective_action.generated.ts"; import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../agents/agent_scope_frontier.generated.ts"; From 396635e73cffb521d3c2f8daf64af38cc483179b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:07:37 +0800 Subject: [PATCH 32/37] fix(context): reuse verified Goal sources and scoped reading prose Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/context_delivery.ts | 17 +++++- .../work_items/interaction_contract.ts | 1 + .../test_work_acceptance_context.py | 7 +++ .../control_plane_ts/context_delivery.test.ts | 55 ++++++++++++++++++- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 06145786d2..a037804907 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -7,6 +7,7 @@ import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_d import {canonicalAuthorityJson, canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; import {reusableTurnContext} from "../capability_hooks.ts"; import {COORDINATION_STATE_CONTRACT} from "../coordination/coordination_state_contract.generated.ts"; +import {completeGoalNarrative} from "../work_items/goal_context_source.ts"; const VERSION = "context_delivery_v1"; export const OUTCOME_CONTEXT_PROJECTION_SCHEMA = "outcome_context_projection_v0"; @@ -196,8 +197,22 @@ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: re const inner = jsonObject(context.context); if (!inner) return {paths: [[]], valid: false}; const projected = freshPaths(inner, surface, legacyQuotaFields, binding); + const wrapper: Path[] = []; + for (const [key, value] of Object.entries(context)) { + if (key === "context" || key === "instruction" && typeof value === "string") continue; + // Reuse only exact explanatory prose. Scope, identities, current + // verdicts and unknown controls remain literal, including after reorder. + if (key === "outcome_index" && Array.isArray(value) && value.length) { + value.forEach((item, i) => { + const row = jsonObject(item); + if (!row) { wrapper.push([key, i]); return; } + for (const [field, detail] of Object.entries(row)) + if (field !== "objective" || typeof detail !== "string") wrapper.push([key, i, field]); + }); + } else wrapper.push([key]); + } return {valid: projected.valid, paths: [ - ...Object.keys(context).filter(key => key !== "context").map(key => [key]), + ...wrapper, ...projected.paths.map(path => ["context", ...path]), ]}; } diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index d51fbbd266..1ecc136fff 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -882,6 +882,7 @@ export function planInteractionWorkContext(request: JsonObject): JsonObject { && packet.effective_action !== EffectiveAction.HEARTBEAT_SETTLED_SKIP}; } + export function assertGoalContextDelivered(channel: JsonObject): void { const reads = Array.isArray(channel.required_reads) ? channel.required_reads : []; if (reads.some(read => jsonObject(read)?.source === "goal_state")) { diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index a0c14ade16..df561ceb04 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -372,6 +372,11 @@ def test_acceptance_observation_does_not_transport_the_large_selected_body(tmp_p def test_captured_acceptance_does_not_replace_current_work_admission(acceptance_goal, tmp_path, change): project, document, acceptance_cli, run = acceptance_goal goal, agent, selected = "goal-acceptance", "agent-a", "todo_export" + from canonical_authority_fixture import empty_todo_display + + # Dispatch needs the complete non-Todo Goal, while the canonical provider + # retains this work. The old unmarked display left a pending Goal read. + empty_todo_display(project / "state.md") def cli(*arguments): code, result = run(*arguments) @@ -408,6 +413,8 @@ def configure(*arguments): first = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json"), scheduler_execution_context=scheduler_execution_context_for_runtime_profile("codex_app_heartbeat")) + assert not any(row.get("source") == "goal_state" for row in full["interaction_contract"]["agent_channel"]["required_reads"]) + assert any(row.get("source") == "goal_state" for row in first["work_context"]["sources"]) basis = next(row["content"] for row in first["work_context"]["sources"] if row.get("source") == "goal_acceptance") assert first["action"]["delivery_allowed"] is True diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index 4ee158935f..674b87fea5 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -1,6 +1,6 @@ -import {createHash} from "node:crypto"; import assert from "node:assert/strict"; import test from "node:test"; +import {createHash} from "node:crypto"; import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import {compactTodoContextSource, OUTCOME_CONTEXT_PROJECTION_SCHEMA, prepareContextDelivery, restoreContextDelivery} from "../../loopx/control_plane/quota/context_delivery.ts"; @@ -186,6 +186,59 @@ function inline(delivery: JsonObject, path: (string | number)[]): unknown { } } +test("scoped outcome indexes reuse exact prose while current scope and verdicts remain literal", () => { + const guidance = "Read every required outcome and recover this same capture before work. ".repeat(100); + const objective = "An independent peer's complete export and recovery outcome. ".repeat(100); + const original = {schema_version: OUTCOME_CONTEXT_PROJECTION_SCHEMA, + source_capture: {path: "/capture/original.json", sha256: "a".repeat(64)}, + read_scope: {complete: true, scope_applied: true, required_outcome_refs: ["own"]}, + instruction: guidance, unknown_scope_control: {may_publish: false}, + outcome_index: ["one", "two"].map(id => ({id, outcome_ref: id, objective: objective + id, + owner_agent_id: "peer", revision: 1, work: [{todo_id: id, status: "open"}], + required_in_scope: false, requirement_count: 1, + requirement_states: [{requirement_ids: [id], status: "unverified", + agent_review: {conclusion: "uncertain", basis_current: false, independently_verified: false}}], + unknown_index_control: {stop: true}})), + context: {interaction_contract: {agent_channel: {work_context: context()}}}}; + const args = {scope, surface: "quota_packet", capture_id: "scoped-before", context: original}; + const baseline = prepareContextDelivery(args); + for (const reordered of [false, true]) { + const current = structuredClone(original); + current.source_capture = {path: "/capture/current.json", sha256: "b".repeat(64)}; + current.outcome_index[0].revision = 2; + current.outcome_index[0].requirement_states[0].status = "failed"; + current.outcome_index[0].requirement_states[0].agent_review.conclusion = "unmet"; + if (reordered) current.outcome_index.reverse(); + const request = {...args, context: current, capture_id: "scoped-after"}; + const fullCurrent = prepareContextDelivery(request); + const delta = prepareContextDelivery({...request, baseline, + retained: {complete: true, target: baseline.target}}); + assert.equal(delta.mode, "delta"); + const wire = JSON.stringify(delta.changes); + assert.ok(!wire.includes(guidance), "Unchanged reading guidance must use the exact retained baseline"); + assert.ok(!wire.includes(objective), "Index prose must not be resent with current verdicts"); + for (const key of ["schema_version", "source_capture", "read_scope", "unknown_scope_control"]) + assert.deepEqual(inline(delta, [key]), current[key as keyof typeof current]); + current.outcome_index.forEach((row, i) => { + for (const [key, value] of Object.entries(row)) if (key !== "objective") + assert.deepEqual(inline(delta, ["outcome_index", i, key]), value, key); + }); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline, current: fullCurrent}).context, current); + assert.deepEqual(restoreContextDelivery({delivery: delta, current: fullCurrent}).context, current); + assert.deepEqual(fullCurrent.context, current); // Cold and lost-context delivery remain complete. + } + const corrected = structuredClone(original); + corrected.instruction = "A new owner correction requires the complete current source."; + corrected.outcome_index[0].objective = "A changed outcome must reach the current input."; + const request = {...args, context: corrected, capture_id: "corrected"}; + const delta = prepareContextDelivery({...request, baseline, + retained: {complete: true, target: baseline.target}}); + assert.equal(inline(delta, ["instruction"]), corrected.instruction); + assert.equal(inline(delta, ["outcome_index", 0, "objective"]), corrected.outcome_index[0].objective); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline, current: prepareContextDelivery(request)}).context, corrected); + assert.deepEqual(baseline.context, original); +}); + test("complete original Goal text reuses the same integrity witness as Host dispatch", () => { const goal = {state_file: "goal.md", text: long, full_source: true, revision: `sha256:${createHash("sha256").update(long, "utf8").digest("hex")}`}; From 36e7ac5166d1248300093e5ef5763747126204d9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:16:01 +0800 Subject: [PATCH 33/37] docs(controller): record complete input costs and native delivery slice Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../2026-10-28-packaged-heartbeat.md | 65 +++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 49 ++++++++++++++ .../rfcs/loopx-overall-roadmap-v0.md | 18 +++-- .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 11 ++-- 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index 9c4de238e2..6d10aa110d 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -325,3 +325,68 @@ independent review or protected-action receipt. The C1 qualification above remains bound to its frozen main base; later main changes are not implicitly qualified. C1 stays an unfinished author integration with its existing cost, consumer and retirement gaps. + +## Complete Goal reuse and independently usable native slice + +C1 is subsequently rebased onto main +`f5e9be3671c1b5c80eba5c62fe4d617bfefd66d6`. Runtime +`a0cc4c14b` reuses the exact unchanged scoped-reading instruction and objective, +while keeping every other wrapper field current. It also reuses complete Goal +text only after the existing source-integrity predicate succeeds. That predicate +moves unchanged into `goal_context_source.ts`, shared by fulfillment and the +codec. Changed text, control fields, scope, indexes, ownership, revisions and +unknown fields stay literal; malformed witnesses and missing baselines cannot +hide required reads. This removes repeated interpretation rather than adding +another context or decision owner. + +Source `ebdce8a49b4faa584571cfb688aa2c2a238a5860` retains this behavior and +native complete-Goal regression coverage. Its rebuilt wheel has 1,889 product +members matching source, with no packaged-only or mismatched member. The same +unchanged private harness compares it with the original quota baseline +`843be494266774b63b3de61d0adf5dd7f4f877a2`, using equal-length namespaces and +the same six outcomes, 24 clauses, work, stops and backends. Both arms include +mandatory supplementary Goal-file reads; recovery also includes the two +invalidation/startup hook outputs and reads the original capture without a new +admission. Optional peer expansion is separate. + +| Backend / phase | Original quota stdout bytes | C1 stdout bytes | Calls, quota → C1 | RPCs, quota → C1 | +| --- | ---: | ---: | ---: | ---: | +| File / cold | 94,892 | 90,918 | 13 → 14 | 103 → 103 | +| File / warm | 58,289 | 52,474 | 9 → 8 | 99 → 98 | +| File / loss recovery | 92,068 | 87,418 | 15 → 14 | 8 → 8 | +| SQLite / cold | 95,002 | 91,048 | 13 → 14 | 103 → 103 | +| SQLite / warm | 58,401 | 52,598 | 9 → 8 | 99 → 98 | +| SQLite / loss recovery | 92,178 | 87,540 | 15 → 14 | 8 → 8 | + +Calls count native CLI/hooks and any mandatory Goal-file read, separately from +RPCs. Capture/delta bytes alone are larger than quota in this cohort; the +complete-input comparison improves because quota separately requires 7,952 Goal +bytes. C1's optional expansion of all five independent peer outcomes still adds +13,940 content bytes and ten CLI/hook calls per phase. Complete clauses, exact +reconstruction, compaction recovery, tamper/early-spend rejection, one durable +writeback/debit and replay pass on both backends. These are deterministic +packaged-consumer checks, not actual model retention or outcome acceptance. +The single cohort ran alongside background work; it does not qualify latency, +model tokens/cache, billing, human cost or complete long-horizon benefit. +Historical negative warm observations remain attached to their earlier source. + +The final affected integration selection passes 205 Python cases, 112 typed +cases, typecheck, configured mypy and scoped Ruff. The packaged source +still fails the unchanged historical v4 oracle (digest `d12a8843834aea84…`) at +27,934 > 13,000 characters; the earlier 26,253-character observation used a +different current-source oracle (`1b5d010a30e608c6…`) and remains separate. +Neither failure is waived or relabeled by the positive paged-input comparison. + +The independently usable [#7575](https://github.com/loopx-project/loopx/pull/7575) +at `d980d621219b10353fdaeb058a1a0fd5714f4389`, based on main +`2cdff65d886d13121ffd78dccd96dd7413ab9d01`, supplies complete Goal material +through the existing opt-in native path and exact warm reuse. It has no dependency +on unmerged C1. Its main-baseline complete-Goal oracle fails on File and SQLite, +then passes with the change. Its 175 Python cases, 57 typed cases, typecheck, +configured mypy, scoped Ruff and packaged real-backend recovery/settlement pass. +Matched native stdout improves about 7.7 KB warm, but cold and loss recovery each +add about 1.2 KB and one RPC. That tradeoff stays explicit; this normal author PR +does not certify complete C1 or promote the default. C1 reuses the same predicate +and native source behavior, with scoped-wrapper reuse remaining in C1. Remaining +callers, writer cutover, historical-reader retirement and actual decision/outcome +qualification continue in the original work stream. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index 8efadc3773..af16197420 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -245,3 +245,52 @@ GitHub 随后报告 #7557 已合并为 `4b2b97b7b726d902a20969e3911fd15d15b4bb2e 贡献文件均与修复 head 相同。发生合并不能补足尚缺的新 head 独立评审或受保护操作 回执。上述 C1 资格仍绑定冻结的 main 基线,不隐含认证后续 main 变化;C1 仍是尚未 完成的作者集成,原有成本、消费者及退役缺口保持。 + +## 完整 Goal 复用与可独立使用的原生切片 + +C1 随后 rebase 到 main `f5e9be3671c1b5c80eba5c62fe4d617bfefd66d6`。Runtime +`a0cc4c14b` 精确复用不变的作用域阅读指令和 objective,wrapper 其他字段仍取当前值; +完整 Goal 正文也仅在既有来源完整性判定通过后复用。该判定原样移到 +`goal_context_source.ts`,由 fulfillment 与 codec 共享。正文变化、控制字段、scope、 +索引、归属、修订及未知字段仍按字面交付;畸形凭据或缺少基线不能隐藏必读内容。 +这减少重复解释,没有再建上下文或决策 owner。 + +Source `ebdce8a49b4faa584571cfb688aa2c2a238a5860` 保留上述行为和原生完整 Goal +回归覆盖。重建 wheel 的 1,889 个产品成员与源码一致,没有仅存在于包中或不匹配的 +成员。使用同一份未改动的私有 harness,与原 quota 基线 +`843be494266774b63b3de61d0adf5dd7f4f877a2` 比较:namespace 等长,六项成果、 +24 条要求、工作、停止条件及后端相同。两组都计入必需的额外 Goal 文件读取;恢复 +还计入两个失效/启动 hook 输出,沿原 capture 读取,不重新准入。可选 peer 展开单列。 + +| 后端/阶段 | 原 quota stdout 字节 | C1 stdout 字节 | 调用,quota → C1 | RPC,quota → C1 | +| --- | ---: | ---: | ---: | ---: | +| File/冷态 | 94,892 | 90,918 | 13 → 14 | 103 → 103 | +| File/温态 | 58,289 | 52,474 | 9 → 8 | 99 → 98 | +| File/上下文丢失恢复 | 92,068 | 87,418 | 15 → 14 | 8 → 8 | +| SQLite/冷态 | 95,002 | 91,048 | 13 → 14 | 103 → 103 | +| SQLite/温态 | 58,401 | 52,598 | 9 → 8 | 99 → 98 | +| SQLite/上下文丢失恢复 | 92,178 | 87,540 | 15 → 14 | 8 → 8 | + +调用数包含原生 CLI/hook 及必需的 Goal 文件读取,与 RPC 分列。此 cohort 的 capture/ +delta 自身字节仍大于 quota;完整输入对照改善,是因为原 quota 还须另外读取 7,952 +字节的 Goal。C1 可选展开五项独立 peer 成果,每个阶段仍另增 13,940 正文字节及十次 +CLI/hook 调用。两后端的完整条款、精确重建、压缩恢复、篡改/提前扣额拒绝、一次 +持久写回/扣额及重放均通过。这是确定性打包消费者检查,不是实际模型保留或成果验收。 +单个 cohort 与后台工作并行,不认证延迟、模型 token/cache、账单、人工或完整长程 +收益。历史温态负向观察仍绑定原先源码。 + +最终受影响集合集成的 205 项 Python、112 项 typed、typecheck、配置内 mypy 和限定 +Ruff 通过。打包源码在未改动的历史 v4 oracle +(digest `d12a8843834aea84…`)仍为 27,934 > 13,000 字符而失败;早先 26,253 字符 +观察使用另一份当时源码 oracle(`1b5d010a30e608c6…`),单独保留。正向的分页输入 +比较不豁免或重标这两项失败。 + +可独立使用的 [#7575](https://github.com/loopx-project/loopx/pull/7575),精确 head +`d980d621219b10353fdaeb058a1a0fd5714f4389`、基于 main +`2cdff65d886d13121ffd78dccd96dd7413ab9d01`,在现有 opt-in 原生路径完整交付 Goal +并精确复用温态正文,不依赖尚未合入的 C1。完整 Goal oracle 在 main 的 File、SQLite +均失败,修改后通过。175 项 Python、57 项 typed、typecheck、配置内 mypy、限定 Ruff +及打包真实后端恢复/结算通过。匹配的原生 stdout 温态少约 7.7 KB,但冷态及丢失恢复 +各增约 1.2 KB 和一次 RPC。该取舍明确保留;这个正常作者 PR 不认证完整 C1 或提升默认。 +C1 复用相同判定与原生来源行为,作用域 wrapper 复用仍留在 C1。剩余调用方、writer +切换、历史 reader 退役及实际决策/成果资格,沿原工作流继续。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index f8236e9408..951e733996 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -205,13 +205,17 @@ input; new-admission cutover stays separate from historical receipt replay. The [packaged C1 heartbeat checkpoint](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md) preserves required source and same-capture settlement semantics on isolated File/SQLite. Its diagnostic follow-through reuses one typed owner and removes the -Python duplicates; [#7557](https://github.com/loopx-project/loopx/pull/7557) proposes -the independently usable legacy-reader slice, shared unchanged by #7430. Repeated -quota-baseline measurements reduce cold/recovery bytes but still grow warm input -and add all-peer read cost; the integration's existing v4 budget failure remains. -Continue C1 on warm composition cost and remaining real consumers with full -unknown-field/obligation preservation. This is no default promotion or positive -complete-cost qualification; controller-model and outcome acceptance stay open. +Python duplicates; [#7557](https://github.com/loopx-project/loopx/pull/7557) is merged, +and #7430 reuses its repaired module. Later complete-Goal and scoped-prose reuse +improves cold, warm and recovery stdout against the original quota baseline when +both arms include all mandatory reads. Earlier negative warm cohorts and the +integration's existing v4 budget failure remain recorded; optional all-peer reads +still add cost. [#7575](https://github.com/loopx-project/loopx/pull/7575) proposes the +independently usable native complete-Goal/reuse slice: warm input improves, while +cold and recovery incur extra overhead. Continue C1 on remaining real consumers, +writer cutover and reader retirement with complete unknown-field/obligation +preservation. These engineering observations do not promote defaults or qualify +model-token, complete-cost or actual decision/outcome benefit. The same integration codec now adopts merged [#7528](https://github.com/loopx-project/loopx/pull/7528) for exact relocated evidence with literal current overrides, including scoped diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 2b38e5a472..37490fc2f7 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -158,10 +158,13 @@ C1 的[消费链切换表](controller-executor-unified-contract-v0.zh-CN.md#c1- [C1 打包 heartbeat 检查点](ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md) 在隔离 File/SQLite 保留必读来源及同 capture 结算语义。后续诊断投影复用同一 typed owner 并删除 Python 重复算法;[#7557](https://github.com/loopx-project/loopx/pull/7557) -提案可独立使用的旧读取方切片,#7430 原样复用相同模块。重跑原 quota 基线对照后 -冷态/恢复字节下降,但温态输入仍增长,全部 peer 补读另增成本,集成原有 v4 预算 -失败仍保留。原 C1 继续诊断温态组合成本及剩余真实消费者,保留未知字段和完整义务; -这不是默认提升或完整成本正向资格,controller 模型与成果验收保持开放。 +已合并,#7430 复用修复后的模块。后续完整 Goal 与作用域指令复用,在两组均计入全部 +必读内容后,相比原 quota 基线减少冷态、温态和恢复 stdout。历史温态负向 cohort 与 +集成原有 v4 预算失败仍保留,可选全部 peer 补读仍另增成本。 +[#7575](https://github.com/loopx-project/loopx/pull/7575) 提案可独立使用的原生完整 Goal/ +复用切片:温态输入改善,冷态与恢复则增加开销。原 C1 继续剩余真实消费者、writer +切换与 reader 退役,保留未知字段和完整义务。这些工程观察不提升默认,也不认证模型 +token、完整成本或实际决策/成果收益。 同一集成 codec 现采用已合入的 [#7528](https://github.com/loopx-project/loopx/pull/7528),在精确 搬移证据上叠加字面当前控制,覆盖 scoped context。Ledger 同时记录局部搬移差分变小 From 87c14a5aa5bcc126060c9d43571aa7b1594c5385 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:52:55 +0800 Subject: [PATCH 34/37] fix(context): retain one Goal integrity import after main integration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/context_delivery.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index a037804907..0552dd64dc 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -1,4 +1,3 @@ -import {completeGoalNarrative} from "../work_items/goal_context_source.ts"; /** Presentation of immutable quota, Turn and work-context captures. No admission or state writes. * Confirmed final input and explicit native-hook continuity assumptions remain * distinct; successful capture handoff alone is never retained-input proof. */ From 4604ee4793eb44f31a4fad9c218ec508cece050d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:20:38 +0800 Subject: [PATCH 35/37] refactor(context): share identical complete Todo summary lanes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota/diagnostic_projection.ts | 59 +++++++++++++++ loopx/control_plane/quota/turn_envelope.ts | 9 ++- .../test_complete_turn_decision.py | 25 +++++++ tests/control_plane_ts/turn_envelope.test.ts | 72 +++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/quota/diagnostic_projection.ts b/loopx/control_plane/quota/diagnostic_projection.ts index 58d804ab60..ebf2f3cf4f 100644 --- a/loopx/control_plane/quota/diagnostic_projection.ts +++ b/loopx/control_plane/quota/diagnostic_projection.ts @@ -7,6 +7,8 @@ import type {JsonObject} from "../effect_program.ts"; import {jsonObject} from "../runtime_decode.ts"; import {TODO_CANONICAL_READ_RECORD_FIELDS} from "../coordination/coordination_state_contract.ts"; +import {canonicalAuthorityJson} from "../coordination/authority_store_codec.ts"; +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; type ProjectionPath = (string | number)[]; type ProjectionChange = {path: ProjectionPath; value: unknown} | @@ -71,6 +73,63 @@ const DIAGNOSTIC_LANES = new Set([ "recent_completed_advancement_items", "completed_without_successor_items", "other_agent_bound_user_action_items", ]); + +/** Complete contracts keep one literal copy of equal summary lanes. A reference + * names an array in this same signed document, never a fresh Todo read. Only + * registered lanes participate; counts, candidate collections and unknown fields + * retain their original values. Consumers resolve before applying display caps. */ +export function resolveQuotaSummaryLanes(payload: JsonObject): JsonObject { + let result = payload; + for (const role of ["agent", "user"] as const) { + const root = `${role}_todo_summary`, summary = jsonObject(payload[root]); + if (!summary || summary.schema_version !== undefined && summary.schema_version !== "todo_summary_v0") continue; + let expanded = summary; + for (const lane of DIAGNOSTIC_LANES) { + const key = `${lane}_ref`; + if (!Object.hasOwn(summary, key)) continue; + const reference = summary[key]; + const prefix = `$.${root}.`; + const owner = typeof reference === "string" && reference.startsWith(prefix) + ? reference.slice(prefix.length) : ""; + if (Object.hasOwn(summary, lane) || !DIAGNOSTIC_LANES.has(owner) + || !Array.isArray(summary[owner]) || Object.hasOwn(summary, `${owner}_ref`)) { + throw new EffectRuntimeRequestError("Todo summary lane reference requires one literal array in the same summary"); + } + expanded = {...expanded, [lane]: summary[owner]}; + delete expanded[key]; + } + if (expanded !== summary) result = {...result, [root]: expanded}; + } + return result; +} + +export function referenceQuotaSummaryLanes(payload: JsonObject): JsonObject { + let result = resolveQuotaSummaryLanes(payload); + for (const role of ["agent", "user"] as const) { + const root = `${role}_todo_summary`, summary = jsonObject(result[root]); + if (!summary || summary.schema_version !== undefined && summary.schema_version !== "todo_summary_v0") continue; + const owners = new Map(); + let shared = summary; + // Retain the normally visible lane first. Full-source composition shares + // exact arrays, including any future item fields, without selecting work. + const preferred = role === "agent" ? "first_executable_items" : "first_open_items"; + for (const lane of [preferred, ...DIAGNOSTIC_LANES].filter((key, index, keys) => keys.indexOf(key) === index)) { + const value = summary[lane]; + if (!Array.isArray(value) || value.length === 0 || Object.hasOwn(summary, `${lane}_ref`)) continue; + const encoded = JSON.stringify(canonicalAuthorityJson(value)); + const owner = owners.get(encoded); + if (!owner) { owners.set(encoded, lane); continue; } + const reference = `$.${root}.${owner}`; + const inlineBytes = Buffer.byteLength(JSON.stringify({[lane]: value}), "utf8"); + const referenceBytes = Buffer.byteLength(JSON.stringify({[`${lane}_ref`]: reference}), "utf8"); + if (inlineBytes <= referenceBytes) continue; + shared = {...shared, [`${lane}_ref`]: reference}; + delete shared[lane]; + } + if (shared !== summary) result = {...result, [root]: shared}; + } + return result; +} // The read codec adds these display fields to canonical records. Retain them // on visible items; they do not make a known diagnostic list a future schema. const ITEM_FIELDS = new Set([...TODO_CANONICAL_READ_RECORD_FIELDS, "index", "content_revision"]); diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index c31956144a..c1ebb50eb7 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -7,7 +7,8 @@ import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from import {prepareContextDelivery, restoreContextDelivery, OUTCOME_CONTEXT_PROJECTION_SCHEMA} from "./context_delivery.ts"; import {updateNativeContextLifecycle} from "./native_context_lifecycle.ts"; import {OUTCOME_CONTEXT_READ_PLAN_SCHEMA} from "../goals/outcome_requirements.ts"; -import {contextProjectionChanges, projectQuotaDiagnostics} from "./diagnostic_projection.ts"; +import {contextProjectionChanges, projectQuotaDiagnostics, referenceQuotaSummaryLanes, + resolveQuotaSummaryLanes} from "./diagnostic_projection.ts"; import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; import { @@ -469,7 +470,8 @@ function capturedOutcomeContext(request: JsonObject): JsonObject { requireNonEmptyString(source.path, "original capture path"); if (typeof source.sha256 !== "string" || !BARE_SHA256_PATTERN.test(source.sha256)) throw new EffectRuntimeRequestError("outcome context needs the original file digest"); - const context = projectQuotaDiagnostics(structuredClone(payload), {capture_ref: "$.source_capture"}); + const context = projectQuotaDiagnostics(structuredClone(payload.schema_version === COMPLETE_TURN_DECISION_VERSION + ? resolveQuotaSummaryLanes(payload) : payload), {capture_ref: "$.source_capture"}); const carriers = acceptanceCarriers(context); const channel = jsonObject(jsonObject(context.interaction_contract)?.agent_channel); if (!channel || !scopeInteractionOutcomeReadGuidance(channel)) return payload; @@ -1504,6 +1506,7 @@ function compactPeerActivation(contract: JsonObject, detailRef: string): JsonObj } function turnActionProjection(payload: JsonObject): JsonObject { + if (payload.schema_version === COMPLETE_TURN_DECISION_VERSION) payload = resolveQuotaSummaryLanes(payload); const projection = actionProjection(payload); const orchestration = object(projection.task_orchestration_contract); if (Object.keys(orchestration).length > 0) { @@ -1709,6 +1712,7 @@ function requireCompleteDecisionSource(source: JsonObject): void { replanAuthoringContract(source); requireVisionAuditReferences(source); requireCurrentTaskTitleReference(source); + resolveQuotaSummaryLanes(source); resolveExploreGuidance(source, object(object(object(source.interaction_contract).agent_channel).work_context)); // This root field selects execution, unlike optional evidence carried intact // in the source. Reuse both registered owners; do not inherit the historical @@ -1850,6 +1854,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { source = referenceExploreGuidance(source); source = referenceCurrentTaskTitle(source); source = referenceVisionContinuation(source); + source = referenceQuotaSummaryLanes(source); requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 7c399f68f5..ee4c2480b7 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -104,6 +104,27 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): compose_turn_decision({**source, "schema_version": "unknown"}) +@pytest.mark.parametrize("reference", [None, False, [], "$.user_todo_summary.first_executable_items", + "$.agent_todo_summary.missing", "$.agent_todo_summary.backlog_items"]) +def test_complete_readers_refuse_invalid_diagnostic_references_with_valid_hashes(reference): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + lane = [{"todo_id": "task", "text": "Preserve the complete requirement. " * 80}] + source["agent_todo_summary"] = {"schema_version": "todo_summary_v0", + "first_executable_items": lane, "backlog_items": copy.deepcopy(lane)} + contract = compose_turn_decision(source) + assert contract["agent_todo_summary"]["backlog_items_ref"] == "$.agent_todo_summary.first_executable_items" + contract["agent_todo_summary"]["backlog_items_ref"] = reference + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review")): + with pytest.raises(RuntimeError, match="summary lane reference"): + read() + + def test_pending_goal_capture_remains_readable_but_cannot_dispatch_business_work(): from loopx.control_plane.quota.turn_envelope import build_turn_controller_input from loopx.control_plane.turn_driver.codex_cli import prepare_codex_cli_input @@ -341,6 +362,10 @@ def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeyp original = (capture / "decision.json").read_bytes() contract = json.loads(original) assert contract["schema_version"] == "loopx_turn_envelope_v1" + summary = contract["agent_todo_summary"] + assert summary["first_open_items_ref"] == "$.agent_todo_summary.first_executable_items" + assert summary["first_executable_items"][0]["todo_id"] == TODO_ID + assert summary["first_executable_items"][0]["status"] == "open" assert output["interaction_contract"] == contract["interaction_contract"] assert output["contract_binding"] == contract["contract_binding"] assert output == contract # Every original fact and instruction survives rendering. diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index 330fddcdec..e2a8977536 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -20,6 +20,78 @@ import { EffectRuntimeRequestError } from "../../loopx/control_plane/effect_runt import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; import { TURN_ENVELOPE_SECTION_TARGETS } from "../../loopx/control_plane/quota/turn_envelope_budget.ts"; import { projectPeerOrchestration } from "../../loopx/control_plane/quota/peer_orchestration.ts"; +import {projectQuotaDiagnostics, referenceQuotaSummaryLanes, resolveQuotaSummaryLanes} + from "../../loopx/control_plane/quota/diagnostic_projection.ts"; + +test("complete diagnostic lanes share exact arrays without losing distinct facts or future fields", () => { + const item = {todo_id: "task", text: "Original requirements and final stop. ".repeat(80), + status: "blocked", future: {may_publish: false, value: null}}; + const lane = [item, {...item, todo_id: "dependency", status: "open"}]; + const source: JsonObject = {agent_todo_summary: {schema_version: "todo_summary_v0", open_count: 2, + first_open_items: structuredClone(lane), first_executable_items: lane, + backlog_items: structuredClone(lane), claimed_open_items: [...lane].reverse(), + deferred_items: [{...item, future: {may_publish: 0, value: null}}, lane[1]], + future_items: structuredClone(lane), future_control: {requires_stop: true}}, + user_todo_summary: {first_open_items: lane, gate_open_items: structuredClone(lane)}, + action_portfolio: {suggested_actions: lane}, future: {items: lane}}; + const before = structuredClone(source), shared = referenceQuotaSummaryLanes(source); + const summary = shared.agent_todo_summary as JsonObject; + for (const key of ["first_open_items", "backlog_items"]) { + assert.equal(summary[key], undefined); + assert.equal(summary[`${key}_ref`], "$.agent_todo_summary.first_executable_items"); + } + for (const key of ["first_executable_items", "claimed_open_items", "deferred_items", "future_items", "future_control", "open_count"]) + assert.deepEqual(summary[key], (source.agent_todo_summary as JsonObject)[key]); + assert.deepEqual(resolveQuotaSummaryLanes(shared), source); + assert.deepEqual(referenceQuotaSummaryLanes(shared), shared); + assert.deepEqual(projectQuotaDiagnostics(resolveQuotaSummaryLanes(shared), {capture_ref: "$.source_capture"}), + projectQuotaDiagnostics(source, {capture_ref: "$.source_capture"})); + assert.deepEqual(source, before); + assert.deepEqual(referenceQuotaSummaryLanes({agent_todo_summary: {...source.agent_todo_summary as JsonObject, + schema_version: "future_summary"}}).agent_todo_summary, + {...source.agent_todo_summary as JsonObject, schema_version: "future_summary"}); + assert.deepEqual(referenceQuotaSummaryLanes({agent_todo_summary: {first_open_items: [1], first_executable_items: [true]}}), + {agent_todo_summary: {first_open_items: [1], first_executable_items: [true]}}); +}); + +test("diagnostic references reject ambiguity, chains, wrong roots and missing literal owners", () => { + const lane = [{todo_id: "task", text: "Complete source. ".repeat(40)}]; + const valid = {agent_todo_summary: {first_executable_items: lane, + backlog_items_ref: "$.agent_todo_summary.first_executable_items"}}; + for (const patch of [{backlog_items: lane}, {backlog_items_ref: null}, {backlog_items_ref: []}, + {backlog_items_ref: "$.user_todo_summary.first_executable_items"}, + {backlog_items_ref: "$.agent_todo_summary.future_items", future_items: lane}, + {backlog_items_ref: "$.agent_todo_summary.backlog_items"}, + {first_executable_items: null}, {first_executable_items_ref: "$.agent_todo_summary.items", items: lane}]) { + const invalid = {agent_todo_summary: {...valid.agent_todo_summary, ...patch}}; + const before = structuredClone(invalid); + assert.throws(() => resolveQuotaSummaryLanes(invalid), /summary lane reference/); + assert.deepEqual(invalid, before); + } +}); + +test("complete producer and Host preserve action semantics when diagnostic lanes share a source", () => { + const source = payload(); + const lane = [{todo_id: "todo-1", text: "Keep complete requirements and stops. ".repeat(80), + status: "open", priority: "P0", claimed_by: "agent-ts"}]; + source.agent_todo_summary = {schema_version: "todo_summary_v0", first_open_items: lane, + first_executable_items: structuredClone(lane), backlog_items: structuredClone(lane), open_count: 1}; + const before = structuredClone(source); + const complete = evaluateTurnEnvelope({operation: "compose_complete", payload: source, scheduler_execution_args: ""}); + assert.equal((complete.agent_todo_summary as JsonObject).backlog_items_ref, "$.agent_todo_summary.first_executable_items"); + const restored = resolveQuotaSummaryLanes(complete); + delete restored.schema_version; delete restored.contract_binding; + assert.deepEqual(restored, source); + const legacy = buildTurnEnvelope({payload: source, scheduler_execution_args: ""}); + const current = buildTurnEnvelope({payload: complete, scheduler_execution_args: ""}); + assert.deepEqual(turnEnvelopeActionSignatureDocument(current), turnEnvelopeActionSignatureDocument(legacy)); + assert.deepEqual(evaluateTurnEnvelope({operation: "compose_complete", payload: complete, scheduler_execution_args: ""}), complete); + const host = evaluateTurnEnvelope({operation: "host_dispatch_context", payload: complete, envelope: current}); + assert.equal(host.primary_action, (source.interaction_contract as JsonObject).agent_channel.primary_action); + assert.equal(evaluateTurnEnvelope({operation: "complete_host_lineage", payload: complete, envelope: current}).goal_id, + source.goal_id); + assert.deepEqual(source, before); +}); test("captured outcome lookup shares exact content across registered carriers and retains revisions", () => { const one = {id: "shared", owner_agent_id: "peer", objective: "Original", revision: 1, From c59d82b0e1d8187dcc4ff9b1136b67fc5fe266de Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:20:38 +0800 Subject: [PATCH 36/37] test(context): distinguish diagnostic edits from projected documents Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane_ts/goal_outcome_refinement.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/control_plane_ts/goal_outcome_refinement.test.ts b/tests/control_plane_ts/goal_outcome_refinement.test.ts index 66f4eb153b..b0af3de88b 100644 --- a/tests/control_plane_ts/goal_outcome_refinement.test.ts +++ b/tests/control_plane_ts/goal_outcome_refinement.test.ts @@ -16,6 +16,7 @@ import {projectReplanSemantics} from "../../loopx/control_plane/work_items/repla import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts"; import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts"; import {projectInteractionRequiredReads} from "../../loopx/control_plane/work_items/interaction_contract.ts"; +import {contextProjectionChanges, projectQuotaDiagnostics} from "../../loopx/control_plane/quota/diagnostic_projection.ts"; const goal = "tour-goal"; const goalRef = {goal_id: goal, goal_instance_id: "ginst_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}; @@ -210,7 +211,11 @@ test("native and legacy diagnostics share one owner without hiding decision cont const before = structuredClone(payload), source_capture = {path: "/capture/one ' quoted.json", sha256: "a".repeat(64)}; const native = nativeProjection(payload, source_capture); const scoped = native.context as JsonObject; - const legacy = evaluateTurnEnvelope({operation: "reference_context", payload, diagnostics: {}}); + const legacy = projectQuotaDiagnostics(payload); + // The bridge returns display edits so Python keeps untouched numeric facts. + // Compare that wire separately from the complete projected document. + assert.deepEqual(evaluateTurnEnvelope({operation: "reference_context", payload, diagnostics: {}}), + contextProjectionChanges(payload, legacy)); const one = scoped.agent_todo_summary as JsonObject, two = legacy.agent_todo_summary as JsonObject; assert.equal(one.backlog_items, undefined); assert.deepEqual(one.first_executable_items, [task]); @@ -228,7 +233,7 @@ test("native and legacy diagnostics share one owner without hiding decision cont } const detail = evaluateTurnEnvelope({operation: "reference_context", payload, diagnostics: {agent_detail: true, user_detail: true}}); - assert.deepEqual(detail, payload); + assert.deepEqual(detail, {changes: []}); assert.deepEqual(payload, before); // The derived wrapper must still be checked against the exact original. evaluateTurnEnvelope({operation: "captured_outcome", payload: native, source_payload: payload, outcome_ref: ""}); From d410e297d96bb3620cfdac1d3432d87acc198c47 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:22:24 +0800 Subject: [PATCH 37/37] docs(context): qualify same-capture diagnostic lane reuse Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../2026-10-28-packaged-heartbeat.md | 54 +++++++++++++++++++ .../2026-10-28-packaged-heartbeat.zh-CN.md | 41 ++++++++++++++ .../rfcs/loopx-overall-roadmap-v0.md | 8 +-- .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 5 +- docs/reference/required-work-context.md | 18 +++++++ 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md index 6d10aa110d..e0c0ec989f 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.md @@ -390,3 +390,57 @@ does not certify complete C1 or promote the default. C1 reuses the same predicat and native source behavior, with scoped-wrapper reuse remaining in C1. Remaining callers, writer cutover, historical-reader retirement and actual decision/outcome qualification continue in the original work stream. + +## Same-capture Todo-summary lane reuse + +C1 subsequently rebases onto main `ffc90182bc2a896aa9980cf24dcf23a0e3276e3d`, +which includes #7575 merged as `a6283d923e3c951443fea65ba1a855c4f7c6c335`. +Its native Goal producer and integrity owner are adopted rather than copied. +Runtime `4604ee479` adds exact lane sharing in the existing typed diagnostic +owner. Equal nonempty registered arrays retain one literal owner and use +same-summary `_ref` references where smaller. Equality preserves order, +JSON types and unknown item fields; other fields and candidate collections stay +unchanged. The complete composer, action projection and native presentation use +this owner, resolving references before display caps. No extra state read, +capture, admission or settlement owner is introduced. Ambiguous, chained, +missing and cross-summary targets are rejected, including with valid inner +contract hashes. Older captures remain readable. + +The same source-checkout workload emits 27,934 characters before this change +and 25,862 afterward: 2,072 fewer (7.4%), entirely in the agent summary. All other +root-section lengths are unchanged. The rebuilt wheel, with 1,889 source-matching +product members, independently fails the unchanged `d12a8843834aea84…` v4 oracle +at **25,862 > 13,000**. This is the opt-in complete-contract variant, not the +default quota output budget. Neither ceiling is raised or claimed passed. +An identical real File/SQLite producer oracle fails on immutable pre-change +`87c14a5aa` and passes with the references. The integration passes 333 Python +cases; five explicit native-Codex environment cases remain skipped. The 125 typed +cases, control-plane typecheck, configured mypy and scoped Ruff pass. A stale +bridge test is corrected separately: display edits are compared with edits, +and the resolved document retains its original semantic assertions. + +The unchanged packaged harness again compares original quota `843be494…` with +this integrated candidate, in equal-length namespaces and with all mandatory +Goal reads. This new cohort measures the complete integration; its difference +does not isolate the lane change from the rebase and preceding C1 work. + +| Backend / phase | Original quota stdout bytes | C1 stdout bytes | Calls, quota → C1 | RPCs, quota → C1 | +| --- | ---: | ---: | ---: | ---: | +| File / cold | 94,510 | 89,911 | 13 → 12 | 103 → 102 | +| File / warm | 57,931 | 52,094 | 9 → 8 | 99 → 98 | +| File / loss recovery | 91,720 | 87,048 | 15 → 14 | 8 → 8 | +| SQLite / cold | 94,622 | 90,638 | 13 → 14 | 103 → 103 | +| SQLite / warm | 58,043 | 52,218 | 9 → 8 | 99 → 98 | +| SQLite / loss recovery | 91,830 | 87,170 | 15 → 14 | 8 → 8 | + +Both backends preserve all 24 clauses, full/delta reconstruction, original-capture +loss recovery without new admission, exact optional peer reads, tamper and early +spend refusal, and one writeback/debit under replay; work stays open. Complete +capture/delta bytes alone remain larger than quota, which separately reads +7,952 mandatory Goal bytes. Five optional peer outcomes still add 13,940 content +bytes and ten CLI/hook calls per phase. Warm RPC response bytes and loss-recovery +request/response bytes grow. One uncontrolled cohort does not qualify latency, +model retention/tokens/cache, billing, human cost or accepted outcomes. Remaining +caller replacement, writer cutover, historical-reader retirement and actual +single-session decision/outcome qualification stay open; the controller-model +comparison remains a separate later experiment. diff --git a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md index af16197420..677ed5a076 100644 --- a/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md +++ b/docs/architecture/rfcs/ledger/controller-executor-unified-contract-v0/2026-10-28-packaged-heartbeat.zh-CN.md @@ -294,3 +294,44 @@ Ruff 通过。打包源码在未改动的历史 v4 oracle 各增约 1.2 KB 和一次 RPC。该取舍明确保留;这个正常作者 PR 不认证完整 C1 或提升默认。 C1 复用相同判定与原生来源行为,作用域 wrapper 复用仍留在 C1。剩余调用方、writer 切换、历史 reader 退役及实际决策/成果资格,沿原工作流继续。 + +## 同 capture 的 Todo 摘要列表复用 + +C1 随后 rebase 到 main `ffc90182bc2a896aa9980cf24dcf23a0e3276e3d`,其中 #7575 +已合入为 `a6283d923e3c951443fea65ba1a855c4f7c6c335`。原生 Goal producer 和完整性 +owner 直接采用,不复制。Runtime `4604ee479` 在既有 typed 诊断 owner 中加入精确 +列表复用:相等且非空的已注册数组仅保留一份字面正文,在更小时用同摘要内的 +`_ref` 引用。相等比较保留顺序、JSON 类型和未知条目字段;其他字段与候选集合 +不变。完整 composer、action projection 和 native 展示共用此 owner,应用展示上限 +前先展开引用。不新增状态读取、capture、准入或结算 owner。正文/引用歧义、引用链、 +目标缺失和跨摘要目标均拒绝,包括内部契约 hash 有效的情况。旧 capture 仍可读取。 + +同一源码工作负载从修改前 27,934 字符降到修改后 25,862,少 2,072(7.4%),全部来自 +agent summary;其他根字段长度不变。重建 wheel 的 1,889 个产品成员与源码一致, +独立运行未改动的 `d12a8843834aea84…` v4 oracle,仍为 **25,862 > 13,000** 而失败。 +这是 opt-in 完整契约变体,不是默认 quota 输出预算;两项上限均未提高,也不称通过。 +相同真实 File/SQLite producer oracle 在不可变修改前 `87c14a5aa` 失败,加入引用后 +通过。集成通过 333 项 Python;五项须显式 native Codex 环境的用例仍跳过。125 项 +typed、control-plane typecheck、配置内 mypy 和限定 Ruff 通过。另行纠正过期 bridge +测试:display edits 与 edits 比较,展开后的文档保留原语义断言。 + +同一未改动的打包 harness,再以等长 namespace 和全部必读 Goal 内容,对照原 quota +`843be494…` 与当前集成候选。此新 cohort 测量完整集成,其差异不能把列表修改与 +rebase、先前 C1 工作分离归因。 + +| 后端/阶段 | 原 quota stdout 字节 | C1 stdout 字节 | 调用,quota → C1 | RPC,quota → C1 | +| --- | ---: | ---: | ---: | ---: | +| File/冷态 | 94,510 | 89,911 | 13 → 12 | 103 → 102 | +| File/温态 | 57,931 | 52,094 | 9 → 8 | 99 → 98 | +| File/上下文丢失恢复 | 91,720 | 87,048 | 15 → 14 | 8 → 8 | +| SQLite/冷态 | 94,622 | 90,638 | 13 → 14 | 103 → 103 | +| SQLite/温态 | 58,043 | 52,218 | 9 → 8 | 99 → 98 | +| SQLite/上下文丢失恢复 | 91,830 | 87,170 | 15 → 14 | 8 → 8 | + +两后端保留全部 24 条要求、全量/差分重建、无需新准入的原 capture 丢失恢复、精确 +可选 peer 补读、篡改及提前扣额拒绝、一次写回/扣额和重放,工作保持 open。完整 +capture/delta 自身字节仍大于 quota;原 quota 另读 7,952 字节必需 Goal。可选五项 +peer 成果每阶段仍另增 13,940 正文字节和十次 CLI/hook 调用。温态 RPC 响应字节与 +丢失恢复的请求/响应字节增长。一次非受控 cohort 不认证延迟、模型保留/token/ +cache、账单、人工或验收成果。剩余调用方替换、writer 切换、历史 reader 退役及实际 +单会话决策/成果资格仍未完成;controller 模型比较另作后续实验。 diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 951e733996..377482cf14 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -210,9 +210,11 @@ and #7430 reuses its repaired module. Later complete-Goal and scoped-prose reuse improves cold, warm and recovery stdout against the original quota baseline when both arms include all mandatory reads. Earlier negative warm cohorts and the integration's existing v4 budget failure remain recorded; optional all-peer reads -still add cost. [#7575](https://github.com/loopx-project/loopx/pull/7575) proposes the -independently usable native complete-Goal/reuse slice: warm input improves, while -cold and recovery incur extra overhead. Continue C1 on remaining real consumers, +still add cost. [#7575](https://github.com/loopx-project/loopx/pull/7575) is merged +as an independently usable native complete-Goal/reuse slice: warm input improves, +while cold and recovery incur extra overhead. C1 adopts that shared implementation +and now references identical Todo-summary lanes within the same complete capture; +the original v4 budget still fails. Continue C1 on remaining real consumers, writer cutover and reader retirement with complete unknown-field/obligation preservation. These engineering observations do not promote defaults or qualify model-token, complete-cost or actual decision/outcome benefit. diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 37490fc2f7..f99532cf3a 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -161,8 +161,9 @@ owner 并删除 Python 重复算法;[#7557](https://github.com/loopx-project/l 已合并,#7430 复用修复后的模块。后续完整 Goal 与作用域指令复用,在两组均计入全部 必读内容后,相比原 quota 基线减少冷态、温态和恢复 stdout。历史温态负向 cohort 与 集成原有 v4 预算失败仍保留,可选全部 peer 补读仍另增成本。 -[#7575](https://github.com/loopx-project/loopx/pull/7575) 提案可独立使用的原生完整 Goal/ -复用切片:温态输入改善,冷态与恢复则增加开销。原 C1 继续剩余真实消费者、writer +[#7575](https://github.com/loopx-project/loopx/pull/7575) 已合并,提供可独立使用的原生完整 +Goal/复用切片:温态输入改善,冷态与恢复则增加开销。C1 采用该共享实现,并在同一 +完整 capture 中引用相同的 Todo 摘要列表;原 v4 预算仍失败。原 C1 继续剩余真实消费者、writer 切换与 reader 退役,保留未知字段和完整义务。这些工程观察不提升默认,也不认证模型 token、完整成本或实际决策/成果收益。 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 307827930b..167551ae18 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -132,6 +132,17 @@ retains its complete body and declaration. Todo-less reads and the explicit reduces repeated binding observations without treating a shorter context as acceptance, authority or a measured model benefit. +The complete Turn contract stores equal, nonempty registered Todo-summary lanes +once when a local reference is smaller. A `_ref` names a literal array in +the same signed summary, for example +`$.agent_todo_summary.first_executable_items`; it requires no additional read. +Equality includes array order, JSON types and unknown item fields. Resolve these +references before applying display caps. Literal/reference ambiguity, missing +targets, reference chains and references outside that summary are rejected. +Counts, candidate collections and unknown summary fields keep their original +values. The original capture remains complete; this encoding changes no work +selection, authority, recovery or settlement rule. + Within the opt-in native context transport, a verified same-capture reading plan now produces `outcome_context_projection_v0`: an explicit model presentation, with the current Agent's and related outcomes expanded and other independent @@ -527,6 +538,13 @@ Goal adapter 仍完整读取文件、校验 UTF-8,并以全部字节计算修 没有选定工作的读取及显式 `goal-acceptance inspect` 保留完整列表;正文减少不等于 验收、执行权限或已经测得模型收益。 +完整 Turn 契约在本地引用更小时,仅存一份相等且非空的已注册 Todo 摘要列表。 +`_ref` 指向同一签名摘要中的字面数组,例如 +`$.agent_todo_summary.first_executable_items`,不需要额外读取。相等比较包含数组 +顺序、JSON 类型和未知条目字段;应用展示上限前须先展开引用。正文与引用并存、目标 +缺失、引用链或跨摘要引用均拒绝。计数、候选集合及未知摘要字段保留原值。原 capture +仍完整;编码不改变工作选择、权限、恢复或结算规则。 + 可选 native context transport 在同 capture 读取计划完整匹配时生成显式模型展示 `outcome_context_projection_v0`:展开当前 Agent 及相关成果,其他独立成果保留当前 状态索引和精确原来源引用。全局意图、检查、权限、停止和未完成读取保持完整;既有