mirror of
https://github.com/razzant/ouroboros.git
synced 2026-11-04 14:51:43 +00:00
docs: C6 round 5.4 — the residual list disposed, the syscall-window truth, the recycled-pid mechanism, the ledger section
DESIGN §8: the last-proof->rename residual stated as the erased-then-quarantined loss it is on POSIX (silent on Windows), the recycled-pid disclosure rewritten to the real mechanism (EPERM read dead until now) and the real wedge with its 90 s window named, the POSIX-only release-under-flock and the contention-branch orphan disclosed; §10/§12: 'is skipped' qualified to entries that open, every reader path inspection typed, the stamp-less anchor's reset and stray-file consequences and the lock-free readers' transient UNKNOWN disclosed. PERSISTENCE: the lock row's mechanism, the ledger and archive rows' Reset columns (reset both together), the quarantine file's new source. ARCHITECTURE row: the same corrections in one line. Packet: §1/§3/§5.8/§5.9/§5.10 and the round-5 and 5.3 rows carry correction markers; a 'Round 5.4' section with the per-item disposition, the red-first table and the disclosed-not-fixed list. Ledger: the round's section with SUPERSEDES notes for the three lines it corrects and the gate evidence.
This commit is contained in:
parent
02338c9b98
commit
b4938c313d
5 changed files with 408 additions and 54 deletions
File diff suppressed because one or more lines are too long
|
|
@ -56,7 +56,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
|
|||
| Path | Writer | Format | schema_version | Retention | Reset |
|
||||
|---|---|---|---|---|---|
|
||||
| `settings.json` | `ouroboros/config.py` save_settings (lock `settings.json.lock`, integrity guard) | JSON env-key map | none — not needed: migration is per-key inside `load_settings` (legacy keys migrate on read); external sha256 pin `OUROBOROS_SETTINGS_SHA256` makes it immutable when set | fixed-size overwrite | recreated from defaults; ALL owner secrets/modes lost — never delete casually |
|
||||
| `settings.json.lock`, `*.lock` sidecars, `locks/**` | `ouroboros/platform_layer.py` lock family (+ `supervisor/state.py`, `supervisor/update_merge.py`, `ouroboros/skill_lifecycle_queue.py`) | O_EXCL lockfiles (unlinked on release) or flock files (persistent); plus one transient kernel-lock capability probe file per lock directory (`.kernel-lock-probe.<pid>.<ns>`, created, locked and unlinked once per process by `kernel_file_locks_enforced`) | none — not needed | self-healing (mtime/pid staleness) — except a lock whose owner died and whose pid was REUSED: it reads as alive (POSIX `kill(0)` answers EPERM for another user's process on a shared host), is never reclaimed by age, and needs a hand repair | zero durable state; deleting while stopped is a no-op |
|
||||
| `settings.json.lock`, `*.lock` sidecars, `locks/**` | `ouroboros/platform_layer.py` lock family (+ `supervisor/state.py`, `supervisor/update_merge.py`, `ouroboros/skill_lifecycle_queue.py`) | O_EXCL lockfiles (unlinked on release) or flock files (persistent); plus one transient kernel-lock capability probe file per lock directory (`.kernel-lock-probe.<pid>.<ns>`, created, locked and unlinked once per process by `kernel_file_locks_enforced`) | none — not needed | self-healing (mtime/pid staleness) — except a lock whose owner died and whose pid was REUSED by a live process: it reads as alive whoever owns the pid (`kill(0)` succeeds for a same-uid impostor and answers EPERM for another user's — both alive since round 5.4), is never reclaimed by age while the impostor lives, and needs a hand repair meanwhile | zero durable state; deleting while stopped is a no-op |
|
||||
|
||||
## 2. `state/` — singletons (fixed-size overwrite)
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
|
|||
|
||||
| Path | Writer | schema/record marker | Retention | Reset |
|
||||
|---|---|---|---|---|
|
||||
| `state/usage_attempts.jsonl` (+ `.quarantine.jsonl`, lock) | `ouroboros/usage_ledger.py` single chokepoint (own O_APPEND+fsync under named lock — NOT append_jsonl); `ouroboros/usage_compaction.py` rewrites it whole (verified candidate, atomic swap) under the same lock | no `_schema_version`; its own validated contract: dense `seq`, `kind` discriminator, `state` machine, per-row `candidate_measurement_kind`, attribution `physical_attempt_v1`; compacted files lead with a stamped `usage_baseline` header + `usage_baseline_group` rows — accepted | bounded by CPL4-C6 compaction (config `USAGE_LEDGER_COMPACT_BYTES`, 8 MB): terminal non-review attempt chains fold into the baseline block, raw segment archived first (fsync'd) — see docs/v7next/DESIGN_USAGE_COMPACTION.md; 20 MB WARN = compaction broken, unfoldable residue, or the name-tier refusal (typed `usage_ledger_compaction_refused` event, once per process); torn tails quarantined, never GC'd | monetary history destroyed, `seq` restarts, budget fences read $0; watermark survives so legacy import will NOT re-run — deleting the ledger alone is unrecoverable |
|
||||
| `state/usage_attempts.jsonl` (+ `.quarantine.jsonl`, lock) | `ouroboros/usage_ledger.py` single chokepoint (own O_APPEND+fsync under named lock — NOT append_jsonl); `ouroboros/usage_compaction.py` rewrites it whole (verified candidate, atomic swap) under the same lock | no `_schema_version`; its own validated contract: dense `seq`, `kind` discriminator, `state` machine, per-row `candidate_measurement_kind`, attribution `physical_attempt_v1`; compacted files lead with a stamped `usage_baseline` header + `usage_baseline_group` rows — accepted | bounded by CPL4-C6 compaction (config `USAGE_LEDGER_COMPACT_BYTES`, 8 MB): terminal non-review attempt chains fold into the baseline block, raw segment archived first (fsync'd) — see docs/v7next/DESIGN_USAGE_COMPACTION.md; 20 MB WARN = compaction broken, unfoldable residue, or the name-tier refusal (typed `usage_ledger_compaction_refused` event, once per process); torn tails — and a charge a compaction swap erased inside its last syscall, read back from the old inode (POSIX, round 5.4) — quarantined, never GC'd | monetary history destroyed, `seq` restarts, budget fences read $0; watermark survives so legacy import will NOT re-run — deleting the ledger alone is unrecoverable; and a ledger reset while `archive/usage_ledger/` survives leaves every history question a permanent `generation newer` corruption verdict (the stamp-less anchor) — move/delete the archive with it, or keep both |
|
||||
| `state/process_ledger.jsonl` | `ouroboros/process_custody.py` (spawn chokepoint) | none — downgrade-safe field split (`start_time`/`start_time_boot`) is the versioning device — accepted | self-compacting: reapers rewrite survivors-only | prior-generation supervised processes permanently orphaned (fingerprint index lost) |
|
||||
| `state/evolution_checkpoints.jsonl` | `ouroboros/evolution_checkpoints.py` | `schema_version: 1` on every row (+`kind` on outcome rows) | unbounded append; read bounded (last 200) — accepted (structured solve-capability history is the product) | absorbed/abandoned objectives can be re-proposed (BUG3 regression) |
|
||||
| `state/consciousness_observations.jsonl` | `ouroboros/consciousness.py` (raw locked append under the shared sidecar-lock seam) | rows typed by the observation contract; no version key — accepted (bounded truthful replay reads the tail) | acknowledged rows past GC retention fold into `archive/consciousness_observations_*.jsonl` at startup (CPL4-C23; unACKed rows NEVER pruned; any malformed line or ghost ack skips the fold; archive written before the live rewrite); render bounded (last 10 / 12 K chars) | Background Consciousness observation inbox lost; replay starts empty |
|
||||
|
|
@ -156,7 +156,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
|
|||
| `services/<task>/*.log` | `ouroboros/workspace_executor.py`, `tools/services.py` | none — raw text; archived content becomes observability blob with events.jsonl receipt | GC-retention prune at startup (archive-then-unlink); per-task terminal archive; oversize logs retained live | live tails lost; archived blobs survive |
|
||||
| `projects/<id>/**` (knowledge, journal, workpad, reflections, `.project.json` marker) | `ouroboros/project_facts.py`, `projects_registry.py` | marker unversioned; registry stamped (§2) | NEVER age-pruned (owner curates; delete = durable tombstone) | per-project memory lost; registry row survives, room reappears empty |
|
||||
| `archive/**` (rotated segments, `rescue/`, `usage_import/`, `managed_repo/`) | rotation + `supervisor/git_ops_rescue.py`, `usage_legacy_import.py`, `launcher_bootstrap.py` | segments inherit source shape; usage_import carries sha256 sidecar | UNBOUNDED BY DESIGN — durable history, never GC'd (P1) — accepted | memory horizon truncated; rescue copies of uncommitted work destroyed |
|
||||
| `archive/usage_ledger/segment_*.jsonl` | `ouroboros/usage_compaction.py` (CPL4-C6: exact pre-compaction ledger bytes, written + fsync'd BEFORE the live swap) | each segment is a whole valid ledger generation; hash-pinned by the live `usage_baseline` header (`source_sha256`), chained recursively through each segment's own leading header | UNBOUNDED BY DESIGN — the folded monetary history, never GC'd (P1); read via `archived_attempt_ids` (tamper-evident, per-attempt joins for the CPL-5 reverse sweep) | folded per-attempt monetary history unrecoverable; live aggregates (baseline block) survive, but seal/attempt joins for folded ids break |
|
||||
| `archive/usage_ledger/segment_*.jsonl` | `ouroboros/usage_compaction.py` (CPL4-C6: exact pre-compaction ledger bytes, written + fsync'd BEFORE the live swap) | each segment is a whole valid ledger generation; hash-pinned by the live `usage_baseline` header (`source_sha256`), chained recursively through each segment's own leading header | UNBOUNDED BY DESIGN — the folded monetary history, never GC'd (P1); read via `archived_attempt_ids` (tamper-evident, per-attempt joins for the CPL-5 reverse sweep) | folded per-attempt monetary history unrecoverable; live aggregates (baseline block) survive, but seal/attempt joins for folded ids break — the mirror of the ledger row: deleting the ARCHIVE alone under a stamped ledger is a typed chain break on every history question, deleting the LEDGER alone beside a surviving archive a permanent `generation newer` verdict; reset both together |
|
||||
| `observability/{calls,blobs,salvaged}/**` | `ouroboros/observability.py` (private 0700/0600, CAS gzip) | call manifests `schema_version: 1` + custody/redaction honesty markers; blob refs sha-verified on read | preserved indefinitely BY CONTRACT (the startup census counts, never deletes); the inert `OUROBOROS_OBSERVABILITY_RETENTION_DAYS` knob is RETIRED (CPL4-C22, owner 7A; key in `RETIRED_SETTING_KEYS`) | every recorded `result_ref`/`manifest_ref` dangles (strict readers raise); salvaged outputs unrecoverable |
|
||||
| `claudexor/**` | EXTERNAL writer — the claudexord daemon (Ouroboros only mkdirs, appends `daemon.log`, writes `ouroboros-owned.json` marker) | marker unversioned | daemon-owned; grows unbounded under our root — disclosed external plane | owner harness logins/profiles lost (fresh device-auth required) |
|
||||
| `playwright-browsers/` | `ouroboros/tools/browser.py` (vendor install) | none — vendor tree | no GC — accepted (vendor cache) | re-downloaded on next browser use |
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ absolutes are bounded per tier.
|
|||
| surface | change | why |
|
||||
|---|---|---|
|
||||
| `docs/v7next/DESIGN_USAGE_COMPACTION.md` | NEW — the ratified contract | invariants, fold scope, decimal rule, seq policy, crash order, trigger, CPL-5 join |
|
||||
| `ouroboros/usage_compaction.py` | NEW leaf (D16, ~490 lines; round 5.2: `owned_and_intact` beats on both sides of its look, the typed durable `usage_ledger_compaction_refused` event, non-regular archive entries classified — `O_NONBLOCK` open, `S_ISREG` — and typed `_load_segment` failures; round 5.3: the heartbeat REQUIRED by both entry points, the orphan exemption a byte-prefix proof read from the classified descriptor, the anchor running on a stamp-less live file, the root handle typed, the name-tier mark following the landed row, the path-shape scan classifying before the open, the chain-union cache bounded — 1197 lines) | fold policy + prove-then-swap + archive + history readers; imports FROM `usage_ledger`/`_usage_rows`, called INTO by `usage_accounting` — the one-way substrate seam is unchanged |
|
||||
| `ouroboros/usage_compaction.py` | NEW leaf (D16, ~490 lines; round 5.2: `owned_and_intact` beats on both sides of its look, the typed durable `usage_ledger_compaction_refused` event, non-regular archive entries classified — `O_NONBLOCK` open, `S_ISREG` — and typed `_load_segment` failures; round 5.3: the heartbeat REQUIRED by both entry points, the orphan exemption a byte-prefix proof read from the classified descriptor, the anchor running on a stamp-less live file, the root handle typed, the name-tier mark following the landed row, the path-shape scan classifying before the open, the chain-union cache bounded — 1197 lines; round 5.4: the refusal mark following a True append only, every reader path inspection typed and the stamp-less check exact on ENOENT, the swap's old-inode witness quarantining an erased charge and raising instead of receipting, `_build_candidate`'s beat required — 1245 lines) | fold policy + prove-then-swap + archive + history readers; imports FROM `usage_ledger`/`_usage_rows`, called INTO by `usage_accounting` — the one-way substrate seam is unchanged |
|
||||
| `ouroboros/usage_ledger.py` | `_validate_records` learns the two baseline kinds; round 5: `LOCK_REL` (lock-path SSOT) and the atomic writer routing its precondition through `replace_atomic` | head-only baseline block, exactly one header at seq 1, group rows joined by `baseline_id` + positive `folded_attempt_count`; a baseline row in an appended tail or after any non-baseline row = corrupt. Everything else (locking, append arithmetic, quarantine, resume fingerprints) is untouched |
|
||||
| `ouroboros/_usage_rows.py` | `_summary` + `_physical_call_count` baseline-aware | header skipped; group rows: count axes × `folded_attempt_count`, sums added once. Weight-1 paths are byte-equivalent to the previous code (pure refactor for existing kinds) |
|
||||
| `ouroboros/usage_accounting.py` | +5 lines in `reserve_attempt` | the opportunistic trigger under the already-held monetary lock; contained (never raises into the reservation) |
|
||||
|
|
@ -41,9 +41,9 @@ absolutes are bounded per tier.
|
|||
| `ouroboros/domains.toml`, `docs/DOMAIN_MAP.md` | new module seated D16; graph regenerated via `check_domains.py --write` | manifest completeness gate |
|
||||
| `docs/PERSISTENCE.md` | usage-ledger row rewritten (bounded by compaction); NEW `archive/usage_ledger/segment_*.jsonl` row | inventory truth; scan pin 123→124 in `tests/test_persistence_inventory.py` |
|
||||
| `tests/test_gateway_abi3_removals.py` | one per-site allowlist row | `_build_candidate` writes the internal ledger-plane `cost_usd` key (same class as every existing ledger writer row there) |
|
||||
| `tests/test_usage_compaction.py` | NEW pin suite (61 test items after round 5.3; the suite entered the 1001-1500 size band with a recorded rationale — it stood at 1512 lines for one commit, `79a1b9fb`, with the manifest stale, and the round-5.2 fold `6ad110e9` returned it inside the band without a new abstraction; 1492 after round 5.2 — and LEFT the band in round 5.3 at `e08a0392`: 1597 at the tip, in the ungated 1501-1600 zone, the manifest regenerated with it; the owner decision is stated in §9 "Round 5.3") | see §3, §6, §7, §8 and §9 |
|
||||
| `tests/test_lockfile_helpers.py` | +5 lock-ownership pins in round 3, +2 in round 4, +5 in round 5 (one Windows-only), +2 in round 5.2 (the lock-less creator; LockFileEx classification), +2 in round 5.3 (an unreadable own identity is never a hold; the design note's refusal sets) with the classification pin extended to the unsupported set | the finding-1 fixes are platform primitives, so they are pinned where those primitives live |
|
||||
| `ouroboros/platform_layer.py` | lock ownership: `_lock_identity`, inode-guarded stale eviction and release, ownership-reporting heartbeat; round 5: `kernel_file_locks_enforced` capability predicate (enforced vs name tier), fail-closed kernel refusal, LockFileEx error classification; round 5.2: the owner pid written BEFORE the kernel lock and a won lock returned only while the path still names it (an evicted creator re-contends), `_win32_lock_error` classifying ERROR_LOCK_VIOLATION alone as busy; round 5.3: a descriptor whose own identity cannot be read is never a hold (its live-pid stamp removed with it), `_WIN32_LOCK_ERRNOS` mapping ERROR_INVALID_FUNCTION / ERROR_NOT_SUPPORTED onto the unsupported set, both kill-tree sweeps on `force_kill_pid` (exactly 1500 lines) | round 3, finding 1; round 5, finding 1; round 5.2, findings 1 and W; round 5.3, findings 1 and L5 |
|
||||
| `tests/test_usage_compaction.py` | NEW pin suite (61 test items after round 5.3; the suite entered the 1001-1500 size band with a recorded rationale — it stood at 1512 lines for one commit, `79a1b9fb`, with the manifest stale, and the round-5.2 fold `6ad110e9` returned it inside the band without a new abstraction; 1492 after round 5.2 — and LEFT the band in round 5.3 at `e08a0392`: 1597 at the tip, in the ungated 1501-1600 zone, the manifest regenerated with it; the owner decision is stated in §9 "Round 5.3"; round 5.4: 66 items, back at 1597 after five new/extended pins — paid for by a `compacted` fixture folding twenty-one seed-then-compact preambles and by argument-list/data-literal reflows, no claim dropped) | see §3, §6, §7, §8 and §9 |
|
||||
| `tests/test_lockfile_helpers.py` | +5 lock-ownership pins in round 3, +2 in round 4, +5 in round 5 (one Windows-only), +2 in round 5.2 (the lock-less creator; LockFileEx classification), +2 in round 5.3 (an unreadable own identity is never a hold; the design note's refusal sets) with the classification pin extended to the unsupported set, +3 in round 5.4 (a pid answering EPERM is alive and its aged lock is not reclaimed; ENOLCK keeps the enforced tier and the acquisition fails closed; two threads racing the first probe run one probe) with the probe pin's name-tier selector moved to EOPNOTSUPP and an ENOLCK clause, and the identity pin extended with the heartbeat's refresh clauses | the finding-1 fixes are platform primitives, so they are pinned where those primitives live |
|
||||
| `ouroboros/platform_layer.py` | lock ownership: `_lock_identity`, inode-guarded stale eviction and release, ownership-reporting heartbeat; round 5: `kernel_file_locks_enforced` capability predicate (enforced vs name tier), fail-closed kernel refusal, LockFileEx error classification; round 5.2: the owner pid written BEFORE the kernel lock and a won lock returned only while the path still names it (an evicted creator re-contends), `_win32_lock_error` classifying ERROR_LOCK_VIOLATION alone as busy; round 5.3: a descriptor whose own identity cannot be read is never a hold (its live-pid stamp removed with it), `_WIN32_LOCK_ERRNOS` mapping ERROR_INVALID_FUNCTION / ERROR_NOT_SUPPORTED onto the unsupported set, both kill-tree sweeps on `force_kill_pid` (exactly 1500 lines); round 5.4: ENOLCK out of the unsupported set (fails closed), winerror 1 onto ENOSYS, the tier cache decided once under `_KERNEL_LOCK_TIER_LOCK`, `pid_is_alive` reading EPERM as alive with `pid_provably_gone` its one-line negation (1497 lines) | round 3, finding 1; round 5, finding 1; round 5.2, findings 1 and W; round 5.3, findings 1 and L5; round 5.4, R1 and R7 |
|
||||
| `ouroboros/utils.py` | `replace_atomic(precondition=)`: the precondition is asked before EVERY attempt, the Windows sharing-violation retries included; returns False without replacing when refused | round 5, finding 2 |
|
||||
| `ADOPTION_v7next.md` | CPL-4 row: C6 landed + verification hook | adoption gate |
|
||||
| `docs/v7next/LEDGER_CORRECTIONS.md` | append-only C6 lane section | provenance |
|
||||
|
|
@ -146,6 +146,17 @@ absolutes are bounded per tier.
|
|||
regular file`); the name-tier pin requires exactly one durable
|
||||
`usage_ledger_compaction_refused` row and the tripwire text naming the
|
||||
tier (§9, second block)
|
||||
- round 5.4 adds four in `tests/test_lockfile_helpers.py` and, in this
|
||||
suite, two new pins plus three strengthened ones: a stamp-less ledger still
|
||||
inspects its archive fail-closed (a regular file where the directory
|
||||
belongs, an uninspectable archive); a path inspection the reader cannot
|
||||
make is typed corruption (the not-searchable segment directory, an
|
||||
`is_symlink` the kernel refuses); the swap-lie pin gains the `erased`
|
||||
variant (a charge landed inside the rename syscall is quarantined, flags
|
||||
integrity, and the pass raises instead of receipting); the name-tier pin
|
||||
gains the append-returned-False shape; the reserve-path pin proves the
|
||||
heartbeat it is handed is THAT lock's (aged lock renewed), not a callable
|
||||
(§9 "Round 5.4")
|
||||
|
||||
Mutation-probed red (not just green-once): `_summary` weight math, group-sum
|
||||
rounding, folding of dispatched rows — each flips at least one pin.
|
||||
|
|
@ -225,9 +236,19 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
eviction is impossible under the held flock and the heartbeat-fresh
|
||||
mtime); such a robbery is caught at the next proof, and the irreducible
|
||||
residual is the interval between the final ownership proof and the rename
|
||||
syscall, in which a charge landed by that holder would be erased — then
|
||||
surfaced as `UsageLedgerCorrupt` by the post-swap re-read, or quarantined
|
||||
seq-misnumbered on the next read. In-protocol, no writer can append in the
|
||||
syscall, in which a charge landed by that holder IS erased by the rename.
|
||||
*(Correction, round 5.4: this packet, DESIGN §8/§12.9 and the round-5.2
|
||||
ledger line said the loss was "then surfaced by the post-swap re-read or
|
||||
quarantined seq-misnumbered on the next read" — neither could ever see it:
|
||||
the re-read compares the NEW inode against the candidate and the archive
|
||||
segment is the pre-row snapshot, so the loss was SILENT and a success
|
||||
receipt was returned. Now, on POSIX, the swap holds the old inode open
|
||||
across the rename and reads back what landed beyond the proven snapshot:
|
||||
those bytes are quarantined — `state/usage_attempts.quarantine.jsonl`,
|
||||
which flips `integrity_degraded` — and the pass raises typed instead of
|
||||
receipting; the charge is not re-appended. Windows cannot hold the
|
||||
destination open through `os.replace`: silent there, disclosed. §9 "Round
|
||||
5.4", R5.)* In-protocol, no writer can append in the
|
||||
compare→replace window, because every writer of this ledger takes the same
|
||||
owner-aware lock and has no unlocked fallback. On the name tier no such
|
||||
claim is made at all: the pass does not run there (§5.10). The round-5
|
||||
|
|
@ -240,9 +261,24 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
any generation rather than corruption, so it cannot deny service to the
|
||||
whole history; a directory or special file is not a segment (segments are
|
||||
regular files by construction) and is skipped — a FIFO is opened
|
||||
`O_NONBLOCK`, so it cannot hang the question either; an entry the scan
|
||||
`O_NONBLOCK`, so it cannot hang the question either *(qualified, round 5.4:
|
||||
an entry that OPENS but is not a regular file is skipped; one the kernel
|
||||
refuses to open at all — a UNIX socket, ENXIO — is corruption like any
|
||||
other unopenable entry)*; an entry the scan
|
||||
cannot list, open or read IS corruption — the scan did not complete, the
|
||||
data root's own handle included since round 5.3 — and the scan runs through
|
||||
data root's own handle included since round 5.3, and since round 5.4 every
|
||||
path inspection the reader makes (the symlink bounds on both archive levels
|
||||
and on the named segment; the stamp-less archive check, which ends the
|
||||
question early only on an exact ENOENT) is typed the same way instead of
|
||||
escaping as a bare `OSError` or a silent empty answer. Disclosed since
|
||||
round 5.4: with a stamp-less live file every parsable regular file in the
|
||||
archive that is not its byte-prefix is `generation newer`, so a ledger
|
||||
reset beside a surviving archive is a PERMANENT corruption verdict on every
|
||||
history question (reset both together) and an operator's stray JSON file
|
||||
there is corruption on a stamp-less ledger only; and the readers being
|
||||
lock-free, a compaction that commits between a question's live-header read
|
||||
and its anchor scan makes that ONE question UNKNOWN (`generation newer`),
|
||||
the next question walking the new chain — and the scan runs through
|
||||
the very handle the chain walk held, entries opened relative to it (and
|
||||
without a dir-fd the classification happens BEFORE the open, which is the
|
||||
step a directory refuses on Windows and a FIFO blocks on: round 5.3). The round-5 wording ("an entry the scan cannot list, open
|
||||
|
|
@ -264,11 +300,18 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
explicit in round 5): `platform_layer.kernel_file_locks_enforced` locks a
|
||||
scratch file in the lock directory once per process; only the kernel's
|
||||
own "this filesystem cannot" selects the name tier — exactly
|
||||
ENOLCK/EOPNOTSUPP/ENOTSUP/ENOSYS (until round 5.3 this packet and the
|
||||
design note spelled three of the four), and since round 5.3 the two
|
||||
Win32 answers of a volume without byte-range locks — ERROR_INVALID_FUNCTION,
|
||||
which LockFileEx answers on `\\wsl$` (microsoft/WSL#5762), and
|
||||
ERROR_NOT_SUPPORTED, error 50 on a Samba share — map onto the first two:
|
||||
EOPNOTSUPP/ENOTSUP/ENOSYS *(correction, round 5.4: ENOLCK was in the set
|
||||
until then — "no locks available" is a missing lock daemon OR an exhausted
|
||||
kernel lock table, not a capability answer, and it selected the tier where
|
||||
the round-3 race returns; it now keeps the enforced tier and every live
|
||||
acquisition the kernel refuses with it fails closed, so a lockd-less NFS
|
||||
refuses every monetary write with `UsageAccountingError` instead of running
|
||||
the name protocol; the per-directory verdict is decided ONCE under a module
|
||||
lock, so racing threads share one probe — §9 "Round 5.4", R1)*, and since
|
||||
round 5.3 the two Win32 answers of a volume without byte-range locks —
|
||||
ERROR_INVALID_FUNCTION, which LockFileEx answers on `\\wsl$`
|
||||
(microsoft/WSL#5762), and ERROR_NOT_SUPPORTED, error 50 on a Samba share —
|
||||
map onto ENOSYS and EOPNOTSUPP (onto ENOLCK and EOPNOTSUPP before round 5.4):
|
||||
CPython's winerror→errno table lands both on EINVAL, which left the name
|
||||
tier structurally unreachable on Windows, so a lock-less volume there
|
||||
failed EVERY monetary append closed instead of degrading to it.
|
||||
|
|
@ -279,8 +322,23 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
age — the deliberate trade of an availability incident (the wedged writer
|
||||
must die first) for the correctness incident (age-evicting a live
|
||||
monetary writer). Windows cannot unlink an open file: its eviction and
|
||||
release re-check the path after closing the probe, and a freshly won
|
||||
lock — held open by its owner — is undeletable there; Windows also has no
|
||||
release re-check the path after the close (release included — the POSIX
|
||||
"unlink under the still-held flock" is POSIX only, stated so since round
|
||||
5.4), and a freshly won lock — held open by its owner — is undeletable
|
||||
there. Disclosed since round 5.4, both tiers: a contention answer on the
|
||||
creator's OWN fresh file (a foreign flock holder that never unlinks it)
|
||||
leaves that live-pid-stamped file on the path — the creator re-contends
|
||||
against it until its timeout and owner-aware acquirers never age it out
|
||||
while the process lives; no in-protocol holder produces that shape.
|
||||
Also since round 5.4 the recycled-pid disclosure names its real
|
||||
mechanism: `pid_is_alive` read EPERM as DEAD, so another user's recycled
|
||||
pid was reclaimed through the age path (flock-guarded on the enforced
|
||||
tier) and only a same-uid recycle wedged — the opposite of what round 5.3
|
||||
wrote; EPERM now reads alive (the process exists), so ANY live impostor —
|
||||
same uid or another — wedges the lock from the 90 s staleness window
|
||||
(`usage_ledger._locked`, `stale_sec=90.0`) until it exits, the probe
|
||||
flock deliberately unconsulted while the pid reads alive (a mixed-tier
|
||||
name-tier holder has none); Windows also has no
|
||||
`dir_fd`/`O_DIRECTORY`, so its archive bound and anchor scan stay
|
||||
path-based (fail-closed on any OSError since round 5). *Name tier* —
|
||||
kernel-lockless filesystems only — keeps the O_EXCL name protocol with
|
||||
|
|
@ -474,7 +532,7 @@ mutation it names, on this base, before the fix landed.
|
|||
|
||||
| # | what round 4 left open | fix | red-first pin |
|
||||
|---|---|---|---|
|
||||
| 1 | HIGH — on ANY `OSError` from the kernel lock the acquisition silently degraded to the pathname/inode name tier, where the round-3 race returns (and on Windows the errno-less `LockFileEx` failure fell into the same degrade) | the tier is an explicit capability predicate, `platform_layer.kernel_file_locks_enforced(lock_path)`: one scratch-file kernel lock per lock directory per process; only ENOLCK/EOPNOTSUPP/ENOSYS select the name tier. On the enforced tier contention (EAGAIN/EACCES/EWOULDBLOCK) stands down and re-contends; every other refusal fails CLOSED — no descriptor, our own file removed, a stale lock never evicted without the held flock. `_win32_lock` raises an `OSError` carrying the Windows error so `ERROR_LOCK_VIOLATION` classifies as contention. The name tier makes no kernel call at all, and `compact_usage_ledger_locked` refuses it with the typed `NAME_TIER_REFUSAL` (logged; appends continue under the name protocol, disclosed). `usage_ledger.LOCK_REL` is the lock-path SSOT *(correction, round 5.3: the busy set is EAGAIN/EWOULDBLOCK alone — EACCES fails closed since round 5.2, finding W — and the unsupported set is ENOLCK/EOPNOTSUPP/ENOTSUP/ENOSYS plus the two Win32 codes round 5.3 maps onto it; §9 "Round 5.3", findings 2 and L5)* | `test_a_kernel_refusal_that_is_not_contention_fails_closed`, `test_a_stale_lock_is_never_evicted_without_the_kernel_hold`, `test_the_name_tier_is_chosen_by_the_predicate_not_by_a_refusal`, `test_the_capability_probe_decides_once_and_leaves_no_residue`, `test_windows_lockfileex_contention_reads_as_busy` (skipif not Windows) in `tests/test_lockfile_helpers.py`; `test_the_pass_refuses_on_the_name_tier_while_appends_continue` |
|
||||
| 1 | HIGH — on ANY `OSError` from the kernel lock the acquisition silently degraded to the pathname/inode name tier, where the round-3 race returns (and on Windows the errno-less `LockFileEx` failure fell into the same degrade) | the tier is an explicit capability predicate, `platform_layer.kernel_file_locks_enforced(lock_path)`: one scratch-file kernel lock per lock directory per process; only ENOLCK/EOPNOTSUPP/ENOSYS select the name tier. On the enforced tier contention (EAGAIN/EACCES/EWOULDBLOCK) stands down and re-contends; every other refusal fails CLOSED — no descriptor, our own file removed, a stale lock never evicted without the held flock. `_win32_lock` raises an `OSError` carrying the Windows error so `ERROR_LOCK_VIOLATION` classifies as contention. The name tier makes no kernel call at all, and `compact_usage_ledger_locked` refuses it with the typed `NAME_TIER_REFUSAL` (logged; appends continue under the name protocol, disclosed). `usage_ledger.LOCK_REL` is the lock-path SSOT *(correction, round 5.3: the busy set is EAGAIN/EWOULDBLOCK alone — EACCES fails closed since round 5.2, finding W — and the unsupported set is ENOLCK/EOPNOTSUPP/ENOTSUP/ENOSYS plus the two Win32 codes round 5.3 maps onto it; §9 "Round 5.3", findings 2 and L5; correction, round 5.4: ENOLCK left the set — it fails closed — so the set is EOPNOTSUPP/ENOTSUP/ENOSYS, with winerror 1 mapped onto ENOSYS; §9 "Round 5.4", R1)* | `test_a_kernel_refusal_that_is_not_contention_fails_closed`, `test_a_stale_lock_is_never_evicted_without_the_kernel_hold`, `test_the_name_tier_is_chosen_by_the_predicate_not_by_a_refusal`, `test_the_capability_probe_decides_once_and_leaves_no_residue`, `test_windows_lockfileex_contention_reads_as_busy` (skipif not Windows) in `tests/test_lockfile_helpers.py`; `test_the_pass_refuses_on_the_name_tier_while_appends_continue` |
|
||||
| 2 | HIGH — the ownership→snapshot precondition ran once before `utils.replace_atomic`, which retries `os.replace` up to ten times with pauses on a Windows sharing violation: a charge appended (or a hold lost) between attempts was erased by the retry that landed | `replace_atomic(src, dst, *, precondition=None)` asks the precondition immediately before EVERY attempt, retries included, and returns False without replacing when refused; `_write_bytes_atomic_fsync` routes its ownership-first, snapshot-second proof through it. POSIX behaviour is unchanged (one syscall) | `test_a_refused_rename_re_proves_the_hold_and_the_snapshot_before_retrying[append]` / `[hold_lost]` (first attempt raises `PermissionError`, the intrusion lands, the second call never happens, the row survives / the ledger is byte-identical) |
|
||||
| 3 | MEDIUM — `_no_newer_archived_epoch` walked the archive by pathname and turned `OSError` into "no evidence": a directory swapped after the safe chain walk could hide a newer generation and admit a forged rollback (the §5.10 claim was false) | `archived_attempt_ids` opens the `O_DIRECTORY\|O_NOFOLLOW` handle chain ONCE — after the live-header read, for the rest of the question *(round 5.2: a directory swapped before that open is the same power as deleting the newer segments, disclosed; a non-regular entry is skipped, not corruption)*; segment loads and the anchor scan open entries relative to that same held handle (one `_open_archive_entry` rule; path-based only where `dir_fd` is absent). An entry the scan cannot list, open or read is `UsageLedgerCorrupt`; a first row that reads but does not parse stays the disclosed torn-segment case | `test_the_epoch_anchor_scans_the_directory_the_chain_was_walked_in` (POSIX; a look-alike directory swapped in after the walk), `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (a dangling entry) |
|
||||
| 4 | LOW — two surviving mutations (deleting the first commit-section beat; losing the hold between rename retries) and three doc absolutes («cannot finish while robbed», «a hold lost anyway abandons», «kernel-held») stated without their tier | both pinned (the second by finding 2's `[hold_lost]` variant); DESIGN §8/§10/§12, ARCHITECTURE and this packet now state the contract per tier — enforced tier vs name tier | `test_a_hold_lost_before_the_first_commit_look_writes_no_orphan` |
|
||||
|
|
@ -622,7 +680,7 @@ round's own fixes (3b and 4b below).
|
|||
| 4b | LOW, own residue of 4 (resumed half) — the prefix proof re-opened the entry by NAME after classifying it, so the bytes compared against the live file came from a second open: an entry swapped in between (an empty file; a writer-less FIFO under `O_NONBLOCK`) read as zero bytes and passed the anchor although the segment claiming the newer generation had just been read — the same power as deleting that segment before the scan (disclosed), but one open more than the proof needs | one open per entry: classify, parse the leading row and — when it claims a newer generation — `lseek` to the start and compare from the same descriptor (six lines fewer) | `test_a_restored_previous_generation_is_out_anchored_not_taken_for_an_orphan` (a second open of any name answers an empty file; the verdict must still be reached), red on the two-open shape |
|
||||
| 6 | MEDIUM — `_archive_dir_fds` wrapped every open below the root and left the ROOT's own outside the `try`; `archived_attempt_ids` calls it with no handler, so an unreadable data root (permissions, EMFILE/ENFILE) left a bare `OSError` on the join surface — the class round 5.2 closed one function away | the root open is inside the `try` and typed `usage archive root is not readable` | `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (chmod `0o111` half) |
|
||||
| L1 | LOW — the name-tier root was marked BEFORE the append, so one transient failure (ENOSPC, an unwritable `logs/`) turned the durable typed event back into a log line for the process's life, and the 20 MB tripwire then names an event that does not exist; the key was the unresolved path while the sibling growth guard resolves (two spellings of one root on this workspace) | the mark follows the row that landed; both maps key on the resolved root | `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (failing append, then a landing one; then the same root through a symlink) |
|
||||
| L2 | LOW — a lock whose owner died and whose pid was REUSED is never reclaimed (POSIX `kill(0)` answers EPERM for another user's process on this shared host), although the enforced tier's probe flock would settle it; `PERSISTENCE.md` still called these locks self-healing | **disclosed, not changed** — the finding's own alternative. Taking the probe flock whenever the file is aged would also evict a LIVE holder of a mixed-tier install (the name-tier process holds no flock), trading a rare wedge for the two-writer class §5.10 already names; DESIGN §8, the ARCHITECTURE row and the PERSISTENCE row now state the wedge and the hand repair | — |
|
||||
| L2 | LOW — a lock whose owner died and whose pid was REUSED is never reclaimed (POSIX `kill(0)` answers EPERM for another user's process on this shared host), although the enforced tier's probe flock would settle it; `PERSISTENCE.md` still called these locks self-healing *(correction, round 5.4: the mechanism was misstated — `pid_is_alive` read EPERM as DEAD, so another user's recycle WAS reclaimed through the age path and only a same-uid recycle wedged; EPERM reads alive since round 5.4 and the disclosure names the real wedge — §9 "Round 5.4", R7)* | **disclosed, not changed** — the finding's own alternative. Taking the probe flock whenever the file is aged would also evict a LIVE holder of a mixed-tier install (the name-tier process holds no flock), trading a rare wedge for the two-writer class §5.10 already names; DESIGN §8, the ARCHITECTURE row and the PERSISTENCE row now state the wedge and the hand repair | — |
|
||||
| L3 | LOW — the swap's own crash durability was unpinned on both sides: deleting the candidate temp's `fsync` (MUT-H) or the ledger directory's `fsync` after the rename (MUT-L) left the battery green, while the archive half carries three pins | one pin records the fsync'd inodes and the moment of the replace | `test_the_swap_fsyncs_the_candidate_before_the_rename_and_its_directory_after` |
|
||||
| L4 | LOW — `_snapshot_intact` reduced to a size comparison (MUT-E) also left everything green: every intrusion the pins inject is an append | one intrusion rewrites a byte in place, changing no length | `test_a_same_size_rewrite_between_the_recheck_and_the_replace_also_refuses` |
|
||||
| L5 | LOW — no LockFileEx refusal could select the name tier on Windows (CPython lands ERROR_INVALID_FUNCTION and ERROR_NOT_SUPPORTED on EINVAL), so a lock-less Windows volume failed every monetary append closed instead of degrading as disclosed; the classification pin's POSIX half asserted through errnos the function does not set | `_win32_lock_error` classifies by one table — 33 busy, 1/50 unsupported, anything else winerror-derived and fail-closed — and the classified codes carry their own errno (the 4-argument form derives errno FROM the winerror on Windows and ignores the one passed). Live evidence for exactly those two codes: LockFileEx on `\\wsl$` answers ERROR_INVALID_FUNCTION ("Incorrect function", microsoft/WSL#5762) and on a Samba share ERROR_NOT_SUPPORTED (error 50, "The network request is not supported", samba list thread "FileLockEx Problem") | `test_lockfileex_refusals_classify_by_the_win32_error` (1/50 must land in the unsupported set; 5/32/6 in neither) |
|
||||
|
|
@ -695,3 +753,107 @@ DESIGN §8/§10/§12, the ARCHITECTURE row, PERSISTENCE, this section and the
|
|||
ledger, with the design-note pin — follows. Gate evidence:
|
||||
`docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 fix-round 5.3 (base
|
||||
`5e4829e3`)".
|
||||
|
||||
### Round 5.4 — owner-bounded micro-round (base `096437c2`, owner batch №12, answer A)
|
||||
|
||||
Scope fixed by the owner: the residual list left by the Fable lenses over
|
||||
round 5.3 and the independent gpt-5.6-sol read-only review — eight items, no
|
||||
new exploration, no redesign. **All eight disposed**: seven changed in code
|
||||
with red-first pins, one (R8 c–f) closed by the disclosures it asked for.
|
||||
Every behaviour fix on the monetary path was observed RED on the pre-fix
|
||||
shape before the fix landed (table below), then green with it.
|
||||
|
||||
| # | residual | disposition | fix | red-first pin |
|
||||
|---|---|---|---|---|
|
||||
| R1 | HIGH — `ENOLCK` sat in the unsupported set: "no locks available" is a missing lock daemon OR an exhausted kernel lock table, not the kernel saying this filesystem cannot, yet it selected the name tier — where the round-3 race returns; the per-directory tier cache was read and written with no synchronisation, so two first threads could run two probes and disagree | **fixed** | `_LOCK_UNSUPPORTED_ERRNOS` is exactly `EOPNOTSUPP`/`ENOTSUP`/`ENOSYS` (winerror 1 → `ENOSYS`, 50 → `EOPNOTSUPP`); ENOLCK keeps the enforced tier and a live acquisition the kernel refuses with it fails closed — no descriptor, our own file removed, no name protocol — so a lockd-less NFS refuses every monetary write typed (`UsageAccountingError`, the round-3 no-unlocked-fallback pin) and the pass is never entered; `_KERNEL_LOCK_TIER_LOCK` makes the probe single-flight: one probe, one verdict per directory per process. DESIGN §8, the ARCHITECTURE row and §5.10 spell the set (the design-note pin compares it by number) | `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause; name tier now selected by EOPNOTSUPP), `test_enolck_keeps_the_enforced_tier_and_the_acquisition_fails_closed`, `test_two_threads_racing_the_first_probe_run_one_probe_and_read_one_tier` |
|
||||
| R2 | MEDIUM — the root was marked "already told" whether or not `append_jsonl` landed the refusal row; the helper reports exhausted retries as `False`, not an exception, so one transient failure silenced the durable event for the process's life | **fixed** | the mark follows a `True` answer only; the failed append is logged by the helper and retried at the next refusal | `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (a False-returning append between the raising and the landing one) |
|
||||
| R3 | MEDIUM — the stamp-less fast path (`Path.is_dir()`) bypassed the typed root open: a regular file where the archive directory belongs answered a silent `frozenset()`, an uninspectable archive a bare `OSError` | **fixed** | before any compaction the question ends early only on the kernel's exact `ENOENT`; a non-directory or an uninspectable archive is `UsageLedgerCorrupt`; every case that reads anything then goes through `_archive_dir_fds`' typed root open as before (a plain `os.stat` classification first, because the Windows path shape has no dir-fd to route through) | `test_a_stamp_less_ledger_still_inspects_its_archive_fail_closed` |
|
||||
| R4 | MEDIUM — bare `OSError` still escaped `archived_attempt_ids` through `pathlib`'s `is_symlink()` in `_archive_dir_bounded` (both levels) and `_segment_path` (the named segment): `pathlib` re-raises everything but ENOENT/ENOTDIR/EBADF/ELOOP, and an `archive/usage_ledger` readable but not searchable (a `chmod -R 600 data/` hardening) refused the segment's own `lstat` | **fixed, as a class** | both inspections wrap their `OSError` into `UsageLedgerCorrupt` ("cannot be inspected"), the same rule the opens follow since round 5.3 | `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` (the real chmod-600 shape, then `Path.is_symlink` raising `PermissionError`) |
|
||||
| R5 | MEDIUM — DESIGN §8/§12.9, §5.8 here and the round-5.2 ledger line said a charge landed between the last proof and the rename is "erased, then surfaced by the post-swap re-read or quarantined at the next read"; neither could see it — the re-read compares the NEW inode against the candidate, the archive segment is the pre-row snapshot — so the loss was SILENT and a success receipt was returned | **fixed (POSIX) + docs corrected** | `_swap_ledger_fsync` holds the OLD inode open across the rename (the only witness left) and reads back whatever landed beyond the proven snapshot's length AFTER the fact: those bytes go to `state/usage_attempts.quarantine.jsonl` (`raw_base64`, the torn-tail shape, which flips `integrity_degraded`) and the pass raises `UsageLedgerCorrupt` instead of returning a receipt — never re-appended (`seq` belongs to the live file). Detected by size; Windows cannot hold the destination open through `os.replace` and stays a disclosed silent loss. The trigger's failure log no longer claims the ledger is uncompacted after such a raise | `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt[erased]` (the round-3 `[written_over]` variant unchanged) |
|
||||
| R6 | MEDIUM — the production heartbeat wire was pinned as `callable(...)`: a constant-True stub (M9) survived the whole battery, every ownership proof a no-op | **fixed (pin)** | the reserve-path pin ages the lock file to the epoch, calls the heartbeat it is handed and requires `True` AND a renewed mtime — judged outside the contained call, so the red names the stub | `test_reserve_path_compacts_only_past_config_threshold` |
|
||||
| R7 | MEDIUM — the recycled-pid disclosure named the wrong mechanism: `pid_is_alive` folded EPERM into "dead", so another user's recycled pid went down the age-eviction path (flock-guarded on the enforced tier) and only a same-uid recycle wedged — while DESIGN §8, PERSISTENCE and the round-5.3 L2 row said EPERM read alive | **fixed + disclosure corrected** | EPERM (the process EXISTS) reads alive; only ESRCH is dead; anything undeterminable reads present, as Windows already did — so `pid_provably_gone` is the exact negation of `pid_is_alive` and became one line (the reaper's docstring corrected with it). The real residual, stated in DESIGN §8, §5.10, ARCHITECTURE and PERSISTENCE: any live impostor — same uid or another — wedges the lock from the 90 s staleness window (`usage_ledger._locked`, `stale_sec=90.0`, a literal there, not a `config.py` constant) until it exits; the probe flock is deliberately not consulted while the pid reads alive (a mixed-tier name-tier holder has none) | `test_a_pid_that_refuses_our_signal_is_alive_and_its_lock_is_not_reclaimed` |
|
||||
| R8a | LOW — `_build_candidate` defaulted `beat` to a no-op inside the monetary path | **fixed** | `beat` is required; omitting it is a `TypeError` at the call, contained by the pass, which then never compacts | control: `test_the_long_build_and_verification_section_beats_the_lock` red under M10 |
|
||||
| R8b | LOW — the heartbeat's own failure modes were unpinned: the `not held or` guard (the heartbeat's analog of round 5.3 finding 1) and the `False` on a refused `utime` could each be removed with both suites green | **fixed (pin)** | the identity pin gains a refresh clause: a refused renewal answers False; an unreadable own identity with the path absent is not a match of two empty answers; a stranger's file at the path is not ours | `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (refresh clauses) |
|
||||
| R8c | LOW — a compaction committing between a question's live-header read and its anchor scan yields a transient false `generation newer` (UNKNOWN) | **disclosed** (DESIGN §10, §5.9, ARCHITECTURE) | the owner's scope for this item was disclosure; the bounded single retry the lens offered is not taken this round | — |
|
||||
| R8d | LOW — stamp-less anchor consequences undisclosed: a ledger reset beside a surviving archive is a permanent `generation newer` verdict; a stray JSON file is corruption on a stamp-less ledger only | **disclosed** (PERSISTENCE ledger and archive rows' Reset columns, DESIGN §10, §5.9, ARCHITECTURE) | reset both together, or keep both | — |
|
||||
| R8e | LOW — "an entry that is not a regular file … is skipped" was absolute; a UNIX socket cannot be opened at all (ENXIO) and reads as corruption | **qualified** (DESIGN §10/§12.5, §5.9, ARCHITECTURE) | an entry that OPENS but is not regular is skipped; one the kernel refuses to open at all is corruption. No socket pin: `AF_UNIX` paths are capped at 108 bytes and pytest's tmp paths exceed it, and a `chdir`-relative bind leaks process state into a parallel suite — a LOW not worth that hazard | — |
|
||||
| R8f | LOW — two absolutes: "a release unlinks before its close, under the still-held flock" (POSIX only: Windows closes, then re-checks and unlinks) and no mention of the contention-branch orphan (an `EAGAIN` on the creator's OWN fresh file leaves a live-pid-stamped file the creator re-contends against and no owner-aware acquirer ages out) | **disclosed** (DESIGN §8, §5.10, ARCHITECTURE) | the orphan shape has no in-protocol producer (an evictor's flock unlinks what it judged): theoretical on the enforced tier, stated | — |
|
||||
|
||||
Red observed, not argued — each pin against the exact pre-fix shape or
|
||||
mutation it names, in this worktree (pin red, fix applied or mutation
|
||||
reverted, pin green):
|
||||
|
||||
| pin | mutation / pre-fix shape | red observed |
|
||||
|---|---|---|
|
||||
| `test_a_pid_that_refuses_our_signal_is_alive_and_its_lock_is_not_reclaimed` | `platform_layer.py` @ `bd9e99a4` (EPERM folded into dead) | `assert (False is True)` on `pid_is_alive(EPERM)`; the lock clause alone: the aged lock evicted, fd 3 returned, the file re-stamped with our pid |
|
||||
| `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) | `platform_layer.py` @ `01c89685` (ENOLCK in the unsupported set) | `AssertionError: 37` — errno 37 selected the name tier (`False is True`) |
|
||||
| `test_enolck_keeps_the_enforced_tier_and_the_acquisition_fails_closed` | same | `enforced = False`; a descriptor (fd 3) returned on the name tier, the lock file present, no kernel call made |
|
||||
| `test_two_threads_racing_the_first_probe_run_one_probe_and_read_one_tier` | same (no cache lock) | `2 == 1`: both threads ran a probe |
|
||||
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (False-returning append) | `usage_compaction.py` @ `7ce7e83d` (mark regardless of the return value) | `FileNotFoundError: …/logs/events.jsonl` — the False answer marked the root, no row ever landed |
|
||||
| `test_a_stamp_less_ledger_still_inspects_its_archive_fail_closed` | `usage_compaction.py` @ `b9c43911` (`is_dir()` fast path) | regular file: `DID NOT RAISE`, answered `frozenset()`; `archive/` chmod 000: bare `PermissionError` |
|
||||
| `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` | same (bare `is_symlink()`) | `usage_ledger` chmod 600: bare `PermissionError: [Errno 13] … segment_ep0001_….jsonl` from the segment's own lstat; `Path.is_symlink` raising: bare `PermissionError` |
|
||||
| `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt[erased]` | `usage_compaction.py` @ `d99ff6a9` (no old-inode witness) | `DID NOT RAISE`; standalone: receipt returned, the charge gone from the ledger, no quarantine file, `integrity_degraded` False |
|
||||
| `test_reserve_path_compacts_only_past_config_threshold` (heartbeat clause) | M9: `heartbeat=lambda: True` at the production wire | `a stub, not the held lock's heartbeat` (`[False] == [True]`); the other 84 items of the two suites green under the same mutation |
|
||||
| `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (refresh clauses) | M15: the `not held or` guard removed from `refresh_exclusive_file_lock` | `assert True is False`: the blind descriptor renewed with the path absent |
|
||||
| same | M16: a refused `utime` answers True | `assert True is False` |
|
||||
| `test_the_long_build_and_verification_section_beats_the_lock` (control, R8a) | M10: `beat` dropped at the `_build_candidate` call | `TypeError: _build_candidate() missing 1 required positional argument: 'beat'`, contained by the pass → `assert None is not None` |
|
||||
|
||||
Disclosed, not fixed (with the reason each time):
|
||||
|
||||
1. **R1's consequence.** An install whose `state/` answers ENOLCK
|
||||
persistently (bare NFS without lockd) now refuses every monetary write
|
||||
closed — `UsageAccountingError` at the writer, one warning per attempt
|
||||
naming errno 37 — where round 5 ran the name protocol there. That is the
|
||||
owner's decision (fail closed, no name tier); the repair is a filesystem
|
||||
that locks. "Compaction refuses with a typed reason" is structural, not a
|
||||
new code path: with no lock the pass is never entered, and the round-3 pin
|
||||
`test_every_ledger_writer_refuses_when_the_lock_cannot_be_taken` is the
|
||||
typed refusal. sol's further suggestion — binding the established tier to
|
||||
the returned hold and passing that attestation to the compactor — is not
|
||||
taken: the module lock leaves one verdict per directory per process, which
|
||||
is the disagreement the attestation would have caught; a mixed-tier
|
||||
install ACROSS processes stays the round-5.2 disclosure.
|
||||
2. **R2 and fsync.** No `events.jsonl` row is fsync'd, this one included;
|
||||
the mark is per-process memory that dies with the same crash that could
|
||||
lose an un-fsync'd row, so a new process re-tells. The residual — a
|
||||
delayed writeback error with the process alive (row lost, mark standing)
|
||||
— is the same for every event row and is not closed here.
|
||||
3. **R5's bounds.** Detection is by size: a same-size in-place rewrite of
|
||||
the old inode inside the rename syscall is not a landed charge and is
|
||||
not seen. The erased bytes are preserved and flagged, never re-appended
|
||||
(a hand repair from the quarantine row). Windows: silent, disclosed.
|
||||
4. **R7's trade.** The wedge now covers another-uid recycles too (they
|
||||
were reclaimed through the flock-guarded age path before); the
|
||||
alternative — the probe flock on any aged file — would evict a live
|
||||
name-tier holder of a mixed-tier install (round 5.3 L2 stands).
|
||||
5. **R8c–f** are disclosures by the owner's scope: the transient UNKNOWN
|
||||
(no retry added), the reset-beside-archive verdict, the socket shape (no
|
||||
pin: the 108-byte `AF_UNIX` cap and a `chdir` hazard), the contention
|
||||
orphan and the POSIX-only release-under-flock.
|
||||
6. **Windows** stays unexecuted on this host, as in every round.
|
||||
|
||||
Size ratchet, stated plainly: `ouroboros/platform_layer.py` 1500 → 1497
|
||||
inside its band — `pid_provably_gone` folded to the one-line negation it now
|
||||
is and the docstrings it gained reflowed, paying for `threading`, the tier
|
||||
lock and the EPERM branch. `ouroboros/usage_compaction.py` 1197 → 1245
|
||||
inside its band; the owner asked for the band rationale to be extended in the
|
||||
same commit when the file grows, but the ratchet's own transition rule makes
|
||||
a surviving rationale immutable between adjacent manifests
|
||||
(`validate_manifest_transition`, "surviving band rationale is immutable"),
|
||||
so — as in rounds 5, 5.2 and 5.3 — the growth is recorded here and in the
|
||||
ledger instead. `tests/test_usage_compaction.py` 1597 → 1597: the round
|
||||
added five new or extended pins (+67 lines) and paid with two no-behaviour
|
||||
commits — a `compacted` fixture folding twenty-one verbatim seed-then-compact
|
||||
preambles (−39) and argument-list/data-literal reflows within the file's line
|
||||
width (−28); no claim, docstring, message or assertion was dropped, and no
|
||||
neighbour suite was added. The three-line headroom under the 1600 hard cap is
|
||||
what it was; the owner decision owed since round 5.3 still stands.
|
||||
|
||||
Round-5.4 commits (author and committer `Ouroboros`, single-intent):
|
||||
`bd9e99a4` (the fixture fold, no behaviour change), `01c89685` (R7),
|
||||
`7ce7e83d` (R1), `12558046` (R2), `b9c43911` (the reflow, no behaviour
|
||||
change), `d99ff6a9` (R3 + R4), `ea4d4337` (R5), `9306f962` (R6),
|
||||
`02338c9b` (R8a + R8b); the docs commit — DESIGN §8/§10/§12, the
|
||||
ARCHITECTURE row, PERSISTENCE, this section and the ledger — follows. Gate
|
||||
evidence: `docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 micro-round 5.4
|
||||
(owner batch №12 A, base 096437c2)".
|
||||
|
|
|
|||
|
|
@ -248,13 +248,21 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
|
|||
pass before the answer is even asked. The lock primitives are
|
||||
ownership-exact and kernel-guarded: a stale eviction unlinks only while
|
||||
HOLDING the flock on the very fd it judged abandoned (path re-checked
|
||||
against it), so of two racing reclaimers at most one can evict; a release
|
||||
unlinks before its close, under the still-held flock; the heartbeat answers
|
||||
OWNERSHIP rather than success, including for a lock file replaced
|
||||
atomically (the path never absent). Windows cannot unlink an open file: its
|
||||
eviction re-checks the path after closing the probe, and a freshly won
|
||||
lock — held open by its owner — is undeletable there, which is what keeps
|
||||
that shape exclusive. The acquisition is identity-checked too: a creator
|
||||
against it), so of two racing reclaimers at most one can evict; on POSIX a
|
||||
release unlinks before its close, under the still-held flock; the heartbeat
|
||||
answers OWNERSHIP rather than success, including for a lock file replaced
|
||||
atomically (the path never absent), and answers `False` — never a renewal —
|
||||
when its own descriptor's identity cannot be read or the kernel refuses the
|
||||
`utime`. Windows cannot unlink an open file: its eviction re-checks the
|
||||
path after closing the probe AND its release re-checks after its close, a
|
||||
freshly won lock — held open by its owner — being undeletable there, which
|
||||
is what keeps that shape exclusive. Disclosed, both tiers: a contention
|
||||
answer (`EAGAIN`) on the creator's OWN fresh file — a foreign flock holder
|
||||
that never unlinks it — leaves that file on the path, stamped with the
|
||||
creator's live pid; the creator re-contends against it until its timeout,
|
||||
and owner-aware acquirers never age it out while this process lives. No
|
||||
in-protocol holder produces that shape (an evictor's flock unlinks what it
|
||||
judged), so it is theoretical on the enforced tier. The acquisition is identity-checked too: a creator
|
||||
stalled between its O_EXCL create and its kernel lock (SIGSTOP, a suspend,
|
||||
a debugger, clock skew) can be judged abandoned and evicted, and its lock
|
||||
then lands on an inode the path no longer names — not a hold: a won lock is
|
||||
|
|
@ -270,11 +278,22 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
|
|||
inode. It fails closed, and the file we stamped with our LIVE pid is removed
|
||||
with it when its bytes are still exactly the ones we wrote — left behind, no
|
||||
owner-aware reclaimer could ever evict it and the lock wedges for good.
|
||||
**Residual, disclosed:** the owner-aware rule reads a RECYCLED pid as alive
|
||||
(POSIX `kill(0)` answers EPERM for another user's process), so a lock whose
|
||||
owner died and whose pid was reused is never reclaimed by age, even though
|
||||
the enforced tier's probe flock would settle it; `state/usage_attempts.lock`
|
||||
then needs a hand repair (`docs/PERSISTENCE.md` says so in its row).
|
||||
**Residual, disclosed (mechanism corrected in round 5.4):** the owner-aware
|
||||
rule asks `pid_is_alive(owner_pid)`, and a RECYCLED pid — one a live
|
||||
process now owns — reads as alive whoever owns it: `kill(0)` succeeds for
|
||||
a same-uid impostor and answers EPERM for another user's, which round 5.4
|
||||
made "alive" too (it read as "dead" before, so another user's recycle was
|
||||
reclaimed through the age path — the probe flock guarding it on the
|
||||
enforced tier — while only a same-uid recycle wedged; this note named the
|
||||
opposite mechanism). So a lock whose owner died and whose pid was reused
|
||||
is never reclaimed by age while the impostor lives: the wedge begins once
|
||||
the dead owner's file is older than the 90 s staleness window
|
||||
(`usage_ledger._locked`, `stale_sec=90.0` — a literal there, not a
|
||||
`config.py` constant) and ends when the impostor exits, even though the
|
||||
enforced tier's probe flock would settle it at once — deliberately not
|
||||
consulted while the pid reads alive, because a mixed-tier name-tier holder
|
||||
holds no flock; `state/usage_attempts.lock` needs a hand repair meanwhile
|
||||
(`docs/PERSISTENCE.md` says so in its row).
|
||||
On this tier we do not claim a pass can never be robbed. The bounded claim:
|
||||
a concurrent holder can exist only after the lock file is removed by an
|
||||
actor OUTSIDE the lock protocol — a hand repair, a foreign helper, a
|
||||
|
|
@ -284,10 +303,23 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
|
|||
checkpoint, immediately before every rename attempt and once more AFTER
|
||||
the in-swap snapshot look, so the irreducible residual is the interval
|
||||
between that last proof and the rename syscall: a charge the robber lands
|
||||
inside it is erased by the swap, then surfaced as `UsageLedgerCorrupt` by
|
||||
the post-swap re-read or quarantined seq-misnumbered at the next read. A
|
||||
`False` heartbeat — or one that cannot be answered at all — aborts the
|
||||
pass, leaving the ledger byte-identical.
|
||||
inside it is ERASED by the swap. Until round 5.4 this note claimed the
|
||||
post-swap re-read or the next read's seq quarantine would surface it;
|
||||
neither can — the re-read compares the NEW inode against the candidate and
|
||||
the archive segment is the pre-row snapshot — so the loss was silent and
|
||||
the pass returned a success receipt. Now, on POSIX, the swap holds the OLD
|
||||
inode open across the rename (the only witness left) and reads whatever
|
||||
landed beyond the proven snapshot's length AFTER the fact: those bytes go
|
||||
to `state/usage_attempts.quarantine.jsonl` (`raw_base64`, the shape a torn
|
||||
tail takes, which flips `integrity_degraded`) and the pass raises
|
||||
`UsageLedgerCorrupt` instead of returning a receipt — the charge is still
|
||||
gone from the live file (never re-appended: `seq` belongs to the live
|
||||
file), but it is preserved, flagged and typed. Detected by size: a
|
||||
same-size in-place rewrite inside that one syscall is not a landed charge
|
||||
and is not seen. Windows cannot hold the destination open through
|
||||
`os.replace`, so there the loss stays silent, disclosed. A `False`
|
||||
heartbeat — or one that cannot be answered at all — aborts the pass,
|
||||
leaving the ledger byte-identical.
|
||||
*Name tier* (a filesystem the kernel says cannot lock — one answering
|
||||
`EOPNOTSUPP`/`ENOSYS` to `flock`; a lockd-less NFS answers `ENOLCK` and is
|
||||
fail-closed, above): the O_EXCL name protocol runs alone with re-check-then-unlink
|
||||
|
|
@ -437,14 +469,39 @@ the live replay, so this lane ships the join surface the sweep must use:
|
|||
not complete, so the question is UNKNOWN — the data root's own handle
|
||||
included, since a bare `OSError` from THAT one open (an unreadable root,
|
||||
fd exhaustion) would escape the sweep's UNKNOWN mapping entirely. An entry
|
||||
that is not a regular file — a stray `backup/` directory, a FIFO — is no
|
||||
segment: segments are regular files by construction, no generation lives
|
||||
there, and it is skipped. Where a dir-fd is held that classification is an
|
||||
`fstat` after an `O_NONBLOCK` open; where none is (Windows, or any os
|
||||
that OPENS but is not a regular file — a stray `backup/` directory, a
|
||||
FIFO, a device — is no segment: segments are regular files by
|
||||
construction, no generation lives there, and it is skipped; one the
|
||||
kernel refuses to open at all (a UNIX socket, `ENXIO`) is corruption like
|
||||
any other unopenable entry. Where a dir-fd is held that classification is
|
||||
an `fstat` after an `O_NONBLOCK` open; where none is (Windows, or any os
|
||||
without `O_DIRECTORY`) it is a `stat` BEFORE the open, because there the
|
||||
open is the step a directory refuses and a writer-less FIFO blocks on. A first row that reads but does not parse is a
|
||||
torn segment from a crashed write: no evidence of any generation, left to
|
||||
the walk;
|
||||
open is the step a directory refuses and a writer-less FIFO blocks on. A
|
||||
first row that reads but does not parse is a torn segment from a crashed
|
||||
write: no evidence of any generation, left to the walk. Every path
|
||||
inspection the reader makes is typed the same way (round 5.4): `pathlib`
|
||||
re-raises every `OSError` but `ENOENT`/`ENOTDIR`/`EBADF`/`ELOOP` from
|
||||
`is_symlink`/`is_dir`, so the symlink bounds on both archive levels and
|
||||
on the named segment (an `archive/usage_ledger` readable but not
|
||||
searchable — the shape a `chmod -R 600 data/` hardening produces —
|
||||
refused the segment's own `lstat`) raise `UsageLedgerCorrupt`, never a
|
||||
bare `OSError`; and on a STAMP-LESS live file the question ends early
|
||||
only on the kernel's exact "no archive directory" (`ENOENT`) — a regular
|
||||
file standing where the directory belongs, or an archive that cannot be
|
||||
inspected, is UNKNOWN (typed), never a silent empty answer — and every
|
||||
case that reads anything then goes through the same typed root open.
|
||||
Disclosed, stamp-less anchor: with the floor at epoch zero every parsable
|
||||
regular file in `archive/usage_ledger/` that is not a byte-prefix of a
|
||||
stamp-less live file is `generation newer` — a fresh ledger started
|
||||
beside a surviving archive (a reset that deleted the ledger alone) leaves
|
||||
every history question a PERMANENT corruption verdict for the life of the
|
||||
install, and an operator's stray `notes.jsonl` there is skipped on a
|
||||
stamped ledger but is corruption on a stamp-less one; move or delete the
|
||||
archive with the ledger, or keep both. Disclosed, lock-free readers: a
|
||||
compaction that commits between a question's live-header read and its
|
||||
anchor scan makes that ONE question UNKNOWN (`generation newer`: the
|
||||
freshly committed segment is prefix-tested against the new live file);
|
||||
the next question walks the new chain;
|
||||
- the archive directory must be **this data root's own**: neither
|
||||
`archive/` nor `archive/usage_ledger` may be a symlink, the resolved
|
||||
directory must be exactly the resolved root's archive path, and no segment
|
||||
|
|
@ -547,13 +604,18 @@ byte authority.
|
|||
swapped between the chain walk and the anchor scan (a look-alike carrying
|
||||
the newest NAME with the forged live header included), an archive entry the
|
||||
anchor cannot open (the data root's own handle included), a named segment
|
||||
that is not a regular file, and an unreadable leading row are each typed
|
||||
corruption (UNKNOWN/skip), never silent absence and never a bare
|
||||
`OSError`; an uncommitted orphan segment of the live generation is NOT
|
||||
corruption (its bytes are still a prefix of the live file), a stray
|
||||
directory or FIFO in the archive is no segment and is skipped on both
|
||||
shapes of the scan (the FIFO cannot hang the question), and the chain union
|
||||
is built once per chain and cached within a bound.
|
||||
that is not a regular file, an unreadable leading row, and a path
|
||||
inspection the kernel refuses (the symlink bound on either archive level
|
||||
or on the named segment; the stamp-less archive check, which ends the
|
||||
question early only on an exact `ENOENT`) are each typed corruption
|
||||
(UNKNOWN/skip), never silent absence and never a bare `OSError`; an
|
||||
uncommitted orphan segment of the live generation is NOT corruption (its
|
||||
bytes are still a prefix of the live file), a stray directory or FIFO in
|
||||
the archive — an entry that opens but is not a regular file — is no
|
||||
segment and is skipped on both shapes of the scan (the FIFO cannot hang
|
||||
the question; an entry the kernel refuses to open at all, a UNIX socket,
|
||||
is corruption), and the chain union is built once per chain and cached
|
||||
within a bound.
|
||||
6. **Idempotency survives**: subscription/external replays after compaction
|
||||
dedup (no double charge) and still conflict-check; legacy import stays
|
||||
correct with and without its watermark.
|
||||
|
|
@ -589,8 +651,11 @@ byte authority.
|
|||
look answered True refuses it too, so the last proof precedes the rename
|
||||
by one syscall); a pass that loses the hold abandons its work at its next
|
||||
proof instead of swapping (bounded, not absolute: a charge landed by an
|
||||
out-of-protocol holder inside that one syscall is erased, then surfaced by
|
||||
the post-swap re-read or quarantined at the next read); of two reclaimers
|
||||
out-of-protocol holder inside that one syscall is erased by the rename —
|
||||
on POSIX its bytes are read back from the old inode held open across the
|
||||
swap, quarantined with `integrity_degraded` raised, and the pass raises
|
||||
typed instead of returning a receipt; on Windows the loss is silent,
|
||||
disclosed); of two reclaimers
|
||||
racing over a stale lock at most one can evict (eviction only under the
|
||||
held flock on the judged fd); a creator evicted while still lock-less
|
||||
never returns a descriptor, nor does one whose own identity cannot be read
|
||||
|
|
|
|||
|
|
@ -5487,3 +5487,130 @@ Cross-cutting disclosures:
|
|||
data root was an isolated mktemp); the lane's HEAD unmoved after both.
|
||||
With this recorded, round 5.3 is executed and verified on this host; the
|
||||
Windows LockFileEx mechanics remain owed to the 3-OS CI matrix (item 3a).
|
||||
|
||||
## From the C6 micro-round 5.4 (owner batch №12 A, base 096437c2)
|
||||
|
||||
1. Owner-bounded micro-round over the round-5.3 candidate `096437c2`: the
|
||||
residual list left by the Fable lenses and the independent gpt-5.6-sol
|
||||
read-only review — eight items, no new exploration. All eight disposed:
|
||||
seven changed in code with red-first pins, one (R8 c–f) closed by the
|
||||
disclosures it asked for. Full disposition and the red-first table:
|
||||
`docs/v7next/C6_REVIEW_PACKET.md` §9, "Round 5.4".
|
||||
- **ENOLCK fails closed; the tier is decided once under a lock** (R1,
|
||||
HIGH): "no locks available" is a missing lock daemon OR an exhausted
|
||||
kernel lock table, not a capability answer, yet it selected the name
|
||||
tier. The unsupported set is exactly `EOPNOTSUPP`/`ENOTSUP`/`ENOSYS`
|
||||
(winerror 1 → ENOSYS, 50 → EOPNOTSUPP); ENOLCK keeps the enforced tier
|
||||
and every live acquisition the kernel refuses with it fails closed, so
|
||||
a lockd-less NFS refuses every monetary write typed instead of running
|
||||
the name protocol; `_KERNEL_LOCK_TIER_LOCK` makes the probe
|
||||
single-flight. SUPERSEDES the round-5 and round-5.3 entries above where
|
||||
they spell the unsupported set with ENOLCK. Commit `7ce7e83d`.
|
||||
- **A pid that refuses our signal is alive** (R7, MEDIUM): `pid_is_alive`
|
||||
folded EPERM into "dead", so another user's recycled pid was reclaimed
|
||||
through the age path and only a same-uid recycle wedged — the OPPOSITE
|
||||
of the mechanism the round-5.3 L2 bullet above, DESIGN §8 and
|
||||
PERSISTENCE stated. EPERM reads alive (the process exists); only ESRCH
|
||||
is dead; `pid_provably_gone` is the exact negation and became one line.
|
||||
The real residual: any live impostor wedges the lock from the 90 s
|
||||
staleness window (`usage_ledger._locked`, `stale_sec=90.0`) until it
|
||||
exits. SUPERSEDES the L2 bullet's mechanism. Commit `01c89685`.
|
||||
- **The name-tier refusal is "told" only once its row landed** (R2,
|
||||
MEDIUM): `append_jsonl` reports exhausted retries as `False`; the mark
|
||||
now follows a `True` answer only. Commit `12558046`.
|
||||
- **The history reader types every path inspection, the stamp-less
|
||||
question included** (R3 + R4, MEDIUM): `pathlib` re-raises every
|
||||
`OSError` but ENOENT/ENOTDIR/EBADF/ELOOP; the symlink bounds on both
|
||||
archive levels and on the named segment, and the stamp-less fast path
|
||||
(`is_dir()`: a silent `frozenset()` for a regular file where the
|
||||
directory belongs, a bare `OSError` for an uninspectable one), are
|
||||
`UsageLedgerCorrupt` now; before any compaction the question ends early
|
||||
only on an exact ENOENT. Commit `d99ff6a9`.
|
||||
- **A charge erased inside the swap's one syscall is quarantined and
|
||||
surfaced, never receipted** (R5, MEDIUM): the round-5.2 §3(b) line above
|
||||
and the round-5.3 §3(b) line — "erased, then surfaced as
|
||||
`UsageLedgerCorrupt` by the post-swap re-read or quarantined
|
||||
seq-misnumbered at the next read" — were FALSE: neither could see it,
|
||||
the loss was silent and a success receipt was returned. On POSIX the
|
||||
swap holds the old inode open across the rename and reads back what
|
||||
landed beyond the proven snapshot: quarantined (`raw_base64`,
|
||||
`integrity_degraded`), typed raise, no receipt, never re-appended;
|
||||
Windows stays a disclosed silent loss. SUPERSEDES both lines. Commit
|
||||
`ea4d4337`.
|
||||
- **The reserve path is pinned to the held lock's OWN heartbeat** (R6,
|
||||
MEDIUM): `assert callable(...)` let a constant-True stub survive the
|
||||
battery; the pin now requires the aged lock renewed. Commit `9306f962`.
|
||||
- **`_build_candidate`'s beat is required; the heartbeat's blind spots
|
||||
are pinned** (R8a, R8b, LOW). Commit `02338c9b`.
|
||||
- **Disclosed** (R8c–f): the transient `generation newer` UNKNOWN when a
|
||||
compaction commits mid-question; a ledger reset beside a surviving
|
||||
archive as a permanent verdict (PERSISTENCE both rows); "is skipped"
|
||||
qualified to entries that OPEN (a UNIX socket is corruption); the
|
||||
POSIX-only release-under-flock and the contention-branch orphan. Docs
|
||||
commit (this section).
|
||||
Two no-behaviour commits paid for the pins inside the 1600 hard cap
|
||||
without a neighbour suite: `bd9e99a4` (a `compacted` fixture folding
|
||||
twenty-one seed-then-compact preambles, −39) and `b9c43911`
|
||||
(argument-list/data-literal reflows, −28); every claim kept.
|
||||
2. Red observed, not argued (each pin run against the exact pre-fix shape or
|
||||
mutation it names, in this worktree; pin red, fix applied or mutation
|
||||
reverted, pin green):
|
||||
|
||||
| pin | mutation / base | red observed |
|
||||
|---|---|---|
|
||||
| `test_a_pid_that_refuses_our_signal_is_alive_and_its_lock_is_not_reclaimed` | `platform_layer.py` @ `bd9e99a4` | `assert (False is True)`; the aged lock evicted, fd 3 returned |
|
||||
| `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) | `platform_layer.py` @ `01c89685` | `AssertionError: 37` — errno 37 selected the name tier |
|
||||
| `test_enolck_keeps_the_enforced_tier_and_the_acquisition_fails_closed` | same | `enforced = False`, fd 3 returned on the name tier |
|
||||
| `test_two_threads_racing_the_first_probe_run_one_probe_and_read_one_tier` | same | `2 == 1`: two probes |
|
||||
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (False append) | `usage_compaction.py` @ `7ce7e83d` | `FileNotFoundError: …/logs/events.jsonl` |
|
||||
| `test_a_stamp_less_ledger_still_inspects_its_archive_fail_closed` | `usage_compaction.py` @ `b9c43911` | `DID NOT RAISE` (answered `frozenset()`); bare `PermissionError` |
|
||||
| `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` | same | bare `PermissionError` from the segment's lstat; bare `PermissionError` from `is_symlink` |
|
||||
| `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt[erased]` | `usage_compaction.py` @ `d99ff6a9` | `DID NOT RAISE`: receipt returned, the charge gone, no quarantine, `integrity_degraded` False |
|
||||
| `test_reserve_path_compacts_only_past_config_threshold` (heartbeat clause) | M9 (`heartbeat=lambda: True`) | `a stub, not the held lock's heartbeat`; 84 other items green under M9 |
|
||||
| `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (refresh clauses) | M15 / M16 | `assert True is False` under each |
|
||||
| `test_the_long_build_and_verification_section_beats_the_lock` (control) | M10 (`beat` dropped at the call) | `TypeError … missing 1 required positional argument: 'beat'` → `assert None is not None` |
|
||||
3. Disclosed, not fixed: (a) a lockd-less NFS `state/` now fails every
|
||||
monetary write closed (the owner's decision; the repair is a filesystem
|
||||
that locks); (b) no `events.jsonl` row is fsync'd — the per-process mark
|
||||
dies with the crash that could lose the row, a delayed writeback error
|
||||
with the process alive is the residual; (c) R5 detects by size (a
|
||||
same-size in-place rewrite inside the syscall is not a landed charge) and
|
||||
Windows stays silent; (d) the recycled-pid wedge now covers another-uid
|
||||
recycles too, the flock alternative would evict a live mixed-tier holder;
|
||||
(e) R8c–f as disclosures; no socket pin (the 108-byte `AF_UNIX` cap, a
|
||||
`chdir` hazard); (f) size: `platform_layer.py` 1500 → 1497 and
|
||||
`usage_compaction.py` 1197 → 1245 inside their bands (the band rationale
|
||||
is immutable between adjacent manifests by the ratchet's own rule, so the
|
||||
owner's "extend it in the same commit" is recorded here instead),
|
||||
`tests/test_usage_compaction.py` 1597 → 1597; (g) Windows unexecuted here;
|
||||
(h) sol's tier-attestation-on-the-hold suggestion not taken (the module
|
||||
lock closes the intra-process disagreement; cross-process mixed tiers
|
||||
remain the round-5.2 disclosure).
|
||||
4. Gate evidence (this host, isolated env roots per invocation, venv python
|
||||
3.10.12; `git rev-parse HEAD` verified unmoved after every pytest run;
|
||||
author and committer `Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>`
|
||||
on every commit, no push):
|
||||
- targeted at `02338c9b` (docs in the tree, uncommitted):
|
||||
`tests/test_usage_compaction.py` + `tests/test_lockfile_helpers.py`
|
||||
85 passed, 1 skipped (Windows-only), exit 0; the four other
|
||||
`tests/test_usage_*.py` suites + `tests/test_persistence_inventory.py`
|
||||
94 passed, exit 0; the `pid_is_alive`/lock consumers
|
||||
(`test_process_custody`, `test_services_tool_v2`, `test_launcher_sync`,
|
||||
`test_extension_reload_all`, `test_review_reconciliation_custody`,
|
||||
`test_evolution_restart_claims`,
|
||||
`test_available_subagents_runtime_review_fixes`,
|
||||
`test_launcher_server_reaper`, `test_bughunt_fixes`,
|
||||
`test_task_status_flow`, `test_evolution_commit_receipt`,
|
||||
`test_skill_lifecycle_queue`, `test_context`, `test_startup_hygiene`,
|
||||
`test_atomic_write_v639`) 382 passed, exit 0; no
|
||||
`tests/test_platform_layer*.py` exists in this tree;
|
||||
- `ruff check . --select F` exit 0; `scripts/check_domains.py` exit 0;
|
||||
`scripts/regenerate_size_ratchet.py --check` exit 0 (at every commit of
|
||||
the round the module stayed inside its band and the suite under the
|
||||
hard cap); `scripts/regenerate_inventories.py --check` exit 0;
|
||||
`git diff --check` exit 0 — each as its own command, rc printed.
|
||||
With this recorded, round 5.4 is executed on this host; the CI-shape
|
||||
battery and the serial pass were not re-run for this micro-round (its
|
||||
surfaces are the two pinned suites and the lock/liveness consumers above,
|
||||
all run), and the Windows LockFileEx mechanics remain owed to the 3-OS CI
|
||||
matrix.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue