From 90b96af316fd8480941c941c7fcc45deb436f825 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:58:03 +0200 Subject: [PATCH 01/10] feat: stream certified native context with resource-admitted sessions --- .../pm-rust-bounded-read-sessions.toon | 32 ++ .../pm-rust-bounded-read-sessions.jsonl | 8 + .github/workflows/ci.yml | 13 +- Cargo.toml | 2 +- README.md | 29 +- docs/BOUNDED_READ_SESSIONS.md | 78 +++ examples/read_session.rs | 19 + justfile | 6 +- package-lock.json | 12 + package.json | 10 + scripts/read-session-validate.mts | 113 +++++ src/lib.rs | 6 + src/main.rs | 68 +++ src/read_session.rs | 454 ++++++++++++++++++ src/workspace.rs | 16 +- tests/read_session_contract.rs | 148 ++++++ tests/release_pipeline_contract.rs | 31 ++ tests/support/read_session_unit.rs | 353 ++++++++++++++ 18 files changed, 1389 insertions(+), 9 deletions(-) create mode 100644 .agents/pm/features/pm-rust-bounded-read-sessions.toon create mode 100644 .agents/pm/history/pm-rust-bounded-read-sessions.jsonl create mode 100644 docs/BOUNDED_READ_SESSIONS.md create mode 100644 examples/read_session.rs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/read-session-validate.mts create mode 100644 src/read_session.rs create mode 100644 tests/read_session_contract.rs create mode 100644 tests/support/read_session_unit.rs diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon new file mode 100644 index 0000000..a589e7c --- /dev/null +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -0,0 +1,32 @@ +id: pm-rust-bounded-read-sessions +title: Stream certified native reads under caller resource limits +description: "Embeddable bounded read sessions for non-Node hosts, reusing existing native query and output contracts." +type: Feature +status: in_progress +priority: 2 +tags[3]: embedding,resources,sdk +created_at: "2026-10-23T14:33:11.991Z" +updated_at: "2026-10-23T14:48:34.131Z" +assignee: codex-agent +claim_principal: codex-agent +author: codex-agent +acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." +parent: pm-rust-u4al +comments[2]{created_at,author,text}: + "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." + "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." +files[6]{path,scope}: + scripts/read-session-validate.mts,project + src/main.rs,project + src/read_session.rs,project + src/workspace.rs,project + tests/read_session_contract.rs,project + tests/support/read_session_unit.rs,project +tests[2]{command,scope,provenance{author,created_at,source_kind,source_ref}}: + "cargo test --locked --lib read_session::tests -- --test-threads=1",project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions + cargo test --locked --test read_session_contract -- --test-threads=1,project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions +docs[3]{path,scope}: + docs/BOUNDED_READ_SESSIONS.md,project + examples/read_session.rs,project + README.md,project +body: "" diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl new file mode 100644 index 0000000..263e9ee --- /dev/null +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -0,0 +1,8 @@ +{"hash_algorithm":"sha256","ts":"2026-10-23T14:33:11.991Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-rust-bounded-read-sessions"},{"op":"add","path":"/metadata/title","value":"Stream certified native reads under caller resource limits"},{"op":"add","path":"/metadata/description","value":"Embeddable bounded read sessions for non-Node hosts, reusing existing native query and output contracts."},{"op":"add","path":"/metadata/type","value":"Feature"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["embedding","resources","sdk"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-23T14:33:11.991Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-23T14:33:11.991Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates."},{"op":"add","path":"/metadata/parent","value":"pm-rust-u4al"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"914f3d6ddfeb1113b79a7d9a71163c68c11f5d07f525bbc11ca1c4ff19e41c56","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":"b2a6deeb13ad38dc495276c88023bdbdbf38e20a58a6005e0b859fcd033f3238"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:33:19.148Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:33:19.148Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"914f3d6ddfeb1113b79a7d9a71163c68c11f5d07f525bbc11ca1c4ff19e41c56","after_hash":"645c08e7524d4c942346f2478531cca486faa099af2e96405a1f8ba02fe38fa7","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":"49dbd8a9575bddf980b320624f1918764402411cff322272519a5875e38178b9"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:33:28.329Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:33:28.329Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"645c08e7524d4c942346f2478531cca486faa099af2e96405a1f8ba02fe38fa7","after_hash":"86ac415cf8b5bf96e166c7267d2f1813f0b5c93f7e771758c759a21de12c3f44","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":"acfefa1e4be9493e166e141bbd1dc6faa20cd779ce5146daf93a24c44794cb60"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:33:35.077Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:33:35.077Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-23T14:33:35.077Z","author":"codex-agent","text":"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness."}]}],"before_hash":"86ac415cf8b5bf96e166c7267d2f1813f0b5c93f7e771758c759a21de12c3f44","after_hash":"e6230e2dc3a06590eb34e7ad65718412f771a3de9121f97278c6d8e46859aa4a","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":"588695cc9a655f01c823dd51452b42dc4150ead01a6a7cfa6d347af53eb5ecdf"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:44:13.698Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/1","value":{"created_at":"2026-10-23T14:44:13.698Z","author":"codex-agent","text":"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:44:13.698Z"}],"before_hash":"e6230e2dc3a06590eb34e7ad65718412f771a3de9121f97278c6d8e46859aa4a","after_hash":"7c29acde36b964c8d05d0a3cee1be33d568a2ec798b1f862949f683337daf5c1","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":"7938bb3299b289837309db5110b53a1579973a53ee6e75dcc81dc8adf2b81d90"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:48:04.952Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:48:04.952Z"},{"op":"add","path":"/metadata/files","value":[{"path":"scripts/read-session-validate.mts","scope":"project"},{"path":"src/main.rs","scope":"project"},{"path":"src/read_session.rs","scope":"project"},{"path":"src/workspace.rs","scope":"project"},{"path":"tests/read_session_contract.rs","scope":"project"},{"path":"tests/support/read_session_unit.rs","scope":"project"}]}],"before_hash":"7c29acde36b964c8d05d0a3cee1be33d568a2ec798b1f862949f683337daf5c1","after_hash":"f9184722f2747bc5a4ab4c97473275d7a100ccc40906729df1702ad55e8cdeba","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":"063d06db195c8b933374e52f62a5f47033c4a9eb0bc75228411d9dbf94a26a54"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:48:17.407Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:48:17.407Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/BOUNDED_READ_SESSIONS.md","scope":"project"},{"path":"examples/read_session.rs","scope":"project"},{"path":"README.md","scope":"project"}]}],"before_hash":"f9184722f2747bc5a4ab4c97473275d7a100ccc40906729df1702ad55e8cdeba","after_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","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":"0f24893896619eacb2c12cc0cf76c98d6120641f02b1fb7c85605999e8056c6d"} +{"hash_algorithm":"sha256","ts":"2026-10-23T14:48:34.131Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:48:34.131Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"cargo test --locked --lib read_session::tests -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}},{"command":"cargo test --locked --test read_session_contract -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}]}],"before_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","after_hash":"c69c209ff908733b8d67c2da820045b8acb6e13181e6fd4ada9930580010803f","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":"47c7e4de6b7c3943867a0570d222185103caedd34fb55564b89b56a70732e45c"} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ada3fc0..fe90ac1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,10 +128,16 @@ jobs: include: - os: ubuntu-latest gate: Release gate aggregate + runtime: npm - os: macos-latest gate: Release gate aggregate macos-latest + runtime: npm - os: windows-latest gate: Release gate aggregate windows-latest + runtime: npm + - os: ubuntu-latest + gate: Release gate aggregate bun + runtime: bun defaults: run: shell: bash @@ -180,8 +186,13 @@ jobs: npm ci --prefix tests/oracle --ignore-scripts oracle_root=$(node -p "require('node:path').resolve('tests/oracle/node_modules/@unbrained/pm-cli')") echo "PM_NODE_CLI=$oracle_root" >> "$GITHUB_ENV" + - name: Install Bun for the Bun aggregate + if: matrix.runtime == 'bun' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: latest - name: Run release:check aggregate gate - run: just release-check + run: ${{ matrix.runtime }} run release:check - name: Install clone-local pm merge drivers run: node tests/oracle/launch.mts merge install diff --git a/Cargo.toml b/Cargo.toml index 33c742c..7700594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ serde_json = { version = "1.0.145", features = ["preserve_order", "raw_value"] } sha2 = "0.11.0" thiserror = "2.0.17" time = { version = "0.3.44", features = ["formatting", "parsing"] } +tempfile = "3.23.0" toon-format = { version = "0.7.0", default-features = false } [dev-dependencies] @@ -36,7 +37,6 @@ assert_cmd = "2.0.17" predicates = "3.1.3" proptest = "1.9.0" serde_yaml_ng = "0.10.0" -tempfile = "3.23.0" [target.'cfg(windows)'.dependencies] same-file = "1.0.6" diff --git a/README.md b/README.md index 489eda0..196dfa0 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,31 @@ Native item mutations recover pending journals across operation names before applying later edits. See [journal recovery](docs/CROSS_OPERATION_JOURNAL_RECOVERY.md) for crash-state regressions and operator conflict remedies. +## Embeddable bounded read sessions + +`Workspace::read_session` freezes a private disk-backed snapshot, strictly decodes +all items and checks unique identities before serving any context. Native hosts +pull `SessionPage` values with backpressure; no Node, Bun, Git or background worker +is required. The session reuses canonical list pagination, lifecycle registries, +output budgeting and lossless JSON/TOON codecs. Its receipt certifies the admitted +snapshot and completion of **brief projected context**, not full-field delivery. + +```sh +cargo run -- read-session --memory-bytes 536870912 --page-items 20 --output-tokens 4000 --json +cargo run --example read_session -- path/to/project +``` + +JSON emits one compact `{output,receipt}` frame per line. Default TOON emits a +receipt and canonical page followed by `---`. An optional `--cancel-file` marker +requests cancellation before admission or between pages; removing it allows a new +CLI invocation. SDK callers can share `SessionCancellation` across threads or call +`ReadSession::cancel()` for immediate cleanup while idle. Drop and terminal pulls +remove the snapshot. See the [embedding contract](docs/BOUNDED_READ_SESSIONS.md) for +accounting limits, cancellation latency, integrity boundaries and typed remedies. + ## Without git -Workspace discovery, list/get/search/context/history/dependencies, item mutations, +Read sessions and workspace discovery, list/get/search/context/history/dependencies, item mutations, ownership and linked artifacts work in plain directories. Concurrent writers share the same tracker and retain every admitted item edit and append-only history entry. The multi-process write contract tests 50 writers against one such tracker without @@ -161,8 +183,9 @@ evidence about refs, not about the whole upstream object database. ### Changelog and release tooling -Since pm-rust has no root `package.json`, changelog and release-note targets are -exposed via a [`justfile`](justfile): +The root `package.json` exposes `npm run release:check` and +`bun run release:check` aliases for the unchanged native gate. Changelog and +release-note targets remain available through the [`justfile`](justfile): ```bash just changelog # prepend new release section to CHANGELOG.md diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md new file mode 100644 index 0000000..632b00c --- /dev/null +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -0,0 +1,78 @@ +# Bounded native read sessions + +The v1 embedding surface is `Workspace::read_session`, `ReadSessionLimits`, +`ReadSession::next_page`, `ReadSession::receipt`, `ReadSession::cancel`, +`SessionCancellation`, `SessionPage`, `SessionReceipt` and `SessionRefusal`. +The [runnable Rust host](../examples/read_session.rs) prints each canonical page +and then drops it. Applications retain ownership of returned pages and cancellation +handles; accumulating pages consumes application memory outside session accounting. + +A session captures settings, the configured status/workflow schema files, and +regular TOON items in a private temporary directory. It validates each **complete** +item with the existing strict decoder and rejects duplicate IDs before delivery. +It uses `Workspace::list_page` unchanged with all statuses, brief projection, +caller page size and the existing output token budget. Producer and output +continuations retain the original tracker query identity. JSON overrides preserve +unpaired UTF-16 compaction cuts; the existing TOON codec can refuse those cuts. +The original tracker is never written. Git, extensions, JavaScript and telemetry +are absent from the native path. The package's pinned oracle is unchanged. + +The snapshot is an immutable capture of individually read files, not a database +transaction or an atomic point-in-time view across concurrent source writers. +Its SHA-256 commits to the deterministic sequence of relative names, byte lengths +and admitted bytes. It certifies validated source items, including terminal items; +`complete` becomes true only on the final delivered page. Field compaction and +brief-field omissions remain explicit in each canonical page. This is not the +SDK's unbounded full-field `complete_list` certificate. An interrupted or refused +session must never be promoted to a complete context read. + +The memory ceiling is an **admission accounting contract**, not an operating-system +RSS limiter. Admission reserves 8 MiB plus 256 bytes per output token, then 128 +bytes per source byte, 8 KiB plus 16 bytes per relative filename byte per copied +file, and 1 KiB per enumerated directory entry. The cumulative ledger conservatively +covers source decoding, indexes and rendering; it refuses before reading a file +that cannot fit. Source reads are limited to the admitted length plus one byte +and refuse length drift. Directory enumeration charges before retaining entries; +depth is limited to 32. The private disk snapshot occupies at most the admitted +source bytes, apart from filesystem metadata. No full source corpus is retained +between pulls. Each pull still scans the admitted snapshot through the existing +reader; v1 adds bounded admission and embedding ownership, not a new query engine. + +Allocator overhead, stack size, codec implementation, operating system and host +application allocations affect actual RSS. The validation harness separately +measures child peak RSS against the declared ceiling on large synthetic trackers. +Hosts requiring a hard process-wide physical limit must additionally use their +operating system's process/container limits. Accounting coefficients and the +measured platform envelope are explicit so a reservation cannot be mistaken for +an allocator-enforced universal bound. + +Cancellation is cooperative: admission checks between directory entries and file +reads, and delivery checks before and after each canonical page query. It cannot +interrupt a synchronous decoder/query already running. A shared token wakes no +background task: call `next_page` after cancellation, call `cancel` while idle, +or drop the session to remove private storage. A successful final page also +removes storage. Process termination that bypasses Rust destructors is outside +this lifecycle guarantee. The CLI's cancel marker is polled before admission and +at page boundaries; it is not an OS signal handler. + +| Refusal code | Caller remedy | +| --- | --- | +| `session_cancelled` | Reopen with a fresh cancellation token | +| `session_memory_limit` | Increase the admitted byte ceiling or use a smaller tracker | +| `session_invalid_limits` | Use positive controls; row/token controls are at most 1,000,000 | +| `session_unsupported_source` | Use regular files, at most 32 directory levels, tracker-relative schema files | +| `session_source_refused` | Repair tracker integrity or increase the per-page token budget | +| `session_storage_failed` | Check temporary storage permissions and free space | + +All failures terminate delivery and release admitted snapshot resources. Refusals +are typed in Rust and emitted as compact structured JSON on CLI stderr with exit +code 2. An output writer failure drops the session through ordinary Rust unwinding. +The source tracker remains unchanged after cancellation and failed admission. + +Reproduce the non-git public-copy and synthetic measurements with +`node scripts/read-session-validate.mts --source path/to/public-tracker-copy`. +The harness uses the real SDK's complete-list certificate and bounded list calls +as independent oracles, initializes its synthetic tracker through the SDK, checks +all delivered identities and bytes, and measures fresh native child peak RSS with +GNU time. Only aggregate numbers and hashes are reported; source contents and +scratch paths are excluded. diff --git a/examples/read_session.rs b/examples/read_session.rs new file mode 100644 index 0000000..2f2566d --- /dev/null +++ b/examples/read_session.rs @@ -0,0 +1,19 @@ +//! Pull certified native context pages without loading the SDK into Node. +use pm_rust::{ReadSessionLimits, SessionCancellation, Workspace}; +use std::{error::Error, path::Path}; + +/// Opens a caller-selected tracker and discards each page after processing it. +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).ok_or("pass a tracker directory")?; + let workspace = Workspace::discover(Path::new(&path))?; + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + &pm_rust::current_timestamp(), + )?; + while let Some(page) = session.next_page()? { + print!("{}", page.output.stringify_read_json()); + eprintln!("{}", serde_json::to_string(&page.receipt)?); + } + Ok(()) +} diff --git a/justfile b/justfile index 03baca2..243c8c9 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,8 @@ # pm-rust release and changelog targets. # -# Since pm-rust has no package.json, these justfile recipes expose the -# pm-changelog tooling that every other fleet package provides via npm -# scripts. They are runnable in CI and documented in the README. +# These native recipes expose pm-changelog and the Rust aggregate gate. +# The root package.json provides npm and Bun aliases without replacing any +# native checks. Recipes are runnable in CI and documented in the README. # # The `--date` parameter is fixed so the generated changelog heading does not # depend on the clock. This prevents CI from failing on a random day in a diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7843c86 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "pm-rust-development", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pm-rust-development", + "version": "0.0.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd4681b --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "pm-rust-development", + "private": true, + "version": "0.0.0", + "scripts": { + "release:check": "just release-check", + "changelog:full": "just changelog-full", + "changelog:check": "just changelog-check" + } +} diff --git a/scripts/read-session-validate.mts b/scripts/read-session-validate.mts new file mode 100644 index 0000000..66785eb --- /dev/null +++ b/scripts/read-session-validate.mts @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { PmClient, certifyCompleteListResult, serializeItemDocument } from '../tests/oracle/node_modules/@unbrained/pm-cli/dist/sdk/index.js'; +import type { ItemDocument } from '../tests/oracle/node_modules/@unbrained/pm-cli/dist/sdk/index.js'; + +interface Receipt { protocol: string; source_items: number; source_bytes: number; admitted_memory_bytes: number; memory_ceiling_bytes: number; pages: number; complete: boolean; snapshot_sha256: string } +interface Frame { output: { items: { id: string }[]; next_cursor?: string; continuation_kind?: string }; receipt: Receipt } +const clock = '2026-10-21T12:00:00.000Z'; +const ceiling = 512 * 1024 * 1024; + +/** Read an explicit argument or a safe default. */ +function option(flag: string, fallback: string): string { + const index = process.argv.indexOf(flag); + return index < 0 ? fallback : (process.argv[index + 1] ?? fallback); +} + +/** Hash every source byte and relative filename, excluding disposable runtime caches. */ +function fingerprint(root: string): string { + const digest = createHash('sha256'); + /** Visit only the explicit copied tracker, never the host filesystem. */ + function visit(path: string, relative: string): void { + for (const entry of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + if (['runtime', 'search', 'locks'].includes(entry.name) && !relative) continue; + const name = join(relative, entry.name); + if (entry.isDirectory()) visit(join(path, entry.name), name); + else if (entry.isFile()) digest.update(name).update(readFileSync(join(path, entry.name))); + else throw new Error('Source copy must contain only regular files'); + } + } + visit(root, ''); + return digest.digest('hex'); +} + +/** Make the copied tracker read-only for native validation, then restore for cleanup. */ +function permissions(root: string, readonly: boolean): void { + for (const entry of readdirSync(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) permissions(path, readonly); + else chmodSync(path, readonly ? 0o400 : 0o600); + } + chmodSync(root, readonly ? 0o500 : 0o700); +} + +/** Certify complete oracle identities and compare every native page with the real SDK. */ +async function measure(root: string, label: string, binary: string, scratch: string): Promise> { + const tracker = join(root, '.agents/pm'); + const client = new PmClient({ cwd: root, pmRoot: tracker, noExtensions: true }); + const complete = certifyCompleteListResult(await client.listAllComplete()); + const before = fingerprint(tracker); + permissions(tracker, true); + const rssFile = join(scratch, `${label}-rss.txt`); + const started = performance.now(); + const result = spawnSync('/usr/bin/time', ['-f', '%M', '-o', rssFile, binary, '--workspace', root, 'read-session', '--memory-bytes', String(ceiling), '--page-items', '1000', '--output-tokens', '100000', '--json'], { + env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(root) }, maxBuffer: 64 * 1024 * 1024, + }); + const elapsedMs = performance.now() - started; + permissions(tracker, false); + assert.equal(result.status, 0, result.stderr.toString()); + assert.equal(fingerprint(tracker), before, 'Native source bytes must remain unchanged'); + const frames = result.stdout.toString().trim().split('\n').map(line => JSON.parse(line) as Frame); + const ids = frames.flatMap(frame => frame.output.items.map(row => row.id)); + assert.equal(new Set(ids).size, complete.complete_list.item_count); + assert.deepEqual([...ids].sort(), complete.items.map(row => row.id).sort()); + let after: string | undefined; + for (const frame of frames) { + const expected = await client.list({ excludeTerminal: false, brief: true, limit: '1000', outputBudget: '100000', ...(after ? { after } : {}) }); + assert.deepEqual(frame.output, JSON.parse(JSON.stringify(expected)), 'Native page must match the real SDK'); + after = frame.output.next_cursor; + } + const last = frames.at(-1); + assert.ok(last?.receipt.complete); + assert.equal(last.receipt.source_items, complete.complete_list.item_count); + assert.ok(last.receipt.admitted_memory_bytes <= ceiling); + const peakRssKiB = Number(readFileSync(rssFile, 'utf8').trim()); + assert.ok(peakRssKiB * 1024 <= ceiling, 'Measured child RSS exceeds declared memory ceiling'); + return { label, source_items: complete.complete_list.item_count, pages: frames.length, source_bytes: last.receipt.source_bytes, admitted_memory_bytes: last.receipt.admitted_memory_bytes, declared_ceiling_bytes: ceiling, peak_rss_kib: peakRssKiB, elapsed_ms: Math.round(elapsedMs), output_bytes: result.stdout.length, oracle_page_comparisons: frames.length, unchanged_source_sha256: before, snapshot_sha256: last.receipt.snapshot_sha256 }; +} + +/** Generate a genuine non-git tracker with the SDK, then validate a public copy. */ +async function main(): Promise { + const binary = resolve(option('--binary', 'target/debug/pm-rust')); + const source = option('--source', ''); + if (!source) throw new Error('--source requires an explicitly selected public tracker copy'); + const scratch = mkdtempSync(join(tmpdir(), 'pm-read-session-validation-')); + process.env.GIT_CEILING_DIRECTORIES = scratch; + try { + const synthetic = join(scratch, 'synthetic'); + mkdirSync(synthetic); + const client = new PmClient({ cwd: synthetic, pmRoot: join(synthetic, '.agents/pm'), noExtensions: true }); + await client.init('synthetic-', { defaults: true, agentGuidance: 'skip' }); + const count = Number(option('--items', '10000')); + assert.ok(Number.isSafeInteger(count) && count > 0); + const tasks = join(synthetic, '.agents/pm/tasks'); + mkdirSync(tasks, { recursive: true }); + for (let index = 0; index < count; index++) { + const id = `synthetic-${String(index).padStart(5, '0')}`; + const document: ItemDocument = { metadata: { id, title: `Synthetic item ${index}`, description: "", type: 'Task', status: index % 5 === 0 ? 'closed' : 'open', priority: 2, tags: [], created_at: clock, updated_at: clock }, body: '' }; + writeFileSync(join(tasks, `${id}.toon`), serializeItemDocument(document)); + } + const copied = join(scratch, 'public-copy'); + mkdirSync(join(copied, '.agents'), { recursive: true }); + cpSync(resolve(source), join(copied, '.agents/pm'), { recursive: true, filter: path => !['runtime', 'search', 'locks'].includes(path.split('/').at(-1) ?? '') }); + const results = [await measure(synthetic, 'synthetic', binary, scratch), await measure(copied, 'public-copy', binary, scratch)]; + console.log(JSON.stringify({ contract: 'pm-rust/read-session-validation/v1', results }, null, 2)); + } finally { rmSync(scratch, { recursive: true, force: true }); } +} + +await main(); diff --git a/src/lib.rs b/src/lib.rs index 6bd8df0..7e23246 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod ownership; mod pagination; mod read_output; mod read_projection; +mod read_session; mod search; mod selection; mod toon_output; @@ -53,3 +54,8 @@ pub use workspace::{ItemFilter, ListResult, Workspace}; /// Published canonical `pm` release used by this compatibility slice. pub const COMPATIBLE_PM_VERSION: &str = "2026.10.21"; + +pub use read_session::{ + ReadSession, ReadSessionLimits, SessionCancellation, SessionPage, SessionReceipt, + SessionRefusal, +}; diff --git a/src/main.rs b/src/main.rs index a2d01e7..4756a19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,24 @@ struct Cli { #[derive(Debug, Subcommand)] enum Command { + /// Stream a strictly validated immutable tracker snapshot under resource admission limits. + ReadSession { + /// Accounted working-set ceiling in bytes (allocator RSS is measured separately). + #[arg(long, default_value_t = 536_870_912)] + memory_bytes: u64, + /// Maximum canonical rows per delivered page. + #[arg(long, default_value_t = 20)] + page_items: usize, + /// Existing per-page renderer token ceiling. + #[arg(long, default_value_t = 4000)] + output_tokens: usize, + /// Cancel at a page boundary when this caller-owned marker exists. + #[arg(long)] + cancel_file: Option, + /// Emit one compact JSON frame per line instead of TOON documents. + #[arg(long)] + json: bool, + }, /// Read configured assurance declarations without changing the registry. Assurance { /// Read one declaration or discover configured IDs. @@ -572,6 +590,51 @@ mod tests; fn run(cli: Cli) -> Result<(), Box> { let workspace = Workspace::discover(&cli.workspace)?; match cli.command { + Command::ReadSession { + memory_bytes, + page_items, + output_tokens, + cancel_file, + json, + } => { + let cancellation = pm_rust::SessionCancellation::default(); + if cancel_file.as_ref().is_some_and(|path| path.exists()) { + cancellation.cancel(); + } + let limits = pm_rust::ReadSessionLimits { + memory_bytes, + page_items, + output_tokens, + toon: !json, + }; + let mut session = workspace.read_session( + &limits, + cancellation.clone(), + &pm_rust::current_timestamp(), + )?; + let mut stdout = std::io::stdout().lock(); + loop { + if cancel_file.as_ref().is_some_and(|path| path.exists()) { + cancellation.cancel(); + } + let Some(page) = session.next_page()? else { + break; + }; + if json { + writeln!(stdout, "{}", pm_rust::stringify_json(&page, false))?; + } else { + let receipt = pm_rust::ListOutput::from(serde_json::to_value(&page.receipt)?); + writeln!( + stdout, + "{}{}---", + pm_rust::stringify_toon(&receipt)?, + pm_rust::stringify_toon(&page.output)? + )?; + } + stdout.flush()?; + } + return Ok(()); + } Command::Assurance { action, kind, @@ -1371,6 +1434,11 @@ fn main() -> ExitCode { match run(cli) { Ok(()) => ExitCode::SUCCESS, Err(error) => { + if let Some(refusal) = error.downcast_ref::() { + let payload = serde_json::json!({"code":refusal.code(),"detail":refusal.to_string(),"required":refusal.to_string(),"exit_code":2}); + let _ = write_json_to(&mut std::io::stderr().lock(), &payload); + return ExitCode::from(2); + } if matches!( error.downcast_ref::(), Some(pm_rust::PmRustError::HistoryVerificationFailed) diff --git a/src/read_session.rs b/src/read_session.rs new file mode 100644 index 0000000..946b149 --- /dev/null +++ b/src/read_session.rs @@ -0,0 +1,454 @@ +//! Resource-admitted, immutable native read sessions with pull backpressure. + +use crate::{ItemFilter, ListOptions, ListOutput, PmRustError, Workspace}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + collections::BTreeSet, + fmt::Write as _, + fs, + io::Read, + path::{Component, Path}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +/// Caller-controlled admission ceilings; independent of Node and Git. +#[derive(Clone, Debug)] +pub struct ReadSessionLimits { + /// Accounted native working-set ceiling in bytes, including rendering reserve. + pub memory_bytes: u64, + /// Maximum rows per pull, before the existing output budget compacts them. + pub page_items: usize, + /// Existing renderer token ceiling for each page. + pub output_tokens: usize, + /// Select the existing TOON rather than JSON budgeting contract. + pub toon: bool, +} + +impl Default for ReadSessionLimits { + /// Chooses a 512 MiB admission ceiling and agent-sized pages. + fn default() -> Self { + Self { + memory_bytes: 512 * 1024 * 1024, + page_items: 20, + output_tokens: 4000, + toon: false, + } + } +} + +/// Thread-safe cooperative cancellation; no worker threads or outstanding tasks. +#[derive(Clone, Debug, Default)] +pub struct SessionCancellation(Arc); + +impl SessionCancellation { + /// Requests cancellation; the next copy checkpoint or pull releases storage. + pub fn cancel(&self) { + self.0.store(true, Ordering::Release); + } + + /// Refuses work after cancellation without resetting the shared token. + fn check(&self) -> Result<(), SessionRefusal> { + if self.0.load(Ordering::Acquire) { + Err(SessionRefusal::Cancelled) + } else { + Ok(()) + } + } +} + +/// Stable embedding refusals, each with explicit caller recovery guidance. +#[derive(Debug, thiserror::Error)] +pub enum SessionRefusal { + /// Caller cancelled before the next admission or delivery boundary. + #[error("read session cancelled; open a new session with a fresh cancellation token")] + Cancelled, + /// A required working set exceeds caller admission limits. + #[error("read session memory ceiling exceeded; raise memory_bytes or select a smaller tracker")] + MemoryLimit, + /// Caller supplied a zero or unrepresentable resource control. + #[error( + "invalid session limits; use positive memory_bytes, page_items and output_tokens (at most 1000000)" + )] + InvalidLimits, + /// A source cannot be safely frozen under this bounded contract. + #[error( + "unsupported session source; use regular files, at most 32 directory levels, and tracker-relative schema files" + )] + UnsupportedSource, + /// The canonical reader rejected source or output data. + #[error( + "read session source refused: {0}; repair tracker integrity or increase output_tokens and reopen" + )] + Source(#[from] PmRustError), + /// Snapshot creation, reading, or cleanup failed. + #[error("read session storage failed: {0}; check scratch storage permissions and free space")] + Storage(#[from] std::io::Error), +} + +impl SessionRefusal { + /// Stable machine code independent of human-readable operating-system errors. + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + Self::Cancelled => "session_cancelled", + Self::MemoryLimit => "session_memory_limit", + Self::InvalidLimits => "session_invalid_limits", + Self::UnsupportedSource => "session_unsupported_source", + Self::Source(_) => "session_source_refused", + Self::Storage(_) => "session_storage_failed", + } + } +} + +/// Certification and resource evidence for one immutable session snapshot. +#[derive(Clone, Debug, Serialize)] +pub struct SessionReceipt { + /// Versioned embedding contract, independent of oracle versioning. + pub protocol: &'static str, + /// Digest of ordered relative filenames and exact admitted bytes. + pub snapshot_sha256: String, + /// Fully decoded, unique source items before filtering. + pub source_items: usize, + /// Total frozen file bytes; snapshot disk use is bounded by this value. + pub source_bytes: u64, + /// Accounted peak reserve, not a measurement of process RSS. + pub admitted_memory_bytes: u64, + /// Caller-declared admission ceiling. + pub memory_ceiling_bytes: u64, + /// Pages successfully delivered so far. + pub pages: usize, + /// True only after the final page has been delivered. + pub complete: bool, +} + +/// One oracle-compatible page and its separate session certificate. +#[derive(Debug, Serialize)] +pub struct SessionPage { + /// Canonical list envelope, retaining lossless JSON overrides. + pub output: ListOutput, + /// Session evidence; projected field omissions remain in the list envelope. + pub receipt: SessionReceipt, +} + +/// A pull-based, disk-backed read session. Drop removes private snapshot storage. +/// +/// Retaining returned pages is the caller's responsibility. Cancellation is +/// cooperative between file copies and page queries; it does not preempt decoding. +#[derive(Debug)] +pub struct ReadSession { + /// Private snapshot ownership; taken on completion, cancellation or failure. + snapshot: Option, + /// Reader using private storage and the original query identity. + workspace: Workspace, + /// Existing canonical query controls; the session owns continuation. + options: ListOptions, + /// Exact snapshot evidence and aggregate delivery state. + receipt: SessionReceipt, + /// Shared cancellation token. + cancellation: SessionCancellation, + /// Stable read clock selected once by the caller. + timestamp: String, +} + +impl Workspace { + /// Freezes and strictly certifies all items before returning a native session. + /// + /// # Errors + /// Returns typed admission, cancellation, storage or source refusals. No Git + /// subprocess, extensions, telemetry or writes to the source tracker occur. + pub fn read_session( + &self, + limits: &ReadSessionLimits, + cancellation: SessionCancellation, + timestamp: &str, + ) -> Result { + cancellation.check()?; + if limits.memory_bytes == 0 + || limits.page_items == 0 + || limits.output_tokens == 0 + || limits.page_items > 1_000_000 + || limits.output_tokens > 1_000_000 + { + return Err(SessionRefusal::InvalidLimits); + } + let reserved = 8 * 1024 * 1024 + limits.output_tokens as u64 * 256; + let mut admission = Admission { + ceiling: limits.memory_bytes, + used: reserved, + bytes: 0, + items: 0, + hash: Sha256::new(), + ids: BTreeSet::new(), + cancellation: &cancellation, + }; + admission.charge(0)?; + let snapshot = tempfile::tempdir()?; + let target = snapshot.path().join(".agents/pm"); + fs::create_dir_all(&target)?; + admission.copy( + &self.pm_root().join("settings.json"), + &target.join("settings.json"), + Path::new("settings.json"), + false, + )?; + let settings: serde_json::Value = + serde_json::from_slice(&fs::read(target.join("settings.json"))?) + .map_err(|_| SessionRefusal::UnsupportedSource)?; + for name in ["statuses", "workflows"] { + let fallback = format!("schema/{name}.json"); + let relative = Path::new( + settings["schema"]["files"][name] + .as_str() + .unwrap_or(&fallback), + ); + if relative + .components() + .any(|part| !matches!(part, Component::Normal(_))) + { + return Err(SessionRefusal::UnsupportedSource); + } + let mut schema_source = self.pm_root().to_path_buf(); + for component in relative.components() { + schema_source.push(component); + if schema_source.is_symlink() { + return Err(SessionRefusal::UnsupportedSource); + } + } + if schema_source.exists() { + admission.copy(&schema_source, &target.join(relative), relative, false)?; + } + } + admission.walk(self.pm_root(), &target, Path::new(""), 0)?; + let mut snapshot_sha256 = String::with_capacity(64); + for byte in admission.hash.finalize() { + let _ = write!(snapshot_sha256, "{byte:02x}"); + } + let receipt = SessionReceipt { + protocol: "pm-rust/read-session/v1", + snapshot_sha256, + source_items: admission.items, + source_bytes: admission.bytes, + admitted_memory_bytes: admission.used, + memory_ceiling_bytes: limits.memory_bytes, + pages: 0, + complete: false, + }; + let workspace = self.with_read_snapshot(target); + Ok(ReadSession { + snapshot: Some(snapshot), + workspace, + options: ListOptions { + all: true, + brief: true, + limit: Some(limits.page_items.to_string()), + output_budget: Some(limits.output_tokens.to_string()), + toon: limits.toon, + ..ListOptions::default() + }, + receipt, + cancellation, + timestamp: timestamp.to_owned(), + }) + } +} + +impl ReadSession { + /// Reads the current resource receipt without consuming or materializing a page. + #[must_use] + pub fn receipt(&self) -> &SessionReceipt { + &self.receipt + } + + /// Cancels this session synchronously and releases its private snapshot. + /// + /// # Errors + /// Returns storage cleanup failures; the session remains terminal. + pub fn cancel(&mut self) -> Result<(), SessionRefusal> { + self.cancellation.cancel(); + self.cleanup() + } + + /// Removes snapshot storage exactly once, including on terminal failures. + fn cleanup(&mut self) -> Result<(), SessionRefusal> { + if let Some(snapshot) = self.snapshot.take() { + snapshot.close()?; + } + Ok(()) + } + + /// Pulls one bounded canonical list page; `None` means successful exhaustion. + /// + /// # Errors + /// Cancellation, source and output errors terminate the session and release + /// resources. A caller must drop each page to keep caller-retained memory bounded. + pub fn next_page(&mut self) -> Result, SessionRefusal> { + if let Err(error) = self.cancellation.check() { + self.cleanup()?; + return Err(error); + } + if self.snapshot.is_none() { + return Ok(None); + } + let result = self + .workspace + .list_page(&ItemFilter::default(), &self.options, &self.timestamp) + .map_err(SessionRefusal::from); + let output = match result { + Ok(output) => output, + Err(error) => { + self.cleanup()?; + return Err(error); + } + }; + #[cfg(test)] + tests::AFTER_QUERY_HOOK.with(|hook| hook.get()(&self.cancellation)); + if let Err(error) = self.cancellation.check() { + self.cleanup()?; + return Err(error); + } + self.options.after = output["next_cursor"].as_str().map(str::to_owned); + self.options.output_cursor = None; + if output["continuation_kind"] == "output_cursor" { + self.options.output_cursor = self.options.after.take(); + } + self.receipt.pages += 1; + self.receipt.complete = + self.options.after.is_none() && self.options.output_cursor.is_none(); + if self.receipt.complete { + self.cleanup()?; + } + Ok(Some(SessionPage { + output, + receipt: self.receipt.clone(), + })) + } +} + +/// Conservative admission ledger for private source storage and existing readers. +struct Admission<'a> { + /// Caller ceiling. + ceiling: u64, + /// Accounted reserve. + used: u64, + /// Exact source bytes. + bytes: u64, + /// Validated item count. + items: usize, + /// Ordered snapshot digest. + hash: Sha256, + /// Strict global identity uniqueness. + ids: BTreeSet, + /// Cooperative cancellation boundary. + cancellation: &'a SessionCancellation, +} + +impl Admission<'_> { + /// Reserves before allocating file bytes or adding index entries. + fn charge(&mut self, bytes: u64) -> Result<(), SessionRefusal> { + self.used = self.used.saturating_add(bytes); + if self.used > self.ceiling { + Err(SessionRefusal::MemoryLimit) + } else { + Ok(()) + } + } + + /// Copies one bounded regular file, validating items with the canonical decoder. + fn copy( + &mut self, + source: &Path, + target: &Path, + relative: &Path, + item: bool, + ) -> Result<(), SessionRefusal> { + self.cancellation.check()?; + let metadata = fs::symlink_metadata(source)?; + if !metadata.is_file() { + return Err(SessionRefusal::UnsupportedSource); + } + let size = metadata.len(); + self.charge( + size.saturating_mul(128) + .saturating_add(8192 + relative.as_os_str().len() as u64 * 16), + )?; + #[cfg(test)] + tests::BEFORE_COPY_HOOK.with(|hook| hook.get()(source, self.cancellation)); + let mut bytes = Vec::new(); + fs::File::open(source)? + .take(size.saturating_add(1)) + .read_to_end(&mut bytes)?; + if bytes.len() as u64 != size { + return Err(SessionRefusal::UnsupportedSource); + } + self.cancellation.check()?; + if item { + let text = + std::str::from_utf8(&bytes).map_err(|_| SessionRefusal::UnsupportedSource)?; + let document = crate::item::decode_item(source, text)?; + if !self.ids.insert(document.metadata.id.clone()) { + return Err(SessionRefusal::Source(PmRustError::DuplicateItemId { + id: document.metadata.id, + first: source.to_path_buf(), + second: source.to_path_buf(), + })); + } + self.items += 1; + } + fs::create_dir_all(target.parent().ok_or(SessionRefusal::UnsupportedSource)?)?; + fs::write(target, &bytes)?; + self.hash.update(relative.to_string_lossy().as_bytes()); + self.hash.update([0]); + self.hash.update(size.to_le_bytes()); + self.hash.update(&bytes); + self.bytes += size; + Ok(()) + } + + /// Visits deterministic source entries without retaining the corpus contents. + fn walk( + &mut self, + source: &Path, + target: &Path, + relative: &Path, + depth: usize, + ) -> Result<(), SessionRefusal> { + self.cancellation.check()?; + if depth > 32 { + return Err(SessionRefusal::UnsupportedSource); + } + let mut entries = Vec::new(); + for entry in fs::read_dir(source)? { + self.cancellation.check()?; + self.charge(1024)?; + entries.push(entry?); + } + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let name = entry.file_name(); + let kind = entry.file_type()?; + if depth == 0 + && crate::workspace::NON_ITEM_DIRECTORIES.contains(&name.to_string_lossy().as_ref()) + { + continue; + } + if kind.is_symlink() { + return Err(SessionRefusal::UnsupportedSource); + } + let relative = relative.join(&name); + if kind.is_dir() { + self.walk(&entry.path(), &target.join(&name), &relative, depth + 1)?; + } else if depth > 0 && entry.path().extension().is_some_and(|ext| ext == "toon") { + self.copy(&entry.path(), &target.join(name), &relative, true)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "../tests/support/read_session_unit.rs"] +mod tests; diff --git a/src/workspace.rs b/src/workspace.rs index 23a7c1c..023a134 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -75,6 +75,7 @@ pub struct ListResult { pub struct Workspace { pm_root: PathBuf, query_pm_root: PathBuf, + session_query_identity: Option, } /// Two workspaces are equal when they name the same canonical tracker. @@ -137,6 +138,7 @@ impl Workspace { return Ok(Self { pm_root: current, query_pm_root: query_current, + session_query_identity: None, }); } let candidate = current.join(".agents/pm"); @@ -144,6 +146,7 @@ impl Workspace { return Ok(Self { pm_root: candidate, query_pm_root: query_current.join(".agents").join("pm"), + session_query_identity: None, }); } if !current.pop() { @@ -155,6 +158,15 @@ impl Workspace { } } + /// Rebinds private snapshot storage while retaining published cursor identity. + pub(crate) fn with_read_snapshot(&self, pm_root: PathBuf) -> Self { + Self { + pm_root, + query_pm_root: self.query_pm_root.clone(), + session_query_identity: Some(self.query_pm_root().to_path_buf()), + } + } + /// Returns the canonical tracker root. #[must_use] pub fn pm_root(&self) -> &Path { @@ -163,7 +175,9 @@ impl Workspace { /// Returns the platform's working-directory spelling for published query hashes. pub(crate) fn query_pm_root(&self) -> &Path { - query_root_path(&self.query_pm_root, &self.pm_root, cfg!(windows)) + self.session_query_identity + .as_deref() + .unwrap_or_else(|| query_root_path(&self.query_pm_root, &self.pm_root, cfg!(windows))) } /// Reads and validates every stored TOON item, sorted by identifier. diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs new file mode 100644 index 0000000..eba8d32 --- /dev/null +++ b/tests/read_session_contract.rs @@ -0,0 +1,148 @@ +//! Public CLI and SDK session contracts against genuine non-git trackers. +use pm_rust::{ReadSessionLimits, SessionCancellation, Workspace}; +use serde_json::Value; +use std::{fs, path::Path, process::Command}; + +/// Fallible test result. +type TestResult = Result<(), Box>; + +/// Initializes a real tracker with the pinned CLI, outside any Git repository. +fn fixture() -> Result> { + let directory = tempfile::tempdir()?; + let launcher = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/oracle/launch.mts"); + let initialized = Command::new("node") + .arg(launcher) + .current_dir(directory.path()) + .args(["init", "--defaults", "--agent-guidance", "skip"]) + .env( + "GIT_CEILING_DIRECTORIES", + directory.path().parent().ok_or("temp parent")?, + ) + .output()?; + assert!( + initialized.status.success(), + "{}", + String::from_utf8_lossy(&initialized.stderr) + ); + fs::create_dir_all(directory.path().join(".agents/pm/tasks"))?; + for index in 0..3 { + fs::write( + directory + .path() + .join(format!(".agents/pm/tasks/demo-{index}.toon")), + format!( + "id: demo-{index}\ntitle: Example {index}\ntype: Task\nstatus: open\npriority: 1\ncreated_at: \"2026-10-21T12:00:00.000Z\"\nupdated_at: \"2026-10-21T12:00:00.000Z\"\nbody: retained\n" + ), + )?; + } + Ok(directory) +} + +#[test] +fn read_session_cli_needs_no_git_or_node_and_emits_bounded_frames() -> TestResult { + let directory = fixture()?; + let no_executables = tempfile::tempdir()?; + for json in [false, true] { + let mut command = Command::new(env!("CARGO_BIN_EXE_pm-rust")); + command + .current_dir(directory.path()) + .args(["read-session", "--page-items", "1", "--cancel-file"]) + .arg(directory.path().join("absent-cancel-marker")); + if json { + command.arg("--json"); + } + let result = command + .env("PATH", no_executables.path()) + .env( + "GIT_CEILING_DIRECTORIES", + directory.path().parent().ok_or("temp parent")?, + ) + .output()?; + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let output = String::from_utf8(result.stdout)?; + if json { + let pages = output + .lines() + .map(serde_json::from_str::) + .collect::, _>>()?; + assert_eq!(pages.len(), 3); + for (index, page) in pages.iter().enumerate() { + assert_eq!(page["output"]["items"].as_array().map(Vec::len), Some(1)); + assert_eq!(page["receipt"]["complete"], index == 2); + } + } else { + assert_eq!(output.matches("---\n").count(), 3); + assert!(output.contains("protocol: pm-rust/read-session/v1")); + } + } + let marker = directory.path().join("cancel"); + fs::write(&marker, "")?; + for controls in [ + vec!["--memory-bytes", "1"], + vec!["--output-tokens", "0"], + vec!["--cancel-file", marker.to_str().ok_or("marker")?], + ] { + let result = Command::new(env!("CARGO_BIN_EXE_pm-rust")) + .current_dir(directory.path()) + .arg("read-session") + .args(controls) + .output()?; + assert_eq!(result.status.code(), Some(2)); + assert!(result.stdout.is_empty()); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert!( + refusal["code"] + .as_str() + .is_some_and(|code| code.starts_with("session_")) + ); + assert!( + refusal["required"] + .as_str() + .is_some_and(|text| text.contains(';')) + ); + } + Ok(()) +} + +#[test] +fn canonical_output_budget_continuations_do_not_skip_context_rows() -> TestResult { + let directory = fixture()?; + let workspace = Workspace::discover(directory.path())?; + // Large titles force the existing output cursor rather than only a producer cursor. + for entry in fs::read_dir(workspace.pm_root().join("tasks"))? { + let path = entry?.path(); + let text = fs::read_to_string(&path)?.replace( + "title: Example", + &format!("title: {} Example", "x".repeat(3000)), + ); + fs::write(path, text)?; + } + for toon in [false, true] { + let mut session = workspace.read_session( + &ReadSessionLimits { + output_tokens: 1500, + page_items: 3, + toon, + ..ReadSessionLimits::default() + }, + SessionCancellation::default(), + "2026-10-21T12:00:00.000Z", + )?; + let mut ids = std::collections::BTreeSet::new(); + let mut pages = 0; + while let Some(page) = session.next_page()? { + pages += 1; + assert!(pages <= 3); + for row in page.output["items"].as_array().ok_or("missing rows")? { + assert!(ids.insert(row["id"].as_str().ok_or("missing id")?.to_owned())); + } + } + assert!(session.receipt().complete); + assert_eq!(ids.len(), 3); + } + Ok(()) +} diff --git a/tests/release_pipeline_contract.rs b/tests/release_pipeline_contract.rs index 8c2d0da..9088682 100644 --- a/tests/release_pipeline_contract.rs +++ b/tests/release_pipeline_contract.rs @@ -1120,3 +1120,34 @@ fn ci_pm_cli_pin_is_single_and_current() -> Result<(), BoxError> { ); Ok(()) } + +/// Both package-manager entrypoints execute the unchanged native release gate. +#[test] +fn npm_and_bun_share_the_full_native_aggregate() -> Result<(), BoxError> { + let package: serde_json::Value = serde_json::from_str(&read_repo_file("package.json")?)?; + assert_eq!(package["scripts"]["release:check"], "just release-check"); + let workflow = parse_workflow(".github/workflows/ci.yml")?; + let jobs = map_get(&workflow, "jobs").ok_or("missing jobs")?; + let release = map_get(jobs, "release-check").ok_or("missing release job")?; + let matrix = map_get( + map_get(release, "strategy").ok_or("missing strategy")?, + "matrix", + ) + .ok_or("missing matrix")?; + let entries = map_get(matrix, "include") + .and_then(Value::as_sequence) + .ok_or("missing release matrix")?; + for runtime in ["npm", "bun"] { + assert!( + entries + .iter() + .any(|entry| map_get(entry, "runtime").and_then(Value::as_str) == Some(runtime)) + ); + } + let steps = job_steps(&workflow, "release-check")?; + assert_eq!( + step_run(&steps, "Run release:check aggregate gate")?, + "${{ matrix.runtime }} run release:check" + ); + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs new file mode 100644 index 0000000..75d95e5 --- /dev/null +++ b/tests/support/read_session_unit.rs @@ -0,0 +1,353 @@ +//! Session admission, immutable-source, terminal cleanup and refusal contracts. +use super::*; + +/// Shared fallible test return. +type TestResult = Result<(), Box>; +/// Fixed read clock. +const CLOCK: &str = "2026-10-21T12:00:00.000Z"; + +/// Creates a small real non-git canonical tracker, not a reader mock. +fn fixture(count: usize) -> Result<(tempfile::TempDir, Workspace), Box> { + let directory = tempfile::tempdir()?; + let root = directory.path().join(".agents/pm"); + fs::create_dir_all(root.join("tasks/nested"))?; + fs::create_dir_all(root.join("history"))?; + fs::write( + root.join("settings.json"), + include_str!("../fixtures/list-settings.json"), + )?; + fs::write(root.join("history/ignored.toon"), "invalid ignored history")?; + fs::write(root.join("tasks/ignored.txt"), "ignored")?; + fs::write(root.join("tasks/extensionless"), "ignored")?; + for index in 0..count { + fs::write( + root.join(format!("tasks/nested/demo-{index}.toon")), + format!( + "id: demo-{index}\ntitle: Example {index}\ntype: Task\nstatus: open\npriority: 1\ncreated_at: \"{CLOCK}\"\nupdated_at: \"{CLOCK}\"\nbody: retained\n" + ), + )?; + } + let workspace = Workspace::discover(directory.path())?; + Ok((directory, workspace)) +} + +#[test] +fn session_pages_reuse_canonical_output_and_freeze_source() -> TestResult { + let (_directory, workspace) = fixture(7)?; + let mut limits = ReadSessionLimits { + page_items: 2, + ..ReadSessionLimits::default() + }; + for toon in [false, true] { + limits.toon = toon; + let mut session = workspace.read_session(&limits, SessionCancellation::default(), CLOCK)?; + let snapshot = session.workspace.pm_root().to_path_buf(); + assert_eq!(session.receipt().source_items, 7); + assert!(session.receipt().admitted_memory_bytes < limits.memory_bytes); + let mut options = session.options.clone(); + let mut ids = BTreeSet::new(); + while let Some(page) = session.next_page()? { + let expected = workspace.list_page(&ItemFilter::default(), &options, CLOCK)?; + assert_eq!( + page.output.stringify_read_json(), + expected.stringify_read_json() + ); + assert_eq!( + crate::stringify_toon(&page.output)?, + crate::stringify_toon(&expected)? + ); + options.after = page.output["next_cursor"].as_str().map(str::to_owned); + for row in page.output["items"].as_array().ok_or("missing items")? { + assert!(ids.insert(row["id"].as_str().ok_or("missing id")?.to_owned())); + } + assert_eq!(page.receipt.complete, ids.len() == 7); + } + assert!(session.receipt().complete); + assert_eq!(session.receipt().pages, 4); + assert_eq!(ids.len(), 7); + assert!(!snapshot.exists()); + assert!(session.next_page()?.is_none()); + } + let mut session = workspace.read_session(&limits, SessionCancellation::default(), CLOCK)?; + let snapshot = session.workspace.pm_root().to_path_buf(); + fs::remove_dir_all(workspace.pm_root().join("tasks"))?; + assert!(session.next_page()?.is_some()); + session.cancel()?; + assert!(!snapshot.exists()); + assert!(!session.receipt().complete); + assert!(matches!( + session.next_page(), + Err(SessionRefusal::Cancelled) + )); + session.cancel()?; + Ok(()) +} + +#[test] +fn admission_refuses_before_querying_and_cancellation_releases_resources() -> TestResult { + let (_directory, workspace) = fixture(1)?; + let token = SessionCancellation::default(); + token.cancel(); + assert!(matches!( + workspace.read_session(&ReadSessionLimits::default(), token, CLOCK), + Err(SessionRefusal::Cancelled) + )); + for (memory_bytes, page_items, output_tokens) in [ + (0, 1, 1), + (1, 0, 1), + (1, 1, 0), + (1, 1_000_001, 1), + (1, 1, 1_000_001), + ] { + assert!(matches!( + workspace.read_session( + &ReadSessionLimits { + memory_bytes, + page_items, + output_tokens, + toon: false + }, + SessionCancellation::default(), + CLOCK + ), + Err(SessionRefusal::InvalidLimits) + )); + } + for ceiling in [1, 9 * 1024 * 1024] { + let limits = ReadSessionLimits { + memory_bytes: ceiling, + ..ReadSessionLimits::default() + }; + assert!(matches!( + workspace.read_session(&limits, SessionCancellation::default(), CLOCK), + Err(SessionRefusal::MemoryLimit) + )); + } + let token = SessionCancellation::default(); + let mut session = + workspace.read_session(&ReadSessionLimits::default(), token.clone(), CLOCK)?; + let snapshot = session.workspace.pm_root().to_path_buf(); + token.cancel(); + assert!(matches!( + session.next_page(), + Err(SessionRefusal::Cancelled) + )); + assert!(!snapshot.exists()); + let session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + )?; + let snapshot = session.workspace.pm_root().to_path_buf(); + drop(session); + assert!(!snapshot.exists()); + let errors = [ + SessionRefusal::Cancelled, + SessionRefusal::MemoryLimit, + SessionRefusal::InvalidLimits, + SessionRefusal::UnsupportedSource, + SessionRefusal::Source(PmRustError::OutputBudgetExceeded), + SessionRefusal::Storage(std::io::Error::other("synthetic storage refusal")), + ]; + for error in errors { + assert!(error.code().starts_with("session_")); + assert!(error.to_string().contains(';')); + } + Ok(()) +} + +#[test] +fn snapshot_validates_all_items_and_schema_before_delivery() -> TestResult { + let (_directory, workspace) = fixture(1)?; + let open = || { + workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + ) + }; + let item = workspace.pm_root().join("tasks/nested/demo-0.toon"); + fs::copy(&item, workspace.pm_root().join("tasks/duplicate.toon"))?; + assert!(matches!( + open(), + Err(SessionRefusal::Source(PmRustError::DuplicateItemId { .. })) + )); + fs::write(workspace.pm_root().join("tasks/duplicate.toon"), "invalid")?; + assert!(matches!( + open(), + Err(SessionRefusal::Source( + PmRustError::InvalidItemDocument { .. } + )) + )); + fs::write(workspace.pm_root().join("tasks/duplicate.toon"), [255])?; + assert!(matches!(open(), Err(SessionRefusal::UnsupportedSource))); + fs::remove_file(workspace.pm_root().join("tasks/duplicate.toon"))?; + for settings in [ + "invalid", + r#"{"schema":{"files":{"statuses":"../external.json"}}}"#, + ] { + fs::write(workspace.pm_root().join("settings.json"), settings)?; + assert!(matches!(open(), Err(SessionRefusal::UnsupportedSource))); + } + fs::write( + workspace.pm_root().join("settings.json"), + r#"{"schema":{"files":{"statuses":"schema/custom.json"}}}"#, + )?; + fs::create_dir_all(workspace.pm_root().join("schema"))?; + fs::write(workspace.pm_root().join("schema/custom.json"), "{}")?; + fs::write(workspace.pm_root().join("schema/workflows.json"), "{}")?; + let mut session = open()?; + assert!(session.next_page()?.is_some()); + let (_empty, workspace) = fixture(0)?; + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + )?; + assert!( + session + .next_page()? + .ok_or("empty final page missing")? + .receipt + .complete + ); + Ok(()) +} + +#[test] +fn small_output_budget_refuses_and_terminal_query_failure_cleans_up() -> TestResult { + let (_directory, workspace) = fixture(2)?; + let limits = ReadSessionLimits { + output_tokens: 1, + ..ReadSessionLimits::default() + }; + let mut session = workspace.read_session(&limits, SessionCancellation::default(), CLOCK)?; + let snapshot = session.workspace.pm_root().to_path_buf(); + assert!(matches!( + session.next_page(), + Err(SessionRefusal::Source(_)) + )); + assert!(!snapshot.exists()); + assert!(session.next_page()?.is_none()); + Ok(()) +} + +thread_local! { + /// Deterministic filesystem drift/cancellation after actual metadata admission. + pub(super) static BEFORE_COPY_HOOK: std::cell::Cell = const { std::cell::Cell::new(copy_noop) }; + /// Deterministic cancellation after the actual canonical query. + pub(super) static AFTER_QUERY_HOOK: std::cell::Cell = const { std::cell::Cell::new(query_noop) }; +} + +/// Leaves a real source unchanged by default. +fn copy_noop(_: &Path, _: &SessionCancellation) {} +/// Leaves ordinary query delivery unchanged. +fn query_noop(_: &SessionCancellation) {} +/// Changes the actual source length after admission and before bounded reading. +fn grow_source(path: &Path, _: &SessionCancellation) { + let _ = fs::write(path, "changed-size source"); +} +/// Cancels after admission of one real source file. +fn cancel_copy(_: &Path, token: &SessionCancellation) { + token.cancel(); +} +/// Constructs a private ledger for deterministic boundary tests. +fn admission(token: &SessionCancellation) -> Admission<'_> { + Admission { + ceiling: u64::MAX, + used: 0, + bytes: 0, + items: 0, + hash: Sha256::new(), + ids: BTreeSet::new(), + cancellation: token, + } +} + +#[test] +fn source_drift_and_mid_operation_cancellation_are_fail_closed() -> TestResult { + let directory = tempfile::tempdir()?; + let source = directory.path().join("source"); + let target = directory.path().join("target"); + fs::write(&source, "original")?; + let token = SessionCancellation::default(); + BEFORE_COPY_HOOK.with(|hook| hook.set(grow_source)); + let result = admission(&token).copy(&source, &target, Path::new("source"), false); + BEFORE_COPY_HOOK.with(|hook| hook.set(copy_noop)); + assert!(matches!(result, Err(SessionRefusal::UnsupportedSource))); + assert!(!target.exists()); + BEFORE_COPY_HOOK.with(|hook| hook.set(cancel_copy)); + let result = admission(&token).copy(&source, &target, Path::new("source"), false); + BEFORE_COPY_HOOK.with(|hook| hook.set(copy_noop)); + assert!(matches!(result, Err(SessionRefusal::Cancelled))); + let (_directory, workspace) = fixture(1)?; + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + )?; + let snapshot = session.workspace.pm_root().to_path_buf(); + AFTER_QUERY_HOOK.with(|hook| hook.set(SessionCancellation::cancel)); + let result = session.next_page(); + AFTER_QUERY_HOOK.with(|hook| hook.set(query_noop)); + assert!(matches!(result, Err(SessionRefusal::Cancelled))); + assert!(!snapshot.exists()); + assert!(!session.receipt().complete); + Ok(()) +} + +#[test] +fn unsupported_files_and_depth_refuse_with_recoverable_storage_errors() -> TestResult { + let directory = tempfile::tempdir()?; + let token = SessionCancellation::default(); + let mut ledger = admission(&token); + assert!(matches!( + ledger.copy( + directory.path(), + &directory.path().join("target"), + Path::new("dir"), + false + ), + Err(SessionRefusal::UnsupportedSource) + )); + assert!(matches!( + ledger.walk(directory.path(), directory.path(), Path::new(""), 33), + Err(SessionRefusal::UnsupportedSource) + )); + assert!(matches!( + ledger.copy( + &directory.path().join("absent"), + &directory.path().join("target"), + Path::new("absent"), + false + ), + Err(SessionRefusal::Storage(_)) + )); + #[cfg(unix)] + { + let (_directory, workspace) = fixture(1)?; + std::os::unix::fs::symlink("missing", workspace.pm_root().join("tasks/link"))?; + assert!(matches!( + workspace.read_session(&ReadSessionLimits::default(), token, CLOCK), + Err(SessionRefusal::UnsupportedSource) + )); + } + #[cfg(windows)] + { + let (_directory, workspace) = fixture(1)?; + // Directory junctions need no symlink privilege on Windows runners. + let target = directory.path().join("junction"); + fs::create_dir(&target)?; + let output = std::process::Command::new("cmd") + .args(["/C", "mklink", "/J"]) + .arg(workspace.pm_root().join("tasks/link")) + .arg(target) + .output()?; + assert!(output.status.success()); + assert!(matches!( + workspace.read_session(&ReadSessionLimits::default(), token, CLOCK), + Err(SessionRefusal::UnsupportedSource) + )); + } + Ok(()) +} From d385191b49010ec62b6c42a73c9126a42e83b631 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:05:45 +0200 Subject: [PATCH 02/10] fix: refuse omitted session results and isolate non-git fixtures --- .../pm-rust-bounded-read-sessions.toon | 5 +-- .../pm-rust-bounded-read-sessions.jsonl | 1 + src/read_session.rs | 4 +++ tests/read_session_contract.rs | 35 +++++++++++++++++++ tests/support/read_session_unit.rs | 28 +++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index a589e7c..57c7e86 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,15 +6,16 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T14:48:34.131Z" +updated_at: "2026-10-23T15:04:31.443Z" assignee: codex-agent claim_principal: codex-agent author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[2]{created_at,author,text}: +comments[3]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." + "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. files[6]{path,scope}: scripts/read-session-validate.mts,project src/main.rs,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index 263e9ee..023ec06 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -6,3 +6,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T14:48:04.952Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:48:04.952Z"},{"op":"add","path":"/metadata/files","value":[{"path":"scripts/read-session-validate.mts","scope":"project"},{"path":"src/main.rs","scope":"project"},{"path":"src/read_session.rs","scope":"project"},{"path":"src/workspace.rs","scope":"project"},{"path":"tests/read_session_contract.rs","scope":"project"},{"path":"tests/support/read_session_unit.rs","scope":"project"}]}],"before_hash":"7c29acde36b964c8d05d0a3cee1be33d568a2ec798b1f862949f683337daf5c1","after_hash":"f9184722f2747bc5a4ab4c97473275d7a100ccc40906729df1702ad55e8cdeba","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":"063d06db195c8b933374e52f62a5f47033c4a9eb0bc75228411d9dbf94a26a54"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:48:17.407Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:48:17.407Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/BOUNDED_READ_SESSIONS.md","scope":"project"},{"path":"examples/read_session.rs","scope":"project"},{"path":"README.md","scope":"project"}]}],"before_hash":"f9184722f2747bc5a4ab4c97473275d7a100ccc40906729df1702ad55e8cdeba","after_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","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":"0f24893896619eacb2c12cc0cf76c98d6120641f02b1fb7c85605999e8056c6d"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:48:34.131Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:48:34.131Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"cargo test --locked --lib read_session::tests -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}},{"command":"cargo test --locked --test read_session_contract -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}]}],"before_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","after_hash":"c69c209ff908733b8d67c2da820045b8acb6e13181e6fd4ada9930580010803f","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":"47c7e4de6b7c3943867a0570d222185103caedd34fb55564b89b56a70732e45c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:04:31.443Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/2","value":{"created_at":"2026-10-23T15:04:31.443Z","author":"codex-agent","text":"Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:04:31.443Z"}],"before_hash":"c69c209ff908733b8d67c2da820045b8acb6e13181e6fd4ada9930580010803f","after_hash":"c29c8db80469354c645f0aa8a526eb22df21df4d5db165c32a3c0efea8dd0753","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":"f8ff46e99d6a8920871b2255179edee1e41130cdbc0235177b34e9d4736c9d0e"} diff --git a/src/read_session.rs b/src/read_session.rs index 946b149..642cc94 100644 --- a/src/read_session.rs +++ b/src/read_session.rs @@ -310,6 +310,10 @@ impl ReadSession { self.cleanup()?; return Err(error); } + if output["read_output"]["result_omitted"] == true { + self.cleanup()?; + return Err(PmRustError::OutputBudgetExceeded.into()); + } self.options.after = output["next_cursor"].as_str().map(str::to_owned); self.options.output_cursor = None; if output["continuation_kind"] == "output_cursor" { diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index eba8d32..4b5a430 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -13,6 +13,8 @@ fn fixture() -> Result> { let initialized = Command::new("node") .arg(launcher) .current_dir(directory.path()) + .arg("--path") + .arg(directory.path().join(".agents/pm")) .args(["init", "--defaults", "--agent-guidance", "skip"]) .env( "GIT_CEILING_DIRECTORIES", @@ -24,6 +26,10 @@ fn fixture() -> Result> { "{}", String::from_utf8_lossy(&initialized.stderr) ); + assert!( + directory.path().join(".agents/pm/settings.json").is_file(), + "init must create the explicitly selected tracker" + ); fs::create_dir_all(directory.path().join(".agents/pm/tasks"))?; for index in 0..3 { fs::write( @@ -146,3 +152,32 @@ fn canonical_output_budget_continuations_do_not_skip_context_rows() -> TestResul } Ok(()) } + +/// Public embedding admission refuses early and owns a source-independent snapshot. +#[test] +fn embedding_admission_and_snapshot_lifetime_are_enforced() -> TestResult { + let directory = fixture()?; + let workspace = Workspace::discover(directory.path())?; + assert!(matches!( + workspace.read_session( + &ReadSessionLimits { + memory_bytes: 1, + ..ReadSessionLimits::default() + }, + SessionCancellation::default(), + "2026-10-21T12:00:00.000Z" + ), + Err(pm_rust::SessionRefusal::MemoryLimit) + )); + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + "2026-10-21T12:00:00.000Z", + )?; + fs::remove_dir_all(workspace.pm_root().join("tasks"))?; + let page = session.next_page()?.ok_or("snapshot page missing")?; + assert_eq!(page.output["items"].as_array().map(Vec::len), Some(3)); + assert!(page.receipt.complete); + assert!(session.next_page()?.is_none()); + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 75d95e5..4dd5627 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -351,3 +351,31 @@ fn unsupported_files_and_depth_refuse_with_recoverable_storage_errors() -> TestR } Ok(()) } + +#[test] +fn schema_directory_links_cannot_escape_snapshot_admission() -> TestResult { + let (directory, workspace) = fixture(1)?; + let outside = directory.path().join("outside-schema"); + fs::create_dir(&outside)?; + fs::write(outside.join("statuses.json"), "{}")?; + #[cfg(unix)] + std::os::unix::fs::symlink(&outside, workspace.pm_root().join("schema"))?; + #[cfg(windows)] + { + let output = std::process::Command::new("cmd") + .args(["/C", "mklink", "/J"]) + .arg(workspace.pm_root().join("schema")) + .arg(&outside) + .output()?; + assert!(output.status.success()); + } + assert!(matches!( + workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK + ), + Err(SessionRefusal::UnsupportedSource) + )); + Ok(()) +} From bdb43c746f331e679a4ce138f070a4a545c24334 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:43:39 +0200 Subject: [PATCH 03/10] fix: isolate session resources and certify bounded native delivery --- .../pm-rust-bounded-read-sessions.toon | 24 ++++++-- .../pm-rust-bounded-read-sessions.jsonl | 7 +++ docs/BOUNDED_READ_SESSIONS.md | 51 ++++++++++++++++- scripts/read-session-clock.mts | 11 ++++ scripts/read-session-validate.mts | 19 ++++++- src/list.rs | 2 +- src/main.rs | 6 +- src/read_session.rs | 25 ++++---- tests/read_session_contract.rs | 55 +++++++++++++++++- tests/support/read_session_unit.rs | 57 +++++++++++++++++-- 10 files changed, 221 insertions(+), 36 deletions(-) create mode 100644 scripts/read-session-clock.mts diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 57c7e86..d6209b8 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,26 +6,38 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T15:04:31.443Z" -assignee: codex-agent -claim_principal: codex-agent +updated_at: "2026-10-23T15:43:24.944Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[3]{created_at,author,text}: +comments[7]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. -files[6]{path,scope}: + "2026-10-23T15:13:48.701Z",codex-agent,"Focused linked suites passed 7 SDK and 4 CLI regressions before the strengthened complete-source-deletion check. Public-oracle validation distinguishes SDK convenience envelopes from CLI envelopes: use the real SDK complete-list certificate and row comparisons, plus the existing published CLI launcher under a fixed clock for byte-exact page comparisons. Frozen sessions now load lifecycle registries from snapshot storage while retaining only cursor identity from the original tracker. Behavioral reverts are being verified independently; no release thresholds or oracle pins changed." + "2026-10-23T15:20:20.151Z",codex-agent,"Behavioral revert proof complete: separately disable Admission::charge, bypass Workspace::with_read_snapshot, restore lifecycle reads to the live query root, bypass cleanup on cancellation, and remove the result_omitted guard. Each isolated variant compiled and failed its targeted regression (five expected exit-101 test failures); all original functions were restored. Restored linked suites passed 7 SDK plus 4 CLI tests; the npm/Bun aggregate wiring regression passed 1 test; formatting, strict Clippy, private rustdoc and strict erasable TypeScript checks passed. The public copy has 78 items and unchanged authoritative source hashes. Final large measurements and CI remain pending." + "2026-10-23T15:41:49.605Z",codex-agent,"Final resource and oracle evidence: 10,000 SDK-generated items in 10 pages, 1,959,611 source bytes, admission 381,185,760 bytes, peak RSS 43,688 KiB, native wall 28,001 ms, output 988,986 bytes; public tracker copy at 5862034 has 78 items in 1 page, 757,075 source bytes, admission 131,681,424 bytes, peak RSS 13,440 KiB, wall 437 ms, output 12,549 bytes. Both used a 536,870,912-byte declared ceiling and passed complete-list certificates and identity equality. All 11 pages matched canonical CLI JSON payloads with indentation normalization only; source hashes were unchanged. Binary SHA-256 was stable before and after validation. Seven behavioral reverts failed independently, including clock charging and duplicate-path remedies; all restored. CLI 2026.10.23 overlap rechecked; package oracle remains pinned to 2026.10.21. Evidence and reproduction: docs/BOUNDED_READ_SESSIONS.md. PR: https://github.com/unbraind/pm-rust/pull/110. Full CI aggregate evidence pending; item remains in_progress for orchestrator verification." + "2026-10-23T15:43:20.715Z",codex-agent,"Final focused validation: pm test --run --progress --timeout 900 passed all 3 linked commands, totaling 9 SDK session tests, 4 CLI session tests and 1 npm/Bun aggregate wiring test; zero failed or ignored. Strict Clippy across all targets/features, formatting, private rustdoc and strict erasable TypeScript checks passed. Real storage loss and explicit CLI clock selection are covered. Core decode_unique supplies both duplicate-file paths. Clock-storage reservation has its own failing behavioral revert. All seven revert proofs restored; no generated data or binaries are tracked. Implementation is ready for CI and review in PR 110; only full CI evidence remains pending. Release claim without closing so the orchestrator can verify and close." +files[15]{path,scope}: + .github/workflows/ci.yml,project + Cargo.toml,project + justfile,project + package-lock.json,project + package.json,project + scripts/read-session-clock.mts,project scripts/read-session-validate.mts,project + src/lib.rs,project + src/list.rs,project src/main.rs,project src/read_session.rs,project src/workspace.rs,project tests/read_session_contract.rs,project + tests/release_pipeline_contract.rs,project tests/support/read_session_unit.rs,project -tests[2]{command,scope,provenance{author,created_at,source_kind,source_ref}}: +tests[3]{command,scope,provenance{author,created_at,source_kind,source_ref}}: "cargo test --locked --lib read_session::tests -- --test-threads=1",project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test read_session_contract -- --test-threads=1,project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions + cargo test --locked --test release_pipeline_contract npm_and_bun_share_the_full_native_aggregate -- --test-threads=1,project,codex-agent,"2026-10-23T15:34:24.686Z",local_mutation,feat/bounded-read-sessions docs[3]{path,scope}: docs/BOUNDED_READ_SESSIONS.md,project examples/read_session.rs,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index 023ec06..61f4c03 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -7,3 +7,10 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T14:48:17.407Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T14:48:17.407Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/BOUNDED_READ_SESSIONS.md","scope":"project"},{"path":"examples/read_session.rs","scope":"project"},{"path":"README.md","scope":"project"}]}],"before_hash":"f9184722f2747bc5a4ab4c97473275d7a100ccc40906729df1702ad55e8cdeba","after_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","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":"0f24893896619eacb2c12cc0cf76c98d6120641f02b1fb7c85605999e8056c6d"} {"hash_algorithm":"sha256","ts":"2026-10-23T14:48:34.131Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T14:48:34.131Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"cargo test --locked --lib read_session::tests -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}},{"command":"cargo test --locked --test read_session_contract -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T14:48:33.983Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}]}],"before_hash":"8fc8cb5799f3ed72f320a45ce9bed97f0f6fe427def9d0fb22f5db8ad1e7414e","after_hash":"c69c209ff908733b8d67c2da820045b8acb6e13181e6fd4ada9930580010803f","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":"47c7e4de6b7c3943867a0570d222185103caedd34fb55564b89b56a70732e45c"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:04:31.443Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/2","value":{"created_at":"2026-10-23T15:04:31.443Z","author":"codex-agent","text":"Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:04:31.443Z"}],"before_hash":"c69c209ff908733b8d67c2da820045b8acb6e13181e6fd4ada9930580010803f","after_hash":"c29c8db80469354c645f0aa8a526eb22df21df4d5db165c32a3c0efea8dd0753","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":"f8ff46e99d6a8920871b2255179edee1e41130cdbc0235177b34e9d4736c9d0e"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:13:48.701Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/3","value":{"created_at":"2026-10-23T15:13:48.701Z","author":"codex-agent","text":"Focused linked suites passed 7 SDK and 4 CLI regressions before the strengthened complete-source-deletion check. Public-oracle validation distinguishes SDK convenience envelopes from CLI envelopes: use the real SDK complete-list certificate and row comparisons, plus the existing published CLI launcher under a fixed clock for byte-exact page comparisons. Frozen sessions now load lifecycle registries from snapshot storage while retaining only cursor identity from the original tracker. Behavioral reverts are being verified independently; no release thresholds or oracle pins changed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:13:48.701Z"}],"before_hash":"c29c8db80469354c645f0aa8a526eb22df21df4d5db165c32a3c0efea8dd0753","after_hash":"c619d30c42079327268d5d023f90d0efbbff9ca0dd57caae6081819bc1c4d8d7","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":"e9029e0aaea275a9c0a06b8b8117399cd922c88ed2a517c154894fa24c8d2134"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:20:20.151Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/4","value":{"created_at":"2026-10-23T15:20:20.151Z","author":"codex-agent","text":"Behavioral revert proof complete: separately disable Admission::charge, bypass Workspace::with_read_snapshot, restore lifecycle reads to the live query root, bypass cleanup on cancellation, and remove the result_omitted guard. Each isolated variant compiled and failed its targeted regression (five expected exit-101 test failures); all original functions were restored. Restored linked suites passed 7 SDK plus 4 CLI tests; the npm/Bun aggregate wiring regression passed 1 test; formatting, strict Clippy, private rustdoc and strict erasable TypeScript checks passed. The public copy has 78 items and unchanged authoritative source hashes. Final large measurements and CI remain pending."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:20:20.151Z"}],"before_hash":"c619d30c42079327268d5d023f90d0efbbff9ca0dd57caae6081819bc1c4d8d7","after_hash":"7ae669fb82bd730fa43f5514c720fdaaa7e7557cb1519930ea8f5a1eac45d71b","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":"be711ff780663feaee85eea753b02bd633a18285b1c7fb325392cb8d58f7a055"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:34:18.485Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/files/5/path","value":"scripts/read-session-clock.mts"},{"op":"replace","path":"/metadata/files/4/path","value":"package.json"},{"op":"replace","path":"/metadata/files/3/path","value":"package-lock.json"},{"op":"replace","path":"/metadata/files/2/path","value":"justfile"},{"op":"replace","path":"/metadata/files/1/path","value":"Cargo.toml"},{"op":"replace","path":"/metadata/files/0/path","value":".github/workflows/ci.yml"},{"op":"add","path":"/metadata/files/6","value":{"path":"scripts/read-session-validate.mts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"src/lib.rs","scope":"project"}},{"op":"add","path":"/metadata/files/8","value":{"path":"src/list.rs","scope":"project"}},{"op":"add","path":"/metadata/files/9","value":{"path":"src/main.rs","scope":"project"}},{"op":"add","path":"/metadata/files/10","value":{"path":"src/read_session.rs","scope":"project"}},{"op":"add","path":"/metadata/files/11","value":{"path":"src/workspace.rs","scope":"project"}},{"op":"add","path":"/metadata/files/12","value":{"path":"tests/read_session_contract.rs","scope":"project"}},{"op":"add","path":"/metadata/files/13","value":{"path":"tests/release_pipeline_contract.rs","scope":"project"}},{"op":"add","path":"/metadata/files/14","value":{"path":"tests/support/read_session_unit.rs","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:34:18.485Z"}],"before_hash":"7ae669fb82bd730fa43f5514c720fdaaa7e7557cb1519930ea8f5a1eac45d71b","after_hash":"17908d6b31856b33ee40d75221c3a6a25edd7bb7d41e4f4ee238ad5f87a3454a","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":"372e7108001e03006a2aea6b856a02007bae70f7dd8b55cacee8bfd848d6ec73"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:34:24.872Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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":"cargo test --locked --test release_pipeline_contract npm_and_bun_share_the_full_native_aggregate -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T15:34:24.686Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:34:24.872Z"}],"before_hash":"17908d6b31856b33ee40d75221c3a6a25edd7bb7d41e4f4ee238ad5f87a3454a","after_hash":"675269280fd61cf5b83d57bc4484a6f780406876a964fb8f5ecaac7d92789c12","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":"ca3880192efe35cdb2fc846ff6ec2cfb2c09d33aa1042d8dd133ea9cd852e7c9"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:41:49.605Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:41:49.605Z","author":"codex-agent","text":"Final resource and oracle evidence: 10,000 SDK-generated items in 10 pages, 1,959,611 source bytes, admission 381,185,760 bytes, peak RSS 43,688 KiB, native wall 28,001 ms, output 988,986 bytes; public tracker copy at 5862034 has 78 items in 1 page, 757,075 source bytes, admission 131,681,424 bytes, peak RSS 13,440 KiB, wall 437 ms, output 12,549 bytes. Both used a 536,870,912-byte declared ceiling and passed complete-list certificates and identity equality. All 11 pages matched canonical CLI JSON payloads with indentation normalization only; source hashes were unchanged. Binary SHA-256 was stable before and after validation. Seven behavioral reverts failed independently, including clock charging and duplicate-path remedies; all restored. CLI 2026.10.23 overlap rechecked; package oracle remains pinned to 2026.10.21. Evidence and reproduction: docs/BOUNDED_READ_SESSIONS.md. PR: https://github.com/unbraind/pm-rust/pull/110. Full CI aggregate evidence pending; item remains in_progress for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:41:49.605Z"}],"before_hash":"675269280fd61cf5b83d57bc4484a6f780406876a964fb8f5ecaac7d92789c12","after_hash":"e843282352c11356dd79709c85023407908cc8d79cc0c61765737ac26be2eb42","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":"a74ab46831861c86500e0a8ee169f20f8f8920c0447b631b4c9dce88afcdbab0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:43:20.716Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:43:20.715Z","author":"codex-agent","text":"Final focused validation: pm test --run --progress --timeout 900 passed all 3 linked commands, totaling 9 SDK session tests, 4 CLI session tests and 1 npm/Bun aggregate wiring test; zero failed or ignored. Strict Clippy across all targets/features, formatting, private rustdoc and strict erasable TypeScript checks passed. Real storage loss and explicit CLI clock selection are covered. Core decode_unique supplies both duplicate-file paths. Clock-storage reservation has its own failing behavioral revert. All seven revert proofs restored; no generated data or binaries are tracked. Implementation is ready for CI and review in PR 110; only full CI evidence remains pending. Release claim without closing so the orchestrator can verify and close."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:43:20.716Z"}],"before_hash":"e843282352c11356dd79709c85023407908cc8d79cc0c61765737ac26be2eb42","after_hash":"daa648c010438ea4149b3e8e7413bffda448d4384cd91d78f667b9fa04a2b332","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":"3c917b3181c86bab26ef4002beed959e19bdb946d18633f364467a160b9c68b5"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:43:24.944Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:43:24.944Z"}],"before_hash":"daa648c010438ea4149b3e8e7413bffda448d4384cd91d78f667b9fa04a2b332","after_hash":"53633ec30ccf79e4a14313b796d6e3dc4c0ef705d43450b599a1f62e81715044","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":"02c8aa7e2e290553f57305ba40218b33cd7a5f5cfbc5db98d879e02a31bef7ea"} diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index 632b00c..a1f391e 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -14,7 +14,8 @@ It uses `Workspace::list_page` unchanged with all statuses, brief projection, caller page size and the existing output token budget. Producer and output continuations retain the original tracker query identity. JSON overrides preserve unpaired UTF-16 compaction cuts; the existing TOON codec can refuse those cuts. -The original tracker is never written. Git, extensions, JavaScript and telemetry +Canonical list recovery flags refer to the ordinary `list` command; session +controls are documented separately. The original tracker is never written. Git, extensions, JavaScript and telemetry are absent from the native path. The package's pinned oracle is unchanged. The snapshot is an immutable capture of individually read files, not a database @@ -28,7 +29,7 @@ session must never be promoted to a complete context read. The memory ceiling is an **admission accounting contract**, not an operating-system RSS limiter. Admission reserves 8 MiB plus 256 bytes per output token, then 128 -bytes per source byte, 8 KiB plus 16 bytes per relative filename byte per copied +bytes per source byte and caller clock byte, 8 KiB plus 16 bytes per relative filename byte per copied file, and 1 KiB per enumerated directory entry. The cumulative ledger conservatively covers source decoding, indexes and rendering; it refuses before reading a file that cannot fit. Source reads are limited to the admitted length plus one byte @@ -73,6 +74,50 @@ Reproduce the non-git public-copy and synthetic measurements with `node scripts/read-session-validate.mts --source path/to/public-tracker-copy`. The harness uses the real SDK's complete-list certificate and bounded list calls as independent oracles, initializes its synthetic tracker through the SDK, checks -all delivered identities and bytes, and measures fresh native child peak RSS with +all delivered identities and compact JSON payloads (normalizing oracle indentation +only), and measures fresh native child peak RSS with GNU time. Only aggregate numbers and hashes are reported; source contents and scratch paths are excluded. + +## Acceptance evidence + +Measured with Rust 1.90.0, the unoptimized native binary and the pinned +2026.10.21 oracle on a contended development host. Every source is a plain, +non-git directory initialized through the real SDK or copied from public Git +objects. Files and directories were read-only during native validation. Each +native child is measured once with GNU time; wall times are observations rather +than performance guarantees. Caller controls: 512 MiB admission ceiling, +1,000 rows per page and 100,000 output tokens per page. + +| Dataset | Items | Pages | Source bytes | Admission bytes | Peak RSS (KiB) | Wall (ms) | Output bytes | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SDK-generated tracker | 10,000 | 10 | 1,959,611 | 381,185,760 | 43,688 | 28,001 | 988,986 | +| Public pm-rust tracker copy | 78 | 1 | 757,075 | 131,681,424 | 13,440 | 437 | 12,549 | + +Both trackers passed the SDK's whole-corpus certificate. All identities matched; +all 11 session pages preserved the published CLI's canonical JSON payloads, +including cursor envelopes and omissions. Only indentation is normalized when +comparing the CLI's pretty continuations with compact embedded frames. The +existing differential suites independently retain exact CLI byte comparisons. +Before/after authoritative source fingerprints matched for both trackers. + +The public copy came from commit +`5862034d9618aba23ee909d7a9f9bbdd1b78e252` of +[`unbraind/pm-rust`](https://github.com/unbraind/pm-rust/tree/5862034d9618aba23ee909d7a9f9bbdd1b78e252/.agents/pm). +Its unchanged source SHA-256 is +`f498f5527c1203481d01f42a473082756e48a173bc5b8dea54b222e8dfb536c2`; +its admitted snapshot SHA-256 is +`f3401ddf796ef684cebaeac8ac130e4a277e90b7132c1ed963cf1655bf74d189`. +The measured native binary SHA-256, checked before and after validation, is +`2f66a145e0d038ab66731965e98b12c5eadd960babc5e05d9f1f854caf05751d`. +Only text evidence is tracked; generated trackers, executable files and raw +outputs remain disposable. + +Seven isolated behavioral reverts compiled and failed their targeted regression: +admission charging, snapshot binding, registry isolation, cancellation cleanup, +omission-only output refusal, clock-storage accounting and duplicate-file +remedies. All functions were restored before successful validation. Tests also +exercise real no-git initialization, a native PATH without Git or Node, empty +trackers, output-budget continuations, peer cancellation before and after a real +query, source length drift, schema symlink refusal, real query storage loss, +explicit cancellation, drop and successful terminal cleanup. diff --git a/scripts/read-session-clock.mts b/scripts/read-session-clock.mts new file mode 100644 index 0000000..c880b97 --- /dev/null +++ b/scripts/read-session-clock.mts @@ -0,0 +1,11 @@ +// Preload a fixed clock in the existing oracle launcher and its final CLI child. +// External wall time and GNU time resource measurements remain real. +const fixed = Date.parse(process.env.FIXED_CLOCK ?? '2026-10-21T12:00:00.000Z'); +globalThis.Date = new Proxy(Date, { + construct(target, args, newTarget) { + return Reflect.construct(target, args.length ? args : [fixed], newTarget); + }, + get(target, key, receiver) { + return key === 'now' ? () => fixed : Reflect.get(target, key, receiver); + }, +}); diff --git a/scripts/read-session-validate.mts b/scripts/read-session-validate.mts index 66785eb..ec019f5 100644 --- a/scripts/read-session-validate.mts +++ b/scripts/read-session-validate.mts @@ -4,6 +4,7 @@ import { createHash } from 'node:crypto'; import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { performance } from 'node:perf_hooks'; import { PmClient, certifyCompleteListResult, serializeItemDocument } from '../tests/oracle/node_modules/@unbrained/pm-cli/dist/sdk/index.js'; import type { ItemDocument } from '../tests/oracle/node_modules/@unbrained/pm-cli/dist/sdk/index.js'; @@ -12,6 +13,8 @@ interface Receipt { protocol: string; source_items: number; source_bytes: number interface Frame { output: { items: { id: string }[]; next_cursor?: string; continuation_kind?: string }; receipt: Receipt } const clock = '2026-10-21T12:00:00.000Z'; const ceiling = 512 * 1024 * 1024; +const pageItems = option('--page-items', '1000'); +assert.ok(Number.isSafeInteger(Number(pageItems)) && Number(pageItems) > 0); /** Read an explicit argument or a safe default. */ function option(flag: string, fallback: string): string { @@ -54,8 +57,9 @@ async function measure(root: string, label: string, binary: string, scratch: str const before = fingerprint(tracker); permissions(tracker, true); const rssFile = join(scratch, `${label}-rss.txt`); + console.error(JSON.stringify({ phase: 'native', label, source_items: complete.complete_list.item_count })); const started = performance.now(); - const result = spawnSync('/usr/bin/time', ['-f', '%M', '-o', rssFile, binary, '--workspace', root, 'read-session', '--memory-bytes', String(ceiling), '--page-items', '1000', '--output-tokens', '100000', '--json'], { + const result = spawnSync('/usr/bin/time', ['-f', '%M', '-o', rssFile, binary, '--workspace', root, 'read-session', '--memory-bytes', String(ceiling), '--page-items', pageItems, '--output-tokens', '100000', '--timestamp', clock, '--json'], { env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname(root) }, maxBuffer: 64 * 1024 * 1024, }); const elapsedMs = performance.now() - started; @@ -66,10 +70,19 @@ async function measure(root: string, label: string, binary: string, scratch: str const ids = frames.flatMap(frame => frame.output.items.map(row => row.id)); assert.equal(new Set(ids).size, complete.complete_list.item_count); assert.deepEqual([...ids].sort(), complete.items.map(row => row.id).sort()); + console.error(JSON.stringify({ phase: 'oracle-pages', label, pages: frames.length })); let after: string | undefined; + let sdkAfter: string | undefined; + const oracleLauncher = fileURLToPath(new URL('../tests/oracle/launch.mts', import.meta.url)); + const clockPreload = pathToFileURL(fileURLToPath(new URL('./read-session-clock.mts', import.meta.url))).href; for (const frame of frames) { - const expected = await client.list({ excludeTerminal: false, brief: true, limit: '1000', outputBudget: '100000', ...(after ? { after } : {}) }); - assert.deepEqual(frame.output, JSON.parse(JSON.stringify(expected)), 'Native page must match the real SDK'); + const expected = await client.list({ excludeTerminal: false, brief: true, limit: pageItems, outputBudget: '100000', ...(sdkAfter ? { after: sdkAfter } : {}) }); + assert.ok('items' in expected, 'SDK page must not be omitted'); + assert.deepEqual(frame.output.items.map(row => row.id), expected.items.map(row => row.id), 'Native rows must match the real SDK'); + sdkAfter = expected.next_cursor ?? undefined; + const oracle = spawnSync(process.execPath, [oracleLauncher, '--path', tracker, 'list', '--all', '--brief', '--limit', pageItems, '--output-budget', '100000', '--json', ...(after ? ['--after', after] : [])], { cwd: root, env: { ...process.env, NODE_OPTIONS: `--import=${clockPreload}`, FIXED_CLOCK: clock }, maxBuffer: 64 * 1024 * 1024 }); + assert.equal(oracle.status, 0, 'Published CLI oracle must succeed'); + assert.equal(JSON.stringify(frame.output), JSON.stringify(JSON.parse(oracle.stdout.toString())), 'Compact native frame must preserve the published CLI JSON payload'); after = frame.output.next_cursor; } const last = frames.at(-1); diff --git a/src/list.rs b/src/list.rs index d09163a..cd9605e 100644 --- a/src/list.rs +++ b/src/list.rs @@ -160,7 +160,7 @@ fn read_selection( }; let documents = query_documents(workspace, projection, filters, options)?; let warnings = unknown_tags(&documents, options.tag.as_deref()); - let lifecycle = crate::list_registry::Lifecycle::load(workspace.query_pm_root())?; + let lifecycle = crate::list_registry::Lifecycle::load(workspace.pm_root())?; let mut documents = select_documents( documents, filters, diff --git a/src/main.rs b/src/main.rs index 4756a19..864238b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,9 @@ struct Cli { enum Command { /// Stream a strictly validated immutable tracker snapshot under resource admission limits. ReadSession { + /// Reproducible read clock for conformance fixtures. + #[arg(long, hide = true)] + timestamp: Option, /// Accounted working-set ceiling in bytes (allocator RSS is measured separately). #[arg(long, default_value_t = 536_870_912)] memory_bytes: u64, @@ -591,6 +594,7 @@ fn run(cli: Cli) -> Result<(), Box> { let workspace = Workspace::discover(&cli.workspace)?; match cli.command { Command::ReadSession { + timestamp, memory_bytes, page_items, output_tokens, @@ -610,7 +614,7 @@ fn run(cli: Cli) -> Result<(), Box> { let mut session = workspace.read_session( &limits, cancellation.clone(), - &pm_rust::current_timestamp(), + ×tamp.unwrap_or_else(pm_rust::current_timestamp), )?; let mut stdout = std::io::stdout().lock(); loop { diff --git a/src/read_session.rs b/src/read_session.rs index 642cc94..ea493be 100644 --- a/src/read_session.rs +++ b/src/read_session.rs @@ -4,11 +4,11 @@ use crate::{ItemFilter, ListOptions, ListOutput, PmRustError, Workspace}; use serde::Serialize; use sha2::{Digest, Sha256}; use std::{ - collections::BTreeSet, + collections::BTreeMap, fmt::Write as _, fs, io::Read, - path::{Component, Path}, + path::{Component, Path, PathBuf}, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -175,14 +175,15 @@ impl Workspace { { return Err(SessionRefusal::InvalidLimits); } - let reserved = 8 * 1024 * 1024 + limits.output_tokens as u64 * 256; + let reserved = (8 * 1024 * 1024 + limits.output_tokens as u64 * 256) + .saturating_add((timestamp.len() as u64).saturating_mul(128)); let mut admission = Admission { ceiling: limits.memory_bytes, used: reserved, bytes: 0, items: 0, hash: Sha256::new(), - ids: BTreeSet::new(), + ids: BTreeMap::new(), cancellation: &cancellation, }; admission.charge(0)?; @@ -345,7 +346,7 @@ struct Admission<'a> { /// Ordered snapshot digest. hash: Sha256, /// Strict global identity uniqueness. - ids: BTreeSet, + ids: BTreeMap, /// Cooperative cancellation boundary. cancellation: &'a SessionCancellation, } @@ -392,14 +393,12 @@ impl Admission<'_> { if item { let text = std::str::from_utf8(&bytes).map_err(|_| SessionRefusal::UnsupportedSource)?; - let document = crate::item::decode_item(source, text)?; - if !self.ids.insert(document.metadata.id.clone()) { - return Err(SessionRefusal::Source(PmRustError::DuplicateItemId { - id: document.metadata.id, - first: source.to_path_buf(), - second: source.to_path_buf(), - })); - } + crate::workspace::decode_unique( + source, + text, + crate::read_projection::Projection::Full, + &mut self.ids, + )?; self.items += 1; } fs::create_dir_all(target.parent().ok_or(SessionRefusal::UnsupportedSource)?)?; diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index 4b5a430..2ebd8d8 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -55,7 +55,7 @@ fn read_session_cli_needs_no_git_or_node_and_emits_bounded_frames() -> TestResul .args(["read-session", "--page-items", "1", "--cancel-file"]) .arg(directory.path().join("absent-cancel-marker")); if json { - command.arg("--json"); + command.args(["--json", "--timestamp", "2026-10-21T12:00:00.000Z"]); } let result = command .env("PATH", no_executables.path()) @@ -82,7 +82,7 @@ fn read_session_cli_needs_no_git_or_node_and_emits_bounded_frames() -> TestResul } } else { assert_eq!(output.matches("---\n").count(), 3); - assert!(output.contains("protocol: pm-rust/read-session/v1")); + assert!(output.contains("pm-rust/read-session/v1")); } } let marker = directory.path().join("cancel"); @@ -174,10 +174,59 @@ fn embedding_admission_and_snapshot_lifetime_are_enforced() -> TestResult { SessionCancellation::default(), "2026-10-21T12:00:00.000Z", )?; - fs::remove_dir_all(workspace.pm_root().join("tasks"))?; + fs::remove_dir_all(workspace.pm_root())?; let page = session.next_page()?.ok_or("snapshot page missing")?; assert_eq!(page.output["items"].as_array().map(Vec::len), Some(3)); assert!(page.receipt.complete); assert!(session.next_page()?.is_none()); Ok(()) } + +/// The CLI polls cancellation at delivery boundaries and removes its scratch snapshot. +#[test] +fn cli_cancellation_and_success_remove_private_storage() -> TestResult { + use std::io::{BufRead, BufReader}; + use std::process::Stdio; + let directory = fixture()?; + let tasks = directory.path().join(".agents/pm/tasks"); + let template = fs::read_to_string(tasks.join("demo-0.toon"))?; + for index in 3..200 { + fs::write( + tasks.join(format!("demo-{index}.toon")), + template.replace("demo-0", &format!("demo-{index}")), + )?; + } + let scratch = tempfile::tempdir()?; + let marker = directory.path().join("cancel-after-page"); + let mut child = Command::new(env!("CARGO_BIN_EXE_pm-rust")) + .current_dir(directory.path()) + .args([ + "read-session", + "--page-items", + "1", + "--json", + "--cancel-file", + ]) + .arg(&marker) + .env("TMPDIR", scratch.path()) + .env("TMP", scratch.path()) + .env("TEMP", scratch.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + let mut stdout = BufReader::new(child.stdout.take().ok_or("missing stdout")?); + let mut first = String::new(); + stdout.read_line(&mut first)?; + let frame: Value = serde_json::from_str(&first)?; + assert_eq!(frame["receipt"]["complete"], false); + fs::write(&marker, "cancel")?; + // Drain after setting the marker, so even a full pipe cannot block cancellation. + let mut remaining = String::new(); + std::io::Read::read_to_string(&mut stdout, &mut remaining)?; + let result = child.wait_with_output()?; + assert_eq!(result.status.code(), Some(2)); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert_eq!(refusal["code"], "session_cancelled"); + assert_eq!(fs::read_dir(scratch.path())?.count(), 0); + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 4dd5627..65a2ab5 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -1,5 +1,6 @@ //! Session admission, immutable-source, terminal cleanup and refusal contracts. use super::*; +use std::collections::BTreeSet; /// Shared fallible test return. type TestResult = Result<(), Box>; @@ -70,7 +71,7 @@ fn session_pages_reuse_canonical_output_and_freeze_source() -> TestResult { } let mut session = workspace.read_session(&limits, SessionCancellation::default(), CLOCK)?; let snapshot = session.workspace.pm_root().to_path_buf(); - fs::remove_dir_all(workspace.pm_root().join("tasks"))?; + fs::remove_dir_all(workspace.pm_root())?; assert!(session.next_page()?.is_some()); session.cancel()?; assert!(!snapshot.exists()); @@ -168,10 +169,14 @@ fn snapshot_validates_all_items_and_schema_before_delivery() -> TestResult { }; let item = workspace.pm_root().join("tasks/nested/demo-0.toon"); fs::copy(&item, workspace.pm_root().join("tasks/duplicate.toon"))?; - assert!(matches!( - open(), - Err(SessionRefusal::Source(PmRustError::DuplicateItemId { .. })) - )); + let Err(SessionRefusal::Source(PmRustError::DuplicateItemId { first, second, .. })) = open() + else { + return Err("duplicate source must refuse".into()); + }; + assert_ne!( + first, second, + "refusal must identify both conflicting files" + ); fs::write(workspace.pm_root().join("tasks/duplicate.toon"), "invalid")?; assert!(matches!( open(), @@ -259,7 +264,7 @@ fn admission(token: &SessionCancellation) -> Admission<'_> { bytes: 0, items: 0, hash: Sha256::new(), - ids: BTreeSet::new(), + ids: BTreeMap::new(), cancellation: token, } } @@ -379,3 +384,43 @@ fn schema_directory_links_cannot_escape_snapshot_admission() -> TestResult { )); Ok(()) } + +/// Caller clock storage participates in admission before its owned copy is retained. +#[test] +fn clock_storage_is_admitted_before_session_construction() -> TestResult { + let (_directory, workspace) = fixture(1)?; + let limits = ReadSessionLimits { + memory_bytes: 16 * 1024 * 1024, + ..ReadSessionLimits::default() + }; + assert!(matches!( + workspace.read_session( + &limits, + SessionCancellation::default(), + &"x".repeat(128 * 1024) + ), + Err(SessionRefusal::MemoryLimit) + )); + Ok(()) +} + +/// A real storage loss during a canonical query terminates and cleans the session. +#[test] +fn query_storage_failure_releases_snapshot_and_never_certifies_completion() -> TestResult { + let (_directory, workspace) = fixture(1)?; + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + )?; + let snapshot = session.workspace.pm_root().to_path_buf(); + fs::remove_file(snapshot.join("settings.json"))?; + assert!(matches!( + session.next_page(), + Err(SessionRefusal::Source(_)) + )); + assert!(!session.receipt().complete); + assert!(!snapshot.exists()); + assert!(session.next_page()?.is_none()); + Ok(()) +} From d1325cdcbc0ff14af2f2657699bc030b6a2e9c98 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:56:37 +0200 Subject: [PATCH 04/10] fix: preserve Windows registry paths outside read snapshots --- .agents/pm/features/pm-rust-bounded-read-sessions.toon | 9 ++++++--- .agents/pm/history/pm-rust-bounded-read-sessions.jsonl | 5 +++++ docs/BOUNDED_READ_SESSIONS.md | 6 +++--- src/list.rs | 2 +- src/workspace.rs | 9 +++++++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index d6209b8..a1cc19e 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T15:43:24.944Z" +updated_at: "2026-10-23T15:56:25.145Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[7]{created_at,author,text}: +comments[9]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -18,6 +18,8 @@ comments[7]{created_at,author,text}: "2026-10-23T15:20:20.151Z",codex-agent,"Behavioral revert proof complete: separately disable Admission::charge, bypass Workspace::with_read_snapshot, restore lifecycle reads to the live query root, bypass cleanup on cancellation, and remove the result_omitted guard. Each isolated variant compiled and failed its targeted regression (five expected exit-101 test failures); all original functions were restored. Restored linked suites passed 7 SDK plus 4 CLI tests; the npm/Bun aggregate wiring regression passed 1 test; formatting, strict Clippy, private rustdoc and strict erasable TypeScript checks passed. The public copy has 78 items and unchanged authoritative source hashes. Final large measurements and CI remain pending." "2026-10-23T15:41:49.605Z",codex-agent,"Final resource and oracle evidence: 10,000 SDK-generated items in 10 pages, 1,959,611 source bytes, admission 381,185,760 bytes, peak RSS 43,688 KiB, native wall 28,001 ms, output 988,986 bytes; public tracker copy at 5862034 has 78 items in 1 page, 757,075 source bytes, admission 131,681,424 bytes, peak RSS 13,440 KiB, wall 437 ms, output 12,549 bytes. Both used a 536,870,912-byte declared ceiling and passed complete-list certificates and identity equality. All 11 pages matched canonical CLI JSON payloads with indentation normalization only; source hashes were unchanged. Binary SHA-256 was stable before and after validation. Seven behavioral reverts failed independently, including clock charging and duplicate-path remedies; all restored. CLI 2026.10.23 overlap rechecked; package oracle remains pinned to 2026.10.21. Evidence and reproduction: docs/BOUNDED_READ_SESSIONS.md. PR: https://github.com/unbraind/pm-rust/pull/110. Full CI aggregate evidence pending; item remains in_progress for orchestrator verification." "2026-10-23T15:43:20.715Z",codex-agent,"Final focused validation: pm test --run --progress --timeout 900 passed all 3 linked commands, totaling 9 SDK session tests, 4 CLI session tests and 1 npm/Bun aggregate wiring test; zero failed or ignored. Strict Clippy across all targets/features, formatting, private rustdoc and strict erasable TypeScript checks passed. Real storage loss and explicit CLI clock selection are covered. Core decode_unique supplies both duplicate-file paths. Clock-storage reservation has its own failing behavioral revert. All seven revert proofs restored; no generated data or binaries are tracked. Implementation is ready for CI and review in PR 110; only full CI evidence remains pending. Release claim without closing so the orchestrator can verify and close." + "2026-10-23T15:52:07.547Z",codex-agent,"Exact-head Windows CI found a registry diagnostic path regression: normal reads reported canonical extended-length paths instead of the published caller spelling. Preserve query-root spelling for ordinary registry reads, while sessions resolve registry storage to their private snapshot. Existing workflow_fallbacks_and_settings_errors regression detected this. Reclaim for the compatibility correction; thresholds remain unchanged." + "2026-10-23T15:56:21.378Z",codex-agent,"Windows diagnostic correction passes the targeted registry regression plus the 9 SDK, 4 CLI and 1 pipeline wiring tests locally. Repeated final-binary validation after the correction: synthetic 10,000 items / 10 pages, admission 381,185,760 bytes, peak RSS 43,396 KiB, wall 23,797 ms, output 988,986 bytes; public copy 78 items / 1 page, admission 131,681,424 bytes, peak RSS 13,568 KiB, wall 167 ms, output 12,549 bytes. Both remain under the declared 536,870,912-byte ceiling, with all 11 canonical oracle payload comparisons, SDK certificates, identity equality and unchanged source hashes passing. Final binary SHA-256 verified before/after: 6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a. docs/BOUNDED_READ_SESSIONS.md updated. Release claim; Windows and full CI gates must verify the new head." files[15]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project @@ -34,10 +36,11 @@ files[15]{path,scope}: tests/read_session_contract.rs,project tests/release_pipeline_contract.rs,project tests/support/read_session_unit.rs,project -tests[3]{command,scope,provenance{author,created_at,source_kind,source_ref}}: +tests[4]{command,scope,provenance{author,created_at,source_kind,source_ref}}: "cargo test --locked --lib read_session::tests -- --test-threads=1",project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test read_session_contract -- --test-threads=1,project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test release_pipeline_contract npm_and_bun_share_the_full_native_aggregate -- --test-threads=1,project,codex-agent,"2026-10-23T15:34:24.686Z",local_mutation,feat/bounded-read-sessions + "cargo test --locked --lib list_registry::tests::workflow_fallbacks_and_settings_errors -- --test-threads=1",project,codex-agent,"2026-10-23T15:56:02.726Z",local_mutation,feat/bounded-read-sessions docs[3]{path,scope}: docs/BOUNDED_READ_SESSIONS.md,project examples/read_session.rs,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index 61f4c03..fb2fa01 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -14,3 +14,8 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:41:49.605Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:41:49.605Z","author":"codex-agent","text":"Final resource and oracle evidence: 10,000 SDK-generated items in 10 pages, 1,959,611 source bytes, admission 381,185,760 bytes, peak RSS 43,688 KiB, native wall 28,001 ms, output 988,986 bytes; public tracker copy at 5862034 has 78 items in 1 page, 757,075 source bytes, admission 131,681,424 bytes, peak RSS 13,440 KiB, wall 437 ms, output 12,549 bytes. Both used a 536,870,912-byte declared ceiling and passed complete-list certificates and identity equality. All 11 pages matched canonical CLI JSON payloads with indentation normalization only; source hashes were unchanged. Binary SHA-256 was stable before and after validation. Seven behavioral reverts failed independently, including clock charging and duplicate-path remedies; all restored. CLI 2026.10.23 overlap rechecked; package oracle remains pinned to 2026.10.21. Evidence and reproduction: docs/BOUNDED_READ_SESSIONS.md. PR: https://github.com/unbraind/pm-rust/pull/110. Full CI aggregate evidence pending; item remains in_progress for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:41:49.605Z"}],"before_hash":"675269280fd61cf5b83d57bc4484a6f780406876a964fb8f5ecaac7d92789c12","after_hash":"e843282352c11356dd79709c85023407908cc8d79cc0c61765737ac26be2eb42","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":"a74ab46831861c86500e0a8ee169f20f8f8920c0447b631b4c9dce88afcdbab0"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:43:20.716Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:43:20.715Z","author":"codex-agent","text":"Final focused validation: pm test --run --progress --timeout 900 passed all 3 linked commands, totaling 9 SDK session tests, 4 CLI session tests and 1 npm/Bun aggregate wiring test; zero failed or ignored. Strict Clippy across all targets/features, formatting, private rustdoc and strict erasable TypeScript checks passed. Real storage loss and explicit CLI clock selection are covered. Core decode_unique supplies both duplicate-file paths. Clock-storage reservation has its own failing behavioral revert. All seven revert proofs restored; no generated data or binaries are tracked. Implementation is ready for CI and review in PR 110; only full CI evidence remains pending. Release claim without closing so the orchestrator can verify and close."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:43:20.716Z"}],"before_hash":"e843282352c11356dd79709c85023407908cc8d79cc0c61765737ac26be2eb42","after_hash":"daa648c010438ea4149b3e8e7413bffda448d4384cd91d78f667b9fa04a2b332","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":"3c917b3181c86bab26ef4002beed959e19bdb946d18633f364467a160b9c68b5"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:43:24.944Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:43:24.944Z"}],"before_hash":"daa648c010438ea4149b3e8e7413bffda448d4384cd91d78f667b9fa04a2b332","after_hash":"53633ec30ccf79e4a14313b796d6e3dc4c0ef705d43450b599a1f62e81715044","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":"02c8aa7e2e290553f57305ba40218b33cd7a5f5cfbc5db98d879e02a31bef7ea"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:52:05.619Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:52:05.619Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"53633ec30ccf79e4a14313b796d6e3dc4c0ef705d43450b599a1f62e81715044","after_hash":"118cc64759dd1335bd57a5d60d6522563f1db171ff336cd3e51d8bc23361cade","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":"9e76582afc8646fc07ba9ecdc3e26836b2718f50ebb55b5013fd1a8346a8a168"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:52:07.547Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:52:07.547Z","author":"codex-agent","text":"Exact-head Windows CI found a registry diagnostic path regression: normal reads reported canonical extended-length paths instead of the published caller spelling. Preserve query-root spelling for ordinary registry reads, while sessions resolve registry storage to their private snapshot. Existing workflow_fallbacks_and_settings_errors regression detected this. Reclaim for the compatibility correction; thresholds remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:52:07.547Z"}],"before_hash":"118cc64759dd1335bd57a5d60d6522563f1db171ff336cd3e51d8bc23361cade","after_hash":"d7e0f12c863fcf444d2fb6e20974da0ead3c6d3760cbc22b178186e56e6db267","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":"6db3379206cfab9a2bc2bde770fb32eda8270d7e2157725b313d13a219f01024"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:56:02.934Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/3","value":{"command":"cargo test --locked --lib list_registry::tests::workflow_fallbacks_and_settings_errors -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T15:56:02.726Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:56:02.934Z"}],"before_hash":"d7e0f12c863fcf444d2fb6e20974da0ead3c6d3760cbc22b178186e56e6db267","after_hash":"0952e97318f46d8e148ec067962793e070630d4ea9983cd36a86bc1db90812e6","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":"add6e819de7c11d44a3389de71431bbb3b0a37dccd94f17aef40c21b7a23ac34"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:56:21.379Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:56:21.378Z","author":"codex-agent","text":"Windows diagnostic correction passes the targeted registry regression plus the 9 SDK, 4 CLI and 1 pipeline wiring tests locally. Repeated final-binary validation after the correction: synthetic 10,000 items / 10 pages, admission 381,185,760 bytes, peak RSS 43,396 KiB, wall 23,797 ms, output 988,986 bytes; public copy 78 items / 1 page, admission 131,681,424 bytes, peak RSS 13,568 KiB, wall 167 ms, output 12,549 bytes. Both remain under the declared 536,870,912-byte ceiling, with all 11 canonical oracle payload comparisons, SDK certificates, identity equality and unchanged source hashes passing. Final binary SHA-256 verified before/after: 6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a. docs/BOUNDED_READ_SESSIONS.md updated. Release claim; Windows and full CI gates must verify the new head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:56:21.379Z"}],"before_hash":"0952e97318f46d8e148ec067962793e070630d4ea9983cd36a86bc1db90812e6","after_hash":"655d158147150ecb2137b301cbcf2217785e7e65e6b20aaede602a7e536c5ce3","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":"d49bc43d76d68428faf1427ab2581f43a87e0d96ddd0e1072afafad325f961e6"} +{"hash_algorithm":"sha256","ts":"2026-10-23T15:56:25.145Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:56:25.145Z"}],"before_hash":"655d158147150ecb2137b301cbcf2217785e7e65e6b20aaede602a7e536c5ce3","after_hash":"6848d56b594ce3b72cd71adc09d6271a28f5cf8e9d66963a61f73162a4e99f16","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":"0506310421f78db8df275a7bc2ad7fed22aa30e666458f0236f661bae275fb72"} diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index a1f391e..cede27a 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -91,8 +91,8 @@ than performance guarantees. Caller controls: 512 MiB admission ceiling, | Dataset | Items | Pages | Source bytes | Admission bytes | Peak RSS (KiB) | Wall (ms) | Output bytes | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -| SDK-generated tracker | 10,000 | 10 | 1,959,611 | 381,185,760 | 43,688 | 28,001 | 988,986 | -| Public pm-rust tracker copy | 78 | 1 | 757,075 | 131,681,424 | 13,440 | 437 | 12,549 | +| SDK-generated tracker | 10,000 | 10 | 1,959,611 | 381,185,760 | 43,396 | 23,797 | 988,986 | +| Public pm-rust tracker copy | 78 | 1 | 757,075 | 131,681,424 | 13,568 | 167 | 12,549 | Both trackers passed the SDK's whole-corpus certificate. All identities matched; all 11 session pages preserved the published CLI's canonical JSON payloads, @@ -109,7 +109,7 @@ Its unchanged source SHA-256 is its admitted snapshot SHA-256 is `f3401ddf796ef684cebaeac8ac130e4a277e90b7132c1ed963cf1655bf74d189`. The measured native binary SHA-256, checked before and after validation, is -`2f66a145e0d038ab66731965e98b12c5eadd960babc5e05d9f1f854caf05751d`. +`6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a`. Only text evidence is tracked; generated trackers, executable files and raw outputs remain disposable. diff --git a/src/list.rs b/src/list.rs index cd9605e..664429d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -160,7 +160,7 @@ fn read_selection( }; let documents = query_documents(workspace, projection, filters, options)?; let warnings = unknown_tags(&documents, options.tag.as_deref()); - let lifecycle = crate::list_registry::Lifecycle::load(workspace.pm_root())?; + let lifecycle = crate::list_registry::Lifecycle::load(workspace.read_pm_root())?; let mut documents = select_documents( documents, filters, diff --git a/src/workspace.rs b/src/workspace.rs index 023a134..eeba18f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -180,6 +180,15 @@ impl Workspace { .unwrap_or_else(|| query_root_path(&self.query_pm_root, &self.pm_root, cfg!(windows))) } + /// Selects snapshot storage or the ordinary published read-path spelling. + pub(crate) fn read_pm_root(&self) -> &Path { + if self.session_query_identity.is_some() { + &self.pm_root + } else { + self.query_pm_root() + } + } + /// Reads and validates every stored TOON item, sorted by identifier. /// /// # Errors From 7a8c0de017c0bdd3852d9c034c0979fae46c372e Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:16:14 +0200 Subject: [PATCH 05/10] Keep failed session delivery uncertified and exercise storage boundaries --- .../pm-rust-bounded-read-sessions.toon | 13 +- .../pm-rust-bounded-read-sessions.jsonl | 7 + README.md | 2 +- docs/BOUNDED_READ_SESSIONS.md | 27 +- src/main.rs | 31 +- src/read_session.rs | 112 +++++-- tests/read_session_contract.rs | 54 ++++ tests/support/main_unit.rs | 36 +++ tests/support/read_session_unit.rs | 294 ++++++++++++++++++ 9 files changed, 520 insertions(+), 56 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index a1cc19e..455701e 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T15:56:25.145Z" +updated_at: "2026-10-23T17:15:07.881Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[9]{created_at,author,text}: +comments[12]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -20,7 +20,10 @@ comments[9]{created_at,author,text}: "2026-10-23T15:43:20.715Z",codex-agent,"Final focused validation: pm test --run --progress --timeout 900 passed all 3 linked commands, totaling 9 SDK session tests, 4 CLI session tests and 1 npm/Bun aggregate wiring test; zero failed or ignored. Strict Clippy across all targets/features, formatting, private rustdoc and strict erasable TypeScript checks passed. Real storage loss and explicit CLI clock selection are covered. Core decode_unique supplies both duplicate-file paths. Clock-storage reservation has its own failing behavioral revert. All seven revert proofs restored; no generated data or binaries are tracked. Implementation is ready for CI and review in PR 110; only full CI evidence remains pending. Release claim without closing so the orchestrator can verify and close." "2026-10-23T15:52:07.547Z",codex-agent,"Exact-head Windows CI found a registry diagnostic path regression: normal reads reported canonical extended-length paths instead of the published caller spelling. Preserve query-root spelling for ordinary registry reads, while sessions resolve registry storage to their private snapshot. Existing workflow_fallbacks_and_settings_errors regression detected this. Reclaim for the compatibility correction; thresholds remain unchanged." "2026-10-23T15:56:21.378Z",codex-agent,"Windows diagnostic correction passes the targeted registry regression plus the 9 SDK, 4 CLI and 1 pipeline wiring tests locally. Repeated final-binary validation after the correction: synthetic 10,000 items / 10 pages, admission 381,185,760 bytes, peak RSS 43,396 KiB, wall 23,797 ms, output 988,986 bytes; public copy 78 items / 1 page, admission 131,681,424 bytes, peak RSS 13,568 KiB, wall 167 ms, output 12,549 bytes. Both remain under the declared 536,870,912-byte ceiling, with all 11 canonical oracle payload comparisons, SDK certificates, identity equality and unchanged source hashes passing. Final binary SHA-256 verified before/after: 6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a. docs/BOUNDED_READ_SESSIONS.md updated. Release claim; Windows and full CI gates must verify the new head." -files[15]{path,scope}: + "2026-10-23T16:33:37.805Z",codex-agent,"CI run 39982597630 completed the regular and coverage-instrumented suites, then enforced unchanged thresholds and found one uncovered output-cursor branch, two lines and 30 error-propagation regions. Add actual filesystem fault coverage, bounded-reader I/O tests and consumer-disconnect writer tests without SDK mocks. Coverage review also identified a delivery invariant: completion/pages must update only after successful terminal cleanup, so a cleanup error cannot certify an undelivered final page. Reclaim for that correction and complete fault-path verification; keep all four coverage thresholds at 100." + "2026-10-23T17:07:49.714Z",codex-agent,"CI retained all four 100-percent thresholds and exposed untested storage propagation plus a final-cleanup ordering defect. Final-page cleanup now precedes receipt certification; failed cleanup remains terminal without counting an undelivered page. Added genuine output-cursor continuation over 300 rows, directory/read/write/flush faults, and consumer-disconnect coverage. All five linked commands passed: 14 SDK, 5 CLI, 1 framing, 1 pipeline and 1 registry regression tests, zero failures or ignored tests. Strict Clippy passed. Delivery-cleanup and output-cursor behavioral reverts compiled and each failed their new regression; restored code passed, bringing independent revert proofs to nine. Full npm and Bun release gates remain reserved for CI." + "2026-10-23T17:14:54.325Z",codex-agent,"Final corrected-binary validation passed on SDK-created and read-only non-git public tracker copies. Synthetic: 10000 items, 10 pages, 1959611 source bytes, 381185760 admitted bytes, 43652 KiB peak RSS, 131120 ms wall, 988986 output bytes. Public copy: 78 items, 1 page, 757075 source bytes, 131681424 admitted bytes, 13824 KiB peak RSS, 782 ms wall, 12549 output bytes. Declared ceiling 536870912 bytes. Both complete SDK certificates passed; all identities and 11 canonical CLI JSON payloads matched after indentation normalization only. Source fingerprints unchanged. Measured binary SHA-256 b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71 matched before and after. Runnable Rust example completed 78 items in four pages with neither Node nor Git on PATH. Private rustdoc, formatting and strict Clippy passed. Runtime test-result recording is disabled by existing project policy; linked execution and this comment retain the results. Ready PR 110 remains subject to the unchanged full CI gates; claim released for orchestrator verification, item remains in progress." +files[16]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project justfile,project @@ -35,12 +38,14 @@ files[15]{path,scope}: src/workspace.rs,project tests/read_session_contract.rs,project tests/release_pipeline_contract.rs,project + tests/support/main_unit.rs,project tests/support/read_session_unit.rs,project -tests[4]{command,scope,provenance{author,created_at,source_kind,source_ref}}: +tests[5]{command,scope,provenance{author,created_at,source_kind,source_ref}}: "cargo test --locked --lib read_session::tests -- --test-threads=1",project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test read_session_contract -- --test-threads=1,project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test release_pipeline_contract npm_and_bun_share_the_full_native_aggregate -- --test-threads=1,project,codex-agent,"2026-10-23T15:34:24.686Z",local_mutation,feat/bounded-read-sessions "cargo test --locked --lib list_registry::tests::workflow_fallbacks_and_settings_errors -- --test-threads=1",project,codex-agent,"2026-10-23T15:56:02.726Z",local_mutation,feat/bounded-read-sessions + cargo test --locked --bin pm-rust session_frame_writer_preserves_errors_and_framing -- --test-threads=1,project,codex-agent,"2026-10-23T17:03:49.263Z",local_mutation,feat/bounded-read-sessions docs[3]{path,scope}: docs/BOUNDED_READ_SESSIONS.md,project examples/read_session.rs,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index fb2fa01..9e53405 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -19,3 +19,10 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T15:56:02.934Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/3","value":{"command":"cargo test --locked --lib list_registry::tests::workflow_fallbacks_and_settings_errors -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T15:56:02.726Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:56:02.934Z"}],"before_hash":"d7e0f12c863fcf444d2fb6e20974da0ead3c6d3760cbc22b178186e56e6db267","after_hash":"0952e97318f46d8e148ec067962793e070630d4ea9983cd36a86bc1db90812e6","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":"add6e819de7c11d44a3389de71431bbb3b0a37dccd94f17aef40c21b7a23ac34"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:56:21.379Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:56:21.378Z","author":"codex-agent","text":"Windows diagnostic correction passes the targeted registry regression plus the 9 SDK, 4 CLI and 1 pipeline wiring tests locally. Repeated final-binary validation after the correction: synthetic 10,000 items / 10 pages, admission 381,185,760 bytes, peak RSS 43,396 KiB, wall 23,797 ms, output 988,986 bytes; public copy 78 items / 1 page, admission 131,681,424 bytes, peak RSS 13,568 KiB, wall 167 ms, output 12,549 bytes. Both remain under the declared 536,870,912-byte ceiling, with all 11 canonical oracle payload comparisons, SDK certificates, identity equality and unchanged source hashes passing. Final binary SHA-256 verified before/after: 6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a. docs/BOUNDED_READ_SESSIONS.md updated. Release claim; Windows and full CI gates must verify the new head."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T15:56:21.379Z"}],"before_hash":"0952e97318f46d8e148ec067962793e070630d4ea9983cd36a86bc1db90812e6","after_hash":"655d158147150ecb2137b301cbcf2217785e7e65e6b20aaede602a7e536c5ce3","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":"d49bc43d76d68428faf1427ab2581f43a87e0d96ddd0e1072afafad325f961e6"} {"hash_algorithm":"sha256","ts":"2026-10-23T15:56:25.145Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T15:56:25.145Z"}],"before_hash":"655d158147150ecb2137b301cbcf2217785e7e65e6b20aaede602a7e536c5ce3","after_hash":"6848d56b594ce3b72cd71adc09d6271a28f5cf8e9d66963a61f73162a4e99f16","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":"0506310421f78db8df275a7bc2ad7fed22aa30e666458f0236f661bae275fb72"} +{"hash_algorithm":"sha256","ts":"2026-10-23T16:33:35.573Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T16:33:35.573Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"6848d56b594ce3b72cd71adc09d6271a28f5cf8e9d66963a61f73162a4e99f16","after_hash":"4a75d583e7aab65e6cdf5f0fb31a641cd347c97d4c922a45dd02dd17a14840b6","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":"7ff159567590cd792f9607c1a606452eb01ef945b180463e9c9ed690e431194e"} +{"hash_algorithm":"sha256","ts":"2026-10-23T16:33:37.806Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T16:33:37.805Z","author":"codex-agent","text":"CI run 39982597630 completed the regular and coverage-instrumented suites, then enforced unchanged thresholds and found one uncovered output-cursor branch, two lines and 30 error-propagation regions. Add actual filesystem fault coverage, bounded-reader I/O tests and consumer-disconnect writer tests without SDK mocks. Coverage review also identified a delivery invariant: completion/pages must update only after successful terminal cleanup, so a cleanup error cannot certify an undelivered final page. Reclaim for that correction and complete fault-path verification; keep all four coverage thresholds at 100."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T16:33:37.806Z"}],"before_hash":"4a75d583e7aab65e6cdf5f0fb31a641cd347c97d4c922a45dd02dd17a14840b6","after_hash":"22c539211c8257613b4b57e14063e63ba026a62779cb52b37a0c1b8a262c03c4","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":"b2eaf888824e128cb446ca94f07765d4655b97426930154a58371ce90a94e71c"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:03:36.606Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/files/14/path","value":"tests/support/main_unit.rs"},{"op":"add","path":"/metadata/files/15","value":{"path":"tests/support/read_session_unit.rs","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:03:36.606Z"}],"before_hash":"22c539211c8257613b4b57e14063e63ba026a62779cb52b37a0c1b8a262c03c4","after_hash":"0f06d2def879b51d440da9b5b08b1ebd248d1af58a1126f3ff432244636c1744","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":"4f4f5df2ee2f3d3d598260368d82914a1b64aaaddde94f98dd7da16b8a7048e6"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:03:49.575Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/4","value":{"command":"cargo test --locked --bin pm-rust session_frame_writer_preserves_errors_and_framing -- --test-threads=1","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T17:03:49.263Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:03:49.575Z"}],"before_hash":"0f06d2def879b51d440da9b5b08b1ebd248d1af58a1126f3ff432244636c1744","after_hash":"42d8a245f65bfdc27f1bffaf717e6c18eb24e8c5a03a04cf214efc3c00bc4cd1","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":"c8d437339269820023ef5fd0bcf904801145d4a5ea49a9a00331784e50d03d66"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:07:49.715Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:07:49.714Z","author":"codex-agent","text":"CI retained all four 100-percent thresholds and exposed untested storage propagation plus a final-cleanup ordering defect. Final-page cleanup now precedes receipt certification; failed cleanup remains terminal without counting an undelivered page. Added genuine output-cursor continuation over 300 rows, directory/read/write/flush faults, and consumer-disconnect coverage. All five linked commands passed: 14 SDK, 5 CLI, 1 framing, 1 pipeline and 1 registry regression tests, zero failures or ignored tests. Strict Clippy passed. Delivery-cleanup and output-cursor behavioral reverts compiled and each failed their new regression; restored code passed, bringing independent revert proofs to nine. Full npm and Bun release gates remain reserved for CI."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:07:49.715Z"}],"before_hash":"42d8a245f65bfdc27f1bffaf717e6c18eb24e8c5a03a04cf214efc3c00bc4cd1","after_hash":"067ad052004c1391f08c02a827b7e043245cc5a4ad03d73859ad5412257740db","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":"3c9ce99b404bd09144cf0672e489fd55f465d9a20ecaaf685ef04b012b9c44c2"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:14:54.332Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:14:54.325Z","author":"codex-agent","text":"Final corrected-binary validation passed on SDK-created and read-only non-git public tracker copies. Synthetic: 10000 items, 10 pages, 1959611 source bytes, 381185760 admitted bytes, 43652 KiB peak RSS, 131120 ms wall, 988986 output bytes. Public copy: 78 items, 1 page, 757075 source bytes, 131681424 admitted bytes, 13824 KiB peak RSS, 782 ms wall, 12549 output bytes. Declared ceiling 536870912 bytes. Both complete SDK certificates passed; all identities and 11 canonical CLI JSON payloads matched after indentation normalization only. Source fingerprints unchanged. Measured binary SHA-256 b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71 matched before and after. Runnable Rust example completed 78 items in four pages with neither Node nor Git on PATH. Private rustdoc, formatting and strict Clippy passed. Runtime test-result recording is disabled by existing project policy; linked execution and this comment retain the results. Ready PR 110 remains subject to the unchanged full CI gates; claim released for orchestrator verification, item remains in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:14:54.332Z"}],"before_hash":"067ad052004c1391f08c02a827b7e043245cc5a4ad03d73859ad5412257740db","after_hash":"a27de2ba324af481719406c2ed732138cc65bbd5a2ae67755aa72920be46032f","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":"5b20d791165d7eb1c52bff673713c88f505c6f0df3708e3d806f49f2762f4a8b"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:15:07.881Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:15:07.881Z"}],"before_hash":"a27de2ba324af481719406c2ed732138cc65bbd5a2ae67755aa72920be46032f","after_hash":"bdb626821f6445e2a2c66c11b9d5125a707de97c5aba20b2800b330cc88e1b8d","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":"0c17155adc53843b138200671f1b607b97a737cfbf29caf9c9f111e6c5904598"} diff --git a/README.md b/README.md index 196dfa0..91c7d1b 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ receipt and canonical page followed by `---`. An optional `--cancel-file` marker requests cancellation before admission or between pages; removing it allows a new CLI invocation. SDK callers can share `SessionCancellation` across threads or call `ReadSession::cancel()` for immediate cleanup while idle. Drop and terminal pulls -remove the snapshot. See the [embedding contract](docs/BOUNDED_READ_SESSIONS.md) for +attempt snapshot cleanup; explicit calls report storage failures. See the [embedding contract](docs/BOUNDED_READ_SESSIONS.md) for accounting limits, cancellation latency, integrity boundaries and typed remedies. ## Without git diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index cede27a..6b825e9 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -2,7 +2,8 @@ The v1 embedding surface is `Workspace::read_session`, `ReadSessionLimits`, `ReadSession::next_page`, `ReadSession::receipt`, `ReadSession::cancel`, -`SessionCancellation`, `SessionPage`, `SessionReceipt` and `SessionRefusal`. +`SessionCancellation`, `SessionPage` (including `stringify_toon`), +`SessionReceipt` and `SessionRefusal`. The [runnable Rust host](../examples/read_session.rs) prints each canonical page and then drops it. Applications retain ownership of returned pages and cancellation handles; accumulating pages consumes application memory outside session accounting. @@ -61,11 +62,15 @@ at page boundaries; it is not an OS signal handler. | `session_cancelled` | Reopen with a fresh cancellation token | | `session_memory_limit` | Increase the admitted byte ceiling or use a smaller tracker | | `session_invalid_limits` | Use positive controls; row/token controls are at most 1,000,000 | -| `session_unsupported_source` | Use regular files, at most 32 directory levels, tracker-relative schema files | +| `session_unsupported_source` | Use stable UTF-8 regular files, valid settings JSON, at most 32 directory levels, tracker-relative schema files | | `session_source_refused` | Repair tracker integrity or increase the per-page token budget | | `session_storage_failed` | Check temporary storage permissions and free space | -All failures terminate delivery and release admitted snapshot resources. Refusals +Terminal outcomes remove private storage when the operating system permits it. +Cleanup I/O failures terminate delivery, keep an undelivered final page uncertified, +and may require the host to remove remaining private files after repairing scratch +storage permissions. Drop attempts cleanup; use explicit cancellation or a pull +result to observe cleanup errors. Refusals are typed in Rust and emitted as compact structured JSON on CLI stderr with exit code 2. An output writer failure drops the session through ordinary Rust unwinding. The source tracker remains unchanged after cancellation and failed admission. @@ -91,8 +96,8 @@ than performance guarantees. Caller controls: 512 MiB admission ceiling, | Dataset | Items | Pages | Source bytes | Admission bytes | Peak RSS (KiB) | Wall (ms) | Output bytes | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -| SDK-generated tracker | 10,000 | 10 | 1,959,611 | 381,185,760 | 43,396 | 23,797 | 988,986 | -| Public pm-rust tracker copy | 78 | 1 | 757,075 | 131,681,424 | 13,568 | 167 | 12,549 | +| SDK-generated tracker | 10,000 | 10 | 1,959,611 | 381,185,760 | 43,652 | 131,120 | 988,986 | +| Public pm-rust tracker copy | 78 | 1 | 757,075 | 131,681,424 | 13,824 | 782 | 12,549 | Both trackers passed the SDK's whole-corpus certificate. All identities matched; all 11 session pages preserved the published CLI's canonical JSON payloads, @@ -109,15 +114,17 @@ Its unchanged source SHA-256 is its admitted snapshot SHA-256 is `f3401ddf796ef684cebaeac8ac130e4a277e90b7132c1ed963cf1655bf74d189`. The measured native binary SHA-256, checked before and after validation, is -`6d591b2a41c1edc0c02fb2f055f2fc0f35cc333eb8acdcee9447ac1c13c3d35a`. +`b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71`. Only text evidence is tracked; generated trackers, executable files and raw outputs remain disposable. -Seven isolated behavioral reverts compiled and failed their targeted regression: +Nine isolated behavioral reverts compiled and failed their targeted regression: admission charging, snapshot binding, registry isolation, cancellation cleanup, -omission-only output refusal, clock-storage accounting and duplicate-file -remedies. All functions were restored before successful validation. Tests also +omission-only output refusal, clock-storage accounting, duplicate-file +remedies, delivery cleanup ordering and output-cursor continuation. All functions +were restored before successful validation. Tests also exercise real no-git initialization, a native PATH without Git or Node, empty trackers, output-budget continuations, peer cancellation before and after a real query, source length drift, schema symlink refusal, real query storage loss, -explicit cancellation, drop and successful terminal cleanup. +explicit cancellation, drop, cleanup failures, disconnected consumers, bounded +read/iterator faults, writer/flush faults and successful terminal cleanup. diff --git a/src/main.rs b/src/main.rs index 864238b..5afa3be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -584,6 +584,24 @@ fn write_json_to( .map_err(|error| -> Box { Box::new(error) }) } +/// Writes a complete session frame and propagates codec, pipe and flush failures. +fn write_session_page_to( + writer: &mut dyn std::io::Write, + page: &pm_rust::SessionPage, + json: bool, +) -> Result<(), Box> { + let bytes = if json { + let mut bytes = pm_rust::stringify_json(page, false); + bytes.push('\n'); + bytes + } else { + page.stringify_toon()? + }; + writer.write_all(bytes.as_bytes())?; + writer.flush()?; + Ok(()) +} + #[cfg(test)] #[path = "../tests/support/main_unit.rs"] mod tests; @@ -624,18 +642,7 @@ fn run(cli: Cli) -> Result<(), Box> { let Some(page) = session.next_page()? else { break; }; - if json { - writeln!(stdout, "{}", pm_rust::stringify_json(&page, false))?; - } else { - let receipt = pm_rust::ListOutput::from(serde_json::to_value(&page.receipt)?); - writeln!( - stdout, - "{}{}---", - pm_rust::stringify_toon(&receipt)?, - pm_rust::stringify_toon(&page.output)? - )?; - } - stdout.flush()?; + write_session_page_to(&mut stdout, &page, json)?; } return Ok(()); } diff --git a/src/read_session.rs b/src/read_session.rs index ea493be..15d0ea2 100644 --- a/src/read_session.rs +++ b/src/read_session.rs @@ -7,7 +7,7 @@ use std::{ collections::BTreeMap, fmt::Write as _, fs, - io::Read, + io::{self, Read}, path::{Component, Path, PathBuf}, sync::{ Arc, @@ -76,7 +76,7 @@ pub enum SessionRefusal { InvalidLimits, /// A source cannot be safely frozen under this bounded contract. #[error( - "unsupported session source; use regular files, at most 32 directory levels, and tracker-relative schema files" + "unsupported session source; use stable UTF-8 regular files, valid settings JSON, at most 32 directory levels, and tracker-relative schema files" )] UnsupportedSource, /// The canonical reader rejected source or output data. @@ -134,6 +134,21 @@ pub struct SessionPage { pub receipt: SessionReceipt, } +impl SessionPage { + /// Renders one framed TOON page using the canonical codecs. + /// + /// # Errors + /// Returns the canonical TOON refusal for unsupported JSON string overrides. + pub fn stringify_toon(&self) -> Result { + let receipt = ListOutput::from(serde_json::json!(&self.receipt)); + Ok(format!( + "{}{}---\n", + crate::toon_output::render_output(&receipt), + crate::stringify_toon(&self.output)? + )) + } +} + /// A pull-based, disk-backed read session. Drop removes private snapshot storage. /// /// Retaining returned pages is the caller's responsibility. Cancellation is @@ -188,6 +203,8 @@ impl Workspace { }; admission.charge(0)?; let snapshot = tempfile::tempdir()?; + #[cfg(test)] + tests::SNAPSHOT_CREATED_HOOK.with(|hook| hook.get()(snapshot.path())); let target = snapshot.path().join(".agents/pm"); fs::create_dir_all(&target)?; admission.copy( @@ -320,12 +337,12 @@ impl ReadSession { if output["continuation_kind"] == "output_cursor" { self.options.output_cursor = self.options.after.take(); } - self.receipt.pages += 1; - self.receipt.complete = - self.options.after.is_none() && self.options.output_cursor.is_none(); - if self.receipt.complete { + let complete = self.options.after.is_none() && self.options.output_cursor.is_none(); + if complete { self.cleanup()?; } + self.receipt.pages += 1; + self.receipt.complete = complete; Ok(Some(SessionPage { output, receipt: self.receipt.clone(), @@ -382,10 +399,7 @@ impl Admission<'_> { )?; #[cfg(test)] tests::BEFORE_COPY_HOOK.with(|hook| hook.get()(source, self.cancellation)); - let mut bytes = Vec::new(); - fs::File::open(source)? - .take(size.saturating_add(1)) - .read_to_end(&mut bytes)?; + let bytes = read_source_file(source, size)?; if bytes.len() as u64 != size { return Err(SessionRefusal::UnsupportedSource); } @@ -403,6 +417,8 @@ impl Admission<'_> { } fs::create_dir_all(target.parent().ok_or(SessionRefusal::UnsupportedSource)?)?; fs::write(target, &bytes)?; + #[cfg(test)] + tests::AFTER_COPY_HOOK.with(|hook| hook.get()(source, target)); self.hash.update(relative.to_string_lossy().as_bytes()); self.hash.update([0]); self.hash.update(size.to_le_bytes()); @@ -423,35 +439,73 @@ impl Admission<'_> { if depth > 32 { return Err(SessionRefusal::UnsupportedSource); } + let mut directory = fs::read_dir(source)?; + let mut entries = self.collect_entries(&mut directory)?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let kind = entry.file_type(); + self.visit_entry(&entry, kind, target, relative, depth)?; + } + Ok(()) + } + + /// Charges directory entries before retaining them and preserves iterator faults. + fn collect_entries( + &mut self, + directory: &mut dyn Iterator>, + ) -> Result, SessionRefusal> { let mut entries = Vec::new(); - for entry in fs::read_dir(source)? { + for entry in directory { self.cancellation.check()?; self.charge(1024)?; entries.push(entry?); } - entries.sort_by_key(std::fs::DirEntry::file_name); - for entry in entries { - let name = entry.file_name(); - let kind = entry.file_type()?; - if depth == 0 - && crate::workspace::NON_ITEM_DIRECTORIES.contains(&name.to_string_lossy().as_ref()) - { - continue; - } - if kind.is_symlink() { - return Err(SessionRefusal::UnsupportedSource); - } - let relative = relative.join(&name); - if kind.is_dir() { - self.walk(&entry.path(), &target.join(&name), &relative, depth + 1)?; - } else if depth > 0 && entry.path().extension().is_some_and(|ext| ext == "toon") { - self.copy(&entry.path(), &target.join(name), &relative, true)?; - } + Ok(entries) + } + + /// Visits one real directory record and preserves its authoritative type result. + fn visit_entry( + &mut self, + entry: &fs::DirEntry, + kind: io::Result, + target: &Path, + relative: &Path, + depth: usize, + ) -> Result<(), SessionRefusal> { + let name = entry.file_name(); + let kind = kind?; + if depth == 0 + && crate::workspace::NON_ITEM_DIRECTORIES.contains(&name.to_string_lossy().as_ref()) + { + return Ok(()); + } + if kind.is_symlink() { + return Err(SessionRefusal::UnsupportedSource); + } + let relative = relative.join(&name); + if kind.is_dir() { + self.walk(&entry.path(), &target.join(&name), &relative, depth + 1)?; + } else if depth > 0 && entry.path().extension().is_some_and(|ext| ext == "toon") { + self.copy(&entry.path(), &target.join(name), &relative, true)?; } Ok(()) } } +/// Opens and bounds one admitted source, retaining ordinary I/O failures. +fn read_source_file(path: &Path, size: u64) -> io::Result> { + match fs::File::open(path) { + Ok(file) => read_source_bytes(&mut file.take(size.saturating_add(1))), + Err(error) => Err(error), + } +} + +/// Consumes a bounded reader without replacing read failures with partial bytes. +fn read_source_bytes(reader: &mut dyn Read) -> io::Result> { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).map(|_| bytes) +} + #[cfg(test)] #[path = "../tests/support/read_session_unit.rs"] mod tests; diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index 2ebd8d8..c19382f 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -230,3 +230,57 @@ fn cli_cancellation_and_success_remove_private_storage() -> TestResult { assert_eq!(fs::read_dir(scratch.path())?.count(), 0); Ok(()) } + +/// A disconnected native consumer terminates delivery and removes private storage. +#[test] +fn disconnected_consumer_releases_session_and_storage_refusals_are_typed() -> TestResult { + use std::process::Stdio; + let directory = fixture()?; + let scratch = tempfile::tempdir()?; + for json in [false, true] { + let mut command = Command::new(env!("CARGO_BIN_EXE_pm-rust")); + command + .current_dir(directory.path()) + .args(["read-session", "--page-items", "1"]) + .env("TMPDIR", scratch.path()) + .env("TMP", scratch.path()) + .env("TEMP", scratch.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if json { + command.arg("--json"); + } + let mut child = command.spawn()?; + drop(child.stdout.take()); + let result = child.wait_with_output()?; + assert!(!result.status.success()); + assert_eq!(fs::read_dir(scratch.path())?.count(), 0); + } + let blocker = scratch.path().join("blocked-temp"); + fs::write(&blocker, "not a directory")?; + let result = Command::new(env!("CARGO_BIN_EXE_pm-rust")) + .current_dir(directory.path()) + .arg("read-session") + .env("TMPDIR", &blocker) + .env("TMP", &blocker) + .env("TEMP", &blocker) + .output()?; + assert_eq!(result.status.code(), Some(2)); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert_eq!(refusal["code"], "session_storage_failed"); + let source = directory.path().join(".agents/pm/tasks/demo-0.toon"); + for (bytes, code) in [ + (&b"invalid"[..], "session_source_refused"), + (&b"\xff"[..], "session_unsupported_source"), + ] { + fs::write(&source, bytes)?; + let result = Command::new(env!("CARGO_BIN_EXE_pm-rust")) + .current_dir(directory.path()) + .arg("read-session") + .output()?; + assert_eq!(result.status.code(), Some(2)); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert_eq!(refusal["code"], code); + } + Ok(()) +} diff --git a/tests/support/main_unit.rs b/tests/support/main_unit.rs index 8dd947a..3488275 100644 --- a/tests/support/main_unit.rs +++ b/tests/support/main_unit.rs @@ -1543,3 +1543,39 @@ fn published_refusal_arguments_strip_all_workspace_aliases() { } } } + +/// Session frames retain canonical payloads and propagate real writer/codec failures. +#[test] +fn session_frame_writer_preserves_errors_and_framing() -> Result<(), Box> { + let mut page = pm_rust::SessionPage { + output: serde_json::json!({"items":[{"title":"example"}],"count":1}).into(), + receipt: pm_rust::SessionReceipt { + protocol: "pm-rust/read-session/v1", + snapshot_sha256: "synthetic".to_owned(), + source_items: 1, + source_bytes: 100, + admitted_memory_bytes: 1000, + memory_ceiling_bytes: 2000, + pages: 1, + complete: true, + }, + }; + for json in [false, true] { + let mut bytes = Vec::new(); + super::write_session_page_to(&mut bytes, &page, json)?; + if json { + let parsed: serde_json::Value = serde_json::from_slice(&bytes)?; + assert_eq!(parsed["receipt"]["complete"], true); + } else { + assert!(String::from_utf8(bytes)?.ends_with("---\n")); + } + assert!(super::write_session_page_to(&mut WriteFailure, &page, json).is_err()); + assert!(super::write_session_page_to(&mut FlushFailure, &page, json).is_err()); + } + page.output = pm_rust::ListOutput::with_json_overrides( + (*page.output).clone(), + std::collections::BTreeMap::from([("/items/0/title".to_owned(), "\"\\ud83d\"".to_owned())]), + ); + assert!(super::write_session_page_to(&mut Vec::new(), &page, false).is_err()); + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 65a2ab5..b760147 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -238,6 +238,14 @@ fn small_output_budget_refuses_and_terminal_query_failure_cleans_up() -> TestRes } thread_local! { + /// Real storage faults immediately after private directory allocation. + pub(super) static SNAPSHOT_CREATED_HOOK: std::cell::Cell = const { std::cell::Cell::new(path_noop) }; + /// Real copied-file faults before subsequent snapshot reads. + pub(super) static AFTER_COPY_HOOK: std::cell::Cell = const { std::cell::Cell::new(paths_noop) }; + /// Snapshot selected for a post-query cleanup fault. + static QUERY_FAULT_PATH: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + /// Owns every displaced snapshot and restores its replacement after the test. + static QUERY_FAULT_GUARD: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; /// Deterministic filesystem drift/cancellation after actual metadata admission. pub(super) static BEFORE_COPY_HOOK: std::cell::Cell = const { std::cell::Cell::new(copy_noop) }; /// Deterministic cancellation after the actual canonical query. @@ -424,3 +432,289 @@ fn query_storage_failure_releases_snapshot_and_never_certifies_completion() -> T assert!(session.next_page()?.is_none()); Ok(()) } + +/// Leaves an allocated snapshot unchanged during ordinary tests. +fn path_noop(_: &Path) {} +/// Leaves copied bytes unchanged during ordinary tests. +fn paths_noop(_: &Path, _: &Path) {} +/// Blocks creation of the private tracker with an actual regular file. +fn block_snapshot_parent(root: &Path) { + let _ = fs::write(root.join(".agents"), "blocked"); +} +/// Removes a completed settings copy before the constructor reads it. +fn remove_settings_copy(source: &Path, target: &Path) { + if source + .file_name() + .is_some_and(|name| name == "settings.json") + { + let _ = fs::remove_file(target); + } +} +/// Removes an admitted source before opening it. +fn remove_source(source: &Path, _: &SessionCancellation) { + let _ = fs::remove_file(source); +} + +/// A real replacement of private snapshot storage, with scoped cleanup ownership. +struct CleanupFault { + /// Exact generated path replaced by a regular file. + original: PathBuf, + /// Owns the displaced snapshot and its contents. + _backup: tempfile::TempDir, +} + +impl Drop for CleanupFault { + fn drop(&mut self) { + let _ = fs::remove_file(&self.original); + } +} + +/// Replaces a private snapshot with a file so remove-directory cleanup must fail. +fn cleanup_fault(original: &Path) -> Result> { + let backup = tempfile::tempdir()?; + fs::rename(original, backup.path().join("snapshot"))?; + fs::write(original, "replacement")?; + Ok(CleanupFault { + original: original.to_path_buf(), + _backup: backup, + }) +} + +/// Injects a real post-query storage replacement without changing query results. +fn fault_after_query(_: &SessionCancellation) { + let path = QUERY_FAULT_PATH.with(|path| path.borrow().clone()); + if let Some(path) = path + && let Ok(guard) = cleanup_fault(&path) + { + QUERY_FAULT_GUARD.with(|slot| *slot.borrow_mut() = Some(guard)); + } +} + +/// Adds peer cancellation after a real post-query storage replacement. +fn cancel_and_fault_after_query(token: &SessionCancellation) { + fault_after_query(token); + token.cancel(); +} + +/// Every failed cleanup leaves the session terminal without certifying delivery. +#[test] +fn cleanup_failure_never_certifies_or_counts_an_undelivered_page() -> TestResult { + for mode in [ + "cancel-before", + "query-error", + "cancel-after", + "omitted", + "complete", + ] { + let (_directory, workspace) = fixture(1)?; + let token = SessionCancellation::default(); + let limits = ReadSessionLimits { + output_tokens: if mode == "omitted" { 1 } else { 4000 }, + ..ReadSessionLimits::default() + }; + let mut session = workspace.read_session(&limits, token.clone(), CLOCK)?; + let root = session + .snapshot + .as_ref() + .ok_or("snapshot missing")? + .path() + .to_path_buf(); + let mut guard = None; + if matches!(mode, "cancel-before" | "query-error") { + guard = Some(cleanup_fault(&root)?); + if mode == "cancel-before" { + token.cancel(); + } + } else { + QUERY_FAULT_PATH.with(|path| *path.borrow_mut() = Some(root)); + AFTER_QUERY_HOOK.with(|hook| { + hook.set(if mode == "cancel-after" { + cancel_and_fault_after_query + } else { + fault_after_query + }); + }); + } + let result = session.next_page(); + AFTER_QUERY_HOOK.with(|hook| hook.set(query_noop)); + QUERY_FAULT_PATH.with(|path| path.borrow_mut().take()); + QUERY_FAULT_GUARD.with(|slot| drop(slot.borrow_mut().take())); + drop(guard); + assert!( + matches!(result, Err(SessionRefusal::Storage(_))), + "cleanup mode {mode}: {result:?}" + ); + assert!( + !session.receipt().complete, + "undelivered completion in {mode}" + ); + assert_eq!(session.receipt().pages, 0, "undelivered page in {mode}"); + assert!(session.snapshot.is_none()); + } + Ok(()) +} + +/// Constructor and publication failures remove partially admitted private copies. +#[test] +fn actual_snapshot_and_copy_storage_faults_are_typed() -> TestResult { + let (_directory, workspace) = fixture(1)?; + let open = || { + workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + ) + }; + SNAPSHOT_CREATED_HOOK.with(|hook| hook.set(block_snapshot_parent)); + let result = open(); + SNAPSHOT_CREATED_HOOK.with(|hook| hook.set(path_noop)); + assert!(matches!(result, Err(SessionRefusal::Storage(_)))); + AFTER_COPY_HOOK.with(|hook| hook.set(remove_settings_copy)); + let result = open(); + AFTER_COPY_HOOK.with(|hook| hook.set(paths_noop)); + assert!(matches!(result, Err(SessionRefusal::Storage(_)))); + fs::create_dir_all(workspace.pm_root().join("schema/status-directory"))?; + fs::write( + workspace.pm_root().join("settings.json"), + r#"{"schema":{"files":{"statuses":"schema/status-directory"}}}"#, + )?; + assert!(matches!(open(), Err(SessionRefusal::UnsupportedSource))); + let directory = tempfile::tempdir()?; + let source = directory.path().join("source"); + fs::write(&source, "source bytes")?; + let token = SessionCancellation::default(); + BEFORE_COPY_HOOK.with(|hook| hook.set(remove_source)); + let result = admission(&token).copy( + &source, + &directory.path().join("target"), + Path::new("source"), + false, + ); + BEFORE_COPY_HOOK.with(|hook| hook.set(copy_noop)); + assert!(matches!(result, Err(SessionRefusal::Storage(_)))); + fs::write(&source, "source bytes")?; + assert!(matches!( + admission(&token).copy(&source, Path::new(""), Path::new("source"), false), + Err(SessionRefusal::UnsupportedSource) + )); + let blocker = directory.path().join("blocker"); + fs::write(&blocker, "blocked")?; + assert!(matches!( + admission(&token).copy(&source, &blocker.join("child"), Path::new("source"), false), + Err(SessionRefusal::Storage(_)) + )); + assert!(matches!( + admission(&token).copy(&source, directory.path(), Path::new("source"), false), + Err(SessionRefusal::Storage(_)) + )); + token.cancel(); + assert!(matches!( + admission(&token).copy(&source, directory.path(), Path::new("source"), false), + Err(SessionRefusal::Cancelled) + )); + assert!(matches!( + admission(&token).walk(directory.path(), directory.path(), Path::new(""), 0), + Err(SessionRefusal::Cancelled) + )); + Ok(()) +} + +/// Real directory records preserve enumeration, cancellation, type and charge faults. +#[test] +fn directory_fault_boundaries_preserve_authoritative_failures() -> TestResult { + let directory = tempfile::tempdir()?; + let source = directory.path().join("entry"); + fs::write(&source, "bytes")?; + let token = SessionCancellation::default(); + assert!(matches!( + admission(&token).walk(&source, directory.path(), Path::new(""), 0), + Err(SessionRefusal::Storage(_)) + )); + let mut ledger = admission(&token); + ledger.ceiling = 0; + assert!(matches!( + ledger.walk(directory.path(), directory.path(), Path::new(""), 0), + Err(SessionRefusal::MemoryLimit) + )); + let mut iterator = fs::read_dir(directory.path())? + .chain(std::iter::once(Err(io::Error::other("enumeration fault")))); + assert!(matches!( + admission(&token).collect_entries(&mut iterator), + Err(SessionRefusal::Storage(_)) + )); + let entry = fs::read_dir(directory.path())? + .next() + .transpose()? + .ok_or("entry missing")?; + assert!(matches!( + admission(&token).visit_entry( + &entry, + Err(io::Error::other("type fault")), + directory.path(), + Path::new(""), + 0 + ), + Err(SessionRefusal::Storage(_)) + )); + let mut iterator = fs::read_dir(directory.path())?.inspect(|_| token.cancel()); + assert!(matches!( + admission(&token).collect_entries(&mut iterator), + Err(SessionRefusal::Cancelled) + )); + Ok(()) +} + +/// A failed bounded I/O stream never masquerades as successfully read partial bytes. +struct ReadFailure; +impl Read for ReadFailure { + fn read(&mut self, _: &mut [u8]) -> io::Result { + Err(io::Error::other("read fault")) + } +} + +#[test] +fn bounded_source_reader_preserves_errors_and_overflow_detection_byte() -> TestResult { + let directory = tempfile::tempdir()?; + let source = directory.path().join("source"); + fs::write(&source, "abc")?; + assert_eq!(read_source_file(&source, 1)?, b"ab"); + assert!(read_source_file(&directory.path().join("missing"), 1).is_err()); + assert!(read_source_bytes(&mut ReadFailure).is_err()); + Ok(()) +} + +/// Output compaction must use the existing snapshot cursor until all rows are served. +#[test] +fn resource_session_continues_output_budget_pages_without_missing_rows() -> TestResult { + let (_directory, workspace) = fixture(300)?; + for toon in [false, true] { + let mut session = workspace.read_session( + &ReadSessionLimits { + page_items: 300, + output_tokens: 1000, + toon, + ..ReadSessionLimits::default() + }, + SessionCancellation::default(), + CLOCK, + )?; + let mut ids = BTreeSet::new(); + let mut cursors = 0; + while let Some(page) = session.next_page()? { + assert!(session.receipt().pages <= 300); + if page.output["continuation_kind"] == "output_cursor" { + cursors += 1; + } + for row in page.output["items"].as_array().ok_or("missing rows")? { + assert!(ids.insert(row["id"].as_str().ok_or("missing id")?.to_owned())); + } + } + assert!( + cursors > 0, + "fixture must exercise snapshot output continuations" + ); + assert_eq!(ids.len(), 300); + assert!(session.receipt().complete); + } + Ok(()) +} From 2796152c2c330938830cea4658db8c494cad9ddb Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:45:41 +0200 Subject: [PATCH 06/10] Exercise native SDK snapshot creation refusal in a fresh process --- .../pm-rust-bounded-read-sessions.toon | 6 ++- .../pm-rust-bounded-read-sessions.jsonl | 4 ++ docs/BOUNDED_READ_SESSIONS.md | 3 +- tests/support/read_session_unit.rs | 41 +++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 455701e..86deb49 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T17:15:07.881Z" +updated_at: "2026-10-23T17:44:53.526Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[12]{created_at,author,text}: +comments[14]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -23,6 +23,8 @@ comments[12]{created_at,author,text}: "2026-10-23T16:33:37.805Z",codex-agent,"CI run 39982597630 completed the regular and coverage-instrumented suites, then enforced unchanged thresholds and found one uncovered output-cursor branch, two lines and 30 error-propagation regions. Add actual filesystem fault coverage, bounded-reader I/O tests and consumer-disconnect writer tests without SDK mocks. Coverage review also identified a delivery invariant: completion/pages must update only after successful terminal cleanup, so a cleanup error cannot certify an undelivered final page. Reclaim for that correction and complete fault-path verification; keep all four coverage thresholds at 100." "2026-10-23T17:07:49.714Z",codex-agent,"CI retained all four 100-percent thresholds and exposed untested storage propagation plus a final-cleanup ordering defect. Final-page cleanup now precedes receipt certification; failed cleanup remains terminal without counting an undelivered page. Added genuine output-cursor continuation over 300 rows, directory/read/write/flush faults, and consumer-disconnect coverage. All five linked commands passed: 14 SDK, 5 CLI, 1 framing, 1 pipeline and 1 registry regression tests, zero failures or ignored tests. Strict Clippy passed. Delivery-cleanup and output-cursor behavioral reverts compiled and each failed their new regression; restored code passed, bringing independent revert proofs to nine. Full npm and Bun release gates remain reserved for CI." "2026-10-23T17:14:54.325Z",codex-agent,"Final corrected-binary validation passed on SDK-created and read-only non-git public tracker copies. Synthetic: 10000 items, 10 pages, 1959611 source bytes, 381185760 admitted bytes, 43652 KiB peak RSS, 131120 ms wall, 988986 output bytes. Public copy: 78 items, 1 page, 757075 source bytes, 131681424 admitted bytes, 13824 KiB peak RSS, 782 ms wall, 12549 output bytes. Declared ceiling 536870912 bytes. Both complete SDK certificates passed; all identities and 11 canonical CLI JSON payloads matched after indentation normalization only. Source fingerprints unchanged. Measured binary SHA-256 b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71 matched before and after. Runnable Rust example completed 78 items in four pages with neither Node nor Git on PATH. Private rustdoc, formatting and strict Clippy passed. Runtime test-result recording is disabled by existing project policy; linked execution and this comment retain the results. Ready PR 110 remains subject to the unchanged full CI gates; claim released for orchestrator verification, item remains in progress." + "2026-10-23T17:42:43.290Z",codex-agent,"The Windows aggregate completed both 603-test passes with zero failures or ignored tests. Lines, functions and branches reached 100 percent; one region remained uncovered in the instrumented SDK variant of actual temporary-directory creation failure. Existing CLI coverage exercised the production variant, but cfg(test) lifecycle hooks produce a distinct instrumented variant. Adding an isolated unit-process regression that invokes the real SDK against an actual blocked scratch parent, without changing production behavior or thresholds. Linux and macOS regular suites passed 620 and 609 tests respectively; all three latency guards and CodeQL passed." + "2026-10-23T17:44:50.648Z",codex-agent,"Fresh-process SDK regression passed against a real file blocking the scratch directory; no environment mutation, SDK mock, production change or threshold reduction. The linked SDK suite now passes 15 tests, zero failures/ignored tests; the other four unchanged linked commands passed previously, for 23 focused tests. Strict Clippy and formatting passed. Production binary SHA-256 remains b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71, so the final 10000-item and public-copy measurements remain exact. Bun independently completed two 620-test passes and reached the same sole uncovered creation-error region, confirming the targeted cause. Ready PR 110 will carry the final exact-head full gate URL and counts. Item remains in progress, released for orchestrator verification." files[16]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index 9e53405..d292ec7 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -26,3 +26,7 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T17:07:49.715Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:07:49.714Z","author":"codex-agent","text":"CI retained all four 100-percent thresholds and exposed untested storage propagation plus a final-cleanup ordering defect. Final-page cleanup now precedes receipt certification; failed cleanup remains terminal without counting an undelivered page. Added genuine output-cursor continuation over 300 rows, directory/read/write/flush faults, and consumer-disconnect coverage. All five linked commands passed: 14 SDK, 5 CLI, 1 framing, 1 pipeline and 1 registry regression tests, zero failures or ignored tests. Strict Clippy passed. Delivery-cleanup and output-cursor behavioral reverts compiled and each failed their new regression; restored code passed, bringing independent revert proofs to nine. Full npm and Bun release gates remain reserved for CI."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:07:49.715Z"}],"before_hash":"42d8a245f65bfdc27f1bffaf717e6c18eb24e8c5a03a04cf214efc3c00bc4cd1","after_hash":"067ad052004c1391f08c02a827b7e043245cc5a4ad03d73859ad5412257740db","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":"3c9ce99b404bd09144cf0672e489fd55f465d9a20ecaaf685ef04b012b9c44c2"} {"hash_algorithm":"sha256","ts":"2026-10-23T17:14:54.332Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:14:54.325Z","author":"codex-agent","text":"Final corrected-binary validation passed on SDK-created and read-only non-git public tracker copies. Synthetic: 10000 items, 10 pages, 1959611 source bytes, 381185760 admitted bytes, 43652 KiB peak RSS, 131120 ms wall, 988986 output bytes. Public copy: 78 items, 1 page, 757075 source bytes, 131681424 admitted bytes, 13824 KiB peak RSS, 782 ms wall, 12549 output bytes. Declared ceiling 536870912 bytes. Both complete SDK certificates passed; all identities and 11 canonical CLI JSON payloads matched after indentation normalization only. Source fingerprints unchanged. Measured binary SHA-256 b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71 matched before and after. Runnable Rust example completed 78 items in four pages with neither Node nor Git on PATH. Private rustdoc, formatting and strict Clippy passed. Runtime test-result recording is disabled by existing project policy; linked execution and this comment retain the results. Ready PR 110 remains subject to the unchanged full CI gates; claim released for orchestrator verification, item remains in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:14:54.332Z"}],"before_hash":"067ad052004c1391f08c02a827b7e043245cc5a4ad03d73859ad5412257740db","after_hash":"a27de2ba324af481719406c2ed732138cc65bbd5a2ae67755aa72920be46032f","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":"5b20d791165d7eb1c52bff673713c88f505c6f0df3708e3d806f49f2762f4a8b"} {"hash_algorithm":"sha256","ts":"2026-10-23T17:15:07.881Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:15:07.881Z"}],"before_hash":"a27de2ba324af481719406c2ed732138cc65bbd5a2ae67755aa72920be46032f","after_hash":"bdb626821f6445e2a2c66c11b9d5125a707de97c5aba20b2800b330cc88e1b8d","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":"0c17155adc53843b138200671f1b607b97a737cfbf29caf9c9f111e6c5904598"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:42:38.088Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:42:38.088Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"bdb626821f6445e2a2c66c11b9d5125a707de97c5aba20b2800b330cc88e1b8d","after_hash":"88ca2780f0e611d344449d3ccf91035bf3d8231dfbac043526df4f6f0552b5ed","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":"1d01cb5e6c85718e9a4f13253f76c33185e3d11107b0766cc83c8efcc37820cd"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:42:43.291Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:42:43.290Z","author":"codex-agent","text":"The Windows aggregate completed both 603-test passes with zero failures or ignored tests. Lines, functions and branches reached 100 percent; one region remained uncovered in the instrumented SDK variant of actual temporary-directory creation failure. Existing CLI coverage exercised the production variant, but cfg(test) lifecycle hooks produce a distinct instrumented variant. Adding an isolated unit-process regression that invokes the real SDK against an actual blocked scratch parent, without changing production behavior or thresholds. Linux and macOS regular suites passed 620 and 609 tests respectively; all three latency guards and CodeQL passed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:42:43.291Z"}],"before_hash":"88ca2780f0e611d344449d3ccf91035bf3d8231dfbac043526df4f6f0552b5ed","after_hash":"148fa2152ec7bc0fcbe04fae5b24352729a7ab4c47ac8445158995b413c2e080","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":"f19e19623e3eb1933627d273e5e927aff72028e4729855c64621d9702b42ca34"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:44:50.649Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/13","value":{"created_at":"2026-10-23T17:44:50.648Z","author":"codex-agent","text":"Fresh-process SDK regression passed against a real file blocking the scratch directory; no environment mutation, SDK mock, production change or threshold reduction. The linked SDK suite now passes 15 tests, zero failures/ignored tests; the other four unchanged linked commands passed previously, for 23 focused tests. Strict Clippy and formatting passed. Production binary SHA-256 remains b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71, so the final 10000-item and public-copy measurements remain exact. Bun independently completed two 620-test passes and reached the same sole uncovered creation-error region, confirming the targeted cause. Ready PR 110 will carry the final exact-head full gate URL and counts. Item remains in progress, released for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:44:50.649Z"}],"before_hash":"148fa2152ec7bc0fcbe04fae5b24352729a7ab4c47ac8445158995b413c2e080","after_hash":"73fdfc2c762c6b52276cc0d780fbccb4c418b0e84acbe2158fdc97c5617970e6","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":"4b0eb116ffa25085b8e207ed907d71b6d890a4bfb7ad803917bd14ee46ee8143"} +{"hash_algorithm":"sha256","ts":"2026-10-23T17:44:53.526Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:44:53.526Z"}],"before_hash":"73fdfc2c762c6b52276cc0d780fbccb4c418b0e84acbe2158fdc97c5617970e6","after_hash":"7f63ccef6ceed5b1734007e9b98b7e69b5451ab7cc0b0004e518525a9915a2d3","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":"d72768ba07c755afc4dc2cc112d2f8b1e8c3a5c1dc8d32695429d39ba228b871"} diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index 6b825e9..34b09d3 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -127,4 +127,5 @@ exercise real no-git initialization, a native PATH without Git or Node, empty trackers, output-budget continuations, peer cancellation before and after a real query, source length drift, schema symlink refusal, real query storage loss, explicit cancellation, drop, cleanup failures, disconnected consumers, bounded -read/iterator faults, writer/flush faults and successful terminal cleanup. +read/iterator faults, writer/flush faults, actual snapshot-creation refusal in a +fresh SDK process, and successful terminal cleanup. diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index b760147..86e2a14 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -718,3 +718,44 @@ fn resource_session_continues_output_budget_pages_without_missing_rows() -> Test } Ok(()) } + +/// A fresh native SDK process preserves real temporary-directory creation failures. +#[test] +fn snapshot_temp_creation_failure_is_typed() -> TestResult { + const SOURCE: &str = "PM_RUST_SESSION_TEMP_FAILURE_SOURCE"; + if let Some(source) = std::env::var_os(SOURCE) { + let workspace = Workspace::discover(&PathBuf::from(source))?; + assert!(matches!( + workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + ), + Err(SessionRefusal::Storage(_)) + )); + return Ok(()); + } + let (directory, _workspace) = fixture(1)?; + let scratch = tempfile::tempdir()?; + let blocker = scratch.path().join("blocked-temp"); + fs::write(&blocker, "not a directory")?; + let result = std::process::Command::new(std::env::current_exe()?) + .args([ + "--exact", + "read_session::tests::snapshot_temp_creation_failure_is_typed", + "--nocapture", + ]) + .env(SOURCE, directory.path()) + .env("TMPDIR", &blocker) + .env("TMP", &blocker) + .env("TEMP", &blocker) + .output()?; + assert!( + result.status.success(), + "{}{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + assert!(String::from_utf8(result.stdout)?.contains("1 passed")); + Ok(()) +} From 6604eacb88060e621161ec664060b912b53c077f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:34:42 +0200 Subject: [PATCH 07/10] Refuse schema lookup errors before certifying read sessions --- .../pm-rust-bounded-read-sessions.toon | 6 ++- .../pm-rust-bounded-read-sessions.jsonl | 4 ++ docs/BOUNDED_READ_SESSIONS.md | 16 +++++-- src/read_session.rs | 12 ++++- tests/read_session_contract.rs | 36 ++++++++++++++ tests/support/read_session_unit.rs | 48 +++++++++++++++++++ 6 files changed, 113 insertions(+), 9 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 86deb49..8e45a80 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[3]: embedding,resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T17:44:53.526Z" +updated_at: "2026-10-23T18:34:02.898Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[14]{created_at,author,text}: +comments[16]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -25,6 +25,8 @@ comments[14]{created_at,author,text}: "2026-10-23T17:14:54.325Z",codex-agent,"Final corrected-binary validation passed on SDK-created and read-only non-git public tracker copies. Synthetic: 10000 items, 10 pages, 1959611 source bytes, 381185760 admitted bytes, 43652 KiB peak RSS, 131120 ms wall, 988986 output bytes. Public copy: 78 items, 1 page, 757075 source bytes, 131681424 admitted bytes, 13824 KiB peak RSS, 782 ms wall, 12549 output bytes. Declared ceiling 536870912 bytes. Both complete SDK certificates passed; all identities and 11 canonical CLI JSON payloads matched after indentation normalization only. Source fingerprints unchanged. Measured binary SHA-256 b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71 matched before and after. Runnable Rust example completed 78 items in four pages with neither Node nor Git on PATH. Private rustdoc, formatting and strict Clippy passed. Runtime test-result recording is disabled by existing project policy; linked execution and this comment retain the results. Ready PR 110 remains subject to the unchanged full CI gates; claim released for orchestrator verification, item remains in progress." "2026-10-23T17:42:43.290Z",codex-agent,"The Windows aggregate completed both 603-test passes with zero failures or ignored tests. Lines, functions and branches reached 100 percent; one region remained uncovered in the instrumented SDK variant of actual temporary-directory creation failure. Existing CLI coverage exercised the production variant, but cfg(test) lifecycle hooks produce a distinct instrumented variant. Adding an isolated unit-process regression that invokes the real SDK against an actual blocked scratch parent, without changing production behavior or thresholds. Linux and macOS regular suites passed 620 and 609 tests respectively; all three latency guards and CodeQL passed." "2026-10-23T17:44:50.648Z",codex-agent,"Fresh-process SDK regression passed against a real file blocking the scratch directory; no environment mutation, SDK mock, production change or threshold reduction. The linked SDK suite now passes 15 tests, zero failures/ignored tests; the other four unchanged linked commands passed previously, for 23 focused tests. Strict Clippy and formatting passed. Production binary SHA-256 remains b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71, so the final 10000-item and public-copy measurements remain exact. Bun independently completed two 620-test passes and reached the same sole uncovered creation-error region, confirming the targeted cause. Ready PR 110 will carry the final exact-head full gate URL and counts. Item remains in progress, released for orchestrator verification." + "2026-10-23T18:26:37.115Z",codex-agent,"Blocking review found that Path::exists suppressed schema lookup failures and allowed a session to certify defaults when the canonical list reader refused. Pulled the PR branch, merged advanced main and regenerated/checked the changelog inside the merge. Reusing this feature item for the correction: optional schema lookup will fall back only for NotFound, preserve canonical I/O path and error kind for other failures, and refuse admission before completion. Add a regular-file schema-parent regression and verify behavioral revert failure, targeted Rust tests, strict Clippy, and the unchanged npm/Bun CI aggregates." + "2026-10-23T18:34:00.980Z",codex-agent,"Fixed blocking schema lookup refusal by replacing the lossy existence check with symlink_metadata and an explicit NotFound-only default branch. All other lookup errors retain the canonical read-path spelling and original OS error in PmRustError::Io, refusing admission before any certificate. Regression schema_parent_file_preserves_canonical_lookup_error compares path, NotADirectory kind and raw OS code with list_page for default/configured statuses and configured workflows. CLI regression schema_parent_file_never_certifies_cli_completion verifies JSON/TOON refusal, empty stdout, unchanged source and removed private scratch storage. Behavioral revert proof: restoring only the original Workspace::read_session made the SDK regression compile and fail with exit 101, reporting that schema lookup must refuse before session certification. Restored fix passed all 16 SDK and 6 CLI session tests, also through both matching pm-linked commands; strict all-target/all-feature Clippy and formatting passed. Ten independent behavioral revert proofs now exist. Prior resource measurements are retained as explicitly identified pre-review observations; completed large-fixture work was not repeated. Read-only health found no pending merge receipt; existing local environment advisories were left untouched, with the clean CI gate required for full health evidence." files[16]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index d292ec7..c865470 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -30,3 +30,7 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T17:42:43.291Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:42:43.290Z","author":"codex-agent","text":"The Windows aggregate completed both 603-test passes with zero failures or ignored tests. Lines, functions and branches reached 100 percent; one region remained uncovered in the instrumented SDK variant of actual temporary-directory creation failure. Existing CLI coverage exercised the production variant, but cfg(test) lifecycle hooks produce a distinct instrumented variant. Adding an isolated unit-process regression that invokes the real SDK against an actual blocked scratch parent, without changing production behavior or thresholds. Linux and macOS regular suites passed 620 and 609 tests respectively; all three latency guards and CodeQL passed."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:42:43.291Z"}],"before_hash":"88ca2780f0e611d344449d3ccf91035bf3d8231dfbac043526df4f6f0552b5ed","after_hash":"148fa2152ec7bc0fcbe04fae5b24352729a7ab4c47ac8445158995b413c2e080","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":"f19e19623e3eb1933627d273e5e927aff72028e4729855c64621d9702b42ca34"} {"hash_algorithm":"sha256","ts":"2026-10-23T17:44:50.649Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/13","value":{"created_at":"2026-10-23T17:44:50.648Z","author":"codex-agent","text":"Fresh-process SDK regression passed against a real file blocking the scratch directory; no environment mutation, SDK mock, production change or threshold reduction. The linked SDK suite now passes 15 tests, zero failures/ignored tests; the other four unchanged linked commands passed previously, for 23 focused tests. Strict Clippy and formatting passed. Production binary SHA-256 remains b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71, so the final 10000-item and public-copy measurements remain exact. Bun independently completed two 620-test passes and reached the same sole uncovered creation-error region, confirming the targeted cause. Ready PR 110 will carry the final exact-head full gate URL and counts. Item remains in progress, released for orchestrator verification."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T17:44:50.649Z"}],"before_hash":"148fa2152ec7bc0fcbe04fae5b24352729a7ab4c47ac8445158995b413c2e080","after_hash":"73fdfc2c762c6b52276cc0d780fbccb4c418b0e84acbe2158fdc97c5617970e6","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":"4b0eb116ffa25085b8e207ed907d71b6d890a4bfb7ad803917bd14ee46ee8143"} {"hash_algorithm":"sha256","ts":"2026-10-23T17:44:53.526Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T17:44:53.526Z"}],"before_hash":"73fdfc2c762c6b52276cc0d780fbccb4c418b0e84acbe2158fdc97c5617970e6","after_hash":"7f63ccef6ceed5b1734007e9b98b7e69b5451ab7cc0b0004e518525a9915a2d3","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":"d72768ba07c755afc4dc2cc112d2f8b1e8c3a5c1dc8d32695429d39ba228b871"} +{"hash_algorithm":"sha256","ts":"2026-10-23T18:26:32.077Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T18:26:32.077Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"7f63ccef6ceed5b1734007e9b98b7e69b5451ab7cc0b0004e518525a9915a2d3","after_hash":"cc10f4cdf507fe70f272c8c8de1fd0f7670a9a7b3e2b79abccb3a42dfb65ef07","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":"10d5225990a7a82312fe03be8dacd8e76f3dbd039646641ae69b75258fa759b4"} +{"hash_algorithm":"sha256","ts":"2026-10-23T18:26:37.116Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/14","value":{"created_at":"2026-10-23T18:26:37.115Z","author":"codex-agent","text":"Blocking review found that Path::exists suppressed schema lookup failures and allowed a session to certify defaults when the canonical list reader refused. Pulled the PR branch, merged advanced main and regenerated/checked the changelog inside the merge. Reusing this feature item for the correction: optional schema lookup will fall back only for NotFound, preserve canonical I/O path and error kind for other failures, and refuse admission before completion. Add a regular-file schema-parent regression and verify behavioral revert failure, targeted Rust tests, strict Clippy, and the unchanged npm/Bun CI aggregates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T18:26:37.116Z"}],"before_hash":"cc10f4cdf507fe70f272c8c8de1fd0f7670a9a7b3e2b79abccb3a42dfb65ef07","after_hash":"27f9c16250e763b5950939b3a1fdd2afe6e23b9a0eab24aae1c5c0e8b4d803e6","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":"7d230c189d8d08232c3f9cbafe94622b5373489dfaa842fdac32320affe87596"} +{"hash_algorithm":"sha256","ts":"2026-10-23T18:34:00.980Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/15","value":{"created_at":"2026-10-23T18:34:00.980Z","author":"codex-agent","text":"Fixed blocking schema lookup refusal by replacing the lossy existence check with symlink_metadata and an explicit NotFound-only default branch. All other lookup errors retain the canonical read-path spelling and original OS error in PmRustError::Io, refusing admission before any certificate. Regression schema_parent_file_preserves_canonical_lookup_error compares path, NotADirectory kind and raw OS code with list_page for default/configured statuses and configured workflows. CLI regression schema_parent_file_never_certifies_cli_completion verifies JSON/TOON refusal, empty stdout, unchanged source and removed private scratch storage. Behavioral revert proof: restoring only the original Workspace::read_session made the SDK regression compile and fail with exit 101, reporting that schema lookup must refuse before session certification. Restored fix passed all 16 SDK and 6 CLI session tests, also through both matching pm-linked commands; strict all-target/all-feature Clippy and formatting passed. Ten independent behavioral revert proofs now exist. Prior resource measurements are retained as explicitly identified pre-review observations; completed large-fixture work was not repeated. Read-only health found no pending merge receipt; existing local environment advisories were left untouched, with the clean CI gate required for full health evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T18:34:00.980Z"}],"before_hash":"27f9c16250e763b5950939b3a1fdd2afe6e23b9a0eab24aae1c5c0e8b4d803e6","after_hash":"4bb073c43a9e552652b1c96e2d1faa0f486050b14388e0f3eb732afca2d3d78c","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":"4edf6e865fe99150255594e0f48f9614f9b2846c04af4c6304541d2ce23c709a"} +{"hash_algorithm":"sha256","ts":"2026-10-23T18:34:02.898Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T18:34:02.898Z"}],"before_hash":"4bb073c43a9e552652b1c96e2d1faa0f486050b14388e0f3eb732afca2d3d78c","after_hash":"3d1af684135f70fe0bcf4feb9fee1e351f0fced84d23dc25093154debdfeb802","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":"de4a369f400207e3d2250dc2da5a0a5051555709c53f67f48bc4585748ef323e"} diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index 34b09d3..388f88f 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -16,8 +16,10 @@ caller page size and the existing output token budget. Producer and output continuations retain the original tracker query identity. JSON overrides preserve unpaired UTF-16 compaction cuts; the existing TOON codec can refuse those cuts. Canonical list recovery flags refer to the ordinary `list` command; session -controls are documented separately. The original tracker is never written. Git, extensions, JavaScript and telemetry -are absent from the native path. The package's pinned oracle is unchanged. +controls are documented separately. Optional registry lookup falls back only for +`NotFound`. Other lookup failures retain the canonical list error path and operating-system error and refuse +admission before any completion certificate. The original tracker is never written. +Git, extensions, JavaScript and telemetry are absent from the native path. The package's pinned oracle is unchanged. The snapshot is an immutable capture of individually read files, not a database transaction or an atomic point-in-time view across concurrent source writers. @@ -86,7 +88,10 @@ scratch paths are excluded. ## Acceptance evidence -Measured with Rust 1.90.0, the unoptimized native binary and the pinned +The following resource measurements are retained from the pre-review binary +identified below. The subsequent schema-lookup refusal correction is verified by +its focused regression and the PR CI gate. Measured with Rust 1.90.0, the +unoptimized native binary and the pinned 2026.10.21 oracle on a contended development host. Every source is a plain, non-git directory initialized through the real SDK or copied from public Git objects. Files and directories were read-only during native validation. Each @@ -118,10 +123,11 @@ The measured native binary SHA-256, checked before and after validation, is Only text evidence is tracked; generated trackers, executable files and raw outputs remain disposable. -Nine isolated behavioral reverts compiled and failed their targeted regression: +Ten isolated behavioral reverts compiled and failed their targeted regression: admission charging, snapshot binding, registry isolation, cancellation cleanup, omission-only output refusal, clock-storage accounting, duplicate-file -remedies, delivery cleanup ordering and output-cursor continuation. All functions +remedies, delivery cleanup ordering, output-cursor continuation and refusal of +registry lookup failures behind a regular-file schema parent. All functions were restored before successful validation. Tests also exercise real no-git initialization, a native PATH without Git or Node, empty trackers, output-budget continuations, peer cancellation before and after a real diff --git a/src/read_session.rs b/src/read_session.rs index 15d0ea2..a60ef06 100644 --- a/src/read_session.rs +++ b/src/read_session.rs @@ -236,8 +236,16 @@ impl Workspace { return Err(SessionRefusal::UnsupportedSource); } } - if schema_source.exists() { - admission.copy(&schema_source, &target.join(relative), relative, false)?; + match fs::symlink_metadata(&schema_source) { + Ok(_) => admission.copy(&schema_source, &target.join(relative), relative, false)?, + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(source) => { + return Err(PmRustError::Io { + path: self.read_pm_root().join(relative), + source, + } + .into()); + } } } admission.walk(self.pm_root(), &target, Path::new(""), 0)?; diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index c19382f..7631d99 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -284,3 +284,39 @@ fn disconnected_consumer_releases_session_and_storage_refusals_are_typed() -> Te } Ok(()) } + +/// Schema lookup errors refuse CLI delivery and remove the partially copied snapshot. +#[test] +fn schema_parent_file_never_certifies_cli_completion() -> TestResult { + let directory = fixture()?; + let root = directory.path().join(".agents/pm"); + fs::remove_dir_all(root.join("schema"))?; + fs::write(root.join("schema"), "schema is a regular file")?; + let scratch = tempfile::tempdir()?; + for json in [false, true] { + let mut command = Command::new(env!("CARGO_BIN_EXE_pm-rust")); + command + .current_dir(directory.path()) + .arg("read-session") + .env("TMPDIR", scratch.path()) + .env("TMP", scratch.path()) + .env("TEMP", scratch.path()); + if json { + command.arg("--json"); + } + let result = command.output()?; + assert_eq!(result.status.code(), Some(2)); + assert!( + result.stdout.is_empty(), + "refused source must emit no certificate" + ); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert_eq!(refusal["code"], "session_source_refused"); + assert_eq!(fs::read_dir(scratch.path())?.count(), 0); + } + assert_eq!( + fs::read_to_string(root.join("schema"))?, + "schema is a regular file" + ); + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 86e2a14..4f4a9e1 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -759,3 +759,51 @@ fn snapshot_temp_creation_failure_is_typed() -> TestResult { assert!(String::from_utf8(result.stdout)?.contains("1 passed")); Ok(()) } + +/// Registry lookup faults preserve canonical errors instead of certifying defaults. +#[test] +fn schema_parent_file_preserves_canonical_lookup_error() -> TestResult { + for (name, configured) in [("statuses", false), ("statuses", true), ("workflows", true)] { + let (_directory, workspace) = fixture(1)?; + let mut settings: serde_json::Value = + serde_json::from_slice(&fs::read(workspace.pm_root().join("settings.json"))?)?; + if !configured { + settings["schema"]["files"] = serde_json::json!({}); + } + if name == "workflows" { + settings["schema"]["files"]["statuses"] = serde_json::json!("statuses.json"); + fs::write(workspace.pm_root().join("statuses.json"), "{}")?; + } + fs::write( + workspace.pm_root().join("settings.json"), + serde_json::to_vec(&settings)?, + )?; + let blocker = workspace.pm_root().join("schema"); + fs::write(&blocker, "schema is a regular file")?; + let Err(PmRustError::Io { + path: expected_path, + source: expected_error, + }) = workspace.list_page(&ItemFilter::default(), &ListOptions::default(), CLOCK) + else { + return Err("canonical list must refuse the blocked schema parent".into()); + }; + assert_eq!(expected_error.kind(), io::ErrorKind::NotADirectory); + assert_eq!( + expected_path, + workspace.read_pm_root().join(format!("schema/{name}.json")) + ); + let result = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + ); + let Err(SessionRefusal::Source(PmRustError::Io { path, source })) = result else { + return Err("schema lookup must refuse before session certification".into()); + }; + assert_eq!(path, expected_path); + assert_eq!(source.kind(), expected_error.kind()); + assert_eq!(source.raw_os_error(), expected_error.raw_os_error()); + assert_eq!(fs::read_to_string(blocker)?, "schema is a regular file"); + } + Ok(()) +} From de9f1abe485e6bfd9a3b1231e7d5c932ded2492f Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:29:13 +0200 Subject: [PATCH 08/10] test: preserve schema lookup semantics across platforms --- .../pm-rust-bounded-read-sessions.toon | 11 ++- .agents/pm/history/pm-rust-b4yl.jsonl | 1 + .../pm-rust-bounded-read-sessions.jsonl | 8 ++ .agents/pm/tasks/pm-rust-b4yl.toon | 5 +- docs/BOUNDED_READ_SESSIONS.md | 5 +- tests/read_session_contract.rs | 96 ++++++++++++++++++- tests/support/read_session_unit.rs | 65 ++++++++++++- 7 files changed, 182 insertions(+), 9 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 8e45a80..7b95d2b 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -4,13 +4,13 @@ description: "Embeddable bounded read sessions for non-Node hosts, reusing exist type: Feature status: in_progress priority: 2 -tags[3]: embedding,resources,sdk +tags[4]: embedding,"gh:unbraind/pm-rust#110",resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T18:34:02.898Z" +updated_at: "2026-10-23T19:29:04.655Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[16]{created_at,author,text}: +comments[21]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -27,6 +27,11 @@ comments[16]{created_at,author,text}: "2026-10-23T17:44:50.648Z",codex-agent,"Fresh-process SDK regression passed against a real file blocking the scratch directory; no environment mutation, SDK mock, production change or threshold reduction. The linked SDK suite now passes 15 tests, zero failures/ignored tests; the other four unchanged linked commands passed previously, for 23 focused tests. Strict Clippy and formatting passed. Production binary SHA-256 remains b521fc24870ac80b095e3f417af765e58c0a50a616b455b04b8b0c7e430bfa71, so the final 10000-item and public-copy measurements remain exact. Bun independently completed two 620-test passes and reached the same sole uncovered creation-error region, confirming the targeted cause. Ready PR 110 will carry the final exact-head full gate URL and counts. Item remains in progress, released for orchestrator verification." "2026-10-23T18:26:37.115Z",codex-agent,"Blocking review found that Path::exists suppressed schema lookup failures and allowed a session to certify defaults when the canonical list reader refused. Pulled the PR branch, merged advanced main and regenerated/checked the changelog inside the merge. Reusing this feature item for the correction: optional schema lookup will fall back only for NotFound, preserve canonical I/O path and error kind for other failures, and refuse admission before completion. Add a regular-file schema-parent regression and verify behavioral revert failure, targeted Rust tests, strict Clippy, and the unchanged npm/Bun CI aggregates." "2026-10-23T18:34:00.980Z",codex-agent,"Fixed blocking schema lookup refusal by replacing the lossy existence check with symlink_metadata and an explicit NotFound-only default branch. All other lookup errors retain the canonical read-path spelling and original OS error in PmRustError::Io, refusing admission before any certificate. Regression schema_parent_file_preserves_canonical_lookup_error compares path, NotADirectory kind and raw OS code with list_page for default/configured statuses and configured workflows. CLI regression schema_parent_file_never_certifies_cli_completion verifies JSON/TOON refusal, empty stdout, unchanged source and removed private scratch storage. Behavioral revert proof: restoring only the original Workspace::read_session made the SDK regression compile and fail with exit 101, reporting that schema lookup must refuse before session certification. Restored fix passed all 16 SDK and 6 CLI session tests, also through both matching pm-linked commands; strict all-target/all-feature Clippy and formatting passed. Ten independent behavioral revert proofs now exist. Prior resource measurements are retained as explicitly identified pre-review observations; completed large-fixture work was not repeated. Read-only health found no pending merge receipt; existing local environment advisories were left untouched, with the clean CI gate required for full health evidence." + "2026-10-23T19:04:00.858Z",codex-agent,"Review round: remote head pulled and already contains main. GraphQL reports zero review threads and no CodeRabbit review body. Failed CI run 40004446634 shows Windows canonical list treats a schema parent file as NotFound, while the regression incorrectly requires Unix NotADirectory. Preserve strict Unix refusal assertions and verify Windows canonical fallback parity explicitly. Recheck the blocking lookup fix with an independent behavioral revert, self-review the complete diff, and run the unchanged native release gate." + "2026-10-23T19:06:26.096Z",codex-agent,"Behavioral revert proof repeated at this head: replacing only the optional registry lookup in Workspace::read_session with the previous exists() check compiled successfully and failed both cargo +1.90.0 test --locked --lib read_session::tests::schema_parent_file_preserves_canonical_lookup_error -- --exact and cargo +1.90.0 test --locked --test read_session_contract schema_parent_file_never_certifies_cli_completion -- --exact, each exit 101. SDK reported admission unexpectedly succeeded; CLI returned exit 0 instead of 2. Restored the exact production source. Focused restored suites pass 16 SDK and 6 CLI tests on Unix. The added Windows regressions check canonical default-registry parity and cleanup rather than requiring a Unix-only OS error. Whole-diff self-review covered schema path components, bounded source errors, snapshot isolation, completion/cleanup, output continuations, and CLI writer failures; no additional instance of the swallowed lookup defect was found." + "2026-10-23T19:10:58.495Z",codex-agent,"Coverage self-review: the non-NotFound registry error must also be exercised on Windows, where the file-parent case is NotFound. Added a platform-independent real invalid-filename regression for statuses and workflows using a NUL path from settings; it compares the exact canonical InvalidInput refusal and rejects admission. This preserves error-branch coverage on every release platform without mocking filesystem behavior or lowering thresholds." + "2026-10-23T19:26:31.374Z",codex-agent,"Portable refusal revert proof: restoring only the previous exists() lookup compiled and made schema_invalid_path_preserves_canonical_lookup_error fail with exit 101 for admission unexpectedly succeeding. Restoring the exact source passed the test. All five existing pm-linked commands passed on retry (17 SDK, 6 CLI and three supporting tests; 26 total). The first linked CLI attempt hit bundle_integrity_torn_install during the oracle reinstall and passed after installation completed. Strict Clippy, private rustdoc, formatting and isolated CI-style health passed. The local unchanged just release-check attempt was explicitly stopped under heavy host contention after 334 library, 35 CLI unit and branch-merge tests passed and while ownership differentials continued; this is incomplete gate evidence. Push the reviewed candidate and require all unchanged full CI aggregates, including Windows and Bun, before proceeding to PR 105. No CodeRabbit review body exists; its issue comment says automatic review was skipped. Full health history advisories remain tracked separately by pm-rust-b4yl." + "2026-10-23T19:29:03.183Z",codex-agent,"Final review candidate adds portable SDK and CLI invalid-filename checks alongside strict Unix file-parent refusals and explicit Windows canonical-default checks. Both portable regressions independently compiled and failed with only the old exists() behavior restored (exit 101): SDK admitted an invalid path; CLI leaked the private snapshot diagnostic instead of the canonical source diagnostic. All production bytes are restored. Final pm-linked CLI suite passed 7 tests; unchanged linked SDK/supporting suites passed 17 plus 3 tests, for 27 focused cases. Strict Clippy was corrected to use explicit ItemFilter/ListOptions defaults and rerun at unchanged warning policy. Final full gates are delegated to the unchanged CI matrix after push; local full gate was stopped as documented, never reported as passed. Release the claim for independent verification; keep the item in progress." files[16]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project diff --git a/.agents/pm/history/pm-rust-b4yl.jsonl b/.agents/pm/history/pm-rust-b4yl.jsonl index 3d31e8f..d8a35a1 100644 --- a/.agents/pm/history/pm-rust-b4yl.jsonl +++ b/.agents/pm/history/pm-rust-b4yl.jsonl @@ -1,3 +1,4 @@ {"hash_algorithm":"sha256","ts":"2026-10-17T11:01:24.761Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"a26315c2ed8a86e0bab4a61a","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-rust-b4yl"},{"op":"add","path":"/metadata/title","value":"Investigate tracker hash discontinuity after older SDK normalization"},{"op":"add","path":"/metadata/description","value":"The first Round 3 claim writes used the changelog-only SDK 2026.10.9 instead of the repo-pinned oracle 2026.10.14. That parser dropped linked-test updater metadata before hashing: history verification now reports before_hash_mismatch at entry 44 for pm-rust-bj7a and entry 74 for pm-rust-unih. All original history prefixes are byte-identical and current items match latest after_hash values. Subsequent writes use the pinned oracle. Preserve append-only records; investigate a forward-compatible migration or verifier strategy without re-anchoring or rewriting history. Default strict health skips history_drift, so its pass must not be described as history integrity."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["conformance","follow-up","history"]},{"op":"add","path":"/metadata/created_at","value":"2026-10-17T11:01:24.761Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-10-17T11:01:24.761Z"},{"op":"add","path":"/metadata/author","value":"codex-agent"},{"op":"add","path":"/metadata/parent","value":"pm-rust-u4al"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"37b6ccaa114246bfed9b6f3cde31a2387bea55806eb9713718a2e1ebb793ed6b","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":"8696293da7020e662601409c849fdacf5ed4b2da67100ef1eb284534c460db91"} {"hash_algorithm":"sha256","ts":"2026-10-18T13:11:30.015Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"d2230ca3c341d18f807d0784","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":null,"topic":null},"op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-18T13:11:30.015Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-10-18T13:11:30.015Z","author":"codex-agent","text":"PR #92 second-review check reconfirms the existing discontinuities: full health --strict-exit reports history_drift_chain_mismatch for pm-rust-bj7a and pm-rust-unih. A fresh no-local clone of prior head c511d607579d746e201a15e54a37b287c79b1195 reproduces both; those items and history streams remain unchanged by this review. The documented CI command health --check-only --require-merge-drivers --strict-exit passes and must not be reported as verification of these older chains. Preserve this existing follow-up open; no re-anchor, repair or rewrite performed."}]}],"before_hash":"37b6ccaa114246bfed9b6f3cde31a2387bea55806eb9713718a2e1ebb793ed6b","after_hash":"4995d03671b78fd70695cd1fdad422cbceb1fe8167af4a65f34a9a2452a10167","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":"85460349955718a3f67d3011cbfbc2df3708a249b4a780381cdc5370a5626a92"} {"hash_algorithm":"sha256","ts":"2026-10-21T11:12:40.193Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"cd9de771f79e7aa84bf4d0b1","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/1","value":{"created_at":"2026-10-21T11:12:40.192Z","author":"codex-agent","text":"PR 101 review-round supplementary full health with pinned pm-cli 2026.10.21 confirms the same two existing chain mismatches in pm-rust-bj7a and pm-rust-unih. Both item/history files are unchanged relative to origin/main and the review candidate. Full health checks 74 items and reports two chain mismatches, zero hash mismatches, zero workspace-state mismatches. No existing records were rewritten. This is independent of the release-check and default CI health gates, which do not establish full history integrity."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-21T11:12:40.193Z"}],"before_hash":"4995d03671b78fd70695cd1fdad422cbceb1fe8167af4a65f34a9a2452a10167","after_hash":"b8f8f7ae79e38ee11668f7e9940491062cb7630c0cb0acf74415f99f0552da77","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":"f6e08d110814b6134ce1bd9e7ed75c0acd97d4cb743e7e96e80c3db04d0b8301"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:09:49.301Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/2","value":{"created_at":"2026-10-23T19:09:49.301Z","author":"codex-agent","text":"PR 110 review round confirms the two already tracked chain mismatches for pm-rust-bj7a and pm-rust-unih. Both item/history pairs are unchanged from origin/main. Full local health authority remains ok but reports these historical advisories plus local extension and merge-driver configuration warnings. No append-only history was repaired or rewritten. Release and CI check-only health evidence is separate from verification of these older chains."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:09:49.301Z"}],"before_hash":"b8f8f7ae79e38ee11668f7e9940491062cb7630c0cb0acf74415f99f0552da77","after_hash":"7c9eaf86fbe749dc202fef9479e9047c501a729e0979be16d8f6443ce339902d","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":"2f2c5c379659409c9bcc00ae48ebc3fa0f8f407fd5741d0c88b71f641db86054"} diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index c865470..a1635ed 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -34,3 +34,11 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T18:26:37.116Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/14","value":{"created_at":"2026-10-23T18:26:37.115Z","author":"codex-agent","text":"Blocking review found that Path::exists suppressed schema lookup failures and allowed a session to certify defaults when the canonical list reader refused. Pulled the PR branch, merged advanced main and regenerated/checked the changelog inside the merge. Reusing this feature item for the correction: optional schema lookup will fall back only for NotFound, preserve canonical I/O path and error kind for other failures, and refuse admission before completion. Add a regular-file schema-parent regression and verify behavioral revert failure, targeted Rust tests, strict Clippy, and the unchanged npm/Bun CI aggregates."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T18:26:37.116Z"}],"before_hash":"cc10f4cdf507fe70f272c8c8de1fd0f7670a9a7b3e2b79abccb3a42dfb65ef07","after_hash":"27f9c16250e763b5950939b3a1fdd2afe6e23b9a0eab24aae1c5c0e8b4d803e6","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":"7d230c189d8d08232c3f9cbafe94622b5373489dfaa842fdac32320affe87596"} {"hash_algorithm":"sha256","ts":"2026-10-23T18:34:00.980Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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/15","value":{"created_at":"2026-10-23T18:34:00.980Z","author":"codex-agent","text":"Fixed blocking schema lookup refusal by replacing the lossy existence check with symlink_metadata and an explicit NotFound-only default branch. All other lookup errors retain the canonical read-path spelling and original OS error in PmRustError::Io, refusing admission before any certificate. Regression schema_parent_file_preserves_canonical_lookup_error compares path, NotADirectory kind and raw OS code with list_page for default/configured statuses and configured workflows. CLI regression schema_parent_file_never_certifies_cli_completion verifies JSON/TOON refusal, empty stdout, unchanged source and removed private scratch storage. Behavioral revert proof: restoring only the original Workspace::read_session made the SDK regression compile and fail with exit 101, reporting that schema lookup must refuse before session certification. Restored fix passed all 16 SDK and 6 CLI session tests, also through both matching pm-linked commands; strict all-target/all-feature Clippy and formatting passed. Ten independent behavioral revert proofs now exist. Prior resource measurements are retained as explicitly identified pre-review observations; completed large-fixture work was not repeated. Read-only health found no pending merge receipt; existing local environment advisories were left untouched, with the clean CI gate required for full health evidence."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T18:34:00.980Z"}],"before_hash":"27f9c16250e763b5950939b3a1fdd2afe6e23b9a0eab24aae1c5c0e8b4d803e6","after_hash":"4bb073c43a9e552652b1c96e2d1faa0f486050b14388e0f3eb732afca2d3d78c","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":"4edf6e865fe99150255594e0f48f9614f9b2846c04af4c6304541d2ce23c709a"} {"hash_algorithm":"sha256","ts":"2026-10-23T18:34:02.898Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"b5b70a65052433b99eb25138","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-23T18:34:02.898Z"}],"before_hash":"4bb073c43a9e552652b1c96e2d1faa0f486050b14388e0f3eb732afca2d3d78c","after_hash":"3d1af684135f70fe0bcf4feb9fee1e351f0fced84d23dc25093154debdfeb802","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":"de4a369f400207e3d2250dc2da5a0a5051555709c53f67f48bc4585748ef323e"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:03:57.322Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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-23T19:03:57.322Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"3d1af684135f70fe0bcf4feb9fee1e351f0fced84d23dc25093154debdfeb802","after_hash":"72c231fff59edba9a6de169b805a898cd25b9f7886e3c1a2de68d0d8137dd932","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":"53b6ac330258b80058a053e90b5166adf7c93af97665a48d3899356d12363818"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:03:58.729Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","agent_provenance":{"model":{"value":"gpt-6.1-sol","source":"probe"},"effort":{"value":"high","source":"probe"},"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:03:58.729Z"},{"op":"replace","path":"/metadata/tags/2","value":"resources"},{"op":"replace","path":"/metadata/tags/1","value":"gh:unbraind/pm-rust#110"},{"op":"add","path":"/metadata/tags/3","value":"sdk"}],"before_hash":"72c231fff59edba9a6de169b805a898cd25b9f7886e3c1a2de68d0d8137dd932","after_hash":"78f11631dea3207c35b1326f7c6db6941562ee845891b02b154bb304ff49f513","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":"40f71d8895ed97c5b272fd1031dced7cede75a8c4deb2925254bc752aa5f91ea"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:04:00.858Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/16","value":{"created_at":"2026-10-23T19:04:00.858Z","author":"codex-agent","text":"Review round: remote head pulled and already contains main. GraphQL reports zero review threads and no CodeRabbit review body. Failed CI run 40004446634 shows Windows canonical list treats a schema parent file as NotFound, while the regression incorrectly requires Unix NotADirectory. Preserve strict Unix refusal assertions and verify Windows canonical fallback parity explicitly. Recheck the blocking lookup fix with an independent behavioral revert, self-review the complete diff, and run the unchanged native release gate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:04:00.858Z"}],"before_hash":"78f11631dea3207c35b1326f7c6db6941562ee845891b02b154bb304ff49f513","after_hash":"7fbcefd511c910dfff97c5304c6f80aae4f859f131140afa43bbb1757d075a4f","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":"6f87a8641e664af75353d0efbb34fcc9bc75d8a5e65816b2b0112b3c269007c7"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:06:26.096Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/17","value":{"created_at":"2026-10-23T19:06:26.096Z","author":"codex-agent","text":"Behavioral revert proof repeated at this head: replacing only the optional registry lookup in Workspace::read_session with the previous exists() check compiled successfully and failed both cargo +1.90.0 test --locked --lib read_session::tests::schema_parent_file_preserves_canonical_lookup_error -- --exact and cargo +1.90.0 test --locked --test read_session_contract schema_parent_file_never_certifies_cli_completion -- --exact, each exit 101. SDK reported admission unexpectedly succeeded; CLI returned exit 0 instead of 2. Restored the exact production source. Focused restored suites pass 16 SDK and 6 CLI tests on Unix. The added Windows regressions check canonical default-registry parity and cleanup rather than requiring a Unix-only OS error. Whole-diff self-review covered schema path components, bounded source errors, snapshot isolation, completion/cleanup, output continuations, and CLI writer failures; no additional instance of the swallowed lookup defect was found."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:06:26.096Z"}],"before_hash":"7fbcefd511c910dfff97c5304c6f80aae4f859f131140afa43bbb1757d075a4f","after_hash":"ae040cd1b9bc39b0d658a0938f3d3292bd06b04a965b96390816cdb529885b54","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":"50efb57a5117b4dd4e5a3ff87ee3793963a31ba95ce70efb12e45879f727a0e2"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:10:58.496Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/18","value":{"created_at":"2026-10-23T19:10:58.495Z","author":"codex-agent","text":"Coverage self-review: the non-NotFound registry error must also be exercised on Windows, where the file-parent case is NotFound. Added a platform-independent real invalid-filename regression for statuses and workflows using a NUL path from settings; it compares the exact canonical InvalidInput refusal and rejects admission. This preserves error-branch coverage on every release platform without mocking filesystem behavior or lowering thresholds."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:10:58.496Z"}],"before_hash":"ae040cd1b9bc39b0d658a0938f3d3292bd06b04a965b96390816cdb529885b54","after_hash":"17dc3c0a02e10b538bb98971bef7d6311cd6a8ce86db788de0826f25ed8ed1ee","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":"d2f93a104e892ed70c78744193dc0b9c5bdb7723c78dcad255342f587fb75b6a"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:26:31.374Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/19","value":{"created_at":"2026-10-23T19:26:31.374Z","author":"codex-agent","text":"Portable refusal revert proof: restoring only the previous exists() lookup compiled and made schema_invalid_path_preserves_canonical_lookup_error fail with exit 101 for admission unexpectedly succeeding. Restoring the exact source passed the test. All five existing pm-linked commands passed on retry (17 SDK, 6 CLI and three supporting tests; 26 total). The first linked CLI attempt hit bundle_integrity_torn_install during the oracle reinstall and passed after installation completed. Strict Clippy, private rustdoc, formatting and isolated CI-style health passed. The local unchanged just release-check attempt was explicitly stopped under heavy host contention after 334 library, 35 CLI unit and branch-merge tests passed and while ownership differentials continued; this is incomplete gate evidence. Push the reviewed candidate and require all unchanged full CI aggregates, including Windows and Bun, before proceeding to PR 105. No CodeRabbit review body exists; its issue comment says automatic review was skipped. Full health history advisories remain tracked separately by pm-rust-b4yl."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:26:31.374Z"}],"before_hash":"17dc3c0a02e10b538bb98971bef7d6311cd6a8ce86db788de0826f25ed8ed1ee","after_hash":"2ddd590dd7cb94902c22586932ea6bb50e93ee7e0fa6799128076a1a7399c024","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":"583e49d9b54449ac19029497d89f958aad1d49fd191199b798220f94a70e4d67"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:29:03.183Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/20","value":{"created_at":"2026-10-23T19:29:03.183Z","author":"codex-agent","text":"Final review candidate adds portable SDK and CLI invalid-filename checks alongside strict Unix file-parent refusals and explicit Windows canonical-default checks. Both portable regressions independently compiled and failed with only the old exists() behavior restored (exit 101): SDK admitted an invalid path; CLI leaked the private snapshot diagnostic instead of the canonical source diagnostic. All production bytes are restored. Final pm-linked CLI suite passed 7 tests; unchanged linked SDK/supporting suites passed 17 plus 3 tests, for 27 focused cases. Strict Clippy was corrected to use explicit ItemFilter/ListOptions defaults and rerun at unchanged warning policy. Final full gates are delegated to the unchanged CI matrix after push; local full gate was stopped as documented, never reported as passed. Release the claim for independent verification; keep the item in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:29:03.183Z"}],"before_hash":"2ddd590dd7cb94902c22586932ea6bb50e93ee7e0fa6799128076a1a7399c024","after_hash":"af65138bc7f4e035a362f3cba0692ed3e817be634fb55d85e651a1b7054e9720","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":"87c1b9ae05eafbbd70e9eaa085ab143a723ea5e5ef90497f9c8c08ed4d136d29"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:29:04.655Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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-23T19:29:04.655Z"}],"before_hash":"af65138bc7f4e035a362f3cba0692ed3e817be634fb55d85e651a1b7054e9720","after_hash":"5af823f41808f68a0c71793877b0ea053c97d1bbee2e505d7e2653f59e986cd1","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":"7f471faceaaaf2edcf7511c6115d875403b5d2fc823cdf6c5a630329fe990494"} diff --git a/.agents/pm/tasks/pm-rust-b4yl.toon b/.agents/pm/tasks/pm-rust-b4yl.toon index e43b8f2..a9d2dd5 100644 --- a/.agents/pm/tasks/pm-rust-b4yl.toon +++ b/.agents/pm/tasks/pm-rust-b4yl.toon @@ -6,10 +6,11 @@ status: open priority: 1 tags[3]: conformance,follow-up,history created_at: "2026-10-17T11:01:24.761Z" -updated_at: "2026-10-21T11:12:40.193Z" +updated_at: "2026-10-23T19:09:49.301Z" author: codex-agent parent: pm-rust-u4al -comments[2]{created_at,author,text}: +comments[3]{created_at,author,text}: "2026-10-18T13:11:30.015Z",codex-agent,"PR #92 second-review check reconfirms the existing discontinuities: full health --strict-exit reports history_drift_chain_mismatch for pm-rust-bj7a and pm-rust-unih. A fresh no-local clone of prior head c511d607579d746e201a15e54a37b287c79b1195 reproduces both; those items and history streams remain unchanged by this review. The documented CI command health --check-only --require-merge-drivers --strict-exit passes and must not be reported as verification of these older chains. Preserve this existing follow-up open; no re-anchor, repair or rewrite performed." "2026-10-21T11:12:40.192Z",codex-agent,"PR 101 review-round supplementary full health with pinned pm-cli 2026.10.21 confirms the same two existing chain mismatches in pm-rust-bj7a and pm-rust-unih. Both item/history files are unchanged relative to origin/main and the review candidate. Full health checks 74 items and reports two chain mismatches, zero hash mismatches, zero workspace-state mismatches. No existing records were rewritten. This is independent of the release-check and default CI health gates, which do not establish full history integrity." + "2026-10-23T19:09:49.301Z",codex-agent,PR 110 review round confirms the two already tracked chain mismatches for pm-rust-bj7a and pm-rust-unih. Both item/history pairs are unchanged from origin/main. Full local health authority remains ok but reports these historical advisories plus local extension and merge-driver configuration warnings. No append-only history was repaired or rewritten. Release and CI check-only health evidence is separate from verification of these older chains. body: "" diff --git a/docs/BOUNDED_READ_SESSIONS.md b/docs/BOUNDED_READ_SESSIONS.md index 388f88f..f063e07 100644 --- a/docs/BOUNDED_READ_SESSIONS.md +++ b/docs/BOUNDED_READ_SESSIONS.md @@ -18,7 +18,10 @@ unpaired UTF-16 compaction cuts; the existing TOON codec can refuse those cuts. Canonical list recovery flags refer to the ordinary `list` command; session controls are documented separately. Optional registry lookup falls back only for `NotFound`. Other lookup failures retain the canonical list error path and operating-system error and refuse -admission before any completion certificate. The original tracker is never written. +admission before any completion certificate. A regular-file schema parent produces +`NotADirectory` on Unix and refuses admission; Windows reports `NotFound` for +that lookup and retains the canonical reader's default-registry behavior. Separate +SDK and CLI regressions verify both platform contracts. The original tracker is never written. Git, extensions, JavaScript and telemetry are absent from the native path. The package's pinned oracle is unchanged. The snapshot is an immutable capture of individually read files, not a database diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index 7631d99..272ad49 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -1,5 +1,5 @@ //! Public CLI and SDK session contracts against genuine non-git trackers. -use pm_rust::{ReadSessionLimits, SessionCancellation, Workspace}; +use pm_rust::{ItemFilter, ListOptions, ReadSessionLimits, SessionCancellation, Workspace}; use serde_json::Value; use std::{fs, path::Path, process::Command}; @@ -285,7 +285,8 @@ fn disconnected_consumer_releases_session_and_storage_refusals_are_typed() -> Te Ok(()) } -/// Schema lookup errors refuse CLI delivery and remove the partially copied snapshot. +/// Unix schema lookup errors refuse delivery and remove the partially copied snapshot. +#[cfg(unix)] #[test] fn schema_parent_file_never_certifies_cli_completion() -> TestResult { let directory = fixture()?; @@ -320,3 +321,94 @@ fn schema_parent_file_never_certifies_cli_completion() -> TestResult { ); Ok(()) } + +/// Windows canonical NotFound fallback also completes through the CLI and cleans up. +#[cfg(windows)] +#[test] +fn schema_parent_file_cli_matches_windows_canonical_defaults() -> TestResult { + let directory = fixture()?; + let root = directory.path().join(".agents/pm"); + fs::remove_dir_all(root.join("schema"))?; + fs::write(root.join("schema"), "schema is a regular file")?; + let scratch = tempfile::tempdir()?; + for json in [false, true] { + let mut command = Command::new(env!("CARGO_BIN_EXE_pm-rust")); + command + .current_dir(directory.path()) + .arg("read-session") + .env("TMPDIR", scratch.path()) + .env("TMP", scratch.path()) + .env("TEMP", scratch.path()); + if json { + command.arg("--json"); + } + let result = command.output()?; + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + if json { + let page: Value = serde_json::from_slice(&result.stdout)?; + assert_eq!(page["receipt"]["complete"], true); + assert_eq!(page["output"]["items"].as_array().map(Vec::len), Some(3)); + } else { + assert!(String::from_utf8(result.stdout)?.contains("complete: true")); + } + assert_eq!(fs::read_dir(scratch.path())?.count(), 0); + } + Ok(()) +} + +/// Invalid filenames refuse admission with source diagnostics rather than snapshot paths. +#[test] +fn schema_invalid_path_preserves_cli_source_diagnostics() -> TestResult { + for name in ["statuses", "workflows"] { + let directory = fixture()?; + let root = directory.path().join(".agents/pm"); + let mut settings: Value = serde_json::from_slice(&fs::read(root.join("settings.json"))?)?; + settings["schema"]["files"][name] = serde_json::json!(format!("schema/{name}\0.json")); + fs::write(root.join("settings.json"), serde_json::to_vec(&settings)?)?; + let expected = Workspace::discover(directory.path())? + .list_page( + &ItemFilter::default(), + &ListOptions::default(), + "2026-10-21T12:00:00.000Z", + ) + .err() + .ok_or("canonical list must refuse the invalid filename")? + .to_string(); + let scratch = tempfile::tempdir()?; + for json in [false, true] { + let mut command = Command::new(env!("CARGO_BIN_EXE_pm-rust")); + command + .current_dir(directory.path()) + .arg("--workspace") + .arg(directory.path()) + .arg("read-session") + .env("TMPDIR", scratch.path()) + .env("TMP", scratch.path()) + .env("TEMP", scratch.path()); + if json { + command.arg("--json"); + } + let result = command.output()?; + assert_eq!(result.status.code(), Some(2)); + assert!( + result.stdout.is_empty(), + "refused source must emit no certificate" + ); + let refusal: Value = serde_json::from_slice(&result.stderr)?; + assert_eq!(refusal["code"], "session_source_refused"); + assert!( + refusal["detail"] + .as_str() + .ok_or("missing detail")? + .contains(&expected), + "admission must retain the canonical source diagnostic: {refusal}" + ); + assert_eq!(fs::read_dir(scratch.path())?.count(), 0); + } + } + Ok(()) +} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 4f4a9e1..94cd823 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -760,7 +760,8 @@ fn snapshot_temp_creation_failure_is_typed() -> TestResult { Ok(()) } -/// Registry lookup faults preserve canonical errors instead of certifying defaults. +/// Unix registry lookup faults preserve canonical errors instead of certifying defaults. +#[cfg(unix)] #[test] fn schema_parent_file_preserves_canonical_lookup_error() -> TestResult { for (name, configured) in [("statuses", false), ("statuses", true), ("workflows", true)] { @@ -807,3 +808,65 @@ fn schema_parent_file_preserves_canonical_lookup_error() -> TestResult { } Ok(()) } + +/// Windows reports a file parent as NotFound, matching canonical schema defaults. +#[cfg(windows)] +#[test] +fn schema_parent_file_matches_windows_canonical_defaults() -> TestResult { + let (_directory, workspace) = fixture(1)?; + fs::write( + workspace.pm_root().join("schema"), + "schema is a regular file", + )?; + let mut session = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + )?; + let expected = workspace.list_page(&ItemFilter::default(), &session.options, CLOCK)?; + let page = session.next_page()?.ok_or("missing defaults page")?; + assert_eq!( + page.output.stringify_read_json(), + expected.stringify_read_json() + ); + assert!(page.receipt.complete); + assert!(session.snapshot.is_none()); + Ok(()) +} + +/// Invalid schema filenames retain canonical I/O failures on every platform. +#[test] +fn schema_invalid_path_preserves_canonical_lookup_error() -> TestResult { + for name in ["statuses", "workflows"] { + let (_directory, workspace) = fixture(1)?; + let mut settings: serde_json::Value = + serde_json::from_slice(&fs::read(workspace.pm_root().join("settings.json"))?)?; + let relative = format!("schema/{name}\0.json"); + settings["schema"]["files"][name] = serde_json::json!(relative); + fs::write( + workspace.pm_root().join("settings.json"), + serde_json::to_vec(&settings)?, + )?; + let Err(PmRustError::Io { + path: expected_path, + source: expected_error, + }) = workspace.list_page(&ItemFilter::default(), &ListOptions::default(), CLOCK) + else { + return Err("canonical list must refuse the invalid schema path".into()); + }; + assert_eq!(expected_error.kind(), io::ErrorKind::InvalidInput); + assert_eq!(expected_path, workspace.read_pm_root().join(&relative)); + let result = workspace.read_session( + &ReadSessionLimits::default(), + SessionCancellation::default(), + CLOCK, + ); + let Err(SessionRefusal::Source(PmRustError::Io { path, source })) = result else { + return Err("invalid schema path must refuse before session certification".into()); + }; + assert_eq!(path, expected_path); + assert_eq!(source.kind(), expected_error.kind()); + assert_eq!(source.raw_os_error(), expected_error.raw_os_error()); + } + Ok(()) +} From e52dc73c6fd59bdfecf4eee4574748aace08eaaa Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:32:06 +0200 Subject: [PATCH 09/10] docs: format Windows schema error references for Clippy --- .agents/pm/features/pm-rust-bounded-read-sessions.toon | 5 +++-- .agents/pm/history/pm-rust-bounded-read-sessions.jsonl | 3 +++ tests/read_session_contract.rs | 2 +- tests/support/read_session_unit.rs | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 7b95d2b..e3f942a 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[4]: embedding,"gh:unbraind/pm-rust#110",resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T19:29:04.655Z" +updated_at: "2026-10-23T19:31:59.073Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[21]{created_at,author,text}: +comments[22]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -32,6 +32,7 @@ comments[21]{created_at,author,text}: "2026-10-23T19:10:58.495Z",codex-agent,"Coverage self-review: the non-NotFound registry error must also be exercised on Windows, where the file-parent case is NotFound. Added a platform-independent real invalid-filename regression for statuses and workflows using a NUL path from settings; it compares the exact canonical InvalidInput refusal and rejects admission. This preserves error-branch coverage on every release platform without mocking filesystem behavior or lowering thresholds." "2026-10-23T19:26:31.374Z",codex-agent,"Portable refusal revert proof: restoring only the previous exists() lookup compiled and made schema_invalid_path_preserves_canonical_lookup_error fail with exit 101 for admission unexpectedly succeeding. Restoring the exact source passed the test. All five existing pm-linked commands passed on retry (17 SDK, 6 CLI and three supporting tests; 26 total). The first linked CLI attempt hit bundle_integrity_torn_install during the oracle reinstall and passed after installation completed. Strict Clippy, private rustdoc, formatting and isolated CI-style health passed. The local unchanged just release-check attempt was explicitly stopped under heavy host contention after 334 library, 35 CLI unit and branch-merge tests passed and while ownership differentials continued; this is incomplete gate evidence. Push the reviewed candidate and require all unchanged full CI aggregates, including Windows and Bun, before proceeding to PR 105. No CodeRabbit review body exists; its issue comment says automatic review was skipped. Full health history advisories remain tracked separately by pm-rust-b4yl." "2026-10-23T19:29:03.183Z",codex-agent,"Final review candidate adds portable SDK and CLI invalid-filename checks alongside strict Unix file-parent refusals and explicit Windows canonical-default checks. Both portable regressions independently compiled and failed with only the old exists() behavior restored (exit 101): SDK admitted an invalid path; CLI leaked the private snapshot diagnostic instead of the canonical source diagnostic. All production bytes are restored. Final pm-linked CLI suite passed 7 tests; unchanged linked SDK/supporting suites passed 17 plus 3 tests, for 27 focused cases. Strict Clippy was corrected to use explicit ItemFilter/ListOptions defaults and rerun at unchanged warning policy. Final full gates are delegated to the unchanged CI matrix after push; local full gate was stopped as documented, never reported as passed. Release the claim for independent verification; keep the item in progress." + "2026-10-23T19:31:57.739Z",codex-agent,"CI run 40011795454 reached the Windows-only test declarations and Clippy correctly rejected two documentation references to NotFound without code formatting. Correct both docstrings using inline code formatting; no executable logic, test assertions or thresholds change. Linux Clippy cannot inspect cfg(windows) declarations, so exact-head Windows CI remains required. Read the completed Windows job log directly while the aggregate run was still active because gh run view --log-failed defers logs until run completion." files[16]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index a1635ed..3c5c357 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -42,3 +42,6 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T19:26:31.374Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/19","value":{"created_at":"2026-10-23T19:26:31.374Z","author":"codex-agent","text":"Portable refusal revert proof: restoring only the previous exists() lookup compiled and made schema_invalid_path_preserves_canonical_lookup_error fail with exit 101 for admission unexpectedly succeeding. Restoring the exact source passed the test. All five existing pm-linked commands passed on retry (17 SDK, 6 CLI and three supporting tests; 26 total). The first linked CLI attempt hit bundle_integrity_torn_install during the oracle reinstall and passed after installation completed. Strict Clippy, private rustdoc, formatting and isolated CI-style health passed. The local unchanged just release-check attempt was explicitly stopped under heavy host contention after 334 library, 35 CLI unit and branch-merge tests passed and while ownership differentials continued; this is incomplete gate evidence. Push the reviewed candidate and require all unchanged full CI aggregates, including Windows and Bun, before proceeding to PR 105. No CodeRabbit review body exists; its issue comment says automatic review was skipped. Full health history advisories remain tracked separately by pm-rust-b4yl."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:26:31.374Z"}],"before_hash":"17dc3c0a02e10b538bb98971bef7d6311cd6a8ce86db788de0826f25ed8ed1ee","after_hash":"2ddd590dd7cb94902c22586932ea6bb50e93ee7e0fa6799128076a1a7399c024","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":"583e49d9b54449ac19029497d89f958aad1d49fd191199b798220f94a70e4d67"} {"hash_algorithm":"sha256","ts":"2026-10-23T19:29:03.183Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/20","value":{"created_at":"2026-10-23T19:29:03.183Z","author":"codex-agent","text":"Final review candidate adds portable SDK and CLI invalid-filename checks alongside strict Unix file-parent refusals and explicit Windows canonical-default checks. Both portable regressions independently compiled and failed with only the old exists() behavior restored (exit 101): SDK admitted an invalid path; CLI leaked the private snapshot diagnostic instead of the canonical source diagnostic. All production bytes are restored. Final pm-linked CLI suite passed 7 tests; unchanged linked SDK/supporting suites passed 17 plus 3 tests, for 27 focused cases. Strict Clippy was corrected to use explicit ItemFilter/ListOptions defaults and rerun at unchanged warning policy. Final full gates are delegated to the unchanged CI matrix after push; local full gate was stopped as documented, never reported as passed. Release the claim for independent verification; keep the item in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:29:03.183Z"}],"before_hash":"2ddd590dd7cb94902c22586932ea6bb50e93ee7e0fa6799128076a1a7399c024","after_hash":"af65138bc7f4e035a362f3cba0692ed3e817be634fb55d85e651a1b7054e9720","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":"87c1b9ae05eafbbd70e9eaa085ab143a723ea5e5ef90497f9c8c08ed4d136d29"} {"hash_algorithm":"sha256","ts":"2026-10-23T19:29:04.655Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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-23T19:29:04.655Z"}],"before_hash":"af65138bc7f4e035a362f3cba0692ed3e817be634fb55d85e651a1b7054e9720","after_hash":"5af823f41808f68a0c71793877b0ea053c97d1bbee2e505d7e2653f59e986cd1","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":"7f471faceaaaf2edcf7511c6115d875403b5d2fc823cdf6c5a630329fe990494"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:31:56.611Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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-23T19:31:56.611Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"5af823f41808f68a0c71793877b0ea053c97d1bbee2e505d7e2653f59e986cd1","after_hash":"8edc01738395f01cad6084eb9bfa789f62aaf347465641e1d677cd3a37a399a8","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":"6bac999b6751e51aaf76f5470f1cf0e13e21f60d57a2723414aa35170be9a576"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:31:57.739Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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/21","value":{"created_at":"2026-10-23T19:31:57.739Z","author":"codex-agent","text":"CI run 40011795454 reached the Windows-only test declarations and Clippy correctly rejected two documentation references to NotFound without code formatting. Correct both docstrings using inline code formatting; no executable logic, test assertions or thresholds change. Linux Clippy cannot inspect cfg(windows) declarations, so exact-head Windows CI remains required. Read the completed Windows job log directly while the aggregate run was still active because gh run view --log-failed defers logs until run completion."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T19:31:57.739Z"}],"before_hash":"8edc01738395f01cad6084eb9bfa789f62aaf347465641e1d677cd3a37a399a8","after_hash":"0f65e82235428845480c8abe2a8ad2820229ed30922a6c742fa67a9548841937","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":"8b074a695b7ca08c9a75fad35aca7554fb7733db1ad09c7f571f40afcff70ed4"} +{"hash_algorithm":"sha256","ts":"2026-10-23T19:31:59.073Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"71244730038862278cb4ef7c","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-23T19:31:59.073Z"}],"before_hash":"0f65e82235428845480c8abe2a8ad2820229ed30922a6c742fa67a9548841937","after_hash":"da432366b5163f5fa5903303eceb3d5be6309536d59d3b140a776fadf3623f58","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":"dd7b599c00cb22a94e6e4c03a6db39e7c9351f396616eabc57a5fe516020843c"} diff --git a/tests/read_session_contract.rs b/tests/read_session_contract.rs index 272ad49..070628e 100644 --- a/tests/read_session_contract.rs +++ b/tests/read_session_contract.rs @@ -322,7 +322,7 @@ fn schema_parent_file_never_certifies_cli_completion() -> TestResult { Ok(()) } -/// Windows canonical NotFound fallback also completes through the CLI and cleans up. +/// Windows canonical `NotFound` fallback also completes through the CLI and cleans up. #[cfg(windows)] #[test] fn schema_parent_file_cli_matches_windows_canonical_defaults() -> TestResult { diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 94cd823..b36e3c4 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -809,7 +809,7 @@ fn schema_parent_file_preserves_canonical_lookup_error() -> TestResult { Ok(()) } -/// Windows reports a file parent as NotFound, matching canonical schema defaults. +/// Windows reports a file parent as `NotFound`, matching canonical schema defaults. #[cfg(windows)] #[test] fn schema_parent_file_matches_windows_canonical_defaults() -> TestResult { From 1c37108ebc94f30e176914113f85cbd11068b32d Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:08:16 +0200 Subject: [PATCH 10/10] Test lossless snapshot hashing across Unix filesystems --- .../pm-rust-bounded-read-sessions.toon | 9 +++-- .../pm-rust-bounded-read-sessions.jsonl | 5 +++ tests/support/read_session_unit.rs | 33 +++++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.agents/pm/features/pm-rust-bounded-read-sessions.toon b/.agents/pm/features/pm-rust-bounded-read-sessions.toon index 6ae7fea..c4a9efe 100644 --- a/.agents/pm/features/pm-rust-bounded-read-sessions.toon +++ b/.agents/pm/features/pm-rust-bounded-read-sessions.toon @@ -6,11 +6,11 @@ status: in_progress priority: 2 tags[4]: embedding,"gh:unbraind/pm-rust#110",resources,sdk created_at: "2026-10-23T14:33:11.991Z" -updated_at: "2026-10-23T21:59:00.516Z" +updated_at: "2026-10-23T22:07:52.953Z" author: codex-agent acceptance_criteria: "Large synthetic trackers respect a declared memory ceiling; cancellation releases resources; wire results remain oracle-conformant; compact JSON/TOON, typed refusals and remedies, README and example; read-only non-git public-tracker validation; unchanged CI gates." parent: pm-rust-u4al -comments[28]{created_at,author,text}: +comments[30]{created_at,author,text}: "2026-10-23T14:33:35.077Z",codex-agent,"Overlap audit: core SDK listAllComplete/certifyCompleteListResult own completeness and CLI list owns pagination. pm-context owns durable context packs and usage receipts; pm-web consumes complete SDK lists. Native pagination is pm-rust-ufzy and latency is pm-rust-wdj4. No existing embedding resource contract found. Reuse Workspace::list_page, strict decoding, lifecycle registries and JSON/TOON codecs. Add bounded private snapshot admission, cooperative cancellation and certified session receipts without another core query or parity pin. The crate remains native Rust; typed TypeScript is used for the validation harness." "2026-10-23T14:44:13.698Z",codex-agent,"Implementation decision: immutable private disk snapshots isolate admitted reads from later writers while preserving original cursor identity. Admission accounts native working-set reserve and bounds every source read; the declared ledger ceiling is distinct from measured process RSS. Reuse canonical list_page, strict complete-item decoder and duplicate checks. Pull ownership supplies backpressure, cancellation is cooperative between copying/query boundaries, and completion/failure/cancel/drop remove snapshots. Add unchanged npm/Bun aliases for the existing Rust release gate and a Bun CI aggregate. Local work is restricted to targeted tests, lint/typecheck and focused validation because the host is saturated." "2026-10-23T15:04:31.443Z",codex-agent,Initial CI and targeted regression found that the canonical output renderer returns an omission-only receipt for infeasible budgets rather than always raising an error. The embedding wrapper now refuses result_omitted before it can mark a session complete and cleans up the snapshot. Keep the canonical renderer unchanged. Also isolate fixture init with an explicit tracker path so linked-test sandbox environment cannot redirect initialization. CI aggregate thresholds remain unchanged. @@ -39,6 +39,8 @@ comments[28]{created_at,author,text}: "2026-10-23T21:20:53.174Z",codex-agent,"All six pm-linked commands passed: 18 session unit tests, 7 CLI/SDK contract tests, 1 release-entrypoint test, 1 registry test, 1 writer test and 1 focused digest rerun (29 executions, 28 distinct tests). Reverted hashing failed as expected; restored hashing passes. No tracked dist assets exist. CodeRabbit only posted an automatic-review-skipped issue comment, with no outside-diff findings." "2026-10-23T21:57:26.517Z",codex-agent,"Local full just release-check passed fmt, strict all-target Clippy, private rustdoc and preceding test suites, then failed only relationship_large_edge_limit_finishes_at_budget_floor: its five-second native child was killed (no exit code) under heavy host load while the oracle returned the expected budget refusal. Orientation suite: 31 passed, 1 deadline failure. Existing follow-up pm-rust-local-packet-deadline covers this condition; no threshold or unrelated code changes. Retry that exact case in isolation and require all full CI aggregates on the pushed merge. Standalone just crash-check passed typechecking, ESLint and all harness tests." "2026-10-23T21:58:57.533Z",codex-agent,"Final local evidence: six pm-linked commands passed (29 executions, 28 distinct tests). Behavioral revert of only filename hashing compiled and failed the unique-digest assertion, then exact source restoration passed. Full just release-check with isolated Git configuration reached 514 passed tests and one five-second native-child deadline failure; isolated cargo +1.90.0 test --locked --test orientation_differential relationship_large_edge_limit_finishes_at_budget_floor -- --exact also hit that deadline on a later query (68.65 seconds total). This is the existing pm-rust-local-packet-deadline condition; only exact-head CI can complete the full gate evidence. Standalone just crash-check passed TypeScript, ESLint and 22 of 22 tests, including real non-git syscall crash/restart and update/comment first-fsync recovery. Main remains the reviewed PR 105 merge, and remote PR 110 head has not moved. Conflict resolution retains both Bun setup and Linux strace installation. Changelog regeneration is byte-identical; regenerate once more after these final tracker writes and require its check. Implementation work is complete; keep this item in progress and release the claim for independent verification. No threads replied to or resolved; no release or PR merge." + "2026-10-23T22:02:40.831Z",codex-agent,"CI on merge 508bd7b exposed a test portability error: macOS rejects the invalid-UTF-8 filesystem rename with Illegal byte sequence. Correct the regression fixture, retaining a Linux real-session filesystem test and adding a Unix Admission::copy digest test using valid source and target files with distinct raw relative-name bytes. This verifies the exact hashing boundary on macOS without assuming its filesystem admits such names. Production hashing and all release thresholds remain unchanged." + "2026-10-23T22:07:47.748Z",codex-agent,"Portable regression verification: cargo +1.90.0 test --locked --lib snapshot_digest_preserves_non_utf8 passed both cases. Restoring only Admission::copy to lossy relative.to_string_lossy().as_bytes() compiled and made BOTH the real Linux filename/directory session test and the Unix raw-relative-name digest test fail their unique-digest assertions (0 passed, 2 failed, exit 101). Exact production source restored. All seven pm-linked commands then passed: 19 session units, 7 CLI/SDK contracts, three supporting single cases, one original digest rerun and two prefix-filtered digest reruns (32 executions, 29 distinct tests). Strict all-target/all-feature Clippy and formatting passed. Initial CI run 40030616438 failed macOS only on the invalid-filename fixture; the correction retains real-filesystem coverage on Linux and validates the same exact digest boundary on Unix with ordinary source and target files. Final CI must rerun all four unchanged aggregate gates. The local five-second packet deadline remains covered by pm-rust-local-packet-deadline; no unrelated production changes or threshold changes." files[17]{path,scope}: .github/workflows/ci.yml,project Cargo.toml,project @@ -57,13 +59,14 @@ files[17]{path,scope}: tests/release_pipeline_contract.rs,project tests/support/main_unit.rs,project tests/support/read_session_unit.rs,project -tests[6]{command,scope,provenance{author,created_at,source_kind,source_ref}}: +tests[7]{command,scope,provenance{author,created_at,source_kind,source_ref}}: "cargo test --locked --lib read_session::tests -- --test-threads=1",project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test read_session_contract -- --test-threads=1,project,codex-agent,"2026-10-23T14:48:33.983Z",local_mutation,feat/bounded-read-sessions cargo test --locked --test release_pipeline_contract npm_and_bun_share_the_full_native_aggregate -- --test-threads=1,project,codex-agent,"2026-10-23T15:34:24.686Z",local_mutation,feat/bounded-read-sessions "cargo test --locked --lib list_registry::tests::workflow_fallbacks_and_settings_errors -- --test-threads=1",project,codex-agent,"2026-10-23T15:56:02.726Z",local_mutation,feat/bounded-read-sessions cargo test --locked --bin pm-rust session_frame_writer_preserves_errors_and_framing -- --test-threads=1,project,codex-agent,"2026-10-23T17:03:49.263Z",local_mutation,feat/bounded-read-sessions cargo +1.90.0 test --locked --lib snapshot_digest_preserves_non_utf8_filename_and_directory_bytes,project,codex-agent,"2026-10-23T21:19:48.349Z",local_mutation,feat/bounded-read-sessions + cargo +1.90.0 test --locked --lib snapshot_digest_preserves_non_utf8,project,codex-agent,"2026-10-23T22:04:08.407Z",local_mutation,feat/bounded-read-sessions docs[3]{path,scope}: docs/BOUNDED_READ_SESSIONS.md,project examples/read_session.rs,project diff --git a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl index 21fdfe2..0860482 100644 --- a/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl +++ b/.agents/pm/history/pm-rust-bounded-read-sessions.jsonl @@ -55,3 +55,8 @@ {"hash_algorithm":"sha256","ts":"2026-10-23T21:58:54.740Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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/files/15/path","value":"tests/support/main_unit.rs"},{"op":"replace","path":"/metadata/files/14/path","value":"tests/release_pipeline_contract.rs"},{"op":"replace","path":"/metadata/files/13/path","value":"tests/read_session_contract.rs"},{"op":"replace","path":"/metadata/files/12/path","value":"src/workspace.rs"},{"op":"replace","path":"/metadata/files/11/path","value":"src/read_session.rs"},{"op":"replace","path":"/metadata/files/10/path","value":"src/main.rs"},{"op":"replace","path":"/metadata/files/9/path","value":"src/list.rs"},{"op":"replace","path":"/metadata/files/8/path","value":"src/lib.rs"},{"op":"replace","path":"/metadata/files/7/path","value":"scripts/read-session-validate.mts"},{"op":"replace","path":"/metadata/files/6/path","value":"scripts/read-session-clock.mts"},{"op":"replace","path":"/metadata/files/5/path","value":"package.json"},{"op":"replace","path":"/metadata/files/4/path","value":"package-lock.json"},{"op":"replace","path":"/metadata/files/3/path","value":"justfile"},{"op":"replace","path":"/metadata/files/2/path","value":"CHANGELOG.md"},{"op":"add","path":"/metadata/files/16","value":{"path":"tests/support/read_session_unit.rs","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T21:58:54.740Z"}],"before_hash":"384dab94100a2d8d59bd592eb1932d51a0f95e22bcc0cc3ce189bb7152c87656","after_hash":"41cf960b70fcdfe936cd427243b31e060df42c03d4be1f32001bbca9ba690003","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":"53600fc0937ec7f61999668ac7adb64599a0038e6825e71fe1a563f5a3f5c098"} {"hash_algorithm":"sha256","ts":"2026-10-23T21:58:57.533Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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/27","value":{"created_at":"2026-10-23T21:58:57.533Z","author":"codex-agent","text":"Final local evidence: six pm-linked commands passed (29 executions, 28 distinct tests). Behavioral revert of only filename hashing compiled and failed the unique-digest assertion, then exact source restoration passed. Full just release-check with isolated Git configuration reached 514 passed tests and one five-second native-child deadline failure; isolated cargo +1.90.0 test --locked --test orientation_differential relationship_large_edge_limit_finishes_at_budget_floor -- --exact also hit that deadline on a later query (68.65 seconds total). This is the existing pm-rust-local-packet-deadline condition; only exact-head CI can complete the full gate evidence. Standalone just crash-check passed TypeScript, ESLint and 22 of 22 tests, including real non-git syscall crash/restart and update/comment first-fsync recovery. Main remains the reviewed PR 105 merge, and remote PR 110 head has not moved. Conflict resolution retains both Bun setup and Linux strace installation. Changelog regeneration is byte-identical; regenerate once more after these final tracker writes and require its check. Implementation work is complete; keep this item in progress and release the claim for independent verification. No threads replied to or resolved; no release or PR merge."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T21:58:57.533Z"}],"before_hash":"41cf960b70fcdfe936cd427243b31e060df42c03d4be1f32001bbca9ba690003","after_hash":"8ca33f92f20cc501c0061b592e91d84a22e6d6fcba41baef0f905fe05c64bc82","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":"38f05e7dccc379d4b751a8e26fe033acdf52dee79edebeaae039be58650e73bf"} {"hash_algorithm":"sha256","ts":"2026-10-23T21:59:00.516Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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-23T21:59:00.516Z"}],"before_hash":"8ca33f92f20cc501c0061b592e91d84a22e6d6fcba41baef0f905fe05c64bc82","after_hash":"05a43bea38e42846185466ea671fb584e8c90d9f5aff12f398416f15118b3d8f","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":"870481dccabfc1f6aea2fd4a629ae21c0f6016e36ecaa6c3c66524ca7a1d526b"} +{"hash_algorithm":"sha256","ts":"2026-10-23T22:02:34.583Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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-23T22:02:34.583Z"},{"op":"add","path":"/metadata/assignee","value":"codex-agent"},{"op":"add","path":"/metadata/claim_principal","value":"codex-agent"}],"before_hash":"05a43bea38e42846185466ea671fb584e8c90d9f5aff12f398416f15118b3d8f","after_hash":"917e1c7ce000b348dabe010d7aac11256b6d0d62f6b234e8b12b682c66958872","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":"1e3866e56ae3afcbec4b8ea85172d5ab126dfc6a6d059c14f41721fea9c10cc0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T22:02:40.832Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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/28","value":{"created_at":"2026-10-23T22:02:40.831Z","author":"codex-agent","text":"CI on merge 508bd7b exposed a test portability error: macOS rejects the invalid-UTF-8 filesystem rename with Illegal byte sequence. Correct the regression fixture, retaining a Linux real-session filesystem test and adding a Unix Admission::copy digest test using valid source and target files with distinct raw relative-name bytes. This verifies the exact hashing boundary on macOS without assuming its filesystem admits such names. Production hashing and all release thresholds remain unchanged."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T22:02:40.832Z"}],"before_hash":"917e1c7ce000b348dabe010d7aac11256b6d0d62f6b234e8b12b682c66958872","after_hash":"a0e78c13a79774e01cdc4c4274cab9983cfcc8da18a26def40457fc04180e94e","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":"9c04216a94a2a9782ee68c0dc15e015dd2c801f4e564d5109b1a38468a3ff6f0"} +{"hash_algorithm":"sha256","ts":"2026-10-23T22:04:08.693Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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/6","value":{"command":"cargo +1.90.0 test --locked --lib snapshot_digest_preserves_non_utf8","scope":"project","provenance":{"author":"codex-agent","created_at":"2026-10-23T22:04:08.407Z","source_kind":"local_mutation","source_ref":"feat/bounded-read-sessions"}}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T22:04:08.693Z"}],"before_hash":"a0e78c13a79774e01cdc4c4274cab9983cfcc8da18a26def40457fc04180e94e","after_hash":"97dee3917423a0b4d2f643cb9ee9a97fa36451c19fce8d1548acf8d0d1dafdad","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":"1932393edc7b39aa6c4963b311dce4de3e9b599faaa8ed388b7cc05032003d03"} +{"hash_algorithm":"sha256","ts":"2026-10-23T22:07:47.748Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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/29","value":{"created_at":"2026-10-23T22:07:47.748Z","author":"codex-agent","text":"Portable regression verification: cargo +1.90.0 test --locked --lib snapshot_digest_preserves_non_utf8 passed both cases. Restoring only Admission::copy to lossy relative.to_string_lossy().as_bytes() compiled and made BOTH the real Linux filename/directory session test and the Unix raw-relative-name digest test fail their unique-digest assertions (0 passed, 2 failed, exit 101). Exact production source restored. All seven pm-linked commands then passed: 19 session units, 7 CLI/SDK contracts, three supporting single cases, one original digest rerun and two prefix-filtered digest reruns (32 executions, 29 distinct tests). Strict all-target/all-feature Clippy and formatting passed. Initial CI run 40030616438 failed macOS only on the invalid-filename fixture; the correction retains real-filesystem coverage on Linux and validates the same exact digest boundary on Unix with ordinary source and target files. Final CI must rerun all four unchanged aggregate gates. The local five-second packet deadline remains covered by pm-rust-local-packet-deadline; no unrelated production changes or threshold changes."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-10-23T22:07:47.748Z"}],"before_hash":"97dee3917423a0b4d2f643cb9ee9a97fa36451c19fce8d1548acf8d0d1dafdad","after_hash":"e4519c0ccf44e89396629bbf7a2189ba0e2f835e63db467512c2e41ae1fbf214","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":"72c2c79d949f36b7211e7bba094ac107ee299deca02b21a5dcf05e26b7ce9106"} +{"hash_algorithm":"sha256","ts":"2026-10-23T22:07:52.953Z","author":"codex-agent","author_source":"asserted","agent_harness":"codex","agent_model":"gpt-6.1-sol","agent_model_source":"probe","agent_instance":"478e676915f6b9152bb6a70f","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-23T22:07:52.953Z"}],"before_hash":"e4519c0ccf44e89396629bbf7a2189ba0e2f835e63db467512c2e41ae1fbf214","after_hash":"c06fc8477335fc9f8fc5d8b945dbcf336bc20cd223f9d3bd27c52384f74bff36","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":"fd87832dd799b753f7136aa89b3070ac0a43919ff0ccd24da1c55de79d04043c"} diff --git a/tests/support/read_session_unit.rs b/tests/support/read_session_unit.rs index 5b54bd1..1bc47e5 100644 --- a/tests/support/read_session_unit.rs +++ b/tests/support/read_session_unit.rs @@ -871,8 +871,8 @@ fn schema_invalid_path_preserves_canonical_lookup_error() -> TestResult { Ok(()) } -/// Distinct native filename bytes must remain distinct in the snapshot certificate. -#[cfg(unix)] +/// Linux filesystems admit raw names that macOS rejects before snapshot admission. +#[cfg(target_os = "linux")] #[test] fn snapshot_digest_preserves_non_utf8_filename_and_directory_bytes() -> TestResult { use std::ffi::OsString; @@ -910,3 +910,32 @@ fn snapshot_digest_preserves_non_utf8_filename_and_directory_bytes() -> TestResu } Ok(()) } + +/// Digest encoding preserves native bytes even on filesystems that refuse raw names. +#[cfg(unix)] +#[test] +fn snapshot_digest_preserves_non_utf8_path_bytes() -> TestResult { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + + let (_temporary, workspace) = fixture(1)?; + let source = workspace.pm_root().join("tasks/nested/demo-0.toon"); + let scratch = tempfile::tempdir()?; + let target = scratch.path().join("copy.toon"); + let token = SessionCancellation::default(); + for directory in [false, true] { + let mut digests = BTreeSet::new(); + for byte in [0x80, 0x81] { + let mut name = b"tasks/".to_vec(); + name.push(byte); + name.extend_from_slice(if directory { b"/demo-0.toon" } else { b".toon" }); + let relative = PathBuf::from(OsString::from_vec(name)); + let mut ledger = admission(&token); + ledger.copy(&source, &target, &relative, true)?; + assert_eq!(ledger.items, 1); + assert_eq!(fs::read(&target)?, fs::read(&source)?); + assert!(digests.insert(ledger.hash.finalize().to_vec())); + } + } + Ok(()) +}