From f2ff93b6bffed9a2132df9852aa4ed7241a2db8a Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:38:27 +0200 Subject: [PATCH 1/3] feat: run durable process trainers through Docker Compose --- .agents/pm/history/pm-rl-7073.jsonl | 3 + .agents/pm/history/pm-rl-7c7m.jsonl | 3 + .agents/pm/history/pm-rl-mjo5.jsonl | 22 +++ .agents/pm/tasks/pm-rl-7073.toon | 17 +++ .agents/pm/tasks/pm-rl-7c7m.toon | 17 +++ .agents/pm/tasks/pm-rl-mjo5.toon | 43 +++++- README.md | 19 ++- RECURSIVE_TRAINING.md | 12 +- compose.ts | 65 +++++++++ docs/process-trainer.md | 128 +++++++++++++++++- index.ts | 6 +- package.json | 6 +- process.ts | 31 +++-- test/compose.test.ts | 200 ++++++++++++++++++++++++++++ test/fixtures/compose-trainer.ts | 15 +++ test/fixtures/compose.Dockerfile | 8 ++ 16 files changed, 562 insertions(+), 33 deletions(-) create mode 100644 .agents/pm/history/pm-rl-7073.jsonl create mode 100644 .agents/pm/history/pm-rl-7c7m.jsonl create mode 100644 .agents/pm/tasks/pm-rl-7073.toon create mode 100644 .agents/pm/tasks/pm-rl-7c7m.toon create mode 100644 compose.ts create mode 100644 test/compose.test.ts create mode 100644 test/fixtures/compose-trainer.ts create mode 100644 test/fixtures/compose.Dockerfile diff --git a/.agents/pm/history/pm-rl-7073.jsonl b/.agents/pm/history/pm-rl-7073.jsonl new file mode 100644 index 00000000..e52eb661 --- /dev/null +++ b/.agents/pm/history/pm-rl-7073.jsonl @@ -0,0 +1,3 @@ +{"hash_algorithm":"sha256","ts":"2026-10-15T11:11:07.179Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-rl-7073"},{"op":"add","path":"/metadata/title","value":"Enforce kernel CPU and storage quotas for Compose trainers"},{"op":"add","path":"/metadata/description","value":"The shared process contract validates trainer-reported CPU milliseconds and polls observed wall/storage use. Compose supplies filesystem and process isolation, but hard CPU-time and disk quotas remain a separate scheduler capability. Add measured kernel-enforced limits without changing the reservation and receipt schema."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-10-15T11:11:07.179Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-15T11:11:07.179Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"A real over-consuming trainer is stopped by kernel-enforced CPU/storage limits; retained receipts settle observed use and explain the enforcement mechanism; supported host requirements are documented."},{"op":"add","path":"/metadata/parent","value":"pm-rl-mjo5"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"9c3ab40b6269d3aa91afa851e05821162a28c77af382649de73a15bfafe63ba2","item_hash_version":3,"message":"","context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8016acae4e5acee741bb17ce23b62c95d730b13539084ff1acde723cbc7bfb76"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:13:23.658Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:13:23.658Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/process-trainer.md","scope":"project"}]}],"before_hash":"9c3ab40b6269d3aa91afa851e05821162a28c77af382649de73a15bfafe63ba2","after_hash":"67a204310ae8f68ef0cfb00e20aa6e9e8b2dbe4cdc169d1affd52e1cdec44d7e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"ce2811c5441a620d2d061096d598e89060298a630ce629e46a04b59cc9fb349b"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:13:27.360Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:13:27.360Z"},{"op":"add","path":"/metadata/files","value":[{"path":"compose.ts","scope":"project"}]}],"before_hash":"67a204310ae8f68ef0cfb00e20aa6e9e8b2dbe4cdc169d1affd52e1cdec44d7e","after_hash":"48c136aeb910689158f902aaf49335f5d33b80f6b84658068da979e7b5a99d9b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"92e0345e7804c19dfaba4445be95af7a70e035d151d824973212826da04f39be"} diff --git a/.agents/pm/history/pm-rl-7c7m.jsonl b/.agents/pm/history/pm-rl-7c7m.jsonl new file mode 100644 index 00000000..9266ca78 --- /dev/null +++ b/.agents/pm/history/pm-rl-7c7m.jsonl @@ -0,0 +1,3 @@ +{"hash_algorithm":"sha256","ts":"2026-10-15T18:17:46.948Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-rl-7c7m"},{"op":"add","path":"/metadata/title","value":"Stabilize packed LM CLI timing under the unchanged acceptance bound"},{"op":"add","path":"/metadata/description","value":"The full release gate observed an existing packed LM CLI timing failure: measured CLI 22138 ms against its unchanged 20000 ms bound. The same unmodified test passed alone at 8955 ms. Investigate repeatable runtime and IO overhead under full coverage without hiding source or weakening the acceptance bound."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":3},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-10-15T18:17:46.948Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-15T18:17:46.948Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Repeated full npm and Bun gates satisfy the existing 20-second packed LM CLI acceptance on declared runner resources; coverage scope and all four thresholds remain unchanged; measurements distinguish CLI time from total test setup time."},{"op":"add","path":"/metadata/parent","value":"pm-rl-mjo5"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"0fc84578a4fbba7c524ccc4b20b410bf09d194ef2beb95d1a6742ff2a13b9155","item_hash_version":3,"message":"","context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"db2dba05a71d16c960aec9e05dc27985e3ca30bf0cebb9b81ea7c1f8793614e8"} +{"hash_algorithm":"sha256","ts":"2026-10-15T18:19:08.367Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T18:19:08.367Z"},{"op":"add","path":"/metadata/files","value":[{"path":"test/lm-cli.test.ts","scope":"project"}]}],"before_hash":"0fc84578a4fbba7c524ccc4b20b410bf09d194ef2beb95d1a6742ff2a13b9155","after_hash":"8be49d9af283b8287daa9fcb3521d0ffc33b55f2edc9676b9924cba414888c8a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"464a9c0bfb24681001a73bd8a0a00507bb752bd2d8c306a2c8c73cc5a4f8ed99"} +{"hash_algorithm":"sha256","ts":"2026-10-15T18:19:13.038Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T18:19:13.038Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/lm-adapter.md","scope":"project"}]}],"before_hash":"8be49d9af283b8287daa9fcb3521d0ffc33b55f2edc9676b9924cba414888c8a","after_hash":"f45f9d1ca6989ee6255adb8eea8a0bc697b3cc5d4cbe4cc48c256cd6dc5ccd73","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6f29f6f065d4aa79e67b3884e083c668c24ed7609030dcc5a9cf4926b60f608a"} diff --git a/.agents/pm/history/pm-rl-mjo5.jsonl b/.agents/pm/history/pm-rl-mjo5.jsonl index 1c6c8f48..fe4a69bf 100644 --- a/.agents/pm/history/pm-rl-mjo5.jsonl +++ b/.agents/pm/history/pm-rl-mjo5.jsonl @@ -1,2 +1,24 @@ {"hash_algorithm":"sha256","ts":"2026-10-14T19:46:39.492Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"778099d6cee592060ec931f6","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-rl-mjo5"},{"op":"add","path":"/metadata/title","value":"Add Docker Compose execution for the durable process trainer contract"},{"op":"add","path":"/metadata/description","value":"Follow the local process adapter with a Compose launcher using the same job identity, staged receipt validation, atomic approved resource reservations and cancellation evidence. Requires isolated container lifecycle and real crash drills; no hosted data inputs."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-10-14T19:46:39.492Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-14T19:46:39.492Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Compose execution preserves launch ownership and process contract, retains staging on failure, and passes real reservation and recovery drills"},{"op":"add","path":"/metadata/parent","value":"pm-rl-apvf"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"cc98bdbfc116b590c6fcdb7176760bd92e9e22c6b7a560b34ce303339013993e","item_hash_version":3,"message":"","context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"6c8b0f39eddc155209273ced72855a5b2a04a1d2feeb3249a05d9dada4f32282"} {"hash_algorithm":"sha256","ts":"2026-10-14T20:28:25.477Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"778099d6cee592060ec931f6","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-14T20:28:25.477Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/process-trainer.md","scope":"project"}]}],"before_hash":"cc98bdbfc116b590c6fcdb7176760bd92e9e22c6b7a560b34ce303339013993e","after_hash":"b20a69da82d01e26280faa2f2b11c72f3bdfd365b0ff3e0717e9223e88c7a313","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"b971b6bce2fe092e702890f7b479ef4a25ad1d2ad97055dd29d0812f275c22c9"} +{"hash_algorithm":"sha256","ts":"2026-10-15T10:58:34.871Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T10:58:34.871Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"b20a69da82d01e26280faa2f2b11c72f3bdfd365b0ff3e0717e9223e88c7a313","after_hash":"3b5f9b6fac882f18fb249b2ffd1a91ecb7125a5b55d4401f5fab04c717bacf8a","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"d9b8c101dee126529c5e1831f27416f23e7a91ad2ed601e18ea59ce4e5187591"} +{"hash_algorithm":"sha256","ts":"2026-10-15T10:58:36.139Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T10:58:36.139Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"3b5f9b6fac882f18fb249b2ffd1a91ecb7125a5b55d4401f5fab04c717bacf8a","after_hash":"2265a7178434dec7bdf9697901db8fbea8c12cadafbe5adc588a9cc8f55b8dc6","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"4662e6ca6c5f423a070dbf6bd6099b81f39ef8a7aeee88e71d9f64b3a75a6e0f"} +{"hash_algorithm":"sha256","ts":"2026-10-15T10:58:38.098Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T10:58:38.098Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-15T10:58:38.098Z","author":"codex-agent","text":"Design: reuse the process job fence, reservation transaction, receipt validator and settlement state machine. Add a trusted Compose launcher with a job-derived project identity and generated isolated staging configuration; mount only job staging and retain all protocol artifacts. Exercise real success, container death, cancellation, contention and two-generation successor collection. Behavioural revert proofs will remove only the relevant launcher or selection behaviour in an isolated snapshot."}]}],"before_hash":"2265a7178434dec7bdf9697901db8fbea8c12cadafbe5adc588a9cc8f55b8dc6","after_hash":"d6bfc83a9689f0435e16c90ba6552eb69bc8998ad5940cd749cc4c4bc0b14978","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f9e16f24b5cae6f3338bbd0aafc877fcdbd01ea160029d8da0595bcefb4de0f1"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:14.363Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-10-15T11:08:14.363Z","author":"codex-agent","text":"Real Docker findings: a capability-restricted container must use the invoking numeric UID/GID to write host staging. Compose down/rm omit one-off run containers on the tested version, so cleanup resolves only the exact job-named container with matching project/service labels and removes its immutable daemon ID before project-scoped down. Project names also hash staging scope to isolate identical jobs in independent trackers. Receipt validation, conservative settled charges and controller fences remain shared with local execution."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:14.363Z"}],"before_hash":"d6bfc83a9689f0435e16c90ba6552eb69bc8998ad5940cd749cc4c4bc0b14978","after_hash":"4ed49daa171c137ad9f870a772e971e05cbad70aaa0112d0f2ca26278fdadfa7","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"fefbe2ba709466975d70a75b9ee678f5851d2d920fb1be70f03159760c77e413"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:16.619Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:16.619Z"},{"op":"add","path":"/metadata/files","value":[{"path":"test/fixtures/compose-trainer.ts","scope":"project"}]}],"before_hash":"4ed49daa171c137ad9f870a772e971e05cbad70aaa0112d0f2ca26278fdadfa7","after_hash":"b24cf0d6fa56916d373c2221c2c74f13c3b17d73f2bc4053c67e7ebe92d380c2","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"9c3d8db831287b2c7a31fe8133d4298736ca6213342382be623db458b6860707"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:18.817Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/1","value":{"path":"RECURSIVE_TRAINING.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:18.817Z"}],"before_hash":"b24cf0d6fa56916d373c2221c2c74f13c3b17d73f2bc4053c67e7ebe92d380c2","after_hash":"b178d1b05e9daed6237fbb090f8cf7e0949038cac148bd075a18f99d94bac5fa","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"8710c8c5e2b0a458b117970ee18aa4926262472f68693ad0164217196f6e4685"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:21.442Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:21.442Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run typecheck","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-15T11:08:21.306Z","source_kind":"local_mutation","source_ref":"feat/compose-process-trainer"}}]}],"before_hash":"b178d1b05e9daed6237fbb090f8cf7e0949038cac148bd075a18f99d94bac5fa","after_hash":"62110ec597835dbbe3861d7d68253c48314896150c9f4f86b836a8a9163d0c34","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"2ac5bbd984f799dc02bc1ca385992c88b7ca4a9c236c4efb0ecccf40b9ea90e6"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:49.584Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/0/path","value":"compose.ts"},{"op":"add","path":"/metadata/files/1","value":{"path":"test/fixtures/compose-trainer.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:49.584Z"}],"before_hash":"62110ec597835dbbe3861d7d68253c48314896150c9f4f86b836a8a9163d0c34","after_hash":"ed3f12d40fd4c3eeb7dc7c704a01269dc15e694e60469b9a8029c7499daf4fa9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"007d95e8af7f4a26246ab180fbf2d3dadeca61a97136196a9ccc9814f0b8d6af"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:51.549Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/1/path","value":"process.ts"},{"op":"add","path":"/metadata/files/2","value":{"path":"test/fixtures/compose-trainer.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:51.549Z"}],"before_hash":"ed3f12d40fd4c3eeb7dc7c704a01269dc15e694e60469b9a8029c7499daf4fa9","after_hash":"c389428469b2e6d9d0c3a27ab5fd2a4ae4c31f29a11cc8cc57bb941c7fe8ed7c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"529ce3d2dd1ae045cfd5d2a060225881b9d998f0474c773d2b59218b417c2c96"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:54.049Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/2/path","value":"process.ts"},{"op":"replace","path":"/metadata/files/1/path","value":"index.ts"},{"op":"add","path":"/metadata/files/3","value":{"path":"test/fixtures/compose-trainer.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:54.049Z"}],"before_hash":"c389428469b2e6d9d0c3a27ab5fd2a4ae4c31f29a11cc8cc57bb941c7fe8ed7c","after_hash":"288ac9e81d49fc46d769a0b113b3b83c0defaa1aa94cdd18eba6741f268ba90d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"26e6e6e47d7dd611dc506e4359730bf501d8db85b213405dfe796190307a0b7b"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:56.434Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/3/path","value":"process.ts"},{"op":"replace","path":"/metadata/files/2/path","value":"package.json"},{"op":"add","path":"/metadata/files/4","value":{"path":"test/fixtures/compose-trainer.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:56.434Z"}],"before_hash":"288ac9e81d49fc46d769a0b113b3b83c0defaa1aa94cdd18eba6741f268ba90d","after_hash":"4ee7c05f34f4c6bfd2629868bf52be037e9c29fd96b0e9bbea2ce95a2de87334","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"bcd1ca8f4ffba1166e42dfcbda043714a0a652e715bb5a5970bdae77d9d7f728"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:08:58.643Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/4/path","value":"test/compose.test.ts"},{"op":"add","path":"/metadata/files/5","value":{"path":"test/fixtures/compose-trainer.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:08:58.643Z"}],"before_hash":"4ee7c05f34f4c6bfd2629868bf52be037e9c29fd96b0e9bbea2ce95a2de87334","after_hash":"b56dfda1eead8b3d25782e816fd0b2fbcc363b75d6a4925790f8fadd3c9b786d","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6025ff9415c0c95e14f9c05cbfeb64d1b4ed3f4c351b104ae8acfd4f9e565241"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:09:01.685Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/6","value":{"path":"test/fixtures/compose.Dockerfile","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:09:01.685Z"}],"before_hash":"b56dfda1eead8b3d25782e816fd0b2fbcc363b75d6a4925790f8fadd3c9b786d","after_hash":"5bfa584f8edadb669df739f1e78450712915646b3bab971b0f7b639c11f7946e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"43b91327c918660a3cbe5a702c13dfb225e6d9f29d98517de025a78d8b15da24"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:09:05.729Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"docs_add","patch":[{"op":"replace","path":"/metadata/docs/1/path","value":"README.md"},{"op":"add","path":"/metadata/docs/2","value":{"path":"RECURSIVE_TRAINING.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:09:05.729Z"}],"before_hash":"5bfa584f8edadb669df739f1e78450712915646b3bab971b0f7b639c11f7946e","after_hash":"8efb520e1ac13289cb6ba54dd22987423df2e1b6cca60e8bf3111d5a6c8e6375","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"0a431e5f272fa1a40e74ec8af3354d5f2630cf40851bc702ac78c827b273f362"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:11:10.946Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-10-15T11:11:10.946Z","author":"codex-agent","text":"Drill progress: launch/receipt/replay, container death, cancellation, two real controller contention, staged-completion SIGKILL recovery and orphan cleanup pass. The two-generation Compose run promoted both changed policies, and generation two collection identifies generation one policy. Kernel CPU/storage quotas are a distinct follow-up to the existing reported-use and polling contract. Full release gates run directly under isolated CI Git/home settings; pm test --run --progress passed the linked typecheck, but project policy reports tracking_disabled so durable gate results are recorded in comments."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:11:10.946Z"}],"before_hash":"8efb520e1ac13289cb6ba54dd22987423df2e1b6cca60e8bf3111d5a6c8e6375","after_hash":"7cc5896f62545f866e9b2636b9a98c53458463509d6d900c7c45512cca965261","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c668df19f05e010eb447a90718a54118cd2a55351626bd03e7c1af0de21b8a15"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:11:35.065Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"node --test test/compose.test.ts","scope":"project","timeout_seconds":300,"pm_context_mode":"none","workspace_context_mode":"snapshot","provenance":{"author":"codex-agent","created_at":"2026-10-15T11:11:34.932Z","source_kind":"local_mutation","source_ref":"feat/compose-process-trainer"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:11:35.065Z"}],"before_hash":"7cc5896f62545f866e9b2636b9a98c53458463509d6d900c7c45512cca965261","after_hash":"f24cafb7bf00d1903481354fb479ba0432daf68c367860b2351fa9c2eabff815","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6fb85dd2bfefffabdcd50617f2f161f4a4f118ff499a550968ad9d2769b7cf88"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:15:23.815Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-10-15T11:15:23.815Z","author":"codex-agent","text":"The final launcher retains the one-off container until controller cleanup rather than using automatic removal. This removes the race between daemon auto-removal and recovery resolving the exact container ID. All trainer outputs remain host-mounted. The linked Compose suite verified successfully with pm_context_mode=none and workspace_context_mode=snapshot; linked typecheck also passed pm test --run --progress. No dependency pins or coverage thresholds changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:15:23.815Z"}],"before_hash":"f24cafb7bf00d1903481354fb479ba0432daf68c367860b2351fa9c2eabff815","after_hash":"b5595ae56689f965a6ddcba49d8aa4d45a22e28cb3a699c72c524cbe8c10d3f5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"b81c1fff0409e7206d4115d560ab23334410a65daa04d64cc09cfdbf371d0cdc"} +{"hash_algorithm":"sha256","ts":"2026-10-15T11:19:39.886Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"xhigh","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-10-15T11:19:39.886Z","author":"codex-agent","text":"Per-test behavioural revert proofs used isolated source snapshots with unchanged tests importing TypeScript directly. Selection/configuration: restoring only parseProcessAdapter from origin/main fails 0/1 because Compose selection is rejected. Restoring only executeReservedProcessJob from origin/main fails each of these eight cases (0/8): launch/receipt/replay lacks container launch evidence; container death misses the required failure; cancellation misses the required refusal; staged-completion SIGKILL recovery lacks container launch evidence; orphan recovery lacks container launch evidence; two generations lack generated Compose configuration; independent trackers miss the required cancellation refusals; checkpoint-without-completion wrongly succeeds. The separate two-real-controller contention case also fails 0/1 with that function restored, lacking container launch evidence. Removing only the pre-launch missing-config guard in stopComposeTrainer fails recovery-before-config-publication 0/1 with a filesystem error instead of the durable receipt-required refusal. Every added case therefore fails under its relevant behavioural revert. Restoring the final functions passes all 11/11, zero failures/skips. Commands: node --test --test-reporter=tap --test-name-pattern= test/compose.test.ts; restored run: node --test --test-reporter=tap test/compose.test.ts."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T11:19:39.886Z"}],"before_hash":"b5595ae56689f965a6ddcba49d8aa4d45a22e28cb3a699c72c524cbe8c10d3f5","after_hash":"9d5cd229d4aa8bcaec06b220f3d2e89620b46103392403a793e3823eac92f539","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"a03a50f7acf0d1ae10d339a39fb37de994c8b1a0f9cbcc81523d4b7013890a8a"} +{"hash_algorithm":"sha256","ts":"2026-10-15T17:57:01.089Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-10-15T17:57:01.089Z","author":"codex-agent","text":"Final-source npm attempt completed 578 tests: 577 passed, one existing packed LM CLI timing assertion failed (measured CLI 22138 ms against the unchanged 20000 ms bound), zero skipped/cancelled/todo. All 28 source files reported 100 percent line/branch/function coverage; the statement gate correctly did not claim success after a test failure. All 11 Compose cases passed, and the explicit pm test --run --progress --only-last snapshot run passed 11/11 in 111.389 seconds. No thresholds or deadline assertions were changed. Retrying the existing timing case and then both complete gates serially."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T17:57:01.089Z"}],"before_hash":"9d5cd229d4aa8bcaec06b220f3d2e89620b46103392403a793e3823eac92f539","after_hash":"f09599cb6318cbf17114350e6d79fbbbf4bb951e5c07762ee3bb279fe43206d5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"34aa98df63e94a948304df52e701ece734dce9acee0da4ae3394001379d154f7"} +{"hash_algorithm":"sha256","ts":"2026-10-15T18:19:18.033Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-15T18:19:18.033Z","author":"codex-agent","text":"Follow-up gap pm-rl-7c7m records the existing packed LM CLI timing variability observed by the full gate. The standalone unchanged case passed at 8955 ms. This task retains the 20000 ms bound and all original coverage thresholds; kernel CPU/storage enforcement remains separately tracked by pm-rl-7073."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T18:19:18.033Z"}],"before_hash":"f09599cb6318cbf17114350e6d79fbbbf4bb951e5c07762ee3bb279fe43206d5","after_hash":"a7f7e9f569f74fe6c5f7fb90d58c809bdca1d1fe9bb031292a7a97451726440e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ef7121237b2f875ba34d308d72b3457451e7830a235f178e92ad955712157c19"} +{"hash_algorithm":"sha256","ts":"2026-10-15T19:38:00.185Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-15T19:38:00.185Z","author":"codex-agent","text":"Final unchanged-source gates PASS: npm run release:check 578/578 and bun run release:check 578/578, each zero fail/skip/cancel/todo. Both measure exact 100/100/100/100 statement/line/branch/function coverage across 28 authored sources, 266 documented declarations, zero duplication, zero production audit findings; identity, packaging and changelog checks pass under isolated CI Git/home settings. Source hashes match the final implementation. All 11 new real Compose tests pass in both full gates and through explicit pm test --run --progress using none/snapshot context. Per-test behavioural reverts and the restored 11/11 result are recorded above. Two generations retain changed checkpoint byte digests a81ee5538f8451969dd7d22df07ea5299dff92e4267200d760c5298a418cf66f and 04205767f4fefcb97c97eedc6fc7739da446f51ab7cc050f7f3f20e5f9527452; the promoted first policy collects generation two. Earlier LM timing miss is tracked by pm-rl-7c7m; passing full gates measure 9038 ms and 12907 ms within the unchanged 20000 ms limit. Kernel CPU/storage quotas remain pm-rl-7073. No new PM CLI defect was filed: result tracking is disabled by policy, so comments retain durable gate evidence; supported snapshot context avoids the known tracker-spawning sandbox collision. Items remain open for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T19:38:00.185Z"}],"before_hash":"a7f7e9f569f74fe6c5f7fb90d58c809bdca1d1fe9bb031292a7a97451726440e","after_hash":"e9cb6ae5726b302ce6e8a16ce4623cdeb63b5f630ec50fbd2ef07bb62532977f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"aafd19d9269f6ddcef5a75c521385b5e0831bd247669c2b3efc1a1c02937e4b6"} +{"hash_algorithm":"sha256","ts":"2026-10-15T19:38:04.500Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T19:38:04.500Z"}],"before_hash":"e9cb6ae5726b302ce6e8a16ce4623cdeb63b5f630ec50fbd2ef07bb62532977f","after_hash":"ddb126bc46f45e455fc43ffe8dd61d0756681a44deb9759e7759b90806a31120","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"aa8f5f1f64e7430be2062d9732e08b8de484772ff4e369dbd9f05ae3ff68c753"} diff --git a/.agents/pm/tasks/pm-rl-7073.toon b/.agents/pm/tasks/pm-rl-7073.toon new file mode 100644 index 00000000..c044d03f --- /dev/null +++ b/.agents/pm/tasks/pm-rl-7073.toon @@ -0,0 +1,17 @@ +id: pm-rl-7073 +title: Enforce kernel CPU and storage quotas for Compose trainers +description: "The shared process contract validates trainer-reported CPU milliseconds and polls observed wall/storage use. Compose supplies filesystem and process isolation, but hard CPU-time and disk quotas remain a separate scheduler capability. Add measured kernel-enforced limits without changing the reservation and receipt schema." +type: Task +status: open +priority: 3 +tags: [] +created_at: "2026-10-15T11:11:07.179Z" +updated_at: "2026-10-15T11:13:27.360Z" +author: codex-agent +acceptance_criteria: A real over-consuming trainer is stopped by kernel-enforced CPU/storage limits; retained receipts settle observed use and explain the enforcement mechanism; supported host requirements are documented. +parent: pm-rl-mjo5 +files[1]{path,scope}: + compose.ts,project +docs[1]{path,scope}: + docs/process-trainer.md,project +body: "" diff --git a/.agents/pm/tasks/pm-rl-7c7m.toon b/.agents/pm/tasks/pm-rl-7c7m.toon new file mode 100644 index 00000000..90e3251e --- /dev/null +++ b/.agents/pm/tasks/pm-rl-7c7m.toon @@ -0,0 +1,17 @@ +id: pm-rl-7c7m +title: Stabilize packed LM CLI timing under the unchanged acceptance bound +description: "The full release gate observed an existing packed LM CLI timing failure: measured CLI 22138 ms against its unchanged 20000 ms bound. The same unmodified test passed alone at 8955 ms. Investigate repeatable runtime and IO overhead under full coverage without hiding source or weakening the acceptance bound." +type: Task +status: open +priority: 3 +tags: [] +created_at: "2026-10-15T18:17:46.948Z" +updated_at: "2026-10-15T18:19:13.038Z" +author: codex-agent +acceptance_criteria: Repeated full npm and Bun gates satisfy the existing 20-second packed LM CLI acceptance on declared runner resources; coverage scope and all four thresholds remain unchanged; measurements distinguish CLI time from total test setup time. +parent: pm-rl-mjo5 +files[1]{path,scope}: + test/lm-cli.test.ts,project +docs[1]{path,scope}: + docs/lm-adapter.md,project +body: "" diff --git a/.agents/pm/tasks/pm-rl-mjo5.toon b/.agents/pm/tasks/pm-rl-mjo5.toon index 16507154..b2979584 100644 --- a/.agents/pm/tasks/pm-rl-mjo5.toon +++ b/.agents/pm/tasks/pm-rl-mjo5.toon @@ -2,14 +2,51 @@ id: pm-rl-mjo5 title: Add Docker Compose execution for the durable process trainer contract description: "Follow the local process adapter with a Compose launcher using the same job identity, staged receipt validation, atomic approved resource reservations and cancellation evidence. Requires isolated container lifecycle and real crash drills; no hosted data inputs." type: Task -status: open +status: in_progress priority: 2 tags: [] created_at: "2026-10-14T19:46:39.492Z" -updated_at: "2026-10-14T20:28:25.477Z" +updated_at: "2026-10-15T19:38:04.500Z" author: codex-agent acceptance_criteria: "Compose execution preserves launch ownership and process contract, retains staging on failure, and passes real reservation and recovery drills" parent: pm-rl-apvf -docs[1]{path,scope}: +comments[8]{created_at,author,text}: + "2026-10-15T10:58:38.098Z",codex-agent,"Design: reuse the process job fence, reservation transaction, receipt validator and settlement state machine. Add a trusted Compose launcher with a job-derived project identity and generated isolated staging configuration; mount only job staging and retain all protocol artifacts. Exercise real success, container death, cancellation, contention and two-generation successor collection. Behavioural revert proofs will remove only the relevant launcher or selection behaviour in an isolated snapshot." + "2026-10-15T11:08:14.363Z",codex-agent,"Real Docker findings: a capability-restricted container must use the invoking numeric UID/GID to write host staging. Compose down/rm omit one-off run containers on the tested version, so cleanup resolves only the exact job-named container with matching project/service labels and removes its immutable daemon ID before project-scoped down. Project names also hash staging scope to isolate identical jobs in independent trackers. Receipt validation, conservative settled charges and controller fences remain shared with local execution." + "2026-10-15T11:11:10.946Z",codex-agent,"Drill progress: launch/receipt/replay, container death, cancellation, two real controller contention, staged-completion SIGKILL recovery and orphan cleanup pass. The two-generation Compose run promoted both changed policies, and generation two collection identifies generation one policy. Kernel CPU/storage quotas are a distinct follow-up to the existing reported-use and polling contract. Full release gates run directly under isolated CI Git/home settings; pm test --run --progress passed the linked typecheck, but project policy reports tracking_disabled so durable gate results are recorded in comments." + "2026-10-15T11:15:23.815Z",codex-agent,The final launcher retains the one-off container until controller cleanup rather than using automatic removal. This removes the race between daemon auto-removal and recovery resolving the exact container ID. All trainer outputs remain host-mounted. The linked Compose suite verified successfully with pm_context_mode=none and workspace_context_mode=snapshot; linked typecheck also passed pm test --run --progress. No dependency pins or coverage thresholds changed. + "2026-10-15T11:19:39.886Z",codex-agent,"Per-test behavioural revert proofs used isolated source snapshots with unchanged tests importing TypeScript directly. Selection/configuration: restoring only parseProcessAdapter from origin/main fails 0/1 because Compose selection is rejected. Restoring only executeReservedProcessJob from origin/main fails each of these eight cases (0/8): launch/receipt/replay lacks container launch evidence; container death misses the required failure; cancellation misses the required refusal; staged-completion SIGKILL recovery lacks container launch evidence; orphan recovery lacks container launch evidence; two generations lack generated Compose configuration; independent trackers miss the required cancellation refusals; checkpoint-without-completion wrongly succeeds. The separate two-real-controller contention case also fails 0/1 with that function restored, lacking container launch evidence. Removing only the pre-launch missing-config guard in stopComposeTrainer fails recovery-before-config-publication 0/1 with a filesystem error instead of the durable receipt-required refusal. Every added case therefore fails under its relevant behavioural revert. Restoring the final functions passes all 11/11, zero failures/skips. Commands: node --test --test-reporter=tap --test-name-pattern= test/compose.test.ts; restored run: node --test --test-reporter=tap test/compose.test.ts." + "2026-10-15T17:57:01.089Z",codex-agent,"Final-source npm attempt completed 578 tests: 577 passed, one existing packed LM CLI timing assertion failed (measured CLI 22138 ms against the unchanged 20000 ms bound), zero skipped/cancelled/todo. All 28 source files reported 100 percent line/branch/function coverage; the statement gate correctly did not claim success after a test failure. All 11 Compose cases passed, and the explicit pm test --run --progress --only-last snapshot run passed 11/11 in 111.389 seconds. No thresholds or deadline assertions were changed. Retrying the existing timing case and then both complete gates serially." + "2026-10-15T18:19:18.033Z",codex-agent,Follow-up gap pm-rl-7c7m records the existing packed LM CLI timing variability observed by the full gate. The standalone unchanged case passed at 8955 ms. This task retains the 20000 ms bound and all original coverage thresholds; kernel CPU/storage enforcement remains separately tracked by pm-rl-7073. + "2026-10-15T19:38:00.185Z",codex-agent,"Final unchanged-source gates PASS: npm run release:check 578/578 and bun run release:check 578/578, each zero fail/skip/cancel/todo. Both measure exact 100/100/100/100 statement/line/branch/function coverage across 28 authored sources, 266 documented declarations, zero duplication, zero production audit findings; identity, packaging and changelog checks pass under isolated CI Git/home settings. Source hashes match the final implementation. All 11 new real Compose tests pass in both full gates and through explicit pm test --run --progress using none/snapshot context. Per-test behavioural reverts and the restored 11/11 result are recorded above. Two generations retain changed checkpoint byte digests a81ee5538f8451969dd7d22df07ea5299dff92e4267200d760c5298a418cf66f and 04205767f4fefcb97c97eedc6fc7739da446f51ab7cc050f7f3f20e5f9527452; the promoted first policy collects generation two. Earlier LM timing miss is tracked by pm-rl-7c7m; passing full gates measure 9038 ms and 12907 ms within the unchanged 20000 ms limit. Kernel CPU/storage quotas remain pm-rl-7073. No new PM CLI defect was filed: result tracking is disabled by policy, so comments retain durable gate evidence; supported snapshot context avoids the known tracker-spawning sandbox collision. Items remain open for orchestrator verification." +files[7]{path,scope}: + compose.ts,project + index.ts,project + package.json,project + process.ts,project + test/compose.test.ts,project + test/fixtures/compose-trainer.ts,project + test/fixtures/compose.Dockerfile,project +tests[2]: + - command: npm run typecheck + scope: project + provenance: + author: codex-agent + created_at: "2026-10-15T11:08:21.306Z" + source_kind: local_mutation + source_ref: feat/compose-process-trainer + - command: node --test test/compose.test.ts + scope: project + timeout_seconds: 300 + pm_context_mode: none + workspace_context_mode: snapshot + provenance: + author: codex-agent + created_at: "2026-10-15T11:11:34.932Z" + source_kind: local_mutation + source_ref: feat/compose-process-trainer +docs[3]{path,scope}: docs/process-trainer.md,project + README.md,project + RECURSIVE_TRAINING.md,project body: "" diff --git a/README.md b/README.md index b705edd2..b0dec89e 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ query the host can already answer, not a feature to build. | `pm rl loop resume ` | Load the seed's persisted programme; requires the governing `--approval` Decision. `--force-takeover` audits explicit recovery of an ambiguous or stale controller lease. | All commands listed above are implemented. [Recursive training execution](RECURSIVE_TRAINING.md) -documents the local process adapter and the remaining Compose/scheduler work. +documents the local process and Docker Compose adapters and remaining scheduler work. ## Implemented refusals @@ -340,11 +340,20 @@ than folded into a rate. `pm rl loop run` trains the bounded contextual-bandit policy, persists real checkpoint updates and uses each promoted successor for the next collection batch. Its resource -bounds and provenance refusals apply to every generation. Durable continuation and calibration-head updates are implemented. The local process bandit adapter and atomic resource reservations are implemented. GPU scheduling and Compose trainers remain under [`pm-rl-apvf`](.agents/pm/epics/pm-rl-apvf.toon), extending the programme specified +bounds and provenance refusals apply to every generation. Durable continuation and calibration-head updates are implemented. The local process bandit adapter and atomic resource reservations are implemented. GPU scheduling remains under [`pm-rl-apvf`](.agents/pm/epics/pm-rl-apvf.toon), extending the programme specified under [`pm-rl-yi7j`](.agents/pm/epics/pm-rl-yi7j.toon). ## Durable external process training +| Programme selection | Execution | Training input | +| --- | --- | --- | +| `trainer: "bandit"` | In-process numerical policy | Synthetic on-policy samples | +| `trainer: "systemone"` | Calibration head over a frozen decision model | Explicitly selected labelled items | +| `trainer: "lm"` | Bounded CPU LoRA update | Programme-declared sequences | +| Bandit with `adapter.kind: "process-bandit"` | Packaged local child process | Collected training batch | +| Bandit with `adapter.kind: "compose-bandit"` and immutable `adapter.image` | Isolated Docker Compose trainer | Same batch and receipt contract; only job staging is mounted | + + Select `adapter: {"kind":"process-bandit","allowance":{"training_steps":1,"wall_ms":5000,"storage_bytes":65536,"compute_units":1000,"tokens":0}}` in a bandit programme to run the real packaged TypeScript trainer as a child process. The approved Decision must declare both `permitted_promotions` and an @@ -360,14 +369,14 @@ pm rl loop resume process-demo --approval process-approval Default output is compact; `--json` includes complete jobs, receipts and charges. [The process contract and measured two-generation proof](docs/process-trainer.md) -include the approval example, NDJSON protocol, crash recovery and cancellation +include the approval example, NDJSON protocol, Compose configuration, crash recovery and cancellation semantics. Checkpoints and failed-job evidence live in tracker-owned staging. ## Not in scope - **Current execution boundary.** `run log` accepts NDJSON from an external trainer. Native bounded bandit and System One calibration-head training are implemented as `pm rl loop run`. Local process training is implemented with atomic reservations and validated staged receipts. - Compose and GPU scheduling remain in [Recursive training execution](RECURSIVE_TRAINING.md). + GPU scheduling remains in [Recursive training execution](RECURSIVE_TRAINING.md). - **No separate metric store.** The history stream *is* the store. Retained evidence necessarily grows with retained measurements. Each segment is capped at 48 KiB decoded and 65 KiB serialized. In the representative sustained integration workload—not as a universal @@ -448,4 +457,4 @@ It stops at the first rejected candidate and retains the last accepted checkpoin It does not mutate a tracker, grant a budget, execute an LLM, or launch a hosted job. Evaluation is an adaptive validation set; a separate final benchmark is needed after repeated selection. See [the execution contract](RECURSIVE_TRAINING.md) -for the local process adapter and remaining Compose work; [the decision adapter](docs/durable-systemone-validation.md) documents the implemented frozen-model calibration trainer. +for the local process and Compose adapters; [the decision adapter](docs/durable-systemone-validation.md) documents the implemented frozen-model calibration trainer. diff --git a/RECURSIVE_TRAINING.md b/RECURSIVE_TRAINING.md index 928bc335..b76bea03 100644 --- a/RECURSIVE_TRAINING.md +++ b/RECURSIVE_TRAINING.md @@ -1,6 +1,6 @@ # Recursive training execution -Status: the persisted bounded loop is implemented as `pm rl loop run` ([`pm-rl-hjg1`](.agents/pm/features/pm-rl-hjg1.toon)): one command executes collect → train → evaluate → compare against the promoted baseline → promote or reject over the built-in deterministic contextual bandit, records every generation as real tracker items (a collection Run with per-sample metric notes, a Generation item with the derived configuration and full provenance, refusal reasons as item history), promotes only through the existing transactional contamination- and budget-checked gate, derives the next generation's learning rate and evaluation episode count deterministically from the previous evaluation results, and terminates on its own hard bounds — generation limit, total sample budget, unchanged checkpoint, training-to-evaluation gap above `maximum_gap`, statistical gate refusal, or an exhausted approved promotion budget. Durable continuation is implemented in [pm-rl-od32](.agents/pm/features/pm-rl-od32.toon): SDK controller leases, shared worktree launch authority, persisted replay, status/resume commands and signal recovery. [pm-rl-qhz2](.agents/pm/features/pm-rl-qhz2.toon) adds real per-question temperature/bias gradient updates over a frozen System One model; [pm-rl-ip21](.agents/pm/features/pm-rl-ip21.toon) records CPU-only live tev1 generations with retained canonical checkpoints, per-query PM receipts and a 15-minute whole-run deadline. See [the measured adapter contract](docs/durable-systemone-validation.md). [pm-rl-upe0](.agents/pm/features/pm-rl-upe0.toon) adds a real bounded TypeScript causal LM with frozen copy-pretrained weights, REINFORCE LoRA updates, canonical disk checkpoints and verified successor collection. [LM validation](docs/lm-adapter.md) records three real CLI generations and held-out improvement; criterion 2 is met for this CPU adapter. Opted-in remote endpoint receipts recover the HTTP-to-PM commit window in [pm-rl-9nlg](.agents/pm/issues/pm-rl-9nlg.toon). [pm-rl-shgm](.agents/pm/features/pm-rl-shgm.toon) implements the durable local process bandit adapter, staged crash-safe completion receipts and atomic approved resource reservations. [Process validation](docs/process-trainer.md) records two external generations. Docker Compose remains [pm-rl-mjo5](.agents/pm/tasks/pm-rl-mjo5.toon). +Status: the persisted bounded loop is implemented as `pm rl loop run` ([`pm-rl-hjg1`](.agents/pm/features/pm-rl-hjg1.toon)): one command executes collect → train → evaluate → compare against the promoted baseline → promote or reject over the built-in deterministic contextual bandit, records every generation as real tracker items (a collection Run with per-sample metric notes, a Generation item with the derived configuration and full provenance, refusal reasons as item history), promotes only through the existing transactional contamination- and budget-checked gate, derives the next generation's learning rate and evaluation episode count deterministically from the previous evaluation results, and terminates on its own hard bounds — generation limit, total sample budget, unchanged checkpoint, training-to-evaluation gap above `maximum_gap`, statistical gate refusal, or an exhausted approved promotion budget. Durable continuation is implemented in [pm-rl-od32](.agents/pm/features/pm-rl-od32.toon): SDK controller leases, shared worktree launch authority, persisted replay, status/resume commands and signal recovery. [pm-rl-qhz2](.agents/pm/features/pm-rl-qhz2.toon) adds real per-question temperature/bias gradient updates over a frozen System One model; [pm-rl-ip21](.agents/pm/features/pm-rl-ip21.toon) records CPU-only live tev1 generations with retained canonical checkpoints, per-query PM receipts and a 15-minute whole-run deadline. See [the measured adapter contract](docs/durable-systemone-validation.md). [pm-rl-upe0](.agents/pm/features/pm-rl-upe0.toon) adds a real bounded TypeScript causal LM with frozen copy-pretrained weights, REINFORCE LoRA updates, canonical disk checkpoints and verified successor collection. [LM validation](docs/lm-adapter.md) records three real CLI generations and held-out improvement; criterion 2 is met for this CPU adapter. Opted-in remote endpoint receipts recover the HTTP-to-PM commit window in [pm-rl-9nlg](.agents/pm/issues/pm-rl-9nlg.toon). [pm-rl-shgm](.agents/pm/features/pm-rl-shgm.toon) implements the durable local process bandit adapter, staged crash-safe completion receipts and atomic approved resource reservations. [Process validation](docs/process-trainer.md) records two external generations. [pm-rl-mjo5](.agents/pm/tasks/pm-rl-mjo5.toon) adds Docker Compose execution of the same process contract, with real container death, cancellation, contention and two-generation successor drills. ## Durable local continuation @@ -41,7 +41,7 @@ collection. These local receipts do not provide remote endpoint idempotency. A persisted programme names its initial policy checkpoint, training and evaluation datasets, environment and reward revisions, trainer adapter, seed, and an approved resource-budget Decision. Every artifact has a content digest. The evaluator's held-out dataset remains inaccessible to collection and training. Each promoted successor supplies the policy for the next collection cycle, so a successful demonstration must change a real checkpoint and use it in the next generation. -The local process adapter now runs the real TypeScript bandit trainer out of process. A Docker Compose adapter remains a follow-up using the same execution contract. Trainer-specific numerical kernels may be external dependencies, but process execution, provenance, validation and state transitions belong to pm-rl. A local process adapter is the first durable target; a remote scheduler is a later adapter with the same contract. +The local process adapter now runs the real TypeScript bandit trainer out of process. The Docker Compose adapter uses the same execution contract, reservations and staged receipt validator. Trainer-specific numerical kernels may be external dependencies, but process execution, provenance, validation and state transitions belong to pm-rl. A local process adapter is the first durable target; a remote scheduler is a later adapter with the same contract. The implemented loop bounds generations, collection samples and approved promotions. The process adapter additionally bounds training steps, elapsed time, staging storage and adapter-reported compute or token use. It cannot create a larger budget Decision itself. Before each external training phase, the controller atomically reserves the required allowance with the SDK workspace transaction coordinator; replay reuses the same reservation and terminal outcomes settle it. Cancellation, exhausted resources, invalid evidence and failed evaluation must stop progression. A higher proxy reward alone never authorizes promotion. @@ -57,11 +57,11 @@ Adapters report structured progress and completion evidence over a documented st `pm rl loop run` is already registered and tested for bounded in-process bandit training. The following criteria apply to the remaining durable process adapters and LLM training work. -1. **Met for the local process adapter:** two real external bandit generations and successor collection are recorded in [process validation](docs/process-trainer.md). A real small local policy is trained for at least two generations. Generation two collects with generation one's newly trained checkpoint. The experiment records before/after weights or checkpoint hashes and a held-out objective; a simulated process that emits a prewritten metric does not satisfy this criterion. +1. **Met for local process and Compose adapters:** two real external bandit generations and successor collection are recorded in [process validation](docs/process-trainer.md). A real small local policy is trained for at least two generations. Generation two collects with generation one's newly trained checkpoint. The experiment records before/after weights or checkpoint hashes and a held-out objective; a simulated process that emits a prewritten metric does not satisfy this criterion. 2. **Met for the in-process CPU adapter:** a bounded LLM adapter demonstrates collection, a real parameter update, held-out evaluation and successor use. Model and dataset licenses, checkpoint size and resource limits are declared in the programme before execution. See [the measured LM adapter](docs/lm-adapter.md) and [pm-rl-upe0](.agents/pm/features/pm-rl-upe0.toon). The external Docker trainer remains open. -3. **Met for local process execution:** real controller contention proves one launch and one reservation; independently approved worktree jobs preserve their metrics and lineage on merge. Two controllers contend for one job: exactly one process launches and exactly one budget reservation is charged. Concurrent unrelated jobs keep their metrics and lineage when branches merge in either direction. -4. **Met for the local process adapter:** real SIGKILL after staged completion and before its receipt commit recovers without retraining or double charge; planning/evaluation/promotion crashes and mid-fit SIGTERM retain evidence. Crash and cancellation drills cover every transition, including a complete checkpoint without a committed receipt. Restart is idempotent and does not overwrite prior artifacts. -5. **Met for the local process adapter:** dataset overlap, reward/environment drift, excessive gap, missing/corrupt receipts, invalid checkpoints and exceeded resource allowances refuse progression; failed candidates never collect the next generation. Contamination, a training-to-evaluation gap above `maximum_gap`, stale reward or environment identity, corrupt checkpoints, missing metrics and exceeded resource limits prevent promotion. A failed candidate cannot become the next collection policy. +3. **Met for local process and Compose execution:** real controller contention proves one launch and one reservation; independently approved worktree jobs preserve their metrics and lineage on merge. Two controllers contend for one job: exactly one process launches and exactly one budget reservation is charged. Concurrent unrelated jobs keep their metrics and lineage when branches merge in either direction. +4. **Met for local process and Compose adapters:** real SIGKILL after staged completion and before its receipt commit recovers without retraining or double charge; planning/evaluation/promotion crashes and mid-fit SIGTERM retain evidence. Crash and cancellation drills cover every transition, including a complete checkpoint without a committed receipt. Restart is idempotent and does not overwrite prior artifacts. +5. **Met for local process and Compose adapters:** dataset overlap, reward/environment drift, excessive gap, missing/corrupt receipts, invalid checkpoints and exceeded resource allowances refuse progression; failed candidates never collect the next generation. Contamination, a training-to-evaluation gap above `maximum_gap`, stale reward or environment identity, corrupt checkpoints, missing metrics and exceeded resource limits prevent promotion. A failed candidate cannot become the next collection policy. 6. Unit, real-process integration, CLI acceptance, replay, performance and failure-path tests cover every authored source file at exact statement/line/branch/function thresholds. The docstring gate covers the declared source surface, and the scope inventory names anything it excludes. ## Data boundary and capacity diff --git a/compose.ts b/compose.ts new file mode 100644 index 00000000..f4d1fdec --- /dev/null +++ b/compose.ts @@ -0,0 +1,65 @@ +/** Controller-owned Docker Compose lifecycle for the durable process trainer protocol. */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { devNull } from "node:os"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; +import { launchTrainer, type ProcessInput, type Resources } from "./process.ts"; +import { writeTrainerArtifact } from "./process-bandit.ts"; +import { expectedFail } from "./refuse.ts"; + +/** Derive an isolated Compose project from the full content-bound process job identity. */ +export function composeProject(id: string, directory: string): string { + if (!/^process-[a-f0-9]{64}$/u.test(id)) expectedFail("Invalid Compose job identity.", "process_job_drift"); + return `pm-rl-${createHash("sha256").update(JSON.stringify([id, resolve(directory)])).digest("hex")}`; +} + +/** Generate only one networkless trainer with job-local storage and no host service inputs. */ +export function composeConfiguration(image: string): unknown { + return { services: { trainer: { + image, pull_policy: "never", network_mode: "none", read_only: true, + user: `${process.getuid!()}:${process.getgid!()}`, + cap_drop: ["ALL"], security_opt: ["no-new-privileges:true"], + working_dir: "/job", stdin_open: true, restart: "no", + volumes: [ + { type: "bind", source: ".", target: "/job", bind: { create_host_path: false } }, + { type: "bind", source: "./compose.json", target: "/job/compose.json", read_only: true, bind: { create_host_path: false } }, + ], + } } }; +} + +/** Pin every Compose invocation to this generated file and project, ignoring ambient discovery. */ +function composeArgs(directory: string, id: string): string[] { + return ["compose", "--ansi", "never", "--env-file", devNull, "--project-directory", directory, "--project-name", composeProject(id, directory), "--file", join(directory, "compose.json")]; +} + +/** Stop only this job's Compose project, preserving every host artifact and recording stop evidence. */ +export async function stopComposeTrainer(directory: string, id: string, image: string, reason: string): Promise { + const file = join(directory, "compose.json"); + // A crash before config publication cannot have launched a container. + if (!existsSync(file)) return; + if (readFileSync(file, "utf8") !== JSON.stringify(composeConfiguration(image))) expectedFail("Compose configuration drifted; retained for inspection.", "process_job_drift"); + const project = composeProject(id, directory); + const options = { timeout: 15_000, maxBuffer: 65_536 }; + // Compose down/rm omit one-off run containers. Resolve only this exact named, labelled container, + // then remove its immutable daemon ID so name reuse cannot grant stop authority. + const listed = await promisify(execFile)("docker", ["container", "ls", "--all", "--no-trunc", "--filter", `name=^/${project}-trainer$`, "--filter", `label=com.docker.compose.project=${project}`, "--filter", "label=com.docker.compose.service=trainer", "--format", "{{.ID}}"], options); + const container = listed.stdout.trim(); + if (container.length > 0) await promisify(execFile)("docker", ["container", "rm", "--force", container], options); + await promisify(execFile)("docker", [...composeArgs(directory, id), "down", "--timeout", "0"], options); + const evidence = join(directory, "compose-stop.json"); + if (!existsSync(evidence)) writeTrainerArtifact(evidence, { format: "pm-rl/compose-stop/1", job: id, project, reason }); +} + +/** Run the container as the trainer; the controller owns its reservation, watchdog and cleanup. */ +export async function launchComposeTrainer(directory: string, id: string, input: ProcessInput, allowance: Resources, image: string, signal?: AbortSignal): Promise { + writeTrainerArtifact(join(directory, "compose.json"), composeConfiguration(image)); + let reason = "failed"; + try { + await launchTrainer(directory, id, input, allowance, ["docker", ...composeArgs(directory, id), "run", "--no-deps", "--pull", "never", "-T", "--name", `${composeProject(id, directory)}-trainer`, "trainer"], signal); + reason = "exited"; + } finally { + await stopComposeTrainer(directory, id, image, signal?.aborted === true ? "cancelled" : reason); + } +} diff --git a/docs/process-trainer.md b/docs/process-trainer.md index 6995ff15..56ffbb53 100644 --- a/docs/process-trainer.md +++ b/docs/process-trainer.md @@ -1,11 +1,11 @@ -# Durable local process trainer +# Durable process trainers [pm-rl-shgm](../.agents/pm/features/pm-rl-shgm.toon) implements the local-process slice of [pm-rl-apvf](../.agents/pm/epics/pm-rl-apvf.toon). Collection, held-out evaluation and promotion use the existing fenced recursive controller. The packaged TypeScript bandit kernel performs a real REINFORCE parameter update in an external child. Both the numerical implementation and synthetic data are MIT. -Docker Compose remains [pm-rl-mjo5](../.agents/pm/tasks/pm-rl-mjo5.toon). +[pm-rl-mjo5](../.agents/pm/tasks/pm-rl-mjo5.toon) adds Docker Compose execution using the same durable job and receipt contract. ## Programme and approved resources @@ -72,7 +72,7 @@ explicitly before executing a foreign branch's artifacts. ## Execution channel and completion contract -The CLI chooses a fixed packaged trainer, launching Node with the script as an +The local-process CLI chooses a fixed packaged trainer, launching Node with the script as an **argv array**, `shell: false`. It accepts no command field in the programme and never extracts executable commands from PM item text. The SDK adapter entry point can receive argv selected by trusted application code; it binds the launcher @@ -86,8 +86,8 @@ actual collected actions/rewards, `training_config`, and dataset/collection lineage digests. Environment and reward content identities are included. The held-out dataset's digest is included for attribution; its examples are absent. Training cannot read held-out data through this input channel. Local processes -run as the invoking user; filesystem isolation and process-tree/cgroup quotas -belong to the Compose follow-up. +run as the invoking user. The Compose adapter below supplies filesystem and +container lifecycle isolation; kernel CPU/storage quotas are tracked separately. Stdout is UTF-8 line-delimited JSON. Every event has `job` and `type`: @@ -148,6 +148,124 @@ possibly active fit. SIGTERM/AbortSignal stops the owned child, records external fit is terminal; another attempt requires a newly authorized programme identity. Completed phases before a boundary cancellation remain resumable. +## Docker Compose execution + +A bandit programme can select the container adapter for every generation: + +```json +{ + "kind": "compose-bandit", + "image": "sha256:<64 lowercase hexadecimal digits from docker build --quiet>", + "allowance": { + "training_steps": 1, + "wall_ms": 30000, + "storage_bytes": 65536, + "compute_units": 1000, + "tokens": 0 + } +} +``` + +The image must already exist in the local Docker daemon and implement the stdin, +NDJSON and staged bandit checkpoint protocol above. Mutable tags, repository +names, arbitrary commands and extra adapter fields refuse at configuration parse. +`docker compose run --pull never --no-deps -T` runs the image's entrypoint. The +image identity is part of the trusted launcher digest; source-policy, dataset, +programme and generation identities retain the existing process job schema. +The CLI, SDK and persisted resume/status paths use the same selection. Switching +images requires a new programme identity. The SDK's local-process argv override +does not override a Compose image's entrypoint. + +The controller reserves before launch and retains launch ownership. A project +name hashes the complete job ID and its staging location, isolating independent +trackers even when they have identical synthetic inputs. The controller fsyncs +an exclusive `compose.json` in the job directory and explicitly passes its file, +project and directory on every Compose call. Ambient Compose discovery and dotenv +loading are disabled. Only this job directory is mounted at the container's +working directory. The generated file is additionally mounted read-only to +prevent the trainer changing cleanup instructions. There are no host ports, +networks, Docker socket mounts, hosted inputs or host environment forwarding. +The root filesystem is read-only, capabilities are dropped, privilege escalation +is disabled and the trainer uses the invoking user's numeric UID/GID. Image +entrypoints must work within those restrictions. + +The container is the trainer process. The controller supplies input, applies the +existing wall/storage watchdog and validates its stream against durable host +files. Compute units retain the trainer-reported CPU-millisecond meaning; these +checks do not promise CPU scheduling, a cgroup CPU-time ceiling or an OS disk quota. +Kernel-enforced CPU/storage ceilings are tracked in +[pm-rl-7073](../.agents/pm/tasks/pm-rl-7073.toon). +The reservation and settlement transaction is shared with local execution. +Failure/cancellation releases the active hold, retaining the conservative settled +charge and original artifacts rather than assuming unknown consumption is zero. + +On normal exit, container death, watchdog refusal or cancellation, cleanup removes +only the exact job-named container with matching Compose project/service labels, +using its immutable daemon ID, then runs project-scoped `compose down`. The trainer +is created without automatic removal so recovery and daemon auto-removal cannot +race over container ownership. Explicit +container removal matters because Compose `down` and `rm` omit one-off `run` +containers on supported hosts. Cleanup never removes images or host artifacts. +A fsynced `compose-stop.json` records job, project and exit/cancellation/recovery +reason. The job Task and append-only history contain the durable failure or +cancellation receipt and settled charge. A failed cleanup refuses progression. + +Recovery never launches a previously running job. It stops any surviving owned +container, validates staged completion, then either adopts the original checkpoint +and settles once or commits failure with retained staging. A kill after completion +publication and before the PM materialization commit therefore trains only once. +A cancelled fit remains terminal. Transfer staging explicitly before resuming in +a relocated workspace; active containers belong to their original local location. + +### Real container drills + +Run `node --test test/compose.test.ts` from a source checkout. Its tiny local +`test/fixtures/compose.Dockerfile` uses `node:22-alpine`, copies only the actual +TypeScript numerical trainer and fixture entrypoint, and installs no dependencies. +Only the base image may need a pull; trainer execution has no network and never +pulls. The fixture performs the real REINFORCE update rather than emitting a +prewritten checkpoint. A second locally built image publishes a partial artifact +and waits, providing a deterministic kill/cancel barrier. + +The suite skips real Docker drills only when `docker compose version` fails and +prints an explicit `SKIPPED` reason. A working Compose CLI with a failed daemon, +image build or drill is a failure. It checks launch/receipt/replay, container kill, +cancellation, competing real controllers, controller SIGKILL before receipt commit, +orphan cleanup and two bounded generations with retained receipts. The second +collection Run must identify the first promoted checkpoint; both checkpoint byte +hashes must change. The same full coverage thresholds include the Compose source. + +The measured two-generation Compose drill promoted policy digests +`2a2616b67e223fbee4480df1716b75d7542a736f574bce0eed331ac17ad90866` +and `4242c0ffdd131a7c535bd18c42f6118e89f6cb27ed728485ddce43464d08b5c4`. +Their exact checkpoint byte digests were +`a81ee5538f8451969dd7d22df07ea5299dff92e4267200d760c5298a418cf66f` +and `04205767f4fefcb97c97eedc6fc7739da446f51ab7cc050f7f3f20e5f9527452`. +The second collection Run identifies the first promoted policy. Each job charged +one training step and zero tokens; completion and stop receipts remained under +its job directory. These are synthetic numerical-policy results. + +All 11 added cases fail with their relevant behaviour reverted in isolated source +snapshots: the previous adapter parser (one case), previous reserved execution +function (nine cases), or removal of the pre-launch recovery guard (one case). +The restored implementation passes 11/11 with zero skips. No test fixture, +coverage scope or threshold is changed by those proofs. + +### Compose release validation + +Both `npm run release:check` and `bun run release:check` pass **578/578 tests**, +with zero failures, skips, cancellations or todo cases. Each gate measures exact +**100/100/100/100** statement/line/branch/function coverage across all **28** +authored sources, **266** documented declarations, zero duplication and zero +production audit findings. Packaging and changelog checks pass. Dependency pins, +coverage scope and all thresholds are unchanged. + +One earlier npm attempt missed the existing packed LM CLI bound: the measured +CLI took 22.138 seconds against its unchanged 20-second limit. The same case +measured 9.038 seconds in the passing full npm gate and 12.907 seconds in the +passing full Bun gate. The separate test setup time is not the timed CLI interval. +[pm-rl-7c7m](../.agents/pm/tasks/pm-rl-7c7m.toon) tracks that existing variability. + ## Two-generation packed CLI measurement The real packed CLI selected `process-bandit` for both fits, used 128 collection diff --git a/index.ts b/index.ts index ad755523..4ad1fe67 100644 --- a/index.ts +++ b/index.ts @@ -3332,7 +3332,7 @@ async function inspectBanditGeneration(client: PmClient, programme: LoopProgramm if (config.process !== undefined) { if (pmRoot === undefined) fail("Process replay requires its tracker artifact root.", "process_artifact_root_required"); const processInput = banditProcessInput(config, chain.step, generation, receipt, request.id); - const expectedJob = processJobId(processInput, request.processTrainerArgv); + const expectedJob = processJobId(processInput, request.processTrainerArgv, programme.config.process); const related = (await loopProcessJobs(client, request.id, programme)).filter((job) => Number(jsonObject(job.binding, "Process binding")["generation"]) === generation); if (related.length > 1 || (related.length === 1 && related[0].id !== expectedJob && request.approval.length > 0)) fail("Trainer launcher identity changed; use the original trusted launcher or a new programme id.", "process_job_drift", EXIT_CODE.CONFLICT); const job = related.length === 1 ? related[0] : await readProcessJob(client, expectedJob); @@ -3739,7 +3739,7 @@ async function executePendingGeneration(client: PmClient, coordinates: Workspace const job = await executeProcessTrainer(client, coordinates, processInput, request.approval, programme.config.process!, request.signal, request.processTrainerArgv); return runLoopGeneration(programme.config, chain.step, generation, chain.current as BanditCheckpoint, job.completion!.policy) as unknown as JsonValue; }); - const job = (await readProcessJob(client, processJobId(processInput, request.processTrainerArgv)))!; + const job = (await readProcessJob(client, processJobId(processInput, request.processTrainerArgv, programme.config.process)))!; const candidate = verifiedProcessPolicy(job, coordinates.pmRoot, processInput)!; receipt = runLoopGeneration(programme.config, chain.step, generation, chain.current as BanditCheckpoint, candidate); if (hashJson(trained.value) !== hashJson(receipt as unknown as JsonValue)) fail("Process training receipt drifted.", "loop_step_drift", EXIT_CODE.CONFLICT); @@ -3870,7 +3870,7 @@ async function finishProcessGeneration(client: PmClient, coordinates: WorkspaceC if (config?.process === undefined) return; const input = banditProcessInput(config, chain.step, generation, receipt, request.id); await lease.guard(async () => { - const job = (await readProcessJob(client, processJobId(input, request.processTrainerArgv)))!; + const job = (await readProcessJob(client, processJobId(input, request.processTrainerArgv, config.process)))!; await verifyProcessJobBudget(client, job); await transitionProcessJob(client, coordinates, job, state, outcome); }); diff --git a/package.json b/package.json index 2bbff023..aef7b1d7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "dist/process.*", "dist/process-bandit.*", "dist/scripts/script-launcher.*", - "docs/process-trainer.md" + "docs/process-trainer.md", + "dist/compose.*" ], "engines": { "node": ">=22.18.0" @@ -94,7 +95,8 @@ "test/lm-cli.test.ts", "test/process.test.ts", "test/process-loop.test.ts", - "test/process-cli.test.ts" + "test/process-cli.test.ts", + "test/compose.test.ts" ], "thresholds": { "statements": 100, diff --git a/process.ts b/process.ts index 5a97d294..e7e2a886 100644 --- a/process.ts +++ b/process.ts @@ -12,18 +12,26 @@ import { EXIT_CODE, isPmCliExpectedError, certifyCompleteListResult } from "@unb import { banditCheckpoint, type BanditCheckpoint, type BanditExample, type BanditObservation } from "./bandit.ts"; import { canonicalJson, commitProcessTransaction, loopProcessStartTime, withProcessJobFence, readCompleteLoopItem, type JsonValue } from "./index.ts"; import { expectedFail } from "./refuse.ts"; +import { launchComposeTrainer, stopComposeTrainer } from "./compose.ts"; /** Independently accounted resource dimensions; all values are nonnegative safe integers. */ export const RESOURCE_KEYS = ["training_steps", "wall_ms", "storage_bytes", "compute_units", "tokens"] as const; /** A bounded allowance or measured resource vector; compute units are CPU milliseconds in the reference trainer. */ export type Resources = Readonly>; /** Programme-selected packaged trainer; commands are never read from tracker item text. */ -export interface ProcessAdapter { +export type ProcessAdapter = { /** Versioned adapter selector; only the packaged bandit trainer is CLI-selectable. */ readonly kind: "process-bandit"; /** Maximum allowance atomically reserved before launching this generation. */ readonly allowance: Resources; -} +} | { + /** Container execution of the identical staged bandit protocol. */ + readonly kind: "compose-bandit"; + /** Locally built immutable image ID; execution never pulls images. */ + readonly image: string; + /** Maximum allowance atomically reserved before this generation launches. */ + readonly allowance: Resources; +}; /** Domain lifecycle, independent of the tracker item's planning status. */ export type ProcessJobState = "planned" | "running" | "materialized" | "evaluated" | "promoted" | "failed" | "cancelled" | "invalidated"; /** Immutable inputs delivered on stdin; held-out examples are deliberately absent. */ @@ -115,10 +123,12 @@ export function parseResources(value: unknown): Resources { export function parseProcessAdapter(value: unknown): ProcessAdapter { if (value === null || typeof value !== "object" || Array.isArray(value)) expectedFail("Invalid process adapter.", "process_invalid_adapter"); const record = value as Record; - if (record["kind"] !== "process-bandit" || Object.keys(record).some((key) => key !== "kind" && key !== "allowance")) expectedFail("Select the packaged process-bandit adapter.", "process_invalid_adapter"); + const compose = record["kind"] === "compose-bandit"; + if ((!compose && record["kind"] !== "process-bandit") || Object.keys(record).some((key) => key !== "kind" && key !== "allowance" && !(compose && key === "image"))) expectedFail("Select process-bandit or compose-bandit.", "process_invalid_adapter"); + if (compose && (typeof record["image"] !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(record["image"]))) expectedFail("Compose requires a local immutable image ID.", "process_invalid_adapter"); const allowance = parseResources(record["allowance"]); if (allowance.training_steps !== 1 || allowance.wall_ms < 1 || allowance.wall_ms > 3_600_000 || allowance.storage_bytes < 1 || allowance.storage_bytes > 1_073_741_824) expectedFail("Process training requires one step, bounded wall time and staging bytes.", "process_invalid_allowance"); - return { kind: "process-bandit", allowance }; + return compose ? { kind: "compose-bandit", image: record["image"] as string, allowance } : { kind: "process-bandit", allowance }; } /** Resolve the packaged reference script for either source execution or the compiled package. */ export function referenceTrainerArgv(moduleUrl = import.meta.url): readonly string[] { @@ -126,8 +136,9 @@ export function referenceTrainerArgv(moduleUrl = import.meta.url): readonly stri return [process.execPath, fileURLToPath(new URL(`./process-bandit.${suffix}`, moduleUrl))]; } /** Derive replay identity including the trusted executable selection without persisting its paths. */ -export function processJobId(input: ProcessInput, argv: readonly string[] = referenceTrainerArgv()): string { - return `process-${processDigest({ input, launcher: processDigest(argv) }).slice(7)}`; +export function processJobId(input: ProcessInput, argv: readonly string[] = referenceTrainerArgv(), adapter?: ProcessAdapter): string { + const launcher = adapter?.kind === "compose-bandit" ? ["pm-rl/compose-bandit/1", adapter.image] : argv; + return `process-${processDigest({ input, launcher: processDigest(launcher) }).slice(7)}`; } /** Resolve this job's retained evidence directory under the tracker. */ export function processStaging(root: string, id: string): string { @@ -377,7 +388,7 @@ function failedProcessCharge(directory: string, allowance: Resources): Resources /** Recover staged work or execute exactly one reserved phase; failures settle conservatively and retain evidence. */ export async function executeProcessTrainer(client: PmClient, coordinates: ProcessCoordinates, input: ProcessInput, approval: string, adapter: ProcessAdapter, signal?: AbortSignal, argv: readonly string[] = referenceTrainerArgv()): Promise { - const id = processJobId(input, argv); + const id = processJobId(input, argv, adapter); return withProcessJobFence(coordinates, id, () => executeReservedProcessJob(client, coordinates, input, approval, adapter, id, signal, argv)); } @@ -391,9 +402,11 @@ async function executeReservedProcessJob(client: PmClient, coordinates: ProcessC mkdirSync(directory, { recursive: true }); if (job.state === "planned") { job = await transitionProcessJob(client, coordinates, job, "running", "launched"); - await launchTrainer(directory, id, input, job.allowance, argv, signal); + if (adapter.kind === "compose-bandit") await launchComposeTrainer(directory, id, input, job.allowance, adapter.image, signal); + else await launchTrainer(directory, id, input, job.allowance, argv, signal); } else { - if (existsSync(join(directory, "runner.json"))) { + if (adapter.kind === "compose-bandit") await stopComposeTrainer(directory, id, adapter.image, "recovery"); + else if (existsSync(join(directory, "runner.json"))) { const runner = JSON.parse(readFileSync(join(directory, "runner.json"), "utf8")) as { pid: number; start: string }; stopTrainer(runner.pid, runner.start); } diff --git a/test/compose.test.ts b/test/compose.test.ts new file mode 100644 index 00000000..c2eecdc8 --- /dev/null +++ b/test/compose.test.ts @@ -0,0 +1,200 @@ +/** Real Docker Compose contract, container death, cancellation, controller races and successor collection. */ +import assert from "node:assert/strict"; +import { execFileSync, fork, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { setTimeout } from "node:timers/promises"; +import { test } from "node:test"; +import { composeConfiguration, composeProject, stopComposeTrainer } from "../compose.ts"; +import { resumeRlLoop, rlLoopStatus } from "../index.ts"; +import { executeProcessTrainer, listProcessJobs, parseProcessAdapter, processJobId, processStaging, readProcessJob, reserveProcessJob, type ProcessAdapter } from "../process.ts"; +import { configuration, processWorkspace, trainingInput } from "./fixtures/process-workspace.ts"; + +// SIGKILL-able controllers and trainers must not leave truncated V8 reports. +delete process.env["NODE_V8_COVERAGE"]; +const version = spawnSync("docker", ["compose", "version"], { encoding: "utf8" }); +const skip = version.status === 0 ? false : `SKIPPED: docker compose version failed: ${version.error?.message ?? version.stderr.trim()}`; +const allowance = { training_steps: 1, wall_ms: 30_000, storage_bytes: 65_536, compute_units: 1000, tokens: 0 }; +const budget = { ...allowance, training_steps: 2, wall_ms: 60_000, storage_bytes: 131_072, compute_units: 2000 }; +const images = new Map(); + +/** Build only a local fixture plus the Node base; a build or daemon failure is a hard test failure. */ +function adapter(mode = "fit"): ProcessAdapter { + let image = images.get(mode); + if (image === undefined) { + image = execFileSync("docker", ["build", "--quiet", "--file", "test/fixtures/compose.Dockerfile", "--build-arg", `MODE=${mode}`, "."], { encoding: "utf8", timeout: 120_000 }).trim(); + assert.match(image, /^sha256:[a-f0-9]{64}$/u); images.set(mode, image); + } + return { kind: "compose-bandit", image, allowance }; +} + +/** Wait for a durable container barrier, with a bounded timeout and no timing-based assertions. */ +async function partial(directory: string): Promise { + for (let attempt = 0; attempt < 600; attempt += 1) { + if (existsSync(join(directory, "partial.json"))) return; + await setTimeout(25); + } + assert.fail("Container did not publish its partial artifact"); +} + +/** Assert cleanup against the exact content-derived name, never a host-wide container query. */ +function removed(id: string, directory: string): void { + const result = spawnSync("docker", ["container", "inspect", `${composeProject(id, directory)}-trainer`], { encoding: "utf8" }); + assert.notEqual(result.status, 0); assert.match(result.stderr, /No such (object|container)/u); +} + +/** Fork the real recursive controller, with barriers at the existing transaction boundaries. */ +function controller(pmRoot: string, approval: string, mode: string, selected: ProcessAdapter, generations = 1) { + const config = { ...configuration, adapter: selected, max_generations: generations }; + const child = fork(new URL("./fixtures/process-controller.ts", import.meta.url), [pmRoot, approval, JSON.stringify(config), mode], { silent: true, execArgv: [], env: { ...process.env, NODE_V8_COVERAGE: undefined } }); + const messages: unknown[] = []; let diagnostics = ""; + child.stderr!.on("data", (chunk: Buffer) => { diagnostics += String(chunk); }); + const result = new Promise((resolve, reject) => { child.once("error", reject); child.on("message", (message) => messages.push(message)); child.once("exit", (code) => { if (messages.length === 0 && code !== null && code !== 0) reject(new Error(diagnostics)); else resolve(code); }); }); + /** Observe a real IPC barrier, including a notification already received. */ + const ready = (name = "ready"): Promise => messages.includes(name) ? Promise.resolve() : new Promise((resolve, reject) => { child.on("message", (message) => { if (message === name) resolve(); }); child.once("exit", () => reject(new Error(`Controller exited before barrier: ${diagnostics}`))); }); + return { child, result, ready, messages }; +} + +test("Compose selection binds immutable images and validates isolated configuration", async () => { + const image = `sha256:${"a".repeat(64)}`; const selected = { kind: "compose-bandit", image, allowance } as const; + assert.deepEqual(parseProcessAdapter(selected), selected); + for (const bad of [undefined, 42, "node:22-alpine", "repo@sha256:abc", `sha256:${"g".repeat(64)}`]) assert.throws(() => parseProcessAdapter({ ...selected, image: bad })); + assert.throws(() => parseProcessAdapter({ ...selected, command: "untrusted" })); + const input = trainingInput(); const id = processJobId(input, undefined, selected); + assert.equal(processJobId(input, ["ignored-host-path"], selected), id); + assert.notEqual(processJobId(input), id); + assert.notEqual(processJobId(input, undefined, { ...selected, image: `sha256:${"b".repeat(64)}` }), id); + assert.match(composeProject(id, "."), /^pm-rl-[a-f0-9]{64}$/u); assert.throws(() => composeProject("foreign", ".")); + assert.notEqual(composeProject(id, "workspace-a"), composeProject(id, "workspace-b")); + const generated = composeConfiguration(image) as { services: { trainer: Record } }; + assert.equal(generated.services.trainer["network_mode"], "none"); + assert.equal(generated.services.trainer["ports"], undefined); assert.equal(generated.services.trainer["pull_policy"], "never"); + assert.equal(generated.services.trainer["read_only"], true); + const { root } = await processWorkspace(); const directory = join(root, "staging"); mkdirSync(directory); + await stopComposeTrainer(directory, id, image, "recovery"); + writeFileSync(join(directory, "compose.json"), "{}"); + await assert.rejects(stopComposeTrainer(directory, id, image, "recovery"), /configuration drifted/); +}); + +test("real Compose launch produces a receipt and replays one settled reservation", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter(); const input = trainingInput(); + const job = await executeProcessTrainer(client, coordinates, input, approval, selected); + assert.equal(job.id, processJobId(input, undefined, selected)); assert.equal(job.state, "materialized"); assert.equal(job.settled, true); + assert.equal(job.charge.training_steps, 1); assert.notEqual(job.completion!.policy.digest, input.source.digest); + const directory = processStaging(pmRoot, job.id); const retained = readFileSync(join(directory, "completion.json")); + assert.deepEqual(await executeProcessTrainer(client, coordinates, input, approval, selected), job); + assert.deepEqual(readFileSync(join(directory, "completion.json")), retained); + assert.equal(readFileSync(join(directory, "launches.ndjson"), "utf8").trim().split("\n").length, 1); + assert.equal(JSON.parse(readFileSync(join(directory, "compose-stop.json"), "utf8")).reason, "exited"); removed(job.id, processStaging(pmRoot, job.id)); +}); + +test("real Compose container death retains staging, commits failure and releases the active reservation", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter("hang"); const input = trainingInput(); const id = processJobId(input, undefined, selected); + const directory = processStaging(pmRoot, id); + const run = assert.rejects(executeProcessTrainer(client, coordinates, input, approval, selected), /unsuccessfully/); + await partial(directory); execFileSync("docker", ["kill", `${composeProject(id, directory)}-trainer`], { stdio: "ignore" }); await run; + const job = (await readProcessJob(client, id))!; + assert.equal(job.state, "failed"); assert.equal(job.outcome, "trainer_failure"); assert.equal(job.settled, true); + assert.deepEqual(job.charge, allowance); assert.ok(existsSync(join(directory, "partial.json"))); + assert.equal(JSON.parse(readFileSync(join(directory, "compose-stop.json"), "utf8")).reason, "failed"); + assert.equal((await listProcessJobs(client)).filter((entry) => !entry.settled).length, 0); removed(id, directory); + await assert.rejects(executeProcessTrainer(client, coordinates, input, approval, selected), /Process job is failed/); +}); + +test("real Compose cancellation stops the container and retains attributable cancellation evidence", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter("hang"); const input = trainingInput(); const id = processJobId(input, undefined, selected); + const directory = processStaging(pmRoot, id); const abort = new AbortController(); + const run = assert.rejects(executeProcessTrainer(client, coordinates, input, approval, selected, abort.signal), /cancelled/); + await partial(directory); abort.abort(); await run; + const job = (await readProcessJob(client, id))!; + assert.equal(job.state, "cancelled"); assert.equal(job.outcome, "process_cancelled"); assert.equal(job.settled, true); + assert.equal(JSON.parse(readFileSync(join(directory, "compose-stop.json"), "utf8")).reason, "cancelled"); + assert.ok(readFileSync(join(pmRoot, "history", `rl-${id}.jsonl`), "utf8").includes("process_cancelled")); + assert.ok(existsSync(join(directory, "partial.json"))); removed(id, directory); +}); + +test("two real Compose controllers produce exactly one launch and one reservation", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter(); + const contenders = [controller(pmRoot, approval, "race", selected), controller(pmRoot, approval, "race", selected)]; + await Promise.all(contenders.map((entry) => entry.ready("starting"))); + for (const entry of contenders) entry.child.send("start"); + const winner = await Promise.any(contenders.map(async (entry) => { await entry.ready(); return entry; })); + const loser = contenders.find((entry) => entry !== winner)!; + assert.equal(await loser.result, 1); assert.ok(JSON.stringify(loser.messages).includes("loop_controller_active")); + winner.child.send("continue"); assert.equal(await winner.result, 0); + const jobs = await listProcessJobs(client); assert.equal(jobs.length, 1); assert.equal(jobs[0].charge.training_steps, 1); + const directory = processStaging(pmRoot, jobs[0].id); + assert.equal(readFileSync(join(directory, "launches.ndjson"), "utf8").trim().split("\n").length, 1); + assert.equal((await resumeRlLoop(client, coordinates, { id: "process-drill", approval })).resumed_generations, 1); removed(jobs[0].id, directory); +}); + +test("real Compose completed staging survives controller SIGKILL before receipt commit", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter(); + const running = controller(pmRoot, approval, "materialized", selected); await running.ready(); + const [job] = await listProcessJobs(client); assert.equal(job.state, "running"); const directory = processStaging(pmRoot, job.id); + const checkpoint = readFileSync(join(directory, "checkpoint.json")); const completion = readFileSync(join(directory, "completion.json")); + running.child.kill("SIGKILL"); await running.result; + assert.equal((await resumeRlLoop(client, coordinates, { id: "process-drill", approval })).promoted, 1); + assert.deepEqual(readFileSync(join(directory, "checkpoint.json")), checkpoint); assert.deepEqual(readFileSync(join(directory, "completion.json")), completion); + assert.equal(readFileSync(join(directory, "launches.ndjson"), "utf8").trim().split("\n").length, 1); + assert.equal((await listProcessJobs(client)).length, 1); removed(job.id, processStaging(pmRoot, job.id)); +}); + +test("real Compose recovery terminates an orphan trainer without relaunching a running job", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter("hang"); + const running = controller(pmRoot, approval, "cancel", selected); + let id = ""; + for (let attempt = 0; attempt < 600; attempt += 1) { const jobs = await listProcessJobs(client); if (jobs.length === 1) { id = jobs[0].id; break; } await setTimeout(25); } + assert.ok(id); const directory = processStaging(pmRoot, id); await partial(directory); + running.child.kill("SIGKILL"); await running.result; + await assert.rejects(resumeRlLoop(client, coordinates, { id: "process-drill", approval }), /no completion receipt/); + const job = (await readProcessJob(client, id))!; assert.equal(job.state, "failed"); assert.equal(job.settled, true); + assert.ok(existsSync(join(directory, "partial.json"))); assert.equal(readFileSync(join(directory, "launches.ndjson"), "utf8").trim().split("\n").length, 1); removed(id, directory); +}); + +test("real Compose two-generation run changes checkpoints and collects with the promoted successor", { skip }, async (t) => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter(); + // Use a real controller subprocess, then independently reconstruct all results through the SDK. + const running = controller(pmRoot, approval, "done", selected, 2); assert.equal(await running.result, 0); + const report = await rlLoopStatus(client, "process-drill", pmRoot); assert.equal(report.promoted, 2); + const jobs = report.process_jobs!; assert.equal(jobs.length, 2); assert.notEqual(jobs[0].completion!.digest, jobs[1].completion!.digest); + assert.notEqual(jobs[0].completion!.policy.digest, trainingInput().source.digest); + assert.equal((await client.get(report.generations[1].run!)).item.component, jobs[0].completion!.policy.digest); + for (const job of jobs) { + const directory = processStaging(pmRoot, job.id); + assert.equal(job.state, "promoted"); assert.equal(job.settled, true); assert.ok(existsSync(join(directory, "completion.json"))); + assert.equal(JSON.parse(readFileSync(join(directory, "compose.json"), "utf8")).services.trainer.image, selected.kind === "compose-bandit" ? selected.image : "unexpected-local-adapter"); + assert.equal(readFileSync(join(directory, "launches.ndjson"), "utf8").trim().split("\n").length, 1); removed(job.id, directory); + } + assert.equal((await resumeRlLoop(client, coordinates, { id: "process-drill", approval })).resumed_generations, 2); + t.diagnostic(JSON.stringify(jobs.map((job) => ({ checkpoint: job.completion!.digest, policy: job.completion!.policy.digest, usage: job.charge })))); +}); + +test("real Compose recovery before config publication fails safely without a container launch", { skip }, async () => { + const { client, coordinates, approval, pmRoot } = await processWorkspace(budget); const selected = adapter(); const input = trainingInput(); const id = processJobId(input, undefined, selected); + const job = await reserveProcessJob(client, coordinates, id, input, approval, allowance); + await client.update(id, { body: JSON.stringify({ ...job, state: "running" }) }); + await assert.rejects(executeProcessTrainer(client, coordinates, input, approval, selected), /no completion receipt/); + assert.equal((await readProcessJob(client, id))!.state, "failed"); assert.equal(existsSync(join(processStaging(pmRoot, id), "launches.ndjson")), false); removed(id, processStaging(pmRoot, id)); +}); + +test("real Compose independent trackers with identical jobs cannot stop each other's containers", { skip }, async () => { + const first = await processWorkspace(budget); const second = await processWorkspace(budget); const selected = adapter("hang"); const input = trainingInput(); const id = processJobId(input, undefined, selected); + const directories = [first, second].map((workspace) => processStaging(workspace.pmRoot, id)); + const aborts = [new AbortController(), new AbortController()]; + const runs = [first, second].map((workspace, index) => assert.rejects(executeProcessTrainer(workspace.client, workspace.coordinates, input, workspace.approval, selected, aborts[index].signal), /cancelled/)); + await Promise.all(directories.map(partial)); + aborts[0].abort(); await runs[0]; removed(id, directories[0]); + const alive = execFileSync("docker", ["container", "inspect", `${composeProject(id, directories[1])}-trainer`, "--format", "{{.State.Running}}"], { encoding: "utf8" }); assert.equal(alive.trim(), "true"); + aborts[1].abort(); await runs[1]; removed(id, directories[1]); +}); + +test("real Compose checkpoint without completion is refused and cannot collect a successor", { skip }, async () => { + const { client, approval, pmRoot } = await processWorkspace(budget); const selected = adapter("checkpoint-only"); + const running = controller(pmRoot, approval, "done", selected, 2); + assert.equal(await running.result, 1); assert.ok(JSON.stringify(running.messages).includes("process_receipt_required")); + const [job] = await listProcessJobs(client); const directory = processStaging(pmRoot, job.id); + assert.equal(job.state, "failed"); assert.equal(job.settled, true); assert.ok(existsSync(join(directory, "checkpoint.json"))); + assert.equal(existsSync(join(directory, "completion.json")), false); + assert.equal((await client.listAllComplete({})).items.filter((item) => item.type === "Run").length, 1); removed(job.id, directory); +}); diff --git a/test/fixtures/compose-trainer.ts b/test/fixtures/compose-trainer.ts new file mode 100644 index 00000000..1c3174f7 --- /dev/null +++ b/test/fixtures/compose-trainer.ts @@ -0,0 +1,15 @@ +/** Tiny real container trainer; the hang mode exposes deterministic kill and cancellation barriers. */ +import { appendFileSync, unlinkSync } from "node:fs"; +import { Writable } from "node:stream"; +import { setTimeout } from "node:timers/promises"; +import { runReferenceTrainer, writeTrainerArtifact } from "../../process-bandit.ts"; + +appendFileSync("launches.ndjson", `${JSON.stringify({ started: true })}\n`); +if (process.env["TRAINER_MODE"] === "hang") { + process.stdin.resume(); + writeTrainerArtifact("partial.json", { partial: "retained" }); + await setTimeout(120_000); +} else if (process.env["TRAINER_MODE"] === "checkpoint-only") { + const sink = new Writable({ write(_chunk, _encoding, callback) { callback(); } }); + await runReferenceTrainer(process.stdin, sink); unlinkSync("completion.json"); +} else await runReferenceTrainer(); diff --git a/test/fixtures/compose.Dockerfile b/test/fixtures/compose.Dockerfile new file mode 100644 index 00000000..79189842 --- /dev/null +++ b/test/fixtures/compose.Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine +WORKDIR /trainer +COPY package.json bandit.ts promotion.ts process-bandit.ts ./ +COPY scripts/script-launcher.ts ./scripts/ +COPY test/fixtures/compose-trainer.ts ./test/fixtures/ +ARG MODE=fit +ENV TRAINER_MODE=$MODE +ENTRYPOINT ["node", "--experimental-strip-types", "/trainer/test/fixtures/compose-trainer.ts"] From 3adf73ca2d69aadcdf9ad081242d9e18d03ac9fc Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:04:15 +0200 Subject: [PATCH 2/3] Preserve Compose trainer outcomes when cleanup fails --- .agents/pm/history/pm-rl-mjo5.jsonl | 8 ++++ .agents/pm/tasks/pm-rl-mjo5.toon | 21 ++++++++-- compose.ts | 5 ++- docs/process-trainer.md | 20 +++++++++- process.ts | 16 ++++++-- test/compose.test.ts | 60 +++++++++++++++++++++++++++-- 6 files changed, 116 insertions(+), 14 deletions(-) diff --git a/.agents/pm/history/pm-rl-mjo5.jsonl b/.agents/pm/history/pm-rl-mjo5.jsonl index fe4a69bf..8a75f5ac 100644 --- a/.agents/pm/history/pm-rl-mjo5.jsonl +++ b/.agents/pm/history/pm-rl-mjo5.jsonl @@ -22,3 +22,11 @@ {"hash_algorithm":"sha256","ts":"2026-10-15T18:19:18.033Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-10-15T18:19:18.033Z","author":"codex-agent","text":"Follow-up gap pm-rl-7c7m records the existing packed LM CLI timing variability observed by the full gate. The standalone unchanged case passed at 8955 ms. This task retains the 20000 ms bound and all original coverage thresholds; kernel CPU/storage enforcement remains separately tracked by pm-rl-7073."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T18:19:18.033Z"}],"before_hash":"f09599cb6318cbf17114350e6d79fbbbf4bb951e5c07762ee3bb279fe43206d5","after_hash":"a7f7e9f569f74fe6c5f7fb90d58c809bdca1d1fe9bb031292a7a97451726440e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"ef7121237b2f875ba34d308d72b3457451e7830a235f178e92ad955712157c19"} {"hash_algorithm":"sha256","ts":"2026-10-15T19:38:00.185Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-10-15T19:38:00.185Z","author":"codex-agent","text":"Final unchanged-source gates PASS: npm run release:check 578/578 and bun run release:check 578/578, each zero fail/skip/cancel/todo. Both measure exact 100/100/100/100 statement/line/branch/function coverage across 28 authored sources, 266 documented declarations, zero duplication, zero production audit findings; identity, packaging and changelog checks pass under isolated CI Git/home settings. Source hashes match the final implementation. All 11 new real Compose tests pass in both full gates and through explicit pm test --run --progress using none/snapshot context. Per-test behavioural reverts and the restored 11/11 result are recorded above. Two generations retain changed checkpoint byte digests a81ee5538f8451969dd7d22df07ea5299dff92e4267200d760c5298a418cf66f and 04205767f4fefcb97c97eedc6fc7739da446f51ab7cc050f7f3f20e5f9527452; the promoted first policy collects generation two. Earlier LM timing miss is tracked by pm-rl-7c7m; passing full gates measure 9038 ms and 12907 ms within the unchanged 20000 ms limit. Kernel CPU/storage quotas remain pm-rl-7073. No new PM CLI defect was filed: result tracking is disabled by policy, so comments retain durable gate evidence; supported snapshot context avoids the known tracker-spawning sandbox collision. Items remain open for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T19:38:00.185Z"}],"before_hash":"a7f7e9f569f74fe6c5f7fb90d58c809bdca1d1fe9bb031292a7a97451726440e","after_hash":"e9cb6ae5726b302ce6e8a16ce4623cdeb63b5f630ec50fbd2ef07bb62532977f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"aafd19d9269f6ddcef5a75c521385b5e0831bd247669c2b3efc1a1c02937e4b6"} {"hash_algorithm":"sha256","ts":"2026-10-15T19:38:04.500Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"f5d0e8949390cc13c5995346","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T19:38:04.500Z"}],"before_hash":"e9cb6ae5726b302ce6e8a16ce4623cdeb63b5f630ec50fbd2ef07bb62532977f","after_hash":"ddb126bc46f45e455fc43ffe8dd61d0756681a44deb9759e7759b90806a31120","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"aa8f5f1f64e7430be2062d9732e08b8de484772ff4e369dbd9f05ae3ff68c753"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:04:59.287Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:04:59.287Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"ddb126bc46f45e455fc43ffe8dd61d0756681a44deb9759e7759b90806a31120","after_hash":"071e472d4efd8fcbb164887c560c6fcf27969bd2474fb796e2654868af643706","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"23faba19e2e1542f0a55f0a1b7227976c6c541a97f288b5a4e0831ebf0b1a845"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:05:02.235Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/8","value":{"created_at":"2026-10-15T20:05:02.235Z","author":"codex-agent","text":"Review round 1 confirmed: launchComposeTrainer finally cleanup can replace cancellation or trainer failure and can invalidate a successful completion. Plan: persist a distinct cleanup_failed diagnostic on the process job while preserving primary execution outcome; cover cancellation, success, and trainer failure with real Compose plus failure injection at the Docker exec boundary, then record behavioural revert proof and unchanged npm/Bun release gates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:05:02.235Z"}],"before_hash":"071e472d4efd8fcbb164887c560c6fcf27969bd2474fb796e2654868af643706","after_hash":"2d29deef4353e4532ea8632b4d26ef923ba36d2e83a6c2c5f6cdac435fd5cbf3","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"64d0b5860ded789d8329c1d848ac556ef3f928f597082427a165a1fe6d9b3c90"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:07:48.538Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"add","path":"/metadata/tests/2","value":{"command":"node --test --test-name-pattern=\"cleanup failure\" test/compose.test.ts","scope":"project","timeout_seconds":300,"pm_context_mode":"none","workspace_context_mode":"snapshot","provenance":{"author":"codex-agent","created_at":"2026-10-15T20:07:48.430Z","source_kind":"local_mutation","source_ref":"feat/compose-process-trainer"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:07:48.538Z"}],"before_hash":"2d29deef4353e4532ea8632b4d26ef923ba36d2e83a6c2c5f6cdac435fd5cbf3","after_hash":"f42265b545f818ced4cecd7ea82115fea9b13493fcd7072789fa323bed8dce4c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"21a0e6c3a99a141222625996ce2c245a1ffc8a9a5cc0cfe687bce290a746b10f"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:10:03.237Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/9","value":{"created_at":"2026-10-15T20:10:03.237Z","author":"codex-agent","text":"Review round 1 implementation: launch cleanup reports through a separate diagnostic callback; settlement commits cleanup.code=cleanup_failed and a bounded error alongside the original outcome/completion. Recovery preserves an existing completion through the same diagnostic path. Error text replaces the job staging directory and is capped at 4096 characters. Real Compose regressions pass 4/4 via node --test --test-name-pattern=\"cleanup failure\" test/compose.test.ts and the linked pm test run (snapshot workspace, pm_context_mode=none); no failures/skips/cancellations/todo. The trainer-failure case kills the real job container and verifies the original Trainer exited unsuccessfully error and trainer_failure outcome. No SDK mocks."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:10:03.237Z"}],"before_hash":"f42265b545f818ced4cecd7ea82115fea9b13493fcd7072789fa323bed8dce4c","after_hash":"7d854bfd8ec40276d8c035f445b8708e8e3e7f843fe96c88786320f8ba46391e","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"074472b606d9adf1977fa57f9d9f472f2730eb90a2e4fde29712e0508cd1d0ab"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:10:04.639Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/10","value":{"created_at":"2026-10-15T20:10:04.639Z","author":"codex-agent","text":"Behavioural revert proof, final fixtures: in an isolated source snapshot, revert only launchComposeTrainer cleanup handling to its original throwing finally (retain the callback signature so the caller still compiles). node --test --test-name-pattern=\"(cancellation|success|trainer failure) plus cleanup failure\" test/compose.test.ts fails 3/3: cancellation and trainer error are replaced by the injected Compose down error; successful completion rejects. Separately reverting only the recovery cleanup catch in executeReservedProcessJob fails its recovery regression 1/1. Restored code passes all 4/4 through pm test --run --only-last --progress. Real Docker launch/train/kill/remove and real temporary SDK trackers run in every case; only compose down fails through an executable boundary wrapper. Test result tracking is disabled by project policy, so this comment preserves the results."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:10:04.639Z"}],"before_hash":"7d854bfd8ec40276d8c035f445b8708e8e3e7f843fe96c88786320f8ba46391e","after_hash":"091bff1b7f00f97cb96b7923d84ca081d1fe2fe7861011e288df54290730dc11","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"8b72bebbb58073e6c2c7c0fa5d43a9a246e3cf7f9eae41c4a74bc393179780e0"} +{"hash_algorithm":"sha256","ts":"2026-10-15T20:37:13.115Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/11","value":{"created_at":"2026-10-15T20:37:13.115Z","author":"codex-agent","text":"Review round 1 npm release gate PASS: npm run release:check under isolated CI Git/home settings passed 582/582 tests, zero fail/skip/cancel/todo. Coverage remains exact 100/100/100/100 statement/line/branch/function across 28 authored sources; 266 documented declarations, zero duplication and production audit findings; identity, attestation, packaging and changelog checks passed. All 15 real Compose tests passed. Existing LM CLI timing measured 12623 ms within the unchanged 20000 ms bound. Running the unchanged full Bun gate next."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:37:13.115Z"}],"before_hash":"091bff1b7f00f97cb96b7923d84ca081d1fe2fe7861011e288df54290730dc11","after_hash":"b519896010d5603276f1a19115e8dfe0bd3e78825efb86dd65dc15b9c3a8237c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"51494a8107171793a1d830287b89f9024063441adfd5f6a7e72093fc9356db68"} +{"hash_algorithm":"sha256","ts":"2026-10-15T21:03:59.855Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/12","value":{"created_at":"2026-10-15T21:03:59.855Z","author":"codex-agent","text":"Review round 1 final verdict: finding confirmed and fixed. Both npm run release:check and bun run release:check PASS 582/582 tests each, zero fail/skip/cancel/todo, unchanged exact 100/100/100/100 statement/line/branch/function coverage across 28 sources and 266 documented declarations. Zero duplication and production audit findings; identity, attestation, packaging and changelog checks pass under isolated CI Git/home settings. All 15 real Compose cases pass in both gates; 4/4 new cleanup regressions also pass via the linked PM snapshot test. Behavioural revert proofs (3/3 launch cases and 1/1 recovery case fail when only the relevant behavior is reverted) are recorded above. Gated source hashes match. Packed LM CLI measured 12623 ms npm and 12272 ms Bun within the unchanged 20000 ms bound. Cleanup failure remains visible as cleanup_failed and can require inspection of retained job resources; existing timing and kernel quota follow-ups remain unchanged. No new PM CLI/SDK defect encountered; test result tracking is disabled by project policy. Leaving the item open for orchestrator verification and releasing the claim."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T21:03:59.855Z"}],"before_hash":"b519896010d5603276f1a19115e8dfe0bd3e78825efb86dd65dc15b9c3a8237c","after_hash":"38902a0075e18c4ef8a1cc1157183b710fffba4e91f2947462e1ced609570404","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"bc7372332c9ef36072e7f21119ca2a5eb31efa3971209b28662a10cc91b8019b"} +{"hash_algorithm":"sha256","ts":"2026-10-15T21:04:01.562Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T21:04:01.562Z"}],"before_hash":"38902a0075e18c4ef8a1cc1157183b710fffba4e91f2947462e1ced609570404","after_hash":"21d8d66684281a416c5c84fe244d704d01d3a9d0a762fb9c267e6aaa937c52e9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"2d1c36c50b604f84b344de63a2736bbec58b629a7f655501f266e5c1c7a56038"} diff --git a/.agents/pm/tasks/pm-rl-mjo5.toon b/.agents/pm/tasks/pm-rl-mjo5.toon index b2979584..7d0521de 100644 --- a/.agents/pm/tasks/pm-rl-mjo5.toon +++ b/.agents/pm/tasks/pm-rl-mjo5.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags: [] created_at: "2026-10-14T19:46:39.492Z" -updated_at: "2026-10-15T19:38:04.500Z" +updated_at: "2026-10-15T21:04:01.562Z" author: codex-agent acceptance_criteria: "Compose execution preserves launch ownership and process contract, retains staging on failure, and passes real reservation and recovery drills" parent: pm-rl-apvf -comments[8]{created_at,author,text}: +comments[13]{created_at,author,text}: "2026-10-15T10:58:38.098Z",codex-agent,"Design: reuse the process job fence, reservation transaction, receipt validator and settlement state machine. Add a trusted Compose launcher with a job-derived project identity and generated isolated staging configuration; mount only job staging and retain all protocol artifacts. Exercise real success, container death, cancellation, contention and two-generation successor collection. Behavioural revert proofs will remove only the relevant launcher or selection behaviour in an isolated snapshot." "2026-10-15T11:08:14.363Z",codex-agent,"Real Docker findings: a capability-restricted container must use the invoking numeric UID/GID to write host staging. Compose down/rm omit one-off run containers on the tested version, so cleanup resolves only the exact job-named container with matching project/service labels and removes its immutable daemon ID before project-scoped down. Project names also hash staging scope to isolate identical jobs in independent trackers. Receipt validation, conservative settled charges and controller fences remain shared with local execution." "2026-10-15T11:11:10.946Z",codex-agent,"Drill progress: launch/receipt/replay, container death, cancellation, two real controller contention, staged-completion SIGKILL recovery and orphan cleanup pass. The two-generation Compose run promoted both changed policies, and generation two collection identifies generation one policy. Kernel CPU/storage quotas are a distinct follow-up to the existing reported-use and polling contract. Full release gates run directly under isolated CI Git/home settings; pm test --run --progress passed the linked typecheck, but project policy reports tracking_disabled so durable gate results are recorded in comments." @@ -19,6 +19,11 @@ comments[8]{created_at,author,text}: "2026-10-15T17:57:01.089Z",codex-agent,"Final-source npm attempt completed 578 tests: 577 passed, one existing packed LM CLI timing assertion failed (measured CLI 22138 ms against the unchanged 20000 ms bound), zero skipped/cancelled/todo. All 28 source files reported 100 percent line/branch/function coverage; the statement gate correctly did not claim success after a test failure. All 11 Compose cases passed, and the explicit pm test --run --progress --only-last snapshot run passed 11/11 in 111.389 seconds. No thresholds or deadline assertions were changed. Retrying the existing timing case and then both complete gates serially." "2026-10-15T18:19:18.033Z",codex-agent,Follow-up gap pm-rl-7c7m records the existing packed LM CLI timing variability observed by the full gate. The standalone unchanged case passed at 8955 ms. This task retains the 20000 ms bound and all original coverage thresholds; kernel CPU/storage enforcement remains separately tracked by pm-rl-7073. "2026-10-15T19:38:00.185Z",codex-agent,"Final unchanged-source gates PASS: npm run release:check 578/578 and bun run release:check 578/578, each zero fail/skip/cancel/todo. Both measure exact 100/100/100/100 statement/line/branch/function coverage across 28 authored sources, 266 documented declarations, zero duplication, zero production audit findings; identity, packaging and changelog checks pass under isolated CI Git/home settings. Source hashes match the final implementation. All 11 new real Compose tests pass in both full gates and through explicit pm test --run --progress using none/snapshot context. Per-test behavioural reverts and the restored 11/11 result are recorded above. Two generations retain changed checkpoint byte digests a81ee5538f8451969dd7d22df07ea5299dff92e4267200d760c5298a418cf66f and 04205767f4fefcb97c97eedc6fc7739da446f51ab7cc050f7f3f20e5f9527452; the promoted first policy collects generation two. Earlier LM timing miss is tracked by pm-rl-7c7m; passing full gates measure 9038 ms and 12907 ms within the unchanged 20000 ms limit. Kernel CPU/storage quotas remain pm-rl-7073. No new PM CLI defect was filed: result tracking is disabled by policy, so comments retain durable gate evidence; supported snapshot context avoids the known tracker-spawning sandbox collision. Items remain open for orchestrator verification." + "2026-10-15T20:05:02.235Z",codex-agent,"Review round 1 confirmed: launchComposeTrainer finally cleanup can replace cancellation or trainer failure and can invalidate a successful completion. Plan: persist a distinct cleanup_failed diagnostic on the process job while preserving primary execution outcome; cover cancellation, success, and trainer failure with real Compose plus failure injection at the Docker exec boundary, then record behavioural revert proof and unchanged npm/Bun release gates." + "2026-10-15T20:10:03.237Z",codex-agent,"Review round 1 implementation: launch cleanup reports through a separate diagnostic callback; settlement commits cleanup.code=cleanup_failed and a bounded error alongside the original outcome/completion. Recovery preserves an existing completion through the same diagnostic path. Error text replaces the job staging directory and is capped at 4096 characters. Real Compose regressions pass 4/4 via node --test --test-name-pattern=\"cleanup failure\" test/compose.test.ts and the linked pm test run (snapshot workspace, pm_context_mode=none); no failures/skips/cancellations/todo. The trainer-failure case kills the real job container and verifies the original Trainer exited unsuccessfully error and trainer_failure outcome. No SDK mocks." + "2026-10-15T20:10:04.639Z",codex-agent,"Behavioural revert proof, final fixtures: in an isolated source snapshot, revert only launchComposeTrainer cleanup handling to its original throwing finally (retain the callback signature so the caller still compiles). node --test --test-name-pattern=\"(cancellation|success|trainer failure) plus cleanup failure\" test/compose.test.ts fails 3/3: cancellation and trainer error are replaced by the injected Compose down error; successful completion rejects. Separately reverting only the recovery cleanup catch in executeReservedProcessJob fails its recovery regression 1/1. Restored code passes all 4/4 through pm test --run --only-last --progress. Real Docker launch/train/kill/remove and real temporary SDK trackers run in every case; only compose down fails through an executable boundary wrapper. Test result tracking is disabled by project policy, so this comment preserves the results." + "2026-10-15T20:37:13.115Z",codex-agent,"Review round 1 npm release gate PASS: npm run release:check under isolated CI Git/home settings passed 582/582 tests, zero fail/skip/cancel/todo. Coverage remains exact 100/100/100/100 statement/line/branch/function across 28 authored sources; 266 documented declarations, zero duplication and production audit findings; identity, attestation, packaging and changelog checks passed. All 15 real Compose tests passed. Existing LM CLI timing measured 12623 ms within the unchanged 20000 ms bound. Running the unchanged full Bun gate next." + "2026-10-15T21:03:59.855Z",codex-agent,"Review round 1 final verdict: finding confirmed and fixed. Both npm run release:check and bun run release:check PASS 582/582 tests each, zero fail/skip/cancel/todo, unchanged exact 100/100/100/100 statement/line/branch/function coverage across 28 sources and 266 documented declarations. Zero duplication and production audit findings; identity, attestation, packaging and changelog checks pass under isolated CI Git/home settings. All 15 real Compose cases pass in both gates; 4/4 new cleanup regressions also pass via the linked PM snapshot test. Behavioural revert proofs (3/3 launch cases and 1/1 recovery case fail when only the relevant behavior is reverted) are recorded above. Gated source hashes match. Packed LM CLI measured 12623 ms npm and 12272 ms Bun within the unchanged 20000 ms bound. Cleanup failure remains visible as cleanup_failed and can require inspection of retained job resources; existing timing and kernel quota follow-ups remain unchanged. No new PM CLI/SDK defect encountered; test result tracking is disabled by project policy. Leaving the item open for orchestrator verification and releasing the claim." files[7]{path,scope}: compose.ts,project index.ts,project @@ -27,7 +32,7 @@ files[7]{path,scope}: test/compose.test.ts,project test/fixtures/compose-trainer.ts,project test/fixtures/compose.Dockerfile,project -tests[2]: +tests[3]: - command: npm run typecheck scope: project provenance: @@ -45,6 +50,16 @@ tests[2]: created_at: "2026-10-15T11:11:34.932Z" source_kind: local_mutation source_ref: feat/compose-process-trainer + - command: "node --test --test-name-pattern=\"cleanup failure\" test/compose.test.ts" + scope: project + timeout_seconds: 300 + pm_context_mode: none + workspace_context_mode: snapshot + provenance: + author: codex-agent + created_at: "2026-10-15T20:07:48.430Z" + source_kind: local_mutation + source_ref: feat/compose-process-trainer docs[3]{path,scope}: docs/process-trainer.md,project README.md,project diff --git a/compose.ts b/compose.ts index f4d1fdec..39d2ecd4 100644 --- a/compose.ts +++ b/compose.ts @@ -53,13 +53,14 @@ export async function stopComposeTrainer(directory: string, id: string, image: s } /** Run the container as the trainer; the controller owns its reservation, watchdog and cleanup. */ -export async function launchComposeTrainer(directory: string, id: string, input: ProcessInput, allowance: Resources, image: string, signal?: AbortSignal): Promise { +export async function launchComposeTrainer(directory: string, id: string, input: ProcessInput, allowance: Resources, image: string, onCleanupFailure: (error: unknown) => void, signal?: AbortSignal): Promise { writeTrainerArtifact(join(directory, "compose.json"), composeConfiguration(image)); let reason = "failed"; try { await launchTrainer(directory, id, input, allowance, ["docker", ...composeArgs(directory, id), "run", "--no-deps", "--pull", "never", "-T", "--name", `${composeProject(id, directory)}-trainer`, "trainer"], signal); reason = "exited"; } finally { - await stopComposeTrainer(directory, id, image, signal?.aborted === true ? "cancelled" : reason); + try { await stopComposeTrainer(directory, id, image, signal?.aborted === true ? "cancelled" : reason); } + catch (error) { onCleanupFailure(error); } } } diff --git a/docs/process-trainer.md b/docs/process-trainer.md index 56ffbb53..aa211e73 100644 --- a/docs/process-trainer.md +++ b/docs/process-trainer.md @@ -217,6 +217,13 @@ publication and before the PM materialization commit therefore trains only once. A cancelled fit remains terminal. Transfer staging explicitly before resuming in a relocated workspace; active containers belong to their original local location. +Cleanup failure is recorded independently as `cleanup: { code: "cleanup_failed", error }` +in the durable process job body and transition history. It preserves a validated +completion, cancellation outcome, or original trainer error. The diagnostic is +bounded to 4,096 characters and replaces the job staging path with ``. +Recovery uses the same separate diagnostic path; retained artifacts remain +available for inspection when cleanup could not finish. + ### Real container drills Run `node --test test/compose.test.ts` from a source checkout. Its tiny local @@ -253,7 +260,14 @@ coverage scope or threshold is changed by those proofs. ### Compose release validation -Both `npm run release:check` and `bun run release:check` pass **578/578 tests**, +Review round 1 adds four real Compose cleanup regressions: cancellation, success, +trainer failure and recovery of completed staging. An executable wrapper fails +only `compose down`; container launch, training, kill and removal remain real. +Reverting only launch cleanup handling fails the three primary-outcome cases; +reverting only recovery cleanup handling fails the recovery case. Fixed code +passes 4/4 through the linked PM snapshot run and both complete release gates. + +Both `npm run release:check` and `bun run release:check` pass **582/582 tests**, with zero failures, skips, cancellations or todo cases. Each gate measures exact **100/100/100/100** statement/line/branch/function coverage across all **28** authored sources, **266** documented declarations, zero duplication and zero @@ -263,7 +277,9 @@ coverage scope and all thresholds are unchanged. One earlier npm attempt missed the existing packed LM CLI bound: the measured CLI took 22.138 seconds against its unchanged 20-second limit. The same case measured 9.038 seconds in the passing full npm gate and 12.907 seconds in the -passing full Bun gate. The separate test setup time is not the timed CLI interval. +passing full Bun gate. Review round 1 measured 12.623 seconds (npm) and 12.272 +seconds (Bun), also within the unchanged bound. The separate test setup time is +not the timed CLI interval. [pm-rl-7c7m](../.agents/pm/tasks/pm-rl-7c7m.toon) tracks that existing variability. ## Two-generation packed CLI measurement diff --git a/process.ts b/process.ts index e7e2a886..a4817537 100644 --- a/process.ts +++ b/process.ts @@ -96,6 +96,8 @@ export interface ProcessJob { readonly completion: ProcessCompletion | null; /** Recorded terminal outcome or phase description. */ readonly outcome: string; + /** Cleanup diagnostics retained independently of the trainer outcome and validated completion. */ + readonly cleanup?: { readonly code: "cleanup_failed"; readonly error: string }; } /** Workspace coordinates for the SDK transaction journal. */ export interface ProcessCoordinates { @@ -398,25 +400,31 @@ async function executeReservedProcessJob(client: PmClient, coordinates: ProcessC const policy = verifiedProcessPolicy(job, coordinates.pmRoot, input); if (policy !== null) return transitionProcessJob(client, coordinates, job, job.state, job.outcome); const directory = processStaging(coordinates.pmRoot, id); + const diagnostics: { cleanup?: ProcessJob["cleanup"] } = {}; + /** Attach bounded cleanup evidence to the same durable settlement, without exposing the staging path. */ + const cleanupFailed = (error: unknown): void => { diagnostics.cleanup = { code: "cleanup_failed", error: String(error).replaceAll(directory, "").slice(0, 4096) }; }; try { mkdirSync(directory, { recursive: true }); if (job.state === "planned") { job = await transitionProcessJob(client, coordinates, job, "running", "launched"); - if (adapter.kind === "compose-bandit") await launchComposeTrainer(directory, id, input, job.allowance, adapter.image, signal); + if (adapter.kind === "compose-bandit") await launchComposeTrainer(directory, id, input, job.allowance, adapter.image, cleanupFailed, signal); else await launchTrainer(directory, id, input, job.allowance, argv, signal); } else { - if (adapter.kind === "compose-bandit") await stopComposeTrainer(directory, id, adapter.image, "recovery"); + if (adapter.kind === "compose-bandit") { + try { await stopComposeTrainer(directory, id, adapter.image, "recovery"); } + catch (error) { cleanupFailed(error); } + } else if (existsSync(join(directory, "runner.json"))) { const runner = JSON.parse(readFileSync(join(directory, "runner.json"), "utf8")) as { pid: number; start: string }; stopTrainer(runner.pid, runner.start); } } const completion = validateProcessCompletion(directory, id, input, job.allowance); - return await transitionProcessJob(client, coordinates, job, "materialized", "receipt_validated", completion, completion.usage); + return await transitionProcessJob(client, coordinates, { ...job, ...diagnostics }, "materialized", "receipt_validated", completion, completion.usage); } catch (error) { const state = signal?.aborted === true ? "cancelled" : "failed"; const charge = job.state === "planned" ? zeroResources() : failedProcessCharge(directory, job.allowance); - await transitionProcessJob(client, coordinates, job, state, isPmCliExpectedError(error) ? String(error.context.code) : "trainer_failure", null, charge); + await transitionProcessJob(client, coordinates, { ...job, ...diagnostics }, state, isPmCliExpectedError(error) ? String(error.context.code) : "trainer_failure", null, charge); throw error; } } diff --git a/test/compose.test.ts b/test/compose.test.ts index c2eecdc8..2347dee2 100644 --- a/test/compose.test.ts +++ b/test/compose.test.ts @@ -2,12 +2,12 @@ import assert from "node:assert/strict"; import { execFileSync, fork, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { setTimeout } from "node:timers/promises"; import { test } from "node:test"; -import { composeConfiguration, composeProject, stopComposeTrainer } from "../compose.ts"; +import { composeConfiguration, composeProject, launchComposeTrainer, stopComposeTrainer } from "../compose.ts"; import { resumeRlLoop, rlLoopStatus } from "../index.ts"; -import { executeProcessTrainer, listProcessJobs, parseProcessAdapter, processJobId, processStaging, readProcessJob, reserveProcessJob, type ProcessAdapter } from "../process.ts"; +import { executeProcessTrainer, listProcessJobs, parseProcessAdapter, processJobId, processStaging, readProcessJob, reserveProcessJob, transitionProcessJob, type ProcessAdapter } from "../process.ts"; import { configuration, processWorkspace, trainingInput } from "./fixtures/process-workspace.ts"; // SIGKILL-able controllers and trainers must not leave truncated V8 reports. @@ -198,3 +198,57 @@ test("real Compose checkpoint without completion is refused and cannot collect a assert.equal(existsSync(join(directory, "completion.json")), false); assert.equal((await client.listAllComplete({})).items.filter((item) => item.type === "Run").length, 1); removed(job.id, directory); }); + +/** Run the real Docker executable except for a deterministic failure at the cleanup exec boundary. */ +async function cleanupFailure(mode: string, cancelled = false, recovery = false): Promise { + const { client, coordinates, approval, pmRoot, root } = await processWorkspace(budget); + const selected = adapter(mode); const input = trainingInput(); const id = processJobId(input, undefined, selected); + const directory = processStaging(pmRoot, id); const abort = new AbortController(); + assert.equal(selected.kind, "compose-bandit"); + if (selected.kind !== "compose-bandit") assert.fail("Expected Compose adapter"); + if (recovery) { + const job = await reserveProcessJob(client, coordinates, id, input, approval, allowance); + await transitionProcessJob(client, coordinates, job, "running", "launched"); + mkdirSync(directory, { recursive: true }); + await launchComposeTrainer(directory, id, input, allowance, selected.image, () => assert.fail("Unexpected cleanup failure")); + } + const docker = execFileSync("which", ["docker"], { encoding: "utf8" }).trim(); + const bin = join(root, "bin"); mkdirSync(bin); + writeFileSync(join(bin, "docker"), `#!/bin/sh +case "$*" in + *" down --timeout 0") echo "injected Compose cleanup failure" >&2; exit 71 ;; +esac +exec '${docker.replaceAll("'", "'\\''")}' "$@" +`, { mode: 0o755 }); + const path = process.env["PATH"]; process.env["PATH"] = `${bin}${delimiter}${path}`; + try { + const run = executeProcessTrainer(client, coordinates, input, approval, selected, abort.signal); + const result = mode === "fit" ? run : assert.rejects(run, cancelled ? /Trainer cancelled/ : /Trainer exited unsuccessfully/); + if (mode === "hang") { + await partial(directory); + if (cancelled) abort.abort(); + else execFileSync("docker", ["kill", `${composeProject(id, directory)}-trainer`], { stdio: "ignore" }); + } + await result; + const job = (await readProcessJob(client, id))!; + assert.equal(job.state, mode === "fit" ? "materialized" : cancelled ? "cancelled" : "failed"); + assert.equal(job.outcome, mode === "fit" ? "receipt_validated" : cancelled ? "process_cancelled" : "trainer_failure"); + assert.equal(job.settled, true); assert.equal(job.cleanup!.code, "cleanup_failed"); + assert.match(job.cleanup!.error, /injected Compose cleanup failure/); + assert.equal(job.cleanup!.error.includes(directory), false); + assert.equal(job.completion !== null, mode === "fit"); + const history = readFileSync(join(pmRoot, "history", `rl-${id}.jsonl`), "utf8"); + assert.ok(history.includes("cleanup_failed")); assert.ok(history.includes(job.outcome)); + if (mode === "fit") assert.deepEqual(await executeProcessTrainer(client, coordinates, input, approval, selected), job); + removed(id, directory); + } finally { + process.env["PATH"] = path; + await stopComposeTrainer(directory, id, selected.image, "test-cleanup"); + } +} + +test("real Compose cancellation plus cleanup failure preserves process_cancelled and a durable diagnostic", { skip }, async () => { await cleanupFailure("hang", true); }); +test("real Compose success plus cleanup failure preserves completion and a durable diagnostic", { skip }, async () => { await cleanupFailure("fit"); }); +test("real Compose trainer failure plus cleanup failure preserves the original trainer error", { skip }, async () => { await cleanupFailure("hang"); }); + +test("real Compose recovery plus cleanup failure preserves completed staging and a durable diagnostic", { skip }, async () => { await cleanupFailure("fit", false, true); }); From 874944e2d457287bc344fc82f9f54d2dd7cfdf8f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:36:26 +0200 Subject: [PATCH 3/3] Align the Compose cleanup paragraph with the separate cleanup diagnostic --- .agents/pm/history/pm-rl-mjo5.jsonl | 1 + .agents/pm/tasks/pm-rl-mjo5.toon | 5 +++-- docs/process-trainer.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.agents/pm/history/pm-rl-mjo5.jsonl b/.agents/pm/history/pm-rl-mjo5.jsonl index 8a75f5ac..79744a41 100644 --- a/.agents/pm/history/pm-rl-mjo5.jsonl +++ b/.agents/pm/history/pm-rl-mjo5.jsonl @@ -30,3 +30,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-15T20:37:13.115Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/11","value":{"created_at":"2026-10-15T20:37:13.115Z","author":"codex-agent","text":"Review round 1 npm release gate PASS: npm run release:check under isolated CI Git/home settings passed 582/582 tests, zero fail/skip/cancel/todo. Coverage remains exact 100/100/100/100 statement/line/branch/function across 28 authored sources; 266 documented declarations, zero duplication and production audit findings; identity, attestation, packaging and changelog checks passed. All 15 real Compose tests passed. Existing LM CLI timing measured 12623 ms within the unchanged 20000 ms bound. Running the unchanged full Bun gate next."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T20:37:13.115Z"}],"before_hash":"091bff1b7f00f97cb96b7923d84ca081d1fe2fe7861011e288df54290730dc11","after_hash":"b519896010d5603276f1a19115e8dfe0bd3e78825efb86dd65dc15b9c3a8237c","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"51494a8107171793a1d830287b89f9024063441adfd5f6a7e72093fc9356db68"} {"hash_algorithm":"sha256","ts":"2026-10-15T21:03:59.855Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/12","value":{"created_at":"2026-10-15T21:03:59.855Z","author":"codex-agent","text":"Review round 1 final verdict: finding confirmed and fixed. Both npm run release:check and bun run release:check PASS 582/582 tests each, zero fail/skip/cancel/todo, unchanged exact 100/100/100/100 statement/line/branch/function coverage across 28 sources and 266 documented declarations. Zero duplication and production audit findings; identity, attestation, packaging and changelog checks pass under isolated CI Git/home settings. All 15 real Compose cases pass in both gates; 4/4 new cleanup regressions also pass via the linked PM snapshot test. Behavioural revert proofs (3/3 launch cases and 1/1 recovery case fail when only the relevant behavior is reverted) are recorded above. Gated source hashes match. Packed LM CLI measured 12623 ms npm and 12272 ms Bun within the unchanged 20000 ms bound. Cleanup failure remains visible as cleanup_failed and can require inspection of retained job resources; existing timing and kernel quota follow-ups remain unchanged. No new PM CLI/SDK defect encountered; test result tracking is disabled by project policy. Leaving the item open for orchestrator verification and releasing the claim."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T21:03:59.855Z"}],"before_hash":"b519896010d5603276f1a19115e8dfe0bd3e78825efb86dd65dc15b9c3a8237c","after_hash":"38902a0075e18c4ef8a1cc1157183b710fffba4e91f2947462e1ced609570404","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"bc7372332c9ef36072e7f21119ca2a5eb31efa3971209b28662a10cc91b8019b"} {"hash_algorithm":"sha256","ts":"2026-10-15T21:04:01.562Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"7d3fa6b9c2cdba64d2aba754","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T21:04:01.562Z"}],"before_hash":"38902a0075e18c4ef8a1cc1157183b710fffba4e91f2947462e1ced609570404","after_hash":"21d8d66684281a416c5c84fe244d704d01d3a9d0a762fb9c267e6aaa937c52e9","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"effort":{"status":"resolved","resolver":"codex_session_file","rule_version":"v1"},"version":{"status":"unavailable","reason":"harness_unavailable","resolver":"ai_agent_version","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"2d1c36c50b604f84b344de63a2736bbec58b629a7f655501f266e5c1c7a56038"} +{"hash_algorithm":"sha256","ts":"2026-10-15T21:36:08.213Z","author":"claude-orchestrator","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"cd687996088c07eef02d762c","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.301","source":"probe"}},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/13","value":{"created_at":"2026-10-15T21:36:08.213Z","author":"claude-orchestrator","text":"Review round 2 (CodeRabbit thread on docs/process-trainer.md:211): the Compose cleanup paragraph said a failed cleanup refuses progression, contradicting the cleanup-diagnostic section and executeReservedProcessJob (cleanup_failed is recorded separately and preserves a validated completion, cancellation or trainer error). Sentence replaced so both sections agree; doc-only change."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-15T21:36:08.213Z"}],"before_hash":"21d8d66684281a416c5c84fe244d704d01d3a9d0a762fb9c267e6aaa937c52e9","after_hash":"455def3024a63f7632426fa1858e214ce4b9a23c4ae14b2a30be18f072869dec","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"22694672ace89397d43a84673d7f19026745da2ae26dda452ae868f2ad0784c8"} diff --git a/.agents/pm/tasks/pm-rl-mjo5.toon b/.agents/pm/tasks/pm-rl-mjo5.toon index 7d0521de..909e62ff 100644 --- a/.agents/pm/tasks/pm-rl-mjo5.toon +++ b/.agents/pm/tasks/pm-rl-mjo5.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags: [] created_at: "2026-10-14T19:46:39.492Z" -updated_at: "2026-10-15T21:04:01.562Z" +updated_at: "2026-10-15T21:36:08.213Z" author: codex-agent acceptance_criteria: "Compose execution preserves launch ownership and process contract, retains staging on failure, and passes real reservation and recovery drills" parent: pm-rl-apvf -comments[13]{created_at,author,text}: +comments[14]{created_at,author,text}: "2026-10-15T10:58:38.098Z",codex-agent,"Design: reuse the process job fence, reservation transaction, receipt validator and settlement state machine. Add a trusted Compose launcher with a job-derived project identity and generated isolated staging configuration; mount only job staging and retain all protocol artifacts. Exercise real success, container death, cancellation, contention and two-generation successor collection. Behavioural revert proofs will remove only the relevant launcher or selection behaviour in an isolated snapshot." "2026-10-15T11:08:14.363Z",codex-agent,"Real Docker findings: a capability-restricted container must use the invoking numeric UID/GID to write host staging. Compose down/rm omit one-off run containers on the tested version, so cleanup resolves only the exact job-named container with matching project/service labels and removes its immutable daemon ID before project-scoped down. Project names also hash staging scope to isolate identical jobs in independent trackers. Receipt validation, conservative settled charges and controller fences remain shared with local execution." "2026-10-15T11:11:10.946Z",codex-agent,"Drill progress: launch/receipt/replay, container death, cancellation, two real controller contention, staged-completion SIGKILL recovery and orphan cleanup pass. The two-generation Compose run promoted both changed policies, and generation two collection identifies generation one policy. Kernel CPU/storage quotas are a distinct follow-up to the existing reported-use and polling contract. Full release gates run directly under isolated CI Git/home settings; pm test --run --progress passed the linked typecheck, but project policy reports tracking_disabled so durable gate results are recorded in comments." @@ -24,6 +24,7 @@ comments[13]{created_at,author,text}: "2026-10-15T20:10:04.639Z",codex-agent,"Behavioural revert proof, final fixtures: in an isolated source snapshot, revert only launchComposeTrainer cleanup handling to its original throwing finally (retain the callback signature so the caller still compiles). node --test --test-name-pattern=\"(cancellation|success|trainer failure) plus cleanup failure\" test/compose.test.ts fails 3/3: cancellation and trainer error are replaced by the injected Compose down error; successful completion rejects. Separately reverting only the recovery cleanup catch in executeReservedProcessJob fails its recovery regression 1/1. Restored code passes all 4/4 through pm test --run --only-last --progress. Real Docker launch/train/kill/remove and real temporary SDK trackers run in every case; only compose down fails through an executable boundary wrapper. Test result tracking is disabled by project policy, so this comment preserves the results." "2026-10-15T20:37:13.115Z",codex-agent,"Review round 1 npm release gate PASS: npm run release:check under isolated CI Git/home settings passed 582/582 tests, zero fail/skip/cancel/todo. Coverage remains exact 100/100/100/100 statement/line/branch/function across 28 authored sources; 266 documented declarations, zero duplication and production audit findings; identity, attestation, packaging and changelog checks passed. All 15 real Compose tests passed. Existing LM CLI timing measured 12623 ms within the unchanged 20000 ms bound. Running the unchanged full Bun gate next." "2026-10-15T21:03:59.855Z",codex-agent,"Review round 1 final verdict: finding confirmed and fixed. Both npm run release:check and bun run release:check PASS 582/582 tests each, zero fail/skip/cancel/todo, unchanged exact 100/100/100/100 statement/line/branch/function coverage across 28 sources and 266 documented declarations. Zero duplication and production audit findings; identity, attestation, packaging and changelog checks pass under isolated CI Git/home settings. All 15 real Compose cases pass in both gates; 4/4 new cleanup regressions also pass via the linked PM snapshot test. Behavioural revert proofs (3/3 launch cases and 1/1 recovery case fail when only the relevant behavior is reverted) are recorded above. Gated source hashes match. Packed LM CLI measured 12623 ms npm and 12272 ms Bun within the unchanged 20000 ms bound. Cleanup failure remains visible as cleanup_failed and can require inspection of retained job resources; existing timing and kernel quota follow-ups remain unchanged. No new PM CLI/SDK defect encountered; test result tracking is disabled by project policy. Leaving the item open for orchestrator verification and releasing the claim." + "2026-10-15T21:36:08.213Z",claude-orchestrator,"Review round 2 (CodeRabbit thread on docs/process-trainer.md:211): the Compose cleanup paragraph said a failed cleanup refuses progression, contradicting the cleanup-diagnostic section and executeReservedProcessJob (cleanup_failed is recorded separately and preserves a validated completion, cancellation or trainer error). Sentence replaced so both sections agree; doc-only change." files[7]{path,scope}: compose.ts,project index.ts,project diff --git a/docs/process-trainer.md b/docs/process-trainer.md index aa211e73..56e0dce5 100644 --- a/docs/process-trainer.md +++ b/docs/process-trainer.md @@ -208,7 +208,8 @@ container removal matters because Compose `down` and `rm` omit one-off `run` containers on supported hosts. Cleanup never removes images or host artifacts. A fsynced `compose-stop.json` records job, project and exit/cancellation/recovery reason. The job Task and append-only history contain the durable failure or -cancellation receipt and settled charge. A failed cleanup refuses progression. +cancellation receipt and settled charge. A failed cleanup never changes that outcome: +it is recorded as the separate `cleanup` diagnostic described below. Recovery never launches a previously running job. It stops any surviving owned container, validates staged completion, then either adopts the original checkpoint