You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
NoKVAuthorityStore (loopx/control_plane/coordination/nokv_authority_store.ts) keeps the whole retained journal in one CAS envelope at metadata/loopx-authority/<digest>.json, republishes it on every commit, and refuses at 16 MiB (DEFAULT_MAX_ENVELOPE_BYTES → failed/authority_envelope_too_large). Measured on mainda6d797, the envelope grows by roughly one full projection per commit, so the candidate cannot hold the §7.2 minimum continuity load (864 renewals/day) for a single day. This issue proposes a bounded layout for the NoKV candidate, asks for the owner decisions it depends on (RFC §12 Q5 and the capacity vocabulary), and does not request scheduling on the R5 path: NoKV is a conformance reference, not the selected local profile.
Related: RFC §6.2 (read the one CAS envelope until qualified), §7.1 (sealed receipt segments, owner decision Q5), §7.2 (bounded head constraints), Appendix C (store_capacity_exhausted), #4069 (NoKV-plane evidence scope), NoKV-Lab/NoKV#511 (holt/fdb gate), NoKV-Lab/NoKV#514 (publish incarnation fence, the Appendix A hold).
Probe: the store driven through its public AuthorityStore methods with the production-scale fixtures; each commit carries one lease-renewal event and one receipt (~0.6 KiB). In-memory transport isolates the store's own cost; the live rows ran against a single NoKV 0.11.0 owner with an S3-compatible local object store.
Profile
Commits to 16 MiB
Envelope growth per commit
At 864 renewals/day
At 10k commits/day
Cumulative bytes to the cap (write / read)
22 KiB projection (productionScaleHistoryProjection, natural size 21,858 B)
739
≈ 22.7 KiB
0.86 day
1.8 h
5.9 GiB / 11.9 GiB
64 KiB projection (same fixture padded)
252
≈ 66.7 KiB
0.29 day
0.6 h
2.0 GiB / 4.4 GiB
Every committed[] row retains its full projection, so growth per commit ≈ projection size, not event size. Every read decodes the whole envelope and re-derives the revision chain: with the 22 KiB profile loadAuthority / readReceipt take ≈ 0.77 s at 600 commits (CPU only); live, with the 64 KiB profile, commit latency went from 0.34 s to 3.5 s and readReceipt / loadAuthority from 66 ms to 1.3 s between commit 2 and commit 251. Each commit performs two envelope reads (pre-read and read-back) and each read a find_workspaces round trip.
Provider facts that shape the design (live, same owner): a create-only republish of identical bytes under the same operation_id / artifact_revision_id replays as applied (idempotent); a create-only publish with fresh ids on an existing path is a typed conflict; reusing ids with different bytes is a replay mismatch the helper can only report as ambiguous; 4, 8, 16 and 24 MiB single objects are all admitted and readable (24 MiB needs the helper transport's response cap raised above its 32 MiB default), so the 16 MiB cap is LoopX's constant, not a NoKV limit.
Proposed layout (loopx_nokv_authority_store_v1)
Three object kinds under the existing digest key d, using only the three transport verbs the helper already has (store_identity, read_blob, cas_publish_blob; create-only is expected_generation: null). No helper, SDK-pin or transport change.
Head (same path, one generation CAS per commit): schema_version, tenant_id, goal_id, store_identity, storage_generation, head_generation_offset, provider_revision, cursor, head, window, sealed, index. window = checkpoint projection + parent revision + up to 64 rows in the SQLite v2 row shape (cursor, provider_revision, operation_id, events, receipts, delta, state_digest, parent_state_digest), built with the existing authorityStateDelta / authorityStateDeltaReconstructs. sealed keeps only the count and the latest segment pointer (§7.2: a head that lists every pointer is still unbounded). Head size is independent of history: ≈ 78 KiB for the 22 KiB profile, ≈ 176 KiB for 64 KiB.
Segment k (…/<d>/segments/<k>.json, create-only, content-derived ids): checkpoint + 64 rows + previous pointer; events and receipts byte-for-byte, so retain_all semantics are unchanged.
Index k (…/<d>/index/<k>.json, create-only): the segment table plus [operation_id, cursor] pairs sorted with authorityUnicodeCompare covering 1..64k. readReceipt for an old operation is head + index binary search + one segment replay: three reads, never a chain walk (§7.2). The index is republished per sealed window (≈ 0.55·N² bytes cumulative, ≈ 55 MB at 10k commits); a bucketed index is a possible v2 if the ledger shows it matters.
scanCommitted locates segments through the index and replays window by window, so the full-projection page contract (AuthorityJournalScan.page) is preserved; loadAuthority proves the live head from the head object alone (≤ 64 deltas + ≤ 64 revision digests).
providerRevision is unchanged: its generation input becomes Number(cursor) explicitly; physical generation = cursor + head_generation_offset (offset 1 after a one-time swap), so cursors, operation ids, provider revisions, events and receipts stay byte-identical across the migration.
Commit protocol: read head → revision pre-check → operation pre-check (window + index) → build the row → at a window boundary publish S_k then I_k create-only (a read-back with the same sha256 counts as applied; a different sha256 is nokv_segment_publish_conflict; a missing object retries with a fresh attempt id up to 3 times, then nokv_segment_publish_exhausted; the head has not moved, so nothing here is ambiguous) → size guard store_capacity_exhausted (replacing authority_envelope_too_large, Appendix C vocabulary) → head CAS exactly as today, including read-back settlement. Objects published before a head CAS that is refused or lost are orphans reachable only through the head pointers; readReceipt / scanCommitted never see them, which keeps the "receipt missing after a rejected commit" conformance assertions intact.
Migration (nokv_authority_migration.ts, modelled on sqlite_authority_migration.ts): plan by default, explicit execute; publish every segment/index create-only; before the single head CAS, read every published object back and replay it from the empty state through the decoder, comparing each state_digest / projection digest (the fix(authority): keep legacy JSON keys readable across the sqlite migration #4497 lesson: sha256 equality is not readability); identity-sequence digest equal before and after; rollback exports back to v0 and refuses above 16 MiB. v1 refuses to read a v0 head with a typed failure that asks for an explicit migration.
Shared code: authority_state_log.ts gains replayAuthorityStateWindow; decodeRetainedAuthorityJournal stays File-only; AuthorityStore gains no member (conformance suites hand-write the five-method literal); provider profile literals for NoKV change to artifact_first_segment_publish_then_head_cas / bounded_head_window_sealed_segments_and_operation_index (only revision_primitive, store_lineage_mapping and the capacity_and_receipt_retention hold are pinned by tests today).
Test plan
All 12 suites bundled by registerAuthorityStoreConformance on both NoKV registration points (FakeNoKVTransport and the real helper subprocess) plus authority_provider_parity.test.ts stay green. Because those suites never cross the 64-commit window (deepest cursor is 6), a NoKV-specific 192-commit test (porting replayFixtureHistory) asserts readReceipt through the index for cursor 1 and a mid-segment operation, scanCommitted across the 64→65 and 128→129 boundaries, head size independent of history, and receipt equality before and after sealing. Layout and migration tests include "" and __proto__ projection keys (the #4497 fixture) because the NoKV path crosses the Python JSON boundary. The PR records node --test tests/control_plane_ts/nokv_jsonl_transport.test.ts wall-clock on main and on the head, since old-operation reads become three round trips.
Decisions needed from the owner
Accept §12 Q5 (sealed segments) for NoKV, or keep retain_all_v0 normative and treat this as a NoKV-only qualified layout under Appendix C.
Rename the typed refusal from authority_envelope_too_large to store_capacity_exhausted (Appendix C vocabulary), or keep both.
Object prefix: metadata/loopx-authority/<d>/… (this proposal) or goals/<goal>/receipts/… as written in §7.1.
Whether v1 may refuse to read a v0 head (explicit migration) or must read both.
head_generation_offset (keeps every provider revision byte-identical) versus a fresh lineage identity after migration.
Whether a monolithic per-window index is acceptable for P1 with the bucketed variant deferred to the ledger.
NoKV stays out of provider_matrix.local_profiles; a separate NoKV capacity ledger rather than entries in the SQLite D2 ledger.
Where the evidence lives: §11.2 is the append cluster that made three PRs dirty ([Queue] 19 个 open PR 与 main 冲突:其中 7 个只卡在两组 append-only RFC ledger,3 个卡在已提交的前端 bundle #4677), and examples/docs-governance-smoke.py requires an "Appendix A: Execution ledger" heading before a ledger/ directory may exist for this RFC — should Appendix A be retitled and moved, or should NoKV entries hang off another carrying RFC?
Whether this design lands as an RFC amendment PR (normative) after the decisions above, or stays issue-tracked with the code PRs referencing it.
zh-CN mirror timing for whatever text changes.
Packaging
P0 (this issue: measurements and decisions) → P1 bounded layout (one PR, three reviewable commits: layout module, store rewrite, tests) → P2 migration + operator entrypoint → P3 capacity ledger. Held out: ten-day soak, HA, restart/restore, real cloud S3. NoKV-Lab/NoKV#514 (the publish incarnation fence) is independent and lands in the NoKV 0.11.1 line; adopting it in the helper is a separate LoopX PR after the wheel is released.
Roadmap position
S2 work on a non-selected candidate. It is R5-shaped (bounded head, capacity) but asks for no slot on the owner's R5 path; the selected local profile remains SQLite (#4224).
Summary
NoKVAuthorityStore(loopx/control_plane/coordination/nokv_authority_store.ts) keeps the whole retained journal in one CAS envelope atmetadata/loopx-authority/<digest>.json, republishes it on every commit, and refuses at 16 MiB (DEFAULT_MAX_ENVELOPE_BYTES→failed/authority_envelope_too_large). Measured onmainda6d797, the envelope grows by roughly one full projection per commit, so the candidate cannot hold the §7.2 minimum continuity load (864 renewals/day) for a single day. This issue proposes a bounded layout for the NoKV candidate, asks for the owner decisions it depends on (RFC §12 Q5 and the capacity vocabulary), and does not request scheduling on the R5 path: NoKV is a conformance reference, not the selected local profile.Related: RFC §6.2 (read the one CAS envelope until qualified), §7.1 (sealed receipt segments, owner decision Q5), §7.2 (bounded head constraints), Appendix C (
store_capacity_exhausted), #4069 (NoKV-plane evidence scope), NoKV-Lab/NoKV#511 (holt/fdb gate), NoKV-Lab/NoKV#514 (publish incarnation fence, the Appendix A hold).Measurements (v0 layout,
mainda6d797)Probe: the store driven through its public
AuthorityStoremethods with the production-scale fixtures; each commit carries one lease-renewal event and one receipt (~0.6 KiB). In-memory transport isolates the store's own cost; the live rows ran against a single NoKV 0.11.0 owner with an S3-compatible local object store.productionScaleHistoryProjection, natural size 21,858 B)Every
committed[]row retains its full projection, so growth per commit ≈ projection size, not event size. Every read decodes the whole envelope and re-derives the revision chain: with the 22 KiB profileloadAuthority/readReceipttake ≈ 0.77 s at 600 commits (CPU only); live, with the 64 KiB profile, commit latency went from 0.34 s to 3.5 s andreadReceipt/loadAuthorityfrom 66 ms to 1.3 s between commit 2 and commit 251. Each commit performs two envelope reads (pre-read and read-back) and each read afind_workspacesround trip.Provider facts that shape the design (live, same owner): a create-only republish of identical bytes under the same
operation_id/artifact_revision_idreplays asapplied(idempotent); a create-only publish with fresh ids on an existing path is a typed conflict; reusing ids with different bytes is a replay mismatch the helper can only report asambiguous; 4, 8, 16 and 24 MiB single objects are all admitted and readable (24 MiB needs the helper transport's response cap raised above its 32 MiB default), so the 16 MiB cap is LoopX's constant, not a NoKV limit.Proposed layout (
loopx_nokv_authority_store_v1)Three object kinds under the existing digest key
d, using only the three transport verbs the helper already has (store_identity,read_blob,cas_publish_blob; create-only isexpected_generation: null). No helper, SDK-pin or transport change.schema_version, tenant_id, goal_id, store_identity, storage_generation, head_generation_offset, provider_revision, cursor, head, window, sealed, index.window= checkpoint projection + parent revision + up to 64 rows in the SQLite v2 row shape (cursor, provider_revision, operation_id, events, receipts, delta, state_digest, parent_state_digest), built with the existingauthorityStateDelta/authorityStateDeltaReconstructs.sealedkeeps only the count and the latest segment pointer (§7.2: a head that lists every pointer is still unbounded). Head size is independent of history: ≈ 78 KiB for the 22 KiB profile, ≈ 176 KiB for 64 KiB.…/<d>/segments/<k>.json, create-only, content-derived ids): checkpoint + 64 rows + previous pointer; events and receipts byte-for-byte, soretain_allsemantics are unchanged.…/<d>/index/<k>.json, create-only): the segment table plus[operation_id, cursor]pairs sorted withauthorityUnicodeComparecovering1..64k.readReceiptfor an old operation is head + index binary search + one segment replay: three reads, never a chain walk (§7.2). The index is republished per sealed window (≈ 0.55·N² bytes cumulative, ≈ 55 MB at 10k commits); a bucketed index is a possible v2 if the ledger shows it matters.scanCommittedlocates segments through the index and replays window by window, so the full-projection page contract (AuthorityJournalScan.page) is preserved;loadAuthorityproves the live head from the head object alone (≤ 64 deltas + ≤ 64 revision digests).providerRevisionis unchanged: its generation input becomesNumber(cursor)explicitly; physical generation =cursor + head_generation_offset(offset 1 after a one-time swap), so cursors, operation ids, provider revisions, events and receipts stay byte-identical across the migration.S_kthenI_kcreate-only (a read-back with the same sha256 counts as applied; a different sha256 isnokv_segment_publish_conflict; a missing object retries with a fresh attempt id up to 3 times, thennokv_segment_publish_exhausted; the head has not moved, so nothing here is ambiguous) → size guardstore_capacity_exhausted(replacingauthority_envelope_too_large, Appendix C vocabulary) → head CAS exactly as today, including read-back settlement. Objects published before a head CAS that is refused or lost are orphans reachable only through the head pointers;readReceipt/scanCommittednever see them, which keeps the "receipt missing after a rejected commit" conformance assertions intact.nokv_authority_migration.ts, modelled onsqlite_authority_migration.ts): plan by default, explicitexecute; publish every segment/index create-only; before the single head CAS, read every published object back and replay it from the empty state through the decoder, comparing eachstate_digest/ projection digest (the fix(authority): keep legacy JSON keys readable across the sqlite migration #4497 lesson: sha256 equality is not readability); identity-sequence digest equal before and after;rollbackexports back to v0 and refuses above 16 MiB. v1 refuses to read a v0 head with a typed failure that asks for an explicit migration.authority_state_log.tsgainsreplayAuthorityStateWindow;decodeRetainedAuthorityJournalstays File-only;AuthorityStoregains no member (conformance suites hand-write the five-method literal); provider profile literals for NoKV change toartifact_first_segment_publish_then_head_cas/bounded_head_window_sealed_segments_and_operation_index(onlyrevision_primitive,store_lineage_mappingand thecapacity_and_receipt_retentionhold are pinned by tests today).Test plan
All 12 suites bundled by
registerAuthorityStoreConformanceon both NoKV registration points (FakeNoKVTransportand the real helper subprocess) plusauthority_provider_parity.test.tsstay green. Because those suites never cross the 64-commit window (deepest cursor is 6), a NoKV-specific 192-commit test (portingreplayFixtureHistory) assertsreadReceiptthrough the index for cursor 1 and a mid-segment operation,scanCommittedacross the 64→65 and 128→129 boundaries, head size independent of history, and receipt equality before and after sealing. Layout and migration tests include""and__proto__projection keys (the #4497 fixture) because the NoKV path crosses the Python JSON boundary. The PR recordsnode --test tests/control_plane_ts/nokv_jsonl_transport.test.tswall-clock onmainand on the head, since old-operation reads become three round trips.Decisions needed from the owner
retain_all_v0normative and treat this as a NoKV-only qualified layout under Appendix C.authority_envelope_too_largetostore_capacity_exhausted(Appendix C vocabulary), or keep both.metadata/loopx-authority/<d>/…(this proposal) orgoals/<goal>/receipts/…as written in §7.1.head_generation_offset(keeps every provider revision byte-identical) versus a fresh lineage identity after migration.provider_matrix.local_profiles; a separate NoKV capacity ledger rather than entries in the SQLite D2 ledger.examples/docs-governance-smoke.pyrequires an "Appendix A: Execution ledger" heading before aledger/directory may exist for this RFC — should Appendix A be retitled and moved, or should NoKV entries hang off another carrying RFC?Packaging
P0 (this issue: measurements and decisions) → P1 bounded layout (one PR, three reviewable commits: layout module, store rewrite, tests) → P2 migration + operator entrypoint → P3 capacity ledger. Held out: ten-day soak, HA, restart/restore, real cloud S3. NoKV-Lab/NoKV#514 (the publish incarnation fence) is independent and lands in the NoKV 0.11.1 line; adopting it in the helper is a separate LoopX PR after the wheel is released.
Roadmap position
S2 work on a non-selected candidate. It is R5-shaped (bounded head, capacity) but asks for no slot on the owner's R5 path; the selected local profile remains SQLite (#4224).