diff --git a/.gitattributes b/.gitattributes index c87814d..c97f69c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -76,3 +76,5 @@ provenance/compiler/predecessor/src/adaptive_inference/desktop/hermes/report_con # Preserve exact compiler predecessors and video qualification records. provenance/video-tool/** -text whitespace=cr-at-eol provenance/video-tool/predecessor/src/adaptive_inference/desktop/hermes/report_contract.py whitespace=cr-at-eol,-blank-at-eof +# Exact historical CI fixture bytes include their original CRLF line endings. +provenance/ci-fixtures/** -text whitespace=cr-at-eol diff --git a/AGENTS.md b/AGENTS.md index 982bfab..2208800 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ that goal. A convenient component pass does not shrink it. ## Shared working rules - This repository is the canonical source. Preserve engine/agent adapters as consumers of shared contracts. +- Apply the active goal's [engineering practices](docs/multi-agent-collaboration.md#engineering-practices): small reviewable increments, explicit acceptance, meaningful validation, independent review and durable handoffs. Improve the process from evidence while prioritizing the working main workflow. - Keep citations, source revisions and license notices with every reused mechanism. - Research first before designing a solution to each new problem: search current primary research, GitHub implementations/issues/PRs and official documentation. Reuse or adapt a suitable existing mechanism before inventing one; record the sources inspected, applicability, licensing and why any necessary new work remains. Reuse prior findings when their relevance and revision are checked. - Use the existing queue, feedback and capability boundaries; do not invent unsupported backend controls. diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f470f..c83dc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased — development after 0.4.0a1 + +These changes belong to the development source. The published `0.4.0a1` tag and downloadable packages retain their original capabilities and validation scope. + +- Add **Open saved task** after a safe Stop, including tasks created later through the running controller. Accepted installed IQ3_S trials preserve the original task, confirmed reads, conversation, deadline and consumed work across application close/reopen in the same Windows boot. A completed task can be viewed without starting another worker or model, or writing its result again. See [saved-task reopening](docs/saved-task-reopening.md). +- Add a bounded **C++17 compile-and-test recipe** that an opted-in Hermes task can request through the existing resource handoff. Reuse CMake/CTest, retained process ownership and durable tool feedback. An accepted IQ3_S foreground trial handed resources to the compiler and returned its checked result to the same task. See [the compiler recipe and evidence](docs/compiler-tool-recipe.md). +- Add a bounded **GPU video recipe** using pinned FFmpeg/FFprobe and NVENC. An accepted IQ3_S foreground trial parked inference, encoded and checked one silent 90-frame test clip, restored the model and completed the task from saved tool feedback. This is a fixed test-clip capability; H3, audio synchronization and the full autonomous media workflow remain unqualified. See [the video recipe and evidence](docs/video-tool-recipe.md). +- Recover missing tool feedback through the existing explicit reconciliation path when the completed result is known. Extend this to a pending journal entry when the live tool host retains the exact terminal result. Installed Hermes SDK trials use one actual video operation with scripted provider replies and resource observations; they preserve the task and deadline without replaying the operation. Unknown outcomes remain unresolved. See [completed-result recovery](docs/video-tool-recipe.md#completed-result-feedback-repair) and the [pending-effect receipt](provenance/pending-tool-recovery/installed-worker-loss-2026-11-02.json). +- Add **automatic desktop worker replacement** for that narrowly defined pending-result boundary. In the accepted real IQ3_S trial, foreground activity parked inference, one video operation finished, and the first worker exited before recording completion. The live host reconciled the saved result and launched one replacement automatically; the same task completed within its original deadline after two total model requests. Independent closed review passed 49 checks. The separate shutdown observer failed and remains an open diagnostic; this is not active-tool interruption, whole-host loss or reboot recovery. See [automatic recovery](docs/video-tool-recipe.md#automatic-desktop-recovery) and its [physical receipt](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json). +- Add the **durable reboot-budget foundation**: a fixed expiry and saved remaining-time cap prevent a restart from granting a new execution allowance. Source, package, installation and scripted SDK checks passed. The first actual Windows restart preserved saved work but the original allowance expired before continuation; execution was correctly refused. Successful computer-reboot continuation remains open. See [budget behavior](docs/reboot-budget.md) and the [retained expired attempt](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json). +- Add browser-video observation and comparison tooling, retain unsuccessful measurement attempts and correct continuation-test cleanup. The attempted comparison did not establish desktop/video performance because its observation conditions differed; no broad coexistence or speed claim follows from it. +- Make evidence-based agent engineering an explicit goal requirement: bounded changes, meaningful acceptance, independent review, reproducible handoffs and a protected main branch. See the [collaboration practices](docs/multi-agent-collaboration.md#engineering-practices). +- Correct two Windows CI fixture timing assumptions after retaining their original failures. Use a logical observation clock for an ordinary continuation test and a 30-second whole-fixture join guard for retained control work. Twelve targeted tests passed, including stale-observation refusals; production freshness and task deadlines are unchanged. Connect their exact retained predecessors to the existing historical-source reader after its separate hosted failure. The broader local reader sweep timed out after 118 passing checks and remains incomplete; fresh hosted validation is required. See the [fixture evidence](provenance/ci-fixtures/2026-11-09/record.json). + +The automatic-recovery source passed the complete **1,595-test local core suite** and all **16 additional workflow commands**, with the earlier failed test-double attempt retained. Source checks, installed SDK fixtures and physical IQ3_S evidence remain separately identified in the [evidence ledger](docs/evidence.md) and [local validation record](provenance/automatic-tool-recovery/local-validation-2026-11-08.json). These results do not complete M3, M4, M5, daily-use qualification or the full Adaptive-Hermes-Media-Autonomous Test. + ## 0.4.0a1 — supported Windows reference alpha (2026-11-01) This prerelease packages the supported Windows/IQ3_S/Hermes reference, ordinary diff --git a/HANDOFF.md b/HANDOFF.md index 3c5a9f2..168d2df 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,7 +5,7 @@ Read [AGENTS.md](AGENTS.md) first. The repository, its retained evidence and cur Git state are authoritative; no previous conversation or particular AI provider is required to understand the intended work. -**Snapshot date: 2 November 2026.** Recheck branch heads, PRs, task ownership and +**Snapshot date: 9 November 2026.** Recheck branch heads, PRs, task ownership and the actual machine before acting. This file does not reserve hardware or assert that a previously recorded process is still running. @@ -40,7 +40,30 @@ citations and unsuccessful evidence. The full milestone sequence is in ## Current continuation state -**Active isolated M4 increment:** automatic desktop recovery of a pending +**Integrated development source — 9 November 2026:** PRs #34–#39 merged in +dependency order into `main` at `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`. +Its tree `7d17387075e4ac11be05384c5c1b109eaa76f149` exactly matches reviewed +automatic-recovery head `08053a00d92c065b88bda3ce14f80a7cfe078f0f`. +This integrates the bounded compiler/video recipes, known-result reconciliation, +automatic worker replacement and durable reboot-budget foundation. Saved-task +application reopening was already on main. The [Unreleased notes](CHANGELOG.md#unreleased--development-after-040a1) +describe these development capabilities; published `0.4.0a1` artifacts retain +their original scope. [PLAN.md](PLAN.md) is the authoritative goal, milestone +order and next-work plan. M3–M5 and daily-use qualification remain incomplete. + +The active goal also requires the [agent engineering practices](docs/multi-agent-collaboration.md#engineering-practices). +The follow-up [Windows CI fixture correction](docs/evidence.md#follow-up-windows-ci-fixture-correction) +retains two original hosted failures and twelve passing targeted checks, including +negative expiry checks. Production deadlines and runtime source are unchanged. +Its historical-reader follow-up retains a separate failed hosted pair and an +incomplete local sweep (118 passes, 27 unfinished at the original outer guard). +The next hosted run exposed two untested aliased helper callers; a shared +module-level guard fixes that dependency, with twelve focused successor checks +passing across direct/aliased callers and historical-source assertions. +Use the exact current PR checks before integrating; historical or local passes +do not override a failed required check. Main branch protection is enabled. + +**Latest accepted M4 increment:** automatic desktop recovery of a pending compiler/video operation after worker loss. `DesktopComposition` now detects a non-zero worker exit with one pending effect whose exact terminal result is held by the live tool host, exports `CompilerHost.terminal_receipt`, runs the @@ -51,10 +74,24 @@ and all 16 remaining workflow commands after a tests-only correction (the failed first run is retained). The [installed SDK trial](provenance/automatic-tool-recovery/installed-worker-loss-2026-11-08.json) passed 21 closed-review checks, and the [physical IQ3_S foreground trial](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) passed 49 closed-audit checks: Explorer F5 parked inference, NVENC ran, the -first worker exited with code 86, and the host recovered automatically. Draft -[PR #39](https://github.com/midhatn/adaptive-inference/pull/39) is stacked on -[PR #38](https://github.com/midhatn/adaptive-inference/pull/38). Next: active-tool -interruption, host loss, observer shutdown diagnosis and M5 restart/reboot. +first worker exited with code 86, and the host recovered automatically. The +separate observer failed during shutdown; the accepted task/cleanup audit does +not erase that failure. Recovery requires the live host's exact completed result +and does not qualify interruption of an active tool or loss of that host. + +**Next bounded work:** diagnose the retained observer shutdown failure before +relying on it for a fresh physical acceptance, then follow M4's active-tool +interruption gate. Reuse existing cancellation and ownership mechanisms and +research any missing behavior before implementation. Whole-host loss, successful +OS-reboot continuation and broad desktop/media qualification remain open under +the roadmap. Preserve unchanged IQ3_S quality and the closed accepted/failed +cases; a new experiment needs a fresh case, not a renewed historical budget. + +## Preceding evidence and integration history + +The following dated sources explain how the integrated result was reached. +Their historical tests and installation identities remain separate; they are +not instructions to replay accepted cases or rebuild the released alpha. **Preceding increment (PR #38):** recover a pending compiler/video operation when the tool host has retained its exact terminal result but the worker disappeared @@ -75,10 +112,11 @@ reconciliation to repair both the effect journal and missing native feedback, and completed the same task with a replacement worker within its original 900-second deadline. Provider replies and resource observations were scripted. Independent closed review passed 99 checks with 74 original case files unchanged. Automatic -desktop recovery and unchanged-IQ3_S recovery remain separate gates. +desktop recovery and unchanged-IQ3_S recovery were separate gates for this +increment; the later bounded result above now supplies that evidence. The preceding [completed-feedback increment](https://github.com/midhatn/adaptive-inference/pull/37) -is published in draft at `93eba3ce4eee9609283f360fb7ca51b6c136e8bb` and merged +was published in draft at `93eba3ce4eee9609283f360fb7ca51b6c136e8bb` and merged with the validated video fixture correction at `3a07a7776ed41fc811aa92ef2f8ee0b9639d13f9`, stacked on [the video recipe](https://github.com/midhatn/adaptive-inference/pull/36). Both original hosted workflows at `93eba3c`, `e62a291`, and merged ancestor `3a07a77` @@ -93,7 +131,8 @@ one actual video effect, ordinary reconciliation restored its missing feedback, and a replacement completed the same task within its original deadline. Provider replies and resource observations were scripted. Independent closed review passed 99 checks with 74 original case files unchanged. Unchanged-IQ3_S recovery and automatic desktop -recovery remain separate gates. The first trial stopped on a private checker +recovery were separate gates for that increment, subsequently covered at the +narrow boundary above. The first trial stopped on a private checker projection error and remains recorded as failed. The [safe-restart gate](provenance/video-tool/installed-worker-restart-2026-11-02.json) preserved one completed video operation across two workers in the same task and @@ -107,11 +146,11 @@ Windows Restart preserved saved work but expired before continuation; the [attempt record](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json) retains that failure and the separate observer timeouts. The installed reader refused the expired task before another app/worker launch. Successful reboot -continuation remains open and PR #34 stays draft. Never replay or renew this +continuation remains open after integration of PR #34's source. Never replay or renew this consumed attempt. Its documentation update at `f94e11b` also passed its original 14 hosted jobs and 12 distribution checks, independently verified on attempt 1; -PR #34 records both runs and remains draft. Keep the actual installation fixed while developing -compiler support. The inert +PR #34 records both runs. That source integration does not change the fixed +installation or qualify successful physical reboot. The inert [M3 focus-switch rehearsal](provenance/video-coexistence/inert-focus-switch-2026-11-01.json) passed at the earlier installed source with no inference or playback measurement. Its task is closed. Video-surface attribution and a complete measured comparison @@ -243,7 +282,7 @@ checks and all 1,352 local tests. Exact-head package verification passed for 1,233 source files/62 runtime modules; all committed bytes matched its input. The original post-merge run is tracked separately from these completed gates. -**Current source increment:** later tasks created through the open controller +**Earlier source increment:** later tasks created through the open controller now receive the same recovery context and host lock, and appear in the bounded saved-task selector. Its 49 affected component checks and 75 source-preservation checks passed; the predecessor regression failed at the missing context as @@ -317,15 +356,15 @@ daily-use qualification remain open. | Item | Dated state and verification boundary | | --- | --- | -| Stable main starting point | Commit `8396676dcc55ffbbf09e0ff640db9238519a07ea`, including merged PR #31/#32 and their earlier prerequisites. Verify current `main`; this is a dated anchor, not a claim that it remains HEAD. | +| Integrated main starting point | Commit `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`, including PRs #34–#39, has the exact reviewed `08053a0` tree. Verify current `main`; this is the 9 November anchor, not a claim that it will remain HEAD. | | Released reference and active work | [v0.4.0a1](https://github.com/midhatn/adaptive-inference/releases/tag/v0.4.0a1) is published at tested source `57f673f8e934e275bc2bd51e85d434dc7025bc9a`; PR #28's merged tree matches it exactly. PR #29's measurement preparation merged after its exact-head checks. The first adaptive diagnostic and following control are closed; their scoped results and unmet comparison gates are recorded in the [evidence ledger](docs/evidence.md#first-adaptive-video-diagnostic). | -| Installed source used for the latest experiments | Later-task M5 used `b35b631aca9ec37da9955903b87463201a6b3630`; initial-task M5 used `6ac601546daf32d96e38b6689a5818e4cc8dafa8`; the earlier M3 video sequence used `7306a53085ddc31d53d2ba50e91975c80cfc0014`. All preserved IQ3_S and quality. Source, package, actual-SDK and physical evidence remain separate levels. | -| Current work | The isolated video increment passed local/package/install, installed SDK and real IQ3_S foreground/task gates. Its second physical task passed 47 primary checks, 34 independent functional checks and 17 separate onset-review checks; the first missed its foreground action. Publish this as a separate increment stacked on compiler PR #35, then qualify useful-tool interruption/recovery. PR #34 remains draft after its expired physical restart attempt. Both compiler and video tasks remain closed; observer shutdown and earlier failures remain preserved. | +| Installed source and evidence | The latest automatic-recovery [physical receipt](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) binds its actual installation. Earlier later-task M5 used `b35b631aca9ec37da9955903b87463201a6b3630`; initial-task M5 used `6ac601546daf32d96e38b6689a5818e4cc8dafa8`; M3 video used `7306a53085ddc31d53d2ba50e91975c80cfc0014`. All preserved IQ3_S and quality. A source merge does not update those installations. | +| Current work | Compiler/video handoffs, known-result reconciliation and automatic desktop worker recovery are integrated. The latest physical trial passed 49 closed-audit checks at the completed-tool/surviving-host boundary. Observer shutdown remains unresolved; active-tool interruption and whole-host loss remain open. PR #34's budget source is integrated, but its expired physical restart attempt does not qualify successful reboot continuation. All consumed trials stay closed. | | Preserved M5 same-boot result | The installed same-boot application restart preserved one input read, the task/native session and original deadline; three calls total completed the correct 23-unit answer with one physical write. A third launch viewed the result without additional calls or changes to the eight canonical files. All three app parents joined normally; bounded observer shutdown errors remain retained. See the source-bound M5 record above. | | Preserved M2 recovery result | Case17 passed both original audits: 99 primary recovery/handover checks plus the foreground/lineage supplement. Its correct inventory result survived safe expiry and one Continue; a separate welcome task completed before one normal Stop. Six calls produced two physical writes. Welcome has format-only acceptance. Full M2 is broader than that run. | | Preserved limits and failures | Original quality, 900-second execution budgets, 2,700-second outer bound and task accounting remain unchanged. Cases14/15/16 keep their original failed outcomes. Case17 retains the rejected file-picker input and 30-second operating-target misses; neither repeated a task control nor violated the strict foreground gate. | | Foreground evidence | Case17 retained 25 real document actions, 24 wholly preseal, maximum adjacent interval 54.680872 seconds and preseal tail 37.453106 seconds. The original supplement passed onset, continuous hold and saved-read lineage; the postseal action is excluded from coverage. This is not a desktop-performance or GPU-preemption measurement. | -| Next experiment | Qualify installed useful-tool interruption and worker recovery on the existing compiler/video journal and owner, preserving completed effects and feedback. Do not repeat consumed tasks. A future M5 attempt needs fresh identity, prompt postboot return and unchanged allowance. M3's next bounded diagnostic remains video-surface attribution without inference. | +| Next bounded work | Diagnose the retained observer shutdown failure first, then qualify active-tool interruption using the existing compiler/video journal and owner, preserving completed effects and feedback. Whole-host loss remains separate. A future M5 attempt needs fresh identity, prompt postboot return and unchanged allowance. M3's bounded diagnostic remains video-surface attribution without inference. Follow PLAN.md before starting any fresh case. | | Latest release validation | Corrected source `57f673f` passed all 14 original hosted jobs, final archive/runtime verification and merge-tree verification. All five uploaded assets and the tag were verified at publication. The 48-check watchdog and 21-check refusal corrections retain their own local scope; earlier failed runs remain failed. | | M3 publication | [PR #29](https://github.com/midhatn/adaptive-inference/pull/29) merged at `f6c0da1` with the tested `3136e35` tree. Its original 14 head jobs and seven post-merge jobs passed, including portable archive checks. Earlier archive/fixture failures remain retained; these preparation checks do not requalify the installed runtime. | | Diagnostic publication and test correction | [PR #30](https://github.com/midhatn/adaptive-inference/pull/30) merged at `c348398` with the tested `72ac717` tree after 14 head jobs and seven post-merge jobs passed. Its original `fd4d483` pair retains 13 passes and one Windows SQLite concurrency-fixture failure. The [bounded correction](docs/evidence.md#sqlite-concurrency-fixture-ownership) keeps test threads in a retained child and joins before cleanup; 18 focused checks passed. Runtime, models and physical records are unchanged. | diff --git a/PLAN.md b/PLAN.md index a5f67f5..af32642 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,6 @@ # Adaptive Inference roadmap -**Updated: 2 November 2026. Status: living plan, with evidence-based completion gates.** +**Updated: 9 November 2026. Status: living plan, with evidence-based completion gates.** ## Destination @@ -25,6 +25,22 @@ evidence and citations. See the [always-on vision](docs/always-on-controller.md) [integration contract](docs/integration-contract.md) and [source-preservation contract](docs/source-of-truth.md). +### Engineering requirement for the active goal + +Develop this project using current, evidence-based best practices for agent-led +software engineering. Apply the [collaboration workflow](docs/multi-agent-collaboration.md#engineering-practices) +throughout every milestone: research existing solutions, define a bounded user +result and acceptance criteria, implement a small reviewable change, run the +appropriate checks, obtain independent review where useful, integrate through +protected pull requests, and leave a reproducible handoff. Use observed failures +and review feedback to improve both the implementation and this plan. + +Prefer a working end-to-end increment over speculative frameworks, optional +optimization or exhaustive outlier handling. Preserve failed evidence and +qualification standards; agent agreement alone is not validation. Revisit a +practice when new research or project evidence shows a better approach, keeping +the supporting citations and the reason for the change in this repository. + ## Starting point This snapshot distinguishes released work, development work and future goals. @@ -32,11 +48,13 @@ A roadmap entry is not a qualification receipt. | Area | Position at this revision | | --- | --- | -| Released foundation | Version 0.3.0 includes the portable gateway, normal/background queue, measured execution-cost feedback, durable task foundation, source preservation and scoped integration recipes. | +| Released foundation | Version 0.4.0a1 is the supported Windows reference alpha, including task preparation and desktop setup on the portable gateway, durable task and source-preservation foundation. Later development changes do not alter its published assets. | | Real task evidence | A recorded IQ3_S Hermes parent/child file workflow completed with independently checked artifacts. It does not establish foreground handoff or reboot recovery. | | Native foreground and tool work | One complete source-bound Windows/IQ3_S workflow is accepted: real foreground interaction, verified release, checked GPU encode, restoration and clean shutdown. | | Resource-aware agent waiting | Immediate admission and the pinned Hermes recovery adapter completed the same accepted workflow, with durable waiting following foreground interruption and one same-boot worker restart. | | Desktop protection observation | A later qualification withheld model startup while a foreground game was active. That observation is not a gaming-performance benchmark or a completed qualification. | +| Integrated M4 development | Bounded IQ3_S compiler/video foreground handoffs and automatic worker recovery with an exact completed result retained by the surviving tool host are accepted at their recorded scopes. The independent shutdown observer remains unresolved. | +| Restart development | Same-boot application close/reopen is accepted for the recorded tasks. The reboot-budget foundation is integrated, but the first actual reboot attempt expired before continuation; successful OS-reboot recovery remains open. | | Full always-on experience | Persistent everyday operation, broad foreground coverage, physical reboot recovery and native qualification of other engines remain development work. | The [evidence ledger](docs/evidence.md) and its source-bound receipts define @@ -46,6 +64,29 @@ interrupted runs will remain identified as such. ## Milestones and dependency order +**Current continuation — 9 November 2026:** PRs #34–#39 are merged in dependency +order at `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`. Its tree +`7d17387075e4ac11be05384c5c1b109eaa76f149` matches reviewed development head +`08053a00d92c065b88bda3ce14f80a7cfe078f0f`. The [Unreleased notes](CHANGELOG.md#unreleased--development-after-040a1) +describe this source increment; the published version remains `0.4.0a1`. +The latest automatic-recovery source passed 1,595 local core tests and all +16 remaining workflow commands. Its real IQ3_S foreground/worker-loss trial +passed 49 closed-audit checks at the exact-known-result boundary described in +the [evidence ledger](docs/evidence.md#physical-iq3_s-foreground-recovery). +That result does not complete M4 or qualify successful computer-reboot recovery. + +**Next bounded work:** diagnose the retained observer shutdown failure using its +closed evidence and current primary documentation, then continue M4's active-tool +interruption gate through the existing owned-process and handoff mechanisms. +Preserve completed effects, original task/budget and unchanged IQ3_S quality. +Whole-host loss and the M5 restart/reboot sequence remain separate gates. The +expired reboot trial stays closed; successful continuation needs a fresh, +separately coordinated case. Keep this main workflow ahead of optional ports or +optimization. Do not replay accepted trials merely because their source merged. + +The dated delivery paragraphs below retain their original evidence and sequence; +the current milestone table and continuation above determine the next work. + **Reference alpha delivered — 1 November 2026:** [v0.4.0a1](https://github.com/midhatn/adaptive-inference/releases/tag/v0.4.0a1) is published from `57f673f8e934e275bc2bd51e85d434dc7025bc9a`, after all 14 original hosted jobs, final source/wheel verification and actual merge-tree @@ -202,13 +243,13 @@ checker/reporting head `f049342060b3ae8d04f237bf5632d2b9f459869d` passed all 14 hosted jobs on their original attempts. Source/CI, installed physical acceptance and public reproduction retain their separate evidence boundaries. -| ID | Deliverable | Depends on | Initial status | +| ID | Deliverable | Depends on | Current status | | --- | --- | --- | --- | | M1 | One complete corrected handoff and agent-restart workflow | Existing foundation | Accepted for the bounded Windows/Strata/Hermes IQ3_S composition | -| M2 | A practical persistent controller with durable waiting and clear user intent | M1 for live integration acceptance | Active: bounded foreground/continuation/two-task run passed both audits; public reproduction, task preparation and setup/launcher startup checks available; narrow reference alpha delivery is next | +| M2 | A practical persistent controller with durable waiting and clear user intent | M1 for live integration acceptance | Active: bounded foreground/continuation/two-task run passed both audits; public reproduction and task preparation/setup are available; reference alpha delivered; persistent everyday use remains open | | M3 | Measured coexistence with real desktop applications | M2 for broad qualification; a bounded control-window policy now feeds M2 | Early policy work active; broad desktop qualification planned | -| M4 | Agent-driven compiler and GPU-tool handoffs | M1 and M2; foreground acceptance from M3 | Active: bounded IQ3_S foreground/compiler handoff passed; actual GPU video component passed; installed-agent video qualification is next | -| M5 | Recovery across application restart, sleep/wake and computer reboot | M2 and supported M4 effect contracts | Planned | +| M4 | Agent-driven compiler and GPU-tool handoffs | M1 and M2; foreground acceptance from M3 | Active: bounded IQ3_S compiler/video foreground handoffs and exact-known-result automatic worker recovery accepted; observer shutdown, active-tool interruption, whole-host loss and broader workflows remain open | +| M5 | Recovery across application restart, sleep/wake and computer reboot | M2 and supported M4 effect contracts | Active: bounded same-boot application close/reopen accepted; durable reboot-budget checks passed; first actual reboot attempt expired; successful OS reboot and sleep/wake remain open | | M6 | A reproducible daily-use release with fault and soak qualification | M3, M4 and M5 for its advertised scope | Planned | | M7 | Broader engine and agent adapters using the same contracts | M2 contract stability; native acceptance follows M3–M6 | Staged expansion | | M8 | Better policy and efficiency from measured evidence | Stable M3/M4 workloads and M6 baseline | Optional optimization | @@ -993,10 +1034,10 @@ coordinator exported `CompilerHost.terminal_receipt` after the worker joined and before stopping the mailbox runtime or retiring the host binding, ran ordinary reconciliation, and finished the task with a replacement worker under the original 900-second deadline. Independent closed review passed 99 checks with 74 original -case files unchanged. Next wire automatic desktop recovery through the same -verified host export and reconciliation path, then qualify automatic SDK recovery -and unchanged-IQ3_S recovery. Interruption during active tool execution remains a -subsequent gate. +case files unchanged. This increment's next gate was automatic desktop recovery +through the same verified host export and reconciliation path; the subsequent +automatic SDK and unchanged-IQ3_S results are recorded below. Interruption during +active tool execution remains a subsequent gate. **Automatic desktop recovery result:** `DesktopComposition` now runs that verified export, reconcile and replacement sequence itself, through a narrow recovered-state @@ -1008,8 +1049,9 @@ then passed 1,595 core tests and all 16 remaining workflow commands. The [installed SDK trial](provenance/automatic-tool-recovery/installed-worker-loss-2026-11-08.json) (21 closed-review checks) and the [physical IQ3_S foreground trial](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) (49 closed-audit checks, two earlier failed attempts retained) both completed with -automatic recovery after worker exit 86. Next: interruption during an active tool, -host loss, observer shutdown diagnosis and the M5 restart/reboot gates. +automatic recovery after worker exit 86. Next: diagnose the retained observer +shutdown failure, then qualify interruption during an active tool. Host loss and +the M5 restart/reboot gates remain separate. **Validation adjustment:** the [original video CI](provenance/video-tool/hosted-ci-timeout-2026-11-02.json) closed with all 12 core-test steps passing, but two final distribution checks diff --git a/REFERENCES.bib b/REFERENCES.bib index 08ee883..1edda32 100644 --- a/REFERENCES.bib +++ b/REFERENCES.bib @@ -1131,3 +1131,19 @@ @misc{erlangOtpSupervision url = {https://www.erlang.org/doc/system/sup_princ.html}, note = {Inspected 2026-11-08 for automatic desktop tool recovery. Design reference only for a supervisor-owned, bounded replacement of a failed worker (restart intensity). Implemented through this repository's existing retained-child lifecycle, task journal and reconcile command; no Erlang/OTP code copied} } + +@misc{openaiAgentFirstHarnessEngineering, + author = {Ryan Lopopolo and OpenAI}, + title = {Harness engineering: leveraging Codex in an agent-first world}, + year = {2026}, + url = {https://openai.com/index/harness-engineering/}, + note = {Reviewed 2026-11-09. Repository knowledge as the system of record, concise agent entry instructions, bounded work and inspectable feedback inform this project's existing collaboration workflow. Practice reference only; no implementation copied or reported throughput inherited} +} + +@misc{anthropicApplicationHarnessDesign, + author = {{Anthropic}}, + title = {Harness design for long-running application development}, + year = {2026}, + url = {https://www.anthropic.com/engineering/harness-design-long-running-apps}, + note = {Reviewed 2026-11-09. Structured handoffs and separation of implementation from evaluation inform the evidence-based agent engineering requirement. Independent review still requires actual acceptance evidence; no agent harness code or provider dependency copied} +} diff --git a/docs/evidence.md b/docs/evidence.md index 0d1fcf6..1f77a25 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -2,6 +2,77 @@ This ledger separates portable software tests, source reconstruction, actual SDK/host fixtures, real model tasks and historical predecessor results. A pass in one does not establish a pass in another. The older prototype checks below did not execute the new gateway; the new gateway has its own evidence here. +## Development integration — 9 November 2026 + +The six reviewed development PRs merged in dependency order. Before each merge, +all fourteen original hosted jobs for its exact head had completed successfully; +the prospective merge tree matched that head. After each merge, the actual +merge tree was checked again and the reviewed head was an ancestor of main. +The following identities preserve the integration record independently of PR +descriptions. Hosted source checks do not extend the installed or physical +qualification recorded in the receipts below. + +| PR | Reviewed head | Merge commit | +| --- | --- | --- | +| [#34](https://github.com/midhatn/adaptive-inference/pull/34) | `f94e11b360c15d41b621ee9641301521846774b0` | `3819a8209a66800901654279a01639dd5a305739` | +| [#35](https://github.com/midhatn/adaptive-inference/pull/35) | `1f7f28e1388d4aa39fa6ff7f8ddc2445f88e127d` | `771692fa5abc650679130790a302a03e9c079f24` | +| [#36](https://github.com/midhatn/adaptive-inference/pull/36) | `e62a2915759eb32a42d607b1a0837103bb54e426` | `6753acad46df17b5d35dd9e8a89a176132592fb8` | +| [#37](https://github.com/midhatn/adaptive-inference/pull/37) | `3a07a7776ed41fc811aa92ef2f8ee0b9639d13f9` | `ba6bf2cc665d6851c528791920e591fe4b29d86c` | +| [#38](https://github.com/midhatn/adaptive-inference/pull/38) | `9242434991f2c2c21461061d61d898649299b847` | `4796c680920a4af3ee0ed7cef8b0b153d66852ba` | +| [#39](https://github.com/midhatn/adaptive-inference/pull/39) | `08053a00d92c065b88bda3ce14f80a7cfe078f0f` | `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a` | + +The final merge tree is `7d17387075e4ac11be05384c5c1b109eaa76f149`, identical to +the reviewed #39 head. This is development integration, not a new release or +installation. The existing `0.4.0a1` tag and assets remain unchanged. Successful +physical computer-reboot continuation remains unqualified: the recorded expired +attempt stays failed. The automatic recovery result still requires an exact +completed tool result in the surviving host, and its separate shutdown observer +remains failed pending diagnosis. See the [Unreleased notes](../CHANGELOG.md#unreleased--development-after-040a1) +and [current plan](../PLAN.md) for the applicable capabilities and next work. + +### Follow-up Windows CI fixture correction + +The original post-merge [PR38 main run](https://github.com/midhatn/adaptive-inference/actions/runs/42554614440) +failed on Windows/Python 3.12 when an ordinary continuation test used an expired +observation. The production one-second freshness check correctly refused it. +The original [documentation push](https://github.com/midhatn/adaptive-inference/actions/runs/42555520520) +then failed on Windows/Python 3.11 because a three-second test-only join guard +expired while retained control work was pending. Both original runs stay failed. + +The [correction record](../provenance/ci-fixtures/2026-11-09/record.json) retains +exact predecessor test files, failure identities and local validation. The first +fixture now reuses the existing logical observation-clock pattern. The second +uses the existing project/CPython 30-second whole-fixture guard while preserving +shielded ownership and final-intent assertions. Twelve selected tests passed: +both affected paths, six defining-module callers and five negative expiry checks. +The two callers that borrow the helper from another class were not included. +Three owned inert children joined with their expected exits, without cleanup +kills. Runtime files, production freshness, real execution clocks and task/tool +allowances are unchanged. These source tests do not qualify installed agents, +hardware, physical responsiveness or reboot recovery. Successor hosted CI is a +separate gate; a local pass does not relabel an original failed run. + +Both original workflows at `a83844b` then failed an unchanged historical source +assertion: the historical reader still selected the corrected live fixture. +The follow-up reuses the existing source-lineage chain to select only the exact +retained predecessor of either declared fixture. Unknown source bytes continue +to face the original assertions. Three new archive/identity regression tests +and the originally failing compiler assertion passed. A broader local sweep +logged 118 passes out of 145 planned tests before its original 600-second +outer guard expired; the retained child was terminated and joined, leaving +27 tests incomplete. This is **not a passing full-suite result**. Its partial +log and failed supervisor outcome remain recorded; required fresh hosted CI +must pass on the corrected successor before merge. + +The next original hosted pair at `411a3e0` exposed that missing alias coverage: +`ExpiryRuntimeTests` borrows the helper without inheriting its defining class, +so the new instance-setting lookup raised `AttributeError`. Move the unchanged +30-second guard to module scope. Twelve targeted successor checks passed: +all six direct and both aliased calling tests, three source-archive regressions, +and the original compiler source assertion. The retained test child joined +normally, with no timeout or source change during the check. Production behavior +and prior failed outcomes are unchanged; the new head still requires full CI. + ## Agent-requested GPU video and foreground handoff The [second physical video task](../provenance/video-tool/physical-foreground-video-2026-11-02.json) diff --git a/docs/multi-agent-collaboration.md b/docs/multi-agent-collaboration.md index 93dc879..5305661 100644 --- a/docs/multi-agent-collaboration.md +++ b/docs/multi-agent-collaboration.md @@ -20,6 +20,40 @@ also draws on [research on long-running agent harnesses](https://www.anthropic.c This is a coordination practice, not evidence that adding more agents guarantees success. Sources and the scope of reuse are retained in [REFERENCES.bib](../REFERENCES.bib). +## Engineering practices + +These practices are an explicit requirement of the active project goal, added +9 November 2026. The workflow below applies them without requiring a particular +agent product or a second human reviewer for every change. + +- Keep `AGENTS.md` a concise entry map; use the canonical plan, current handoff + and linked source/evidence instead of relying on an agent's chat memory. +- Define the user-visible outcome, acceptance and ownership before a bounded + change. Prefer a small working increment and reuse established mechanisms. +- Give independent workers disjoint scopes. Review the actual diff and evidence; + agreement between agents does not establish correctness. +- Match checks to the change. Separate deterministic functional tests from real + timing and hardware measurements. Diagnose failures before retrying; never + weaken production requirements to hide a test-environment problem. +- Retain exact inputs, dependencies, citations, failed attempts and rollback + points. Close owned processes and document the next action before handoff. +- Integrate through pull requests with required checks and resolved discussions. + Update the plan when evidence changes the next useful step. Avoid process or + optional edge-case work that does not advance the functional main workflow. + +This extends the existing incremental-session approach using OpenAI's +[repository knowledge and feedback-loop guidance](https://openai.com/index/harness-engineering/) +and Anthropic's [separate implementation/evaluation and structured-handoff work](https://www.anthropic.com/engineering/harness-design-long-running-apps), +reviewed on 9 November 2026. These are engineering references, not inherited +performance guarantees or copied harness code. + +The canonical GitHub `main` branch is protected as of this date: a pull request, +an up-to-date branch, all seven CI job contexts from GitHub Actions and resolved +conversations are required. Force-push and deletion are disabled, including for +administrators. Required human approvals are zero; independent agent review and +the evidence gates above still apply. Fork maintainers should configure their +own equivalent protection; repository files cannot enforce remote settings. + ## Divide work by a concrete contract One work item has a coordinator/integration owner, bounded implementation or diff --git a/docs/reboot-budget.md b/docs/reboot-budget.md index 5bfd6a3..e0e255f 100644 --- a/docs/reboot-budget.md +++ b/docs/reboot-budget.md @@ -1,9 +1,14 @@ # Preserve a task's time allowance across a normal reboot -Status: source, package, hosted, isolated installation and scripted SDK gates -passed. The next gate is an actual clean Stop/Windows Restart/resume trial. -The accepted same-boot application restart does not establish computer-reboot -recovery; no physical reboot result is claimed here. +Status as of 9 November 2026: the source is integrated on main after source, +package, hosted, isolated installation and scripted SDK gates passed. The +[first actual clean Stop/Windows Restart attempt](../provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json) +preserved saved work, but its original allowance expired before continuation. +The installed reader refused execution before another application/worker launch. +That failed attempt stays closed; do not replay it or reset its budget. +Successful physical reboot continuation remains an open gate requiring a fresh, +separately coordinated case. Accepted same-boot application reopening does not +establish computer-reboot recovery. The first source component is the pure [`execution_budget` helper](../src/adaptive_inference/desktop/execution_budget.py). diff --git a/docs/upstream-delivery.md b/docs/upstream-delivery.md index b665d5c..ff849a9 100644 --- a/docs/upstream-delivery.md +++ b/docs/upstream-delivery.md @@ -12,6 +12,33 @@ contains the engine profiles and client entry points. All actual inference qualification uses the established IQ3_S model; fixture responses are labeled separately. No smaller model or reduced reasoning allowance substitutes for it. +## Current status: 9 November 2026 + +The read-only refresh through **2026-11-09 at 03:03 UTC** confirms **ten open, +one merged and one closed unmerged** among the original twelve propagated PRs. +The nine open documentation/example PRs apply cleanly; this does not clear their +review, CLA, preview or draft gates. The remaining open PR, Strata #1493, is a +conflicting experimental draft. Its next step remains a narrow adapter using +current upstream controls, followed by a reproduced missing contract or defect +and exact target validation before revising the broader proposal. + +Hermes #143827 remains merged for the routing-plugin catalog entry. OpenClaw +#182080 remains closed in favor of merged #182565's existing provider-routing +recipe. Neither accepts this repository's newer desktop controller or qualifies +an installed autonomous workflow. Aider #5899 now reports one successful +`license/cla` status dated 8 November; it is not a code-test pass. Other recorded +contribution and check gates remain unresolved, with no new maintainer correction +identified in the original twelve. + +The account used for this refresh has read access, without push, maintain, admin +or triage permissions, in all 13 external repositories checked. Upstream merges +and maintainer-only labels therefore remain with those repositories; fixing our +own fork branches is still possible when a concrete target change is validated. +No upstream comment or branch update was warranted by this status refresh. +Canonical release results are propagated only when they validate a specific +mergeable change in the receiving PR, not as a general progress announcement. +Earlier head identities and validation dates below retain their original scope. + ## Delivery sequence 1. Preserve the implementation, exact source inputs, licenses and runnable checks here. @@ -47,7 +74,7 @@ MLX-LM and vLLM. | Target | Submitted or retained integration | Recorded delivery status | | --- | --- | --- | -| Strata | [Existing #1493](https://github.com/Niko1221/Strata/pull/1493), head `06ba725d8b9bb42b457302f71dd85204e9760c32` | Draft and conflicting as observed on 30 October. Its 46 CPU/fake-engine routing checks remain historical; `metal-link` and native/public-workflow gates remain unresolved. First evaluate existing upstream load/unload/VRAM controls through a narrow canonical adapter before revising the broader draft. [#3399](https://github.com/Niko1221/Strata/pull/3399) is closed unmerged: [e2e99ae5](https://github.com/Niko1221/Strata/commit/e2e99ae54aae9a5a28938223679ab9a5ec4ef33c) ported its exact child-identity test and the token-default fix with credit, alongside kossum's QoS test. This is source/attribution verification, not a new runtime result. | +| Strata | [Existing #1493](https://github.com/Niko1221/Strata/pull/1493), head `06ba725d8b9bb42b457302f71dd85204e9760c32` | Draft and conflicting on 30 October, reconfirmed 9 November. Its 46 CPU/fake-engine routing checks remain historical; `metal-link` and native/public-workflow gates remain unresolved. First evaluate existing upstream load/unload/VRAM controls through a narrow canonical adapter before revising the broader draft. [#3399](https://github.com/Niko1221/Strata/pull/3399) is closed unmerged: [e2e99ae5](https://github.com/Niko1221/Strata/commit/e2e99ae54aae9a5a28938223679ab9a5ec4ef33c) ported its exact child-identity test and the token-default fix with credit, alongside kossum's QoS test. This is source/attribution verification, not a new runtime result. | | llama.cpp | Existing OpenAI-compatible server profile and preserved native source inputs | Human-authored upstream submission required by its contribution policy | | vLLM | Existing compatible server profile; no duplicate provider | Human validation and DCO required before an upstream submission | | SGLang | [#46199](https://github.com/sgl-project/sglang/pull/46199), head `c3aaf7ec` | Ready for review. All 121 returned checks on the unchanged head comprise 33 successes, 82 skips, five failures and one neutral result; the latest processor workflow succeeded. Prior draft/title/aggregate failures remain on this head, and fork preview requires maintainer approval. Six gateway/SDK fixture checks are not native SGLang qualification. | @@ -57,7 +84,7 @@ MLX-LM and vLLM. | TGI | Retained legacy profile | Repository archived; no new upstream PR possible | | Hermes | [Catalog #143827](https://github.com/NousResearch/hermes-agent/pull/143827), head `2c1411b3` | Merged on 24 October at 20:23:02 UTC (`e7b15ab1`); 17 checks succeeded and 23 skipped. The catalog entry pins `d6f4e201` and `plugins/hermes`. This is not upstream acceptance of the desktop controller or new native/agent qualification. | | OpenClaw | [Docs #182080](https://github.com/openclaw/openclaw/pull/182080), head `f64d74b0`, linked to [issue #182077](https://github.com/openclaw/openclaw/issues/182077) | Closed unmerged on 30 October as superseded by [#182565](https://github.com/openclaw/openclaw/pull/182565), merged at `45fc29dcf159095be6251dba21c249af08a5d05a` on 27 October and closing the issue. The successor documents existing same-model routes, settings, fallbacks and precedence. Original patch, schema/MDX checks and failed/skipped CI remain historical. Documentation acceptance does not qualify runtime scheduling or agent performance. | -| Aider | [#5899](https://github.com/Aider-AI/aider/pull/5899), head `d5e113fb` | Open base-path clarification, distinct from existing runtime-normalization #5488. No hosted checks reported. | +| Aider | [#5899](https://github.com/Aider-AI/aider/pull/5899), head `d5e113fb` | Open base-path clarification, distinct from existing runtime-normalization #5488. The earlier snapshot reported no hosted checks; the 9 November refresh reports one successful `license/cla` status dated 8 November, not a code-test pass. | | OpenHands | [SDK #6180](https://github.com/OpenHands/software-agent-sdk/pull/6180), head `415785b5`, and [docs #997](https://github.com/OpenHands/docs/pull/997), head `4ef32371` | Both drafts. The SDK's human-only template section remains unchanged; companion docs should land with or after the example. SDK and gateway checks use HTTP fixtures. | | Continue | [#13415](https://github.com/continuedev/continue/pull/13415), head `12eba630` | Open base-path guide. CLA, Snyk quota, FFmpeg setup and aggregate checks remain unresolved; formatting passed. | | Cline | [Docs #15308](https://github.com/cline/cline/pull/15308), head `cbbc9626`, plus [feature discussion #15309](https://github.com/cline/cline/discussions/15309) | Open route clarification; fork preview needs maintainer approval. Explicit budget-field support is proposed separately and not implemented. | diff --git a/docs/upstream-prs.md b/docs/upstream-prs.md index 69186bc..7e30faf 100644 --- a/docs/upstream-prs.md +++ b/docs/upstream-prs.md @@ -6,6 +6,30 @@ The newer generalized integrations and their published PRs are tracked in the [current delivery ledger](upstream-delivery.md). That ledger supplements this historical inventory; it does not relabel earlier proposals as merged or tested. +## Current status: 9 November 2026 + +A read-only refresh through **2026-11-09 at 03:03 UTC** checked all 57 unique PRs +linked across this registry and the delivery ledger: **24 open, three merged and +30 closed unmerged**. This includes third-party source references, not 57 authored +submissions. Comments, reviews, inline threads and current check results were +read for all 24 open PRs and four relevant closed PRs; the other closed entries +received a metadata/state refresh. No unresolved inline review threads were +returned in that inspected set. This adds no runtime or performance validation. + +The [original twelve propagated PRs](upstream-delivery.md#current-status-9-november-2026) +remain ten open, one merged and one closed with a merged successor. Nine open +documentation/example PRs apply cleanly; our broad Strata #1493 remains a +conflicting draft. Strata #1324 and #1937 are third-party experimental proposals, +not additional owned propagation work. Their second-Windows-PC and upstream +disk-design gates remain separate from this project's recovery qualification. + +The fresh accessible-account search found no involved PR in Comfy-Org/ComfyUI; +the historical repository alias was not searchable. Existing attribution and +dated validation below remain unchanged. No generic project-progress comments, +upstream merges or unrelated propagation were made during this refresh. + +## Historical inventory and refreshes + The inventory was checked at **2026-10-23T22:51:07Z** (24 October 2026 in the reviewer's local date). It includes **29 Strata**, **7 Hermes**, **1 related fork** authored/involved PRs and **6 explicitly cited source/successor PRs**, including closed work. Metadata and complete conversation/review timelines were read for all 43 entries through the GitHub connector at that time. States below are timestamped observations, not a live status feed. The same accessible-account search found no involved PR in Comfy-Org/ComfyUI or its historical comfyanonymous/ComfyUI name. This is not a claim about inaccessible repositories. Titles below are GitHub's current titles. A PR marked closed may have been ported with credit; it is not labeled merged unless GitHub reports a merge. diff --git a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py new file mode 100644 index 0000000..a573611 --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py @@ -0,0 +1,801 @@ +"""Actual bounded CPU children/SQLite/async host; injected native/window boundary. + +No installed SDK/model/native engine/network service/real UI acceptance. +""" +import asyncio +from contextlib import closing +from copy import deepcopy +import ctypes +from ctypes import wintypes +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +import tempfile +import threading +import time +from types import SimpleNamespace +import unittest +from unittest import mock + +from adaptive_inference.config import Config +from adaptive_inference.handoff import HandoffController,ToolRequirements +from adaptive_inference.scheduler import Scheduler +from adaptive_inference.desktop import composition as product +from adaptive_inference.desktop.desktop_controls import ControlledDesktopHost,DesktopControlLifecycle +from adaptive_inference.desktop.continuation_host import HostContinuation,SelectedWorkerStopBinding,selection +from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime +from adaptive_inference.desktop.mailbox_runtime import RuntimeUnavailable +from adaptive_inference.desktop.hermes.continuation import ContinuationAuthority +from adaptive_inference.desktop.control_transport import PreparedTask,ControlTransport,Refused +from adaptive_inference.desktop.ui_intent import UIIntentGate,UIBinding,VerifiedUIObservation +from adaptive_inference.desktop.hermes import recovery as rec,run_recovery as worker +from adaptive_inference.desktop.hermes.service_stop import WorkerStopBinding +from adaptive_inference.execution_clock import CLOCK_ID +from adaptive_inference.task_store import ReconciliationRequired +import test_continuation_worker as seed_fixture +import test_desktop_lifecycle as lifecycle_fixture +from test_desktop_control_transport import Reader,Writer +import continuation_timeout_diagnostics as timeout_diagnostics + +CHILD=Path(__file__).with_name('continuation_cpu_child.py') + + +async def wait_original_ready(child,path,*,timeout_s=30,clock=time.monotonic): + """Bound only this CPU fixture's import/startup; never retry its work.""" + deadline=clock()+timeout_s + while True: + try: + readiness=path.read_text(encoding='utf-8') + except (FileNotFoundError,PermissionError): + # Publication/Windows sharing can briefly prevent observation. + # The same child and original startup bound still govern the wait. + pass + else: + if json.loads(readiness)!={'pid':child.pid,'work_guard_s':15}: + raise AssertionError('CPU fixture readiness identity differs') + return + if child.poll() is not None: + raise AssertionError(f'CPU fixture exited before readiness: {child.returncode}') + if clock()>=deadline:raise TimeoutError('CPU fixture startup deadline exceeded') + await asyncio.sleep(.01) + + +class Adapter: + """Explicit native boundary fixture; status never represents a real engine.""" + def __init__(self): + self._barrier=object();self._cold_start_error=self._startup_failure=self._released=None + self.started_owner=False;self._cold_prepared=True;self.parked_observation={'fixture':True} + self.driver=SimpleNamespace(status=lambda:{'running':False,'state':'stopped','ready':False,'loaded':False}, + owner_idle=lambda:True,port_free=lambda port:True) + def _stopped_identity(self,state): + if state.get('running') is not False:raise RuntimeError('Fixture owner not parked') + async def _call(self,callback,*args):return await asyncio.to_thread(callback,*args) + + +@unittest.skipUnless(os.name == 'nt', 'Windows retained-process creation identity fixture') +class HostTests(unittest.IsolatedAsyncioTestCase): + # Whole-fixture orchestration guard, not a product latency requirement. + # Reuse CPython's short test timeout for real SQLite/filesystem/child work: + # https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py + continuation_timeout_s = 30 + + async def asyncSetUp(self): + self.seed=seed_fixture.SelectedWorkerTests();self.seed.setUp();self.seed.seed_report() + self.addCleanup(self.cleanup_seed) + f=self.seed.f + f.plan['base_python']=sys.executable + (f.run/'plan.json').write_text(json.dumps(f.plan),encoding='utf-8') + c=product.DesktopComposition.__new__(product.DesktopComposition);self.c=c + c.work=f.run;c.root=f.run/'host';c.root.mkdir() + c.handles=[];c.logs=[];c.waits={};c.result={};c.store=f.store;c.plan=f.plan + self.child_receipts=[];self.patches=[] + # unittest skips asyncTearDown when asyncSetUp fails. Register retained + # process/stream cleanup before launch, ahead of the seed directory's + # cleanup (LIFO), just as in test_task_store_verification.py. + self.addCleanup(self.cleanup_retained) + launch=c.launch + def retained_launch(command,kind): + before=len(c.handles) + try:return launch(command,kind) + finally: + for child in c.handles[before:]: + created=wintypes.FILETIME();exited=wintypes.FILETIME() + kernel=wintypes.FILETIME();user=wintypes.FILETIME() + get_times=ctypes.WinDLL('kernel32',use_last_error=True).GetProcessTimes + get_times.argtypes=[wintypes.HANDLE,*([ctypes.POINTER(wintypes.FILETIME)]*4)] + get_times.restype=wintypes.BOOL + if not get_times(int(child._handle),ctypes.byref(created),ctypes.byref(exited),ctypes.byref(kernel),ctypes.byref(user)): + raise ctypes.WinError(ctypes.get_last_error()) + self.child_receipts.append({'pid':child.pid,'created_filetime_100ns':(created.dwHighDateTime<<32)|created.dwLowDateTime, + 'observed_created_utc':datetime.now(timezone.utc).isoformat(),'kind':kind, + 'actual_args':child.args,'executable':sys.executable, + 'executable_sha256':hashlib.sha256(Path(sys.executable).read_bytes()).hexdigest(), + 'fixture_sha256':hashlib.sha256(CHILD.read_bytes()).hexdigest(),'child':child}) + c.launch=retained_launch + c.registry=f.box.registry;c.box=f.box;c.capability=f.cap;c.identity=f.identity;c.original=f.original + c.deadline=10.;c.stop_id='original-stop';c.worker_cap=object();c.worker_exit_verified=False + c.host_epoch='same-host';c.qualification_hold_after_report=False;c.continuation=None + c.worker=c.launch([sys.executable,str(CHILD),'--original','--run',str(c.work)],'worker') + await wait_original_ready(c.worker,c.work/'original-fixture-ready.json') + # Imports have completed and the existing 15-second emergency guard is + # armed. Its exit124 remains a failure; only the original exit5 passes. + self.assertEqual(await asyncio.to_thread(c.worker.wait,18),5) + (c.work/'wire-observations.json').write_text('[]',encoding='utf-8') + self.old=c.store.load('orders-report');self.old_hash=rec.digest(self.old.state) + c.intent_cap=object() + c.gate=UIIntentGate(host_epoch='same-host',capability=c.intent_cap, + verify_current=lambda binding:VerifiedUIObservation(binding,time.monotonic(),True,True)) + c.gui=lifecycle_fixture.GUI();c.gui.release.set() + c.gui.binding=UIBinding('same-host','session','orders-report',101,202,303) + c.gui.process=SimpleNamespace(pid=202,poll=lambda:None) + c.gui.verifier=SimpleNamespace(verify_current=lambda binding:VerifiedUIObservation(binding,time.monotonic(),True,True)) + host=ControlledDesktopHost.__new__(ControlledDesktopHost);c.host=host + host.adapter=Adapter() + policy=SimpleNamespace(reason='foreground_clear',allowed=True,fresh=lambda now:True) + controller=HandoffController(host.adapter,policy,demand=c.registry,restore_requirements=ToolRequirements(1,1,1)) + scheduler=Scheduler(Config(),capacity=controller);controller.bind_scheduler(scheduler);controller.state='parked' + host.controller=controller;host.gateway=SimpleNamespace(scheduler=scheduler,instance='same-gateway') + host.closing=host.closed=False;host.phase='running' + host.primary_error=host.cleanup_error=host.demand_error=None + host.control_runtime=None;host._control_thread=threading.current_thread() + host._control_loop=asyncio.get_running_loop();host._control_rebinding=False;host.closes=0 + async def close(*,timeout_s): + self.assertTrue(all(child.poll() is not None for child in c.handles));self.assertTrue(c.gui.joined) + host.closed=True;host.closes+=1 + return {'closed':True,'primary_error':None,'cleanup_error':None,'controller_fault':None} + host.close=close + c.stop_binding=WorkerStopBinding(c.worker,c.work,c.box,requester_capability=c.capability, + capability=c.worker_cap,stop_request_id=c.stop_id,original=c.original,deadline_monotonic=10., + model=worker.MODEL,quality=worker.QUALITY,fence=c.fence,deadline_clock=CLOCK_ID) + c.prepared=PreparedTask('same-host','orders-report',worker.PROMPT) + c.facade=DesktopControlLifecycle(host,c.prepared,worker_stop=c.stop_binding, + worker_capability=c.worker_cap,gui=c.gui,shutdown_timeout_s=5) + c.facade.bind_window(c.gui.binding,c.gui.verifier, + SimpleNamespace(host_epoch='same-host',task_id='orders-report',session_id='session',objective=worker.PROMPT)) + c.runtime=MailboxRuntime(c.prepared,c.box,requester_capability=c.capability, + gate=c.gate,intent_capability=c.intent_cap,lifecycle=c.facade) + await c.runtime.refresh();host.attach_control_runtime(c.runtime) + c.continuation=HostContinuation(c,c.runtime) + c.runtime.window_ready('session') + c.job=SimpleNamespace(_HANDLE=object()) + c.launch_api=SimpleNamespace(handle=lambda child:child,identity=lambda child:(child.pid,child.pid*10), + in_job=lambda child,job:True) + self.behavior='complete';self.pause_delay=0.;self.tasks=[];self.releases=[] + self.actual_popen=subprocess.Popen + def popen(command,**kwargs): + self.assertNotIn('--qualification-hold-after-report',command) + command=[sys.executable,str(CHILD),*command[command.index('--run'):], + '--behavior',self.behavior,'--pause-delay',str(self.pause_delay)] + return self.actual_popen(command,**kwargs) + patch=mock.patch.object(product.subprocess,'Popen',side_effect=popen);patch.start();self.patches.append(patch) + + async def asyncTearDown(self): + # A completed failed transition is already removed from its set, + # but the fixture restore loop still needs its Stop fence. + self.c.runtime.fence_admission() + for release in self.releases:release.set() + c=self.c + if c.runtime._continuation_tasks: + c.runtime.fence_admission() + await asyncio.gather(*(asyncio.shield(task) for task in tuple(c.runtime._continuation_tasks)),return_exceptions=True) + binding=None + if c.facade._selected is not None and c.facade._selected.get('binding') is not None: + binding=c.facade._selected['binding'] + if binding.worker.poll() is None: + binding.request_stop(c.facade._selected['capability']) + await binding.join(timeout=3) + for child in c.handles: + # CPU emergency bounds are 15 s original / 60 s selected; + # cleanup always joins the exact retained Popen, never PID lookup. + await asyncio.to_thread(child.wait,18) + # join(timeout=3) can return while its shielded post-exit SQLite + # verification still runs. Join that exact retained obligation before + # synchronous seed cleanup removes the files it is verifying. + # https://docs.python.org/3.12/library/asyncio-task.html#shielding-from-cancellation + if binding is not None and binding._work is not None: + await asyncio.shield(binding._work) + if c.facade._wake is not None:await c.facade._wake + if c.facade._stop is not None: + await asyncio.gather(asyncio.shield(c.facade._stop),return_exceptions=True) + for task in self.tasks: + await asyncio.gather(asyncio.shield(task),return_exceptions=True) + + def cleanup_seed(self): + # These nested TestCases were setUp(), not run(). Their default + # doCleanups() catches exceptions without a TestResult. Route both + # levels through this running case's existing outcome instead. + # https://github.com/python/cpython/blob/v3.12.10/Lib/unittest/case.py + with mock.patch.object(self.seed,'_outcome',self._outcome), \ + mock.patch.object(self.seed.f,'_outcome',self._outcome): + self.seed.doCleanups() + + def cleanup_retained(self): + # This also runs after incomplete setup or an asyncTearDown exception. + # A forced stop cleans a failed fixture; it never turns it into a pass. + c=self.c;joined=set();killed=set();errors=[] + for child in c.handles: + try: + if child.poll() is None:child.kill();killed.add(child) + child.wait(timeout=10);joined.add(child) + except Exception as exc:errors.append(exc) + for stream in c.logs: + try:stream.close() + except Exception as exc:errors.append(exc) + for patch in reversed(self.patches): + try:patch.stop() + except Exception as exc:errors.append(exc) + destination=os.environ.get('CONTINUATION_TEST_RECEIPTS') + if destination: + rows=[] + for item in self.child_receipts: + row={key:value for key,value in item.items() if key!='child'} + row.update(exit_code=item['child'].returncode,joined=item['child'] in joined, + cleanup_killed=item['child'] in killed, + join_observed_utc=datetime.now(timezone.utc).isoformat()) + # The existing opt-in CI receipt must outlive temporary logs. + # Capture only this exact retained label, after join and close. + log_path=c.root/(item['kind']+'.log') + try: + matching=[stream for stream in c.logs if Path(stream.name)==log_path] + if len(matching)!=1 or not matching[0].closed: + raise ValueError('Retained child log handle differs or remains open') + with log_path.open('rb') as stream: + stream.seek(0,os.SEEK_END);total=stream.tell() + stream.seek(max(0,total-65536));tail=stream.read(65536) + row['output_log']={'name':log_path.name,'total_bytes':total, + 'captured_bytes':len(tail),'truncated':total>len(tail), + 'tail_sha256':hashlib.sha256(tail).hexdigest(), + 'tail_utf8':tail.decode('utf-8',errors='replace')} + except (OSError,ValueError) as exc: + row['output_log']={'name':log_path.name,'capture_error':type(exc).__name__} + rows.append(row) + payload={'test':self.id(),'children':rows, + 'scope':'CPU Python child and actual SQLite; injected native/window/job boundaries; no SDK/model'} + if hasattr(self,'timeout_observation'): + try:after=timeout_diagnostics.after_join(self,joined) + except Exception as exc:after={'capture_error':type(exc).__name__} + payload['timeout_observation']={'before_teardown':self.timeout_observation,'after_join':after} + output=Path(destination);output.mkdir(exist_ok=True) + (output/(self._testMethodName+'.json')).write_text(json.dumps(payload,indent=2),encoding='utf-8') + if errors:raise ExceptionGroup('CPU fixture cleanup failed',errors) + + def request(self,ident='continue-1'): + return {'host_epoch':'same-host','task_id':'orders-report','session_id':'session','method':'continue', + 'request_id':ident,'execution_id':'orders-report','execution_generation':0, + 'expected_task_revision':self.old.revision} + + def intent(self):return not self.c.runtime._stopping and self.c.gui.session_live('session') + + async def select(self):return await self.c.runtime.select_continuation(self.request(),intent_valid=self.intent) + + async def restore(self): + while True: + record=self.c.continuation.current + if record and record.get('resume_id'): + snap=await asyncio.to_thread(record['mailbox'].snapshot) + if any(row['request_id']==record['resume_id'] and row['outcome']=='applied' for row in snap.receipts): + self.c.host.controller.state='ready' + return + if self.c.runtime._stopping:return + await asyncio.sleep(.01) + + async def continue_task(self,grant,*,restore=True): + if restore: + task=asyncio.create_task(self.restore());self.tasks.append(task) + return await self.c.runtime.continue_selected(grant,intent_valid=self.intent) + + async def start_transport(self): + c=self.c + self.transport=ControlTransport(c.prepared,c.runtime,capability=object(),runner_credential='b'*64,io_timeout=.1) + self.transport.bind_session(self.transport._capability,'session','a'*64) + self.assertTrue((await self.wire('window_ready'))['ok']) + + async def wire(self,method,**values): + request={'host_epoch':'same-host','task_id':'orders-report','session_id':'session','token':'a'*64,'method':method} + if method=='continue':request.update(self.request()) + request.update(values);writer=Writer() + await self.transport.admit(Reader((json.dumps(request)+'\n').encode()),writer) + return json.loads(writer.data) + + async def test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch(self): + c=self.c;await self.start_transport() + restoring=asyncio.create_task(self.restore());self.tasks.append(restoring) + first=await self.wire('continue');self.assertTrue(first['ok']) + task=self.transport._records['continue-1']['task'];self.tasks.append(task) + try: + await asyncio.wait_for(asyncio.shield(task), self.continuation_timeout_s) + except TimeoutError: + if os.environ.get('CONTINUATION_TEST_RECEIPTS'): + try:self.timeout_observation=timeout_diagnostics.before_teardown(self,task) + except Exception as exc:self.timeout_observation={'capture_error':type(exc).__name__} + raise + record=c.continuation.current + self.assertEqual(await c.join_child(record['child']),0) + duplicate=await self.wire('continue') + self.assertEqual(duplicate['continuation']['phase'],'running') + self.assertEqual(len(c.handles),2) + self.assertEqual([r['command'] for r in record['mailbox'].snapshot().requests],['pause','resume']) + # This fixture has no host observation loop to refresh after child exit. + await c.runtime.refresh() + reply=await self.wire('status') + self.assertTrue(reply['ok'],reply) + self.assertEqual(reply['status']['execution_id'],record['grant']['payload']['successor_id']) + await self.transport.aclose(timeout=3) + self.assertTrue(self.transport.lifecycle()['stop_confirmed']) + + async def test_old_read_error_still_faults_and_prevents_selection(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + def failed():entered.set();release.wait(4);raise OSError('actual old read failure') + with mock.patch.object(c.box,'snapshot',side_effect=failed): + read_task=asyncio.create_task(c.runtime.refresh());self.tasks.append(read_task) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + select_task=asyncio.create_task(self.select());self.tasks.append(select_task) + await asyncio.sleep(.05);release.set() + with self.assertRaisesRegex(OSError,'actual old read failure'):await read_task + with self.assertRaisesRegex(OSError,'actual old read failure'):await select_task + self.assertIn('database_operation_failed',c.runtime._faults) + self.assertIsNone(c.continuation.current);self.assertEqual(len(c.handles),1) + + async def test_inflight_mutation_error_is_not_retired_as_refresh(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + before=c.box.snapshot() + def failed(*args):entered.set();release.wait(4);raise OSError('actual mutation failure') + with mock.patch.object(c.box,'submit',side_effect=failed): + control=asyncio.create_task(c.runtime.submit_control('old-pause','pause'));self.tasks.append(control) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + await asyncio.sleep(.05);release.set() + with self.assertRaisesRegex(OSError,'actual mutation failure'):await control + with self.assertRaisesRegex(OSError,'actual mutation failure'):await choosing + self.assertIn('database_operation_failed',c.runtime._faults) + self.assertIsNone(c.continuation.current);self.assertEqual(c.box.snapshot(),before) + + async def test_normal_refresh_still_refuses_stale_observation(self): + with mock.patch.object(self.c.runtime,'_clock',side_effect=[10.,12.,12.]): + with self.assertRaisesRegex(RuntimeUnavailable,'stale'):await self.c.runtime.refresh() + self.assertIn('database_operation_failed',self.c.runtime._faults) + + async def test_stop_during_committed_authorization_retains_no_child_obligation(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + original=ContinuationAuthority.authorize + def held(authority,*args): + grant=original(authority,*args);entered.set();release.wait(4);return grant + with mock.patch.object(ContinuationAuthority,'authorize',new=held): + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + stop=asyncio.create_task(c.runtime.cooperative_stop());self.tasks.append(stop) + await asyncio.sleep(.05);self.assertTrue(c.runtime._stopping);self.assertFalse(stop.done()) + release.set();grant=await choosing;receipt=await stop + self.assertFalse(receipt['uncertain']);self.assertTrue(receipt['host_closed']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + self.assertEqual((await c.runtime.continue_selected(grant,intent_valid=self.intent))['phase'],'blocked') + self.assertEqual(len(c.handles),1) + + async def test_cancelled_select_waiter_does_not_cancel_committed_selection(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + original=ContinuationAuthority.authorize + def held(authority,*args): + grant=original(authority,*args);entered.set();release.wait(4);return grant + with mock.patch.object(ContinuationAuthority,'authorize',new=held): + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + self.assertTrue(await asyncio.to_thread(entered.wait,2));choosing.cancel() + with self.assertRaises(asyncio.CancelledError):await choosing + release.set() + await asyncio.gather(*(asyncio.shield(task) for task in tuple(c.runtime._continuation_tasks))) + self.assertIs(c.facade._selected,c.continuation.current) + self.assertEqual(len(c.handles),1);self.assertFalse(c.runtime._faults) + self.assertTrue((await c.runtime.cooperative_stop())['host_closed']) + + async def test_staging_failure_keeps_grant_obligation_without_relabelling_old_cache(self): + c=self.c;old_box=c.box;old_observe=c.runtime._observe + def observe(box=None): + if box is not None and box is not old_box:raise OSError('staging read failure') + return old_observe(box) + with mock.patch.object(c.runtime,'_observe',side_effect=observe): + with self.assertRaisesRegex(OSError,'staging read failure'):await self.select() + record=c.continuation.current + self.assertTrue(record['committed']);self.assertIs(c.facade._selected,record) + self.assertEqual(c.runtime.status()['execution_id'],'orders-report') + self.assertEqual(c.runtime._cache.snapshot.target.task_id,'orders-report') + self.assertEqual((await c.runtime.lookup_continuation(self.request()))['phase'],'blocked') + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertIsNotNone(receipt['fault']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_unknown_launch_slot_cannot_use_old_worker_receipt(self): + c=self.c;await self.select();record=c.continuation.current + record['launch_attempted']=True + with self.assertRaisesRegex(ReconciliationRequired,'known unstarted'): + await c.runtime.cooperative_stop() + self.assertFalse(c.host.closed);self.assertIsNone(c.facade.worker_receipt) + + async def test_committed_grant_lost_return_is_retained_but_never_launched(self): + c=self.c;original=ContinuationAuthority.authorize + def lost(authority,*args): + original(authority,*args);raise OSError('committed grant return lost') + with mock.patch.object(ContinuationAuthority,'authorize',new=lost): + with self.assertRaisesRegex(OSError,'return lost'):await self.select() + record=c.continuation.current + self.assertTrue(record['committed']);self.assertIs(c.facade._selected,record) + self.assertEqual(record['phase'],'blocked');self.assertIn('continuation_failed',c.runtime._faults) + self.assertEqual(c.runtime.selected_execution(),{'execution_id':'orders-report','execution_generation':0}) + self.assertEqual((await c.runtime.lookup_continuation(self.request()))['phase'],'blocked') + self.assertEqual(len(c.handles),1) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertIsNotNone(receipt['fault']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_unknown_grant_lookup_fences_clean_stop(self): + c=self.c;original=ContinuationAuthority.authorize + def lost(authority,*args): + original(authority,*args);raise OSError('committed grant return lost') + with mock.patch.object(ContinuationAuthority,'authorize',new=lost), \ + mock.patch.object(ContinuationAuthority,'lookup',side_effect=OSError('lookup unavailable')): + with self.assertRaisesRegex(OSError,'return lost'):await self.select() + self.assertIsNone(c.continuation.current) + self.assertFalse(c.continuation.records['continue-1']['authorization_resolved']) + with self.assertRaisesRegex(RuntimeUnavailable,'authorization outcome is unresolved'): + await c.runtime.cooperative_stop() + self.assertFalse(c.host.closed);self.assertIsNone(c.facade.worker_receipt) + + async def test_readiness_has_240_second_cap_with_longer_grant(self): + c=self.c;await self.select();record=c.continuation.current + # No new child: isolate the readiness timer with an explicitly live fixture. + record['child']=SimpleNamespace(pid=999,poll=lambda:None) + record['diagnostics']=c.work/'unused-readiness' + start=record['grant']['deadline_monotonic']-900 + with mock.patch.object(product,'execution_now',side_effect=[start,start+240]): + with self.assertRaisesRegex(TimeoutError,'readiness boundary'): + await c.ready_continuation(record,self.intent) + self.assertEqual(record['readiness_deadline_monotonic'],start+240) + record['child']=None + + async def test_readiness_never_extends_shorter_grant(self): + c=self.c;await self.select();record=c.continuation.current + record['child']=SimpleNamespace(pid=999,poll=lambda:None);record['diagnostics']=c.work/'unused-readiness' + deadline=record['grant']['deadline_monotonic'] + with mock.patch.object(product,'execution_now',side_effect=[deadline-30,deadline]): + with self.assertRaisesRegex(TimeoutError,'readiness boundary'): + await c.ready_continuation(record,self.intent) + self.assertEqual(record['readiness_deadline_monotonic'],deadline);record['child']=None + + async def test_long_bootstrap_status_preserves_selected_pair_and_live_intent(self): + c=self.c;self.pause_delay=1.3;self.behavior='hold' + grant=await self.select() + continuing=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(continuing) + while c.continuation.current['child'] is None:await asyncio.sleep(.01) + await asyncio.sleep(1.1) + status=c.runtime.status() + self.assertEqual(status['execution_id'],grant['execution_id']);self.assertFalse(c.runtime._faults) + self.assertEqual(c.runtime._cache.snapshot.target.task_id,grant['execution_id']) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + + async def test_atomic_select_keeps_logical_identity_and_new_snapshot(self): + c=self.c;old_box=c.box;old_binding=c.gate._binding + grant=await self.select();record=c.continuation.current + self.assertEqual(c.runtime.selected_execution(),selection(record['grant'])) + self.assertIs(c.host.controller.demand,c.registry);self.assertIs(c.runtime.mailbox,c.box) + self.assertIsNot(c.box,old_box);self.assertEqual(c.runtime.status()['task_revision'],0) + self.assertEqual(c.runtime._cache.snapshot.target.task_id,grant['execution_id']) + self.assertEqual(c.prepared.task_id,'orders-report');self.assertEqual(c.gate._binding,old_binding) + self.assertFalse(c.host.controller._demand_valid);self.assertEqual(c.host.controller._demand_rows,()) + self.assertEqual(rec.digest(c.store.load('orders-report').state),self.old_hash) + self.assertEqual(len(c.handles),1) + self.assertEqual(await c.runtime.lookup_continuation(self.request()),grant) + self.assertEqual(len(c.handles),1) + receipt=await c.runtime.cooperative_stop() + self.assertEqual(receipt,{'worker_joined':True,'host_closed':True,'uncertain':False,'fault':None}) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_actual_cpu_child_one_resume_shared_report_and_lifetime(self): + c=self.c;report=(c.work/'work/report.json').read_bytes();effects=(c.work/'effect-receipts.jsonl').read_bytes() + grant=await self.select() + original_save = rec.save + readiness_transitions = [] + def observe_save(path, value): + if Path(path).name == 'controller-ready.json': + readiness_transitions.append(c.runtime._transitioning) + return original_save(path, value) + with mock.patch.object(rec, 'save', side_effect=observe_save): + reply=await self.continue_task(grant) + self.assertEqual(readiness_transitions, [False]) + record=c.continuation.current + self.assertEqual(reply['phase'],'running') + self.assertEqual(await c.join_child(record['child']),0) + self.assertEqual(record['binding']._verify_exit(0),'task_completed') + snapshot=record['mailbox'].snapshot() + self.assertEqual([r['command'] for r in snapshot.requests],['pause','resume']) + self.assertEqual(snapshot.requests[-1]['request_id'],record['resume_id']) + ready=json.loads((record['diagnostics']/'controller-ready.json').read_bytes()) + self.assertEqual(ready['selected_execution'],selection(record['grant'])) + self.assertEqual((c.work/'work/report.json').read_bytes(),report) + self.assertEqual((c.work/'effect-receipts.jsonl').read_bytes(),effects) + self.assertEqual(snapshot.target.state['requests'],0) + self.assertEqual(await c.runtime.lookup_continuation(self.request()),{**grant,'phase':'blocked'}) + self.assertEqual(len(c.handles),2) + receipt=await c.runtime.cooperative_stop();self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + + async def test_new_task_admission_reuses_exact_completed_successor_verifier(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + from adaptive_inference.task_spec import TaskSpec, BUDGET + from adaptive_inference.demand import DemandIdentity + from test_task_admission import create_task + c=self.c + original_binding=c.stop_binding + grant=await self.select();await self.continue_task(grant) + record=c.continuation.current + self.assertEqual(await c.join_child(record['child']),0) + cp=record['mailbox'].snapshot().target + history=c.store.history(cp.task_id) + tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + with self.assertRaisesRegex(ReconciliationRequired,'Original worker no longer'): + with admission._released_checkpoint(admission.initial,'orders-report',self.old.revision, + stop_binding=original_binding):pass + task_spec=TaskSpec({'schema':'adaptive.task-spec.v1','prompt':'Explain one idea.', + 'inputs':[],'outputs':[],'budget':BUDGET,'validation':{'validator':None}}) + payload=admission.request(command_id='new-task',expected_selection_revision=0, + predecessor_execution_id=cp.task_id,predecessor_revision=cp.revision,spec=task_spec, + arrived_at_utc='2026-10-26T12:00:00Z') + authority_cp=admission.store.load(admission.task_id) + with admission.store.claim(admission.task_id,expected_revision=authority_cp.revision) as owner: + with self.assertRaisesRegex(ReconciliationRequired,'Unverified successor'): + admission.reserve(owner,payload,predecessor_cleanup_sha256='1'*64) + admission.retain_execution_binding(record['binding']) + # The continuation authority cannot be claimed while admission holds + # its stable lock; this prevents a concurrent selection during write. + saved=owner.save_checkpoint + def guarded_save(*args,**kwargs): + authority=record['authority'];current=c.store.load(authority.task_id) + with self.assertRaises(TaskBusyError): + with c.store.claim(current.task_id,expected_revision=current.revision):pass + return saved(*args,**kwargs) + with mock.patch.object(owner,'save_checkpoint',side_effect=guarded_save): + reservation=admission.reserve(owner,payload,predecessor_cleanup_sha256='1'*64) + operation=admission.begin(owner,reservation,action='prepare') + run=Path(payload['run']);run.mkdir();(run/'work').mkdir() + identity=DemandIdentity(**{key:payload['demand_identity'][key] for key in + ('registration_id','task_id','owner_id','arrived_at_utc')}) + create_task(TaskStore(run/'task.sqlite3'),identity,task_spec,payload['session_id']) + result_path=record['diagnostics']/f"result-{record['child'].pid}.json" + original=result_path.read_bytes() + try: + changed=json.loads(original);changed['continuation_cleanup_verified']=False + result_path.write_text(json.dumps(changed),encoding='utf-8') + with self.assertRaisesRegex(ReconciliationRequired,'Selected result'): + admission.prepared(owner,reservation,operation_id=operation) + finally: + result_path.write_bytes(original) + admission.prepared(owner,reservation,operation_id=operation) + self.assertEqual(admission.status()['selected']['task_id'],payload['task_id']) + self.assertEqual(c.store.history(cp.task_id),history) + + async def test_new_task_stop_reuses_committed_unlaunched_verifier(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + c=self.c;grant=await self.select();record=c.continuation.current + tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + history=c.store.history(grant['execution_id']) + with admission._unlaunched_checkpoint(admission.initial,c.continuation) as (cp,outcome): + self.assertEqual(cp.task_id,grant['execution_id']) + self.assertEqual(outcome,'selected_not_launched') + for ident in (record['authority'].task_id,cp.task_id): + current=c.store.load(ident) + with self.assertRaises(TaskBusyError): + with c.store.claim(ident,expected_revision=current.revision):pass + self.assertEqual(c.store.history(grant['execution_id']),history) + record['launch_attempted']=True + try: + with self.assertRaisesRegex(ReconciliationRequired,'known unstarted'): + with admission._unlaunched_checkpoint(admission.initial,c.continuation):pass + finally:record['launch_attempted']=False + + async def test_new_task_release_locks_first_continuation_authority_before_row_exists(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + from adaptive_inference.desktop.hermes.continuation_worker import scope_for + c=self.c;tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + with admission._released_checkpoint(admission.initial,'orders-report',self.old.revision, + stop_binding=c.stop_binding): + authority=ContinuationAuthority(c.store,'orders-report', + scope_for(c.work,c.plan,(c.work/'plan.json').read_bytes()),host_generation=c.host_epoch) + cp=c.store.load(authority.task_id) + with self.assertRaises(TaskBusyError): + with c.store.claim(cp.task_id,expected_revision=cp.revision):pass + + async def test_stop_before_initial_pause_ack_joins_new_child_without_resume(self): + self.pause_delay=.5;self.behavior='hold';c=self.c + grant=await self.select() + task=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(task) + while c.continuation.current['child'] is None:await asyncio.sleep(.01) + receipt=await c.runtime.cooperative_stop() + self.assertFalse(receipt['uncertain']);self.assertTrue(receipt['host_closed']) + record=c.continuation.current + self.assertEqual(record['child'].poll(),4) + self.assertFalse(any(row['command']=='resume' for row in record['mailbox'].snapshot().requests)) + self.assertEqual((await task)['phase'],'blocked') + + async def test_stop_while_native_readiness_waits_keeps_resume_once(self): + self.behavior='hold';c=self.c + grant=await self.select() + task=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(task) + while c.continuation.current['phase']!='running':await asyncio.sleep(.01) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + record=c.continuation.current + self.assertEqual(record['child'].poll(),4) + self.assertEqual(sum(r['command']=='resume' for r in record['mailbox'].snapshot().requests),1) + + async def test_stale_queued_control_cannot_mutate_selected_mailbox(self): + await self.select();c=self.c + with self.assertRaisesRegex(Refused,'stale_execution'): + await c.runtime.submit_control('old-click','resume',execution={'execution_id':'orders-report','execution_generation':0}) + self.assertEqual(c.box.snapshot().requests,()) + + async def test_status_during_old_read_drain_remains_exact_after_cache_expiry(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + actual=c.box.snapshot + def blocked():entered.set();release.wait(4);return actual() + with mock.patch.object(c.box,'snapshot',side_effect=blocked): + read_task=asyncio.create_task(c.runtime.refresh());self.tasks.append(read_task) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + select_task=asyncio.create_task(self.select());self.tasks.append(select_task) + await asyncio.sleep(1.1) + status=c.runtime.status() + self.assertEqual(status['execution_id'],'orders-report');self.assertEqual(status['task_revision'],self.old.revision) + self.assertFalse(c.runtime._faults);release.set() + await read_task;grant=await select_task + self.assertEqual(c.runtime.status()['execution_id'],grant['execution_id']) + self.assertEqual(c.runtime.status()['task_revision'],0) + + async def test_inflight_controller_read_is_drained_before_registry_commit(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + actual=c.registry.observations + def blocked():entered.set();release.wait(3);return actual() + with mock.patch.object(c.registry,'observations',side_effect=blocked): + old_read=asyncio.create_task(c.host.controller.refresh_demand());self.tasks.append(old_read) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + await asyncio.sleep(.05);self.assertFalse(choosing.done());release.set() + await old_read;await choosing + self.assertEqual(c.host.controller._demand_rows,());self.assertFalse(c.host.controller._demand_valid) + await c.host.controller.refresh_demand() + self.assertEqual({row.task_id for row in c.host.controller._demand_rows},{c.identity.task_id}) + + async def test_launch_diagnostic_failure_keeps_child_and_binding_for_stop(self): + c=self.c;grant=await self.select();actual=c.event + def event(kind,**fields): + if kind=='worker_launched':raise OSError('after retained CPU child') + return actual(kind,**fields) + with mock.patch.object(c,'event',side_effect=event): + with self.assertRaisesRegex(OSError,'retained CPU child'):await self.continue_task(grant,restore=False) + record=c.continuation.current + self.assertIs(record['child'],c.handles[-1]);self.assertIsNotNone(record['binding']) + receipt=await c.runtime.cooperative_stop() + self.assertEqual(record['child'].poll(),4);self.assertIsNotNone(receipt['fault']) + + async def test_old_or_wrong_generation_result_refused(self): + c=self.c;grant=await self.select();await self.continue_task(grant) + record=c.continuation.current;self.assertEqual(await c.join_child(record['child']),0) + path=record['diagnostics']/f"result-{record['child'].pid}.json";raw=path.read_bytes() + value=json.loads(raw);value['selected_execution']['execution_generation']=0;rec.save(path,value) + with self.assertRaisesRegex(ReconciliationRequired,'Selected result identity'):record['binding']._verify_exit(0) + path.write_bytes(raw) + self.assertEqual(record['binding']._verify_exit(0),'task_completed') + # Even a valid selected result in the old shared root cannot substitute + # for this execution's scoped diagnostic file. + legacy=c.work/path.name;legacy.write_bytes(raw);path.unlink() + try: + with self.assertRaises(FileNotFoundError):record['binding']._verify_exit(0) + finally:path.write_bytes(raw);legacy.unlink() + + async def test_selected_result_requires_matching_lifetime_returncode(self): + c=self.c;grant=await self.select();await self.continue_task(grant) + record=c.continuation.current;self.assertEqual(await c.join_child(record['child']),0) + authority=record['authority'];history=c.store.history(authority.task_id) + completed=next(row for row in reversed(history) if row['event']=='operation_completed') + changed=deepcopy(completed['payload']);changed['event_data']['result']['returncode']=4 + # Connection's transaction context does not close the SQLite handle. + # https://docs.python.org/3.11/library/sqlite3.html#how-to-use-the-connection-context-manager + with closing(sqlite3.connect(c.store.path)) as db, db: + db.execute('UPDATE history SET payload=? WHERE task_id=? AND revision=?', + (json.dumps(changed),authority.task_id,completed['revision'])) + with self.assertRaisesRegex(ReconciliationRequired,'exact result'):record['binding']._verify_exit(0) + with closing(sqlite3.connect(c.store.path)) as db, db: + db.execute('UPDATE history SET payload=? WHERE task_id=? AND revision=?', + (json.dumps(completed['payload']),authority.task_id,completed['revision'])) + + +class FixtureStartupTests(unittest.TestCase): + def test_delayed_import_readiness_has_its_own_bound(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'ready.json' + child=SimpleNamespace(pid=123,poll=lambda:None) + async def release(_): + path.write_text(json.dumps({'pid':123,'work_guard_s':15}),encoding='utf-8') + # Deterministically simulate 11 seconds before ready, without wall + # clock sleeps or a tiny scheduling threshold on the CI runner. + clock=mock.Mock(side_effect=[0.,11.]) + with mock.patch.object(asyncio,'sleep',side_effect=release) as paused: + asyncio.run(wait_original_ready(child,path,clock=clock)) + self.assertEqual(paused.call_count,1) + + def test_readiness_timeout_wrong_identity_and_emergency_exit_refuse(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'ready.json' + child=SimpleNamespace(pid=123,poll=lambda:None) + with self.assertRaisesRegex(TimeoutError,'startup deadline'): + asyncio.run(wait_original_ready(child,path,clock=mock.Mock(side_effect=[0.,31.]))) + path.write_text(json.dumps({'pid':999,'work_guard_s':15}),encoding='utf-8') + with self.assertRaisesRegex(AssertionError,'identity differs'): + asyncio.run(wait_original_ready(child,path)) + path.unlink();child.poll=lambda:124;child.returncode=124 + with self.assertRaisesRegex(AssertionError,'before readiness: 124'): + asyncio.run(wait_original_ready(child,path)) + + @unittest.skipUnless(os.name=='nt','Windows retained-process setup fixture') + def test_async_setup_failure_still_joins_child_and_closes_streams(self): + class FailedSetup(HostTests): + async def test_injected_setup_failure(self): + self.fail('Test body must not run') + case=FailedSetup('test_injected_setup_failure') + result=unittest.TestResult() + with mock.patch.object(sys.modules[__name__],'wait_original_ready', + side_effect=RuntimeError('injected readiness failure')): + case.run(result) + nested='\n'.join(trace for _,trace in result.errors+result.failures) + self.assertEqual(len(result.errors),1,nested) + self.assertIn('injected readiness failure',result.errors[0][1]) + self.assertFalse(result.failures,nested) + self.assertIsNotNone(case.c.worker.returncode) + self.assertTrue(all(stream.closed for stream in case.c.logs)) + self.assertFalse(case.c.work.exists()) + + + @unittest.skipUnless(os.name=='nt','Windows retained-process body failure fixture') + def test_async_body_error_still_finishes_restore_and_retained_cleanup(self): + class FailedBody(HostTests): + async def test_injected_body_failure(self): + grant=await self.select();launch=self.c.launch_continuation + async def fail_after_pause(record,coordinator,intent_valid): + await launch(record,coordinator,intent_valid) + raise RuntimeError('injected failure after initial Pause before Resume') + self.c.launch_continuation=fail_after_pause + await self.continue_task(grant) + + async def asyncTearDown(self): + try: + # Bound this regression itself if the old teardown bug + # returns; original production/fixture budgets stay intact. + await asyncio.wait_for(super().asyncTearDown(),20) + finally: + self.c.runtime.fence_admission() + await asyncio.gather(*self.tasks,return_exceptions=True) + case=FailedBody('test_injected_body_failure');result=unittest.TestResult() + case.run(result) + nested='\n'.join(trace for _,trace in result.errors+result.failures) + self.assertEqual(len(result.errors),1,nested) + self.assertIn('injected failure after initial Pause before Resume',result.errors[0][1]) + self.assertFalse(result.failures,nested) + self.assertTrue(case.c.runtime._stopping) + self.assertTrue(all(task.done() and not task.cancelled() for task in case.tasks)) + self.assertFalse(case.c.runtime._continuation_tasks) + self.assertFalse(case.c.runtime._pending) + self.assertEqual([child.returncode for child in case.c.handles],[5,4]) + self.assertTrue(all(stream.closed for stream in case.c.logs)) + self.assertFalse(case.c.work.exists()) + + +if __name__=='__main__':unittest.main() diff --git a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py new file mode 100644 index 0000000..5396f70 --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py @@ -0,0 +1,571 @@ +"""Real SQLite/mailbox and bounded owned threads; fake streams/UI/lifecycle.""" +import asyncio +from contextlib import contextmanager +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import threading +import time +import unittest +from unittest import mock + +from adaptive_inference.control_mailbox import Mailbox, ControlBlocked, ACKS +from adaptive_inference.demand import DemandIdentity, DemandRegistry, Registration, NAMESPACE +from adaptive_inference.task_store import TaskStore, TaskBusyError +from adaptive_inference.desktop.control_transport import ControlTransport, PreparedTask +from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime, RuntimeUnavailable +from test_desktop_control_transport import Reader, Writer +from adaptive_inference.desktop.ui_intent import UIBinding, UIIntentGate, VerifiedUIObservation + +NOW = '2026-10-24T18:00:00Z' +OBJECTIVE = 'Keep the original task and finish the validated artifact.' + + +class DataFixture: + def data(self): + self.temp = tempfile.TemporaryDirectory(prefix='ui-mailbox-fixture-') + self.addCleanup(self.temp.cleanup) + self.store = TaskStore(Path(self.temp.name) / 'task.sqlite') + self.identity = DemandIdentity('registered', 'task', 'trusted-host', NOW) + self.store.create('task', OBJECTIVE, metadata={NAMESPACE: self.identity.metadata()}) + self.registry = DemandRegistry((Registration(self.store, self.identity),)) + self.cap = object() + self.box = Mailbox(self.registry, 'registered', requester_capability=self.cap) + + def claim(self): + return self.store.claim('task', expected_revision=self.store.load('task').revision) + + def demand(self, state='waiting'): + with self.claim() as owned: + self.registry.transition('registered', owned, state, + expected_revision=owned.checkpoint.revision, updated_at_utc=NOW) + + def submit(self, ident, command): + return self.box.submit(self.cap, ident, command) + + def consume(self): + with self.claim() as owned: + return self.box.consume(owned, updated_at_utc=NOW) + + + + +class FakeLifecycle: + """Only explicit fixture events establish simulated join; no native proof.""" + def __init__(self): + self.bindings = {'session': UIBinding('host', 'session', 'task', 101, 202, 303)} + self.live = {'session'} + self.fenced = False + self.join_entered = asyncio.Event() + self.join = asyncio.Event() + self.join.set() + self.stop_calls = 0 + self.result = {'worker_joined': True, 'host_closed': True, 'uncertain': False, 'fault': None} + + def session_is_live(self, session): + return session in self.live + + def window_ready(self, session): + return self.bindings.get(session) + + def window_closed(self, session): + self.live.discard(session) + + def status(self): + return {'host_phase': 'ready', 'waiting_reason': None} + + def fence_admission(self): + self.fenced = True + + async def cooperative_stop(self): + self.stop_calls += 1 + self.join_entered.set() + await self.join.wait() + return deepcopy(self.result) + + +class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.data() + self.demand() + self.intent = object() + self.gate = UIIntentGate(host_epoch='host', capability=self.intent, + verify_current=lambda binding: VerifiedUIObservation(binding, time.monotonic(), True, True)) + self.host = FakeLifecycle() + self.prepared = PreparedTask('host', 'task', OBJECTIVE) + # Observation age is a logical fixture input; real SQLite setup speed + # does not establish expiry. The freshness tests advance it explicitly. + self.observation_time = 20. + self.runtime = MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=self.intent, lifecycle=self.host, max_age_s=3, + clock=lambda: self.observation_time) + self.transport_cap = object() + self.transport = ControlTransport(self.prepared, self.runtime, capability=self.transport_cap, + runner_credential='b' * 64, io_timeout=.25) + self.transport.bind_session(self.transport_cap, 'session', 'a' * 64) + self.releases = [] + await self.runtime.refresh() + self.assertTrue((await self.request('window_ready'))['ok']) + + async def asyncTearDown(self): + for event in self.releases: + event.set() + self.host.join.set() + await self.transport.aclose(timeout=3) + await self.runtime.cooperative_stop() + + def frame(self, method, **changes): + value = {'host_epoch': 'host', 'task_id': 'task', 'session_id': 'session', + 'token': 'a' * 64, 'method': method} + if method in ('pause', 'resume', 'cancel'): + value['request_id'] = 'one' + value.update(self.runtime.selected_execution()) + value.update(changes) + return (json.dumps(value) + '\n').encode() + + async def request(self, method, **changes): + writer = Writer() + task = self.transport.admit(Reader(self.frame(method, **changes)), writer) + await task + return json.loads(writer.data) + + async def completed_control(self, method, *, request_id): + # A handler may truthfully return pending before the retained mutation + # and its authority read finish. Final-intent assertions need that task. + reply = await self.request(method, request_id=request_id) + self.assertTrue(reply['ok'], reply) + await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) + return reply + + def matches(self): + now = time.monotonic() + return self.gate.matches({'monotonic_s': now, 'foreground': { + 'valid': True, 'hwnd': 101, 'pid': 202, 'creation_time_100ns': 303}}, now) + + async def signal(self, event): + # Bounded waiting uses the actual threading event, not a timing premise. + self.assertTrue(await asyncio.to_thread(event.wait, 2)) + + def block_observation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.snapshot + + def blocked(): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture snapshot release timed out') + return actual() + return entered, release, blocked + + async def test_real_latest_resume_authority_pending_and_applied(self): + self.demand('paused') + value = await self.runtime.submit_control('new-resume', 'resume') + self.assertEqual(value['outcome'], 'pending') + self.assertFalse(value['owner_acknowledged']) + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.consume() + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.assertTrue(self.runtime.status()['controls'][0]['owner_acknowledged']) + + async def test_historical_applied_resume_followed_by_pause_never_activates(self): + self.submit('old-resume', 'resume'); self.consume() + self.submit('later-pause', 'pause'); self.consume() + history = self.store.history(self.box.task_id) + value = await self.completed_control('resume', request_id='old-resume') + self.assertTrue(value['ok']) + self.assertFalse(self.matches()) + self.assertEqual(history, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('old-resume'), False) + + async def test_new_resume_activates_real_gate_then_pause_revokes(self): + result = await self.completed_control('resume', request_id='fresh') + self.assertTrue(result['ok']) + self.assertTrue(self.matches()) + await self.completed_control('pause', request_id='pause') + self.assertFalse(self.matches()) + self.assertEqual(self.store.load('task').prompt, OBJECTIVE) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + + async def test_missing_demand_refuses_authority(self): + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.pop(NAMESPACE) + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + await self.runtime.submit_control('resume', 'resume') + self.assertIs(await self.runtime.resume_authority('resume'), False) + + async def test_inflight_and_interrupted_replacement_are_never_authority(self): + self.submit('resume', 'resume') + with self.claim() as owned: + owned.begin_operation('effect', details={'fixture': 'not executed'}, + expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'operation_inflight') + with self.claim() as replacement: + self.assertTrue(replacement.checkpoint.requires_reconciliation) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'reconciliation') + + async def test_terminal_target_revokes_intent_and_refuses_authority(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.update(status='accepted', output='Validated fixture result') + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertFalse(self.matches()) + + async def test_changed_objective_is_refused_without_claiming_target(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + self.assertIsNone(self.store.load('task').owner_epoch) + + async def test_mismatched_objective_refuses_before_any_new_control_commit(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + before = self.store.history(self.box.task_id) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.submit_control('must-not-record', 'cancel') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_wrong_intent_capability_is_rejected_before_async_work(self): + with self.assertRaises(ValueError): + MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=object(), lifecycle=self.host) + + async def test_refresh_revokes_external_later_pause_even_before_owner_ack(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + self.consume() + self.submit('external-pause', 'pause') + await self.runtime.refresh() + self.assertFalse(self.matches()) + self.assertEqual(self.runtime.status()['controls'][-1]['outcome'], 'pending') + self.consume() + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertFalse(self.matches()) + + async def test_latest_pending_resume_can_keep_intent_while_owner_is_paused(self): + self.demand('paused') + # The explicit SQLite setup mutation may outlive the cached observation. + # Establish fresh admission before testing retained Resume intent. + await self.runtime.refresh() + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertTrue(self.matches()) + + async def test_pending_reply_precedes_retained_resume_authority_completion(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + actual_request = self.request + responded = asyncio.get_running_loop().create_future() + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + async def observed_request(*args, **kwargs): + reply = await actual_request(*args, **kwargs) + responded.set_result(reply) + return reply + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority), \ + mock.patch.object(self, 'request', side_effect=observed_request): + completed = asyncio.create_task(self.completed_control('resume', request_id='resume')) + try: + # Establish authority entry after SQLite submission before the + # unchanged two-second held-snapshot entry check. + await asyncio.wait_for(authority_entered.wait(), 30) + await self.signal(entered) + operation = self.transport._records['resume']['task'] + # Observe the actual response before releasing authority; no sleep + # or assumption about SQLite/CI scheduling establishes this order. + reply = await asyncio.wait_for(asyncio.shield(responded), 2) + self.assertEqual(reply, {'ok': True, **self.runtime.selected_execution(), 'state': 'pending', 'request_id': 'resume', 'recorded': None}) + self.assertFalse(operation.done()) + self.assertFalse(completed.done()) + self.assertFalse(self.matches()) + finally: + release.set() + # Settle the extra fixture task even if an assertion failed; + # shield keeps its retained control operation uncancelled. + await asyncio.wait_for(asyncio.shield( + asyncio.gather(completed, return_exceptions=True)), 3) + self.assertEqual(completed.result(), reply) + self.assertTrue(operation.done()) + self.assertTrue(self.matches()) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_crash_gap_pending_resume_does_not_invent_ack_or_reapply(self): + self.demand('paused') + self.submit('resume', 'resume') + with self.claim() as owned: + real_save = owned.save_checkpoint + def fail_ack(state, **kwargs): + if ACKS in state: + raise OSError('Fixture failed ACK commit') + return real_save(state, **kwargs) + with mock.patch.object(owned, 'save_checkpoint', side_effect=fail_ack): + with self.assertRaises(OSError): + self.box.consume(owned, updated_at_utc=NOW) + revision = owned.checkpoint.revision + self.assertIs(await self.runtime.resume_authority('resume'), True) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + self.assertEqual(owned.checkpoint.revision, revision) + with self.claim() as replacement: + before = replacement.checkpoint.revision + self.box.consume(replacement, updated_at_utc=NOW) + self.assertEqual(replacement.checkpoint.revision, before + 1) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'applied') + + async def test_cancelled_refresh_retains_thread_and_stop_waits_for_it(self): + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + stopping = asyncio.create_task(self.runtime.cooperative_stop()) + await asyncio.sleep(0) + self.assertFalse(self.host.join_entered.is_set()) + self.assertFalse(stopping.done()) + release.set() + result = await asyncio.wait_for(stopping, 3) + self.assertTrue(result['worker_joined'] and result['host_closed']) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 0) + + async def test_cancelled_submit_commits_once_then_explicit_retry_observes_it(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def blocked(*args): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture submit timed out') + return actual(*args) + with mock.patch.object(self.box, 'submit', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.submit_control('stable', 'pause')) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + release.set() + await self.runtime.refresh() + before = self.store.history(self.box.task_id) + result = await self.runtime.submit_control('stable', 'pause') + self.assertEqual(result['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_disconnected_transport_keeps_real_commit_and_stop_join_obligation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def hold_after_commit(*args): + value = actual(*args) + entered.set() + if not release.wait(3): + raise AssertionError('Fixture postcommit wait exceeded bound') + return value + with mock.patch.object(self.box, 'submit', side_effect=hold_after_commit): + writer = Writer(broken=True) + handler = self.transport.admit(Reader(self.frame('pause', request_id='durable')), writer) + await self.signal(entered) + handler.cancel() + await asyncio.gather(handler, return_exceptions=True) + pending = await self.transport.aclose(timeout=.02) + self.assertFalse(pending['closed']) + self.assertFalse(self.host.join_entered.is_set()) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + self.assertEqual(self.box.status('durable')['outcome'], 'pending') + release.set() + final = await self.transport.aclose(timeout=3) + self.assertTrue(final['closed']) + self.assertTrue(writer.closed and writer.joined) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_capacity_remains_bound_while_cancelled_waiter_thread_runs(self): + self.runtime.max_pending = 1 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + release.set() + await self.runtime.cooperative_stop() + + async def test_status_cache_expires_and_does_not_do_database_io(self): + self.runtime.max_age_s = .01 + self.observation_time += .02 + with mock.patch.object(self.box, 'snapshot', side_effect=AssertionError('Blocking status read')): + with self.assertRaises(RuntimeUnavailable): + self.runtime.status() + self.runtime.max_age_s = 3 + await self.runtime.refresh() + + async def test_slow_snapshot_uses_start_time_not_completion_for_freshness(self): + self.runtime.max_age_s = .02 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + self.observation_time += .03 + release.set() + with self.assertRaises(RuntimeUnavailable): + await waiter + + async def test_failed_snapshot_does_not_turn_into_successful_cleanup(self): + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Private fixture path')): + with self.assertRaises(OSError): + await self.runtime.resume_authority('one') + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + self.assertNotIn('Private fixture path', str(self.runtime.diagnostics())) + + async def test_prior_runtime_fault_cannot_be_hidden_by_later_resume_observation(self): + self.submit('resume', 'resume') + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Fixture read failed')): + with self.assertRaises(OSError): + await self.runtime.refresh() + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['host_phase'], 'fault') + self.assertFalse(self.matches()) + + async def test_postcommit_read_failure_retry_observes_commit_but_does_not_clear_fault(self): + actual_snapshot = self.box.snapshot + count = 0 + def fail_second_read(): + nonlocal count + count += 1 + if count == 2: + raise OSError('Fixture observation lost after actual commit') + return actual_snapshot() + with mock.patch.object(self.box, 'snapshot', side_effect=fail_second_read): + with self.assertRaises(OSError): + await self.runtime.submit_control('resume', 'resume') + before = self.store.history(self.box.task_id) + self.assertEqual(self.box.status('resume')['outcome'], 'pending') + retried = await self.runtime.submit_control('resume', 'resume') + self.assertEqual(retried['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('resume'), False) + stopped = await self.runtime.cooperative_stop() + self.assertTrue(stopped['worker_joined'] and stopped['host_closed']) + self.assertEqual(stopped['fault'], 'runtime_operation_failed') + + async def test_malformed_real_ack_prefix_never_becomes_resume_authority(self): + self.submit('resume', 'resume'); self.consume() + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state[ACKS]['receipts'][0]['request_id'] = 'different' + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + with self.assertRaises(ControlBlocked): + await self.runtime.resume_authority('resume') + self.assertFalse(self.matches()) + + async def test_failed_host_fence_remains_visible_even_if_worker_later_joins(self): + with mock.patch.object(self.host, 'fence_admission', side_effect=RuntimeError('Fixture fence failure')): + with self.assertRaises(RuntimeError): + self.runtime.fence_admission() + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + + async def test_stop_waiter_cancellation_preserves_worker_join_and_host_fault(self): + self.host.join.clear() + self.host.result['fault'] = 'fixture_host_fault' + waiter = asyncio.create_task(self.runtime.cooperative_stop()) + await self.host.join_entered.wait() + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + self.assertFalse(self.runtime._stop_task.done()) + self.host.join.set() + result = await self.runtime.cooperative_stop() + self.assertEqual(result['fault'], 'fixture_host_fault') + self.assertEqual(self.host.stop_calls, 1) + + async def test_pause_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # As in the rotation test, first establish SQLite submission, then + # retain the unchanged two-second bound on the held thread entry. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + pause = await self.request('pause', request_id='pause') + self.assertTrue(pause['ok']) + self.assertFalse(self.matches()) + release.set() + await first + await self.transport._records['pause']['task'] + self.assertFalse(self.matches()) + + async def test_rotation_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # SQLite submission precedes the held read. Establish that setup + # explicitly before the unchanged two-second thread-entry check. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + self.transport.bind_session(self.transport_cap, 'new-session', 'c' * 64) + self.host.live.add('new-session') + self.host.bindings['new-session'] = UIBinding('host', 'new-session', 'task', 404, 505, 606) + self.assertTrue((await self.request('window_ready', session_id='new-session', token='c' * 64))['ok']) + release.set() + await first + await asyncio.wait_for(asyncio.shield(self.transport._records['resume']['task']), 3) + self.assertFalse(self.matches()) + + async def test_projection_keeps_only_bounded_fields_and_objective(self): + await self.request('pause', request_id='pause') + value = await self.request('status') + text = json.dumps(value) + self.assertTrue(value['ok']) + self.assertEqual(value['status']['objective'], OBJECTIVE) + for forbidden in ('owner_epoch', 'metadata', str(self.store.path), 'a' * 64, 'b' * 64): + self.assertNotIn(forbidden, text) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/provenance/ci-fixtures/2026-11-09/record.json b/provenance/ci-fixtures/2026-11-09/record.json new file mode 100644 index 0000000..fcba1ad --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/record.json @@ -0,0 +1,145 @@ +{ + "date": "2026-11-09", + "scope": "Test-fixture timing correction; no production runtime or task-budget change", + "base_revision": "0ee8d3726b8184648b8f5e003e527e6409c37f76", + "files": { + "tests/test_continuation_host.py": { + "predecessor_source": "provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py", + "predecessor_sha256": "4858cf5d589dd3960ccf16f16896c8c6c085672d8c6aa4425c37184f02810bbf", + "successor_sha256": "aadeefab7622f5dba4fb1c604cfeca9d09c52263b44a98e62e794a09001ddee0" + }, + "tests/test_desktop_mailbox_runtime.py": { + "predecessor_source": "provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py", + "predecessor_sha256": "02cb14272823904f022307a5757f3a13daa9fe68bf734d6e6238554d57268697", + "intermediate_successor_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430", + "successor_sha256": "55ebc08fa3def908431556bd3e2089a034762296df235ae1df1dbe1647e730c4" + } + }, + "original_failures": [ + { + "run": 42554614440, + "job": 128957438758, + "attempt": 1, + "head": "4796c680920a4af3ee0ed7cef8b0b153d66852ba", + "platform": "windows-latest / Python 3.12", + "test": "test_continuation_host.HostTests.test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch", + "failure": "ExpiredObservation during resume_authority -> MailboxRuntime._install", + "original_outcome": "failed; not rerun or relabeled", + "log_sha256": "deb14ea4ecc09d3e7ebdd01d2089a489e7219832d1eb40661b2adb0033018018" + }, + { + "run": 42555520520, + "job": 128960228351, + "attempt": 1, + "head": "0ee8d3726b8184648b8f5e003e527e6409c37f76", + "platform": "windows-latest / Python 3.11", + "test": "test_desktop_mailbox_runtime.RuntimeTests.test_refresh_revokes_external_later_pause_even_before_owner_ack", + "failure": "TimeoutError in the three-second whole-fixture completed_control join guard", + "original_outcome": "failed; not rerun or relabeled", + "log_sha256": "0ae8d1ee261c9bc59cfee0d1553157b1f2ded137e7329a23998a2b5c076cbede" + } + ], + "changes": [ + "Use the existing logical-observation-clock fixture pattern for one ordinary continuation transport test, refreshing its cache under that clock.", + "Use the existing project/CPython 30-second whole-fixture guard for joining retained control work; keep shielding and final-intent assertions." + ], + "unchanged": [ + "All 77 production runtime modules", + "Production observation freshness and read timeouts", + "Real child, UI, execution and orchestration clocks", + "Task and tool allowances", + "Stale-observation refusals and duplicate-launch assertions" + ], + "validation": { + "tested_revision": "a83844b5fed20bf01dcfd61d4061b00961b787cc", + "mailbox_fixture_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430", + "selected_tests": 12, + "failures": 0, + "errors": 0, + "skips": 0, + "scope": "Both previously failing cases, six defining-module completed_control callers and five expiry/refusal cases; the two aliased callers were not included", + "test_elapsed_s": 3.108, + "receipt_sha256": "debe2466a111ceec2c3186c44fd6f8ca6b329ff443c4da80a67de6c65a7eaffa", + "owned_child_exit_codes": [ + 5, + 5, + 0 + ], + "all_owned_children_joined": true, + "cleanup_killed": false, + "new_hosted_result": "Not claimed by this local record; observe the new commit original workflows separately" + }, + "historical_reader_followup": { + "failed_head": "a83844b5fed20bf01dcfd61d4061b00961b787cc", + "original_failed_runs": [42557615924, 42557619469], + "original_attempt": 1, + "failure": "Historical compiler source assertion read the corrected live continuation fixture rather than its exact retained predecessor", + "representative_job": 128966748035, + "representative_log_sha256": "8a25fb23a4e5e98b531df85501b4167d5aa9433dd25fc4e43cd84a1e6fee5146", + "change": "Reuse the existing outer source-lineage reader to project only the two declared exact successor fixture hashes to their retained predecessors. Unknown bytes still reach unchanged historical assertions. Runtime projection and original manifests are unchanged.", + "source_sha256": { + "tests/automatic_tool_recovery_lineage.py": "49e2c487d8d5f3dad0840523d0fc8bbda2c642e30bd0c685220fe0a5cca18628", + "tests/test_automatic_tool_recovery_lineage.py": "dcf8700726072b98289c6c43ebfa66052767918fe97bb4004bb4f1546c8cf41a" + }, + "local_validation": { + "status": "incomplete_outer_guard_timeout", + "planned_tests": 145, + "completed_passes": 118, + "remaining_including_interrupted": 27, + "logged_failures_or_errors": 0, + "all_three_new_archive_regressions_completed": true, + "original_failed_compiler_assertion_completed": true, + "outer_guard_s": 600, + "retained_test_child_joined": true, + "retained_test_child_terminated_at_guard": true, + "supervisor_exit_code": 1, + "candidate_hashes_match_prelaunch_record": true, + "full_worker_source_inventory_receipt_written": false, + "log_sha256": "67c77aa183e43672656e908b7822c0e50f51e227d60ea8218e61ddc91c9d614f", + "supervisor_sha256": "da520007b3171be30b9a38dd9137c63c333e0f2a18c102cc52563e010bd7aa98", + "partial_audit_sha256": "9cd9533be307b1a684a1ed7f4cb05d2f89d12c62fb42d4e50a715a55f420b237", + "interpretation": "Partial log evidence, not a passing full lineage suite. No rerun or guard extension. Fresh required hosted CI must validate the full successor." + } + }, + "shared_helper_followup": { + "failed_head": "411a3e0ed528a8b99c54f169574d0f7765c67c15", + "original_runs": [42559679949, 42559684062], + "representative_job": 128973128415, + "representative_log_sha256": "d891ff6612f985f308ec2b254dfd3cdaf686ce3352477a1af45f42c9a54867fa", + "failure": "ExpiryRuntimeTests aliases completed_control without inheriting RuntimeTests, so the new self.control_completion_timeout_s attribute was absent", + "change": "Move the unchanged 30-second guard to the defining module scope so both direct and aliased callers use the same bound. Preserve shielding, assertions and production source.", + "validation": { + "tests": 12, + "failures": 0, + "errors": 0, + "skips": 0, + "scope": "Six direct and two aliased helper callers, three archive regressions and the original compiler source assertion", + "elapsed_s": 8.265, + "sources_unchanged": true, + "supervisor_exit_code": 0, + "retained_test_child_joined": true, + "timed_out": false, + "runtime_or_task_budgets_changed": false, + "result_sha256": "9099e90fd24e27928d59a2b26203dac55b1a8107e46d51f248de81d62e0d96e5", + "supervisor_sha256": "41fa51c1bd38a87a4ec741b1cb3738515544d784d97f83a33fbe46905f5a3567", + "log_sha256": "aa00a1903efb47e508dcf741aeeaf083c3f7645ca9bbf32f0065cf6281afc464", + "full_hosted_successor": "Required separately; the earlier failed runs and incomplete local sweep remain unchanged" + }, + "reference": "https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces" + }, + "research": [ + { + "url": "https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py", + "use": "Existing SHORT_TIMEOUT whole-test orchestration guard, not a product latency threshold" + }, + { + "url": "https://docs.python.org/3.12/library/asyncio-task.html#shielding-from-cancellation", + "use": "Keep retained asynchronous work shielded and joined after the caller waits" + } + ], + "limits": [ + "These are CPU/source fixture checks, not installed-agent, model, native-engine or physical responsiveness results.", + "Historical failure logs stay failed. Increasing a fixture join allowance does not extend any product task allowance.", + "Retained predecessor files are historical data for offline inspection, not the selected test implementation." + ] +} diff --git a/tests/automatic_tool_recovery_lineage.py b/tests/automatic_tool_recovery_lineage.py index 31474dd..d4cf4cf 100644 --- a/tests/automatic_tool_recovery_lineage.py +++ b/tests/automatic_tool_recovery_lineage.py @@ -13,6 +13,47 @@ ROOT = Path(__file__).resolve().parents[1] ARCHIVE = 'provenance/automatic-tool-recovery' +CI_FIXTURES = 'provenance/ci-fixtures/2026-11-09' +CI_FIXTURE_FILES = {'tests/test_continuation_host.py', 'tests/test_desktop_mailbox_runtime.py'} + + +def ci_fixture_record(root): + path = root / CI_FIXTURES / 'record.json' + if not path.exists(): + return None + rows = json.loads(path.read_bytes()) + if set(rows['files']) != CI_FIXTURE_FILES: + raise AssertionError('Changed CI fixture source set') + return rows + + +def ci_fixture_inputs(root): + rows = ci_fixture_record(root) + if rows is None: + return [] + return [root / CI_FIXTURES / 'record.json', + *(root / CI_FIXTURES / 'predecessor' / relative for relative in sorted(CI_FIXTURE_FILES))] + + +def ci_fixture_predecessor(relative, data, root): + """Reuse exact-file projection for later test-only timing corrections. + + Archived fixtures are data only. Their old qualification does not apply to + the corrected tests, and unknown bytes still face unchanged inner checks. + """ + if relative not in CI_FIXTURE_FILES: + return data + rows = ci_fixture_record(root) + if rows is None: + return data + row = rows['files'][relative] + expected = CI_FIXTURES + '/predecessor/' + relative + if row['predecessor_source'] != expected: + raise AssertionError('Changed CI fixture predecessor path') + before = (root / expected).read_bytes() + if hashlib.sha256(before).hexdigest() != row['predecessor_sha256']: + raise AssertionError('Changed CI fixture predecessor bytes') + return before if hashlib.sha256(data).hexdigest() == row['successor_sha256'] else data def record(root=None): @@ -25,9 +66,10 @@ def inputs(root=None): root = ROOT if root is None else root rows = record(root) if rows is None: - return [] + return ci_fixture_inputs(root) return [root / ARCHIVE / 'source-lineage.json', - *(root / row['predecessor_source'] for row in rows['files'].values())] + *(root / row['predecessor_source'] for row in rows['files'].values()), + *ci_fixture_inputs(root)] def retained(relative, row, root): @@ -46,6 +88,7 @@ def retained(relative, row, root): def predecessor(relative, data, root=None): """Project known complete successor bytes; unknown bytes remain unknown.""" root = ROOT if root is None else root + data = ci_fixture_predecessor(relative, data, root) rows = record(root) row = None if rows is None else rows['files'].get(relative) if row is None: diff --git a/tests/test_automatic_tool_recovery_lineage.py b/tests/test_automatic_tool_recovery_lineage.py index fd806f3..a55de0e 100644 --- a/tests/test_automatic_tool_recovery_lineage.py +++ b/tests/test_automatic_tool_recovery_lineage.py @@ -18,6 +18,53 @@ def current_runtime(self): return {path.relative_to(runtime).as_posix(): path.read_bytes() for path in runtime.rglob('*.py')} + def test_ci_fixture_delta_preserves_both_exact_archives_without_executing_them(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + self.assertEqual(set(rows['files']), lineage.CI_FIXTURE_FILES) + self.assertTrue(set(lineage.ci_fixture_inputs(lineage.ROOT)) <= set(lineage.inputs())) + for relative, row in rows['files'].items(): + with self.subTest(relative=relative): + current = (lineage.ROOT / relative).read_bytes() + before = (lineage.ROOT / row['predecessor_source']).read_bytes() + self.assertEqual(hashlib.sha256(current).hexdigest(), row['successor_sha256']) + self.assertEqual(hashlib.sha256(before).hexdigest(), row['predecessor_sha256']) + self.assertNotEqual(current, before) + self.assertEqual(lineage.predecessor(relative, current), before) + self.assertEqual(lineage.predecessor(relative, before), before) + for extra in (b'\n# unreviewed fixture\n', b'\nunreviewed = True\n'): + changed = current + extra + self.assertEqual(lineage.predecessor(relative, changed), changed) + self.assertNotEqual(hashlib.sha256(changed).hexdigest(), row['predecessor_sha256']) + with tempfile.TemporaryDirectory() as directory: + self.assertEqual(lineage.predecessor(relative, current, Path(directory)), current) + + def test_ci_fixture_archive_path_and_hash_tampering_is_refused(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + for relative in lineage.CI_FIXTURE_FILES: + data = (lineage.ROOT / relative).read_bytes() + for field in ('predecessor_source', 'predecessor_sha256'): + changed = deepcopy(rows); changed['files'][relative][field] = '0' * 64 + with self.subTest(relative=relative, field=field), mock.patch.object( + lineage, 'ci_fixture_record', return_value=changed): + with self.assertRaisesRegex(AssertionError, 'CI fixture predecessor'): + lineage.predecessor(relative, data) + changed = deepcopy(rows); changed['files'][relative]['successor_sha256'] = '0' * 64 + with mock.patch.object(lineage, 'ci_fixture_record', return_value=changed): + self.assertEqual(lineage.predecessor(relative, data), data) + + def test_modified_ci_fixture_archive_bytes_cannot_satisfy_historical_readers(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + original_read = Path.read_bytes + for relative, row in rows['files'].items(): + current = (lineage.ROOT / relative).read_bytes() + target = lineage.ROOT / row['predecessor_source'] + def read(path): + raw = original_read(path) + return raw + b'\n# changed retained bytes\n' if path == target else raw + with self.subTest(relative=relative), mock.patch.object(Path, 'read_bytes', read): + with self.assertRaisesRegex(AssertionError, 'CI fixture predecessor bytes'): + lineage.predecessor(relative, current) + def test_exact_pending_tool_predecessors_and_evidence_remain_unchanged(self): rows = lineage.record() self.assertEqual(rows['predecessor_revision'], '9242434991f2c2c21461061d61d898649299b847') diff --git a/tests/test_continuation_host.py b/tests/test_continuation_host.py index a573611..864a424 100644 --- a/tests/test_continuation_host.py +++ b/tests/test_continuation_host.py @@ -303,7 +303,15 @@ async def wire(self,method,**values): return json.loads(writer.data) async def test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch(self): - c=self.c;await self.start_transport() + c=self.c + # As in test_desktop_mailbox_runtime, observation age is a logical + # fixture input, not SQLite/CI scheduling speed. Keep the actual child, + # UI, execution and orchestration clocks unchanged; expiry has its own + # explicit negative tests. Refresh before using this clock's cache. + observation_clock=mock.patch.object(c.runtime,'_clock',return_value=time.monotonic()) + observation_clock.start();self.addCleanup(observation_clock.stop) + await c.runtime.refresh() + await self.start_transport() restoring=asyncio.create_task(self.restore());self.tasks.append(restoring) first=await self.wire('continue');self.assertTrue(first['ok']) task=self.transport._records['continue-1']['task'];self.tasks.append(task) diff --git a/tests/test_desktop_mailbox_runtime.py b/tests/test_desktop_mailbox_runtime.py index 5396f70..3e69229 100644 --- a/tests/test_desktop_mailbox_runtime.py +++ b/tests/test_desktop_mailbox_runtime.py @@ -21,6 +21,12 @@ NOW = '2026-10-24T18:00:00Z' OBJECTIVE = 'Keep the original task and finish the validated artifact.' +# Whole-fixture join guard for actual SQLite/thread work, not a product +# freshness or response-latency requirement. Keep it at module scope because +# other fixtures reuse completed_control without inheriting RuntimeTests. +# https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py +CONTROL_COMPLETION_TIMEOUT_S = 30 + class DataFixture: def data(self): @@ -135,7 +141,8 @@ async def completed_control(self, method, *, request_id): # and its authority read finish. Final-intent assertions need that task. reply = await self.request(method, request_id=request_id) self.assertTrue(reply['ok'], reply) - await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) + await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), + CONTROL_COMPLETION_TIMEOUT_S) return reply def matches(self):