instar 1.3.524 → 1.3.526

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +77 -5
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +23 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/ConflictStore.d.ts +150 -0
  8. package/dist/core/ConflictStore.d.ts.map +1 -0
  9. package/dist/core/ConflictStore.js +247 -0
  10. package/dist/core/ConflictStore.js.map +1 -0
  11. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  12. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  13. package/dist/core/PostUpdateMigrator.js +116 -0
  14. package/dist/core/PostUpdateMigrator.js.map +1 -1
  15. package/dist/core/ReplicatedStoreReader.d.ts +83 -0
  16. package/dist/core/ReplicatedStoreReader.d.ts.map +1 -0
  17. package/dist/core/ReplicatedStoreReader.js +100 -0
  18. package/dist/core/ReplicatedStoreReader.js.map +1 -0
  19. package/dist/core/ReplicationBudget.d.ts +183 -0
  20. package/dist/core/ReplicationBudget.d.ts.map +1 -0
  21. package/dist/core/ReplicationBudget.js +253 -0
  22. package/dist/core/ReplicationBudget.js.map +1 -0
  23. package/dist/core/RollbackUnmerge.d.ts +134 -0
  24. package/dist/core/RollbackUnmerge.d.ts.map +1 -0
  25. package/dist/core/RollbackUnmerge.js +286 -0
  26. package/dist/core/RollbackUnmerge.js.map +1 -0
  27. package/dist/core/UnionReader.d.ts +161 -0
  28. package/dist/core/UnionReader.d.ts.map +1 -0
  29. package/dist/core/UnionReader.js +0 -0
  30. package/dist/core/UnionReader.js.map +1 -0
  31. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +56 -2
  32. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
  33. package/dist/monitoring/ActiveWorkSilenceSentinel.js +174 -10
  34. package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
  35. package/dist/monitoring/PromiseBeacon.d.ts +48 -0
  36. package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
  37. package/dist/monitoring/PromiseBeacon.js +186 -41
  38. package/dist/monitoring/PromiseBeacon.js.map +1 -1
  39. package/dist/monitoring/SubagentTracker.d.ts +9 -0
  40. package/dist/monitoring/SubagentTracker.d.ts.map +1 -1
  41. package/dist/monitoring/SubagentTracker.js +11 -0
  42. package/dist/monitoring/SubagentTracker.js.map +1 -1
  43. package/dist/monitoring/sentinelWiring.d.ts +20 -1
  44. package/dist/monitoring/sentinelWiring.d.ts.map +1 -1
  45. package/dist/monitoring/sentinelWiring.js +26 -0
  46. package/dist/monitoring/sentinelWiring.js.map +1 -1
  47. package/dist/scaffold/templates.d.ts.map +1 -1
  48. package/dist/scaffold/templates.js +13 -0
  49. package/dist/scaffold/templates.js.map +1 -1
  50. package/dist/server/AgentServer.d.ts +7 -0
  51. package/dist/server/AgentServer.d.ts.map +1 -1
  52. package/dist/server/AgentServer.js +3 -0
  53. package/dist/server/AgentServer.js.map +1 -1
  54. package/dist/server/routes.d.ts +10 -0
  55. package/dist/server/routes.d.ts.map +1 -1
  56. package/dist/server/routes.js +67 -0
  57. package/dist/server/routes.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/data/builtin-manifest.json +64 -64
  60. package/src/scaffold/templates.ts +13 -0
  61. package/upgrades/1.3.525.md +32 -0
  62. package/upgrades/1.3.526.md +83 -0
  63. package/upgrades/side-effects/hlc-step4-5-union-rollback-bounds.md +148 -0
  64. package/upgrades/side-effects/honest-progress-messaging.md +150 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ReplicatedStoreReader — the LOWEST store-access primitive, the single funnel
3
+ * every replicated-store read routes through so no caller can bypass the
4
+ * no-clobber union rule (WS2 replicated-store foundation, Component 6 / §7.2).
5
+ *
6
+ * Spec: docs/specs/multi-machine-replicated-store-foundation.md §7.1 (namespaced
7
+ * per-origin storage), §7.2 (the union of local + replicated state computed at the
8
+ * LOWEST primitive — enforced by a wiring-integrity test, §12 #11), §7.4 (a
9
+ * dropped origin is excluded from the union LIVE), §11 (machine-local-by-design:
10
+ * computed from the local own + local replica namespaces, no cross-machine call
11
+ * on the read path).
12
+ *
13
+ * THE BYPASS-PROOF FUNNEL. A store's materialized replicated state is the UNION
14
+ * over every participating origin namespace: the own stream + each non-dropped
15
+ * peer replica. This reader is the ONE place that composes that union (via
16
+ * UnionReader.readUnion) — a consumer store reads THROUGH it, never around it. The
17
+ * wiring-integrity test asserts the reader's seams are dependency-injected and not
18
+ * null / not no-ops.
19
+ *
20
+ * PURE-ish: all I/O is the injected `loadOriginRecords` seam (the real wiring reads
21
+ * the CoherenceJournal own stream + the peers/ replica streams for the store's
22
+ * kind(s)). The reader itself only composes + merges + records conflicts —
23
+ * bounded, no fs, no Date directly.
24
+ */
25
+ import type { ReplicatedKindRegistry } from './ReplicatedRecordEnvelope.js';
26
+ import { type ImpactTier, type OriginRecord, type UnionResult } from './UnionReader.js';
27
+ import type { ConflictStore } from './ConflictStore.js';
28
+ import type { DroppedOriginRegistry } from './RollbackUnmerge.js';
29
+ import type { StateSyncStores } from './ReplicatedRecordEnvelope.js';
30
+ /** The seams the reader needs (DI'd so the wiring-integrity test can assert they
31
+ * are real, never null/no-op). */
32
+ export interface ReplicatedStoreReaderSeams {
33
+ /** The replicated-kind registry — the reader serves ONLY registered stores. */
34
+ registry: ReplicatedKindRegistry;
35
+ /** The per-store stateSync flags (a store read is a no-op when its store is not
36
+ * enabled — the dark-by-default posture). */
37
+ stores: StateSyncStores | undefined;
38
+ /** The store's impact tier (high/low) — governs concurrent-edit behavior. A
39
+ * store declares its tier at registration; the default is HIGH (the
40
+ * conservative side — append-both-and-flag never silently clobbers). */
41
+ tierOf: (store: string) => ImpactTier;
42
+ /**
43
+ * Load EVERY origin's current record for (store, recordKey) — the own stream +
44
+ * each peer replica namespace, keyed by origin (one record per origin, the
45
+ * latest by HLC). This is the per-origin materialization the union merges. The
46
+ * real wiring reads the CoherenceJournal own stream + the peers/ replica files
47
+ * for the store's kind(s); a single-machine install returns only the own
48
+ * origin (so the union is a strict no-op = that one record).
49
+ */
50
+ loadOriginRecords: (store: string, recordKey: string) => OriginRecord[];
51
+ /** Enumerate every recordKey the store currently holds across all origins (for
52
+ * readAll). The real wiring scans the own + replica streams. */
53
+ listRecordKeys: (store: string) => string[];
54
+ /** The durable un-merged (store, origin) set — a dropped origin is excluded
55
+ * from the union LIVE (§7.4). */
56
+ droppedOrigins: DroppedOriginRegistry;
57
+ /** The conflict ledger — a HIGH-impact concurrent divergence is recorded here
58
+ * (idempotent on the stable conflictId). */
59
+ conflictStore: ConflictStore;
60
+ }
61
+ export declare class ReplicatedStoreReader {
62
+ private readonly seams;
63
+ constructor(seams: ReplicatedStoreReaderSeams);
64
+ /** Is this store registered + enabled (so a read is non-dark)? A read on an
65
+ * unregistered or disabled store returns a strict no-op (no record). */
66
+ private isLive;
67
+ /**
68
+ * Read one recordKey through the union (§7.2). Returns the no-clobber union
69
+ * result: the resolved value, a conflict descriptor (HIGH-impact concurrent),
70
+ * or a divergence flag (LOW-impact concurrent). A HIGH-impact conflict is
71
+ * RECORDED in the conflict ledger (idempotent) as a side-effect so it is
72
+ * surfaced for operator resolution — the read NEVER clobbers a divergent record.
73
+ *
74
+ * Live-recompute (§7.4): the dropped-origin set is consulted HERE, so a
75
+ * rolled-back origin is excluded from the union instantly with no rewrite.
76
+ */
77
+ read(store: string, recordKey: string): UnionResult;
78
+ /** Read every recordKey the store holds, as a recordKey → UnionResult map (§7.2).
79
+ * Conflicts are recorded as a side-effect (same as read). Bounded by the live
80
+ * key count. */
81
+ readAll(store: string): Map<string, UnionResult>;
82
+ }
83
+ //# sourceMappingURL=ReplicatedStoreReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReplicatedStoreReader.d.ts","sourceRoot":"","sources":["../../src/core/ReplicatedStoreReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE;mCACmC;AACnC,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,EAAE,sBAAsB,CAAC;IACjC;kDAC8C;IAC9C,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC;;6EAEyE;IACzE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;IACtC;;;;;;;OAOG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;IACxE;qEACiE;IACjE,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5C;sCACkC;IAClC,cAAc,EAAE,qBAAqB,CAAC;IACtC;iDAC6C;IAC7C,aAAa,EAAE,aAAa,CAAC;CAC9B;AASD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;gBAEvC,KAAK,EAAE,0BAA0B;IAY7C;6EACyE;IACzE,OAAO,CAAC,MAAM;IAKd;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW;IAkBnD;;qBAEiB;IACjB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;CAQjD"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * ReplicatedStoreReader — the LOWEST store-access primitive, the single funnel
3
+ * every replicated-store read routes through so no caller can bypass the
4
+ * no-clobber union rule (WS2 replicated-store foundation, Component 6 / §7.2).
5
+ *
6
+ * Spec: docs/specs/multi-machine-replicated-store-foundation.md §7.1 (namespaced
7
+ * per-origin storage), §7.2 (the union of local + replicated state computed at the
8
+ * LOWEST primitive — enforced by a wiring-integrity test, §12 #11), §7.4 (a
9
+ * dropped origin is excluded from the union LIVE), §11 (machine-local-by-design:
10
+ * computed from the local own + local replica namespaces, no cross-machine call
11
+ * on the read path).
12
+ *
13
+ * THE BYPASS-PROOF FUNNEL. A store's materialized replicated state is the UNION
14
+ * over every participating origin namespace: the own stream + each non-dropped
15
+ * peer replica. This reader is the ONE place that composes that union (via
16
+ * UnionReader.readUnion) — a consumer store reads THROUGH it, never around it. The
17
+ * wiring-integrity test asserts the reader's seams are dependency-injected and not
18
+ * null / not no-ops.
19
+ *
20
+ * PURE-ish: all I/O is the injected `loadOriginRecords` seam (the real wiring reads
21
+ * the CoherenceJournal own stream + the peers/ replica streams for the store's
22
+ * kind(s)). The reader itself only composes + merges + records conflicts —
23
+ * bounded, no fs, no Date directly.
24
+ */
25
+ import { readUnion, } from './UnionReader.js';
26
+ // RULE 3: EXEMPT — this is NOT a state-detector. ReplicatedStoreReader is a
27
+ // pure store-access funnel that composes the no-clobber union over already-loaded
28
+ // per-origin records (via the injected loadOriginRecords seam). It does not parse
29
+ // provider/CLI output, does not detect external/environment state, and has no
30
+ // signature-matching that could drift across providers — it merely merges
31
+ // in-memory records by HLC + the witness rule. The "*Reader" name matches the
32
+ // Rule-3 pattern heuristic but the substance is a deterministic in-memory merge.
33
+ export class ReplicatedStoreReader {
34
+ seams;
35
+ constructor(seams) {
36
+ // Wiring-integrity preconditions (§12 #11): the seams MUST be real, not null.
37
+ if (!seams)
38
+ throw new Error('ReplicatedStoreReader: seams are required');
39
+ if (!seams.registry)
40
+ throw new Error('ReplicatedStoreReader: registry seam is required (not null)');
41
+ if (typeof seams.loadOriginRecords !== 'function')
42
+ throw new Error('ReplicatedStoreReader: loadOriginRecords seam must be a function (not a no-op)');
43
+ if (typeof seams.listRecordKeys !== 'function')
44
+ throw new Error('ReplicatedStoreReader: listRecordKeys seam must be a function (not a no-op)');
45
+ if (!seams.droppedOrigins)
46
+ throw new Error('ReplicatedStoreReader: droppedOrigins seam is required (not null)');
47
+ if (!seams.conflictStore)
48
+ throw new Error('ReplicatedStoreReader: conflictStore seam is required (not null)');
49
+ if (typeof seams.tierOf !== 'function')
50
+ throw new Error('ReplicatedStoreReader: tierOf seam must be a function');
51
+ this.seams = seams;
52
+ }
53
+ /** Is this store registered + enabled (so a read is non-dark)? A read on an
54
+ * unregistered or disabled store returns a strict no-op (no record). */
55
+ isLive(store) {
56
+ if (!this.seams.registry.getByStore(store))
57
+ return false;
58
+ return this.seams.stores?.[store]?.enabled === true;
59
+ }
60
+ /**
61
+ * Read one recordKey through the union (§7.2). Returns the no-clobber union
62
+ * result: the resolved value, a conflict descriptor (HIGH-impact concurrent),
63
+ * or a divergence flag (LOW-impact concurrent). A HIGH-impact conflict is
64
+ * RECORDED in the conflict ledger (idempotent) as a side-effect so it is
65
+ * surfaced for operator resolution — the read NEVER clobbers a divergent record.
66
+ *
67
+ * Live-recompute (§7.4): the dropped-origin set is consulted HERE, so a
68
+ * rolled-back origin is excluded from the union instantly with no rewrite.
69
+ */
70
+ read(store, recordKey) {
71
+ if (!this.isLive(store)) {
72
+ return { recordKey, value: null, conflict: null, divergenceFlag: false };
73
+ }
74
+ const dropped = this.seams.droppedOrigins.droppedOrigins(store);
75
+ const all = this.seams.loadOriginRecords(store, recordKey);
76
+ // §7.4 LIVE exclusion: drop any rolled-back origin from the participating set.
77
+ const participating = dropped.size === 0 ? all : all.filter((r) => !dropped.has(r.origin));
78
+ const result = readUnion(recordKey, participating, this.seams.tierOf(store));
79
+ if (result.conflict) {
80
+ // Record the HIGH-impact conflict (idempotent on its stable id; raises ONE
81
+ // deduped attention item). The read returns value=null (neither version
82
+ // clobbers) — the operator resolves it.
83
+ this.seams.conflictStore.recordConflict(store, result.conflict);
84
+ }
85
+ return result;
86
+ }
87
+ /** Read every recordKey the store holds, as a recordKey → UnionResult map (§7.2).
88
+ * Conflicts are recorded as a side-effect (same as read). Bounded by the live
89
+ * key count. */
90
+ readAll(store) {
91
+ const out = new Map();
92
+ if (!this.isLive(store))
93
+ return out;
94
+ for (const recordKey of this.seams.listRecordKeys(store)) {
95
+ out.set(recordKey, this.read(store, recordKey));
96
+ }
97
+ return out;
98
+ }
99
+ }
100
+ //# sourceMappingURL=ReplicatedStoreReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReplicatedStoreReader.js","sourceRoot":"","sources":["../../src/core/ReplicatedStoreReader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ReplicationBudget — Component 7 bounds (WS2 replicated-store foundation, §8 +
3
+ * §8.1 Phase C).
4
+ *
5
+ * Spec: docs/specs/multi-machine-replicated-store-foundation.md §8 (per-store /
6
+ * per-record-class bounds, replication rate cap with COALESCING, the AGGREGATE
7
+ * journal budget that caps TOTAL bytes/sec across ALL kinds with proportional
8
+ * fair-share throttle, the sustained-failure breaker, the dark-peer accumulation
9
+ * bound + tombstone horizon forced full-snapshot re-join), §8.1 (Phase C: budgets
10
+ * = per-peer allowance × live online-peer count, hard pool-wide ceiling,
11
+ * hysteresis on the multiplier — NOT a 2-machine constant).
12
+ *
13
+ * PURE-ish: the clock is injected so the token buckets + hysteresis are
14
+ * unit-testable across simulated windows. No fs, no Date directly, no network.
15
+ *
16
+ * The ANTI-STARVATION invariant (§8): a flood on kind A must NOT consume kind B's
17
+ * replication share. The aggregate budget caps total bytes/sec; when pressured,
18
+ * per-kind caps throttle PROPORTIONALLY (fair-share) and the throttle is SURFACED
19
+ * in degradation (never a silent stall). A test enforces the budget ACROSS kinds.
20
+ */
21
+ import { type HlcTimestamp } from './HybridLogicalClock.js';
22
+ import type { KindRetention, RateCapConfig } from './CoherenceJournal.js';
23
+ /**
24
+ * The per-replicated-kind bounds a concrete store (WS2.1+) ships when it
25
+ * registers its kind (§8): a `retention` window (maxFileBytes + rotateKeep) AND a
26
+ * token-bucket rate cap. The foundation provides the SHAPE + conservative
27
+ * defaults; a store overrides per kind. These extend the journal's existing
28
+ * DEFAULT_RETENTION / DEFAULT_RATE_CAP machinery — same types, applied to the
29
+ * replicated kind's own stream.
30
+ */
31
+ export interface ReplicatedKindBounds {
32
+ retention: KindRetention;
33
+ rateCap: RateCapConfig;
34
+ }
35
+ /** Conservative default bounds for a replicated kind that does not override them.
36
+ * A high-write store (preferences) should ship a tighter window; this is the
37
+ * bounded fallback so a kind is NEVER unbounded. */
38
+ export declare const DEFAULT_REPLICATED_KIND_BOUNDS: ReplicatedKindBounds;
39
+ /** One coalesced pending replication: the LATEST record for a recordKey within
40
+ * the current interval (a burst on one key collapses to one record). */
41
+ export interface CoalescedRecord {
42
+ recordKey: string;
43
+ /** The opaque serialized record line (the journal entry to replicate). */
44
+ line: string;
45
+ /** Bytes the line would consume (for the budget accounting). */
46
+ bytes: number;
47
+ /** The HLC of the latest record (for last-wins coalescing within the interval). */
48
+ hlc: HlcTimestamp;
49
+ }
50
+ /**
51
+ * CoalescingReplicator — collapses a burst of edits to one recordKey into ONE
52
+ * replicated record per interval (§8 coalescing). `stage(recordKey, line, bytes,
53
+ * hlc)` keeps only the HLC-LATEST per key; `drain()` emits the coalesced set and
54
+ * clears the buffer for the next interval. Per replicated kind (one instance per
55
+ * kind), so a chatty kind cannot starve another — the aggregate budget below is
56
+ * the cross-kind guard.
57
+ */
58
+ export declare class CoalescingReplicator {
59
+ private pending;
60
+ /** Stage a write; keeps only the HLC-latest per recordKey within the interval. */
61
+ stage(recordKey: string, line: string, bytes: number, hlc: HlcTimestamp): void;
62
+ /** The number of distinct keys currently coalesced (for observability). */
63
+ get pendingCount(): number;
64
+ /** The total bytes the coalesced set would replicate this interval. */
65
+ pendingBytes(): number;
66
+ /** Emit + clear the coalesced set (the LATEST per key). */
67
+ drain(): CoalescedRecord[];
68
+ }
69
+ /** Phase-C budget config (§8.1). */
70
+ export interface PhaseCBudgetConfig {
71
+ /** Per-peer byte allowance (the unit scaled by online-peer count). */
72
+ perPeerBytes: number;
73
+ /** Hard pool-wide ceiling — the scaled budget is clamped here (protects a single
74
+ * machine's disk/CPU regardless of pool size). */
75
+ hardCeilingBytes: number;
76
+ /** Hysteresis: the effective online-peer MULTIPLIER only RISES after the higher
77
+ * peer count has been observed for `hysteresisRiseMs`; it falls immediately
78
+ * (shrinking the budget on a real peer loss is the safe direction). A transient
79
+ * peer-count SPIKE does not instantly widen the budget. */
80
+ hysteresisRiseMs: number;
81
+ }
82
+ /**
83
+ * Compute the Phase-C aggregate budget (§8.1): `min(perPeerBytes ×
84
+ * effectivePeerCount, hardCeilingBytes)`. NOT a 2-machine constant — a 5-machine
85
+ * pool gets a proportionally larger budget, bounded by the hard ceiling.
86
+ *
87
+ * The `effectivePeerCount` is the HYSTERESIS-damped online count (computed by
88
+ * PhaseCBudgetController below); this pure helper just applies the formula so it
89
+ * is independently testable.
90
+ */
91
+ export declare function phaseCBudget(perPeerBytes: number, effectivePeerCount: number, hardCeilingBytes: number): number;
92
+ /**
93
+ * PhaseCBudgetController — tracks the live online-peer count with hysteresis on
94
+ * the multiplier (§8.1). A RISE in peer count only takes effect after it has held
95
+ * for `hysteresisRiseMs` (a transient spike does not widen the budget); a FALL
96
+ * takes effect immediately (shrink-on-loss is safe). `currentBudget()` returns the
97
+ * live aggregate byte budget.
98
+ */
99
+ export declare class PhaseCBudgetController {
100
+ private readonly cfg;
101
+ private readonly now;
102
+ private effectivePeerCount;
103
+ /** Seeded on the first observation — there is no prior narrower budget to
104
+ * protect at bootstrap, so the first count takes effect immediately; only a
105
+ * SUBSEQUENT rise is hysteresis-gated. */
106
+ private seeded;
107
+ /** A pending higher count + when it was first observed (for the rise hysteresis). */
108
+ private pendingRise;
109
+ constructor(cfg: PhaseCBudgetConfig, now: () => number);
110
+ /** Feed the live online-peer count (from the capacity heartbeat). Applies the
111
+ * rise-hysteresis / immediate-fall policy and returns the effective count. */
112
+ observePeerCount(online: number): number;
113
+ /** The current effective (hysteresis-damped) online-peer count. */
114
+ getEffectivePeerCount(): number;
115
+ /** The current aggregate byte budget (§8.1). */
116
+ currentBudget(): number;
117
+ }
118
+ /** A per-kind throttle verdict (§8). When the aggregate budget is pressured, each
119
+ * kind's allowance is its FAIR SHARE of the budget; an over-share request is
120
+ * throttled (deferred to the next interval) and the throttle is surfaced. */
121
+ export interface AggregateThrottleResult {
122
+ /** The bytes admitted this interval for the kind. */
123
+ admittedBytes: number;
124
+ /** The bytes deferred (throttled) — coalesced to the next interval, never dropped. */
125
+ throttledBytes: number;
126
+ /** True iff this kind was throttled (surfaced in degradation; never silent). */
127
+ throttled: boolean;
128
+ }
129
+ /** Degradation surface for the aggregate budget (§8 — never a silent stall). */
130
+ export interface AggregateBudgetDegradation {
131
+ /** Intervals in which at least one kind was throttled. */
132
+ throttledIntervals: number;
133
+ /** Total bytes deferred across all kinds (cumulative). */
134
+ totalThrottledBytes: number;
135
+ /** Per-kind cumulative throttled bytes (so a starving kind is visible). */
136
+ perKindThrottledBytes: Record<string, number>;
137
+ }
138
+ /**
139
+ * AggregateJournalBudget — the cross-kind anti-starvation guard (§8). Each
140
+ * interval, the controller admits each kind's pending bytes up to its FAIR SHARE
141
+ * of the live aggregate budget; an over-share request is throttled (the surplus
142
+ * defers to the next interval, never dropped — coalescing already collapsed it to
143
+ * the latest state). The fair share is `budget / activeKindCount`, with UNUSED
144
+ * share from quiet kinds redistributed to the others (so a single chatty kind on
145
+ * an otherwise-idle pool still gets the whole budget — fair, not wasteful), but
146
+ * NEVER beyond a kind's own pending demand and never letting one kind exceed the
147
+ * aggregate while another has unserved demand.
148
+ *
149
+ * The §12 #9/#15 invariant: a flood on kind A is capped at its fair share when
150
+ * kind B also has demand — B keeps its share; A's surplus is deferred + surfaced.
151
+ */
152
+ export declare class AggregateJournalBudget {
153
+ private readonly degradation;
154
+ /**
155
+ * Allocate the live aggregate `budgetBytes` across the kinds' pending demands
156
+ * for ONE interval. Returns per-kind throttle verdicts. Fair-share with
157
+ * redistribution of unused share, bounded by each kind's demand and the
158
+ * aggregate.
159
+ */
160
+ allocate(budgetBytes: number, demand: Record<string, number>): Record<string, AggregateThrottleResult>;
161
+ /** The cumulative degradation surface (§8 — surfaced, never a silent stall). */
162
+ getDegradation(): AggregateBudgetDegradation;
163
+ }
164
+ /** The re-join verdict for a recovering dark peer (§8). */
165
+ export type RejoinVerdict = {
166
+ mode: 'tail';
167
+ fromSeq: number;
168
+ } | {
169
+ mode: 'full-snapshot';
170
+ reason: 'beyond-tombstone-horizon' | 'below-oldest-retained';
171
+ };
172
+ /**
173
+ * Decide how a recovering peer re-joins a kind's stream (§8 dark-peer accumulation
174
+ * bound + tombstone horizon). If the peer's last-held seq is below the holder's
175
+ * `oldestRetainedSeq` (the tombstone for a delete may have rotated out), a stale
176
+ * tail would risk delete-resurrection — so the peer is FORCED to a full
177
+ * single-origin snapshot re-join (§6.5 delete-resurrection guard). Otherwise a
178
+ * normal seq tail from `lastHeldSeq` is safe.
179
+ *
180
+ * PURE: a function of the seqs — independently testable (§12 #13).
181
+ */
182
+ export declare function rejoinVerdict(lastHeldSeq: number, oldestRetainedSeq: number): RejoinVerdict;
183
+ //# sourceMappingURL=ReplicationBudget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReplicationBudget.d.ts","sourceRoot":"","sources":["../../src/core/ReplicationBudget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,253 @@
1
+ /**
2
+ * ReplicationBudget — Component 7 bounds (WS2 replicated-store foundation, §8 +
3
+ * §8.1 Phase C).
4
+ *
5
+ * Spec: docs/specs/multi-machine-replicated-store-foundation.md §8 (per-store /
6
+ * per-record-class bounds, replication rate cap with COALESCING, the AGGREGATE
7
+ * journal budget that caps TOTAL bytes/sec across ALL kinds with proportional
8
+ * fair-share throttle, the sustained-failure breaker, the dark-peer accumulation
9
+ * bound + tombstone horizon forced full-snapshot re-join), §8.1 (Phase C: budgets
10
+ * = per-peer allowance × live online-peer count, hard pool-wide ceiling,
11
+ * hysteresis on the multiplier — NOT a 2-machine constant).
12
+ *
13
+ * PURE-ish: the clock is injected so the token buckets + hysteresis are
14
+ * unit-testable across simulated windows. No fs, no Date directly, no network.
15
+ *
16
+ * The ANTI-STARVATION invariant (§8): a flood on kind A must NOT consume kind B's
17
+ * replication share. The aggregate budget caps total bytes/sec; when pressured,
18
+ * per-kind caps throttle PROPORTIONALLY (fair-share) and the throttle is SURFACED
19
+ * in degradation (never a silent stall). A test enforces the budget ACROSS kinds.
20
+ */
21
+ import { HybridLogicalClock } from './HybridLogicalClock.js';
22
+ /** Conservative default bounds for a replicated kind that does not override them.
23
+ * A high-write store (preferences) should ship a tighter window; this is the
24
+ * bounded fallback so a kind is NEVER unbounded. */
25
+ export const DEFAULT_REPLICATED_KIND_BOUNDS = {
26
+ retention: { maxFileBytes: 8 * 1024 * 1024, rotateKeep: 4 },
27
+ rateCap: { capacity: 100, refillPerSec: 50 },
28
+ };
29
+ /**
30
+ * CoalescingReplicator — collapses a burst of edits to one recordKey into ONE
31
+ * replicated record per interval (§8 coalescing). `stage(recordKey, line, bytes,
32
+ * hlc)` keeps only the HLC-LATEST per key; `drain()` emits the coalesced set and
33
+ * clears the buffer for the next interval. Per replicated kind (one instance per
34
+ * kind), so a chatty kind cannot starve another — the aggregate budget below is
35
+ * the cross-kind guard.
36
+ */
37
+ export class CoalescingReplicator {
38
+ pending = new Map();
39
+ /** Stage a write; keeps only the HLC-latest per recordKey within the interval. */
40
+ stage(recordKey, line, bytes, hlc) {
41
+ const prev = this.pending.get(recordKey);
42
+ if (prev && HybridLogicalClock.compare(hlc, prev.hlc) <= 0)
43
+ return; // older or equal — keep the newer.
44
+ this.pending.set(recordKey, { recordKey, line, bytes, hlc });
45
+ }
46
+ /** The number of distinct keys currently coalesced (for observability). */
47
+ get pendingCount() {
48
+ return this.pending.size;
49
+ }
50
+ /** The total bytes the coalesced set would replicate this interval. */
51
+ pendingBytes() {
52
+ let n = 0;
53
+ for (const r of this.pending.values())
54
+ n += r.bytes;
55
+ return n;
56
+ }
57
+ /** Emit + clear the coalesced set (the LATEST per key). */
58
+ drain() {
59
+ const out = [...this.pending.values()];
60
+ this.pending.clear();
61
+ return out;
62
+ }
63
+ }
64
+ /**
65
+ * Compute the Phase-C aggregate budget (§8.1): `min(perPeerBytes ×
66
+ * effectivePeerCount, hardCeilingBytes)`. NOT a 2-machine constant — a 5-machine
67
+ * pool gets a proportionally larger budget, bounded by the hard ceiling.
68
+ *
69
+ * The `effectivePeerCount` is the HYSTERESIS-damped online count (computed by
70
+ * PhaseCBudgetController below); this pure helper just applies the formula so it
71
+ * is independently testable.
72
+ */
73
+ export function phaseCBudget(perPeerBytes, effectivePeerCount, hardCeilingBytes) {
74
+ const scaled = perPeerBytes * Math.max(0, effectivePeerCount);
75
+ return Math.min(scaled, hardCeilingBytes);
76
+ }
77
+ /**
78
+ * PhaseCBudgetController — tracks the live online-peer count with hysteresis on
79
+ * the multiplier (§8.1). A RISE in peer count only takes effect after it has held
80
+ * for `hysteresisRiseMs` (a transient spike does not widen the budget); a FALL
81
+ * takes effect immediately (shrink-on-loss is safe). `currentBudget()` returns the
82
+ * live aggregate byte budget.
83
+ */
84
+ export class PhaseCBudgetController {
85
+ cfg;
86
+ now;
87
+ effectivePeerCount = 0;
88
+ /** Seeded on the first observation — there is no prior narrower budget to
89
+ * protect at bootstrap, so the first count takes effect immediately; only a
90
+ * SUBSEQUENT rise is hysteresis-gated. */
91
+ seeded = false;
92
+ /** A pending higher count + when it was first observed (for the rise hysteresis). */
93
+ pendingRise = null;
94
+ constructor(cfg, now) {
95
+ this.cfg = cfg;
96
+ this.now = now;
97
+ }
98
+ /** Feed the live online-peer count (from the capacity heartbeat). Applies the
99
+ * rise-hysteresis / immediate-fall policy and returns the effective count. */
100
+ observePeerCount(online) {
101
+ const t = this.now();
102
+ // First observation seeds the baseline immediately (no prior budget to widen).
103
+ if (!this.seeded) {
104
+ this.seeded = true;
105
+ this.effectivePeerCount = online;
106
+ return this.effectivePeerCount;
107
+ }
108
+ if (online < this.effectivePeerCount) {
109
+ // Immediate fall — shrinking the budget on a real peer loss is safe.
110
+ this.effectivePeerCount = online;
111
+ this.pendingRise = null;
112
+ return this.effectivePeerCount;
113
+ }
114
+ if (online === this.effectivePeerCount) {
115
+ this.pendingRise = null;
116
+ return this.effectivePeerCount;
117
+ }
118
+ // online > effective — gate the rise behind hysteresis.
119
+ if (!this.pendingRise || this.pendingRise.count !== online) {
120
+ this.pendingRise = { count: online, since: t };
121
+ return this.effectivePeerCount;
122
+ }
123
+ if (t - this.pendingRise.since >= this.cfg.hysteresisRiseMs) {
124
+ this.effectivePeerCount = online;
125
+ this.pendingRise = null;
126
+ }
127
+ return this.effectivePeerCount;
128
+ }
129
+ /** The current effective (hysteresis-damped) online-peer count. */
130
+ getEffectivePeerCount() {
131
+ return this.effectivePeerCount;
132
+ }
133
+ /** The current aggregate byte budget (§8.1). */
134
+ currentBudget() {
135
+ return phaseCBudget(this.cfg.perPeerBytes, this.effectivePeerCount, this.cfg.hardCeilingBytes);
136
+ }
137
+ }
138
+ /**
139
+ * AggregateJournalBudget — the cross-kind anti-starvation guard (§8). Each
140
+ * interval, the controller admits each kind's pending bytes up to its FAIR SHARE
141
+ * of the live aggregate budget; an over-share request is throttled (the surplus
142
+ * defers to the next interval, never dropped — coalescing already collapsed it to
143
+ * the latest state). The fair share is `budget / activeKindCount`, with UNUSED
144
+ * share from quiet kinds redistributed to the others (so a single chatty kind on
145
+ * an otherwise-idle pool still gets the whole budget — fair, not wasteful), but
146
+ * NEVER beyond a kind's own pending demand and never letting one kind exceed the
147
+ * aggregate while another has unserved demand.
148
+ *
149
+ * The §12 #9/#15 invariant: a flood on kind A is capped at its fair share when
150
+ * kind B also has demand — B keeps its share; A's surplus is deferred + surfaced.
151
+ */
152
+ export class AggregateJournalBudget {
153
+ degradation = {
154
+ throttledIntervals: 0,
155
+ totalThrottledBytes: 0,
156
+ perKindThrottledBytes: {},
157
+ };
158
+ /**
159
+ * Allocate the live aggregate `budgetBytes` across the kinds' pending demands
160
+ * for ONE interval. Returns per-kind throttle verdicts. Fair-share with
161
+ * redistribution of unused share, bounded by each kind's demand and the
162
+ * aggregate.
163
+ */
164
+ allocate(budgetBytes, demand) {
165
+ const kinds = Object.keys(demand).filter((k) => demand[k] > 0);
166
+ const out = {};
167
+ for (const k of Object.keys(demand)) {
168
+ out[k] = { admittedBytes: 0, throttledBytes: 0, throttled: false };
169
+ }
170
+ if (kinds.length === 0 || budgetBytes <= 0) {
171
+ // Everything is throttled if there is demand but no budget.
172
+ let anyThrottled = false;
173
+ for (const k of kinds) {
174
+ out[k] = { admittedBytes: 0, throttledBytes: demand[k], throttled: true };
175
+ this.degradation.perKindThrottledBytes[k] = (this.degradation.perKindThrottledBytes[k] ?? 0) + demand[k];
176
+ this.degradation.totalThrottledBytes += demand[k];
177
+ anyThrottled = true;
178
+ }
179
+ if (anyThrottled)
180
+ this.degradation.throttledIntervals++;
181
+ return out;
182
+ }
183
+ // Iterative fair-share with redistribution: repeatedly hand each
184
+ // not-yet-satisfied kind an EQUAL slice of the remaining budget, capping at its
185
+ // remaining demand; surplus from a kind that fit under its slice spills to the
186
+ // others. Bounded by kinds.length iterations (each pass satisfies ≥1 kind or
187
+ // exhausts the budget). This guarantees no kind exceeds the aggregate while
188
+ // another has unserved demand (the anti-starvation invariant).
189
+ let remainingBudget = budgetBytes;
190
+ const remainingDemand = new Map(kinds.map((k) => [k, demand[k]]));
191
+ let active = [...kinds];
192
+ let guard = active.length + 1;
193
+ while (active.length > 0 && remainingBudget > 0 && guard-- > 0) {
194
+ const share = remainingBudget / active.length;
195
+ const stillActive = [];
196
+ for (const k of active) {
197
+ const want = remainingDemand.get(k);
198
+ const grant = Math.min(want, share);
199
+ out[k].admittedBytes += grant;
200
+ remainingDemand.set(k, want - grant);
201
+ remainingBudget -= grant;
202
+ if (want - grant > 0)
203
+ stillActive.push(k); // still has demand under its share.
204
+ }
205
+ // If no progress was possible (every active kind already at its share), stop.
206
+ if (stillActive.length === active.length && share <= 0)
207
+ break;
208
+ active = stillActive;
209
+ }
210
+ let anyThrottled = false;
211
+ for (const k of kinds) {
212
+ const left = remainingDemand.get(k) ?? 0;
213
+ if (left > 0) {
214
+ out[k].throttledBytes = left;
215
+ out[k].throttled = true;
216
+ this.degradation.perKindThrottledBytes[k] = (this.degradation.perKindThrottledBytes[k] ?? 0) + left;
217
+ this.degradation.totalThrottledBytes += left;
218
+ anyThrottled = true;
219
+ }
220
+ }
221
+ if (anyThrottled)
222
+ this.degradation.throttledIntervals++;
223
+ return out;
224
+ }
225
+ /** The cumulative degradation surface (§8 — surfaced, never a silent stall). */
226
+ getDegradation() {
227
+ return {
228
+ throttledIntervals: this.degradation.throttledIntervals,
229
+ totalThrottledBytes: this.degradation.totalThrottledBytes,
230
+ perKindThrottledBytes: { ...this.degradation.perKindThrottledBytes },
231
+ };
232
+ }
233
+ }
234
+ /**
235
+ * Decide how a recovering peer re-joins a kind's stream (§8 dark-peer accumulation
236
+ * bound + tombstone horizon). If the peer's last-held seq is below the holder's
237
+ * `oldestRetainedSeq` (the tombstone for a delete may have rotated out), a stale
238
+ * tail would risk delete-resurrection — so the peer is FORCED to a full
239
+ * single-origin snapshot re-join (§6.5 delete-resurrection guard). Otherwise a
240
+ * normal seq tail from `lastHeldSeq` is safe.
241
+ *
242
+ * PURE: a function of the seqs — independently testable (§12 #13).
243
+ */
244
+ export function rejoinVerdict(lastHeldSeq, oldestRetainedSeq) {
245
+ // The peer's next-needed seq has rotated out of the holder's retained window ⇒
246
+ // a tail would skip the gap (incl. possibly a delete tombstone) ⇒ force a full
247
+ // snapshot re-join (the delete-resurrection guard).
248
+ if (lastHeldSeq + 1 < oldestRetainedSeq) {
249
+ return { mode: 'full-snapshot', reason: 'below-oldest-retained' };
250
+ }
251
+ return { mode: 'tail', fromSeq: lastHeldSeq };
252
+ }
253
+ //# sourceMappingURL=ReplicationBudget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReplicationBudget.js","sourceRoot":"","sources":["../../src/core/ReplicationBudget.ts"],"names":[],"mappings":"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"}