@sema-agent/core 7.15.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/memory-engine/delegation-settlement.d.ts +37 -12
- package/dist/core/memory-engine/delegation-settlement.js +128 -39
- package/dist/core/memory-engine/engine.d.ts +9 -8
- package/dist/core/memory-engine/engine.js +115 -120
- package/dist/core/memory-engine/file-backend.d.ts +17 -5
- package/dist/core/memory-engine/file-backend.js +78 -34
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +2 -2
- package/dist/core/memory-engine/layout.d.ts +152 -54
- package/dist/core/memory-engine/layout.js +441 -104
- package/dist/core/memory-engine/types.d.ts +8 -3
- package/dist/core/memory-engine/types.js +5 -0
- package/dist/core/runner/abort-race.d.ts +3 -3
- package/dist/core/runner/active-skill-scope.d.ts +2 -2
- package/dist/core/runner/assemble-result.d.ts +29 -29
- package/dist/core/runner/checkpoint-scope.d.ts +4 -4
- package/dist/core/runner/clock-and-limits.d.ts +3 -3
- package/dist/core/runner/compaction-call-options.d.ts +11 -11
- package/dist/core/runner/compaction-knobs.d.ts +4 -4
- package/dist/core/runner/compaction-seams.d.ts +5 -5
- package/dist/core/runner/contracts.d.ts +222 -224
- package/dist/core/runner/decide-continuation.d.ts +6 -6
- package/dist/core/runner/denial-limit-arms.d.ts +8 -8
- package/dist/core/runner/derived-route-fallback.d.ts +2 -2
- package/dist/core/runner/gate-exit.d.ts +6 -6
- package/dist/core/runner/git-status-frame.d.ts +7 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
- package/dist/core/runner/memory-capture-optout.d.ts +1 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
- package/dist/core/runner/memory-consolidation.d.ts +6 -6
- package/dist/core/runner/park-commit.d.ts +6 -6
- package/dist/core/runner/permission-rule-lanes.d.ts +19 -19
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -5
- package/dist/core/runner/prepare-config-doors.d.ts +21 -21
- package/dist/core/runner/prepare-context-lane.d.ts +6 -6
- package/dist/core/runner/prepare-defer-classify.d.ts +2 -2
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.d.ts +2 -2
- package/dist/core/runner/prepare-gate-stations.d.ts +5 -5
- package/dist/core/runner/prepare-hands-readface.d.ts +33 -33
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-listings.d.ts +3 -3
- package/dist/core/runner/prepare-lsp.d.ts +3 -3
- package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
- package/dist/core/runner/prepare-memory.js +6 -7
- package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.d.ts +6 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +4 -4
- package/dist/core/runner/prepare-prompt-inputs.d.ts +5 -5
- package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
- package/dist/core/runner/prepare-question-face.d.ts +4 -4
- package/dist/core/runner/prepare-run-refs.d.ts +11 -11
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
- package/dist/core/runner/prepare-task.d.ts +12 -12
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
- package/dist/core/runner/remote-env-retry.d.ts +3 -3
- package/dist/core/runner/resume-admission.d.ts +1 -1
- package/dist/core/runner/resume-claim.d.ts +1 -1
- package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
- package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
- package/dist/core/runner/resume-preflight.d.ts +1 -1
- package/dist/core/runner/resume-review-outcome.d.ts +1 -1
- package/dist/core/runner/run-brain-sinks.d.ts +1 -1
- package/dist/core/runner/run-clock-and-content.d.ts +4 -4
- package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.d.ts +2 -2
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-leg.d.ts +3 -3
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -4
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
- package/dist/core/runner/runtask.d.ts +14 -14
- package/dist/core/runner/session-file-state-replay.d.ts +3 -3
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/steer-admission.d.ts +2 -2
- package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
- package/dist/core/runner/stream-reap.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.d.ts +8 -8
- package/dist/core/runner/teardown-bounded.d.ts +2 -2
- package/dist/core/runner/tool-defer-gate.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +33 -33
- package/dist/core/runner/tool-end-body.d.ts +3 -3
- package/dist/core/runner/tool-face-overlay.d.ts +2 -2
- package/dist/core/runner/tool-output-projection.d.ts +2 -2
- package/dist/core/runner/turn-attachments.d.ts +53 -53
- package/dist/core/runner/usage-accounting.d.ts +4 -4
- package/dist/core/runner/workspace-path.d.ts +1 -1
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -40
- package/dist/tools/fs/encoding.d.ts +31 -40
- package/dist/tools/fs/fs-bash.d.ts +35 -35
- package/dist/tools/fs/fs-pdf.d.ts +8 -8
- package/dist/tools/fs/fs-search-tools.d.ts +4 -4
- package/dist/tools/fs/fs-shared.d.ts +144 -158
- package/dist/tools/fs/fs-write.d.ts +8 -8
- package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
- package/dist/tools/fs/index.d.ts +74 -78
- package/dist/tools/fs/notebook.d.ts +24 -25
- package/dist/tools/fs/pdf.d.ts +3 -3
- package/dist/tools/fs/read-deny.d.ts +22 -22
- package/dist/tools/fs/read-face.d.ts +21 -24
- package/dist/tools/fs/repo-map.d.ts +8 -8
- package/dist/tools/fs/safety.d.ts +116 -129
- package/dist/tools/fs/search.d.ts +63 -71
- package/package.json +5 -2
- package/test/export-surface.snapshot.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.16.0 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
### BREAKING — memory directory layout: every scope's home is `<memoryDir>/<label>`, the memory root is nobody's home (#699; design/699; @server @cli @test)
|
|
6
|
+
- **Before.** The first write scope ever used for a memory dir claimed the ROOT itself (loose `*.md` + `MEMORY.md` at `<memoryDir>/`) and every later scope nested beneath it (`<memoryDir>/<label>/`). Ten rules hung off that: a durable root claim, an empty-label ⇔ root-scope registry invariant, three-arm home derivation, a `""` exception in both collision arms, three copies of "exclude the sibling scope dirs when scanning the root", a `skipRoot`/`excludeTopDirs` chmod special case, a runner fence arm refusing a write root that contains another scope, the erase sweep treating the root as an index home, read-only sessions reading the root's `MEMORY.md`, and six sidecars keyed by physical paths.
|
|
7
|
+
- **After (five rules).** ① A scope's home is `join(memoryDir, label)` with `label = scopeDirName(scope)` (the registered label is the truth once written); the memory dir itself is never a home. ② The registry is `scopes.json = { v: 2, scopes, rootOwner?, migrating? }`; every label matches `SCOPE_DIR_LABEL_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/` (the closure of `scopeDirName`'s outputs; pinned by a 5k-input property); the four read shapes `absent | legacy | v2 | migrating` are a closed set (`SCOPE_REGISTRY_SHAPES`, disposition table `REGISTRY_SHAPE_DERIVES_PATHS`, registered in `docs/CLOSED-SETS.md`) — `legacy` and `migrating` are refused fail-closed by `registerScope` / `scopeDirFor` / `registeredScopes` until the adoption has run. ③ A home must be a REAL directory or absent (`lstat` at registration and at every mount; a link or a file at the home path refuses). ④ The two collision arms (exact + case-fold) are unchanged minus the `""` exception. ⑤ Every persistent sidecar key that names a memory file is the ledger form `(scope, rel)` — projection debts, instruction holds, session-account membership, the write-scan fuse — so a home move never re-keys anything (`index-revs.json` and `MEMORY.md` link targets stay model-facing physical coordinates, by design).
|
|
8
|
+
- **Migration (one-shot, two-phase, durable; `adoptRootScopeLayout` in `layout.ts`).** Runs before the first registry-derived path of every mount (`MemoryEngine.materialize` → the backend's `adoptRootScopeLayout` face under its transaction lock, plus the copy-out projection tree directly) and at the head of every backend public face (`ensureLayout`, non-blocking try-acquire; the locked faces run it under their lock; a view object runs it on every call). Phase 0 converts the registry (`rootScope` → `rootOwner`, the owner gets its label) and enumerates the root's loose content on EVERY call; content with an owner (`rootOwner ?? this mount's write scope / the batch's first add scope`) passes four zero-write pre-checks (control plane inside the root; the home a link/file or a non-empty directory with no move in progress; any loose name already present in the home) or refuses with `ControlPlaneCorruptError` (memory-less + loud at the runner). Commit A writes the `migrating` marker; phase 1 moves each entry (`lstat` destination, `rename`, source-gone = already moved) and removes `.casefold-probe-*` residue; phase 2 clears `index-revs.json`; commit B drops the marker. No sidecar key and no index text is rewritten (cross-scope index lines heal under the existing rebuild law: orphan cleared, mechanical row re-minted, model wording lost once). A registry lock heartbeat (`utimes` + strict ownership re-proof, plus the backend's transaction-lock heartbeat) runs before every action; a lost lock throws immediately and the next holder resumes from the marker. Content with NO owner on record (a read-only mount over a registry-less root) is left in place, disclosed once, never served. Two disclosure codes: `memory.layout_migrated`, `memory.layout_unowned_root_content` (incident sink).
|
|
9
|
+
- **BREAKING (package surface).** `MemorySessionHandle.writableRoot` is `string | null` — `null` iff there is no write face (`writeScope === null`); it is never `memoryDir` any more. New `writableRootOf(handle)` (throws on `null`) for call sites that established a write face. `scanEntryFiles(dir, { exclude })` — the `exclude` option is removed. `MemoryEngine.readonlyDirNamesUnderRoot` is REMOVED, together with the runner arm that was its only consumer: the set it answered — another scope's directory lying under the writable root — is empty by construction now that homes never nest. Internal bucket: `claimRootScope` / `rootScopeOf` removed.
|
|
10
|
+
- **BREAKING (on disk; downstream may read or seed these).** `scopes.json` form (above). `projection-debts.json` / `holds.json` / `session-accounts.json` are `version: 2` with `(scope, rel)` keys (`HoldRow.relPath` → `HoldRow.rel`, home-relative; `SessionAccountRow.unattributed` is `Array<{ scope, rel }>` plus `unattributable?: string[]`); each converts once, in its own lock, on the first read that finds the old form (idempotent by field name + version); a row the registry cannot attribute is KEPT (`unresolved` / `unattributable`) and never served / marks the whole session unattributed — never dropped. `scan-fuse.json` is `{ v: 2, counts }` keyed by `scanFuseKey(scope, rel)`; the absolute-path form is dropped (fail-open reset). The user-visible path of every entry and index gains one level (`memory/x.md` → `memory/<label>/x.md`; server's default scope `local` ⇒ `memory/local/`).
|
|
11
|
+
- **Narrowing (read-only sessions).** An all-read-only session (`writableRoot === null`) never reads a `MEMORY.md` from disk in either provenance mode — its index is the mechanical rebuild off committed headers (links computed against the memory dir as a virtual base). The model-worded index the root owner's read-only sessions used to get by accident is no longer served to them; write sessions are unchanged.
|
|
12
|
+
- **Retired special cases.** Root claim at materialize and at `applyPatches`; `siblingScopeDirNames`, `excludedSubdirNames`, the five `isRoot` scan branches, `chmodScopeTree`'s `skipRoot` / `excludeTopDirs`; the erase sweep's memory-root index candidate (its failure text names `<scope homes>`); the harvest/materialize `exclude` sets. `gateWrite` on a read-only handle judges the whole plane (`memoryDir`) and refuses every path under it with `read_only_layering`.
|
|
13
|
+
- **Runner (the fence).** `prepare-memory.ts` consumes `writableRootOf(writeHandle)` at the admission seat and takes an explicit `root !== null` branch in the two write-gate legs. The admission's sibling-scope refusal is gone: `foreignUnderRoot` keeps only the control-plane arm (a directory advertised to the model must not contain the engine's own metadata), and `writeChannelOpen` keeps all four conjuncts. Consequence — the deployment the old arm refused (the write scope owning the root with any other scope registered: a single-user server with an org layer, or any `defaultScopes` extra) is admitted and taught again, and it is not a hole: the admitted directory is that scope's home, so a sibling scope's file, the homes' shared parent and a scope another process registers mid-session all stay outside the fence (`path_not_in_root`).
|
|
14
|
+
- **Operations.** Stop every process sharing a memory dir before upgrading; an older engine re-occupying the root after the move leaves a non-empty home beside loose root content, which the next mount refuses loudly (no silent merge). Interruption safety is process-level (`atomicWriteFileSync` does not fsync directories); every step resumes from the registry marker and the sidecar row forms.
|
|
15
|
+
- **Review rounds (adversarial, three; all findings reproduced by reading, adopted unless noted).** Round 1 — five: a registry commit fenced only before its journal write (a holder stolen from past its commit point could overwrite the stealer's main file / remove its journal) → strict ownership re-proved before each of the three steps; the v1 sidecar key conversions were lazy (a scope registered after the move, named like an old topic directory, would re-read a v1 key's first segment) → converted inside the adoption; a v1 hold row's slug was not shape-checked (a crafted row could derive an escaping seat) → the rel-path shape law on `slug`; registry accumulators over `{}` dropped a `__proto__` scope → null-prototype accumulators; a v1 root owner whose prospective home already stood non-empty was silently re-read under new slugs → refused before conversion. Round 2 — three: conversion ran after the commit that cleared the durable mark → conversion now precedes every registry commit (the v1 registry / the marker stays until every sidecar is in the ledger form); the strict journal roll-forward replayed unfenced → fenced before its replay write and its journal removal; a home spelled in another letter case on a folding volume was enumerated as loose content and moved into itself → recognized by directory identity, self-move refused. Round 3 — three: the strict-ledger commit (holds / session accounts / projection debts) had the round-1 gap → fenced like the registry commit; the marker persisted the root verbatim (a relative spelling resumed from another cwd would read as finished) → absolute; **declared, not chased**: the window between a fence and the atomic writer's publishing rename is inherent to the mkdir-lock design (three rounds on the same root — the lock family — stop here by rule).
|
|
16
|
+
- Pins: `test/memory-layout-migration.test.ts` (39 — P1 registry shapes + the 5k label property; P2 exclusivity + physical form; P3 mainline on the real load path; P4 idempotence + three breakpoints + a foreign-tree marker; P5 four pre-checks; P6 heartbeat + stolen lock; P7 journal-before-adoption; P9 read-only index both modes; P10 owner resolution ①–④; P11 chmod; P12/P15 erase sweep; P14 tripwire; P16 read-only gate; P17 key conversions + unattributable rows; the review-round seams: conversion-before-registration, the escaping v1 hold slug, the `__proto__` scope, the occupied v1 home, the stolen-lock commit refusal, pre-check 4 on the v2 path, the heartbeat refusing at the very next action, the backend face's replay-then-adopt order, conversion-before-commit on both branches, the folded home spelling, the absolute marker root); re-pinned `memory-engine` / `memory-scope-enumeration` / `memory-txn-lock` registry pins and the design/336 hold/account fixtures. Re-pinned end-to-end through the Runner: `test/backlog691-advertised-writable-dir.test.ts` (P8 — two scopes over one memory dir, the write scope the first writer: home admitted and taught, own home read/write both ways, sibling + mid-session third scope + shared parent refused, no refused/skipped line). Wire rows: `docs/wire-consumption/unreleased-699.md`.
|
|
17
|
+
|
|
18
|
+
- Docs/gate (#710) · new `gate:docs-tool-names` fails on a RETIRED tool name (`report_blocked`, `run_workflow`, `web_fetch`, `submit_output`, `present_plan`, `tool_search`, `MultiEdit`, `bash_readonly`, …) surviving in `docs/**/*.md` outside a small historical-record whitelist; one-time rename sweep over `ARCHITECTURE.md`, `GATE-OUTCOME-STATIONS.md`, `INTEGRATION-CORE.md`, `PROMPT-ASSEMBLY-EXECUTION-PLAN.md`, `PROMPT-ASSEMBLY-HANDOFF.md`, `PROMPT-SECTION-MAP.md`, `PROMPT-SENTENCE-EVIDENCE-INDEX.md`, `ROADMAP.md`, `SERVICE-INTEGRATION-GUIDE.md`, `sdk/02`, `sdk/07`, `sdk/09` to their live names, plus one factual fix (`sdk/09-security-and-policies.md`: `handsReadOnly` mounts `Bash` in its read-only posture, not a separate `bash_readonly` tool). No behaviour change.
|
|
19
|
+
|
|
20
|
+
### Contract — `Model.cost` is optional; `isModelPriced` is exported (#718; @server @cli)
|
|
21
|
+
- **Member-level type change (package surface).** `Model.cost` is now `cost?: {…}`. The runtime already read it as optional everywhere — `modelCostToPricing(undefined)` yields the all-zero table, `selectModel` sorts a model without `cost` as `Infinity`, every brain adapter reads `c?.input ?? 0`, and the RB-368 predicate `isModelPriced` takes `cost?: unknown` — only the type claimed it was required, so a host without a price table had to fabricate one or cast. Absent ⇒ UNPRICED; a free model declares four `0` rates; the two are now distinguishable at the type as they already were at runtime. Readers of `model.cost.<rate>` on a core-typed `Model` must guard for absence (compile-time; no core read and no server/cli `src` read does so unguarded).
|
|
22
|
+
- **Exported.** `isModelPriced(model, pricing)` (tier `advanced`) — the one RB-368 predicate ("does a price table EXIST": an id-keyed `RunnerDeps.pricing` entry or the model's own `cost`) that a host previously had to re-implement beside `computeCostMicroUsd` / `modelCostToPricing`.
|
|
23
|
+
- Pins: `test/backlog718-model-cost-optional.test.ts` (the literal without `cost` typechecks; predicate three-way; root export identity); `test/export-surface.snapshot.json`.
|
|
24
|
+
|
|
25
|
+
### Tooling (no behaviour change)
|
|
26
|
+
- Docs (#704 ② pilot sweep · `src/tools/fs`, comments only — zero behaviour face): every comment in the bucket now states a contract or an invariant; the ticket ids (`RB-NNN`/`#NNN`/`design/NNN`), channel coordinates, review-round attributions, batch names and deprecated-corpus byte offsets are gone, and a defect retelling is rewritten as the rule that holds. CC parity/divergence assertions, fail-closed reasons, refusal orderings and safety consequences are kept verbatim in substance. Bucket reading: comment lines 7334→7147, ratio 0.4835→0.4771, process-word hits 863→9 (the 9 remaining are English false positives — `Math.round`, `round trip`). Four bucket ratchets banked (`comment-ratio`, `internal-wording` 46→3, `line-anchor` 15→6, `legacy-residue` 35→33) plus one now-unique `contract-sentences` baseline row removed. Narrative archived to the internal repo.
|
|
27
|
+
- Docs (#704 ② sweep · `src/core/runner`, comments only — zero behaviour face, 36 batch commits): a mechanical two-tier label stripper (leading ticket-cluster removal + capitalize-restart; pure-citation-parenthetical removal) over all 111 files in the bucket, followed by a hand fix where the stripper's capitalization exposed a pre-existing cross-file exact-duplicate sentence (`gate:contract-sentences`) between `prepare-boundary-parks.ts` and `prepare-park-ask.ts` — the shorter copy is now a pointer to the fuller one. Every comment keeps its contract/invariant content, fail-closed reasoning and CC-anchor sentence verbatim; only the leading `design/NNN`/`RB-NNN`/`#NNN`/`[NNNN]`/`codex …` coordinate cluster and pure-citation parentheticals are gone. Bucket reading: comment lines 23666→23667 (+1, the dedup-fix wrap), total lines 47238→47239, ratio 0.5010→0.50101, process-word hits 3048→1340 (56% down; the residue is dominated by domain vocabulary the lexicon false-positives on — `review`/`round` inside real identifiers like `dry_run_review`/`plan_review`/`Math.round` — plus mid-sentence citations the leading-label/pure-citation matchers deliberately do not touch). Two bucket ratchets banked (`comment-ratio` processHitsMax 3048→1340, ratioMax 0.501→0.5011; `internal-wording` 174→121); `legacy-residue` (104) and `line-anchor` (13) unchanged for this bucket. No internal-archive file — every removed span was a short ticket/citation label, not a relocatable narrative paragraph.
|
|
28
|
+
- Docs (#704 ② sweep, second pass · `src/core/runner`, comments only — zero behaviour face, ~30 batch commits, hand-edited): the first pass's leading-label/pure-citation matchers were conservative on mid-sentence and multi-line citations (design/RB/#/[NNNN]/codex-round/rescan clusters woven into the middle of a contract sentence); this pass read every one of the 1204 process-word hits the first pass left (excluding `prepare-memory.ts`, car A/#699's file) by hand, file by file, and either compressed the citation out of the sentence (keeping the contract clause) or confirmed the hit as genuine domain vocabulary the lexicon false-positives on (`review`/`plan_review`/`human-review`/`dry-run review`/`code-review` — real gate-kind and CC-tool names, not review-process narrative; `round` inside `model round`/`classifier round`/`round cap`/`corrective round` — real bounded-iteration counts, not review-round citations). `design`/`RB`/`#`/`codex`/`rescan` citations are now at ZERO across the whole bucket (one `[1995]①` kept verbatim — a quoted test-name string, not a citation). Bucket reading (excluding `prepare-memory.ts`, unchanged by this car): process-word hits 1204→149 (88% down on the touched files); whole-bucket total (incl. `prepare-memory.ts`'s 109, task-0 census regex false-positive fix applied first) 1313→258. Two bucket ratchets banked (`comment-ratio` processHitsMax 1340→258; `internal-wording` 121→91) plus one `contract-sentences` baseline shrink (a 14-file and a 2-file duplicate row both resolved; one new byte-identical duplicate the citation-stripping introduced was reworded, not admitted). `comment-census.mjs`'s `PROCESS_WORD_RE` gained two false-positive exclusions (`Math.round(...)`/`round trip` no longer match `round`; identifier-embedded `review` spellings were already excluded by the existing word boundary, now pinned) — every bucket's baseline in `test/comment-ratio-ratchet.test.ts` re-measured and banked down to match (lower-only). Two structural-pin test landmarks in `prepare-wiring-manifest.ts` were reverted verbatim after breaking `test/design385-slice2b-agents.test.ts`'s exact-substring search (test files are off-limits; the source comment kept its citation instead). No internal-archive file — every removed span was a citation label or a short compressible clause, not a relocatable narrative paragraph.
|
|
29
|
+
- Added · `npm run car:gate -- '<word list>' [--base <sha>] [--full] [--log-dir <dir>]` (`scripts/car-gate.mjs`, #705): runs a dispatch order's door-family as one command — `tsc --noEmit`, a derived vitest suite (content-grepped `test/*.test.ts` ∪ the eleven door-family pins ∪ `test/gate-all-roster.test.ts`; hub names in the word list auto-stripped and announced), and `verify-layering`; `--full` adds `npm run build && node scripts/gate-all.mjs --with-dist --rc`. Any step non-zero makes the whole run non-zero and every step still runs. Pins: `test/car-gate.test.ts`.
|
|
30
|
+
- Added · `npm run gate:comment-ratio` (`scripts/comment-census.mjs`, #704, gate-landing only — no src sweep in this car): a per-directory-bucket ratchet on `src/**/*.ts`'s comment-line ratio and process-word (round/rescan/codex/design-ticket/battle-narrative) hit count, sibling to `internal-wording-gate`/`legacy-residue-gate`. One-way (ratchet-down-must-bank), a HARD cap (0.25 ratio / 0 hits) for a bucket absent from the table, wired into `gate:all`'s static tier. Pins: `test/comment-ratio-ratchet.test.ts`.
|
|
31
|
+
|
|
3
32
|
## 7.15.0 — 2026-09-11
|
|
4
33
|
|
|
5
34
|
### Internal — abstraction unification pass over the 7.15.0 merges (no behaviour change; exports +1 type)
|
|
@@ -14,6 +14,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
14
14
|
"memory.announce_failed",
|
|
15
15
|
"memory.partition_adopt_failed",
|
|
16
16
|
"memory.partition_split",
|
|
17
|
+
"memory.layout_migrated",
|
|
18
|
+
"memory.layout_unowned_root_content",
|
|
17
19
|
"memory.tail",
|
|
18
20
|
"memory.pollution_mark_failed",
|
|
19
21
|
"memory.session_polluted",
|
|
@@ -144,32 +144,54 @@ export declare function replayExternalSettlementEffects(controlDir: string, inpu
|
|
|
144
144
|
sessionId: string;
|
|
145
145
|
}>;
|
|
146
146
|
export declare const SESSION_ACCOUNTS_FILE = "session-accounts.json";
|
|
147
|
+
/** One member of the sticky set: the ledger form `(scope, rel)` — `rel` relative to the scope's
|
|
148
|
+
* home, `/`-separated. Location-free, so a home move never re-keys the membership. */
|
|
149
|
+
export interface UnattributedMember {
|
|
150
|
+
scope: string;
|
|
151
|
+
rel: string;
|
|
152
|
+
}
|
|
147
153
|
export interface SessionAccountRow {
|
|
148
154
|
sessionId: string;
|
|
149
155
|
openedAt: number;
|
|
150
|
-
/** The STICKY unattributed set
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
unattributed:
|
|
156
|
+
/** The STICKY unattributed set, frozen at materialize (§3.6 归属判据): pre-existing, uncommitted
|
|
157
|
+
* files seen while a FOREIGN dangling open row stood. Membership never shrinks on later writes
|
|
158
|
+
* (the sticky law); it clears only when the row is re-opened with a fresh classification (the
|
|
159
|
+
* residue was adjudicated by an intervening harvest). */
|
|
160
|
+
unattributed: UnattributedMember[];
|
|
161
|
+
/** The v1 members (memory-dir-relative paths) the registry could not attribute at the one-time
|
|
162
|
+
* conversion. Kept, never dropped; while any stands, EVERY membership question about
|
|
163
|
+
* this session answers "unattributed" (over-marking is the safe direction). */
|
|
164
|
+
unattributable?: string[];
|
|
155
165
|
closedAt?: number;
|
|
156
166
|
/** r7-3 — the host valve closed this row WITHOUT adjudication: it stops dangling but keeps
|
|
157
167
|
* triggering the residue arm until the next FULL-domain harvest closes an account normally
|
|
158
168
|
* (an unadjudicated close must not launder the residue window it covered). */
|
|
159
169
|
unadjudicated?: true;
|
|
160
170
|
}
|
|
171
|
+
/** The one-time v1 key-form conversions of the three engine sidecars, in one call (each in its own
|
|
172
|
+
* lock, each idempotent). The layout adoption runs this before any later scope registration can
|
|
173
|
+
* change what a v1 key's first segment names. */
|
|
174
|
+
export declare function convertV1SidecarKeys(controlDir: string): void;
|
|
161
175
|
/** The dangling-trigger read: open rows (and unadjudicated closes) belonging to OTHER sessions.
|
|
162
176
|
* These are what arm the unattributed residue classification at a session's materialize. */
|
|
163
177
|
export declare function foreignDanglingSessionAccounts(controlDir: string, selfSessionId: string): SessionAccountRow[];
|
|
178
|
+
/** The membership question the harvest asks per file: is `(scope, rel)` in the session's sticky
|
|
179
|
+
* set? A session carrying members the conversion could not attribute answers yes for every seat. */
|
|
180
|
+
export interface SessionUnattributedSet {
|
|
181
|
+
has(scope: string, rel: string): boolean;
|
|
182
|
+
/** True when no seat can answer yes (no members, none unattributable). */
|
|
183
|
+
readonly empty: boolean;
|
|
184
|
+
}
|
|
185
|
+
export declare function emptyUnattributedSet(): SessionUnattributedSet;
|
|
164
186
|
/** Open (or re-open, on a same-session resume) the session's account row, persisting the sticky
|
|
165
187
|
* unattributed classification frozen at THIS materialize. */
|
|
166
188
|
export declare function openSessionAccount(controlDir: string, input: {
|
|
167
189
|
sessionId: string;
|
|
168
190
|
now: () => number;
|
|
169
|
-
unattributed: readonly
|
|
191
|
+
unattributed: readonly UnattributedMember[];
|
|
170
192
|
}): void;
|
|
171
193
|
/** The current session's sticky unattributed set (the harvest's residue-arm input). */
|
|
172
|
-
export declare function sessionUnattributedSet(controlDir: string, sessionId: string):
|
|
194
|
+
export declare function sessionUnattributedSet(controlDir: string, sessionId: string): SessionUnattributedSet;
|
|
173
195
|
/** Close the session's account row — called ONLY after a FULL-domain harvest (zero BUDGET-deferred
|
|
174
196
|
* files, i.e. `HarvestReport.degraded` absent): a partial harvest's close would launder the deferred
|
|
175
197
|
* residue window (§3.6 序则②). A normal full close also clears every standing `unadjudicated` flag
|
|
@@ -209,8 +231,10 @@ export interface HoldRow {
|
|
|
209
231
|
op: "add" | "update";
|
|
210
232
|
/** CAS anchor at capture time (update form): release refuses to blind-write over a later edit. */
|
|
211
233
|
baseRev?: string;
|
|
212
|
-
/**
|
|
213
|
-
|
|
234
|
+
/** The plane seat the file was removed from, in the ledger form: relative to the write scope's
|
|
235
|
+
* home (`<memoryDir>/<label>`), `/`-separated — `${slug}.md`. Location-free, so a home move never
|
|
236
|
+
* re-keys a hold. */
|
|
237
|
+
rel: string;
|
|
214
238
|
/** The entry slug (path relative to the scope dir, sans `.md`) — the release leg's projection
|
|
215
239
|
* identity and its filename-scan input. */
|
|
216
240
|
slug: string;
|
|
@@ -263,7 +287,8 @@ export declare function noReplaceRestore(stagingPath: string, destPath: string):
|
|
|
263
287
|
*/
|
|
264
288
|
export declare function openInstructionHold(controlDir: string, input: {
|
|
265
289
|
sessionId: string;
|
|
266
|
-
|
|
290
|
+
/** The seat in the ledger form (relative to the write scope's home). */
|
|
291
|
+
rel: string;
|
|
267
292
|
absPath: string;
|
|
268
293
|
content: string;
|
|
269
294
|
entryId?: string;
|
|
@@ -290,11 +315,11 @@ export declare function openInstructionHold(controlDir: string, input: {
|
|
|
290
315
|
* (the file stays on the plane and walks the ordinary/§3.6 arms; never "capture whatever is
|
|
291
316
|
* there now" — that would hold a third writer's bytes under the first writer's account);
|
|
292
317
|
* - `capturing` + custody absent + plane file gone ⇒ `capture_lost` (nothing to hold).
|
|
293
|
-
* - `held` + plane file REAPPEARED at the
|
|
318
|
+
* - `held` + plane file REAPPEARED at the seat ⇒ left alone (a fresh write is a fresh file —
|
|
294
319
|
* the next harvest judges it on its own; the hold's custody bytes stay the hold's).
|
|
295
320
|
*/
|
|
296
321
|
export declare function reconcileHolds(controlDir: string, input: {
|
|
297
|
-
|
|
322
|
+
homeOf: (scope: string) => string;
|
|
298
323
|
now: () => number;
|
|
299
324
|
}): void;
|
|
300
325
|
/** Dispose one held row: custody bytes move to the quarantine directory (host-auditable), the row
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, linkSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { QUARANTINE_DIR, appendChallengeEvents, enqueueMemoryAnnouncement, ensureDirExists, lineageContributionsOfSession, lockedStrictUpdate, markSessionPolluted, readSessionPollution, readStrictSidecar, ControlPlaneCorruptError } from "./layout.js";
|
|
4
|
+
import { QUARANTINE_DIR, appendChallengeEvents, enqueueMemoryAnnouncement, ensureDirExists, v1MemoryPathKeyResolver, lineageContributionsOfSession, lockedStrictUpdate, markSessionPolluted, readProjectionDebts, readSessionPollution, readStrictSidecar, ControlPlaneCorruptError } from "./layout.js";
|
|
5
5
|
export const DELEGATION_SETTLEMENT_FILE = "delegation-settlement.json";
|
|
6
6
|
function reqStr(v) {
|
|
7
7
|
return typeof v === "string" && v.length > 0;
|
|
@@ -233,36 +233,101 @@ export function replayExternalSettlementEffects(controlDir, input) {
|
|
|
233
233
|
return applied;
|
|
234
234
|
}
|
|
235
235
|
export const SESSION_ACCOUNTS_FILE = "session-accounts.json";
|
|
236
|
+
function malformedAccountRow(row) {
|
|
237
|
+
return (typeof row !== "object" ||
|
|
238
|
+
row === null ||
|
|
239
|
+
!reqStr(row.sessionId) ||
|
|
240
|
+
typeof row.openedAt !== "number" ||
|
|
241
|
+
!Number.isFinite(row.openedAt) ||
|
|
242
|
+
!Array.isArray(row.unattributed) ||
|
|
243
|
+
(row.closedAt !== undefined && (typeof row.closedAt !== "number" || !Number.isFinite(row.closedAt))) ||
|
|
244
|
+
(row.unadjudicated !== undefined && row.unadjudicated !== true));
|
|
245
|
+
}
|
|
246
|
+
function isUnattributedMember(x) {
|
|
247
|
+
return typeof x === "object" && x !== null && reqStr(x.scope) && safeRelPath(x.rel);
|
|
248
|
+
}
|
|
236
249
|
function coerceSessionAccounts(raw) {
|
|
237
250
|
if (raw === undefined)
|
|
238
|
-
return { version:
|
|
251
|
+
return { version: 2, rows: [] };
|
|
239
252
|
const rec = raw;
|
|
240
253
|
if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
|
|
241
254
|
throw new ControlPlaneCorruptError("memory session-account ledger has the wrong shape");
|
|
242
|
-
if (rec.version !== 1)
|
|
255
|
+
if (rec.version !== 1 && rec.version !== 2)
|
|
243
256
|
throw new ControlPlaneCorruptError(`memory session-account ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
|
|
244
|
-
|
|
257
|
+
const bad = () => {
|
|
258
|
+
throw new ControlPlaneCorruptError("memory session-account ledger: a row is malformed — refusing (fail-closed; an unevaluable row must not read as adjudicated)");
|
|
259
|
+
};
|
|
260
|
+
const rows = rec.rows;
|
|
261
|
+
for (const r of rows) {
|
|
245
262
|
const row = r;
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
row.unattributed.some((x) => typeof x !== "string") ||
|
|
253
|
-
(row.closedAt !== undefined && (typeof row.closedAt !== "number" || !Number.isFinite(row.closedAt))) ||
|
|
254
|
-
(row.unadjudicated !== undefined && row.unadjudicated !== true)) {
|
|
255
|
-
throw new ControlPlaneCorruptError("memory session-account ledger: a row is malformed — refusing (fail-closed; an unevaluable row must not read as adjudicated)");
|
|
263
|
+
if (malformedAccountRow(row))
|
|
264
|
+
bad();
|
|
265
|
+
const members = Array.isArray(row.unattributed) ? row.unattributed : bad();
|
|
266
|
+
if (rec.version === 1) {
|
|
267
|
+
if (members.some((x) => typeof x !== "string"))
|
|
268
|
+
bad();
|
|
256
269
|
}
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
else {
|
|
271
|
+
if (!members.every(isUnattributedMember))
|
|
272
|
+
bad();
|
|
273
|
+
if (row.unattributable !== undefined && (!Array.isArray(row.unattributable) || row.unattributable.some((x) => typeof x !== "string")))
|
|
274
|
+
bad();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return rec.version === 1 ? { version: 1, rows: rows } : { version: 2, rows: rows };
|
|
278
|
+
}
|
|
279
|
+
function convertSessionAccounts(read, controlDir) {
|
|
280
|
+
if (read.version === 2)
|
|
281
|
+
return { rec: read, converted: false };
|
|
282
|
+
const resolve = v1MemoryPathKeyResolver(controlDir);
|
|
283
|
+
const rows = read.rows.map((r) => {
|
|
284
|
+
const unattributed = [];
|
|
285
|
+
const unattributable = [];
|
|
286
|
+
for (const relPath of r.unattributed) {
|
|
287
|
+
const key = resolve(relPath);
|
|
288
|
+
if (key === undefined)
|
|
289
|
+
unattributable.push(relPath);
|
|
290
|
+
else
|
|
291
|
+
unattributed.push(key);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
sessionId: r.sessionId,
|
|
295
|
+
openedAt: r.openedAt,
|
|
296
|
+
unattributed,
|
|
297
|
+
...(unattributable.length > 0 ? { unattributable } : {}),
|
|
298
|
+
...(r.closedAt !== undefined ? { closedAt: r.closedAt } : {}),
|
|
299
|
+
...(r.unadjudicated !== undefined ? { unadjudicated: r.unadjudicated } : {}),
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
return { rec: { version: 2, rows }, converted: true };
|
|
303
|
+
}
|
|
304
|
+
export function convertV1SidecarKeys(controlDir) {
|
|
305
|
+
readProjectionDebts(controlDir);
|
|
306
|
+
readHolds(controlDir);
|
|
307
|
+
readSessionAccountsConverted(controlDir);
|
|
308
|
+
}
|
|
309
|
+
function readSessionAccountsConverted(controlDir) {
|
|
310
|
+
const read = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
|
|
311
|
+
if (read.version === 2)
|
|
312
|
+
return read;
|
|
313
|
+
return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (again) => {
|
|
314
|
+
const { rec, converted } = convertSessionAccounts(again, controlDir);
|
|
315
|
+
return converted ? { next: rec, result: rec } : { result: rec };
|
|
316
|
+
});
|
|
259
317
|
}
|
|
260
318
|
export function foreignDanglingSessionAccounts(controlDir, selfSessionId) {
|
|
261
|
-
const rec =
|
|
319
|
+
const rec = readSessionAccountsConverted(controlDir);
|
|
262
320
|
return rec.rows.filter((r) => r.sessionId !== selfSessionId && (r.closedAt === undefined || r.unadjudicated === true));
|
|
263
321
|
}
|
|
322
|
+
export function emptyUnattributedSet() {
|
|
323
|
+
return { has: () => false, empty: true };
|
|
324
|
+
}
|
|
325
|
+
function memberKey(scope, rel) {
|
|
326
|
+
return JSON.stringify([scope, rel]);
|
|
327
|
+
}
|
|
264
328
|
export function openSessionAccount(controlDir, input) {
|
|
265
|
-
lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (
|
|
329
|
+
lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
|
|
330
|
+
const { rec } = convertSessionAccounts(read, controlDir);
|
|
266
331
|
const row = rec.rows.find((r) => r.sessionId === input.sessionId);
|
|
267
332
|
if (row === undefined) {
|
|
268
333
|
rec.rows.push({ sessionId: input.sessionId, openedAt: input.now(), unattributed: [...input.unattributed] });
|
|
@@ -276,17 +341,25 @@ export function openSessionAccount(controlDir, input) {
|
|
|
276
341
|
row.openedAt = input.now();
|
|
277
342
|
delete row.closedAt;
|
|
278
343
|
delete row.unadjudicated;
|
|
279
|
-
|
|
344
|
+
const merged = new Map(row.unattributed.map((m) => [memberKey(m.scope, m.rel), m]));
|
|
345
|
+
for (const m of input.unattributed)
|
|
346
|
+
merged.set(memberKey(m.scope, m.rel), { scope: m.scope, rel: m.rel });
|
|
347
|
+
row.unattributed = [...merged.values()];
|
|
280
348
|
}
|
|
281
349
|
return { next: rec, result: undefined };
|
|
282
350
|
});
|
|
283
351
|
}
|
|
284
352
|
export function sessionUnattributedSet(controlDir, sessionId) {
|
|
285
|
-
const
|
|
286
|
-
|
|
353
|
+
const row = readSessionAccountsConverted(controlDir).rows.find((r) => r.sessionId === sessionId);
|
|
354
|
+
if (row === undefined)
|
|
355
|
+
return emptyUnattributedSet();
|
|
356
|
+
const all = (row.unattributable?.length ?? 0) > 0;
|
|
357
|
+
const keys = new Set(row.unattributed.map((m) => memberKey(m.scope, m.rel)));
|
|
358
|
+
return { has: (scope, rel) => all || keys.has(memberKey(scope, rel.split("\\").join("/"))), empty: !all && keys.size === 0 };
|
|
287
359
|
}
|
|
288
360
|
export function closeSessionAccount(controlDir, input) {
|
|
289
|
-
return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (
|
|
361
|
+
return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
|
|
362
|
+
const { rec } = convertSessionAccounts(read, controlDir);
|
|
290
363
|
const row = rec.rows.find((r) => r.sessionId === input.sessionId);
|
|
291
364
|
if (row === undefined)
|
|
292
365
|
return { result: false };
|
|
@@ -303,7 +376,8 @@ export function resolveSessionAccountRecord(controlDir, input) {
|
|
|
303
376
|
e.code = "memory.session_account_resolve_unattributed";
|
|
304
377
|
throw e;
|
|
305
378
|
}
|
|
306
|
-
lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (
|
|
379
|
+
lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
|
|
380
|
+
const { rec } = convertSessionAccounts(read, controlDir);
|
|
307
381
|
const row = rec.rows.find((r) => r.sessionId === input.sessionId);
|
|
308
382
|
if (row === undefined) {
|
|
309
383
|
const e = new Error(`resolveSessionAccount: unknown sessionId ${JSON.stringify(input.sessionId)}`);
|
|
@@ -321,21 +395,23 @@ const HOLD_STATUSES = new Set(["capturing", "held", "released", "disposed"]);
|
|
|
321
395
|
const HOLD_TERMINALS = new Set(["dirty", "expired", "conflict", "discarded", "capture_lost"]);
|
|
322
396
|
function coerceHolds(raw) {
|
|
323
397
|
if (raw === undefined)
|
|
324
|
-
return { version:
|
|
398
|
+
return { version: 2, rows: [] };
|
|
325
399
|
const rec = raw;
|
|
326
400
|
if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
|
|
327
401
|
throw new ControlPlaneCorruptError("memory instruction-hold ledger has the wrong shape");
|
|
328
|
-
if (rec.version !== 1)
|
|
402
|
+
if (rec.version !== 1 && rec.version !== 2)
|
|
329
403
|
throw new ControlPlaneCorruptError(`memory instruction-hold ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
|
|
330
|
-
|
|
404
|
+
const v1 = rec.version === 1;
|
|
405
|
+
const rows = [];
|
|
406
|
+
const raws = rec.rows;
|
|
407
|
+
for (const r of raws) {
|
|
331
408
|
const row = r;
|
|
332
409
|
if (typeof row !== "object" ||
|
|
333
410
|
row === null ||
|
|
334
411
|
!reqStr(row.holdId) ||
|
|
335
412
|
!reqStr(row.sessionId) ||
|
|
336
413
|
(row.op !== "add" && row.op !== "update") ||
|
|
337
|
-
!
|
|
338
|
-
!reqStr(row.slug) ||
|
|
414
|
+
!safeRelPath(row.slug) ||
|
|
339
415
|
!reqStr(row.scope) ||
|
|
340
416
|
!reqStr(row.captureName) ||
|
|
341
417
|
!reqStr(row.contentDigest) ||
|
|
@@ -350,15 +426,28 @@ function coerceHolds(raw) {
|
|
|
350
426
|
(row.entryId !== undefined && typeof row.entryId !== "string") ||
|
|
351
427
|
(row.baseRev !== undefined && typeof row.baseRev !== "string") ||
|
|
352
428
|
(row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
|
|
353
|
-
!safeRelPath(row.relPath) ||
|
|
429
|
+
(v1 ? !safeRelPath(row.relPath) : !safeRelPath(row.rel)) ||
|
|
354
430
|
!safeBasename(row.captureName)) {
|
|
355
431
|
throw new ControlPlaneCorruptError("memory instruction-hold ledger: a row is malformed — refusing (fail-closed; an unevaluable hold must not read as settled)");
|
|
356
432
|
}
|
|
433
|
+
if (v1) {
|
|
434
|
+
const { relPath: _relPath, ...rest } = row;
|
|
435
|
+
rows.push({ ...rest, rel: `${row.slug}.md` });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
rows.push(row);
|
|
439
|
+
}
|
|
357
440
|
}
|
|
358
|
-
return
|
|
441
|
+
return { version: 2, rows };
|
|
442
|
+
}
|
|
443
|
+
function holdsOnDiskAreV1(raw) {
|
|
444
|
+
return typeof raw === "object" && raw !== null && raw.version === 1;
|
|
359
445
|
}
|
|
360
446
|
export function readHolds(controlDir) {
|
|
361
|
-
|
|
447
|
+
const raw = readStrictSidecar(controlDir, HOLDS_FILE, "memory instruction-hold ledger");
|
|
448
|
+
if (!holdsOnDiskAreV1(raw))
|
|
449
|
+
return coerceHolds(raw).rows;
|
|
450
|
+
return lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (again) => ({ next: again, result: again.rows }));
|
|
362
451
|
}
|
|
363
452
|
function sha256(buf) {
|
|
364
453
|
return createHash("sha256").update(buf).digest("hex");
|
|
@@ -397,7 +486,7 @@ export function noReplaceRestore(stagingPath, destPath) {
|
|
|
397
486
|
return "restored";
|
|
398
487
|
}
|
|
399
488
|
export function openInstructionHold(controlDir, input) {
|
|
400
|
-
const holdId = `hold-${input.now()}-${createHash("sha256").update(`${input.sessionId}\n${input.
|
|
489
|
+
const holdId = `hold-${input.now()}-${createHash("sha256").update(`${input.sessionId}\n${input.rel}\n${input.content}`).digest("hex").slice(0, 12)}`;
|
|
401
490
|
const buf = Buffer.from(input.content, "utf8");
|
|
402
491
|
const digest = sha256(buf);
|
|
403
492
|
const captureName = `${holdId}.md`;
|
|
@@ -420,7 +509,7 @@ export function openInstructionHold(controlDir, input) {
|
|
|
420
509
|
...(input.entryId !== undefined ? { entryId: input.entryId } : {}),
|
|
421
510
|
op: input.op,
|
|
422
511
|
...(input.baseRev !== undefined ? { baseRev: input.baseRev } : {}),
|
|
423
|
-
|
|
512
|
+
rel: input.rel,
|
|
424
513
|
slug: input.slug,
|
|
425
514
|
scope: input.scope,
|
|
426
515
|
captureName,
|
|
@@ -490,12 +579,12 @@ export function openInstructionHold(controlDir, input) {
|
|
|
490
579
|
}
|
|
491
580
|
export function reconcileHolds(controlDir, input) {
|
|
492
581
|
const holdDir = join(controlDir, HOLD_DIR);
|
|
493
|
-
const discloseStranded = (holdId,
|
|
582
|
+
const discloseStranded = (holdId, rel, stagingName) => {
|
|
494
583
|
try {
|
|
495
584
|
enqueueMemoryAnnouncement(controlDir, {
|
|
496
585
|
kind: "gate",
|
|
497
586
|
at: input.now(),
|
|
498
|
-
items: [`instruction hold ${holdId}: a concurrent writer's bytes for ${JSON.stringify(
|
|
587
|
+
items: [`instruction hold ${holdId}: a concurrent writer's bytes for ${JSON.stringify(rel)} could not be restored onto the plane — they are preserved at hold/${stagingName} in the memory control plane for host recovery`],
|
|
499
588
|
});
|
|
500
589
|
}
|
|
501
590
|
catch {
|
|
@@ -512,7 +601,7 @@ export function reconcileHolds(controlDir, input) {
|
|
|
512
601
|
terminalLost = true;
|
|
513
602
|
}
|
|
514
603
|
else {
|
|
515
|
-
const plane = join(input.
|
|
604
|
+
const plane = join(input.homeOf(row.scope), row.rel);
|
|
516
605
|
if (existsSync(plane)) {
|
|
517
606
|
try {
|
|
518
607
|
const stagingName = uniqueStagingName(row.holdId, input.now);
|
|
@@ -522,7 +611,7 @@ export function reconcileHolds(controlDir, input) {
|
|
|
522
611
|
unlinkSync(staging);
|
|
523
612
|
}
|
|
524
613
|
else if (noReplaceRestore(staging, plane) === "stranded") {
|
|
525
|
-
discloseStranded(row.holdId, row.
|
|
614
|
+
discloseStranded(row.holdId, row.rel, stagingName);
|
|
526
615
|
}
|
|
527
616
|
}
|
|
528
617
|
catch {
|
|
@@ -531,7 +620,7 @@ export function reconcileHolds(controlDir, input) {
|
|
|
531
620
|
}
|
|
532
621
|
}
|
|
533
622
|
else {
|
|
534
|
-
const plane = join(input.
|
|
623
|
+
const plane = join(input.homeOf(row.scope), row.rel);
|
|
535
624
|
if (existsSync(plane)) {
|
|
536
625
|
const bytes = readFileSync(plane);
|
|
537
626
|
if (sha256(bytes) === row.contentDigest) {
|
|
@@ -549,7 +638,7 @@ export function reconcileHolds(controlDir, input) {
|
|
|
549
638
|
}
|
|
550
639
|
}
|
|
551
640
|
else if (noReplaceRestore(staging, plane) === "stranded") {
|
|
552
|
-
discloseStranded(row.holdId, row.
|
|
641
|
+
discloseStranded(row.holdId, row.rel, stagingName);
|
|
553
642
|
}
|
|
554
643
|
}
|
|
555
644
|
catch {
|
|
@@ -1186,6 +1186,10 @@ export declare class MemoryEngine {
|
|
|
1186
1186
|
* eventId anchors on the pollution record (sessionId + marked-at), so however many times this
|
|
1187
1187
|
* settlement replays the same pollution event, the generation is allocated once. */
|
|
1188
1188
|
private settlePromotions;
|
|
1189
|
+
/** The layout-adoption seat of a mount: a backend pinning its own directory root adopts under its
|
|
1190
|
+
* transaction lock; a copy-out projection tree and a backend without the face are adopted by the
|
|
1191
|
+
* engine directly (registry lock only — nothing else writes those trees during a mount). */
|
|
1192
|
+
private adoptLayoutBeforeMount;
|
|
1189
1193
|
materialize(scopes: readonly string[], writeScope: string | null, opts?: {
|
|
1190
1194
|
adoptionRestricted?: boolean;
|
|
1191
1195
|
sessionId?: string;
|
|
@@ -1242,6 +1246,9 @@ export declare class MemoryEngine {
|
|
|
1242
1246
|
* Throws only on a corrupt hold/settlement ledger — the caller's fail-closed reconciliation arm
|
|
1243
1247
|
* refuses the harvest (integrity unknown must not read as "no holds").
|
|
1244
1248
|
*/
|
|
1249
|
+
/** A hold's plane seat as a memory-dir-relative path (the report/warning spelling every other
|
|
1250
|
+
* harvest path uses), derived from the ledger-form row. */
|
|
1251
|
+
private holdPlaneRel;
|
|
1245
1252
|
private settleDueHolds;
|
|
1246
1253
|
/**
|
|
1247
1254
|
* The release leg: re-walk the captured bytes through the FULL gate set and commit them under
|
|
@@ -1663,8 +1670,6 @@ export declare class MemoryEngine {
|
|
|
1663
1670
|
* @returns the rejection to report, or `undefined` when the index is clean/absent.
|
|
1664
1671
|
*/
|
|
1665
1672
|
private gateDerivedIndex;
|
|
1666
|
-
/** Names of read-only sibling scope dirs directly under the writable root (excluded from scans; public since #691 — the runner refuses to admit a writable root that contains another scope's directory to the file tools' allowlist). */
|
|
1667
|
-
readonlyDirNamesUnderRoot(handle: MemorySessionHandle): Set<string>;
|
|
1668
1673
|
/** Projection write: reads the target to skip a no-op, then writes — the same check-then-use shape
|
|
1669
1674
|
* as the containment restores, so it takes the same no-follow write. A link at a projection path
|
|
1670
1675
|
* is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
|
|
@@ -1699,12 +1704,8 @@ export declare class MemoryEngine {
|
|
|
1699
1704
|
* `getByIds` there would sync-adopt mid-harvest.
|
|
1700
1705
|
*/
|
|
1701
1706
|
private debtCommittedProjection;
|
|
1702
|
-
/**
|
|
1703
|
-
*
|
|
1704
|
-
private siblingScopeDirNames;
|
|
1705
|
-
/** chmod a scope's entry tree (best-effort; skipRoot leaves the top dir writable — used when a
|
|
1706
|
-
* read-only layer's dir IS the shared root / an ancestor of the writable root, which must stay
|
|
1707
|
-
* traversable+writable; excludeTopDirs skips sibling scope homes under a root-owning layer).
|
|
1707
|
+
/** chmod a scope's entry tree (best-effort). The tree is the scope's own home and nothing else —
|
|
1708
|
+
* no other scope's directory can lie under it.
|
|
1708
1709
|
*
|
|
1709
1710
|
* `opts.announced` is the CALLER's per-materialize ledger of paths already disclosed (#495 M-1).
|
|
1710
1711
|
* One materialize sweeps each inherited tree TWICE — restore write perms at the top, re-apply
|