diff --git a/apps/usage-collector/test/collector.test.mjs b/apps/usage-collector/test/collector.test.mjs index c110c4fedf..943b239110 100644 --- a/apps/usage-collector/test/collector.test.mjs +++ b/apps/usage-collector/test/collector.test.mjs @@ -191,7 +191,8 @@ for (const version of ["1.2.4", "2.0.0a1", "2.0.0b2", "2.0.0rc3"]) { await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); const root = await mkdtemp(join(tmpdir(), "loopx-real-collector-")), path = join(root, "usage.json"); t.after(async () => { server.closeAllConnections(); server.close(); await rm(root, { recursive: true, force: true }); }); - const ctx = { env: { LOOPX_USAGE_PING_ENDPOINT: "http://127.0.0.1:" + server.address().port + "/v1/ping" }, + const ctx = { env: { LOOPX_USAGE_PING: "1", + LOOPX_USAGE_PING_ENDPOINT: "http://127.0.0.1:" + server.address().port + "/v1/ping" }, version, python: "3.13", channel: "source", now }; await configureContext(path, ctx, "personal"); await configure(path, ctx, "enable"); const generation = JSON.parse(await readFile(path, "utf8")).generation; diff --git a/docs/development/bugs/workspace-status-access-errors.md b/docs/development/bugs/workspace-status-access-errors.md index 0c12310c9a..2787b6cf0d 100644 --- a/docs/development/bugs/workspace-status-access-errors.md +++ b/docs/development/bugs/workspace-status-access-errors.md @@ -12,7 +12,7 @@ → `collect_status` → 状态依赖读取或 Effect runtime → 错误响应 → 逐 Goal 提示。 归属仍是现有 Dashboard 状态读取与展示边界: -[chat_status_api.py](../../../loopx/chat_status_api.py)、 +[chat/status_api.py](../../../loopx/chat/status_api.py)、 [workspace-progressive-status.ts](../../../apps/presentation/dashboard/src/data/workspace-progressive-status.ts) 和 [i18n.tsx](../../../apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx)。 复用 [Effect runtime](../../../loopx/control_plane/effect_runtime.py) 的类型及诊断码; diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index 9c16c7e517..3484bd5411 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -1682,6 +1682,31 @@ Heartbeat 的正文回归计量先归一化已选择的 runtime、registry 和 a or promotion thresholds stay fixed for that result; revised thresholds belong to a new qualification, never a relabeled historical pass. +The auxiliary-monitor recovery fixture measures the complete original +writeback step with indented JSON, using fixed-width disposable routes on +both File and SQLite. The release base emits 8,642 characters and fails the +former 7,500-character regression ceiling; that failure remains recorded. +The current step carries the complete Vision field and path-decision bounds, +evidence-linked continuation/cost review, original identity and commands. +Deleting those obligations or splitting this small recovery step into another +reference would change its consumer contract. Calibrate only this regression +ceiling to 9,336, retaining the former 694 characters of headroom. Compare +complete authoring bounds and parsed semantic-closeout arguments with the +original admitted contract; recovery specializes the completed Task's boundary. +Spend and settled +recovery keep their 2,000-character ceilings. This preserves useful guidance; +it is neither an output reduction nor a change to any external host limit or +frozen performance criterion. Candidate measurements must still pass. + +辅助 Monitor 的恢复夹具用缩进 JSON 测量完整的原始 writeback 步骤,File 与 SQLite +均使用固定宽度的临时路由。发布基线均为 8,642 字符,原 7,500 字符回归上限的失败 +继续保留。当前步骤包含完整 Vision 字段与路径决策约束、带证据的继续执行及成本 +判断、原始身份和命令。删除这些义务或给这段小型恢复指令另加引用都会改变消费 +合同,因此只将该回归上限校准为 9,336,保留原 694 字符余量,并与最初准入合同 +逐字段比较完整 authoring 约束与解析后的 semantic-closeout 命令参数;恢复步骤保留 +已完成 Task 所需的具体结算边界。spend 和 settled 恢复仍受 2,000 字符上限约束。这不代表输出 +缩小,不改变外部宿主硬限制或冻结性能标准;候选版仍须重新测量并通过检查。 + Native progressive-context qualification found an existing default-output budget failure: immutable base `fb331f45c` and candidate `edbd56b14` both emit 25,836 JSON characters for `quota_should_run/multi_agent` with the unchanged diff --git a/docs/product/releases/v2.3.0.md b/docs/product/releases/v2.3.0.md index 1d899acfc5..c77b247621 100644 --- a/docs/product/releases/v2.3.0.md +++ b/docs/product/releases/v2.3.0.md @@ -1,5 +1,23 @@ # LoopX 2.3.0 +## Release Decision + +**Who should upgrade:** Operators using managed Goals, the Research Workspace, or a supported native host can use the clearer acceptance, source evidence, and request recovery. Windows desktop preview users should install the new stable desktop bundle manually. An existing deployment with a custom ACP endpoint named `antigravity` should rename that endpoint before upgrading. Users who do not need these workflows can stay on 2.2.0. + +**What this release solves:** Goal acceptance can use retained checks and evidence. The Workspace makes captured Turn evidence, report editions, and the original conversation easier to follow. An opted-in research Turn can evaluate the next target against matched measurements. + +**Breaking changes:** Yes. The built-in Antigravity executor reserves the id `antigravity`. Rename an existing custom ACP endpoint using that id. Existing canonical File and SQLite Goals retain their storage owner. Inherited Stride can apply its evidence-conditioned guidance. Use the Goal or machine setting to opt out explicitly. [#7832](https://github.com/loopx-project/loopx/pull/7832), [#7928](https://github.com/loopx-project/loopx/pull/7928) + +**How to verify:** Expect `loopx 2.3.0` and the expected installation owner in Doctor. Open a known Goal and inspect its acceptance record, then open the original source of one Research result. + +**Contributors:** Maintained by [@huangruiteng](https://github.com/huangruiteng). See the community contributions below. + +```bash +loopx --version +loopx doctor +loopx --format json goal-acceptance inspect --goal-id "$goal_id" +``` + ## At a Glance Check what a Goal actually delivered, pick the next research step using comparable evidence, and find the source behind a result in the Workspace. @@ -24,50 +42,15 @@ loopx doctor ### Replan Fixes +- Captured Turn recovery reads the complete original source, including small object projections. Native first reads include the current task operation and its declared continuation. The Task inspector opens the full original evidence and notes. [#8211](https://github.com/loopx-project/loopx/pull/8211), [#8356](https://github.com/loopx-project/loopx/pull/8356), [#8361](https://github.com/loopx-project/loopx/pull/8361) +- Conversation intake asks for clarification when existing Goals are equally relevant. When an accepted report body is available, the response returns that content; a status marker alone still requires retrieval from its existing owner. [#8356](https://github.com/loopx-project/loopx/pull/8356) - A pre-bound heartbeat now keeps the actual Turn identity from its producer through admission and settlement. An expired execution claim surfaces its recovery action. [#8232](https://github.com/loopx-project/loopx/pull/8232), [#7912](https://github.com/loopx-project/loopx/pull/7912) - Current decision cues appear before supporting material. A staged read stays on its original capture across recovery, and composed settlement failures retain the concrete diagnostic. [#7984](https://github.com/loopx-project/loopx/pull/7984), [#7985](https://github.com/loopx-project/loopx/pull/7985), [#8267](https://github.com/loopx-project/loopx/pull/8267) - Finish the original Turn after its completed Todo moves to the archive. [#8264](https://github.com/loopx-project/loopx/pull/8264) +- Settlement guidance separates a final Todo's post-spend closeout from a typed finding about verified scope coverage. Use the observed evidence ID or a public-safe alias in the Vision draft. [#8356](https://github.com/loopx-project/loopx/pull/8356) - A native Host retry keeps the live guard's original capture and managed claim identity. If a saved capture lacks a verified binding, keep its existing claim receipt and obtain a fresh guard for the same Turn if preparation is still needed. [#8279](https://github.com/loopx-project/loopx/pull/8279) - SSH Goals use the compact activation prompt with inherited context delivery. Windows Doctor reads drive-qualified wheel records using the native path parser. -### Contributors - -- [@BigDataDZ](https://github.com/BigDataDZ) — Native host discovery, installer quoting, Windows path handling and Doctor identity diagnostics. [#7886](https://github.com/loopx-project/loopx/pull/7886) -- [@cocolord](https://github.com/cocolord) — CLI demand loading, native context and recovery fixtures, Issue-Fix redirect verification, and original-Turn closeout after automatic archival. [#7855](https://github.com/loopx-project/loopx/pull/7855), [#8264](https://github.com/loopx-project/loopx/pull/8264) -- [@Duang777](https://github.com/Duang777) — Status retry journal and Lark Inbox recovery on damaged state. [#7693](https://github.com/loopx-project/loopx/pull/7693) -- [@gcl-coder](https://github.com/gcl-coder) — Windows settlement qualification, bound scheduler fallback and bounded Git discovery. [#7920](https://github.com/loopx-project/loopx/pull/7920) -- [@HaoooLee](https://github.com/HaoooLee) — Strict registry checks when repository tracking is unknown. [#8162](https://github.com/loopx-project/loopx/pull/8162) -- [@hhyykk](https://github.com/hhyykk) — Owner attention, Issue-Fix correction and original return, and TypeScript composition of Todo and quota reads. [#7860](https://github.com/loopx-project/loopx/pull/7860) -- [@JasonBuildAI](https://github.com/JasonBuildAI) — Host recovery encoding, owner review vocabulary and credential redaction in shared Lark sinks. [#8027](https://github.com/loopx-project/loopx/pull/8027) -- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — Native CLI capture fixtures and canonical receipt and handoff vocabularies. [#8182](https://github.com/loopx-project/loopx/pull/8182) -- [@keoyyo85](https://github.com/keoyyo85) — Quota settlement import isolation and architecture coverage. [#8054](https://github.com/loopx-project/loopx/pull/8054) -- [@LIHUA919](https://github.com/LIHUA919) — Rune source-verification pilot and native recovery and Antigravity admission readbacks. [#7875](https://github.com/loopx-project/loopx/pull/7875) -- [@NIU-123370](https://github.com/NIU-123370) — Bilingual RFC mirror checks, Anti-Entropy help and the release timeline. [#8128](https://github.com/loopx-project/loopx/pull/8128) -- [@songoow](https://github.com/songoow) — Independent Claude Code benchmark runner, expired-claim and Goal feedback recovery, and FrontierCS-2 compatibility. [#7082](https://github.com/loopx-project/loopx/pull/7082) -- [@steven-kid](https://github.com/steven-kid) — Captured Research Turn costs and ranges, readable evidence downloads, and keyboard access in desktop settings. [#7882](https://github.com/loopx-project/loopx/pull/7882) -- [@Weimr2627](https://github.com/Weimr2627) — Status compatibility exports and degraded Git registry fixtures. [#8160](https://github.com/loopx-project/loopx/pull/8160) -- [@xiaobaifly7](https://github.com/xiaobaifly7) — Status readback that distinguishes unavailable history from absent Goal status. [#7917](https://github.com/loopx-project/loopx/pull/7917) -- [@xyx2002OvO](https://github.com/xyx2002OvO) — Private runtime I/O and Windows long path support. [#7603](https://github.com/loopx-project/loopx/pull/7603) -- [@zxdvd](https://github.com/zxdvd) — Strict Codex Turn result schemas. [#7976](https://github.com/loopx-project/loopx/pull/7976) - -## Release Decision - -**Who should upgrade:** Operators using managed Goals, the Research Workspace, or a supported native host can use the clearer acceptance, source evidence, and request recovery. Windows desktop preview users should install the new stable desktop bundle manually. An existing deployment with a custom ACP endpoint named `antigravity` should rename that endpoint before upgrading. - -**What this release solves:** Goal acceptance can use retained checks and evidence. The Workspace makes captured Turn evidence, report editions, and the original conversation easier to follow. An opted-in research Turn can evaluate the next target against matched measurements. - -**Breaking changes:** Yes. The built-in Antigravity executor reserves the id `antigravity`. Rename an existing custom ACP endpoint using that id. Existing canonical File and SQLite Goals retain their storage owner. Inherited Stride can apply its evidence-conditioned guidance. Use the Goal or machine setting to opt out explicitly. [#7832](https://github.com/loopx-project/loopx/pull/7832), [#7928](https://github.com/loopx-project/loopx/pull/7928) - -**How to verify:** Expect `loopx 2.3.0` and the expected installation owner in Doctor. Open a known Goal and inspect its acceptance record, then open the original source of one Research result. - -**Contributors:** Maintained by [@huangruiteng](https://github.com/huangruiteng). See the community contributions below. - -```bash -loopx --version -loopx doctor -loopx --format json goal-acceptance inspect --goal-id "$goal_id" -``` - ## State Kernel & Control Plane - Goal owners can configure and verify checks against retained commitments with an explicit configuration and a revision-bound readback. Existing local Todo acceptance continues under its own declaration. [#8161](https://github.com/loopx-project/loopx/pull/8161), [#8178](https://github.com/loopx-project/loopx/pull/8178) @@ -92,26 +75,32 @@ loopx --format json goal-acceptance inspect --goal-id "$goal_id" - Benchmark maintainers can opt into an independent Claude Code runner, one declared settled-work handoff, or sampled local work probes. Campaign reports can retain both score histories and an explicit small terminal duration tolerance. [#7082](https://github.com/loopx-project/loopx/pull/7082), [#8070](https://github.com/loopx-project/loopx/pull/8070), [#8089](https://github.com/loopx-project/loopx/pull/8089), [#8258](https://github.com/loopx-project/loopx/pull/8258), [#8263](https://github.com/loopx-project/loopx/pull/8263) - The Rune pilot compares source-cited context with direct search through the existing Issue-Fix planning reader. It is a source-checkout experiment. This release makes no benchmark-score or long-horizon cost improvement claim. [#7875](https://github.com/loopx-project/loopx/pull/7875) +- Read authored, source-cited case studies in their benchmark child Goals and return to the original research records. A new study can explicitly give every arm the same native child-task limit. [#8272](https://github.com/loopx-project/loopx/pull/8272), [#8229](https://github.com/loopx-project/loopx/pull/8229) + ## Community Contributors - [@BigDataDZ](https://github.com/BigDataDZ) — Native host discovery, installer quoting, Windows path handling and Doctor identity diagnostics. [#7886](https://github.com/loopx-project/loopx/pull/7886), [#7887](https://github.com/loopx-project/loopx/pull/7887), [#7888](https://github.com/loopx-project/loopx/pull/7888), [#8107](https://github.com/loopx-project/loopx/pull/8107), [#8108](https://github.com/loopx-project/loopx/pull/8108) -- [@cocolord](https://github.com/cocolord) — CLI demand loading, native context and recovery fixtures, Issue-Fix redirect verification, and original-Turn closeout after automatic archival. [#7855](https://github.com/loopx-project/loopx/pull/7855), [#7878](https://github.com/loopx-project/loopx/pull/7878), [#7988](https://github.com/loopx-project/loopx/pull/7988), [#7991](https://github.com/loopx-project/loopx/pull/7991), [#7993](https://github.com/loopx-project/loopx/pull/7993), [#7996](https://github.com/loopx-project/loopx/pull/7996), [#8001](https://github.com/loopx-project/loopx/pull/8001), [#8012](https://github.com/loopx-project/loopx/pull/8012), [#8180](https://github.com/loopx-project/loopx/pull/8180), [#8185](https://github.com/loopx-project/loopx/pull/8185), [#8203](https://github.com/loopx-project/loopx/pull/8203), [#8212](https://github.com/loopx-project/loopx/pull/8212), [#8218](https://github.com/loopx-project/loopx/pull/8218), [#8222](https://github.com/loopx-project/loopx/pull/8222), [#8228](https://github.com/loopx-project/loopx/pull/8228), [#8233](https://github.com/loopx-project/loopx/pull/8233), [#8240](https://github.com/loopx-project/loopx/pull/8240), [#8257](https://github.com/loopx-project/loopx/pull/8257), [#8264](https://github.com/loopx-project/loopx/pull/8264) +- [@cocolord](https://github.com/cocolord) — CLI demand loading, native context and recovery fixtures, Issue-Fix redirect verification, and original-Turn closeout after automatic archival. Deferred feedback loading and immutable recovery readback. Preserved the public Chat status import and isolated report loading from unrelated Turn work. [#7855](https://github.com/loopx-project/loopx/pull/7855), [#7878](https://github.com/loopx-project/loopx/pull/7878), [#7988](https://github.com/loopx-project/loopx/pull/7988), [#7991](https://github.com/loopx-project/loopx/pull/7991), [#7993](https://github.com/loopx-project/loopx/pull/7993), [#7996](https://github.com/loopx-project/loopx/pull/7996), [#8001](https://github.com/loopx-project/loopx/pull/8001), [#8012](https://github.com/loopx-project/loopx/pull/8012), [#8180](https://github.com/loopx-project/loopx/pull/8180), [#8185](https://github.com/loopx-project/loopx/pull/8185), [#8203](https://github.com/loopx-project/loopx/pull/8203), [#8212](https://github.com/loopx-project/loopx/pull/8212), [#8218](https://github.com/loopx-project/loopx/pull/8218), [#8222](https://github.com/loopx-project/loopx/pull/8222), [#8228](https://github.com/loopx-project/loopx/pull/8228), [#8233](https://github.com/loopx-project/loopx/pull/8233), [#8240](https://github.com/loopx-project/loopx/pull/8240), [#8257](https://github.com/loopx-project/loopx/pull/8257), [#8264](https://github.com/loopx-project/loopx/pull/8264), [#8246](https://github.com/loopx-project/loopx/pull/8246), [#8288](https://github.com/loopx-project/loopx/pull/8288), [#8339](https://github.com/loopx-project/loopx/pull/8339), [#8346](https://github.com/loopx-project/loopx/pull/8346), [#8351](https://github.com/loopx-project/loopx/pull/8351), [#8011](https://github.com/loopx-project/loopx/pull/8011), [#8360](https://github.com/loopx-project/loopx/pull/8360) - [@Duang777](https://github.com/Duang777) — Status retry journal and Lark Inbox recovery on damaged state. [#7693](https://github.com/loopx-project/loopx/pull/7693), [#7979](https://github.com/loopx-project/loopx/pull/7979) -- [@gcl-coder](https://github.com/gcl-coder) — Windows settlement qualification, bound scheduler fallback and bounded Git discovery. [#7920](https://github.com/loopx-project/loopx/pull/7920), [#7955](https://github.com/loopx-project/loopx/pull/7955), [#8083](https://github.com/loopx-project/loopx/pull/8083), [#8084](https://github.com/loopx-project/loopx/pull/8084), [#8115](https://github.com/loopx-project/loopx/pull/8115), [#8152](https://github.com/loopx-project/loopx/pull/8152), [#8153](https://github.com/loopx-project/loopx/pull/8153), [#8154](https://github.com/loopx-project/loopx/pull/8154) +- [@gcl-coder](https://github.com/gcl-coder) — Windows settlement qualification, bound scheduler fallback and bounded Git discovery. All Git reads in the change window share one time budget. Integration previews share one Git deadline and Change Quality preserves unusual Git path names. [#7920](https://github.com/loopx-project/loopx/pull/7920), [#7955](https://github.com/loopx-project/loopx/pull/7955), [#8083](https://github.com/loopx-project/loopx/pull/8083), [#8084](https://github.com/loopx-project/loopx/pull/8084), [#8115](https://github.com/loopx-project/loopx/pull/8115), [#8152](https://github.com/loopx-project/loopx/pull/8152), [#8153](https://github.com/loopx-project/loopx/pull/8153), [#8154](https://github.com/loopx-project/loopx/pull/8154), [#8169](https://github.com/loopx-project/loopx/pull/8169), [#8362](https://github.com/loopx-project/loopx/pull/8362), [#8363](https://github.com/loopx-project/loopx/pull/8363), [#8364](https://github.com/loopx-project/loopx/pull/8364) - [@HaoooLee](https://github.com/HaoooLee) — Strict registry checks when repository tracking is unknown. [#8162](https://github.com/loopx-project/loopx/pull/8162) - [@hhyykk](https://github.com/hhyykk) — Owner attention, Issue-Fix correction and original return, and TypeScript composition of Todo and quota reads. [#7860](https://github.com/loopx-project/loopx/pull/7860), [#7861](https://github.com/loopx-project/loopx/pull/7861), [#7870](https://github.com/loopx-project/loopx/pull/7870), [#7877](https://github.com/loopx-project/loopx/pull/7877), [#7881](https://github.com/loopx-project/loopx/pull/7881), [#7892](https://github.com/loopx-project/loopx/pull/7892), [#7893](https://github.com/loopx-project/loopx/pull/7893), [#7894](https://github.com/loopx-project/loopx/pull/7894), [#7895](https://github.com/loopx-project/loopx/pull/7895), [#7900](https://github.com/loopx-project/loopx/pull/7900), [#7901](https://github.com/loopx-project/loopx/pull/7901), [#7904](https://github.com/loopx-project/loopx/pull/7904) - [@JasonBuildAI](https://github.com/JasonBuildAI) — Host recovery encoding, owner review vocabulary and credential redaction in shared Lark sinks. [#8027](https://github.com/loopx-project/loopx/pull/8027), [#8029](https://github.com/loopx-project/loopx/pull/8029), [#8031](https://github.com/loopx-project/loopx/pull/8031), [#8181](https://github.com/loopx-project/loopx/pull/8181) -- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — Native CLI capture fixtures and canonical receipt and handoff vocabularies. [#8182](https://github.com/loopx-project/loopx/pull/8182), [#8183](https://github.com/loopx-project/loopx/pull/8183), [#8184](https://github.com/loopx-project/loopx/pull/8184) +- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — Native CLI capture fixtures and canonical receipt, handoff and material-lifecycle outcome vocabularies. [#8182](https://github.com/loopx-project/loopx/pull/8182), [#8183](https://github.com/loopx-project/loopx/pull/8183), [#8184](https://github.com/loopx-project/loopx/pull/8184), [#7398](https://github.com/loopx-project/loopx/pull/7398) +- [@Jim-jimu](https://github.com/Jim-jimu) — Full write-scope coverage checks in quota admission. [#8271](https://github.com/loopx-project/loopx/pull/8271) - [@keoyyo85](https://github.com/keoyyo85) — Quota settlement import isolation and architecture coverage. [#8054](https://github.com/loopx-project/loopx/pull/8054), [#8055](https://github.com/loopx-project/loopx/pull/8055) -- [@LIHUA919](https://github.com/LIHUA919) — Rune source-verification pilot and native recovery and Antigravity admission readbacks. [#7875](https://github.com/loopx-project/loopx/pull/7875), [#8093](https://github.com/loopx-project/loopx/pull/8093), [#8094](https://github.com/loopx-project/loopx/pull/8094) +- [@LIHUA919](https://github.com/LIHUA919) — Rune source-verification pilot and native recovery and Antigravity admission readbacks. Deterministic conversation and persistent usage fixtures, plus measured 1 MiB budget closure. [#7875](https://github.com/loopx-project/loopx/pull/7875), [#8093](https://github.com/loopx-project/loopx/pull/8093), [#8094](https://github.com/loopx-project/loopx/pull/8094), [#8256](https://github.com/loopx-project/loopx/pull/8256), [#8336](https://github.com/loopx-project/loopx/pull/8336), [#8338](https://github.com/loopx-project/loopx/pull/8338), [#8340](https://github.com/loopx-project/loopx/pull/8340) - [@NIU-123370](https://github.com/NIU-123370) — Bilingual RFC mirror checks, Anti-Entropy help and the release timeline. [#8128](https://github.com/loopx-project/loopx/pull/8128), [#8129](https://github.com/loopx-project/loopx/pull/8129), [#8130](https://github.com/loopx-project/loopx/pull/8130) - [@songoow](https://github.com/songoow) — Independent Claude Code benchmark runner, expired-claim and Goal feedback recovery, and FrontierCS-2 compatibility. [#7082](https://github.com/loopx-project/loopx/pull/7082), [#7912](https://github.com/loopx-project/loopx/pull/7912), [#7914](https://github.com/loopx-project/loopx/pull/7914), [#7916](https://github.com/loopx-project/loopx/pull/7916), [#7954](https://github.com/loopx-project/loopx/pull/7954), [#7958](https://github.com/loopx-project/loopx/pull/7958) -- [@steven-kid](https://github.com/steven-kid) — Captured Research Turn costs and ranges, readable evidence downloads, and keyboard access in desktop settings. [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8019](https://github.com/loopx-project/loopx/pull/8019) +- [@steven-kid](https://github.com/steven-kid) — Captured Research Turn costs and ranges, readable evidence downloads, and desktop settings search that preserves drafts and keyboard access. [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8019](https://github.com/loopx-project/loopx/pull/8019), [#8020](https://github.com/loopx-project/loopx/pull/8020) - [@Weimr2627](https://github.com/Weimr2627) — Status compatibility exports and degraded Git registry fixtures. [#8160](https://github.com/loopx-project/loopx/pull/8160), [#8163](https://github.com/loopx-project/loopx/pull/8163) +- [@xiaoan17](https://github.com/xiaoan17) — LoopX Personal Workspace navigation in the DSH web sidebar. [#8352](https://github.com/loopx-project/loopx/pull/8352) - [@xiaobaifly7](https://github.com/xiaobaifly7) — Status readback that distinguishes unavailable history from absent Goal status. [#7917](https://github.com/loopx-project/loopx/pull/7917) - [@xyx2002OvO](https://github.com/xyx2002OvO) — Private runtime I/O and Windows long path support. [#7603](https://github.com/loopx-project/loopx/pull/7603) - [@zxdvd](https://github.com/zxdvd) — Strict Codex Turn result schemas. [#7976](https://github.com/loopx-project/loopx/pull/7976) +Historical contribution: [@JustinIan-AI](https://github.com/JustinIan-AI) introduced the opt-in Anti-Entropy code governance capability. The original merged PR metadata is unavailable; attribution is verified against the [contribution commit](https://github.com/loopx-project/loopx/commit/ac74f90c4778381b6fcedd5d76059522591c3f42). + ## Documentation & Compatibility - Rename a custom ACP endpoint using the reserved `antigravity` id before installing the new package. Keep the existing canonical File or SQLite data store and inspect Doctor before and after updating. Follow the versioned 2.x migration instructions for any remaining Markdown-only Goal. [#7832](https://github.com/loopx-project/loopx/pull/7832), [#8130](https://github.com/loopx-project/loopx/pull/8130) @@ -162,7 +151,7 @@ loopx configure-goal --goal-id "$goal_id" --no-execution-yardstick-enabled --exe **Activation:** The inherited strategy can use evidence_conditioned_v1. Set the strategy explicitly to pin it for a Goal. -**Validation:** Read the effective execution profile. At the next review, verify its evidence-linked recommendation against the Goal. +**Validation:** Read the effective execution profile. For a staged replan, open `decision_value_review` in the review stage and compare its advice with the Goal evidence before choosing work. [#8375](https://github.com/loopx-project/loopx/pull/8375) **Disable / rollback:** Set the Goal strategy to off, or use the existing clear option to restore inheritance. @@ -180,11 +169,11 @@ loopx configure-goal --goal-id "$goal_id" --execution-stride-strategy off --exec **Activation:** Use the existing Context Delivery setting or request context delivery on a supported host for the next ordinary guard. -**Validation:** Open the saved capture using the path and digest from that guard. Select only the current fields the next action requires. +**Validation:** Open the saved capture using the path and digest from that guard. Select only the current fields the next action requires. If preflight offers Todo selection, run the emitted command and verify that it opens a separate capture for the same Turn. [#8275](https://github.com/loopx-project/loopx/pull/8275) **Disable / rollback:** Disable the setting or pass --context-delivery off on the next ordinary guard. Complete in-flight recovery from its original capture. -**Authority boundary:** A read recovers the admitted Turn's facts. The original guard and settlement retain their authority. +**Authority boundary:** A read recovers the admitted Turn's facts. The original guard and settlement retain their authority. Complete Turn capability advice and result checks bind to the admitted source, including when a retry reads a recorded result. [#8373](https://github.com/loopx-project/loopx/pull/8373) **Docs:** [Versioned documentation](https://github.com/loopx-project/loopx/blob/v2.3.0/docs/reference/required-work-context.md) @@ -343,6 +332,100 @@ loopx issue-fix workflow-plan --goal-id "$goal_id" loopx start-goal --guided --project . --capability-route issue-fix --goal-text "$issue_request" ``` +### Native benchmark child tasks + +**Activation:** This is an explicit per-study option. Inspect the runner help first, then add `--native-child-limit 3` to every arm's separately authorized fresh invocation. It requires a non-game task with native feedback; do not modify a running study. + +**Validation:** Check each fresh profile's native child limit and the LoopX Goal spawn-policy readback. Restored work must match its sealed setting. Configuration acceptance alone does not prove useful delegation. + +**Disable / rollback:** Start a separate study without the flag. Close completed child sessions through the native host; changing an existing session is not a rollback. + +**Authority boundary:** The parent alone submits candidates. Children share the task's time, account, access and Stop. The option grants no extra evaluation opportunity or deeper delegation. Omission preserves the existing configuration and ambient values cannot activate this mode. + +**Docs:** [Versioned runtime guide](https://github.com/loopx-project/loopx/blob/v2.3.0/benchmark/runtime/RUNTIME.md#configure-the-native-job) + +```bash +python -m benchmark.edgebench.run --help +``` + +### Authored research case studies + +**Activation:** Opt in per source-pack registration with `--case-study-json`. Inspect existing packs first. Register the case's pack under its own benchmark child Goal; a global switch or model execution is not needed. + +**Validation:** List the registered packs, then open the case in Research. Follow a cited line back to its exact saved source. Changed sources remain historical, and unavailable translations remain explicit. + +**Disable / rollback:** Return to Research records and analysis or select a previous pack. Omit the companion on future registrations. Editing a companion creates a new version; retain existing source and reading versions. + +**Authority boundary:** Reading runs no analysis or experiment. Registration verifies source bytes and citation ranges, not entailment or scientific validity. Goal parentage grants no additional access or execution authority. + +**Docs:** [Versioned case-study guide](https://github.com/loopx-project/loopx/blob/v2.3.0/docs/reference/trajectory-assessment.md#authored-case-studies) + +```bash +loopx benchmark trajectory-packs --goal-id "$case_goal" --format json +loopx benchmark trajectory-pack-register --goal-id "$case_goal" --input-json "$pack_input" --source-root "$source_root" --title "$case_title" --case-study-json "$case_study" +``` + +### Native receiver setup recovery + +**Activation:** On a fresh native Goal or heartbeat benchmark receiver, append the two readiness options to the authorized trial. The limits are 75 seconds total and 25 seconds per attempt. + +**Validation:** Check that the receiver starts only after account readback succeeds. A bounded account_timeout records a setup timeout if the remaining trial allowance runs out. + +**Disable / rollback:** Omit both options for the single 25-second startup probe. + +**Authority boundary:** The setup observer retries only account read transport timeouts within the original remaining allowance. Stop and authorization or protocol failures end the attempt. The observer performs read-only account checks and emits bounded diagnostics. + +**Docs:** [Versioned benchmark guide](https://github.com/loopx-project/loopx/blob/v2.3.0/benchmark/README.md#explicitly-continue-after-native-account-allowance-is-repaired) + +```bash +python -m benchmark.edgebench.run --worker native-goal --task "$task_id" \ + --tasks-dir "$tasks_dir" --log-dir "$log_dir" --run-id "$run_id" \ + --model "$model" --effort "$effort" --timeout "$trial_seconds" \ + --native-readiness-total-seconds 75 --native-readiness-attempt-seconds 25 +``` + +### Change preparation and integration previews + +**Activation:** Run Change Quality prepare for a bound Goal and named Git base. Use Integration Branch configure to preview an ordered set of source branches. Add --execute to save an authorized local plan. + +**Validation:** Check the exact changed paths in the preparation result. Read Integration Branch status for a saved plan and compare the source order and current heads with the intended integration. + +**Disable / rollback:** Run configure without --execute to preview corrections. For an existing plan, stop invoking sync when you want to leave the local integration ref at its current head. + +**Authority boundary:** Prepare and configure previews read the chosen checkout and Git refs. Integration sync uses a bounded Git deadline and updates a local integration ref only on its authorized execution path. + +**Docs:** [Versioned Change Quality guide](https://github.com/loopx-project/loopx/blob/v2.3.0/loopx/capabilities/change_quality/README.md), [Versioned Integration Branch guide](https://github.com/loopx-project/loopx/blob/v2.3.0/loopx/capabilities/integration_branch/README.md) + +```bash +loopx --format json change-quality prepare --goal-id "$goal_id" --repo-path . --base-ref origin/main +loopx --format json integration-branch configure --repo-path . --base-ref origin/main \ + --integration-branch "$integration_branch" --source-branch "$source_branch" +# Read back an existing saved plan +loopx --format json integration-branch status --repo-path . +``` + +### DSH Personal Workspace panel + +**Activation:** Build and install the plugin from the matching LoopX tag with the compatible DSH 0.2.1-rc.2 source host. Start the LoopX dashboard on its default loopback port and launch the same DSH web profile. The existing beta.6 download predates the panel. + +**Validation:** Open the LoopX sidebar entry in DSH and verify that the Personal Workspace loads. Stop the local dashboard, confirm the panel offers Retry, then start the dashboard and retry in the same DSH page. + +**Disable / rollback:** Remove the plugin from the same DSH web profile. The standalone LoopX dashboard remains available. Older DSH hosts without the required slots retain the GoalBar and skills. + +**Authority boundary:** The embedded view uses LoopX's local dashboard and existing access rules. It shares no state with the DSH session and grants no model tool, credential access, or Goal write authority. + +**Docs:** [Versioned DSH plugin guide](https://github.com/loopx-project/loopx/blob/v2.3.0/packages/dsh-loopx-plugin/README.md#personal-workspace-panel) + +```bash +cd packages/dsh-loopx-plugin +./install.sh +loopx dashboard --no-open +# In a second terminal in the same directory, with the same DSH_HOME +./node_modules/.bin/dsh --profile web --port 0 +# To remove the plugin from that profile +./node_modules/.bin/dsh plugin --profile web remove dsh-loopx-plugin +``` + ## Install / Update For a new PyPI environment, install the package, then install the workflow entry points for the host you use. @@ -366,40 +449,9 @@ loopx doctor ## 中文摘要 -在 Goal 里核对实际交付,在研究工作台对照同条件候选,并回到原始记录查看结果来源。已有安装先执行 `loopx update check`,由当前安装方式执行 `loopx update apply`。 - -- 到 Goal 的验收页查看承诺、检查和读回证据。局部 Todo 仍按自身交付结案。 [#8161](https://github.com/loopx-project/loopx/pull/8161), [#8178](https://github.com/loopx-project/loopx/pull/8178) -- 给已有 Explore 证据的 Goal 启用 Yardstick,在同一目标和测量条件下对照候选。 [#8202](https://github.com/loopx-project/loopx/pull/8202), [#8207](https://github.com/loopx-project/loopx/pull/8207), [#8261](https://github.com/loopx-project/loopx/pull/8261) -- 打开研究记录查看捕获的 Turn 范围、用量对照和可读证据报告,也可以从首页回看一版报告。 [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8116](https://github.com/loopx-project/loopx/pull/8116), [#8146](https://github.com/loopx-project/loopx/pull/8146) -- 在对话里编辑或取消未运行的消息,在支持的会话继续引导当前 Turn,最后回原对话核对答案。 [#8059](https://github.com/loopx-project/loopx/pull/8059), [#8096](https://github.com/loopx-project/loopx/pull/8096), [#8121](https://github.com/loopx-project/loopx/pull/8121), [#8269](https://github.com/loopx-project/loopx/pull/8269) -- 复盘和恢复沿用实际分配的 Turn 与原捕获。过期 claim 会显示恢复动作,结算失败保留诊断。 [#7912](https://github.com/loopx-project/loopx/pull/7912), [#7984](https://github.com/loopx-project/loopx/pull/7984), [#7985](https://github.com/loopx-project/loopx/pull/7985), [#8232](https://github.com/loopx-project/loopx/pull/8232), [#8267](https://github.com/loopx-project/loopx/pull/8267) -- 已完成的 Todo 自动归档后,仍可沿原 Turn 结算。 [#8264](https://github.com/loopx-project/loopx/pull/8264) -- 原生 Host 重试沿用实时准入的原捕获和托管领取身份。缺少可验证绑定的快照仍可读取,保留已有回执。仍需准备时,为同一 Turn 获取新的 guard。 [#8279](https://github.com/loopx-project/loopx/pull/8279) -- SSH Goal 在继承上下文设置时使用紧凑提示。Windows Doctor 能读回带盘符的 wheel 安装记录。 - -### 贡献者 - -- [@BigDataDZ](https://github.com/BigDataDZ) — 原生宿主发现、安装命令引用、Windows 长路径和 Doctor 身份诊断. [#7886](https://github.com/loopx-project/loopx/pull/7886) -- [@cocolord](https://github.com/cocolord) — CLI 按需加载、原生上下文与恢复夹具、Issue-Fix 重定向验证及 Todo 自动归档后的原 Turn 结算。 [#7855](https://github.com/loopx-project/loopx/pull/7855), [#8264](https://github.com/loopx-project/loopx/pull/8264) -- [@Duang777](https://github.com/Duang777) — 状态重试日志和飞书 Inbox 损坏状态处理. [#7693](https://github.com/loopx-project/loopx/pull/7693) -- [@gcl-coder](https://github.com/gcl-coder) — Windows 结算验证、调度回退绑定和 Git 发现时限. [#7920](https://github.com/loopx-project/loopx/pull/7920) -- [@HaoooLee](https://github.com/HaoooLee) — 跟踪状态未知时的严格 registry 检查. [#8162](https://github.com/loopx-project/loopx/pull/8162) -- [@hhyykk](https://github.com/hhyykk) — 负责人提醒、Issue-Fix 修订与原路返回、Todo 与 quota 读回的 TypeScript 合成. [#7860](https://github.com/loopx-project/loopx/pull/7860) -- [@JasonBuildAI](https://github.com/JasonBuildAI) — 宿主恢复编码、owner 评审术语和飞书共享投影凭据脱敏. [#8027](https://github.com/loopx-project/loopx/pull/8027) -- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — 原生 CLI 捕获夹具与回执、交接术语统一. [#8182](https://github.com/loopx-project/loopx/pull/8182) -- [@keoyyo85](https://github.com/keoyyo85) — 结算命令加载隔离和架构覆盖. [#8054](https://github.com/loopx-project/loopx/pull/8054) -- [@LIHUA919](https://github.com/LIHUA919) — Rune 源码核验试验、原生恢复和 Antigravity 准入读回. [#7875](https://github.com/loopx-project/loopx/pull/7875) -- [@NIU-123370](https://github.com/NIU-123370) — 双语 RFC 镜像检查、Anti-Entropy 帮助和版本时间线. [#8128](https://github.com/loopx-project/loopx/pull/8128) -- [@songoow](https://github.com/songoow) — 独立 Claude Code 基准 runner、过期 claim 与 Goal 反馈恢复、FrontierCS-2 兼容. [#7082](https://github.com/loopx-project/loopx/pull/7082) -- [@steven-kid](https://github.com/steven-kid) — 研究 Turn 成本与范围、可读证据下载和桌面设置键盘可用性. [#7882](https://github.com/loopx-project/loopx/pull/7882) -- [@Weimr2627](https://github.com/Weimr2627) — 状态兼容导出和 Git registry 降级夹具. [#8160](https://github.com/loopx-project/loopx/pull/8160) -- [@xiaobaifly7](https://github.com/xiaobaifly7) — 状态读回区分历史不可用与 Goal 状态缺失. [#7917](https://github.com/loopx-project/loopx/pull/7917) -- [@xyx2002OvO](https://github.com/xyx2002OvO) — 私有运行时 I/O 和 Windows 长路径支持. [#7603](https://github.com/loopx-project/loopx/pull/7603) -- [@zxdvd](https://github.com/zxdvd) — 严格的 Codex Turn 结果 schema. [#7976](https://github.com/loopx-project/loopx/pull/7976) - ### 升级决策 -**谁需要升级:**使用受管 Goal、研究工作台或原生宿主的用户。Windows 桌面预览版用户应手动安装新版稳定版桌面包。配置了同名自定义 ACP endpoint 的用户先将 `antigravity` 改名。 +**谁需要升级:**使用受管 Goal、研究工作台或原生宿主的用户。Windows 桌面预览版用户应手动安装新版稳定版桌面包。配置了同名自定义 ACP endpoint 的用户先将 `antigravity` 改名。 不需要这些工作流的用户可以继续使用 2.2.0。 **解决了什么:**在 Goal 核对承诺与证据。在研究结果中查看原 Turn、报告和下一步的可比测量。在原会话恢复和追踪请求。 @@ -409,8 +461,28 @@ loopx doctor **贡献者:**版本维护者为 [@huangruiteng](https://github.com/huangruiteng),具体社区贡献见下方。 +```bash +loopx --version +loopx doctor +loopx --format json goal-acceptance inspect --goal-id "$goal_id" +``` + +在 Goal 里核对实际交付,在研究工作台对照同条件候选,并回到原始记录查看结果来源。已有安装先执行 `loopx update check`,由当前安装方式执行 `loopx update apply`。 + +- 到 Goal 的验收页查看承诺、检查和读回证据。局部 Todo 仍按自身交付结案。 [#8161](https://github.com/loopx-project/loopx/pull/8161), [#8178](https://github.com/loopx-project/loopx/pull/8178) +- 给已有 Explore 证据的 Goal 启用 Yardstick,在同一目标和测量条件下对照候选。 [#8202](https://github.com/loopx-project/loopx/pull/8202), [#8207](https://github.com/loopx-project/loopx/pull/8207), [#8261](https://github.com/loopx-project/loopx/pull/8261) +- 打开研究记录查看捕获的 Turn 范围、用量对照和可读证据报告,也可以从首页回看一版报告。 [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8116](https://github.com/loopx-project/loopx/pull/8116), [#8146](https://github.com/loopx-project/loopx/pull/8146) +- 在对话里编辑或取消未运行的消息,在支持的会话继续引导当前 Turn,最后回原对话核对答案。 [#8059](https://github.com/loopx-project/loopx/pull/8059), [#8096](https://github.com/loopx-project/loopx/pull/8096), [#8121](https://github.com/loopx-project/loopx/pull/8121), [#8269](https://github.com/loopx-project/loopx/pull/8269) +- 复盘和恢复沿用实际分配的 Turn 与原捕获。过期 claim 会显示恢复动作,结算失败保留诊断。 [#7912](https://github.com/loopx-project/loopx/pull/7912), [#7984](https://github.com/loopx-project/loopx/pull/7984), [#7985](https://github.com/loopx-project/loopx/pull/7985), [#8232](https://github.com/loopx-project/loopx/pull/8232), [#8267](https://github.com/loopx-project/loopx/pull/8267) +- 已完成的 Todo 自动归档后,仍可沿原 Turn 结算。 [#8264](https://github.com/loopx-project/loopx/pull/8264) +- 最后一个 Todo 按原计划扣额后收尾。若要声明已覆盖某个范围,写回时需附该范围的核验证据。Vision 草稿使用已观察到的证据标识或可公开别名。 [#8356](https://github.com/loopx-project/loopx/pull/8356) +- 原生 Host 重试沿用实时准入的原捕获和托管领取身份。缺少可验证绑定的快照仍可读取,保留已有回执。仍需准备时,为同一 Turn 获取新的 guard。 [#8279](https://github.com/loopx-project/loopx/pull/8279) +- SSH Goal 在继承上下文设置时使用紧凑提示。Windows Doctor 能读回带盘符的 wheel 安装记录。 + ### 状态内核与控制面 +- 捕获后的 Turn 恢复读取完整原始来源,并能读回小型对象投影;原生首读提供当前任务操作及其声明的后续命令,Task 检查面板可打开完整原始证据与备注。 [#8211](https://github.com/loopx-project/loopx/pull/8211)、[#8356](https://github.com/loopx-project/loopx/pull/8356)、[#8361](https://github.com/loopx-project/loopx/pull/8361) +- 会话请求同时匹配多个现有 Goal 时先澄清;已有已验收报告正文时直接返回内容,只有状态标记时仍由原负责人取回正文。 [#8356](https://github.com/loopx-project/loopx/pull/8356) - Goal 承诺可以配置检查并按 revision 核对结果,局部 Todo 仍按自己的验收声明完成。 [#8161](https://github.com/loopx-project/loopx/pull/8161), [#8178](https://github.com/loopx-project/loopx/pull/8178) - 心跳预检、准入和结算沿用实际 Turn,恢复保持原捕获及错误诊断。 [#7912](https://github.com/loopx-project/loopx/pull/7912), [#8232](https://github.com/loopx-project/loopx/pull/8232), [#8240](https://github.com/loopx-project/loopx/pull/8240), [#8267](https://github.com/loopx-project/loopx/pull/8267) - 继承的 Stride 依据证据和工作形态提供指引。显式开启 Yardstick 后,满足同目标、同验收与同测量条件的不同候选才参与对照。 [#7928](https://github.com/loopx-project/loopx/pull/7928), [#8202](https://github.com/loopx-project/loopx/pull/8202), [#8207](https://github.com/loopx-project/loopx/pull/8207), [#8261](https://github.com/loopx-project/loopx/pull/8261), [#8268](https://github.com/loopx-project/loopx/pull/8268) @@ -433,26 +505,32 @@ loopx doctor - 维护者可显式选择 Claude Code runner、一次结算后交接或本地采样探针。报告支持分数轨迹和小额时长容差。 [#7082](https://github.com/loopx-project/loopx/pull/7082), [#8070](https://github.com/loopx-project/loopx/pull/8070), [#8089](https://github.com/loopx-project/loopx/pull/8089), [#8258](https://github.com/loopx-project/loopx/pull/8258), [#8263](https://github.com/loopx-project/loopx/pull/8263) - Rune 是源码对照试验,经既有 Issue-Fix 路径读取经核验的引用。本版没有基准提分或完整成本改善结论。 [#7875](https://github.com/loopx-project/loopx/pull/7875) +- 在 benchmark child Goal 阅读带来源引用的案例,并返回原研究记录。新 study 可显式为每个 arm 设置相同的原生 child-task 上限。 [#8272](https://github.com/loopx-project/loopx/pull/8272), [#8229](https://github.com/loopx-project/loopx/pull/8229) + ### 社区贡献者 - [@BigDataDZ](https://github.com/BigDataDZ) — 原生宿主发现、安装命令引用、Windows 长路径和 Doctor 身份诊断. [#7886](https://github.com/loopx-project/loopx/pull/7886), [#7887](https://github.com/loopx-project/loopx/pull/7887), [#7888](https://github.com/loopx-project/loopx/pull/7888), [#8107](https://github.com/loopx-project/loopx/pull/8107), [#8108](https://github.com/loopx-project/loopx/pull/8108) -- [@cocolord](https://github.com/cocolord) — CLI 按需加载、原生上下文与恢复夹具、Issue-Fix 重定向验证及 Todo 自动归档后的原 Turn 结算。 [#7855](https://github.com/loopx-project/loopx/pull/7855), [#7878](https://github.com/loopx-project/loopx/pull/7878), [#7988](https://github.com/loopx-project/loopx/pull/7988), [#7991](https://github.com/loopx-project/loopx/pull/7991), [#7993](https://github.com/loopx-project/loopx/pull/7993), [#7996](https://github.com/loopx-project/loopx/pull/7996), [#8001](https://github.com/loopx-project/loopx/pull/8001), [#8012](https://github.com/loopx-project/loopx/pull/8012), [#8180](https://github.com/loopx-project/loopx/pull/8180), [#8185](https://github.com/loopx-project/loopx/pull/8185), [#8203](https://github.com/loopx-project/loopx/pull/8203), [#8212](https://github.com/loopx-project/loopx/pull/8212), [#8218](https://github.com/loopx-project/loopx/pull/8218), [#8222](https://github.com/loopx-project/loopx/pull/8222), [#8228](https://github.com/loopx-project/loopx/pull/8228), [#8233](https://github.com/loopx-project/loopx/pull/8233), [#8240](https://github.com/loopx-project/loopx/pull/8240), [#8257](https://github.com/loopx-project/loopx/pull/8257), [#8264](https://github.com/loopx-project/loopx/pull/8264) +- [@cocolord](https://github.com/cocolord) — CLI 按需加载、原生上下文与恢复夹具、Issue-Fix 重定向验证及 Todo 自动归档后的原 Turn 结算。 反馈适配器延迟加载与不可变恢复读回。 保留公开 Chat 状态导入兼容,并将报告加载与无关 Turn 工作隔离。 [#7855](https://github.com/loopx-project/loopx/pull/7855), [#7878](https://github.com/loopx-project/loopx/pull/7878), [#7988](https://github.com/loopx-project/loopx/pull/7988), [#7991](https://github.com/loopx-project/loopx/pull/7991), [#7993](https://github.com/loopx-project/loopx/pull/7993), [#7996](https://github.com/loopx-project/loopx/pull/7996), [#8001](https://github.com/loopx-project/loopx/pull/8001), [#8012](https://github.com/loopx-project/loopx/pull/8012), [#8180](https://github.com/loopx-project/loopx/pull/8180), [#8185](https://github.com/loopx-project/loopx/pull/8185), [#8203](https://github.com/loopx-project/loopx/pull/8203), [#8212](https://github.com/loopx-project/loopx/pull/8212), [#8218](https://github.com/loopx-project/loopx/pull/8218), [#8222](https://github.com/loopx-project/loopx/pull/8222), [#8228](https://github.com/loopx-project/loopx/pull/8228), [#8233](https://github.com/loopx-project/loopx/pull/8233), [#8240](https://github.com/loopx-project/loopx/pull/8240), [#8257](https://github.com/loopx-project/loopx/pull/8257), [#8264](https://github.com/loopx-project/loopx/pull/8264), [#8246](https://github.com/loopx-project/loopx/pull/8246), [#8288](https://github.com/loopx-project/loopx/pull/8288), [#8339](https://github.com/loopx-project/loopx/pull/8339), [#8346](https://github.com/loopx-project/loopx/pull/8346), [#8351](https://github.com/loopx-project/loopx/pull/8351), [#8011](https://github.com/loopx-project/loopx/pull/8011), [#8360](https://github.com/loopx-project/loopx/pull/8360) - [@Duang777](https://github.com/Duang777) — 状态重试日志和飞书 Inbox 损坏状态处理. [#7693](https://github.com/loopx-project/loopx/pull/7693), [#7979](https://github.com/loopx-project/loopx/pull/7979) -- [@gcl-coder](https://github.com/gcl-coder) — Windows 结算验证、调度回退绑定和 Git 发现时限. [#7920](https://github.com/loopx-project/loopx/pull/7920), [#7955](https://github.com/loopx-project/loopx/pull/7955), [#8083](https://github.com/loopx-project/loopx/pull/8083), [#8084](https://github.com/loopx-project/loopx/pull/8084), [#8115](https://github.com/loopx-project/loopx/pull/8115), [#8152](https://github.com/loopx-project/loopx/pull/8152), [#8153](https://github.com/loopx-project/loopx/pull/8153), [#8154](https://github.com/loopx-project/loopx/pull/8154) +- [@gcl-coder](https://github.com/gcl-coder) — Windows 结算验证、调度回退绑定和 Git 发现时限. 变更窗口内的 Git 读取共享同一时限。 集成预览共享同一 Git 时限,Change Quality 保留含特殊字符的路径。 [#7920](https://github.com/loopx-project/loopx/pull/7920), [#7955](https://github.com/loopx-project/loopx/pull/7955), [#8083](https://github.com/loopx-project/loopx/pull/8083), [#8084](https://github.com/loopx-project/loopx/pull/8084), [#8115](https://github.com/loopx-project/loopx/pull/8115), [#8152](https://github.com/loopx-project/loopx/pull/8152), [#8153](https://github.com/loopx-project/loopx/pull/8153), [#8154](https://github.com/loopx-project/loopx/pull/8154), [#8169](https://github.com/loopx-project/loopx/pull/8169), [#8362](https://github.com/loopx-project/loopx/pull/8362), [#8363](https://github.com/loopx-project/loopx/pull/8363), [#8364](https://github.com/loopx-project/loopx/pull/8364) - [@HaoooLee](https://github.com/HaoooLee) — 跟踪状态未知时的严格 registry 检查. [#8162](https://github.com/loopx-project/loopx/pull/8162) - [@hhyykk](https://github.com/hhyykk) — 负责人提醒、Issue-Fix 修订与原路返回、Todo 与 quota 读回的 TypeScript 合成. [#7860](https://github.com/loopx-project/loopx/pull/7860), [#7861](https://github.com/loopx-project/loopx/pull/7861), [#7870](https://github.com/loopx-project/loopx/pull/7870), [#7877](https://github.com/loopx-project/loopx/pull/7877), [#7881](https://github.com/loopx-project/loopx/pull/7881), [#7892](https://github.com/loopx-project/loopx/pull/7892), [#7893](https://github.com/loopx-project/loopx/pull/7893), [#7894](https://github.com/loopx-project/loopx/pull/7894), [#7895](https://github.com/loopx-project/loopx/pull/7895), [#7900](https://github.com/loopx-project/loopx/pull/7900), [#7901](https://github.com/loopx-project/loopx/pull/7901), [#7904](https://github.com/loopx-project/loopx/pull/7904) - [@JasonBuildAI](https://github.com/JasonBuildAI) — 宿主恢复编码、owner 评审术语和飞书共享投影凭据脱敏. [#8027](https://github.com/loopx-project/loopx/pull/8027), [#8029](https://github.com/loopx-project/loopx/pull/8029), [#8031](https://github.com/loopx-project/loopx/pull/8031), [#8181](https://github.com/loopx-project/loopx/pull/8181) -- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — 原生 CLI 捕获夹具与回执、交接术语统一. [#8182](https://github.com/loopx-project/loopx/pull/8182), [#8183](https://github.com/loopx-project/loopx/pull/8183), [#8184](https://github.com/loopx-project/loopx/pull/8184) +- [@Jiaqiiii523](https://github.com/Jiaqiiii523) — 原生 CLI 捕获夹具与回执、交接和材料生命周期结果术语统一。 [#8182](https://github.com/loopx-project/loopx/pull/8182), [#8183](https://github.com/loopx-project/loopx/pull/8183), [#8184](https://github.com/loopx-project/loopx/pull/8184), [#7398](https://github.com/loopx-project/loopx/pull/7398) +- [@Jim-jimu](https://github.com/Jim-jimu) — quota 准入中的完整 write-scope 覆盖检查。 [#8271](https://github.com/loopx-project/loopx/pull/8271) - [@keoyyo85](https://github.com/keoyyo85) — 结算命令加载隔离和架构覆盖. [#8054](https://github.com/loopx-project/loopx/pull/8054), [#8055](https://github.com/loopx-project/loopx/pull/8055) -- [@LIHUA919](https://github.com/LIHUA919) — Rune 源码核验试验、原生恢复和 Antigravity 准入读回. [#7875](https://github.com/loopx-project/loopx/pull/7875), [#8093](https://github.com/loopx-project/loopx/pull/8093), [#8094](https://github.com/loopx-project/loopx/pull/8094) +- [@LIHUA919](https://github.com/LIHUA919) — Rune 源码核验试验、原生恢复和 Antigravity 准入读回. 稳定的会话时钟和持久化用量夹具,以及 1 MiB 预算验收记录。 [#7875](https://github.com/loopx-project/loopx/pull/7875), [#8093](https://github.com/loopx-project/loopx/pull/8093), [#8094](https://github.com/loopx-project/loopx/pull/8094), [#8256](https://github.com/loopx-project/loopx/pull/8256), [#8336](https://github.com/loopx-project/loopx/pull/8336), [#8338](https://github.com/loopx-project/loopx/pull/8338), [#8340](https://github.com/loopx-project/loopx/pull/8340) - [@NIU-123370](https://github.com/NIU-123370) — 双语 RFC 镜像检查、Anti-Entropy 帮助和版本时间线. [#8128](https://github.com/loopx-project/loopx/pull/8128), [#8129](https://github.com/loopx-project/loopx/pull/8129), [#8130](https://github.com/loopx-project/loopx/pull/8130) - [@songoow](https://github.com/songoow) — 独立 Claude Code 基准 runner、过期 claim 与 Goal 反馈恢复、FrontierCS-2 兼容. [#7082](https://github.com/loopx-project/loopx/pull/7082), [#7912](https://github.com/loopx-project/loopx/pull/7912), [#7914](https://github.com/loopx-project/loopx/pull/7914), [#7916](https://github.com/loopx-project/loopx/pull/7916), [#7954](https://github.com/loopx-project/loopx/pull/7954), [#7958](https://github.com/loopx-project/loopx/pull/7958) -- [@steven-kid](https://github.com/steven-kid) — 研究 Turn 成本与范围、可读证据下载和桌面设置键盘可用性. [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8019](https://github.com/loopx-project/loopx/pull/8019) +- [@steven-kid](https://github.com/steven-kid) — 研究 Turn 成本与范围、可读证据下载、保留草稿的桌面设置搜索与键盘可用性。 [#7882](https://github.com/loopx-project/loopx/pull/7882), [#7973](https://github.com/loopx-project/loopx/pull/7973), [#8017](https://github.com/loopx-project/loopx/pull/8017), [#8019](https://github.com/loopx-project/loopx/pull/8019), [#8020](https://github.com/loopx-project/loopx/pull/8020) - [@Weimr2627](https://github.com/Weimr2627) — 状态兼容导出和 Git registry 降级夹具. [#8160](https://github.com/loopx-project/loopx/pull/8160), [#8163](https://github.com/loopx-project/loopx/pull/8163) +- [@xiaoan17](https://github.com/xiaoan17) — 在 DSH Web 侧栏打开 LoopX Personal Workspace。 [#8352](https://github.com/loopx-project/loopx/pull/8352) - [@xiaobaifly7](https://github.com/xiaobaifly7) — 状态读回区分历史不可用与 Goal 状态缺失. [#7917](https://github.com/loopx-project/loopx/pull/7917) - [@xyx2002OvO](https://github.com/xyx2002OvO) — 私有运行时 I/O 和 Windows 长路径支持. [#7603](https://github.com/loopx-project/loopx/pull/7603) - [@zxdvd](https://github.com/zxdvd) — 严格的 Codex Turn 结果 schema. [#7976](https://github.com/loopx-project/loopx/pull/7976) +历史贡献:[@JustinIan-AI](https://github.com/JustinIan-AI) 引入了显式启用的 Anti-Entropy 代码治理能力。原始合并 PR 元数据目前不可用,归属已依据 [贡献提交](https://github.com/loopx-project/loopx/commit/ac74f90c4778381b6fcedd5d76059522591c3f42) 核实。 + ### 文档与兼容性 - 升级前重命名同名自定义 `antigravity` ACP endpoint。保持原 canonical 存储,用 Doctor 确认升级前后的安装路径。仍有 Markdown-only Goal 时按 2.x 迁移指南处理。 [#7832](https://github.com/loopx-project/loopx/pull/7832), [#8130](https://github.com/loopx-project/loopx/pull/8130) @@ -503,7 +581,7 @@ loopx configure-goal --goal-id "$goal_id" --no-execution-yardstick-enabled --exe **启用:** 证据条件式 Stride。继承策略可采用 evidence_conditioned_v1。可为 Goal 显式固定策略。 -**验证:** 读回有效执行配置,下一次复盘时把建议和 Goal 证据逐项对照。 +**验证:** 读回有效执行配置。分阶段重规划时,先在 review 阶段读取 `decision_value_review`,对照 Goal 证据后再选择工作。 [#8375](https://github.com/loopx-project/loopx/pull/8375) **停用 / 回退:** 将 Goal 策略设为 off,或用既有 clear 参数恢复继承。 @@ -521,11 +599,11 @@ loopx configure-goal --goal-id "$goal_id" --execution-stride-strategy off --exec **启用:** 渐进式上下文恢复。通过已有上下文交付设置,或在支持的宿主对下一次普通 guard 指定 context delivery。 -**验证:** 用 guard 给出的路径和摘要读取保存的捕获。只补读下一步依赖的当前字段。 +**验证:** 用 guard 给出的路径和摘要读取保存的捕获。只补读下一步依赖的当前字段。若预检要求选择 Todo,执行返回的命令,核对它为同一 Turn 打开独立捕获。 [#8275](https://github.com/loopx-project/loopx/pull/8275) **停用 / 回退:** 关闭设置或在下次普通 guard 传 --context-delivery off。正在恢复的 Turn 沿原捕获完成。 -**权限边界:** 读取仅恢复已准入 Turn 的事实,准入与结算仍由原流程负责。 +**权限边界:** 读取仅恢复已准入 Turn 的事实,准入与结算仍由原流程负责。Complete Turn 的能力指引及结果校验绑定准入时的来源,重试读取历史结果时也沿用该来源。 [#8373](https://github.com/loopx-project/loopx/pull/8373) **文档:** [本版文档](https://github.com/loopx-project/loopx/blob/v2.3.0/docs/reference/required-work-context.md) @@ -684,6 +762,100 @@ loopx issue-fix workflow-plan --goal-id "$goal_id" loopx start-goal --guided --project . --capability-route issue-fix --goal-text "$issue_request" ``` +#### Native benchmark child tasks + +**启用:** 按新 study 显式选择。先查看 runner 帮助,再给每个 arm 的已授权新运行命令加入同一个 `--native-child-limit 3`。仅用于采用 native feedback 的非游戏任务,不修改运行中的 study。 + +**验证:** 逐臂核对新 profile 的原生 child limit 和 LoopX Goal spawn policy 读回。恢复工作必须匹配冻结配置;配置通过不代表模型实际完成了有用协作。 + +**停用 / 回退:** 另开不带该参数的 study。由原生宿主关闭已完成 child session;不通过改动继续中的会话回退。 + +**权限边界:** 仅 parent 提交候选。child 共用原任务的时间、账号、访问范围和 Stop,不增加评测机会或委派深度。省略参数保留既有配置,环境中的值不能自动启用本模式。 + +**文档:** [本版运行时指南](https://github.com/loopx-project/loopx/blob/v2.3.0/benchmark/runtime/RUNTIME.md#configure-the-native-job) + +```bash +python -m benchmark.edgebench.run --help +``` + +#### Authored research case studies + +**启用:** 每次注册 source pack 时用 `--case-study-json` 显式附加阅读稿,先列出已有 pack。将该案例的 pack 注册到对应 benchmark child Goal,无需全局开关或模型执行。 + +**验证:** 列出已注册 pack,在 Research 打开案例,沿引用行回到原始来源。来源变化时保留历史提示,缺少翻译时明确显示原文。 + +**停用 / 回退:** 返回 Research records and analysis 或选择原 pack。后续注册省略阅读稿;修改阅读稿时创建新版本,保留既有来源与阅读版本。 + +**权限边界:** 阅读不运行分析或实验。注册检查来源字节和引用行范围,不认证论述蕴含关系或科学有效性;Goal 父子关系不增加访问和执行权限。 + +**文档:** [本版案例阅读指南](https://github.com/loopx-project/loopx/blob/v2.3.0/docs/reference/trajectory-assessment.md#authored-case-studies) + +```bash +loopx benchmark trajectory-packs --goal-id "$case_goal" --format json +loopx benchmark trajectory-pack-register --goal-id "$case_goal" --input-json "$pack_input" --source-root "$source_root" --title "$case_title" --case-study-json "$case_study" +``` + +#### Native receiver setup recovery + +**启用:** 为新的原生 Goal 或 heartbeat benchmark receiver 启动已授权的试验时,附加以下两个 readiness 参数。总时限为 75 秒,每次尝试最多 25 秒。 + +**验证:** 检查账号读取成功后才启动 receiver。若剩余试验额度耗尽,读取有界的 account_timeout 诊断。 + +**停用 / 回退:** 省略两个参数,使用单次 25 秒启动探测。 + +**权限边界:** 仅在原试验剩余额度内重试账号读取传输超时。遇到停止、权限拒绝或协议错误时结束尝试。探测使用只读账号检查,仅输出有界诊断。 + +**文档:** [版本化基准指南](https://github.com/loopx-project/loopx/blob/v2.3.0/benchmark/README.md#explicitly-continue-after-native-account-allowance-is-repaired) + +```bash +python -m benchmark.edgebench.run --worker native-goal --task "$task_id" \ + --tasks-dir "$tasks_dir" --log-dir "$log_dir" --run-id "$run_id" \ + --model "$model" --effort "$effort" --timeout "$trial_seconds" \ + --native-readiness-total-seconds 75 --native-readiness-attempt-seconds 25 +``` + +#### Change preparation and integration previews + +**启用:** 为已绑定的 Goal 和指定 Git 基线运行 Change Quality prepare。用 Integration Branch configure 预览有序的源分支。需要保存已授权的本地计划时加 --execute。 + +**验证:** 核对准备结果中的精确变更路径。对已保存计划读取 Integration Branch status,核对源分支顺序与当前提交。 + +**停用 / 回退:** 省略 --execute 预览计划修正。希望保留当前本地集成分支时,停止调用该计划的 sync。 + +**权限边界:** 准备与配置预览读取选定的检出目录和 Git 引用。集成同步共享一个 Git 时限,只有获得授权并执行后才更新本地集成引用。 + +**文档:** [版本化 Change Quality 指南](https://github.com/loopx-project/loopx/blob/v2.3.0/loopx/capabilities/change_quality/README.md)、[版本化 Integration Branch 指南](https://github.com/loopx-project/loopx/blob/v2.3.0/loopx/capabilities/integration_branch/README.md) + +```bash +loopx --format json change-quality prepare --goal-id "$goal_id" --repo-path . --base-ref origin/main +loopx --format json integration-branch configure --repo-path . --base-ref origin/main \ + --integration-branch "$integration_branch" --source-branch "$source_branch" +# 读取已保存的计划 +loopx --format json integration-branch status --repo-path . +``` + +#### DSH Personal Workspace panel + +**启用:** 从本版 LoopX 标签构建并安装插件,使用兼容的 DSH 0.2.1-rc.2 源码宿主。在默认回环端口启动 LoopX dashboard,再启动同一 DSH Web profile。已发布的 beta.6 下载包早于该面板。 + +**验证:** 在 DSH 侧栏打开 LoopX,确认 Personal Workspace 加载。停止本地 dashboard,确认面板显示 Retry,再启动 dashboard 并在同一 DSH 页面重试。 + +**停用 / 回退:** 从同一 DSH Web profile 移除插件,仍可独立打开 LoopX dashboard。缺少对应挂载位的旧 DSH 宿主保留 GoalBar 和技能。 + +**权限边界:** 嵌入页面沿用 LoopX 本地 dashboard 和原有访问规则。它不共享 DSH 会话状态,也不授予模型工具、凭证读取或 Goal 写入权限。 + +**文档:** [本版 DSH 插件指南](https://github.com/loopx-project/loopx/blob/v2.3.0/packages/dsh-loopx-plugin/README.md#personal-workspace-panel) + +```bash +cd packages/dsh-loopx-plugin +./install.sh +loopx dashboard --no-open +# 在同一目录另开终端,使用相同的 DSH_HOME +./node_modules/.bin/dsh --profile web --port 0 +# 从该 profile 移除插件 +./node_modules/.bin/dsh plugin --profile web remove dsh-loopx-plugin +``` + ### 发布验证 - Package version and tag: `2.3.0` / `v2.3.0`. diff --git a/docs/reference/protocols/active-state-structured-projection-v0.md b/docs/reference/protocols/active-state-structured-projection-v0.md index c4ea0d66a6..8484f3b321 100644 --- a/docs/reference/protocols/active-state-structured-projection-v0.md +++ b/docs/reference/protocols/active-state-structured-projection-v0.md @@ -234,8 +234,9 @@ Status, unfiltered `todo list`, `todo list --thin` without an exact ID, dashboard and chat attention views keep their existing bounded summaries. An exact `todo list --todo-id ID` without `--thin` returns the complete current or retained request text through the existing Task detail reader. The canonical -provider and regenerated active state retain the complete source; a display -summary is not an input to source serialization. Combining `--thin` with an +provider and the File Markdown projection retain the complete source. SQLite +renders a bounded active view and includes an exact Todo query for any omitted +fields. Combining `--thin` with an exact Todo ID is rejected so a bounded inventory projection cannot replace a full requirement read. No new frontend setting, Lark command or parallel state store is introduced. This change stays in the permanent Python Markdown/legacy @@ -256,8 +257,9 @@ their existing closed diagnostics. 上限。显式标题冲突仍拒绝,即使前 500 字符相同;不能靠缩短已提交任务来修显示。 状态、未过滤的 `todo list`、未指定精确 ID 的 `todo list --thin`、前端及聊天继续使用原有 有界摘要。`todo list --todo-id ID` 返回完整的当前或保留请求文本;同时指定 `--thin` 时会 -拒绝请求,避免有界摘要替代完整需求读取。canonical provider 及重建后的活动状态保留完整 -源文本,不新增设置、Lark 命令或第二状态库。本次仅修既有 Python Markdown/兼容 I/O,TS +拒绝请求,避免有界摘要替代完整需求读取。canonical provider 和 File Markdown 投影保留完整 +源文本。SQLite 渲染有界活动视图,为被省略的字段附上精确 Todo 查询。不新增设置、Lark 命令 +或第二状态库。本次仅修既有 Python Markdown/兼容 I/O,TS 的权威、准入和交付确认 owner 不变。 若业务已提交、长文本一致性失败导致显示 pending,先读取当前 provider revision, diff --git a/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py b/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py index 3a39e788c9..7714be24a8 100644 --- a/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py +++ b/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py @@ -56,6 +56,10 @@ def hierarchy_scan_line(path: Path, line: str) -> str: if (path == REPO_ROOT / "docs/architecture/rfcs/controller-executor-unified-contract-v0.md" and line.strip() == 'the current single Agent-scoped slot. One controller owns control dialogue;'): return line.replace("controller owns", "native session role owns") + # Benchmark recovery assigns retries to the experiment provider or controller. + if (path == REPO_ROOT / "loopx/capabilities/benchmark_toolkit/README.md" + and line.strip() == "grading alive until it drains. The provider/controller owns bounded retry after"): + return line.replace("provider/controller owns", "benchmark provider or controller handles") return line @@ -79,6 +83,7 @@ def main() -> int: "handoff_agent", "agent_profile_v0", "primary_checkout", + "controller owns", ): assert LEGACY_PATTERN.search(token), token for domain_term in ("primary_reviewers", "primary_review_score"): diff --git a/examples/evaluations/chat-intake.public.json b/examples/evaluations/chat-intake.public.json index cab5259ce5..ab346a7957 100644 --- a/examples/evaluations/chat-intake.public.json +++ b/examples/evaluations/chat-intake.public.json @@ -402,6 +402,7 @@ "status": "accepted", "covers": "Microsoft cash flow FY2023–2025 with annual-report citations", "content_read": true, + "content": "# report-v2 — Microsoft cash-flow comparison, FY2023–2025\n\nPublic synthetic test artifact; the following index units are fixture data, not Microsoft's actual financial figures.\n\n| Fixture year | Operating cash flow | Capital expenditure | Difference |\n| --- | ---: | ---: | ---: |\n| FY2023 | 100 | 40 | 60 |\n| FY2024 | 110 | 50 | 60 |\n| FY2025 | 120 | 60 | 60 |\n\nIn this synthetic comparison, both series rise and operating cash flow exceeds capital expenditure in every year. Sources in this fixture: annual-report-FY2023, annual-report-FY2024, annual-report-FY2025. This is the complete accepted fixture report, ready to return with its report-v2 reference.", "observed_at": "2026-09-01T12:05:00Z" } ], diff --git a/examples/personal-workspace-browser/research-history.mjs b/examples/personal-workspace-browser/research-history.mjs index a960c11f7b..bbaaac230a 100644 --- a/examples/personal-workspace-browser/research-history.mjs +++ b/examples/personal-workspace-browser/research-history.mjs @@ -866,7 +866,7 @@ export const researchHistoryScenario = { if (response.status() === 409) return route.fulfill({response}); assert.equal(response.status(), 200); const value = await response.json(); - if (new URL(route.request().url()).searchParams.get("view") === "history") pages.push(value); + if (query.get("view") === "history") pages.push({goalId: query.get("goal_id"), packet: value}); await route.fulfill({response, json: value}); }); await page.route("**/api/chat/todo/detail?*", async route => { @@ -889,7 +889,8 @@ export const researchHistoryScenario = { const history = page.getByRole("region", {name: "研究历程", exact: true}); await history.getByRole("status").filter({hasText: "76 / 76"}).waitFor(); assert.equal(pages.length, 2, "The complete Portfolio checkpoint requires every bounded HTTP page"); - assert.deepEqual(pages.flatMap(value => value.items).map(row => row.event_id), capture.events.map(row => row.event_id)); + assert.deepEqual(pages.filter(value => value.goalId === capture.goal_id) + .flatMap(value => value.packet.items).map(row => row.event_id), capture.events.map(row => row.event_id)); assert.equal(await history.locator('.analysis-read-empty').count(), 0, 'No analysis source must not add another explanation card above the recorded history'); const sourceHelp = history.getByRole('button', {name: '分析来源未就绪', exact: true}); @@ -1006,7 +1007,10 @@ export const researchHistoryScenario = { await page.locator('.personal-goal-link',{hasText:other.display_name}).click(); assert.equal(new URL(page.url()).searchParams.get('view'), 'research', 'Comparing Goals must keep the research reading context'); await history.getByRole('status').filter({hasText:`${other.events.length} / ${other.events.length}`}).waitFor(); - assert.deepEqual(pages.flatMap(value=>value.items).map(row=>row.event_id),other.events.map(row=>row.event_id),`${label}: all original pages, without cross-Goal records`); + // A prior Goal's retry can finish after navigation. Bind observations to + // the requested Goal; foreign records inside its response still fail. + assert.deepEqual(pages.filter(value=>value.goalId===other.goal_id) + .flatMap(value=>value.packet.items).map(row=>row.event_id),other.events.map(row=>row.event_id),`${label}: all original pages, without cross-Goal records`); assert.equal(await history.locator('.research-history-detail').count(),0, 'A different Goal must not adopt the prior evidence selection'); assert.equal(await history.locator('.research-route').count(),0); await page.goBack(); diff --git a/loopx/canary/module_metric_baseline.json b/loopx/canary/module_metric_baseline.json index cd724ea7d6..3352c6fa3c 100644 --- a/loopx/canary/module_metric_baseline.json +++ b/loopx/canary/module_metric_baseline.json @@ -34,7 +34,7 @@ "dict_any_count": 0 }, "loopx/chat_agent.py": { - "lines": 2043 + "lines": 2045 }, "loopx/chat_runtime.py": { "any_count": 38, diff --git a/loopx/chat_agent.py b/loopx/chat_agent.py index 8acfe64e8a..0f87e696e2 100644 --- a/loopx/chat_agent.py +++ b/loopx/chat_agent.py @@ -452,6 +452,7 @@ def _agent_item_text(message: dict[str, Any]) -> str: "Resolve the exact object and source; an identifier in another repository, an old waiting task or a closed-but-uncompleted object is not proof of the requested outcome. " "Resolve the intended project, purpose and output destination from the user's request and relevant conversation before matching a role or existing task. A public source or broad collection responsibility does not authorize repurposing private learning notes into an operations library, publication or another project's work. " "If current evidence shows the requested outcome is already satisfied, explain that result with its source and do not create work, delegate, propose a protected action or repeat the effect. " + "Return available accepted result content in this conversation instead of asking its owner to reproduce it. A status or read marker without the requested content does not itself satisfy delivery; retrieve the missing content through a permitted read or its qualified original owner. " "A saved source, read receipt or recorded proposal is not proof that the requested outcome works. Separate source duplication, applicable new information and independently verified completion; an unchanged source may still expose unfinished work, while changed bytes may add no useful information. " "A request for explanation, fact checking, comparison or judgment normally needs your analysis, not automatic assignment. " "Resolve question-form feedback against the whole conversation and any work already owed. A concrete amendment to established delivery still needs completion here or transfer to its established owner under the current work rules; do not end at agreement or design advice. Preserve standalone explanation and opinion requests as answers, without automatic delegation. " @@ -461,6 +462,7 @@ def _agent_item_text(message: dict[str, Any]) -> str: "Resolve ordinary shorthand against the requested subject, project, purpose and destination. A role alias or matching Agent id is a discovery lead, not a selected recipient. Compare plausible recipients using the permitted directory and relevant current work; when the user means recent research, inspect relevant recent deliveries as well. Disclose a material assumption; ask only when competing interpretations would change the action. Do not ask the user to supply a link or Agent id you can resolve or have an authorized qualified recipient verify. " "A previous handoff proves where a request was sent, not who owns its subject. When feedback challenges the recipient or corrects the subject, verify that route before preserving an alleged established owner. " "An explicit Goal/Agent identity or owner correction takes precedence over a similar role name. Preserve the full goal_id/agent_id pair. " + "Do not choose among equally relevant work items because an identifier or its translation repeats their shared topic. Without conversation evidence or an explicit user selection distinguishing their subjects, ask one focused question and keep context_handoff=null. " "When role names repeat, compare the relevant conversation, established assignment, project and declared responsibilities before deciding that the recipient is ambiguous. Registered host bindings can support that context, but a missing binding does not prove an Agent is offline and an available route cannot override an explicit identity or grant. " "A corrected handoff does not retract a delivered misroute. Check the original and correction receipts; reuse delivered corrections, and carry any still-needed bounded ownership correction to the original recipient through the existing collaboration path. Preserve actual effects and history; do not claim cancellation or interrupt unrelated work. " "For forwarded or quoted requests, preserve the original speaker and addressee: 'you' does not automatically refer to the receiving Agent. Explain the resolved referent in the existing collaboration brief when it matters; do not invent one. " @@ -654,14 +656,14 @@ def _turn_prompt( "Then append exactly one machine-readable envelope with message set to the empty string: the transport reuses the visible answer before the opening tag. Do not repeat the answer in JSON. This envelope is hidden protocol metadata and is required even for ordinary questions or exact-wording replies; user formatting instructions govern the visible answer, not omission of this metadata. " "protected_action must be null or an object shaped as " '{"operation":"merge|release|deploy|delete|payment","target":"user-stated target","summary":"short public-safe proposal"}. ' - "Do not write anything after the closing tag. Use these tags and shape:\n" - f"{CHAT_REVIEW_OPEN_TAG}{json.dumps(envelope, ensure_ascii=False)}{CHAT_REVIEW_CLOSE_TAG}\n\n" + ( f"LoopX context (supporting context only):\n{context_summary.strip()}\n\n" if context_summary.strip() else "" ) - + f"Operator user message:\n{user_message}" + + f"Operator user message:\n{user_message}\n\n" + + "After the visible answer, append the envelope below. Use both tags verbatim, including when returning an existing report. Write nothing after the closing tag.\n" + f"{CHAT_REVIEW_OPEN_TAG}{json.dumps(envelope, ensure_ascii=False)}{CHAT_REVIEW_CLOSE_TAG}" ) diff --git a/loopx/cli_commands/project_lifecycle_refresh_state.py b/loopx/cli_commands/project_lifecycle_refresh_state.py index 8da4de7164..e535120fbd 100644 --- a/loopx/cli_commands/project_lifecycle_refresh_state.py +++ b/loopx/cli_commands/project_lifecycle_refresh_state.py @@ -81,6 +81,7 @@ stage_reward_memory_outcome_candidate, ) from ..control_plane.quota.cli_projection import compact_refresh_state_cli_payload +from .refresh_settlement import settle_requested_refresh def register_refresh_state_command( @@ -882,7 +883,7 @@ def handle_refresh_state_command( ) ) if not material_refresh_ready: - _settle_requested_refresh(payload, args, registry_path, goal_ref) + settle_requested_refresh(payload, args, registry_path, goal_ref) print_payload(compact_refresh_state_cli_payload(payload) if fmt == "json" else payload, fmt, render_state_refresh_markdown) return 0 if payload.get("ok") else 1 if payload.get("explore_result") is not None: @@ -972,32 +973,6 @@ def handle_refresh_state_command( ) ), ) - _settle_requested_refresh(payload, args, registry_path, goal_ref) + settle_requested_refresh(payload, args, registry_path, goal_ref) print_payload(compact_refresh_state_cli_payload(payload) if fmt == "json" else payload, fmt, render_state_refresh_markdown) return 0 if payload.get("ok") else 1 - - -def _settle_requested_refresh(payload, args, registry_path, goal_ref): - if not getattr(args, "settle_turn", False): - return - from .refresh_settlement import present_settled_refresh, settle_refreshed_turn - - runtime_root = resolve_runtime_root(load_registry(registry_path), args.runtime_root) - settle_refreshed_turn(payload, registry_path=registry_path, runtime_root=runtime_root, - goal_id=args.goal_id, agent_id=args.agent_id, todo_id=args.todo_id, - replan_obligation_id=args.replan_obligation_id, - turn_instance_id=args.turn_instance_id, goal_ref=goal_ref, - delivery_workspace_path=( - Path(args.delivery_workspace_path) - if args.delivery_workspace_path else None - )) - if getattr(args, "settlement_receipt", False): - from ..control_plane.quota.error_codes import QuotaDecisionTransportError - try: - view = present_settled_refresh(payload, runtime_root=runtime_root) - except QuotaDecisionTransportError as exc: - payload.update(ok=False, error=str(exc)) - return # Exact committed receipts stay visible; never re-admit. - if view is not payload: - payload.clear() - payload.update(view) diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 5c8d0cdbf1..25d8faf845 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -5,7 +5,6 @@ import hashlib import json import os -import shlex import socket import stat import subprocess @@ -29,7 +28,10 @@ native_context_page_budget, ) from .quota_context_reader import ( - _capture_page, + _capture_selection_transport_template, + bind_capture_selection_transport as bind_capture_selection_transport, + _saved_context_read_page, + _context_page_output_enabled, _capture_read_reference, _native_context_read_bytes, _native_context_reference, @@ -265,74 +267,6 @@ def progressive_context_capture_view(payload: dict[str, Any], args: argparse.Nam return readable if page_attached or len(_native_output_bytes(readable)) < len(_native_output_bytes(required)) else required -def _bounded_initial_page(navigation: dict[str, Any], plan: dict[str, Any], *, - page_bytes: int, output_bytes: int, reader_argv: list[str]) -> dict[str, Any] | None: - """Try the typed opening and keep the remainder pinned to the same capture.""" - if "opening" not in plan or "first_read_command" not in navigation: - return None - try: - path, _, document = _read_capture(plan["capture"]["path"], plan["capture"]["sha256"]) - digest = plan["capture"]["sha256"] - opening, content = write_initial_context_read_reference(path, digest, document, plan["opening"]["selections"]) - if len(content) > page_bytes or len(content) >= output_bytes: - return None - remaining, _ = write_initial_context_read_reference(path, digest, document, plan["remaining"]["selections"]) - - def command(reference: Path) -> str: - return context_capture_read_command(path, digest, page_bytes=page_bytes, - max_page_output_bytes=output_bytes, reference_file=reference, reader_argv=reader_argv) - - navigation = {**navigation, "first_read_command": command(opening), "remaining_read_command": command(remaining), - "instruction": ("This opening covers the decision and read obligations. Before work, read its content, " - "then follow remaining_read_command and its cursor through the rest of the initial selection. " - + navigation["instruction"])} - page = _capture_page(path, content, digest, offset=0, size=len(content), output_limit=None, - instruction=False, select=[], reference_file=opening, reader_argv=reader_argv) - # An opening is useful only as a complete, independently readable - # page. Keep the ordinary cursor when even the opening cannot fit. - page["read_instruction"] = ( - "EOF ends the opening. Before work, read its content and follow navigation.remaining_read_command " - "and its next_command cursor through the remaining initial context. Both views pin the same capture. " - "EOF does not establish retention, authority or freshness.") - page["navigation"] = navigation - return page if len(_native_output_bytes(page)) <= output_bytes else None - except (OSError, ValueError): - return None # The same-capture ordinary reader is already available. - - -def _saved_context_read_page(args: argparse.Namespace, navigation: dict[str, Any], *, - initial_reference: str | None, initial_read_plan: dict[str, Any] | None, - output_limit: int | None, reader_argv: list[str]) -> dict[str, Any]: - """Use the ordinary verified reader; the first page and navigation share its output budget.""" - recovery = navigation["recovery"] - pinned = initial_reference if args.initial_read else None - reference = pinned or recovery.get("reference_file") - read_args = argparse.Namespace(**{**vars(args), "quota_command": "context-read", - "reference_file": reference, "capture_file": None if reference else recovery["path"], - "capture_sha256": None if reference else recovery["sha256"], - "max_page_output_bytes": output_limit, - "codex_context_dir": None, "codex_context_root": None, "initial_read": bool(args.initial_read and not pinned)}) - page = read_context_capture(read_args, presentation={"navigation": navigation}, reader_argv=reader_argv) - if page["ok"]: - if (pinned and initial_read_plan and output_limit is not None and not page["eof"] - and "previously_delivered_fields" not in page - and (opening := _bounded_initial_page(navigation, initial_read_plan, - page_bytes=args.page_bytes if args.page_bytes is not None else output_limit, - output_bytes=output_limit, reader_argv=reader_argv)) is not None): - return opening - # Keep the already-budgeted page and cursor ahead of recovery metadata. - page["navigation"] = page.pop("navigation") - elif "recovery_command" not in page: - page["recovery_command"] = recovery["command"] - return page - - -def _context_page_output_enabled(args: argparse.Namespace, activation: dict[str, Any]) -> bool: - return activation["enabled"] or ( - args.quota_command == "context-project" and getattr(args, "initial_read", False) - and activation["progressive_enabled"]) - - def project_saved_context_capture(args: argparse.Namespace, *, hook_session: str | None = None) -> dict[str, Any]: """Present an immutable already-decided quota capture, without authority IO. @@ -445,12 +379,13 @@ def project_saved_context_capture(args: argparse.Namespace, *, hook_session: str presentation = payload if args.initial_read and activation["progressive_enabled"] and not complete: from .quota_presentation import _project_quota_cli_payload + from ..control_plane.quota.turn_envelope import project_captured_replan_cli_view - # Match the ordinary selection guard's view while keeping the - # digest-bound original available for detail recovery. + # Keep the saved bytes intact. Stage unstaged readers against the + # exact path just verified here, including a digest-bound copy. presentation = _project_quota_cli_payload( - payload, argparse.Namespace(quota_command="should-run"), frozenset(), None, - captured_decision_path=str(path), + project_captured_replan_cli_view(payload, captured_decision_path=str(path)), + argparse.Namespace(quota_command="should-run"), frozenset(), None, ) if "host_poll_receipt" in payload: presentation["host_poll_receipt"] = payload["host_poll_receipt"] @@ -711,77 +646,6 @@ def fits(value: dict[str, object]) -> bool: ) from exc -def _capture_selection_transport_template(payload: dict[str, Any], args: argparse.Namespace, *, - capture_root: Path) -> str | None: - """Derive display argv without editing the captured decision.""" - interaction = payload.get("interaction_contract") - cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None - selection = cli.get("selection_command") if isinstance(cli, dict) else None - if not isinstance(selection, dict): - return None - presentation = {**payload, "interaction_contract": {**interaction, - "cli_channel": {**cli, "selection_command": dict(selection)}}} - bind_capture_selection_transport(presentation, args, capture_root=capture_root) - return presentation["interaction_contract"]["cli_channel"]["selection_command"].get("command_args_template") - - -def bind_capture_selection_transport(payload: dict[str, Any], args: argparse.Namespace, *, - capture_root: Path | None = None, - capture_directory: Path | None = None) -> None: - """Retain caller display/capture options in the existing selection command. - - Only transport argv is composed here; the typed quota owner still selects the - action and owns admission. Each selection invocation allocates a new capture. - """ - root = getattr(args, "decision_output_root", None) - # A prepared reference binds one immutable directory. Same-Turn - # selection uses its parent to create a fresh sibling capture. - if capture_directory is not None and getattr(args, "prepared_capture_reference", False): - capture_root = capture_directory.parent - if root is None and capture_root is None: - return - interaction = payload.get("interaction_contract") - cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None - selection = cli.get("selection_command") if isinstance(cli, dict) else None - if not isinstance(selection, dict): - return - template = selection.get("command_args_template") - if not isinstance(template, str): - return - words = shlex.split(template) - options: dict[str, str] = {} - # A saved guard already carries its capture root. Preserve it unless this - # caller explicitly selects another; otherwise use the validated cache. - if root is not None or "--decision-output-root" not in words: - selected_root = root if root is not None else capture_root - options["--decision-output-root"] = str(Path(selected_root).expanduser().absolute()) - if getattr(args, "decision_handoff_socket", None): - options["--decision-handoff-socket"] = args.decision_handoff_socket - # Selection is part of the same Turn, including a caller's explicit opt-out. - if getattr(args, "context_delivery", None) is not None: - options["--context-delivery"] = args.context_delivery - if getattr(args, "delta_delivery", None) is not None: - options["--delta-delivery"] = args.delta_delivery - if getattr(args, "codex_context_dir", None): - options["--codex-context-dir"] = args.codex_context_dir - if getattr(args, "codex_context_root", None): - options["--codex-context-root"] = args.codex_context_root - if getattr(args, "max_page_output_bytes", None) is not None: - options["--max-page-output-bytes"] = str(args.max_page_output_bytes) - replaced = set(options) - if replaced & {"--codex-context-dir", "--codex-context-root"}: - replaced.update(("--codex-context-dir", "--codex-context-root")) - for option in replaced: - while option in words: - index = words.index(option) - del words[index:index + 2] - for option, value in options.items(): - words.extend((option, value)) - if getattr(args, "turn_envelope", False) and "--turn-envelope" not in words: - words.append("--turn-envelope") - selection["command_args_template"] = shlex.join(words) - - def handoff_decision(directory: Path | None, socket_path: str | None, context_delivery_enabled: bool = False) -> tuple[dict[str, Any], str] | None: """Withhold the original output until the Host releases these exact bytes. diff --git a/loopx/cli_commands/quota_context_reader.py b/loopx/cli_commands/quota_context_reader.py index a7e51ed16e..2b503cf58d 100644 --- a/loopx/cli_commands/quota_context_reader.py +++ b/loopx/cli_commands/quota_context_reader.py @@ -20,6 +20,7 @@ read_context_capture_file as _read_capture, read_context_read_reference, write_context_read_reference, + write_initial_context_read_reference, ) from ..control_plane.turn_driver.native_context_profile import ( NATIVE_CONTEXT_PAGE_BYTES, @@ -28,6 +29,78 @@ ) +def _capture_selection_transport_template(payload: dict[str, Any], args: argparse.Namespace, *, + capture_root: Path) -> str | None: + """Derive display argv without editing the captured decision.""" + interaction = payload.get("interaction_contract") + cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None + selection = cli.get("selection_command") if isinstance(cli, dict) else None + if not isinstance(selection, dict): + return None + presentation = {**payload, "interaction_contract": {**interaction, + "cli_channel": {**cli, "selection_command": dict(selection)}}} + bind_capture_selection_transport(presentation, args, capture_root=capture_root) + return presentation["interaction_contract"]["cli_channel"]["selection_command"].get("command_args_template") + + +def bind_capture_selection_transport(payload: dict[str, Any], args: argparse.Namespace, *, + capture_root: Path | None = None, + capture_directory: Path | None = None) -> None: + """Retain caller display/capture options in the existing selection command. + + Only transport argv is composed here; the typed quota owner still selects the + action and owns admission. Each selection invocation allocates a new capture. + """ + root = getattr(args, "decision_output_root", None) + # A prepared reference binds one immutable directory. Same-Turn + # selection uses its parent to create a fresh sibling capture. + if capture_directory is not None and getattr(args, "prepared_capture_reference", False): + capture_root = capture_directory.parent + if root is None and capture_root is None: + return + interaction = payload.get("interaction_contract") + cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None + selection = cli.get("selection_command") if isinstance(cli, dict) else None + if not isinstance(selection, dict): + return + template = selection.get("command_args_template") + if not isinstance(template, str): + return + words = shlex.split(template) + options: dict[str, str] = {} + # A saved guard already carries its capture root. Preserve it unless this + # caller explicitly selects another; otherwise use the validated cache. + if root is not None or "--decision-output-root" not in words: + selected_root = root if root is not None else capture_root + options["--decision-output-root"] = str(Path(selected_root).expanduser().absolute()) + if getattr(args, "decision_handoff_socket", None): + options["--decision-handoff-socket"] = args.decision_handoff_socket + # Selection is part of the same Turn, including a caller's explicit opt-out. + if getattr(args, "context_delivery", None) is not None: + options["--context-delivery"] = args.context_delivery + if getattr(args, "delta_delivery", None) is not None: + options["--delta-delivery"] = args.delta_delivery + if getattr(args, "codex_context_dir", None): + options["--codex-context-dir"] = args.codex_context_dir + if getattr(args, "codex_context_root", None): + options["--codex-context-root"] = args.codex_context_root + if getattr(args, "max_page_output_bytes", None) is not None: + options["--max-page-output-bytes"] = str(args.max_page_output_bytes) + replaced = set(options) + if replaced & {"--codex-context-dir", "--codex-context-root"}: + replaced.update(("--codex-context-dir", "--codex-context-root")) + for option in replaced: + while option in words: + index = words.index(option) + del words[index:index + 2] + for option, value in options.items(): + words.extend((option, value)) + if getattr(args, "turn_envelope", False) and "--turn-envelope" not in words: + words.append("--turn-envelope") + selection["command_args_template"] = shlex.join(words) + + + 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, @@ -507,3 +580,71 @@ def _native_context_read_bytes(reference: dict[str, Any], raw: bytes, *, if page["eof"] or (stop_after is not None and total > stop_after): return total offset = page["next_offset"] + + +def _bounded_initial_page(navigation: dict[str, Any], plan: dict[str, Any], *, + page_bytes: int, output_bytes: int, reader_argv: list[str]) -> dict[str, Any] | None: + """Try the typed opening and keep the remainder pinned to the same capture.""" + if "opening" not in plan or "first_read_command" not in navigation: + return None + try: + path, _, document = _read_capture(plan["capture"]["path"], plan["capture"]["sha256"]) + digest = plan["capture"]["sha256"] + opening, content = write_initial_context_read_reference(path, digest, document, plan["opening"]["selections"]) + if len(content) > page_bytes or len(content) >= output_bytes: + return None + remaining, _ = write_initial_context_read_reference(path, digest, document, plan["remaining"]["selections"]) + + def command(reference: Path) -> str: + return context_capture_read_command(path, digest, page_bytes=page_bytes, + max_page_output_bytes=output_bytes, reference_file=reference, reader_argv=reader_argv) + + navigation = {**navigation, "first_read_command": command(opening), "remaining_read_command": command(remaining), + "instruction": ("This opening covers the decision and read obligations. Before work, read its content, " + "then follow remaining_read_command and its cursor through the rest of the initial selection. " + + navigation["instruction"])} + page = _capture_page(path, content, digest, offset=0, size=len(content), output_limit=None, + instruction=False, select=[], reference_file=opening, reader_argv=reader_argv) + # An opening is useful only as a complete, independently readable + # page. Keep the ordinary cursor when even the opening cannot fit. + page["read_instruction"] = ( + "EOF ends the opening. Before work, read its content and follow navigation.remaining_read_command " + "and its next_command cursor through the remaining initial context. Both views pin the same capture. " + "EOF does not establish retention, authority or freshness.") + page["navigation"] = navigation + return page if len(_native_output_bytes(page)) <= output_bytes else None + except (OSError, ValueError): + return None # The same-capture ordinary reader is already available. + + +def _saved_context_read_page(args: argparse.Namespace, navigation: dict[str, Any], *, + initial_reference: str | None, initial_read_plan: dict[str, Any] | None, + output_limit: int | None, reader_argv: list[str]) -> dict[str, Any]: + """Use the ordinary verified reader; the first page and navigation share its output budget.""" + recovery = navigation["recovery"] + pinned = initial_reference if args.initial_read else None + reference = pinned or recovery.get("reference_file") + read_args = argparse.Namespace(**{**vars(args), "quota_command": "context-read", + "reference_file": reference, "capture_file": None if reference else recovery["path"], + "capture_sha256": None if reference else recovery["sha256"], + "max_page_output_bytes": output_limit, + "codex_context_dir": None, "codex_context_root": None, "initial_read": bool(args.initial_read and not pinned)}) + page = read_context_capture(read_args, presentation={"navigation": navigation}, reader_argv=reader_argv) + if page["ok"]: + if (pinned and initial_read_plan and output_limit is not None and not page["eof"] + and "previously_delivered_fields" not in page + and (opening := _bounded_initial_page(navigation, initial_read_plan, + page_bytes=args.page_bytes if args.page_bytes is not None else output_limit, + output_bytes=output_limit, reader_argv=reader_argv)) is not None): + return opening + # Keep the already-budgeted page and cursor ahead of recovery metadata. + page["navigation"] = page.pop("navigation") + elif "recovery_command" not in page: + page["recovery_command"] = recovery["command"] + return page + + +def _context_page_output_enabled(args: argparse.Namespace, activation: dict[str, Any]) -> bool: + return activation["enabled"] or ( + args.quota_command == "context-project" and getattr(args, "initial_read", False) + and activation["progressive_enabled"]) diff --git a/loopx/cli_commands/refresh_settlement.py b/loopx/cli_commands/refresh_settlement.py index 6cf4019eaa..e9cbd28847 100644 --- a/loopx/cli_commands/refresh_settlement.py +++ b/loopx/cli_commands/refresh_settlement.py @@ -18,6 +18,34 @@ ) +def settle_requested_refresh(payload, args, registry_path, goal_ref): + """Keep refresh's optional settlement composition with its existing owner.""" + if not getattr(args, "settle_turn", False): + return + from ..history import load_registry + from ..paths import resolve_runtime_root + + runtime_root = resolve_runtime_root(load_registry(registry_path), args.runtime_root) + settle_refreshed_turn(payload, registry_path=registry_path, runtime_root=runtime_root, + goal_id=args.goal_id, agent_id=args.agent_id, todo_id=args.todo_id, + replan_obligation_id=args.replan_obligation_id, + turn_instance_id=args.turn_instance_id, goal_ref=goal_ref, + delivery_workspace_path=( + Path(args.delivery_workspace_path) + if args.delivery_workspace_path else None + )) + if getattr(args, "settlement_receipt", False): + from ..control_plane.quota.error_codes import QuotaDecisionTransportError + try: + view = present_settled_refresh(payload, runtime_root=runtime_root) + except QuotaDecisionTransportError as exc: + payload.update(ok=False, error=str(exc)) + return # Exact committed receipts stay visible; never re-admit. + if view is not payload: + payload.clear() + payload.update(view) + + def settle_refreshed_turn( payload: dict[str, Any], *, registry_path: Path, runtime_root: Path, goal_id: str, agent_id: str, todo_id: str | None, diff --git a/loopx/control_plane/goals/vision_checkpoint.ts b/loopx/control_plane/goals/vision_checkpoint.ts index 3968dfc40a..30c9cac5b7 100644 --- a/loopx/control_plane/goals/vision_checkpoint.ts +++ b/loopx/control_plane/goals/vision_checkpoint.ts @@ -94,15 +94,17 @@ export function visionAuthoringContract(autonomousReplan = false): JsonObject { schema_version: GOAL_VISION_REPLAN_SCHEMA_VERSION, state: "vision_patch_proposed", vision_patch: {acceptance_summary: "Verified result; scoped work remains."}, path_delta: { + schema_version: GOAL_PATH_DELTA_SCHEMA_VERSION, outcome: autonomousReplan ? "replan" : "continue", prior_assumption: "Viable route.", observed_reality: "Evidence supports the route.", retained: ["Route"], evidence_refs: ["evidence:verified-result"], }, }, - authoring_hint: "Replace example claims/refs with evidence; obey the live contract and total limit. " + - "For todo_delta, use retain:todo_ for existing work; supported verbs are activate, create, reopen, resume and retain. " + - "Replace todo_ with a verified Todo ID after normal authoring/readback. Entries record plans, not Todo mutations, claims or execution authority. " + - "For ordinary CLI writeback, pass the packet with --agent-vision-json . " + + authoring_hint: "Adapt the example to observed evidence. Use its public-safe IDs or aliases in evidence_refs. " + + "Keep raw paths with the artifact. Follow the live contract and total limit. " + + "Use retain:todo_ for existing work in todo_delta. Verbs are activate, create, reopen, resume and retain. " + + "Use a verified Todo ID after normal authoring/readback. These entries describe plans and grant no Todo mutation, claim or execution authority. " + + "For ordinary CLI writeback, pass --agent-vision-json . " + (autonomousReplan ? "JSON replaces the complete packet. For an existing Vision, the sparse minimal_example shows syntax. " + "Use the current Agent's complete packet and full vision_patch. Read canonical status/history if unavailable. Never reconstruct intent from summaries. " + diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 6d971e43f0..eafe9814f0 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -547,7 +547,7 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include("agent_lane_next_action", "next_step"); include("agent_lane_next_action", "next_action_basis"); include("interaction_contract", "user_channel"); - for (const field of ["primary_action", "delivery_allowed", "must_attempt", "quiet_noop_allowed"]) + for (const field of ["primary_action", "delivery_allowed", "must_attempt", "quiet_noop_allowed", "next_task_action"]) include("interaction_contract", "agent_channel", field); const turnBindingRequired = object(interaction.cli_channel)?.turn_binding_required === true; if (turnBindingRequired) @@ -691,7 +691,14 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly if (!turnBindingRequired) for (const field of ["execution_preparation", "next_cli_actions", "spend_allowed_now", "spend_after_validation", "spend_policy", "quota_spend_source", "delivery_workspace_causality", "vision_continuation_audit"]) - if (field !== "next_cli_actions" || !repeatsSettlement) include("interaction_contract", "cli_channel", field); + if (field !== "next_cli_actions" || !repeatsSettlement) { + const before = pointers.length; + include("interaction_contract", "cli_channel", field); + // A task-facing operation and its declared continuation belong in the + // same opening. Keep the existing exact settlement deduplication. + if (field === "next_cli_actions" && object(channel?.next_task_action) && pointers.length > before) + opening.push(pointers.pop()!); + } // Explicit selection clears next_cli_actions. Deliver the owner's discovery // and same-Turn binding recipe alongside its non-exhaustive suggestions. if (cli?.selection_required === true) { diff --git a/loopx/control_plane/quota/settlement_plan.ts b/loopx/control_plane/quota/settlement_plan.ts index bd939a204b..291154a717 100644 --- a/loopx/control_plane/quota/settlement_plan.ts +++ b/loopx/control_plane/quota/settlement_plan.ts @@ -31,13 +31,14 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan "surface_only cannot settle this Turn. Do not relabel preparation-only work as progress; " + "continue authorized work within existing limits or report a genuine typed blocker. " + "Set --delivery-boundary " + DELIVERY_BOUNDARIES.join("|") + " for the applicable case above. " + - "Typed evidence: --progress-result-class " + PROGRESS_RESULT_CLASSES.join("|") + - ". For no_followup or exploration_exhausted, also pass --progress-coverage-scope-id; " + - "exploration_exhausted also needs --progress-coverage-complete. " + - "Todo completion alone does not establish scope coverage" + - ". For blocked outcome_gap, --progress-work-item-id must match this plan's Todo/obligation id " + - "and --progress-blocker-id names the blocker. Repeat --progress-evidence-id for evidence. " + - "IDs are 1-128 characters, start with an ASCII letter/digit, then letters/digits or ._:/- " + + "Add typed progress flags only when this Turn's evidence supports the observation. " + + "Use --progress-result-class " + PROGRESS_RESULT_CLASSES.join("|") + + ". For no_followup or exploration_exhausted, pass --progress-coverage-scope-id from verified scope coverage; " + + "exploration_exhausted also needs --progress-coverage-complete. For a final Todo, terminal closeout follows spend " + + "under its own plan step. A typed no_followup requires scope coverage beyond Todo completion. " + + "For blocked outcome_gap, --progress-work-item-id must match this plan's Todo/obligation id " + + "and --progress-blocker-id names the blocker. Repeat --progress-evidence-id for observed evidence identifiers. " + + "Use opaque public-safe IDs of 1-128 characters, starting with an ASCII letter/digit, then letters/digits or ._:/- " + "(e.g. evidence:validation-1); an ID does not upload a file. " + "Use --agent-vision-json for the material vision packet; " + "--vision-unchanged-reason only reuses an already evidence-linked vision. " + @@ -57,6 +58,8 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan const inFlight = params.delivery_boundary === "in_flight_continuation"; const autonomousReplan = (params.autonomous_replan === undefined ? false : requireBoolean(params.autonomous_replan, "autonomous_replan")) || identity.binding_kind === "autonomous_replan"; + const todoSemanticWriteback = identity.binding_kind === "todo" && !autonomousReplan + ? semanticWriteback : null; const mode = params.handoff_mode == null ? null : requireStringLiteral(params.handoff_mode, HANDOFF_MODES, "handoff_mode"); const managedClaim = identity.binding_kind === "todo" && mode === "hard_lease"; @@ -93,9 +96,10 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan (autonomousReplan ? "Autonomous replan uses --delivery-boundary semantic_closeout even while its Todo remains open. " + "Bind new decision evidence from this admitted work to its artifact/input; follow the remaining plan, " + "then yield for freshly guarded continuation. " - : (inFlight ? "Choose one: command_template for ordinary in_flight_continuation without new decision evidence; " - : "Use command_template for semantic_closeout; ") + - (semanticWriteback !== null ? "semantic_closeout_command_template" : "--delivery-boundary semantic_closeout") + + : (inFlight ? "Use command_template for ordinary in_flight_continuation without new decision evidence. " : "") + + "Use " + (todoSemanticWriteback !== null + ? "semantic_closeout_command_template with --agent-vision-json" + : "--delivery-boundary semantic_closeout") + " for new decision evidence, route change or Todo completion. " + "Bind new evidence produced or obtained by this admitted work to its artifact/input in fresh Vision/path_delta " + "with --agent-vision-json, including continue/no_change when retaining the route. " + @@ -111,8 +115,7 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan "An already committed Monitor poll needs neither refresh nor spend." + inputHint, idempotency_key_ref: "$.identity.effect_id", expected_receipt: "durable_writeback_receipt", command_template: writeback, - ...(identity.binding_kind === "todo" && !autonomousReplan && semanticWriteback !== null - ? {semantic_closeout_command_template: semanticWriteback} : {}), + ...(todoSemanticWriteback !== null ? {semantic_closeout_command_template: todoSemanticWriteback} : {}), // The same open Todo can reach a semantic feedback checkpoint. Deliver // its existing authoring contract with the executable command variant. // Autonomous replan already carries this owner in its writeback contract. diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 7706090d29..81f0f520d1 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -62,14 +62,15 @@ def _apply_context_projection_changes( try: for change in result["changes"]: path = change["path"] - if not isinstance(path, list) or not path: - raise ValueError("projection path must be nonempty") + if not isinstance(path, list): + raise ValueError("projection path must be a list") + if not path and "remove" in change: + raise ValueError("projection root cannot be removed") parent: Any = projected for key in path[:-1]: parent = parent[key] - key = path[-1] if "remove" in change: - del parent[key] + del parent[path[-1]] continue if "from" in change: value: Any = source @@ -77,6 +78,12 @@ def _apply_context_projection_changes( value = value[part] else: value = change["value"] + if not path: + if not isinstance(value, dict): + raise ValueError("projection root must be an object") + projected = deepcopy(value) + continue + key = path[-1] if isinstance(parent, list) and key == len(parent): parent.append(deepcopy(value)) else: @@ -356,11 +363,14 @@ def bind_turn_decision_capture( ) -def project_captured_replan_cli_view(payload: dict[str, Any]) -> dict[str, Any]: - """Stage replan reads without round-tripping unchanged source values.""" +def project_captured_replan_cli_view( + payload: dict[str, Any], *, captured_decision_path: str | None = None, +) -> dict[str, Any]: + """Stage replan reads against the verified capture without rewriting its source.""" if not isinstance(payload.get("replan_read_path"), str): return payload - result = _turn_envelope_result("replan_cli_view", payload=payload, source_edits=True) + result = _turn_envelope_result("replan_cli_view", payload=payload, source_edits=True, + **({"captured_decision_path": captured_decision_path} if captured_decision_path is not None else {})) return _apply_context_projection_changes(payload, result) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 5d6465e308..649ffaad59 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2379,7 +2379,12 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { const precision = replanReadPrecision(payload, request.source_number_tokens); const sourceEdits = request.source_edits === true; const identity = precision?.identity ?? (sourceEdits ? contextSourceIdentity(payload) : undefined); - const view = replanCliView(payload, identity, undefined, precision); + const packet = object(payload.replan_action_packet); + // A staged capture has already pinned its reader to a checked source. + // An unstaged copy needs the path verified by the current capture reader. + const view = packet.review !== undefined || packet.writeback !== undefined ? payload + : replanCliView(payload, identity, replanReadCommands(payload, + request.captured_decision_path ?? payload.replan_read_path), precision); return sourceEdits ? contextProjectionChanges(payload, view, precision?.values) : view; } if (operation === "compose_complete") { diff --git a/loopx/control_plane/testing/doubao_model_behavior_actor.py b/loopx/control_plane/testing/doubao_model_behavior_actor.py index e5cc483e51..d906370ec9 100644 --- a/loopx/control_plane/testing/doubao_model_behavior_actor.py +++ b/loopx/control_plane/testing/doubao_model_behavior_actor.py @@ -459,7 +459,11 @@ def _onboarding_decision_instruction(phase: str) -> str: } Copy the requested phase exactly. Output JSON only, without markdown or reasoning. The semantic_contract must contain exactly the phase-specific fields -described below. Use null where instructed and never invent identifiers.""" +described below. Use null where instructed and never invent identifiers. +reason_codes is required and must contain 1 to 12 distinct, non-empty strings. +Each reason code must match ^[A-Za-z0-9][A-Za-z0-9_.:@-]{0,159}$; use short +English tokens derived from the observed packet. Never return an empty list, +prose, or non-ASCII reason codes, including when the observation is healthy.""" if phase == "entry": return ( common diff --git a/loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py b/loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py index c837f1bf1c..0787ac018a 100644 --- a/loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +++ b/loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py @@ -59,9 +59,12 @@ "writeback, spend, or state changes that were not executed." ) SCOPED_GATE_READ_RETRY_MESSAGE = ( - "The hermetic host rejected this read shape without executing it. Retry with " - "one bounded read-only command inside the current workspace using pwd, ls, " - "find, rg --files, cat, head, or sed." + "The hermetic host rejected this read shape without executing it; the " + "selected Todo action has not completed. Follow the latest quota result: " + "finish any still-required context read, then perform the selected Todo's " + "bounded read. When selected_todo.text names one relative file, use " + "`cat `; do not rediscover the workspace or inspect state. " + "The rejection grants no additional read scope or tool budget." ) diff --git a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py index 2515a37491..8f938eeb4b 100644 --- a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py +++ b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py @@ -717,7 +717,7 @@ def _execute_cli(command: str, state: _QualificationState) -> dict[str, Any]: vision = argument_value(tokens or [], "--agent-vision-json") if vision: draft = (state.fixture.project_root / vision).resolve() - if not draft.is_relative_to(state.fixture.project_root.resolve()) or not draft.is_file(): + if not draft.is_relative_to(state.fixture.project_root.resolve()): raise _TerminalToolViolation("vision_draft_outside_workspace") if tokens and "--format" not in tokens: command = shlex.join([tokens[0], "--format", "json", *tokens[1:]]) @@ -995,6 +995,15 @@ def _execute_terminal_cli( return output, kind +def _protected_terminal_fixture(state: _QualificationState) -> dict[str, tuple[Any, ...]]: + root = state.fixture.project_root.parent + scratch = str(state.fixture.project_root.relative_to(root) / ".scratch") + return { + path: value for path, value in snapshot_disposable_fixture(root).items() + if path != scratch and not path.startswith(scratch + "/") + } + + def _execute_terminal_workspace( command: str, state: _QualificationState, workspace_shell: VisionShellHost | None, resources: ExitStack, ) -> tuple[str, str, VisionShellHost | None]: @@ -1011,9 +1020,18 @@ def reject_nested_cli(_argv: list[str], _cwd: Path, _output: str) -> str: ) resources.callback(workspace_shell.close) state.workspace_shell_executed = True + before = _protected_terminal_fixture(state) output, exit_code = workspace_shell.execute(command) if exit_code: - raise _TerminalToolViolation("workspace_authoring_failed") + if _protected_terminal_fixture(state) != before: + raise _TerminalToolViolation("workspace_authoring_failed") + # The native exec host returns failed draft commands to the model. + # Correction consumes this same budget and grants no CLI effect. + output = json.dumps({ + "error_code": "shell_nonzero", "exit_code": exit_code, + "output": output[:4096], "truncated": len(output) > 4096, + }) + return output, "shell_nonzero", workspace_shell kind = "workspace_authoring" return output, kind, workspace_shell @@ -1079,6 +1097,14 @@ def qualify( "A receipt without command templates does not replace " "that plan. " f"{QUOTA_FIRST_TOOL_INSTRUCTION}" + "For the captured first read, use the reader's supported " + "bounded page size: replace the existing --page-bytes and " + "--max-page-output-bytes values with 64000 together. " + "Keep its original source, selection and --initial-read; " + "do not add duplicate options or read the whole capture. " + "If it is still paginated, consume next_command through eof " + "before depending on that selection. Read the complete " + "vision_authoring attachment before composing a Vision. " "The exec tool's current workspace is the connected goal " "project root, so task " "paths are directly addressable. Read the exact proof target " @@ -1161,6 +1187,9 @@ def receipt(*, passed: bool, failure_code: str | None) -> dict[str, Any]: "kind": kind if failure_code is None else failure_code, "command_digest": digest_text(tool_call.command), } + if kind == "shell_nonzero" and failure_code is None: + step_receipt["error_code"] = "shell_nonzero" + step_receipt["exit_code"] = json.loads(output)["exit_code"] if failure_code is not None: step_receipt["redacted_command_shape"] = _redacted_command_shape( tool_call.command diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 3a8f4661b3..cf6c63a147 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -603,13 +603,14 @@ def context_selection_transport(payload: dict[str, Any], select: list[str] | Non if isinstance(value, (dict, list)): subtree = branch.get(token, {}) # Length matters when validating array indices or listing children. - child: Any = {} if isinstance(value, dict) else [None] * len(value) + child: Any = {} if isinstance(value, dict) else [0] * len(value) target[key] = child pending.append((value, child, subtree, depth - 1)) else: - # Every scalar has the same address behavior. Keep original scalar - # types, precision, and byte counts at the file rendering boundary. - target[key] = None + # Every scalar has the same address behavior. A one-byte marker + # also prevents short scalar arrays growing during transport. + # Original types, precision and byte counts stay at the renderer. + target[key] = 0 return result diff --git a/loopx/semantics/vocabulary_v0.json b/loopx/semantics/vocabulary_v0.json index f4e04d71cc..659f4a17c2 100644 --- a/loopx/semantics/vocabulary_v0.json +++ b/loopx/semantics/vocabulary_v0.json @@ -273,11 +273,13 @@ }, "producers": [ "loopx/control_plane/quota/turn_envelope.ts::turnEnvelopeRoute", + "loopx/control_plane/quota/turn_envelope.ts::admittedTurnRoute", "loopx/control_plane/turn_driver/driver.py::_typed_route", "loopx/control_plane/turn_driver/driver.py::build_loopx_turn_plan" ], "return_producers": [ "loopx/control_plane/quota/turn_envelope.ts::turnEnvelopeRoute", + "loopx/control_plane/quota/turn_envelope.ts::admittedTurnRoute", "loopx/control_plane/turn_driver/driver.py::_typed_route", "loopx/control_plane/turn_driver/driver.py::build_loopx_turn_plan" ], diff --git a/packages/dsh-loopx-plugin/src/driver.ts b/packages/dsh-loopx-plugin/src/driver.ts index 8197fa4920..0b787eb6c9 100644 --- a/packages/dsh-loopx-plugin/src/driver.ts +++ b/packages/dsh-loopx-plugin/src/driver.ts @@ -1095,6 +1095,9 @@ export class LoopXContinuationDriver { signal, attempts: 3, retryDelaysMs: this.retryDelaysMs, + // The Driver reads the raw admission contract. The child-local opt-out + // also works with supported LoopX releases that predate the CLI flag. + env: { ...process.env, LOOPX_CONTEXT_DELIVERY: 'off' }, validate: payload => payload.goal_id === binding.goalId && typeof payload.ok === 'boolean' && typeof payload.should_run === 'boolean', diff --git a/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts b/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts index 3b3c2070cc..1a2eb17aef 100644 --- a/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts +++ b/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts @@ -315,7 +315,7 @@ describe('DSH admission-to-closeout integration', () => { expect(admission.kind).toBe('enter') const unselected = await runCli(testFixture, [ - 'quota', 'should-run', + 'quota', 'should-run', '--context-delivery', 'off', '--goal-id', goalId, '--agent-id', agentId, '--runtime-profile', 'generic_cli', @@ -337,7 +337,7 @@ describe('DSH admission-to-closeout integration', () => { expect(claimed.payload.ok).toBe(true) const guardArgs = [ - 'quota', 'should-run', + 'quota', 'should-run', '--context-delivery', 'off', '--goal-id', goalId, '--todo-id', todoId, '--agent-id', agentId, diff --git a/scripts/review_sla_report.py b/scripts/review_sla_report.py index 71fddd68b9..4c4c7345cb 100644 --- a/scripts/review_sla_report.py +++ b/scripts/review_sla_report.py @@ -42,7 +42,7 @@ # .github/GOVERNANCE.md. Only these accounts can satisfy the published # first-response target; tests/test_review_sla_report.py checks this list # against that table. -DEFAULT_RESPONDERS = ("huangruiteng", "steven-kid", "maxliux5", "bigdatadz", "loopx-agent", "songoow") +DEFAULT_RESPONDERS = ("huangruiteng", "steven-kid", "maxliux5", "bigdatadz", "loopx-agent", "songoow", "jasonbuildai") # Fallback only for inputs that carry no actor type (an old --input file). # GitHub's typed actors are authoritative: a Bot is never a responder even # when its login looks human, and a human whose login starts with "copilot" diff --git a/tests/architecture/package_kernel_import_pins.json b/tests/architecture/package_kernel_import_pins.json index 7f0db05546..2ddd11bc79 100644 --- a/tests/architecture/package_kernel_import_pins.json +++ b/tests/architecture/package_kernel_import_pins.json @@ -1,13 +1,14 @@ { "schema": "loopx_package_kernel_import_pins_v0", - "baseline_commit": "70bc98198ce9e8c33e461cc41fe77243d713dcf0", - "note": "Exact pin of every `loopx.*` module imported by a co-located package. Pre-existing private edges remain migration debt. The supported-host-integration decision in the monorepo-distribution-split RFC Section 5 permits MemT's two public integration imports; it does not permit private kernel imports. Remove an entry in the PR that removes the import. Adding an entry needs a recorded design decision.", + "baseline_commit": "aee77700812dd60e75375e8b329e552493da8069", + "note": "Exact pin of every `loopx.*` module imported by a co-located package. The supported-host-integration decision in the monorepo-distribution-split RFC Section 5 defines the public host facade used by MemT and Ark and the shared file-lock API. Pre-existing private edges remain pinned migration debt. Remove an entry in the PR that removes the import. Adding an entry needs a recorded design decision.", "imports": { "loopx-codex-provider-routing": [ "loopx.extensions.chat_service_guard" ], "loopx-ark-turn": [ "loopx.control_plane.turn_driver.host_candidate", + "loopx.extensions.turn_host", "loopx.file_lock" ], "loopx-finance-value-discovery": [ diff --git a/tests/architecture/top_level_module_budget.json b/tests/architecture/top_level_module_budget.json index 342ba84390..5ac0dc081a 100644 --- a/tests/architecture/top_level_module_budget.json +++ b/tests/architecture/top_level_module_budget.json @@ -1,6 +1,6 @@ { "schema": "loopx_top_level_module_budget_v0", "baseline_commit": "a4483aaece5bc74ec3f969fd067229487022401b", - "max_top_level_modules": 128, + "max_top_level_modules": 127, "allowlist": ["__init__.py", "entrypoint.py", "cli.py"] } diff --git a/tests/capabilities/test_periodic_report_pending_intent.py b/tests/capabilities/test_periodic_report_pending_intent.py index e002e54da2..37095986d1 100644 --- a/tests/capabilities/test_periodic_report_pending_intent.py +++ b/tests/capabilities/test_periodic_report_pending_intent.py @@ -102,7 +102,7 @@ def no_host(*args, **kwargs): for action, execute, host in [("plan", False, "generic-cli"), ("run-once", False, "generic-cli"), ("run-once", True, "generic-cli"), ("run-once", True, "codex-cli")]: - extras = ["--project", str(registry.parent)] if action == "run-once" else [] + extras = ["--project", str(registry.parent)] if action == "run-once" else ["--include-transaction-detail"] code = main([*prefix, "turn", action, *scope, "--host", host, "--execution-mode", "isolated-headless", *extras, *(["--execute"] if execute else [])]) @@ -114,10 +114,11 @@ def no_host(*args, **kwargs): assert turn["capability_action"]["intent"]["command"] == command assert turn["capability_action"]["command_argv"][:5] == ["loopx", "--registry", str(registry), "--runtime-root", str(runtime)] assert turn["capability_action"]["executed"] is False - assert turn["turn_envelope"]["writeback"]["next_cli_actions"] == [command] - assert turn["turn_envelope"]["replan_action_packet"] is None + work = turn["work_contract"] + assert work["effective_action"] == "governed_capability_intent" + assert work["interaction_contract"]["cli_channel"]["next_cli_actions"] == [command] assert not any(turn["effects"].values()) - disposition = decide_loop_disposition(turn_receipt=None, quota_decision=turn["turn_envelope"]) + disposition = decide_loop_disposition(turn_receipt=None, quota_decision=work) assert disposition["disposition"] == "capability_action_required" assert not disposition["launches_host"] assert not list((runtime / "goals" / GOAL_ID / "periodic_reports").glob("*/editorial_request.json")) diff --git a/tests/control_plane/legacy_acceptance_checkpoint_fixture.py b/tests/control_plane/legacy_acceptance_checkpoint_fixture.py index 05c9f250eb..05e88c1d0f 100644 --- a/tests/control_plane/legacy_acceptance_checkpoint_fixture.py +++ b/tests/control_plane/legacy_acceptance_checkpoint_fixture.py @@ -41,7 +41,9 @@ def legacy_acceptance_checkpoint_cli(project: Path, *, base_source: Path | None prefix = [sys.executable, *command, "--registry", str(project.parent / "registry.json"), "--runtime-root", str(project.parent / "runtime"), "--format", "json"] - environment = {**os.environ, "PYTHONPATH": str(source), "LOOPX_USAGE_PING": "0"} + # Both codec arms compare the raw policy and original receipt identity. + environment = {**os.environ, "PYTHONPATH": str(source), "LOOPX_USAGE_PING": "0", + "LOOPX_CONTEXT_DELIVERY": "off"} def run(*arguments): result = subprocess.run([*prefix, *arguments], cwd=source, env=environment, diff --git a/tests/control_plane/test_agent_preferences.py b/tests/control_plane/test_agent_preferences.py index 4ca7afffad..fb69b6de9a 100644 --- a/tests/control_plane/test_agent_preferences.py +++ b/tests/control_plane/test_agent_preferences.py @@ -1,18 +1,21 @@ """Production CLI -> durable store -> next-turn disclosure; no provider mocks.""" import json -import re from copy import deepcopy from pathlib import Path import pytest from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt -from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery +from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery, turn_plan_host_source, quota_action_signature_document, build_turn_envelope from tests.control_plane.test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, _run_cli, _write_fixture, ) +def _plan_authority(plan): + return extract_turn_authority(turn_plan_host_source(plan)) + + def _assert_initial_preferences(payload, directory, expected): from loopx.cli_commands.quota_capture import native_context_capture_view from tests.control_plane.test_context_first_read import read_selection @@ -67,10 +70,10 @@ def call(action, *args): assert "semantic-preference agent" not in json.dumps(payload) assert "preferences" not in payload["interaction_contract"]["agent_channel"]["work_context"]["instruction"] assert not (runtime / "agent-preferences").exists() - rc, empty_plan = _run_cli(registry, runtime, "turn", "plan", *scope) + rc, empty_plan = _run_cli(registry, runtime, "turn", "plan", "--include-transaction-detail", *scope) assert rc == 0, empty_plan assert "semantic_preference.agent_context" not in json.dumps(empty_plan) - assert "semantic-preference agent" not in render_prompt(extract_turn_authority(empty_plan)) + assert "semantic-preference agent" not in render_prompt(_plan_authority(empty_plan)) args = ("--key", "review.collaboration", "--statement", "Ask designated-reviewer before merging.", "--source-ref", "owner-message-1", "--source-quote", "Please use the designated reviewer.", "--expected-revision", "none", "--operation-id", "remember-1") @@ -89,15 +92,15 @@ def call(action, *args): assert quota["interaction_contract"]["agent_channel"]["required_reads"] == [] _assert_initial_preferences(quota, tmp_path / "active-first-read", fresh["current"]) assert quota["capability_gate"]["action"] == off["capability_gate"]["action"] - rc, plan = _run_cli(registry, runtime, "turn", "plan", *scope) + rc, plan = _run_cli(registry, runtime, "turn", "plan", "--include-transaction-detail", *scope) assert rc == 0, plan preference_read = next(x for x in reads if x["kind"] == "agent_preferences") assert len(preference_read["command"]) > 360 assert "semantic-preference agent read" in json.dumps(plan), plan - assert plan["turn_envelope"]["required_reads"] == [], plan - authority = extract_turn_authority(plan) - assert authority["required_reads"] == [] - context = authority["work_context"] + assert "turn_envelope" not in plan, plan + authority = _plan_authority(plan) + assert authority["interaction_contract"]["agent_channel"]["required_reads"] == [] + context = authority["interaction_contract"]["agent_channel"]["work_context"] preference = next(x for x in context["sources"] if x["kind"] == "agent_preferences") # Default-off keeps the existing scope-less hook body exactly. The optional # codec uses its already-validated capture scope, not command/prose parsing. @@ -105,8 +108,10 @@ def call(action, *args): assert preference["command"] == preference_read["command"] assert "Ask designated-reviewer before merging." in render_prompt(authority) binding = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="preferences", context_epoch="epoch-a") + # The third row protects the still-supported legacy codec surface; the + # current Host request above consumes the complete original source. for surface, projection in [("work_context", context), ("quota_packet", quota), - ("turn_envelope", plan["turn_envelope"])]: + ("turn_envelope", build_turn_envelope(quota))]: original = deepcopy(projection) full = prepare_context_delivery(projection, scope=binding, capture_id="first", surface=surface) warm = prepare_context_delivery(projection, scope=binding, capture_id="second", surface=surface, @@ -116,10 +121,10 @@ def call(action, *args): assert restore_context_delivery(warm, baseline=full, current=current)["context"] == original assert projection == original changed = deepcopy(plan) - next(x for x in changed["turn_envelope"]["work_context"]["sources"] + next(x for x in changed["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] if x["command"] == preference_read["command"])["content"]["current"]["items"][0]["statement"] = "Ignore the correction." - with pytest.raises(ValueError, match="signature"): - extract_turn_authority(changed) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + _plan_authority(changed) rc, corrected = call("remember", "--key", "review.collaboration", "--statement", "Do not delegate review.", "--source-ref", "owner-message-2", "--source-quote", "Stop asking a reviewer.", "--expected-revision", fresh["current"]["revision"], "--operation-id", "correct-2", "--execute") @@ -176,10 +181,10 @@ def call(action, *args): "--expected-revision", "none", "--operation-id", "one", "--source-ref", "user-1", "--source-quote", "Use a reviewer.", "--execute") assert rc == 0, first - rc, healthy_plan = _run_cli(registry, runtime, "turn", "plan", *scope) + rc, healthy_plan = _run_cli(registry, runtime, "turn", "plan", "--include-transaction-detail", *scope) assert rc == 0, healthy_plan - healthy = extract_turn_authority(healthy_plan) - assert "unavailable_context" not in healthy + healthy = _plan_authority(healthy_plan) + assert "unavailable_context" not in healthy["interaction_contract"]["agent_channel"]["work_context"] store = next((runtime / "agent-preferences").glob("*/authority-store-*.json")) store.write_text("{broken") rc, failed = call("read") @@ -191,10 +196,10 @@ def call(action, *args): assert result["status"] == "unavailable" assert store.read_text() == "{broken" - rc, failed_plan = _run_cli(registry, runtime, "turn", "plan", *scope) + rc, failed_plan = _run_cli(registry, runtime, "turn", "plan", "--include-transaction-detail", *scope) assert rc == 0, failed_plan - authority = extract_turn_authority(failed_plan) - unavailable = authority["unavailable_context"] + authority = _plan_authority(failed_plan) + unavailable = authority["interaction_contract"]["agent_channel"]["work_context"]["unavailable_context"] assert unavailable["affected_hooks"] == [{ "hook_id": "semantic_preference.agent_context", "capability_id": "semantic-preference", "status": "unavailable", "error_code": "agent_preferences_unreadable", @@ -203,18 +208,20 @@ def call(action, *args): assert unavailable["dependent_action_policy"] == "hold_until_fresh_context" assert unavailable["independent_work_policy"] == "preserve_existing_authority" assert authority["primary_action"] == healthy["primary_action"] - assert authority["write_scope"] == healthy["write_scope"] - assert authority["required_reads"] == [] + assert authority["goal_boundary"] == healthy["goal_boundary"] + assert authority["interaction_contract"]["agent_channel"]["required_reads"] == [] prompt = render_prompt(authority) - assert "discard cached context" in prompt and "Independent work may" in prompt + rendered = json.loads(prompt.split("Turn request:\n", 1)[1].split("\n\n", 1)[0]) + assert rendered["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["unavailable_context"] == unavailable + assert "work_context" in prompt and "independent work retains existing authority" in prompt assert "Use a reviewer." not in prompt and "{broken" not in prompt rc, quota_envelope = _run_cli(registry, runtime, "quota", "should-run", *scope, "--turn-envelope") assert rc == 0, quota_envelope assert quota_envelope["contract_capsule"]["unavailable_context"] == unavailable changed = deepcopy(failed_plan) - del changed["turn_envelope"]["contract_capsule"]["unavailable_context"] - with pytest.raises(ValueError, match="signature"): - extract_turn_authority(changed) + del changed["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["unavailable_context"] + with pytest.raises(ValueError, match="complete Turn contract rejected"): + _plan_authority(changed) @pytest.mark.parametrize("other_scope", ["goal", "agent"]) @@ -238,7 +245,7 @@ def test_shared_runtime_preserves_unconfigured_scope_projection(tmp_path, other_ def projections(): result = [] - for command in [("quota", "should-run"), ("turn", "plan")]: + for command in [("quota", "should-run"), ("turn", "plan", "--include-transaction-detail")]: rc, payload = _run_cli(registry, runtime, *command, *scope) assert rc == 0, payload result.append(payload) @@ -254,13 +261,12 @@ def projections(): after = projections() for old, new in zip(before, after): assert new.get("turn_start_capability_hook_dispatch") == old.get("turn_start_capability_hook_dispatch") - old_authority, new_authority = map(extract_turn_authority, (before[1], after[1])) - for authority in (old_authority, new_authority): - # The complete observation includes a fresh clock. Keep every signed - # action dimension; only the raw observation's digest may differ. - observed_digest = authority["action_signature"].pop("source_decision_hash") - assert re.fullmatch(r"sha256:[0-9a-f]{64}", observed_digest) - assert new_authority == old_authority + old_authority, new_authority = map(_plan_authority, (before[1], after[1])) + # The complete source retains each observation's clock and transaction. + # Compare all registered action dimensions through their canonical owner, + # and retain the exact current context rather than a historical Host alias. + assert quota_action_signature_document(new_authority) == quota_action_signature_document(old_authority) + assert new_authority["interaction_contract"]["agent_channel"]["work_context"] == old_authority["interaction_contract"]["agent_channel"]["work_context"] rc, empty = _run_cli(registry, runtime, "semantic-preference", "agent", "read", *scope) assert rc == 0 and empty["current"]["items"] == [] assert len(list((runtime / "agent-preferences").iterdir())) == 1 diff --git a/tests/control_plane/test_codex_operation_launch.py b/tests/control_plane/test_codex_operation_launch.py index 3e8acbf9ca..c0df971b3d 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,13 @@ 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 oversized_input(request): + text, schema = original_input(request) + return text + "\n" + "x" * (8 * 1024 * 1024), schema + + monkeypatch.setattr(codex_operation_host, "prepare_codex_cli_input", oversized_input) sessions, drained_before_release = [], [] entered = threading.Event() original_start = CodexChatAgentSession.start @@ -314,10 +318,14 @@ 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): + text, schema = original_input(request) + return text + "\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_context_dispatch_recovery.py b/tests/control_plane/test_context_dispatch_recovery.py index 51e6084c7d..a2c8b4bb94 100644 --- a/tests/control_plane/test_context_dispatch_recovery.py +++ b/tests/control_plane/test_context_dispatch_recovery.py @@ -73,10 +73,10 @@ def test_requester_crash_recovers_original_host_and_return_once(prepared_handoff from loopx.capabilities.manager_context.roundtrip import report from loopx.control_plane.collaboration.peers import read_inbox source_id = delegation['source_request_id'] -items = read_inbox(root / 'runtime', root / 'registry.json', envelope['goal_id'], actor)['items'] +items = read_inbox(root / 'runtime', root / 'registry.json', identity['goal_id'], actor)['items'] assert any(item['request_id'] == source_id for item in items) -acknowledge(root / 'runtime', envelope['goal_id'], actor, source_id, 'adopt', 'Read the original request.') -report(root / 'runtime', envelope['goal_id'], actor, source_id, 'conclusion', 'Original request independently handled.') +acknowledge(root / 'runtime', identity['goal_id'], actor, source_id, 'adopt', 'Read the original request.') +report(root / 'runtime', identity['goal_id'], actor, source_id, 'conclusion', 'Original request independently handled.') print(json.dumps(build_result""")) (root / "hold").touch() spawned = [] diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 8c409607ec..915e4ceaef 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -2,6 +2,7 @@ from __future__ import annotations from argparse import Namespace +from copy import deepcopy import hashlib import json from pathlib import Path @@ -356,6 +357,32 @@ def invoke(command): assert not registry.exists() and not runtime.exists() + +@pytest.mark.parametrize("bound", [False, True]) +def test_first_read_delivers_current_task_operation_with_its_continuation(tmp_path, decision, bound): + action = {"operation": "read_private_source", "target_ref": "fixture/private-source.json", + "future_constraint": {"allowed": False, "measurement": 2**60 + 1}} + commands = ["loopx quota should-run --goal-id example --agent-id worker"] + decision["interaction_contract"]["agent_channel"]["next_task_action"] = action + decision["interaction_contract"]["cli_channel"].update( + turn_binding_required=bound, next_cli_actions=commands) + original = deepcopy(decision) + view = native_context_capture_view(decision, tmp_path) + _page, values = read_selection(view["first_read_command"]) + + def delivered(pointer): + root = max((key for key in values if pointer == key or pointer.startswith(key + "/")), key=len) + value = values[root] + for key in pointer[len(root):].split("/")[1:]: + value = value[key] + return value + + assert delivered("/interaction_contract/agent_channel/next_task_action") == action + assert delivered("/interaction_contract/cli_channel/next_cli_actions") == commands + assert json.loads(Path(view["recovery"]["path"]).read_bytes()) == original + assert decision == original + + def test_initial_read_coalesces_whole_contract_with_exact_values_and_unknown_obligations(tmp_path, decision): cli = decision["interaction_contract"]["cli_channel"] cli["settlement_plan"]["ordered_steps"] = [{ diff --git a/tests/control_plane/test_context_selection_transport.py b/tests/control_plane/test_context_selection_transport.py index 8825f3c6f0..c5d76bd084 100644 --- a/tests/control_plane/test_context_selection_transport.py +++ b/tests/control_plane/test_context_selection_transport.py @@ -48,13 +48,13 @@ def test_explicit_reader_transports_less_prose_and_returns_original_values(tmp_p assert len(transported_contexts) == 1 transported = transported_contexts[0] assert len(json.dumps(transported, ensure_ascii=False).encode()) < len(raw) // 2 - assert transported["rows"][0] == {"text": None, "small": "ab", "empty": {}, "array": []} - assert transported["numbers"] == value["numbers"] + # The planning transport carries addresses; exact scalar values above come + # from the independently verified original, including non-JS-safe numbers. assert text not in json.dumps(transported, ensure_ascii=False) @pytest.mark.parametrize("mode", ["initial", "root_index", "nested_index"]) -def test_navigation_keeps_full_scalar_input(tmp_path, transported_contexts, mode): +def test_navigation_preserves_original_values_and_index_metadata(tmp_path, transported_contexts, mode): instruction = "Preserve all current requirements and stop before publication. " * 10 value = {"ok": True, "goal_id": "synthetic", "interaction_contract": {"schema_version": "loopx_interaction_contract_v0", @@ -64,18 +64,37 @@ def test_navigation_keeps_full_scalar_input(tmp_path, transported_contexts, mode path, raw, args = captured_reader(tmp_path, value, **options) page = read_context_capture(args) assert page["ok"] and page["eof"], page - assert transported_contexts == [value] assert path.read_bytes() == raw if mode == "initial": + assert transported_contexts == [value] assert instruction in page["content"] + else: + parent = value if mode == "root_index" else value["interaction_contract"] + prefix = "" if mode == "root_index" else "/interaction_contract" + fields = json.loads(page["content"])["selections"][0]["fields"] + expected = {} + for key, item in parent.items(): + pointer = prefix + "/" + key + field = {"pointer": pointer, + "type": "object" if isinstance(item, dict) else "boolean" if isinstance(item, bool) else "string", + "bytes": len(json.dumps(item, ensure_ascii=False, separators=(",", ":")).encode())} + if isinstance(item, dict): + field["count"] = len(item) + expected[pointer] = field + assert {field["pointer"]: field for field in fields} == expected + if mode == "root_index": + assert transported_contexts == [value] + else: + assert instruction not in json.dumps(transported_contexts, ensure_ascii=False) def test_short_scalar_arrays_do_not_grow_the_planning_payload(tmp_path, transported_contexts): value = {"values": [0, 1, False, True, None, "", "a", "ab"] * 60} - _, _, args = captured_reader(tmp_path, value, select=["/values"]) + _, raw, args = captured_reader(tmp_path, value, select=["/values"]) page = read_context_capture(args) assert page["ok"] and page["eof"], page - assert transported_contexts == [value] + assert len(transported_contexts) == 1 + assert len(json.dumps(transported_contexts[0], ensure_ascii=False).encode()) <= len(raw) assert json.loads(page["content"])["selections"][0]["value"] == value["values"] diff --git a/tests/control_plane/test_delegation_lane_readiness.py b/tests/control_plane/test_delegation_lane_readiness.py index 741f1c2e8d..7b6932d82e 100644 --- a/tests/control_plane/test_delegation_lane_readiness.py +++ b/tests/control_plane/test_delegation_lane_readiness.py @@ -111,10 +111,10 @@ def test_busy_handoff_resumes_after_source_completion_without_another_dispatch(s from loopx.control_plane.collaboration.peers import read_inbox from loopx.capabilities.manager_context.roundtrip import report source_id = delegation['source_request_id'] -items = read_inbox(root / 'runtime', root / 'registry.json', envelope['goal_id'], actor)['items'] +items = read_inbox(root / 'runtime', root / 'registry.json', identity['goal_id'], actor)['items'] assert any(item['request_id'] == source_id for item in items) -acknowledge(root / 'runtime', envelope['goal_id'], actor, source_id, 'adopt', 'Read original request') -report(root / 'runtime', envelope['goal_id'], actor, source_id, 'conclusion', 'Original request handled') +acknowledge(root / 'runtime', identity['goal_id'], actor, source_id, 'adopt', 'Read original request') +report(root / 'runtime', identity['goal_id'], actor, source_id, 'conclusion', 'Original request handled') print(json.dumps(build_result""")) def dispatch(): diff --git a/tests/control_plane/test_delegation_source_replay.py b/tests/control_plane/test_delegation_source_replay.py index 360867c3e7..7e51778b5d 100644 --- a/tests/control_plane/test_delegation_source_replay.py +++ b/tests/control_plane/test_delegation_source_replay.py @@ -311,8 +311,8 @@ def test_busy_lane_after_dispatch_keeps_the_original_operation_retryable(saved_h host.write_text(host.read_text().replace("print(json.dumps(build_result", """ from loopx.capabilities.manager_context.roundtrip import report source_id = delegation['source_request_id'] -acknowledge(root / 'runtime', envelope['goal_id'], actor, source_id, 'adopt', 'Read original request') -report(root / 'runtime', envelope['goal_id'], actor, source_id, 'conclusion', 'Original request handled') +acknowledge(root / 'runtime', identity['goal_id'], actor, source_id, 'adopt', 'Read original request') +report(root / 'runtime', identity['goal_id'], actor, source_id, 'conclusion', 'Original request handled') print(json.dumps(build_result""")) worker.execute(operation, prepared_source_request_id=receipt["request_id"]) assert worker.read(operation)["status"] == "accepted" @@ -434,10 +434,10 @@ def native_preflight(service, binding_id): from loopx.control_plane.collaboration.peers import read_inbox from loopx.capabilities.manager_context.roundtrip import report source_id = delegation['source_request_id'] -items = read_inbox(root / 'runtime', root / 'registry.json', envelope['goal_id'], actor)['items'] +items = read_inbox(root / 'runtime', root / 'registry.json', identity['goal_id'], actor)['items'] assert any(item['request_id'] == source_id for item in items) -acknowledge(root / 'runtime', envelope['goal_id'], actor, source_id, 'adopt', 'Read original request') -report(root / 'runtime', envelope['goal_id'], actor, source_id, 'conclusion', 'Original request handled') +acknowledge(root / 'runtime', identity['goal_id'], actor, source_id, 'adopt', 'Read original request') +report(root / 'runtime', identity['goal_id'], actor, source_id, 'conclusion', 'Original request handled') print(json.dumps(build_result""")) worker.execute(operation, prepared_source_request_id=receipt["request_id"]) assert worker.read(operation)["status"] == "accepted" diff --git a/tests/control_plane/test_doubao_onboarding_model_behavior_actor.py b/tests/control_plane/test_doubao_onboarding_model_behavior_actor.py index ed6568d792..b838d5c5bd 100644 --- a/tests/control_plane/test_doubao_onboarding_model_behavior_actor.py +++ b/tests/control_plane/test_doubao_onboarding_model_behavior_actor.py @@ -4,10 +4,13 @@ from collections.abc import Mapping from typing import Any +import pytest + from loopx.control_plane.testing.doubao_model_behavior_actor import ( DOUBAO_2_1_PRO_MODEL, DOUBAO_CHAT_COMPLETIONS_ENDPOINT, DoubaoOnboardingModelBehaviorActor, + _onboarding_decision_instruction, ) from loopx.control_plane.testing.onboarding_model_behavior_qualification import ( ONBOARDING_MODEL_BEHAVIOR_DECISION_SCHEMA_VERSION, @@ -121,3 +124,42 @@ def transport( assert result["schema_version"] == ONBOARDING_MODEL_BEHAVIOR_RESULT_SCHEMA_VERSION assert result["tool_calls"] == [] assert "fixture-key" not in json.dumps(result, sort_keys=True) + + +@pytest.mark.parametrize("phase", ["entry", "postcondition"]) +def test_onboarding_reason_code_instruction_matches_the_strict_result_boundary(phase): + instruction = _onboarding_decision_instruction(phase) + assert "reason_codes is required and must contain 1 to 12" in instruction + assert "^[A-Za-z0-9][A-Za-z0-9_.:@-]{0,159}$" in instruction + assert "including when the observation is healthy" in instruction + + +@pytest.mark.parametrize( + "reasons", [[], ["not a token"], [f"reason_{index}" for index in range(13)]], +) +def test_onboarding_invalid_reason_codes_are_not_repaired_or_retried(reasons): + calls = 0 + decision = { + "schema_version": ONBOARDING_MODEL_BEHAVIOR_DECISION_SCHEMA_VERSION, + "phase": "entry", + "next_action": "connect_if_needed", + "semantic_contract": onboarding_entry_semantic_contract(_packet()), + "reason_codes": reasons, + } + + def transport(**kwargs): + nonlocal calls + calls += 1 + return {"choices": [{"message": {"content": json.dumps(decision)}}]} + + actor = DoubaoOnboardingModelBehaviorActor( + api_key="fixture-key-not-a-secret", transport=transport, + ) + request = build_onboarding_model_behavior_actor_request( + _packet(), qualification_id="onboarding-invalid-reasons", phase="entry", + ) + raw = actor(request) + with pytest.raises(ValueError, match="reason_codes"): + normalize_onboarding_model_behavior_actor_result(raw, phase="entry") + assert raw["decision"]["reason_codes"] == decision["reason_codes"] + assert calls == 1 diff --git a/tests/control_plane/test_explore_settlement_attachment.py b/tests/control_plane/test_explore_settlement_attachment.py index f2ab717b6a..ea23087232 100644 --- a/tests/control_plane/test_explore_settlement_attachment.py +++ b/tests/control_plane/test_explore_settlement_attachment.py @@ -98,7 +98,9 @@ def plans(value): def test_driver_owned_turn_plan_does_not_invent_cli_attachment_command(tmp_path, mode): import subprocess import sys - from tests.test_loopx_turn_driver import _write_live_fixture + from tests.test_loopx_turn_driver import ( + _write_live_fixture, _completion_host_and_validation_scripts, _turn_run_once_completion_argv, + ) from loopx.configure_goal import configure_goal project, runtime, path = _write_live_fixture(tmp_path) @@ -110,11 +112,24 @@ def test_driver_owned_turn_plan_does_not_invent_cli_attachment_command(tmp_path, ], capture_output=True, text=True) assert result.returncode == 0, result.stdout + result.stderr packet = json.loads(result.stdout) - assert "settlement_plan" not in packet["turn_envelope"]["writeback"] - # This host owns writeback through callbacks, not an agent refresh command. - # Its read hook still exposes Explore, without promising a CLI attachment - # transport in the callback plan. - hooks = packet.get("turn_start_capability_hook_dispatch", {}).get("results", []) + assert "settlement_plan" not in packet["work_contract"]["interaction_contract"]["cli_channel"] + # Inspection is pure. Execute the original driver-owned path to observe its + # activation-bound hook and the exact input actually received by the Host. + assert "turn_start_capability_hook_dispatch" not in packet["work_contract"] + workspace = project / "host-workspace" + workspace.mkdir() + host, validate = _completion_host_and_validation_scripts() + host = host.replace('request = json.load(sys.stdin)', + 'request = json.load(sys.stdin)\npathlib.Path("received.json").write_text(json.dumps(request))') + executed = subprocess.run([ + sys.executable, "-m", "loopx.cli", + *_turn_run_once_completion_argv(workspace, runtime, path, host, validate), + ], capture_output=True, text=True, timeout=90) + assert executed.returncode == 0, executed.stdout + executed.stderr + request = json.loads((workspace / "received.json").read_text()) + source = request["work_contract"] + assert "settlement_plan" not in source["interaction_contract"]["cli_channel"] + hooks = source.get("turn_start_capability_hook_dispatch", {}).get("results", []) assert any(h.get("capability_id") == "explore" for h in hooks) == (mode != "off") diff --git a/tests/control_plane/test_fallback_wait_admission.py b/tests/control_plane/test_fallback_wait_admission.py index 7695d7de07..1526e91f48 100644 --- a/tests/control_plane/test_fallback_wait_admission.py +++ b/tests/control_plane/test_fallback_wait_admission.py @@ -8,7 +8,7 @@ import pytest from loopx.cli import main as cli_main -from loopx.control_plane.effect_runtime import EffectRuntimeRejected +from loopx.control_plane.coordination.local_authority_shadow_projection import ProjectionValueError from loopx.control_plane.goals.goal_vision import normalize_goal_vision_packet from loopx.control_plane.goals.goal_frontier import fallback_source from canonical_authority_fixture import ( @@ -432,7 +432,7 @@ def test_duplicate_authored_fallback_is_rejected_before_canonical_creation( ) before = state_file.read_bytes() - with pytest.raises(EffectRuntimeRejected, match="duplicate succession identity"): + with pytest.raises(ProjectionValueError, match="duplicate todos identity"): initialize_canonical_source_fixture( tmp_path / "runtime", GOAL_ID, state_path=state_file, provider="sqlite", ) @@ -440,7 +440,7 @@ def test_duplicate_authored_fallback_is_rejected_before_canonical_creation( # Invalid authored input never becomes authority. The actual CLI cannot # fabricate fallback advice from the uninitialized source afterward. assert cli_main(args) == 1 - result = json.loads(capsys.readouterr().out) + result = read_cli_policy_payload(capsys.readouterr().out, returncode=1) assert result["should_run"] is False assert result["state"] == "blocked_health" assert "canonical_authority_required" in json.dumps(result) diff --git a/tests/control_plane/test_frontier_deadline.py b/tests/control_plane/test_frontier_deadline.py index 33a9a84232..b7caca89ce 100644 --- a/tests/control_plane/test_frontier_deadline.py +++ b/tests/control_plane/test_frontier_deadline.py @@ -191,7 +191,8 @@ def test_installed_cli_keeps_full_source_deadline_after_compaction(tmp_path, mon before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") child = subprocess.run([sys.executable, "-m", "loopx.cli", "--format", "json", "--registry", str(registry), "quota", "should-run", "--goal-id", "goal-a", "--agent-id", "agent-a", - "--runtime-profile", "ark_managed_agent_goal", "--include-detail", "scheduler"], + "--runtime-profile", "ark_managed_agent_goal", "--include-detail", "scheduler", + "--context-delivery", "off"], capture_output=True, text=True, timeout=60) assert child.returncode == 0, child.stdout or child.stderr packet = json.loads(child.stdout) diff --git a/tests/control_plane/test_goal_amendment_proposal_lifecycle.py b/tests/control_plane/test_goal_amendment_proposal_lifecycle.py index ee85ee9b25..83c46b60ba 100644 --- a/tests/control_plane/test_goal_amendment_proposal_lifecycle.py +++ b/tests/control_plane/test_goal_amendment_proposal_lifecycle.py @@ -16,7 +16,7 @@ projection output; this fixture has no event log, so the real basis is the markdown sequence 0) and submits through the production CLI. 4. The legal settlement runs the quota-returned ``next_cli_actions`` - verbatim (refresh-state writeback + quota spend-slot) — no hand-written + with the authored Vision evidence (refresh-state writeback + quota spend-slot) — no hand-written ACK row is ever appended. 5. After settlement, the settled obligation can no longer anchor a proposal: a new proposal naming it fails closed with nothing retained. @@ -95,6 +95,8 @@ def _write_fixture(root: Path) -> tuple[Path, Path, Path]: "agent_model": "peer_v1", }, "authority_sources": [], + # This fixture tests amendment admission and settlement. + "execution_profile": {"stride_strategy": "off"}, "quota": { "compute": 1.0, "window_hours": 24, @@ -219,6 +221,8 @@ def test_production_quota_obligation_survives_the_full_amendment_lifecycle( runtime, "quota", "should-run", + "--context-delivery", + "off", "--codex-app", "--goal-id", GOAL_ID, @@ -311,9 +315,7 @@ def test_production_quota_obligation_survives_the_full_amendment_lifecycle( assert listed["rows"][0]["proposal_id"] == "gap_lifecycle_001" assert listed["rows"][0]["replan_obligation_id"] == obligation_id - # 4. Legal settlement: run the quota-returned next_cli_actions verbatim - # (refresh-state writeback with the typed semantic successor, then - # the quota spend) — the actual close flow, never a hand-written ACK. + # 4. Legal settlement uses the admitted proposal as this Turn's new evidence. cli_channel = guard["interaction_contract"]["cli_channel"] refresh_command = next( action @@ -327,24 +329,38 @@ def test_production_quota_obligation_survives_the_full_amendment_lifecycle( assert f"--turn-instance-id {TURN_ID}" in command assert "--todo-id" in command or "--replan-obligation-id" in command - refresh_command = ( - refresh_command.replace( - "", - "advanced", - ) - .replace("", "surface-lifecycle-successor") - .replace("", "hypothesis-lifecycle-successor") - .replace("", "probe-lifecycle-successor") - .replace("", "evidence-lifecycle-successor") - ) + vision = tmp_path / "admitted-proposal-vision.json" + vision.write_text(json.dumps({ + "schema_version": "goal_vision_replan_contract_v0", + "state": "active", + "vision_patch": { + "acceptance_summary": "Keep the original completion criteria while the admitted amendment awaits review.", + }, + "path_delta": { + "outcome": "continue", + "prior_assumption": "The bounded slice was stalled without an admitted amendment proposal.", + "observed_reality": "The proposal was admitted for review without changing canonical authority.", + "retained": ["Original Goal objective, completion criteria and proposal review authority."], + "evidence_refs": [listed["rows"][0]["proposal_id"]], + }, + }), encoding="utf-8") + evidence = { + "": "advanced", + "": "surface:amendment-proposal", + "": "hypothesis:proposal-admission", + "": "probe:admission-readback", + "": listed["rows"][0]["proposal_id"], + } + refresh_args = [evidence.get(arg, arg) for arg in _projected_cli_args(refresh_command)] refresh_rc, refresh = _run_cli( registry_path, runtime, - *_projected_cli_args(refresh_command), + *refresh_args, + "--agent-vision-json", str(vision), "--no-global-sync", "--suppress-external-sinks", ) - assert refresh_rc == 0, refresh + assert refresh_rc == 0, refresh.get("error") or refresh assert refresh["settlement_result"]["ok"] is True, refresh spend_rc, spend = _run_cli( @@ -380,6 +396,8 @@ def test_production_quota_obligation_survives_the_full_amendment_lifecycle( runtime, "quota", "should-run", + "--context-delivery", + "off", "--codex-app", "--goal-id", GOAL_ID, diff --git a/tests/control_plane/test_goal_state_context_transport.py b/tests/control_plane/test_goal_state_context_transport.py index 6690e28469..2e07d7d2a0 100644 --- a/tests/control_plane/test_goal_state_context_transport.py +++ b/tests/control_plane/test_goal_state_context_transport.py @@ -18,6 +18,49 @@ from tests.control_plane.codex_checkpoint_fixture import reference_metadata_oracle +def test_scalar_root_projection_keeps_the_complete_view_and_original_number(): + from loopx.control_plane.quota.turn_envelope import _apply_context_projection_changes + + source = {"counter": 9007199254740997} + view = {"ok": True, "goal_id": "example", "stage": "explore", "explore_basis": None} + actual = _apply_context_projection_changes(source, {"changes": [ + {"path": [], "value": view}, + {"path": ["counter"], "from": ["counter"]}, + ]}, base={}) + assert actual == {**view, "counter": 9007199254740997} + assert "counter" not in view + assert source == {"counter": 9007199254740997} + + +def test_root_source_copy_keeps_numeric_identity_without_aliasing(): + import math + + from loopx.control_plane.quota.turn_envelope import _apply_context_projection_changes + + source = {"facts": {"unsafe": 9007199254740997, "negative_zero": -0.0}} + actual = _apply_context_projection_changes(source, { + "changes": [{"path": [], "from": []}], + }, base={}) + assert actual == source and actual is not source + assert actual["facts"] is not source["facts"] + assert math.copysign(1, actual["facts"]["negative_zero"]) == -1 + actual["facts"]["unsafe"] = 0 + assert source["facts"]["unsafe"] == 9007199254740997 + + +@pytest.mark.parametrize("change", [ + {"path": [], "remove": True}, + {"path": [], "value": None}, + {"path": [], "value": []}, + {"path": [], "value": False}, +]) +def test_projection_root_must_remain_an_object(change): + from loopx.control_plane.quota.turn_envelope import _apply_context_projection_changes + + with pytest.raises(RuntimeError, match="Invalid TypeScript context projection changes"): + _apply_context_projection_changes({}, {"changes": [change]}, base={}) + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_goal_availability_transport_preserves_full_read_and_recovery(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -39,12 +82,10 @@ def test_goal_availability_transport_preserves_full_read_and_recovery(tmp_path, state_path=state, provider=provider) source_args = dict(registry_path=registry, runtime_root=runtime, goal_id=goal, todo_id=todo_id) original_effect = context_readback.effect_runtime_result - requests, plans = [], [] + requests, packets = [], [] omit_delivery_profile = [False] def observe(operation, request, **kwargs): - if operation == "work_item.context.plan": - plans.append(deepcopy(request["packet"])) if operation == "work_item.context.project": requests.append(deepcopy(request)) result = original_effect(operation, request, **kwargs) @@ -62,6 +103,7 @@ def guard(): "--runtime-profile", "generic_cli", "--context-delivery", "off"]) packet = json.loads(output.getvalue()) assert code == 0 and packet["should_run"] is True, packet.get("reason") + packets.append(deepcopy(packet)) return packet["interaction_contract"]["agent_channel"] small_channel = guard() @@ -116,7 +158,9 @@ def guard(): state.write_bytes(large) large_channel = guard() large_request = requests[-1] - captured_plan = plans[-1] + # Exercise the public decision, including its work-read obligations. The + # internal planner input deliberately carries only selection metadata. + captured_plan = packets[-1] assert state.read_bytes() == large # Source growth changes neither the complete Agent contract nor transport # size. The full Goal read remains pending; availability is not consumption. diff --git a/tests/control_plane/test_heartbeat_command_authority.py b/tests/control_plane/test_heartbeat_command_authority.py index 59777fac58..6487b890ad 100644 --- a/tests/control_plane/test_heartbeat_command_authority.py +++ b/tests/control_plane/test_heartbeat_command_authority.py @@ -7,7 +7,7 @@ import pytest import test_quota_settlement_cli as cli -from loopx.control_plane.quota.turn_envelope import build_turn_envelope +from loopx.control_plane.quota.turn_envelope import build_turn_envelope, turn_plan_host_source from loopx.control_plane.work_items.interaction_contract import build_interaction_contract from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority @@ -104,10 +104,10 @@ def test_current_selection_and_settlement_override_goal_command(tmp_path, provid assert all(saved.read_bytes() == original for saved, original in captures) code, plan = cli._run_cli(registry, runtime, "turn", "plan", "--goal-id", cli.GOAL_ID, "--agent-id", cli.AGENT_ID, *binding, - "--turn-instance-id", "read-only-command-plan", cwd=project) + "--turn-instance-id", "read-only-command-plan", "--include-transaction-detail", cwd=project) assert code == 0, plan - assert cli.TODO_ID in extract_turn_authority(plan)["primary_action"] - assert COMMAND not in json.dumps(plan["turn_envelope"]["contract_capsule"]) + assert cli.TODO_ID in extract_turn_authority(turn_plan_host_source(plan))["primary_action"] + assert COMMAND not in extract_turn_authority(turn_plan_host_source(plan))["primary_action"] @pytest.mark.parametrize("mode", ["monitor_quiet_skip", "autonomous_replan", "agent_workspace_repair"]) diff --git a/tests/control_plane/test_host_current_admission.py b/tests/control_plane/test_host_current_admission.py index e44ea4ca5c..3fb995e6d1 100644 --- a/tests/control_plane/test_host_current_admission.py +++ b/tests/control_plane/test_host_current_admission.py @@ -67,13 +67,13 @@ def lose_state_after_selection(): assert current["state"] == "blocked_health", current observations.append(current) if host == "exec": - original_prompt = codex_cli._prompt + original_input = codex_cli.prepare_codex_cli_input def lose_state_before_prompt(request): lose_state_after_selection() - return original_prompt(request) + return original_input(request) - monkeypatch.setattr(codex_cli, "_prompt", lose_state_before_prompt) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", lose_state_before_prompt) else: original_start = codex_operation_host.CodexChatAgentSession.start diff --git a/tests/control_plane/test_host_research_recovery_cli.py b/tests/control_plane/test_host_research_recovery_cli.py index 8ef7fe9325..ee9eff72d8 100644 --- a/tests/control_plane/test_host_research_recovery_cli.py +++ b/tests/control_plane/test_host_research_recovery_cli.py @@ -17,6 +17,7 @@ import pytest from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime +from tests.control_plane.codex_checkpoint_fixture import read_cli_policy_payload from loopx.cli import main as cli_main from loopx.cli_commands import turn_run_once as turn_command from loopx.goal_mode_mcp import GoalModeMCPConfig, GoalModeMCPControlPlane @@ -33,7 +34,7 @@ def _cli(registry, runtime, *args, code=0): completed = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *args], cwd=ROOT, capture_output=True, text=True, timeout=90) - result = json.loads(completed.stdout) + result = read_cli_policy_payload(completed.stdout, returncode=completed.returncode) assert completed.returncode == code, json.dumps(result, ensure_ascii=False) return result @@ -77,11 +78,12 @@ def _worker_args(project, runtime, registry, script, request): from hashlib import sha256 from pathlib import Path import subprocess +from tests.control_plane.codex_checkpoint_fixture import read_cli_policy_payload request=json.loads(Path(sys.argv[1]).read_text()) def cli(*args): result=subprocess.run([sys.executable,"-m","loopx.cli","--registry",request["registry"], "--runtime-root",request["runtime"],"--format","json",*args],capture_output=True,text=True,timeout=60) - payload=json.loads(result.stdout) + payload=read_cli_policy_payload(result.stdout,returncode=result.returncode) if result.returncode: Path(request["result"]).write_text(json.dumps({"failed_command":args,"result":payload})) assert result.returncode==0,payload diff --git a/tests/control_plane/test_initial_planning_host_cli.py b/tests/control_plane/test_initial_planning_host_cli.py index 940f8bb305..a2ffbef78f 100644 --- a/tests/control_plane/test_initial_planning_host_cli.py +++ b/tests/control_plane/test_initial_planning_host_cli.py @@ -22,6 +22,8 @@ def failed(kind,error,tb): sys.__excepthook__(kind,error,tb) sys.excepthook=failed def cli(*args): + if args[:2] == ("quota", "should-run"): + args = (*args, "--context-delivery", "off") result=subprocess.run([sys.executable,"-m","loopx.cli","--registry",request["registry"], "--runtime-root",request["runtime"],"--format","json",*args],capture_output=True,text=True,timeout=60) payload=json.loads(result.stdout) diff --git a/tests/control_plane/test_local_provider_settlement_journey.py b/tests/control_plane/test_local_provider_settlement_journey.py index ff61dd7c89..5b0195c0b5 100644 --- a/tests/control_plane/test_local_provider_settlement_journey.py +++ b/tests/control_plane/test_local_provider_settlement_journey.py @@ -13,6 +13,7 @@ from pathlib import Path from canonical_authority_fixture import isolate_sqlite_runtime +from tests.control_plane.codex_checkpoint_fixture import read_cli_policy_payload import test_quota_settlement_cli as settlement from test_todo_list_record_references import expand_references from test_quota_authority_settlement_journey import _source @@ -23,7 +24,7 @@ def test_file_sqlite_file_retains_new_todos_results_and_settled_turn(tmp_path, monkeypatch): isolate_sqlite_runtime(tmp_path, monkeypatch) - project, runtime, registry, state, _ = _source( + project, runtime, registry, state, goal_intent = _source( tmp_path, provider="file", handoff_mode="hard_lease", extra=f"claimed_by={settlement.AGENT_ID}", ) @@ -39,7 +40,7 @@ def run(*args, expected=0): capture_output=True, text=True, timeout=90, check=False, ) assert process.returncode == expected, process.stdout + process.stderr - return json.loads(process.stdout) + return read_cli_policy_payload(process.stdout, returncode=process.returncode) def listed(): payload = run("todo", "list", "--goal-id", goal) @@ -78,6 +79,9 @@ def history(name): assert listed()["authority_read"]["source_authority"] == "file_v0" assert run(*migration, "--execute")["selected_provider"] == "sqlite" state.unlink() # Neither restart nor rollback may restore old display truth. + # Keep registered Goal intent available without restoring any Todo rows. + # Canonical work continuity is distinct from permission to lose intent. + state.write_text(goal_intent) add_args = ("todo", "add", "--goal-id", goal, "--role", "agent", "--text", "Validate work added after the upgrade", "--priority", "P0", @@ -90,11 +94,17 @@ def history(name): assert added_again["status"] == "replayed" guard = run(*guard_args) - assert guard["interaction_contract"]["agent_channel"]["delivery_allowed"] is True + assert guard["should_run"] is True + assert guard["execution_preparation"]["state"] == "claim_required" + assert guard["interaction_contract"]["agent_channel"]["delivery_allowed"] is False identity = guard["heartbeat_receipt"]["settlement_identity"] acquired = run("task-lease", "acquire", *lease_args, "--expected-version", "0", "--ttl-seconds", "3600", "--write-scope", "tests/**") version = str(acquired["lease"]["version"]) + prepared = run(*guard_args) + assert prepared["execution_preparation"]["state"] == "ready" + assert prepared["interaction_contract"]["agent_channel"]["delivery_allowed"] is True + assert prepared["heartbeat_receipt"]["settlement_identity"] == identity proof = ("--task-lease-idempotency-key", "retained-work", "--task-lease-expected-version", version) note = "New result after SQLite upgrade; 中文说明与原操作回执必须保留。" diff --git a/tests/control_plane/test_monitor_observation_admission.py b/tests/control_plane/test_monitor_observation_admission.py index 3755e53035..7b5511b9aa 100644 --- a/tests/control_plane/test_monitor_observation_admission.py +++ b/tests/control_plane/test_monitor_observation_admission.py @@ -382,15 +382,30 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( assert resume["identity"]["turn_instance_id"] == turn assert resume["grants_new_delivery"] is False assert resume["progress_ref"] == "$.settlement_progress" - # Matched complete canonical fixtures emit 6,806 writeback characters on - # both File and SQLite, unchanged across the compared source revisions. - # Keep the full bounded vision/decision guidance, with 694 chars headroom. + # The complete current contract emits 8,642 writeback characters on both + # providers, including Vision bounds and continuation/cost review. Retain + # the historical 7,500 failure and the original 694 chars of headroom. # Spend (1,432) and settled (572) retain their 2,000-character ceiling. resume_budget = ( - 7_500 if (resume["next_step"] or {}).get("kind") == "durable_writeback" + 9_336 if (resume["next_step"] or {}).get("kind") == "durable_writeback" else 2_000 ) assert len(json.dumps(resume, ensure_ascii=False, indent=2)) < resume_budget + if (resume["next_step"] or {}).get("kind") == "durable_writeback": + # Recovery specializes the completed Task to semantic closeout. Keep + # complete authoring bounds and every argument from the admitted path. + from loopx.cli import build_parser + import shlex + + original_writeback = first["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1] + assert resume["next_step"]["vision_authoring"] == original_writeback["vision_authoring"] + + def parsed_closeout(step): + command = step["semantic_closeout_command_template"].replace( + "", "single_surface").replace("", "outcome_progress") + return vars(build_parser().parse_args(shlex.split(command)[1:])) + + assert parsed_closeout(resume["next_step"]) == parsed_closeout(original_writeback) after_cli = poll["after"]["interaction_contract"]["cli_channel"] assert after_cli["settlement_resume_ref"] == "$.settlement_resume" assert after_cli["spend_after_validation"] is False diff --git a/tests/control_plane/test_monitor_target_key_replay.py b/tests/control_plane/test_monitor_target_key_replay.py index 29fe588fcc..bc5c66ff2f 100644 --- a/tests/control_plane/test_monitor_target_key_replay.py +++ b/tests/control_plane/test_monitor_target_key_replay.py @@ -332,7 +332,8 @@ def test_target_only_auxiliary_recovery_preserves_advancement_binding( operation_id='synthetic-auxiliary') turn = 'synthetic-auxiliary-turn' rc, guard = _cli(registry, runtime, 'quota', 'should-run', '--goal-id', GOAL, - '--agent-id', AGENT, '--runtime-profile', 'generic_cli', '--turn-instance-id', turn) + '--agent-id', AGENT, '--runtime-profile', 'generic_cli', '--turn-instance-id', turn, + '--context-delivery', 'off') assert rc == 0 and guard['work_lane_contract']['lane'] == 'advancement_task', guard bound = guard['heartbeat_receipt']['settlement_identity']['todo_id'] assert bound != other['todo_id'] and bound != monitor diff --git a/tests/control_plane/test_monitor_unavailable_closeout_cli.py b/tests/control_plane/test_monitor_unavailable_closeout_cli.py index 708ba6a1c9..1517757ec6 100644 --- a/tests/control_plane/test_monitor_unavailable_closeout_cli.py +++ b/tests/control_plane/test_monitor_unavailable_closeout_cli.py @@ -52,6 +52,8 @@ def test_unavailable_monitor_closes_original_turn_without_observation_or_debit( subprocess.run(["git", "init", str(project)], capture_output=True, check=True) def cli(*args: str) -> tuple[int, dict]: + if args[:2] == ("quota", "should-run"): + args = (*args, "--context-delivery", "off") with monkeypatch.context() as context: context.chdir(project) rc = cli_main(["--registry", str(registry), "--runtime-root", str(runtime), @@ -86,9 +88,6 @@ def cli(*args: str) -> tuple[int, dict]: assert rc == 0 and guard["should_run"] is True, guard assert guard["work_lane_contract"]["obligation"] == "attempt_due_monitor" assert guard["heartbeat_receipt"]["settlement_identity"]["todo_id"] == MONITOR_ID - plan = guard["interaction_contract"]["cli_channel"]["settlement_plan"] - writeback_step = next(step for step in plan["ordered_steps"] if step["kind"] == "durable_writeback") - assert "observation is unavailable" in writeback_step["precondition"] lease_args: tuple[str, ...] = () if provider != "markdown": rc, lease = cli("task-lease", "acquire", "--goal-id", GOAL_ID, "--todo-id", MONITOR_ID, @@ -96,6 +95,12 @@ def cli(*args: str) -> tuple[int, dict]: assert rc == 0 and lease["acquired"] is True, lease lease_args = ("--task-lease-idempotency-key", f"unavailable-{provider}", "--task-lease-expected-version", str(lease["lease"]["version"])) + rc, bound = cli("quota", "should-run", "--goal-id", GOAL_ID, "--codex-app", *binding, *capabilities) + assert rc == 0, bound + assert bound["heartbeat_receipt"]["settlement_identity"] == guard["heartbeat_receipt"]["settlement_identity"] + plan = bound["interaction_contract"]["cli_channel"]["settlement_plan"] + writeback_step = next(step for step in plan["ordered_steps"] if step["kind"] == "durable_writeback") + assert "observation is unavailable" in writeback_step["precondition"] rc, before = cli("todo", "list", "--goal-id", GOAL_ID, "--todo-id", MONITOR_ID) assert rc == 0, before fields = {k: before["todo"].get(k) for k in OBSERVATION_FIELDS} diff --git a/tests/control_plane/test_native_child_replan_guard_cli.py b/tests/control_plane/test_native_child_replan_guard_cli.py index ec132cd4e0..bc7b5a4ccb 100644 --- a/tests/control_plane/test_native_child_replan_guard_cli.py +++ b/tests/control_plane/test_native_child_replan_guard_cli.py @@ -78,6 +78,8 @@ def _fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, tod ), state_path=state, provider=provider) def call(*args: str, expected_code: int = 0) -> dict: + if args[:2] == ("quota", "should-run"): + args = (*args, "--context-delivery", "off") result = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *args], cwd=ROOT, capture_output=True, text=True, timeout=60) diff --git a/tests/control_plane/test_outcome_context_navigation_cli.py b/tests/control_plane/test_outcome_context_navigation_cli.py index 83cfed9bdf..ec140d0f0f 100644 --- a/tests/control_plane/test_outcome_context_navigation_cli.py +++ b/tests/control_plane/test_outcome_context_navigation_cli.py @@ -8,7 +8,8 @@ from canonical_authority_fixture import initialize_canonical_authority from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry +from loopx.control_plane.testing.canary_harness import write_fixture_registry +from test_quota_settlement_cli import _run_cli from loopx.control_plane.todos.machine_section_projection import render_canonical_todo_sections from loopx.control_plane.turn_driver.codex_context import codex_context_hook from loopx.cli_commands.quota_capture import read_context_capture @@ -19,6 +20,7 @@ @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("marked_goal", [True, False]) def test_quota_navigation_is_opt_in_and_original_capture_remains_complete(tmp_path, monkeypatch, marked_goal, provider, inherited): + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) goal, agent = "scope-cli", "author" runtime, registry, state = tmp_path / "runtime", tmp_path / "registry.json", tmp_path / "state.md" todos = [{"schema_version": "todo_item_v0", "todo_id": f"todo_{owner}", "role": "agent", @@ -36,7 +38,7 @@ def test_quota_navigation_is_opt_in_and_original_capture_remains_complete(tmp_pa build_todo_runtime_shadow_projection(goal_id=goal, todos=todos, handoff_mode="soft_claim"), state_path=state, provider=provider) def cli(*args): - code, payload = run_json_cli_result(*args, registry_path=registry, runtime_root=runtime) + code, payload = _run_cli(registry, runtime, *args, cwd=tmp_path, raw_guard=False) assert code == 0, json.dumps(payload) return payload @@ -53,7 +55,7 @@ def cli(*args): "--expected-provider-revision", before["provider_revision"], "--operation-id", f"outcome-{owner}", "--execute") if inherited: from tests.control_plane.test_context_machine_configuration import configure - code, applied = configure(registry, runtime, tmp_path, "on") + code, applied = configure(registry, runtime, tmp_path, "on", delta="auto") assert code == 0 and applied["readback_verified"], applied off = cli("quota", "should-run", "--goal-id", goal, "--agent-id", agent, "--context-delivery", "off") source = next(row["content"] for row in off["interaction_contract"]["agent_channel"]["work_context"]["sources"] @@ -64,13 +66,11 @@ def cli(*args): native.mkdir(mode=0o700) (tmp_path / "captures").mkdir(mode=0o700) monkeypatch.setenv("CODEX_THREAD_ID", "scope-session") - monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") - monkeypatch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") codex_context_hook(str(native), {"hook_event_name": "SessionStart", "session_id": "scope-session", "cwd": str(tmp_path), "source": "startup"}) first = cli("quota", "should-run", "--goal-id", goal, "--agent-id", agent, - *([] if inherited else ["--context-delivery", "on"]), - "--runtime-profile", "generic_cli", "--codex-context-dir", str(native), + *([] if inherited else ["--context-delivery", "on", "--delta-delivery", "auto"]), + "--codex-app", "--codex-context-dir", str(native), "--decision-output-root", str(tmp_path / "captures"), "--turn-instance-id", "scope-turn") first = reference_metadata_oracle(first) page = read_context_capture(argparse.Namespace(capture_file=first["recovery"]["path"], diff --git a/tests/control_plane/test_permanent_blocked_closeout_cli.py b/tests/control_plane/test_permanent_blocked_closeout_cli.py index b5e9e65d0f..57164d8027 100644 --- a/tests/control_plane/test_permanent_blocked_closeout_cli.py +++ b/tests/control_plane/test_permanent_blocked_closeout_cli.py @@ -40,13 +40,13 @@ def cli(*args: str) -> tuple[int, dict]: ), state_path=state, provider=provider) binding = ("--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", f"blocked-lifecycle-{provider}") - guard_args = ("quota", "should-run", "--goal-id", GOAL_ID, "--codex-app", + guard_args = ("quota", "should-run", "--context-delivery", "off", "--goal-id", GOAL_ID, "--codex-app", ) - rc, guard = cli(*guard_args, *binding) - assert rc == 0 and guard["heartbeat_receipt"]["closeout_required"], guard rc, acquired = cli("task-lease", "acquire", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, "--owner", AGENT_ID, "--idempotency-key", "attempt-before-source-loss") assert rc == 0, acquired + rc, guard = cli(*guard_args, *binding) + assert rc == 0 and guard["heartbeat_receipt"]["closeout_required"], guard rc, blocked = cli("todo", "update", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, "--agent-id", AGENT_ID, "--status", "blocked", "--reason", "Original source is no longer available", "--clear-resume-when", diff --git a/tests/control_plane/test_posix_flag_fallback.py b/tests/control_plane/test_posix_flag_fallback.py index 9ddd8d2fca..ff9b7962fd 100644 --- a/tests/control_plane/test_posix_flag_fallback.py +++ b/tests/control_plane/test_posix_flag_fallback.py @@ -39,9 +39,10 @@ def _symlink(path, target) -> None: def test_native_trace_read_survives_a_host_without_the_posix_flags(tmp_path, monkeypatch): trace = tmp_path / "trace.jsonl" trace.write_bytes(b'{"seq": 1}\n') - monkeypatch.delattr(os, "O_NOFOLLOW", raising=False) - monkeypatch.delattr(os, "O_NONBLOCK", raising=False) - assert read_native_trace_file(trace) == b'{"seq": 1}\n' + with monkeypatch.context() as flags: + flags.delattr(os, "O_NOFOLLOW", raising=False) + flags.delattr(os, "O_NONBLOCK", raising=False) + assert read_native_trace_file(trace) == b'{"seq": 1}\n' def test_native_trace_read_refuses_a_symlink_without_the_flag(tmp_path, monkeypatch): diff --git a/tests/control_plane/test_post_writeback_wait_cli.py b/tests/control_plane/test_post_writeback_wait_cli.py index 881f2a1ce3..960436940d 100644 --- a/tests/control_plane/test_post_writeback_wait_cli.py +++ b/tests/control_plane/test_post_writeback_wait_cli.py @@ -105,6 +105,8 @@ def test_committed_progress_before_wait_recovers_only_original_spend( ) def run(*args: str) -> tuple[int, dict]: + if args[:2] == ("quota", "should-run"): + args = (*args, "--context-delivery", "off") with monkeypatch.context() as context: context.chdir(workspace) rc = cli_main( diff --git a/tests/control_plane/test_pr_wait_consumer_adoption.py b/tests/control_plane/test_pr_wait_consumer_adoption.py index bce8b178ef..1b924d909c 100644 --- a/tests/control_plane/test_pr_wait_consumer_adoption.py +++ b/tests/control_plane/test_pr_wait_consumer_adoption.py @@ -52,9 +52,12 @@ def fetch(reference, **kwargs): if observer_entry == "heartbeat": # Native capture is a thin-host transport. Keep its full-mode refusal # separate from the explicitly supported compatibility presentation. + capture_root = tmp_path / "heartbeat-captures" + capture_root.mkdir(mode=0o700) code, refused = _invoke_turn([ *prefix, "heartbeat-prompt", "--goal-id", "loopx-turn-fixture", "--agent-id", "codex-fixture", "--codex-app", "--available-capability", "external_evidence_poll", "--full", + "--decision-output-root", str(capture_root), "--turn-instance-id", "full-capture-refused", ]) assert code == 1 and refused["ok"] is False, refused assert "requires thin mode" in refused["error"] and calls == [] @@ -91,7 +94,8 @@ def capture(decision, **kwargs): " root = Path(kwargs['workspace'])\n" " prompt = kwargs['prompt']\n" " (root / 'consumer-input.txt').write_text(prompt)\n" - " authority = json.JSONDecoder().raw_decode(prompt.split('Turn authority JSON:\\n', 1)[1])[0]\n" + " request = json.JSONDecoder().raw_decode(prompt.split('Turn request:\\n', 1)[1])[0]\n" + " authority = request['work_contract']\n" " selected = authority['selected_todo']\n" f" assert selected['todo_id'] == {todo_id!r}\n" " assert selected['resume_when'] == 'pr_merged:owner/repo#42'\n" @@ -100,7 +104,8 @@ def capture(decision, **kwargs): " assert condition['satisfied'] is True\n" " assert condition['matched_pr_ref'] == 'owner/repo#42'\n" " assert condition['matched_event_at'] == '2026-10-01T00:00:00Z'\n" - " task = next(s['content']['todo'] for s in authority['work_context']['sources']\n" + " context = authority['interaction_contract']['agent_channel']['work_context']\n" + " task = next(s['content']['todo'] for s in context['sources']\n" " if s.get('source') == 'selected_todo')\n" f" assert task['text'] == {before['text']!r}\n" " counter = root / 'consumer-count'\n" @@ -134,7 +139,7 @@ def capture(decision, **kwargs): assert expected["resume_condition"]["matched_event_at"] == "2026-10-01T00:00:00Z" assert envelope["action_signature"]["matches"] is True prompt = (project / "consumer-input.txt").read_text() - consumed = json.JSONDecoder().raw_decode(prompt.split("Turn authority JSON:\n", 1)[1])[0] + consumed = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1])[0]["work_contract"] assert consumed["selected_todo"]["resume_condition"] == expected["resume_condition"] assert "Require the terminal independent artifact marker." in prompt diff --git a/tests/control_plane/test_quota_monitor_clock.py b/tests/control_plane/test_quota_monitor_clock.py index 09659c36c4..c67a0d6f7d 100644 --- a/tests/control_plane/test_quota_monitor_clock.py +++ b/tests/control_plane/test_quota_monitor_clock.py @@ -189,7 +189,7 @@ def test_codex_cli_replan_streak_respects_canonical_monitor_expiry( sys.executable, "-c", entrypoint, "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", "quota", "should-run", "--codex-app", "--goal-id", "goal-a", "--agent-id", "agent-a", - "--turn-instance-id", "expiry-fixture-turn", + "--turn-instance-id", "expiry-fixture-turn", "--context-delivery", "off", ], capture_output=True, text=True, encoding="utf-8", check=False) assert result.returncode == 0, result.stdout + result.stderr guard = json.loads(result.stdout) diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 6aff68f19d..48b650c9c7 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -581,6 +581,8 @@ def _configure_autonomous_replan_fixture( project: Path, runtime: Path, registry_path: Path, + *, + stride_strategy: str | None = None, ) -> None: state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state_path.write_text( @@ -605,6 +607,8 @@ def _configure_autonomous_replan_fixture( ) registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0]["status"] = "active" + if stride_strategy is not None: + registry["goals"][0].setdefault("execution_profile", {})["stride_strategy"] = stride_strategy registry_path.write_text( json.dumps(registry, indent=2, sort_keys=True) + "\n", encoding="utf-8", @@ -651,6 +655,8 @@ def _configure_autonomous_replan_fixture( def _configure_selected_todo_replan_fixture( project: Path, registry_path: Path, + *, + stride_strategy: str | None = None, ) -> None: state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" todo_rows = "\n".join( @@ -681,6 +687,8 @@ def _configure_selected_todo_replan_fixture( ) registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0]["status"] = "active" + if stride_strategy is not None: + registry["goals"][0].setdefault("execution_profile", {})["stride_strategy"] = stride_strategy registry_path.write_text( json.dumps(registry, indent=2, sort_keys=True) + "\n", encoding="utf-8", diff --git a/tests/control_plane/test_quota_spend_commit_runtime.py b/tests/control_plane/test_quota_spend_commit_runtime.py index bfa94dbf83..45ce9713e6 100644 --- a/tests/control_plane/test_quota_spend_commit_runtime.py +++ b/tests/control_plane/test_quota_spend_commit_runtime.py @@ -265,7 +265,8 @@ def test_large_spend_preserves_validation_before_effects(tmp_path: Path) -> None def test_spend_snapshot_limit_rejects_before_commit( tmp_path: Path, monkeypatch, ) -> None: - # Exercise the existing private-file bound with a bounded fixture allocation. + # Cross the inline threshold first so the private snapshot guard is used. + monkeypatch.setattr(effect_runtime, "MAX_REQUEST_BYTES", 1_000_000) monkeypatch.setattr(effect_runtime, "MAX_LOCAL_SNAPSHOT_BYTES", 2_100_000) preview = _preview( before={**_decision(0), "context": "x" * 1_100_000}, diff --git a/tests/control_plane/test_receipt_next_action_cli.py b/tests/control_plane/test_receipt_next_action_cli.py index fbba4bbe51..892ccf81fd 100644 --- a/tests/control_plane/test_receipt_next_action_cli.py +++ b/tests/control_plane/test_receipt_next_action_cli.py @@ -150,7 +150,7 @@ def run(*args: str) -> tuple[int, dict]: handoff_mode="soft_claim", leases=[]), state_path=state, provider=provider) binding = ("--agent-id", fixture.AGENT_ID, "--todo-id", fixture.TODO_ID, "--turn-instance-id", "original-next-action-turn") - guard_args = ("quota", "should-run", "--goal-id", fixture.GOAL_ID, "--codex-app", + guard_args = ("quota", "should-run", "--context-delivery", "off", "--goal-id", fixture.GOAL_ID, "--codex-app", *binding) code, admitted = run(*guard_args) assert code == 0 and admitted["normal_delivery_allowed"], admitted @@ -213,7 +213,7 @@ def run(*args: str) -> tuple[int, dict]: final = read_heartbeat_settlement(runtime, goal_id=fixture.GOAL_ID, agent_id=fixture.AGENT_ID, todo_id=fixture.TODO_ID, turn_instance_id=binding[-1]) assert final is not None and final.writeback == writeback.writeback - code, next_guard = run("quota", "should-run", "--goal-id", fixture.GOAL_ID, + code, next_guard = run("quota", "should-run", "--context-delivery", "off", "--goal-id", fixture.GOAL_ID, "--agent-id", fixture.AGENT_ID, "--todo-id", fixture.ALTERNATIVE_TODO_ID, "--turn-instance-id", "fresh-alternative-turn", "--codex-app", ) assert code == 0 and next_guard["normal_delivery_allowed"], next_guard @@ -230,7 +230,7 @@ def test_todoless_replan_refuses_step_and_recovers_original_settlement( ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = fixture._write_fixture(tmp_path) - fixture._configure_autonomous_replan_fixture(project, runtime, registry) + fixture._configure_autonomous_replan_fixture(project, runtime, registry, stride_strategy="off") state = project / f".codex/goals/{fixture.GOAL_ID}/ACTIVE_GOAL_STATE.md" def run(*args: str) -> tuple[int, dict]: diff --git a/tests/control_plane/test_registry_relative_runtime_resolution.py b/tests/control_plane/test_registry_relative_runtime_resolution.py index e4aa54d1f1..bb82c6a0c8 100644 --- a/tests/control_plane/test_registry_relative_runtime_resolution.py +++ b/tests/control_plane/test_registry_relative_runtime_resolution.py @@ -109,6 +109,8 @@ def test_history_and_quota_anchor_relative_runtime_to_registry_project( str(registry_path), "quota", "should-run", + "--context-delivery", + "off", "--goal-id", "half-speed", cwd=independent_worktree, diff --git a/tests/control_plane/test_replan_saved_context.py b/tests/control_plane/test_replan_saved_context.py index 8a5f2b3766..7b73e58e57 100644 --- a/tests/control_plane/test_replan_saved_context.py +++ b/tests/control_plane/test_replan_saved_context.py @@ -181,9 +181,19 @@ def read(command): if entry == "numeric": assert recovered["precision_fixture"] == {"exact_integer": 2**53 + 1, "exact_float": 1.0} assert type(recovered["precision_fixture"]["exact_float"]) is float - assert recovered["agent_todo_summary"]["goal_acceptance_contract"]["precision_probe"] == 2**53 + 1 + summary = recovered["agent_todo_summary"] sources = recovered["interaction_contract"]["agent_channel"]["work_context"]["sources"] - assert sources[-1]["content"]["goal_acceptance_contract"]["precision_probe"] == 2**53 + 1 + distinct = sources[-1]["content"]["goal_acceptance_contract"] + if "goal_acceptance_contract_ref" in summary: + # Ordinary CLI compaction may share this exact copy. It + # must still point to the distinct value, never the core. + assert summary["goal_acceptance_contract_ref"] == ( + f"$.interaction_contract.agent_channel.work_context.sources[{len(sources) - 1}]" + ".content.goal_acceptance_contract" + ) + else: + assert summary["goal_acceptance_contract"] == distinct + assert distinct["precision_probe"] == 2**53 + 1 else: # Explicit raw selectors and disabled transport keep their existing # content. They are not implicitly converted to a different view. diff --git a/tests/control_plane/test_replan_successor_durable_ack.py b/tests/control_plane/test_replan_successor_durable_ack.py index fcfa3f0c33..8006ba0c0b 100644 --- a/tests/control_plane/test_replan_successor_durable_ack.py +++ b/tests/control_plane/test_replan_successor_durable_ack.py @@ -214,6 +214,8 @@ def test_canonical_periodic_successor_settles_original_open_validation_todo( ), state_path=state, provider=provider) def call(*args: str, expected_error: str | None = None) -> dict: + if args[:2] == ("quota", "should-run"): + args = (*args, "--context-delivery", "off") result = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *args], cwd=project, capture_output=True, text=True, timeout=60) @@ -346,7 +348,7 @@ def call(*args): def guard(): result = call("quota", "should-run", "--goal-id", GOAL, "--agent-id", AGENT, - "--runtime-profile", "generic_cli") + "--runtime-profile", "generic_cli", "--context-delivery", "off") return result.get("autonomous_replan_obligation") original = guard() diff --git a/tests/control_plane/test_replan_terminal_spend_ordering.py b/tests/control_plane/test_replan_terminal_spend_ordering.py index fe1cdea212..75ffb6b962 100644 --- a/tests/control_plane/test_replan_terminal_spend_ordering.py +++ b/tests/control_plane/test_replan_terminal_spend_ordering.py @@ -160,7 +160,7 @@ def test_coverage_backed_no_followup_replan_does_not_strand_its_quota_spend( tmp_path: Path, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) - _configure_autonomous_replan_fixture(project, runtime, registry_path) + _configure_autonomous_replan_fixture(project, runtime, registry_path, stride_strategy="off") _write_terminal_frontier_state(project) _initialize_fixture_authority(project, runtime, provider="sqlite") vision_path = _write_no_followup_vision(tmp_path) @@ -241,7 +241,7 @@ def test_unbound_terminal_spend_stays_rejected(tmp_path: Path) -> None: """The terminal guard keeps rejecting a spend that owns no settlement.""" project, runtime, registry_path = _write_fixture(tmp_path) - _configure_autonomous_replan_fixture(project, runtime, registry_path) + _configure_autonomous_replan_fixture(project, runtime, registry_path, stride_strategy="off") _write_terminal_frontier_state(project) _initialize_fixture_authority(project, runtime, provider="sqlite") vision_path = _write_no_followup_vision(tmp_path) diff --git a/tests/control_plane/test_research_replan_basis_cli.py b/tests/control_plane/test_research_replan_basis_cli.py index 38a1b91aea..775b40cec6 100644 --- a/tests/control_plane/test_research_replan_basis_cli.py +++ b/tests/control_plane/test_research_replan_basis_cli.py @@ -32,6 +32,11 @@ def test_formal_task_instruction_change_rearms_the_actual_worker( "index": index + 1, "source_section": "Agent Todo", "archive_state": "active", "claimed_by": AGENT, "updated_at": "2026-09-01T00:00:00Z"} for index in range(15)] call, runtime, history = _fixture(tmp_path, monkeypatch, provider, future_monitor=False, initial_todos=todos) + # This fixture isolates instruction changes from independent Stride review. + registry_path = tmp_path / "registry.json" + registry = json.loads(registry_path.read_text()) + registry["goals"][0]["execution_profile"] = {"stride_strategy": "off"} + registry_path.write_text(json.dumps(registry)) history.write_text("") calls = tmp_path / "review-calls" args = _args(fake_cli=Path(sys.executable).parent / "loopx", state_file=tmp_path / "scheduler.json", @@ -170,11 +175,11 @@ def test_settled_owner_hold_stays_acknowledged_after_display_edits(acceptance_go original = _configure(cli, document_path)["goal_acceptance_contract"] goal, agent, turn = "goal-acceptance", "agent-a", "turn-original-owner-hold" code, guard = run("quota", "should-run", "--goal-id", goal, "--agent-id", agent, - "--turn-instance-id", turn) + "--turn-instance-id", turn, "--context-delivery", "off") assert code == 0 and guard["effective_action"] == "autonomous_replan_required", guard obligation = guard["autonomous_replan_obligation"] code, reentered = current_run("quota", "should-run", "--goal-id", goal, "--agent-id", agent, - "--turn-instance-id", turn) + "--turn-instance-id", turn, "--context-delivery", "off") assert code == 0, reentered assert reentered["autonomous_replan_obligation"]["obligation_id"] == obligation["obligation_id"] assert reentered["autonomous_replan_obligation"]["replan_context"]["obligation_id"] == obligation["obligation_id"] @@ -219,7 +224,7 @@ def test_settled_owner_hold_stays_acknowledged_after_display_edits(acceptance_go run = current_run def fresh_quota(): - code, packet = run("quota", "should-run", "--goal-id", goal, "--agent-id", agent) + code, packet = run("quota", "should-run", "--goal-id", goal, "--agent-id", agent, "--context-delivery", "off") assert code == 0, packet assert packet.get("autonomous_replan_obligation") is None, { key: packet.get(key) for key in ("should_run", "effective_action", "autonomous_replan_obligation")} @@ -251,6 +256,6 @@ def fresh_quota(): document["objective"] = "Produce the export and retain its checksum under new owner criteria." document_path.write_text(json.dumps(document)) _configure(cli, document_path) - code, changed = run("quota", "should-run", "--goal-id", goal, "--agent-id", agent) + code, changed = run("quota", "should-run", "--goal-id", goal, "--agent-id", agent, "--context-delivery", "off") assert code == 0 and changed["should_run"] is True, changed assert changed["autonomous_replan_obligation"]["obligation_id"] != obligation["obligation_id"] diff --git a/tests/control_plane/test_scheduler_arbitration_native_cli.py b/tests/control_plane/test_scheduler_arbitration_native_cli.py index 4cabc16ae3..cff58dfa8f 100644 --- a/tests/control_plane/test_scheduler_arbitration_native_cli.py +++ b/tests/control_plane/test_scheduler_arbitration_native_cli.py @@ -76,7 +76,7 @@ def make_scheduler_fixture( handoff_mode="soft_claim", leases=[], ), state_path=state, provider=provider) context = ["--host-surface", "codex_app"] if scenario == "invalid_context" else ["--codex-app"] - guard = ["quota", "should-run", *context, "--goal-id", GOAL, "--agent-id", AGENT, + guard = ["quota", "should-run", "--context-delivery", "off", *context, "--goal-id", GOAL, "--agent-id", AGENT, "--turn-instance-id", TURN, ] return {"project": project, "runtime": runtime, "registry": registry, "state": state, "guard": guard} @@ -226,7 +226,10 @@ def host_rrule(minutes: int) -> None: try: host_rrule(30) - packet = call_scheduler_cli(fixture, *fixture["guard"]) + packet = call_scheduler_cli( + fixture, *fixture["guard"], "--include-detail", "agent-todos", + "--include-detail", "user-todos", "--include-detail", "scheduler", + ) authority = scheduler_authority_snapshot(fixture) # Establish valid earlier ACKs through the native commit owner. Only # the synthetic projection clock moves; the monitor facts stay fixed. @@ -320,7 +323,10 @@ def test_elapsed_quiet_backoff_reads_host_and_resets_for_native_work( try: # The reset profile includes full monitor facts; a compact display is # deliberately insufficient to seed the same historical decision. - packet = call_scheduler_cli(fixture, *fixture["guard"], "--include-detail", "agent-todos") + packet = call_scheduler_cli( + fixture, *fixture["guard"], "--include-detail", "agent-todos", + "--include-detail", "scheduler", + ) authority = scheduler_authority_snapshot(fixture) # Seed only this disposable history through the real commit owner. # All subsequent CLI processes use their real clock and host reader. diff --git a/tests/control_plane/test_scoped_gate_explicit_choice.py b/tests/control_plane/test_scoped_gate_explicit_choice.py index f6598ae902..1a98174dbd 100644 --- a/tests/control_plane/test_scoped_gate_explicit_choice.py +++ b/tests/control_plane/test_scoped_gate_explicit_choice.py @@ -106,8 +106,12 @@ def test_generated_heartbeat_known_choice_uses_one_current_guard(tmp_path, provi assert not channel["cli_channel"].get("selection_required", False) assert _heartbeat_receipt_count(runtime, TURN_ID) == 1 else: - assert rc != 0, result - assert not result["interaction_contract"]["agent_channel"]["delivery_allowed"], json.dumps(result) + assert rc != 0 and result["error_code"] == "quota_action_selection_rejected", result + from loopx.control_plane.turn_driver.decision_capture import read_context_capture_file + + detail = result["captured_decision"] + refused = read_context_capture_file(detail["path"], detail["sha256"], private=True)[2] + assert not refused["interaction_contract"]["agent_channel"]["delivery_allowed"], refused assert _heartbeat_receipt_count(runtime, TURN_ID) == 0 assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_scoped_gate_successor_tool_behavior.py b/tests/control_plane/test_scoped_gate_successor_tool_behavior.py index a4a0770be6..6a26d03bdc 100644 --- a/tests/control_plane/test_scoped_gate_successor_tool_behavior.py +++ b/tests/control_plane/test_scoped_gate_successor_tool_behavior.py @@ -176,9 +176,11 @@ def test_model_can_recover_from_one_unexecuted_read_shape( _stop_response("User gate: review the unrelated product first screen."), ] call_count = 0 + requests: list[dict[str, Any]] = [] - def transport(**_: Any) -> Mapping[str, Any]: + def transport(**kwargs: Any) -> Mapping[str, Any]: nonlocal call_count + requests.append(json.loads(kwargs["body"])) response = responses[call_count] call_count += 1 return response @@ -197,6 +199,14 @@ def transport(**_: Any) -> Mapping[str, Any]: "workspace_read_rejected", "selected_action", ] + feedback = requests[2]["messages"][-1] + assert feedback["role"] == "tool" + assert "without executing it" in feedback["content"] + assert "`cat `" in feedback["content"] + assert "do not rediscover the workspace or inspect state" in feedback["content"] + assert "no additional read scope or tool budget" in feedback["content"] + assert "using pwd, ls" not in feedback["content"] + assert receipt["tool_call_limit"] == SCOPED_GATE_SUCCESSOR_TOOL_BEHAVIOR_MAX_CALLS def test_mutating_command_is_not_retried_as_read_intent(tmp_path: Path) -> None: diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index 04dc7e1c12..bdaa1551ea 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -391,7 +391,8 @@ def capture_projection(effect, request, **kwargs): args = build_parser().parse_args([ "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", - "quota", "should-run", "--goal-id", "selected-race-goal", "--agent-id", "agent-a", + "quota", "should-run", "--context-delivery", "off", + "--goal-id", "selected-race-goal", "--agent-id", "agent-a", ]) packet: dict[str, object] = {} diff --git a/tests/control_plane/test_selection_replan_reentry.py b/tests/control_plane/test_selection_replan_reentry.py index 840738dc98..0688ae294c 100644 --- a/tests/control_plane/test_selection_replan_reentry.py +++ b/tests/control_plane/test_selection_replan_reentry.py @@ -1,6 +1,7 @@ """A deferred selection must expose a runnable recovery before settlement.""" import json import shlex +import subprocess import pytest from canonical_authority_fixture import advance_canonical_fixture_authority, read_fixture_todos @@ -31,7 +32,7 @@ def test_replan_selection_recovers_inline_or_refuses_ineligible_choice_and_settl project, runtime, registry = _write_fixture(tmp_path) _configure_selectable_alternative(project) if initial_replan: - _configure_selected_todo_replan_fixture(project, registry) + _configure_selected_todo_replan_fixture(project, registry, stride_strategy="off") _initialize_fixture_authority(project, runtime, provider="sqlite") turn = "turn-selection-preempted" guard = ("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, @@ -49,10 +50,10 @@ def test_replan_selection_recovers_inline_or_refuses_ineligible_choice_and_settl trigger["kind"] for trigger in first["autonomous_replan_obligation"]["triggers"] } if binding == "todo": - _configure_selected_todo_replan_fixture(project, registry) + _configure_selected_todo_replan_fixture(project, registry, stride_strategy="off") selected_id = SELECTED_REPLAN_TODO_ID else: - _configure_autonomous_replan_fixture(project, runtime, registry) + _configure_autonomous_replan_fixture(project, runtime, registry, stride_strategy="off") selected_id = TODO_ID _advance_frontier(project, runtime, "fixture-frontier-after-selection") rc, selected = _run_cli(registry, runtime, *guard, "--todo-id", selected_id) @@ -78,7 +79,9 @@ def test_replan_selection_recovers_inline_or_refuses_ineligible_choice_and_settl assert argv[argv.index("--turn-instance-id") + 1] == turn assert "--todo-id" not in argv and "--replan-obligation-id" not in argv assert _heartbeat_receipt_count(runtime, turn) == 1 - rc, resumed = _run_generated_cli(command, registry_path=registry) + rc, resumed = _run_generated_cli( + shlex.join([*argv, "--context-delivery", "off"]), registry_path=registry, + ) assert rc == 0, resumed receipt = resumed["heartbeat_receipt"] assert receipt["status"] == "upgraded" @@ -182,7 +185,7 @@ def test_reentry_never_replaces_retained_selection_with_recommended_todo(tmp_pat project, runtime, registry = _write_fixture(tmp_path) _configure_selectable_alternative(project) if initial_replan: - _configure_selected_todo_replan_fixture(project, registry) + _configure_selected_todo_replan_fixture(project, registry, stride_strategy="off") _initialize_fixture_authority(project, runtime, provider="sqlite") turn = "turn-selection-recommendation-drift" guard = ("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, @@ -201,7 +204,7 @@ def test_reentry_never_replaces_retained_selection_with_recommended_todo(tmp_pat # The original explicit choice leaves the refreshed frontier while a # different recommended Todo becomes visible under a hard replan. - _configure_selected_todo_replan_fixture(project, registry) + _configure_selected_todo_replan_fixture(project, registry, stride_strategy="off") _advance_frontier(project, runtime, "fixture-recommendation-drift") retained_todo_id = "todo_chain_000000000001" rc, resumed = _run_cli( @@ -349,7 +352,7 @@ def test_inline_reentry_reads_gates_changed_after_choice_receipt( _configure_selected_todo_replan_fixture(project, registry) _initialize_fixture_authority(project, runtime, provider="sqlite") turn = "turn-inline-reentry-new-gate" - guard = ("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + guard = ("quota", "should-run", "--context-delivery", "off", "--codex-app", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", turn, ) if discovery: @@ -493,20 +496,26 @@ def test_native_first_selection_publishes_only_final_bound_capture(tmp_path, mon assert _spend_run_count(runtime) == 0 # The bounded reference reaches the original receipt without another # admission. Reading a selected section does not certify the whole input. - from loopx.cli import build_parser - from loopx.cli_commands.quota_capture import read_context_capture - from loopx.control_plane.turn_driver.decision_capture import CONTEXT_CAPTURE_PAGE_GUIDANCE + from loopx.control_plane.turn_driver.decision_capture import ( + CONTEXT_CAPTURE_PAGE_GUIDANCE, read_context_read_reference, + ) - reader = shlex.split(presented["recovery"]["command"])[1:] - first = read_context_capture(build_parser().parse_args(reader)) + reader = shlex.split(presented["recovery"]["command"]) + result = subprocess.run(reader, cwd=project, capture_output=True, text=True, check=True, timeout=60) + first = json.loads(result.stdout) assert first["ok"] and first["offset"] == 0 assert first["read_instruction"] == CONTEXT_CAPTURE_PAGE_GUIDANCE assert len(json.dumps(first, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 4096 - args = build_parser().parse_args(reader - + ["--select", "/heartbeat_receipt"]) - read = read_context_capture(args) + result = subprocess.run( + [*reader, "--select", "/heartbeat_receipt"], + cwd=project, capture_output=True, text=True, check=True, timeout=60, + ) + read = json.loads(result.stdout) assert read["ok"] and read["eof"] - assert read["sha256"] == presented["recovery"]["sha256"] + _, source_reference = read_context_read_reference(presented["recovery"]["reference_file"]) + _, selected_reference = read_context_read_reference(read["reference_file"]) + assert selected_reference["path"] == source_reference["path"] + assert selected_reference["sha256"] == source_reference["sha256"] assert json.loads(read["content"])["selections"][0]["value"] == original["heartbeat_receipt"] assert _heartbeat_receipt_count(runtime, turn) == 1 assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_settlement_command_guidance_cli.py b/tests/control_plane/test_settlement_command_guidance_cli.py index 8f7d51f021..95d0420e50 100644 --- a/tests/control_plane/test_settlement_command_guidance_cli.py +++ b/tests/control_plane/test_settlement_command_guidance_cli.py @@ -23,11 +23,22 @@ def test_returned_completion_prepares_and_preserves_original_execution( project, runtime, registry = _write_fixture(tmp_path) _initialize_fixture_authority(project, runtime, provider=provider, handoff_mode=handoff_mode) host = ["--codex-app"] if profile == "codex_app" else ["--runtime-profile", profile] - rc, guard = _run_cli(registry, runtime, "quota", "should-run", *host, - "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--todo-id", TODO_ID, "--turn-instance-id", "claim-guidance") + guard_args = ("quota", "should-run", *host, "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", "claim-guidance") + rc, guard = _run_cli(registry, runtime, *guard_args, cwd=project) assert rc == 0, guard assert guard["goal_boundary"]["handoff_mode"] == handoff_mode + if handoff_mode == "hard_lease": + channel = guard["interaction_contract"]["cli_channel"] + assert "settlement_plan" not in channel + preparation = channel["execution_preparation"] + assert preparation["state"] == "claim_required" and preparation["delivery_allowed"] is False + # Follow the original guard's native preparation before requesting its plan. + rc, claim = _run_cli(registry, runtime, *preparation["claim"]["cli_args"], cwd=project) + assert rc == 0 and claim["execution_managed"] is True, claim + rc, guard = _run_cli(registry, runtime, *guard_args, cwd=project) + assert rc == 0, guard + assert guard["interaction_contract"]["cli_channel"]["execution_preparation"]["state"] == "ready" plan = guard["interaction_contract"]["cli_channel"]["settlement_plan"] assert build_turn_envelope(guard)["writeback"]["settlement_plan"] == plan step = next(s for s in plan["ordered_steps"] if s["kind"] == "validation") @@ -35,9 +46,7 @@ def test_returned_completion_prepares_and_preserves_original_execution( command = step["command_template"].replace("", "evidence:accepted-deliverable") if handoff_mode == "hard_lease": assert preparation is not None - # Follow only the returned plan, without a failed completion or global help. - rc, claim = _run_generated_cli(preparation[1], registry_path=registry) - assert rc == 0 and claim["execution_managed"] is True, claim + assert "" in command command = command.replace("", claim["claim_operation_id"]) # A guessed reference must not adopt the active execution or complete work. rc, refused = _run_generated_cli( diff --git a/tests/control_plane/test_settlement_input_discovery_cli.py b/tests/control_plane/test_settlement_input_discovery_cli.py index 95c1990d36..c2c2565dfe 100644 --- a/tests/control_plane/test_settlement_input_discovery_cli.py +++ b/tests/control_plane/test_settlement_input_discovery_cli.py @@ -4,6 +4,7 @@ import json import re import shlex +import subprocess from copy import deepcopy from pathlib import Path @@ -224,7 +225,8 @@ def run(*args): return _run_cli(registry, runtime, *args, cwd=project) rc, configured = run("configure-goal", "--goal-id", GOAL_ID, - "--execution-replan-after-turns", "1", "--execute") + "--execution-replan-after-turns", "1", + "--execution-stride-strategy", "off", "--execute") assert rc == 0, configured for stage in ("initial", "review", "subsequent"): binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, @@ -240,7 +242,15 @@ def run(*args): if stage == "review": channel = guard["interaction_contract"]["agent_channel"] assert channel["must_attempt"] and not channel["delivery_allowed"] - steps = guard["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] + read_command = guard["replan_action_packet"]["writeback"] + result = subprocess.run( + shlex.split(read_command), cwd=project, capture_output=True, text=True, check=True, timeout=60, + ) + staged = json.loads(result.stdout) + assert staged["obligation_id"] == guard["replan_action_packet"]["obligation_id"], staged + writeback = staged["writeback"] + assert writeback["settlement_plan"]["identity"] == guard["heartbeat_receipt"]["settlement_identity"] + steps = writeback["settlement_plan"]["ordered_steps"] authoring = next(step["vision_authoring"] for step in steps if step["kind"] == "durable_writeback") example = re.search(r"retain:todo_", authoring["authoring_hint"]) assert example, "The authoring contract must offer an executable retention example." @@ -253,7 +263,7 @@ def run(*args): path.write_text(json.dumps(vision)) # Consume the replan-only binding, rather than inventing a Todo # delivery admission for a periodic review. - args = shlex.split(guard["interaction_contract"]["cli_channel"]["next_cli_actions"][0]) + args = shlex.split(writeback["next_cli_actions"][0]) args = args[args.index("refresh-state"):] args[args.index("--agent-vision-json") + 1] = str(path) else: diff --git a/tests/control_plane/test_sqlite_authority_cli.py b/tests/control_plane/test_sqlite_authority_cli.py index c7288d7c37..230d97a0ae 100644 --- a/tests/control_plane/test_sqlite_authority_cli.py +++ b/tests/control_plane/test_sqlite_authority_cli.py @@ -204,7 +204,8 @@ def todos(): "--owner", agent, "--idempotency-key", "trial-work") proof = ["--task-lease-idempotency-key", "trial-work", "--task-lease-expected-version", str(lease["lease"]["version"])] - guard_args = ["quota", "should-run", "--goal-id", goal, "--agent-id", agent, + guard_args = ["quota", "should-run", "--context-delivery", "off", + "--goal-id", goal, "--agent-id", agent, "--todo-id", todo, "--codex-app", "--turn-instance-id", turn, ] guard = command(*guard_args) @@ -351,7 +352,7 @@ def listed(*archived_ids): accepted = command(*claim) assert accepted["execution_managed"] is True proof = ["--claim-operation-id", accepted["claim_operation_id"]] - guard = ["quota", "should-run", "--goal-id", goal, "--agent-id", agent, + guard = ["quota", "should-run", "--context-delivery", "off", "--goal-id", goal, "--agent-id", agent, "--todo-id", todo, "--codex-app", "--turn-instance-id", turn, "--available-capability", "filesystem_read", ] assert command(*guard)["normal_delivery_allowed"] is True diff --git a/tests/control_plane/test_terminal_settlement_tool_behavior.py b/tests/control_plane/test_terminal_settlement_tool_behavior.py index 18165729d4..df2505fa1a 100644 --- a/tests/control_plane/test_terminal_settlement_tool_behavior.py +++ b/tests/control_plane/test_terminal_settlement_tool_behavior.py @@ -17,6 +17,7 @@ ScriptedAssistantAction, ScriptedDoubaoExecTransport, ScriptedExecToolAction, + execute_loopx_cli, ) from loopx.control_plane.testing.terminal_settlement_tool_behavior import ( TERMINAL_SETTLEMENT_TOOL_BEHAVIOR_MAX_CALLS, @@ -228,6 +229,8 @@ def test_real_tool_loop_settles_final_todo_after_spend( assert plan["identity"] == completed["settlement_identity"] writeback = next(step for step in plan["ordered_steps"] if step["kind"] == "durable_writeback") assert writeback["vision_authoring"]["schema_version"] == "goal_vision_replan_contract_v0" + assert "Use semantic_closeout_command_template with --agent-vision-json" in writeback["precondition"] + assert "--agent-vision-json " in writeback["semantic_closeout_command_template"] assert completed["next_cli_actions"] == [writeback["command_template"]] assert completed["settlement_result"]["receipts"][0]["step_kind"] == "validation" markdown = render_todo_markdown(completed) @@ -363,14 +366,21 @@ def failed_writeback(command, state): @pytest.mark.skipif(not shell_isolation_available(), reason="Draft authoring requires native OS isolation") -@pytest.mark.parametrize(("draft_target", "coverage_observation", "correct_input"), [ - (".scratch/vision.json", False, False), (".scratch/vision.json", True, False), - (".scratch/vision.json", True, True), - ("fixture/settlement-proof.json", False, False), ("outside", False, False), +@pytest.mark.parametrize(("draft_target", "coverage_observation", "correct_input", "page_bytes"), [ + (".scratch/vision.json", False, False, None), (".scratch/vision.json", True, False, None), + (".scratch/vision.json", True, True, None), + ("fixture/settlement-proof.json", False, False, None), ("outside", False, False, None), + (".scratch/vision.json", True, True, 12000), + (".scratch/vision.json", True, True, 64000), ]) def test_terminal_native_draft_preserves_proof_and_authority( - tmp_path: Path, draft_target: str, coverage_observation: bool, correct_input: bool, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, draft_target: str, + coverage_observation: bool, correct_input: bool, page_bytes: int | None, ) -> None: + if page_bytes is not None: + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) + if page_bytes == 12000: + tmp_path /= "long-recovery-root-" + "x" * 120 fixture = _build_fixture(tmp_path / "oracle") outside = tmp_path / "owner-private.json" outside.write_text("Owner artifact.\n") @@ -394,6 +404,37 @@ def authority_files(): for path in tree.rglob("*") if path.is_file()} before_rejection = {} + initial_page_count = 0 + + def context_pages(request): + values = [json.loads(message["content"]) for message in request["messages"] + if message.get("role") == "tool" and message["content"].startswith("{")] + return [value for value in values + if value.get("schema_version") == "context_capture_selection_page_v1"] + + def initial_read(request): + navigation = json.loads(request["messages"][-1]["content"]) + tokens = shlex.split(navigation["first_read_command"]) + for option in ("--page-bytes", "--max-page-output-bytes"): + tokens[tokens.index(option) + 1] = str(page_bytes) + return ScriptedExecToolAction(command=shlex.join(tokens)) + + def vision_read(request): + nonlocal initial_page_count + pages = context_pages(request) + assert pages[-1]["eof"] is True + offset = 0 + for page in pages: + assert page["offset"] == offset + offset += len(page["content"].encode("utf-8")) + assert offset == pages[-1]["total_bytes"] + initial_page_count = len(pages) + selection = json.loads("".join(page["content"] for page in pages)) + assert selection["selections"] + navigation = next(json.loads(message["content"]) for message in request["messages"] + if message.get("role") == "tool") + return ScriptedExecToolAction(command=navigation["recovery"]["command"] + + " --select /interaction_contract/cli_channel/settlement_plan/ordered_steps/1/vision_authoring") def writeback(request, *, omit_scope=False): if correct_input and not omit_scope: @@ -402,6 +443,11 @@ def writeback(request, *, omit_scope=False): assert rejected["error_code"] == "progress_observation_invalid_arguments" assert "--progress-coverage-scope-id" in rejected["error"] assert authority_files() == before_rejection + if page_bytes is not None and omit_scope: + authoring = context_pages(request)[-1] + assert authoring["eof"] is True + syntax = json.loads(authoring["content"])["selections"][0]["value"] + assert syntax["minimal_example"]["schema_version"] == "goal_vision_replan_contract_v0" command = _writeback_action(request).command + " --agent-vision-json .scratch/vision.json" if coverage_observation: packet = _latest_quota_packet(request) @@ -420,28 +466,84 @@ def completion(request): tokens[tokens.index("--turn-instance-id") + 1] = "${LOOPX_TURN:?}" return ScriptedExecToolAction(command=shlex.join(tokens)) - receipt = DoubaoTerminalSettlementToolBehaviorActor( - api_key="test-only-placeholder", transport=ScriptedDoubaoExecTransport([ + actions = iter([ ScriptedExecToolAction(command=fixture.quota_guard_command), + *([initial_read] if page_bytes is not None else []), ScriptedExecToolAction(command="cat fixture/settlement-proof.json"), + *([vision_read] if page_bytes is not None else []), completion, ScriptedExecToolAction(command="cat > " + shlex.quote(target) + " <<'JSON'\n" + json.dumps(vision) + "\nJSON"), *([partial(writeback, omit_scope=True)] if correct_input else []), writeback, _spend_action, _completion_action, - ]), + ]) + + def next_action(request): + pages = context_pages(request) + if pages and not pages[-1]["eof"]: + return ScriptedExecToolAction(command=pages[-1]["next_command"]) + action = next(actions) + return action(request) if callable(action) else action + + receipt = DoubaoTerminalSettlementToolBehaviorActor( + api_key="test-only-placeholder", transport=ScriptedDoubaoExecTransport( + [next_action] * TERMINAL_SETTLEMENT_TOOL_BEHAVIOR_MAX_CALLS, + ), ).qualify(qualification_id="native-terminal-draft", fixture_root=tmp_path / "actor") - passed = draft_target == ".scratch/vision.json" + passed = draft_target == ".scratch/vision.json" and page_bytes != 12000 assert receipt["qualification_passed"] is passed, receipt assert receipt["boundary"]["shell_commands_executed"] is True + # The original ten-call budget includes every native page. Path length + # changes which planned settlement actions fit, never when EOF is reached. + budgeted_steps = [("validation", initial_page_count + 4), + ("durable_writeback", initial_page_count + 7), + ("quota_spend", initial_page_count + 8), + ("terminal_closeout", initial_page_count + 9)] assert receipt["settlement_receipt_steps"] == ( - ["validation", "durable_writeback", "quota_spend", "terminal_closeout"] if passed else ["validation"] + ["validation", "durable_writeback", "quota_spend", "terminal_closeout"] if passed + else [kind for kind, ordinal in budgeted_steps + if ordinal <= TERMINAL_SETTLEMENT_TOOL_BEHAVIOR_MAX_CALLS] if page_bytes == 12000 + else ["validation"] ) assert (tmp_path / "actor/project/fixture/settlement-proof.json").read_bytes() == fixture.proof_target.read_bytes() assert outside.read_text() == "Owner artifact.\n" if correct_input: - assert receipt["tool_call_count"] == 8 - assert receipt["observed_tool_sequence"].count("progress_observation_invalid_arguments") == 1 + assert receipt["tool_call_count"] == (10 if page_bytes is not None else 8) + assert receipt["observed_tool_sequence"].count("progress_observation_invalid_arguments") == ( + int(initial_page_count + 6 <= TERMINAL_SETTLEMENT_TOOL_BEHAVIOR_MAX_CALLS) + if page_bytes == 12000 else 1 + ) + assert receipt["failure_code"] == ("tool_call_budget_exhausted" if page_bytes == 12000 else None) + + +def test_terminal_first_read_page_size_preserves_complete_original_selection( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) + fixture = _build_fixture(tmp_path / "fixture") + + def execute(command): + return json.loads(execute_loopx_cli( + command, source_root=fixture.source_root, project_root=fixture.project_root, + )) + + navigation = execute(fixture.quota_guard_command.replace( + '"${LOOPX_TURN:?}"', "terminal-page-selection", + )) + command = navigation["first_read_command"] + parts: list[str] = [] + while command: + page = execute(command) + parts.append(page["content"]) + command = page.get("next_command") if not page["eof"] else None + assert len(parts) <= 4 + assert len(parts) > 1 + tokens = shlex.split(navigation["first_read_command"]) + for option in ("--page-bytes", "--max-page-output-bytes"): + tokens[tokens.index(option) + 1] = "64000" + bounded = execute(shlex.join(tokens)) + assert bounded["eof"] is True + assert bounded["content"] == "".join(parts) @pytest.mark.skipif(not shell_isolation_available(), reason="Draft authoring requires native OS isolation") @@ -497,6 +599,214 @@ def writeback(request): assert (tmp_path / "actor/project/fixture/settlement-proof.json").read_bytes() == fixture.proof_target.read_bytes() +@pytest.mark.skipif(not shell_isolation_available(), reason="Draft authoring requires native OS isolation") +def test_terminal_authors_missing_draft_after_unchanged_cli_rejection(tmp_path: Path) -> None: + fixture = _build_fixture(tmp_path / "oracle") + before_rejection: dict[str, bytes] = {} + + def authority_files(): + root = tmp_path / "actor" + return {str(path.relative_to(root)): path.read_bytes() + for tree in (root / "runtime", root / "project/.codex", root / "project/.loopx", root / "home") + for path in tree.rglob("*") if path.is_file()} + + def initial_read(request): + navigation = json.loads(request["messages"][-1]["content"]) + tokens = shlex.split(navigation["first_read_command"]) + for option in ("--page-bytes", "--max-page-output-bytes"): + tokens[tokens.index(option) + 1] = "64000" + return ScriptedExecToolAction(command=shlex.join(tokens)) + + def latest_context_page(request): + for message in reversed(request["messages"]): + if message.get("role") != "tool" or not message["content"].startswith("{"): + continue + page = json.loads(message["content"]) + if page.get("schema_version") == "context_capture_selection_page_v1": + return page + raise AssertionError("Expected the current capture's completed selected read") + + def vision_read(request): + page = latest_context_page(request) + assert page["eof"] is True + assert json.loads(page["content"])["selections"] + navigation = json.loads(next(message["content"] for message in request["messages"] + if message.get("role") == "tool")) + return ScriptedExecToolAction(command=navigation["recovery"]["command"] + + " --select /interaction_contract/cli_channel/settlement_plan/ordered_steps/1/vision_authoring") + + def missing_writeback(request): + page = latest_context_page(request) + assert page["eof"] is True + assert json.loads(page["content"])["selections"][0]["value"]["minimal_example"] + assert not (tmp_path / "actor/project/.scratch/vision.json").exists() + before_rejection.update(authority_files()) + return writeback(request) + + def draft(request): + rejected = QuotaToolContext.from_output(request["messages"][-1]["content"]).packet + assert rejected["ok"] is False and rejected["appended"] is False + assert authority_files() == before_rejection + vision = { + "schema_version": "goal_vision_replan_contract_v0", "state": "vision_closed", + "vision_patch": {"acceptance_summary": "Original proof and Todo validation were checked."}, + "path_delta": { + "outcome": "no_change", "prior_assumption": "Delivery needed validation.", + "observed_reality": "The proof and original Todo validation were checked.", + "retained": ["Validated delivery"], "evidence_refs": ["fixture-settlement-proof"], + }, + } + return ScriptedExecToolAction(command="cat > .scratch/vision.json <<'JSON'\n" + + json.dumps(vision) + "\nJSON") + + def writeback(request): + return ScriptedExecToolAction(command=_writeback_action(request).command + + " --agent-vision-json .scratch/vision.json") + + transport = ScriptedDoubaoExecTransport([ + ScriptedExecToolAction(command=fixture.quota_guard_command), initial_read, + ScriptedExecToolAction(command="cat fixture/settlement-proof.json"), vision_read, + partial(_completion_action, kind="validation"), missing_writeback, draft, + writeback, _spend_action, _completion_action, + ]) + receipt = DoubaoTerminalSettlementToolBehaviorActor( + api_key="test-only-placeholder", transport=transport, + ).qualify(qualification_id="missing-draft-correction", fixture_root=tmp_path / "actor") + assert receipt["qualification_passed"] is True, receipt + assert receipt["tool_call_count"] == len(transport.requests) == TERMINAL_SETTLEMENT_TOOL_BEHAVIOR_MAX_CALLS + assert receipt["settlement_receipt_steps"] == [ + "validation", "durable_writeback", "quota_spend", "terminal_closeout", + ] + assert (tmp_path / "actor/project/fixture/settlement-proof.json").read_bytes() == fixture.proof_target.read_bytes() + + +@pytest.mark.parametrize("path_kind", ["outside", "symlink"]) +def test_terminal_missing_draft_recovery_does_not_allow_path_escape(tmp_path: Path, path_kind: str) -> None: + if path_kind == "symlink" and os.name == "nt": + pytest.skip("Windows symlink creation needs separate privileges") + fixture = _build_fixture(tmp_path / "oracle") + outside = tmp_path / "owner-vision.json" + outside.write_text("Private owner artifact.\n") + + def writeback(request): + target = outside + if path_kind == "symlink": + target = tmp_path / "actor/project/.scratch/vision.json" + target.parent.mkdir(exist_ok=True) + target.symlink_to(outside) + return ScriptedExecToolAction(command=_writeback_action(request).command + + " --agent-vision-json " + shlex.quote(str(target))) + + transport = ScriptedDoubaoExecTransport([ + ScriptedExecToolAction(command=fixture.quota_guard_command), + ScriptedExecToolAction(command="cat fixture/settlement-proof.json"), + partial(_completion_action, kind="validation"), writeback, + ]) + receipt = DoubaoTerminalSettlementToolBehaviorActor( + api_key="test-only-placeholder", transport=transport, + ).qualify(qualification_id="escaped-vision-draft", fixture_root=tmp_path / "actor") + assert receipt["qualification_passed"] is False + assert receipt["failure_code"] == "vision_draft_outside_workspace" + assert receipt["settlement_receipt_steps"] == ["validation"] + assert outside.read_text() == "Private owner artifact.\n" + + +@pytest.mark.skipif(not shell_isolation_available(), reason="Draft authoring requires native OS isolation") +@pytest.mark.parametrize("failed_drafts", [1, 7]) +def test_terminal_shell_error_can_be_corrected_without_resetting_budget( + tmp_path: Path, failed_drafts: int, +) -> None: + fixture = _build_fixture(tmp_path / "oracle") + private_output = "synthetic-private-draft-output" + vision = { + "schema_version": "goal_vision_replan_contract_v0", + "state": "vision_closed", + "vision_patch": {"acceptance_summary": "Original proof and Todo validation were checked."}, + "path_delta": { + "outcome": "no_change", "prior_assumption": "Delivery needed validation.", + "observed_reality": "The proof and original Todo validation were checked.", + "retained": ["Validated delivery"], "evidence_refs": ["fixture-settlement-proof"], + }, + } + + def corrected_draft(request): + feedback = json.loads(request["messages"][-1]["content"]) + assert feedback["error_code"] == "shell_nonzero" + assert feedback["exit_code"] == 7 + assert private_output in feedback["output"] + return ScriptedExecToolAction( + command="cat > .scratch/vision.json <<'JSON'\n" + json.dumps(vision) + "\nJSON", + ) + + def writeback(request): + return ScriptedExecToolAction( + command=_writeback_action(request).command + " --agent-vision-json .scratch/vision.json", + ) + + transport = ScriptedDoubaoExecTransport([ + ScriptedExecToolAction(command=fixture.quota_guard_command), + ScriptedExecToolAction(command="cat fixture/settlement-proof.json"), + partial(_completion_action, kind="validation"), + *([ScriptedExecToolAction(command=( + "python3 -c " + shlex.quote( + "from pathlib import Path; Path('.scratch/partial.json').write_text('{'); " + f"print({private_output!r}); raise SystemExit(7)" + ) + ))] * failed_drafts), + corrected_draft, writeback, _spend_action, _completion_action, + ]) + receipt = DoubaoTerminalSettlementToolBehaviorActor( + api_key="test-only-placeholder", transport=transport, + ).qualify(qualification_id="terminal-shell-correction", fixture_root=tmp_path / "actor") + + assert receipt["qualification_passed"] is (failed_drafts == 1), receipt + assert receipt["tool_call_count"] == (8 if failed_drafts == 1 else 10) + assert receipt["observed_tool_sequence"].count("shell_nonzero") == failed_drafts + assert receipt["settlement_receipt_steps"] == ( + ["validation", "durable_writeback", "quota_spend", "terminal_closeout"] + if failed_drafts == 1 else ["validation"] + ) + assert receipt["failure_code"] == (None if failed_drafts == 1 else "tool_call_budget_exhausted") + assert private_output not in json.dumps(receipt) + assert (tmp_path / "actor/project/fixture/settlement-proof.json").read_bytes() == fixture.proof_target.read_bytes() + + +@pytest.mark.skipif(not shell_isolation_available(), reason="Draft authoring requires native OS isolation") +@pytest.mark.parametrize("protected_target", ["proof", "registry"]) +def test_terminal_shell_error_with_protected_mutation_stops_before_writeback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, protected_target: str, +) -> None: + from loopx.control_plane.testing.vision_shell_host import VisionShellHost + + fixture = _build_fixture(tmp_path / "oracle") + execute = VisionShellHost.execute + + def faulty_host(shell, command): + output, exit_code = execute(shell, command) + # Simulate a broken isolation host, not an authorized draft mutation. + target = (shell.project / "fixture/settlement-proof.json" + if protected_target == "proof" else shell.fixture_root / "runtime/registry.global.json") + target.write_text("synthetic-protected-mutation\n") + return output, exit_code + + monkeypatch.setattr(VisionShellHost, "execute", faulty_host) + transport = ScriptedDoubaoExecTransport([ + ScriptedExecToolAction(command=fixture.quota_guard_command), + ScriptedExecToolAction(command="cat fixture/settlement-proof.json"), + partial(_completion_action, kind="validation"), + ScriptedExecToolAction(command="python3 -c 'raise SystemExit(7)'"), + _writeback_action, _spend_action, _completion_action, + ]) + receipt = DoubaoTerminalSettlementToolBehaviorActor( + api_key="test-only-placeholder", transport=transport, + ).qualify(qualification_id="terminal-mutated-shell-failure", fixture_root=tmp_path / "actor") + + assert receipt["qualification_passed"] is False + assert receipt["failure_code"] == "workspace_authoring_failed" + assert receipt["tool_call_count"] == len(transport.requests) == 4 + assert receipt["settlement_receipt_steps"] == ["validation"] + + def test_terminal_input_rejections_do_not_reset_the_call_budget(tmp_path: Path) -> None: fixture = _build_fixture(tmp_path / "oracle") diff --git a/tests/control_plane/test_todo_completion_result_projection.py b/tests/control_plane/test_todo_completion_result_projection.py index 0df396fc2d..db8ca84a6a 100644 --- a/tests/control_plane/test_todo_completion_result_projection.py +++ b/tests/control_plane/test_todo_completion_result_projection.py @@ -195,7 +195,11 @@ def test_completion_result_stays_canonical_and_projection_recovers( assert "Keep this line unchanged and outside the Todo sections." in displayed assert "Write the accepted result" in displayed assert "Next independent work" in displayed - assert "completion_result" not in displayed + assert "completion_result=" not in displayed + if provider == "sqlite": + pointer = next(line for line in displayed.splitlines() + if "scope=active_todo" in line and "todo_id=todo_result" in line) + assert "completion_result" in pointer.split("omitted_fields=", 1)[1].split(" ", 1)[0].split(",") assert result_body not in displayed assert binding["sha256"] not in displayed diff --git a/tests/control_plane/test_todo_priority_authoring.py b/tests/control_plane/test_todo_priority_authoring.py index 0d93494513..4c05702b6f 100644 --- a/tests/control_plane/test_todo_priority_authoring.py +++ b/tests/control_plane/test_todo_priority_authoring.py @@ -72,7 +72,7 @@ def rows(value): assert "[P4] New task title" not in state.read_text() ordinary = cli("todo", "add", "--role", "agent", "--text", "Investigate P0 prose without declaring priority") assert read(ordinary["todo_id"])["priority"] == "P1" - selected = cli("quota", "should-run", "--codex-app", "--agent-id", AGENT_ID, + selected = cli("quota", "should-run", "--context-delivery", "off", "--codex-app", "--agent-id", AGENT_ID, "--todo-id", ordinary["todo_id"], "--turn-instance-id", "priority-selection", ) assert selected["should_run"] is True gate = cli("todo", "add", "--role", "user", "--task-class", "user_gate", diff --git a/tests/control_plane/test_todo_projection_recovery.py b/tests/control_plane/test_todo_projection_recovery.py index 1c77943984..8bae866d71 100644 --- a/tests/control_plane/test_todo_projection_recovery.py +++ b/tests/control_plane/test_todo_projection_recovery.py @@ -107,7 +107,11 @@ def test_long_committed_todo_rebuilds_from_the_fresh_head_without_a_second_creat before = _read(runtime) record = before["todos"][0] assert record["text"] == text and record["title"] == title - assert text in state.read_text() + displayed = next(line for line in state.read_text().splitlines() if line.startswith("- [ ] " + text[:80])) + if provider == "sqlite": + assert displayed.endswith("...") and "retain the final obligation" not in displayed + else: + assert displayed == "- [ ] " + text # A stale compatibility view must be repaired from the canonical head, # not by editing the accepted Todo or repeating its committed mutation. @@ -115,7 +119,11 @@ def test_long_committed_todo_rebuilds_from_the_fresh_head_without_a_second_creat code, delivered = _run(registry, before["provider_revision"], "--execute") assert code == 0 and delivered["status"] == "delivered", delivered rendered = state.read_text() - assert text in rendered and "Keep this narrative." in rendered + assert displayed in rendered and "Keep this narrative." in rendered + assert delivered["omitted_active_records"] == ([ + {"todo_id": record["todo_id"], "role": "agent", "omitted_fields": ["text"], + "query": f"loopx todo list --goal-id goal-a --todo-id {record['todo_id']}"}, + ] if provider == "sqlite" else []) assert delivered["parse_render_parity"] is True assert _read(runtime) == before code, replay = _run(registry, before["provider_revision"], "--execute") @@ -360,12 +368,25 @@ def test_production_scale_rebuild_retains_order_and_requires_private_declaration assert code == 0, result assert result["todo_count"] == 464 rendered = state.read_text() - for role, count in (("agent", 256), ("user", 208)): - positions = [ - rendered.index(f"todo_id=todo_fixture_{role}_{index:03d} status=") - for index in range(count) - ] - assert positions == sorted(positions) + active, archived, _ = parse_todo_source(rendered) + assert not archived + all_rows = before["todos"] + assert len(all_rows) == 464 + expected_by_role = {} + for role in ("agent", "user"): + rows = [row for row in all_rows if row["role"] == role] + if provider == "sqlite": + completed = [row["todo_id"] for row in rows if row["done"]] + # The fixture has one resume dependency on an early completed + # Agent Todo. Retain it beside the documented five-row tail. + visible_completed = set(completed[-5:]) | {"todo_fixture_agent_003"} + rows = [row for row in rows if not row["done"] or row["todo_id"] in visible_completed] + expected_by_role[role] = [row["todo_id"] for row in rows] + assert [row["todo_id"] for row in active[role]] == expected_by_role[role] + if provider == "sqlite": + omitted = 464 - sum(len(rows) for rows in expected_by_role.values()) + assert omitted > 0 + assert f"active_done_omitted={omitted}" in rendered assert _read(runtime) == before code, replay = _run(registry, before["provider_revision"], "--execute") assert code == 0 and replay["changed"] is False @@ -381,7 +402,7 @@ def test_production_scale_rebuild_retains_order_and_requires_private_declaration active, archived, _ = parse_todo_source(state.read_text()) parsed_ids = [row["todo_id"] for rows in (*active.values(), archived) for row in rows] assert "todo_example_only" not in parsed_ids - assert len(parsed_ids) == 464 + assert parsed_ids == [todo_id for role in active for todo_id in expected_by_role[role]] assert _read(runtime) == before # A routine refresh must also drain the full source, including records well diff --git a/tests/control_plane/test_turn_envelope_budget_warning.py b/tests/control_plane/test_turn_envelope_budget_warning.py index b6c2e3ebbc..4884c94673 100644 --- a/tests/control_plane/test_turn_envelope_budget_warning.py +++ b/tests/control_plane/test_turn_envelope_budget_warning.py @@ -129,6 +129,7 @@ def test_real_turn_plan_cli_warns_without_state_writes(tmp_path, enabled): "json", "turn", "plan", + "--include-transaction-detail", "--goal-id", "loopx-turn-fixture", "--agent-id", @@ -139,7 +140,9 @@ def test_real_turn_plan_cli_warns_without_state_writes(tmp_path, enabled): plan = json.loads(result.stdout) assert plan["ok"] is True assert plan["route"]["would_invoke_host"] is True - envelope = plan["turn_envelope"] + # The complete plan is the current source. Compatibility callers can + # still measure the versioned envelope from that exact decision. + envelope = build_turn_envelope(plan["work_contract"]) assert bool(envelope.get("agent_context")) is enabled assert envelope["compaction"]["warning"]["code"] == "turn_envelope_budget_exceeded" assert envelope["compaction"]["envelope_utf8_bytes"] == len(_wire(envelope)) @@ -172,7 +175,7 @@ def test_installed_skill_defers_delegation_policy_to_enabled_provider(tmp_path): ) contribution = enabled["contributions"][0] assert any( - "max_children is a configured ceiling" in item + "within max_children" in item for item in contribution["guidance"] ) assert contribution["facts"]["capacity_contract"] == { diff --git a/tests/control_plane/test_turn_launch_identity.py b/tests/control_plane/test_turn_launch_identity.py index bba5195961..85485651da 100644 --- a/tests/control_plane/test_turn_launch_identity.py +++ b/tests/control_plane/test_turn_launch_identity.py @@ -9,6 +9,7 @@ from host_process_fixture import write_python_host_fixture from test_replan_successor_guard_reentry import AGENT, GOAL, _fixture +from loopx.control_plane.quota.turn_envelope import read_turn_plan_work def _host(root: Path, *, material: bool) -> Path: @@ -81,7 +82,7 @@ def test_native_cli_uses_original_binding_and_replays_without_new_effects( obligation_id = None subject = todo_id if not todo_id: - envelope = preview["turn_envelope"] + envelope = read_turn_plan_work(preview) obligation_id = envelope["replan_action_packet"]["obligation_id"] assert obligation_id.startswith("replan-") subject = f"autonomous_replan:{obligation_id}" diff --git a/tests/control_plane/test_turn_todoless_settlement.py b/tests/control_plane/test_turn_todoless_settlement.py index daf556a172..a9752d7093 100644 --- a/tests/control_plane/test_turn_todoless_settlement.py +++ b/tests/control_plane/test_turn_todoless_settlement.py @@ -27,7 +27,7 @@ def _fixture(tmp_path, monkeypatch, provider, *, replan=True): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) if replan: - _configure_autonomous_replan_fixture(project, runtime, registry) + _configure_autonomous_replan_fixture(project, runtime, registry, stride_strategy="off") else: state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text('---\nstatus: active\nowner_mode: goal\nobjective: "Answer the fixture request."\n---\n\n' @@ -144,10 +144,12 @@ def test_bound_todoless_planning_executes_and_settles_once(tmp_path, monkeypatch prompt = (project / "received.txt").read_text() assert identity["replan_obligation_id"] in prompt # Inspect the actual model input, not only the saved complete fixture. - authority = json.loads(prompt.split("Turn request:\n", 1)[1])["turn_authority"] - packet = authority["replan_action_packet"] + request = json.loads(prompt.split("Turn request:\n", 1)[1]) + authority = request["work_contract"] + assert authority["replan_action_packet"]["obligation_id"] == identity["replan_obligation_id"] + packet = request["detail_ref"]["replan_action_packet"] assert "read autonomous_replan_obligation.replan_context" not in prompt - assert "Read replan_action_packet.review" in prompt + assert "Read detail_ref.replan_action_packet.review" in prompt assert "core_goal" not in authority # The mandatory review owns its only model copy. stages = {} for stage in ("review", "writeback"): @@ -161,8 +163,8 @@ def test_bound_todoless_planning_executes_and_settles_once(tmp_path, monkeypatch assert _classification_count(runtime, "quota_slot_spent") == 1 # Read the actual source referenced in the Host prompt, preserving the # original observation instead of re-entering quota to reconstruct it. - envelope = journal["plan"]["turn_envelope"] - detail = envelope["detail_ref"] + detail = request["detail_ref"] + assert journal["plan"]["detail_ref"] == detail captured = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True) source = json.loads(captured.stdout) assert source["replan_action_packet"]["obligation_id"] == identity["replan_obligation_id"] diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index d61b3a71b0..6de0c9e551 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -297,7 +297,9 @@ def configure(*arguments): # A capture is an immutable observation. Reading its detail cannot admit # changed work, remove a wait or silently rebind the owner's checks. - code, current = run(*guard, "--todo-id", target, "--turn-instance-id", "current-delivery") + code, current = run(*guard, "--todo-id", target, "--turn-instance-id", "current-delivery", + "--decision-output-dir", str(tmp_path / "current-decision"), + "--context-delivery", "on", "--delta-delivery", "auto") assert code == (0 if change == "checks_removed" else 1), current if code == 0: current = read_cli_policy_payload(json.dumps(current), returncode=0) diff --git a/tests/control_plane/test_work_context_host_delivery.py b/tests/control_plane/test_work_context_host_delivery.py index c913e2eef1..2061396d4b 100644 --- a/tests/control_plane/test_work_context_host_delivery.py +++ b/tests/control_plane/test_work_context_host_delivery.py @@ -152,8 +152,14 @@ def test_authored_requirements_survive_capture_projection_and_host(tmp_path, mon @pytest.mark.parametrize('provider', ['file', 'sqlite']) -@pytest.mark.parametrize('declaration', ['envelope', 'signature', 'coverage']) -def test_real_turn_refuses_unsupported_consumer_version_before_launch(tmp_path, monkeypatch, provider, declaration): +@pytest.mark.parametrize(('declaration', 'diagnostic'), [ + ('version', 'unsupported complete Turn decision version'), + ('hash_codec', 'unsupported complete Turn decision hash codec'), + ('content_hash', 'complete Turn decision content integrity mismatch'), +]) +def test_real_turn_refuses_unsupported_consumer_version_before_launch( + tmp_path, monkeypatch, provider, declaration, diagnostic, +): import contextlib import io import sys @@ -163,19 +169,20 @@ def test_real_turn_refuses_unsupported_consumer_version_before_launch(tmp_path, isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_live_fixture(tmp_path, provider=provider) - original_builder = turn.build_turn_envelope + original_builder = turn.build_loopx_turn_plan observed = [] - def incompatible_writer(*args, **kwargs): - packet = original_builder(*args, **kwargs) - if declaration == 'envelope': + def incompatible_writer(decision, **kwargs): + packet = copy.deepcopy(decision) + assert packet['schema_version'] == 'loopx_turn_envelope_v1' + if declaration == 'version': packet['schema_version'] = 'unsupported_work_contract' else: - packet['action_signature']['schema_version' if declaration == 'signature' else 'coverage'] = 'unsupported_required_semantics' - observed.append(copy.deepcopy(packet)) - return packet + packet['contract_binding'][declaration] = 'unsupported_required_semantics' + observed.append(packet) + return original_builder(packet, **kwargs) - monkeypatch.setattr(turn, 'build_turn_envelope', incompatible_writer) + monkeypatch.setattr(turn, 'build_loopx_turn_plan', incompatible_writer) marker = project / 'host-started' output = io.StringIO() with contextlib.redirect_stdout(output): @@ -187,8 +194,7 @@ def incompatible_writer(*args, **kwargs): result = json.loads(output.getvalue()) assert observed and code != 0, result assert not marker.exists() - assert 'unsupported TurnEnvelope' in result['error'], result - assert 'compatible consumer' in result['error'], result + assert diagnostic in result['error'], result # Generic CLI error effects are unknown; the marker and ledger independently # prove this pre-dispatch refusal, without interpreting null as false. assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_work_lane_narrative_cli.py b/tests/control_plane/test_work_lane_narrative_cli.py index d73021c253..cdb9fd4989 100644 --- a/tests/control_plane/test_work_lane_narrative_cli.py +++ b/tests/control_plane/test_work_lane_narrative_cli.py @@ -70,7 +70,8 @@ def _guard( result = subprocess.run( [sys.executable, "-c", "from loopx.cli import main; raise SystemExit(main())", "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", - "quota", "should-run", "--goal-id", GOAL_ID, "--runtime-profile", "generic_cli"], + "quota", "should-run", "--goal-id", GOAL_ID, "--runtime-profile", "generic_cli", + "--context-delivery", "off"], cwd=project, capture_output=True, text=True, check=True, env={**os.environ, "LOOPX_USAGE_PING": "0"}, ) diff --git a/tests/control_plane_ts/quota_settlement_plan.test.ts b/tests/control_plane_ts/quota_settlement_plan.test.ts index 9b13a9763d..b5dfcc8f7d 100644 --- a/tests/control_plane_ts/quota_settlement_plan.test.ts +++ b/tests/control_plane_ts/quota_settlement_plan.test.ts @@ -63,6 +63,10 @@ test("ordinary completion is conditional on deliverable acceptance, not Turn acc assert.match(plan.steps[1].precondition, /--agent-vision-json/); assert.match(plan.steps[1].precondition, /path_delta.*evidence_refs/); assert.match(plan.steps[1].precondition, /acceptance_summary/); + assert.match(plan.steps[1].precondition, /typed progress flags only when this Turn's evidence supports/); + assert.match(plan.steps[1].precondition, /--progress-coverage-scope-id from verified scope coverage/); + assert.match(plan.steps[1].precondition, /terminal closeout follows spend/); + assert.doesNotMatch(plan.steps[1].command_template!, /--progress-coverage-scope-id/); assert.match(plan.steps[2].precondition, /declared completion validation/); assert.match(plan.steps[3].precondition, /matching writeback and quota spend receipts/); assert.equal(plan.steps[3].conditional, true); @@ -130,8 +134,10 @@ test("semantic feedback checkpoint is an exact variant of the current writeback, assert.equal(plan.steps[1].semantic_closeout_command_template, checkpoint); if (delivery_boundary === "in_flight_continuation") { assert.match(plan.steps[1].precondition, - /^validation succeeded; Choose one: command_template for ordinary in_flight_continuation without new decision evidence; semantic_closeout_command_template for new decision evidence, route change or Todo completion/); + /Use command_template for ordinary in_flight_continuation without new decision evidence/); } + assert.match(plan.steps[1].precondition, + /Use semantic_closeout_command_template with --agent-vision-json for new decision evidence, route change or Todo completion/); assert.deepEqual(plan.steps[1].vision_authoring, visionAuthoringContract()); const serialized = settlementPlanPayload(plan).ordered_steps as Record[]; assert.equal(serialized[1].semantic_closeout_command_template, checkpoint); diff --git a/tests/test_browser_workspace_backend_provenance.py b/tests/test_browser_workspace_backend_provenance.py index 9c6855b5a1..fd7863bc0e 100644 --- a/tests/test_browser_workspace_backend_provenance.py +++ b/tests/test_browser_workspace_backend_provenance.py @@ -169,7 +169,7 @@ def test_research_history_owner_ignores_foreign_pythonpath( captures = ROOT / "apps/presentation/dashboard/src/data/fixtures" names = ["edgebench-research-history.json", "edgebench-graph-checkpoint-history.json", "edgebench-ad-checkpoint-history.json"] - selected = [captures / name for name in names] + selected = [captures / "edgebench-grid-context-on-20261103-history.json"] if explicit: destination = tmp_path / "source captures '引用'" destination.mkdir() diff --git a/tests/test_chat_agent.py b/tests/test_chat_agent.py index 92a13a9e5d..21147f05b3 100644 --- a/tests/test_chat_agent.py +++ b/tests/test_chat_agent.py @@ -310,9 +310,12 @@ def write(packet): monkeypatch.setattr(session, "_write", write) session.send(material, output_schema={"type": "object"} if mode == "structured" else None) prompt = sent[0]["input"][0]["text"] - assert prompt.endswith(material) if mode == "structured": assert prompt == material + else: + request, separator, footer = prompt.rpartition("\n\nAfter the visible answer,") + assert separator and request.endswith(material) + assert footer.endswith(chat_agent.CHAT_REVIEW_CLOSE_TAG) if mode == "review": assert "do not re-execute" in prompt for blank in ["", " \r\n\t\u00a0"]: diff --git a/tests/test_chat_intake_evaluation.py b/tests/test_chat_intake_evaluation.py index 1499c06294..5c694ebc91 100644 --- a/tests/test_chat_intake_evaluation.py +++ b/tests/test_chat_intake_evaluation.py @@ -54,6 +54,22 @@ def test_satisfied_outcome_needs_evidence_and_no_competing_effect(): assert not evaluation.score(case, {"message": "Done."})["passed"] +def test_accepted_report_fixture_supplies_the_content_it_claims_to_have_read(): + suite = json.loads((ROOT / "examples/evaluations/chat-intake.public.json").read_text()) + case = next(row for row in suite["cases"] if row["id"] == "already-accepted-report") + context = suite["contexts"][case["context_ref"]] + report = next(row for row in context["external_observations"] if row["ref"] == "report-v2") + assert report["content_read"] is True and report["status"] == "accepted" + assert isinstance(report["content"], str) and report["content"].strip() + assert "report-v2" in report["content"] + assert "synthetic" in report["content"] + assert evaluation.score(case, {"message": report["content"]})["passed"] + assert not evaluation.score(case, { + "message": report["content"], + "context_handoff": {"goal_id": "cashflow", "agent_id": "researcher"}, + })["passed"] + + def test_shared_intake_guidance_precedes_handoff_without_widening_runtime(): from loopx.chat_agent import CONVERSATION_INTENT_RESOLUTION_INSTRUCTION, _turn_prompt from loopx.chat_manager import manager_agent_objective diff --git a/tests/test_chat_lark_api_contract.py b/tests/test_chat_lark_api_contract.py index c8ac755c1c..2a70d4c025 100644 --- a/tests/test_chat_lark_api_contract.py +++ b/tests/test_chat_lark_api_contract.py @@ -323,7 +323,7 @@ def test_runtime_snapshot_reuses_private_goal_channel_bindings( ) -def test_chat_server_closes_lark_goal_topic_runtime(monkeypatch: Any) -> None: +def test_chat_server_closes_lark_goal_topic_runtime() -> None: closed: list[str] = [] class Closer: @@ -333,14 +333,13 @@ def __init__(self, name: str) -> None: def close(self) -> None: closed.append(self.name) - monkeypatch.setattr(ThreadingHTTPServer, "server_close", lambda _self: None) - server = object.__new__(ChatHTTPServer) + server = ChatHTTPServer(("127.0.0.1", 0), BaseHTTPRequestHandler, bind_and_activate=False) server.lark_app_setup_manager = Closer("setup") # type: ignore[assignment] server.lark_goal_topic_runtime = Closer("topics") # type: ignore[attr-defined] server.runtime_controller = Closer("chat") # type: ignore[assignment] server.server_close() - assert closed == ["setup", "topics", "chat"] + assert closed == ["topics", "setup", "chat"] def test_lark_apps_reports_safe_missing_cli_diagnostic() -> None: diff --git a/tests/test_chat_multi_handoff.py b/tests/test_chat_multi_handoff.py index b6daeb1e62..d630ba84d9 100644 --- a/tests/test_chat_multi_handoff.py +++ b/tests/test_chat_multi_handoff.py @@ -88,7 +88,14 @@ def send(): assert not any(row.get("collaborations") or row.get("collaboration") for row in snapshot["messages"]) results = [row for row in snapshot["messages"] if row.get("origin") == "manager_followup"] assert len(results) == len(receipts) - assert {row["text"].split("Finding from ")[-1] for row in results} == {r["goal_id"] for r in receipts} + expected = { + f"Finding from {receipt['goal_id']}": receipt["request_id"][:8] + for receipt in receipts + } + for row in results: + body, _, attribution = row["text"].partition("\n\n") + assert attribution.endswith(expected.pop(body)) + assert not expected assert registry.read_bytes() == before diff --git a/tests/test_chat_ordinary_project.py b/tests/test_chat_ordinary_project.py index d2477e6a4c..17a4b9aeb1 100644 --- a/tests/test_chat_ordinary_project.py +++ b/tests/test_chat_ordinary_project.py @@ -228,8 +228,10 @@ def test_writable_project_turns_use_project_skills_without_manager_work_selectio assert "Before preparing a new Goal" not in prompt assert "context_delegation catalog" not in prompt assert "A protected_action is only an untrusted proposal" in prompt - assert prompt.endswith(f"Operator user message:\n{message}") - envelope = json.loads(prompt.split(CHAT_REVIEW_OPEN_TAG, 1)[1].split(CHAT_REVIEW_CLOSE_TAG, 1)[0]) + request, footer = prompt.split("Operator user message:\n", 1)[1].split("\n\n", 1) + assert request == message + assert footer.endswith(CHAT_REVIEW_CLOSE_TAG) + envelope = json.loads(footer.split(CHAT_REVIEW_OPEN_TAG, 1)[1].split(CHAT_REVIEW_CLOSE_TAG, 1)[0]) assert envelope["schema_version"] == "loopx_chat_agent_response_v0" assert envelope["message"] == "" assert envelope["proposals"] == [] diff --git a/tests/test_claude_goal_release_qualification.py b/tests/test_claude_goal_release_qualification.py index ecfd873d44..26effac000 100644 --- a/tests/test_claude_goal_release_qualification.py +++ b/tests/test_claude_goal_release_qualification.py @@ -1,6 +1,7 @@ """Ordinary CI covers policy/transport; no test here invokes a model.""" import asyncio +from copy import deepcopy import importlib.util import json import os @@ -163,8 +164,15 @@ async def exercise(): payload = json.loads(guard.content[0].text) assert payload["ok"] is True and payload["selected_todo"]["todo_id"] == "todo_reducer" contract = payload["interaction_contract"] - assert contract["cli_channel"]["next_cli_actions"] == [] + channel = contract["cli_channel"] + assert channel["turn_binding_required"] is True + assert payload["normal_delivery_allowed"] is False + action, = channel["next_cli_actions"] + argv = shlex.split(action) + assert argv[argv.index("quota"):][:2] == ["quota", "should-run"] + assert argv[argv.index("--turn-instance-id") + 1] == "" assert contract["mcp_channel"]["delivery_executor"] == "complete_task" + assert contract["mcp_channel"]["delivery_todo_id"] is None assert contract["mcp_channel"]["vision_authoring"]["fields"]["vision_patch"]["acceptance_summary"] == 420 rejected = await session.call_tool("claim_task", {"todo_id": "todo_reducer", "agent_id": "other-agent"}) assert json.loads(rejected.content[0].text)["ok"] is False @@ -253,12 +261,29 @@ def test_replan_fixture_reentry_executes_bound_vision_settlement_once(tmp_path): assert "--progress-result-class" not in refresh_action assert "--source visible-goal" in spend_action + stride = deepcopy(bound["replan_action_packet"]["stride_control"]["minimal_example"]) + stride["axes"] = { + "work_slice": {"plan": "Verify the listed files against the integrity acceptance.", + "reason": "The original inventory established filenames."}, + "route_distance": {"plan": "Extend the existing inventory route with byte-level verification.", + "reason": "The verified asset list is a valid starting point."}, + "validation_scope": {"plan": "Run an independent verifier against modified and intact copies.", + "reason": "An unchanged copy alone cannot reveal a permissive verifier."}, + } + stride["next_probe"] = { + "scope": "Run the verifier on the isolated manifest fixtures.", + "prediction": "The intact fixture passes and changed copies fail.", + "adoption_criterion": "Adopt the verifier when it distinguishes the intact and modified inputs.", + "cost_bound": "One run per frozen fixture.", + "minimum_evidence": "Original and contrary-case verifier results.", + } vision_path = project / "next-vision.json" vision_path.write_text( json.dumps( { "schema_version": "goal_vision_replan_contract_v0", "state": "vision_patch_proposed", + "stride_decision": stride, "vision_patch": { "vision_summary": "Verify asset contents and deliver a durable integrity manifest.", "role_scope": "Implement and independently validate the bounded manifest integrity stage.", diff --git a/tests/test_collaboration_demo.py b/tests/test_collaboration_demo.py index c3a9aadf95..7d767dec4a 100644 --- a/tests/test_collaboration_demo.py +++ b/tests/test_collaboration_demo.py @@ -58,6 +58,7 @@ def test_demo_preparation_has_executable_independent_worktree(tmp_path): "json", "turn", "plan", + "--include-transaction-detail", "--goal-id", "allocation-demo", "--agent-id", @@ -75,9 +76,8 @@ def test_demo_preparation_has_executable_independent_worktree(tmp_path): plan = json.loads(result.stdout) # The test supplies no provider credentials. Qualify the Goal/worktree # admission independently of provider availability and never invoke a host. - assert plan["turn_envelope"]["execution_policy"]["normal_delivery_allowed"], plan[ - "turn_envelope" - ]["reason"] + assert plan["work_contract"]["normal_delivery_allowed"], plan["work_contract"]["reason"] + assert plan["route"]["kind"] == "ready_for_host" assert not plan["effects"]["host_invoked"] # Preparation cannot overwrite an existing state; model runs need explicit opt-in. for args in (["prepare"], ["run", "--phase", "builder-1"]): diff --git a/tests/test_context_recovery_discovery.py b/tests/test_context_recovery_discovery.py index 94db5ec1de..34dad04e4f 100644 --- a/tests/test_context_recovery_discovery.py +++ b/tests/test_context_recovery_discovery.py @@ -40,7 +40,7 @@ def now(cls, tz=None): return real_datetime.now(tz) + timedelta(minutes=2) monkeypatch.setattr(external_conversations, "datetime", AfterBackoff) counts = Counter() - for owner, name in [(store, "queued_turns"), (store, "messages"), (transport.core, "pending")]: + for owner, name in [(store, "queued_turns"), (store, "messages"), (transport.core, "source_controls")]: original = getattr(owner, name) def counted(*args, _name=name, _read=original, **kwargs): counts[_name] += 1 @@ -49,10 +49,10 @@ def counted(*args, _name=name, _read=original, **kwargs): before_calls, before_requests = len(provider.calls), capture.read_bytes() observe = transport.core.context_recovery_observer() assert [observe(ref) for _ in range(4)] == [True] * 4 - assert counts == {"queued_turns": 1, "messages": 1, "pending": 1} + assert counts == {"queued_turns": 1, "messages": 1, "source_controls": 1} # The next pass rereads rather than retaining a negative/positive cache. assert transport.core.context_recovery_observer()(ref) - assert counts == {"queued_turns": 2, "messages": 2, "pending": 2} + assert counts == {"queued_turns": 2, "messages": 2, "source_controls": 2} assert len(provider.calls) == before_calls and capture.read_bytes() == before_requests diff --git a/tests/test_global_todo_readiness_characterization.py b/tests/test_global_todo_readiness_characterization.py index fc947079dc..931c841b5d 100644 --- a/tests/test_global_todo_readiness_characterization.py +++ b/tests/test_global_todo_readiness_characterization.py @@ -149,7 +149,19 @@ def now(cls, tz=None): ) assert result.returncode == 0, result.stdout + result.stderr outputs.append(result.stdout) - assert outputs[0] == outputs[1] + if command == "goal-portfolio" and output_format == "json": + # Source revisions hash the complete status read. Different + # status schemas can identify the same portfolio projection. + normalized = [] + for output in outputs: + snapshot = json.loads(output) + assert len(snapshot["goals"]) == 1 + for digest in (snapshot.pop("snapshot_id"), snapshot["goals"][0]["source"].pop("revision")): + assert digest.startswith("sha256:") and len(digest) == 71 + normalized.append(snapshot) + assert normalized[0] == normalized[1] + else: + assert outputs[0] == outputs[1] if output_format == "json": payload = json.loads(outputs[1]) if command == "goal-portfolio": diff --git a/tests/test_goal_mode_mcp_settlement.py b/tests/test_goal_mode_mcp_settlement.py index 3a8d84284d..c70e45201d 100644 --- a/tests/test_goal_mode_mcp_settlement.py +++ b/tests/test_goal_mode_mcp_settlement.py @@ -305,6 +305,8 @@ def test_real_mcp_settlement_rejects_missing_writeback_then_commits_same_identit registry, "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", @@ -366,6 +368,8 @@ def test_real_mcp_settlement_rejects_missing_writeback_then_commits_same_identit registry, "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", @@ -403,6 +407,8 @@ def test_real_mcp_settlement_rejects_missing_writeback_then_commits_same_identit registry, "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", @@ -463,6 +469,8 @@ def test_real_mcp_terminal_completion_closes_out_after_spend( registry, "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", diff --git a/tests/test_host_launch_authority.py b/tests/test_host_launch_authority.py index ccfa0e6627..3ba70ad1f8 100644 --- a/tests/test_host_launch_authority.py +++ b/tests/test_host_launch_authority.py @@ -25,6 +25,7 @@ project_registry_transaction, source_session_registry_transaction, ) +from loopx.control_plane.quota.turn_envelope import turn_host_lineage from loopx.control_plane.turn_driver import codex_cli, codex_operation_host from loopx.control_plane.goals.first_party_host_admission import FirstPartyHostGoalAdmission from loopx.control_plane.turn_driver import host_process_transport @@ -257,16 +258,16 @@ def stopped_start(**arguments): monkeypatch.setattr(codex_operation_host.CodexChatAgentSession, "start", stopped_start) else: - prompt = codex_operation_host._prompt + prepare_input = codex_operation_host.prepare_codex_cli_input - def stopped_prompt(request): + def stopped_prepare_input(request): # App-server/thread initialization already completed, but the # request must never cross the native model Turn boundary. assert native_start.exists() stop() - return prompt(request) + return prepare_input(request) - monkeypatch.setattr(codex_operation_host, "_prompt", stopped_prompt) + monkeypatch.setattr(codex_operation_host, "prepare_codex_cli_input", stopped_prepare_input) code, payload = _run(registry, runtime, project, host_arguments) assert stops and code == 1 and "goal_stopped" in json.dumps(payload), payload assert native_start.exists() is (boundary == "turn") @@ -314,14 +315,14 @@ def missing_state_start(**arguments): monkeypatch.setattr(codex_operation_host.CodexChatAgentSession, "start", missing_state_start) else: - prompt = codex_cli._prompt + prepare_input = codex_cli.prepare_codex_cli_input - def missing_state_prompt(request): - assert request["turn_envelope"]["action"]["selected_todo"]["todo_id"] == "todo_fixture0001" + def missing_state_prepare_input(request): + assert turn_host_lineage(request)["todo_id"] == "todo_fixture0001" remove_state() - return prompt(request) + return prepare_input(request) - monkeypatch.setattr(codex_cli, "_prompt", missing_state_prompt) + monkeypatch.setattr(codex_cli, "prepare_codex_cli_input", missing_state_prepare_input) code, payload = _run(registry, runtime, project, host_arguments) assert code == 1 and "goal_launch_not_allowed" in json.dumps(payload), payload @@ -387,16 +388,16 @@ def sync(**_kwargs): host_arguments, native_start, turn_start = _host(tmp_path, monkeypatch, operation_tools) if late_material: owner = codex_operation_host if operation_tools else codex_cli - prompt = owner._prompt + prepare_input = owner.prepare_codex_cli_input - def changed_inbox_prompt(request): + def changed_inbox_prepare_input(request): nonlocal pending_count # A later message was not in this Turn's initial required read. # Operation Host is already open here, before its model Turn. pending_count = 2 - return prompt(request) + return prepare_input(request) - monkeypatch.setattr(owner, "_prompt", changed_inbox_prompt) + monkeypatch.setattr(owner, "prepare_codex_cli_input", changed_inbox_prepare_input) code, payload = _run(registry, runtime, project, host_arguments) assert len(dispatches) == 1 if late_material: diff --git a/tests/test_host_vision_recovery.py b/tests/test_host_vision_recovery.py index 34195b8853..bb795161ac 100644 --- a/tests/test_host_vision_recovery.py +++ b/tests/test_host_vision_recovery.py @@ -159,20 +159,19 @@ def test_terminal_stride_preflight_uses_effective_strategy_before_todo_effects( ): control, project, runtime = control_at(tmp_path) monkeypatch.chdir(project) - if machine_enabled: - from loopx.capabilities.machine_configuration.builtins import build_builtin_machine_configuration_registry - from loopx.capabilities.machine_configuration.store import configure_machine_configuration - - owner = build_builtin_machine_configuration_registry() - configuration = {"schema_version": "loopx_machine_configuration_v0", "namespaces": { - "todo_replan_cadence": {"schema_version": "todo_replan_cadence_machine_defaults_v2", - "count_unit": "completed_todos", "count": 1, - "stride_strategy": "evidence_conditioned_v1"}}} - preview = configure_machine_configuration( - runtime_root=runtime, registry=owner, configuration=configuration) - configure_machine_configuration( - runtime_root=runtime, registry=owner, configuration=configuration, - execute=True, expected_plan_revision=preview["plan_revision"]) + from loopx.capabilities.machine_configuration.builtins import build_builtin_machine_configuration_registry + from loopx.capabilities.machine_configuration.store import configure_machine_configuration + + owner = build_builtin_machine_configuration_registry() + configuration = {"schema_version": "loopx_machine_configuration_v0", "namespaces": { + "todo_replan_cadence": {"schema_version": "todo_replan_cadence_machine_defaults_v2", + "count_unit": "completed_todos", "count": 1, + "stride_strategy": "evidence_conditioned_v1" if machine_enabled else "off"}}} + preview = configure_machine_configuration( + runtime_root=runtime, registry=owner, configuration=configuration) + configure_machine_configuration( + runtime_root=runtime, registry=owner, configuration=configuration, + execute=True, expected_plan_revision=preview["plan_revision"]) authored = vision("vision_patch_proposed") authored["path_delta"].update(outcome="stop", stopped=["Stop this route until provenance is checked."]) authored["path_delta"].pop("retained") diff --git a/tests/test_inbox_receipt_recovery.py b/tests/test_inbox_receipt_recovery.py index f280edc292..57d70acff5 100644 --- a/tests/test_inbox_receipt_recovery.py +++ b/tests/test_inbox_receipt_recovery.py @@ -69,7 +69,10 @@ def test_unreadable_decision_is_not_a_new_request_or_a_terminal_ack(exchange): assert {item["request_id"] for item in page["items"]} == {rid, healthy} blocked = next(item for item in page["items"] if item["request_id"] == rid) assert blocked["inbox_state"] == "receipt_unavailable" - assert blocked["warnings"] == ["decision_unreadable_or_conflicting"] + assert blocked["warnings"] == [ + "decision_unreadable_or_conflicting", + "conclusion_unreadable_or_conflicting", + ] assert not blocked.get("receiver_decision_recorded") path.write_bytes(original) assert [item["request_id"] for item in pending(root, "delivery", "receiver")["items"]] == [healthy] diff --git a/tests/test_kiro_cli_mcp.py b/tests/test_kiro_cli_mcp.py index af00cddd6f..7163aa54e6 100644 --- a/tests/test_kiro_cli_mcp.py +++ b/tests/test_kiro_cli_mcp.py @@ -226,6 +226,8 @@ def test_completion_through_the_kiro_server_settles_once_under_its_profile( registry, "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", @@ -263,7 +265,7 @@ def test_completion_requires_a_connected_delivery_frontier(tmp_path: Path) -> No assert result["settlement"]["failed_stage"] == "guard" assert state_file.read_bytes() == before code, status = _run_cli( - registry, "quota", "should-run", "--goal-id", GOAL_ID, + registry, "quota", "should-run", "--context-delivery", "off", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--runtime-profile", CONFIG.runtime_profile, ) assert code == 0 and status["quota"]["spent_slots"] == 0 diff --git a/tests/test_lark_owner_attention.py b/tests/test_lark_owner_attention.py index 09f8d92adc..cb6e551fa7 100644 --- a/tests/test_lark_owner_attention.py +++ b/tests/test_lark_owner_attention.py @@ -129,7 +129,7 @@ def existing_reply(path): all_started.set() release.wait(5) return 0 - monkeypatch.setattr(o.transport, "pending_delivery_paths", lambda: [Path(str(i)) for i in range(4)]) + monkeypatch.setattr(o.transport, "pending_body_paths", lambda: [Path(str(i)) for i in range(4)]) monkeypatch.setattr(o.transport, "reconcile_request", existing_reply) monkeypatch.setattr(o.attention, "reconcile", lambda: pytest.fail("disabled notifications must not schedule a job")) service = LarkGoalTopicRuntimeService(snapshot_provider=lambda: {}, runtime_root=o.store.root.parent, diff --git a/tests/test_manager_context_execution.py b/tests/test_manager_context_execution.py index 6a94020467..1b75eca070 100644 --- a/tests/test_manager_context_execution.py +++ b/tests/test_manager_context_execution.py @@ -378,12 +378,12 @@ def test_governed_worker_adopts_original_request_and_returns_without_another_mod "from loopx.control_plane.collaboration.peers import read_inbox\n" "from loopx.capabilities.manager_context.roundtrip import report\n" "source_id = delegation['source_request_id']\n" - "items = read_inbox(root / 'runtime', root / 'registry.json', envelope['goal_id'], actor)['items']\n" + "items = read_inbox(root / 'runtime', root / 'registry.json', identity['goal_id'], actor)['items']\n" "assert any(item['request_id'] == source_id for item in items)\n" "for item in items:\n" " if item['request_id'] == source_id:\n" - " acknowledge(root / 'runtime', envelope['goal_id'], actor, item['request_id'], 'adopt', 'Receiver independently read the original scope.')\n" - " report(root / 'runtime', envelope['goal_id'], actor, item['request_id'], 'conclusion', 'Independent fixture result returned to the original request.')\n" + " acknowledge(root / 'runtime', identity['goal_id'], actor, item['request_id'], 'adopt', 'Receiver independently read the original scope.')\n" + " report(root / 'runtime', identity['goal_id'], actor, item['request_id'], 'conclusion', 'Independent fixture result returned to the original request.')\n" ) host.write_text(content.replace(anchor, statement + anchor)) launched = execution.dispatch(service.root, registry, session=session, turn=turn, request=request, receipt=receipt, diff --git a/tests/test_manager_team_plan_guidance.py b/tests/test_manager_team_plan_guidance.py index dbc6e09e40..92a3a8e63a 100644 --- a/tests/test_manager_team_plan_guidance.py +++ b/tests/test_manager_team_plan_guidance.py @@ -59,7 +59,11 @@ class _Refused(RuntimeError): class _Restarted(RuntimeError): error_code = "server_restarted" - assert "服务在处理过程中重启" in manager_failure_reply(_Restarted())[1] + restart_code, restart_reply = manager_failure_reply(_Restarted()) + assert restart_code == "server_restarted" + assert "管家服务重启中断" in restart_reply + assert "原请求和会话已保留" in restart_reply + assert "执行效果不明的操作不会直接重做" in restart_reply class _InvalidRequest(Exception): error_code = "upstream_invalid_request" diff --git a/tests/test_native_steward_reports.py b/tests/test_native_steward_reports.py index 9bca4a26c3..abd4a84abd 100644 --- a/tests/test_native_steward_reports.py +++ b/tests/test_native_steward_reports.py @@ -942,8 +942,8 @@ 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" - " if authority.get('replan_action_packet'):\n" + " request = json.JSONDecoder().raw_decode(prompt.split('Turn request:', 1)[1].lstrip())[0]\n" + " if request.get('detail_ref', {}).get('replan_action_packet'):\n" " return " + repr(json.dumps(replan)) + "\n" " return report_only(params)\n") calls, writes = capture.read_bytes(), list(bot.writes)