instar 1.3.91 → 1.3.92

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 (141) hide show
  1. package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +17 -0
  2. package/dashboard/index.html +108 -0
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +207 -1
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +30 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/config/LiveConfig.d.ts +3 -1
  10. package/dist/config/LiveConfig.d.ts.map +1 -1
  11. package/dist/config/LiveConfig.js +4 -1
  12. package/dist/config/LiveConfig.js.map +1 -1
  13. package/dist/config/stageWriteGuard.d.ts +23 -0
  14. package/dist/config/stageWriteGuard.d.ts.map +1 -0
  15. package/dist/config/stageWriteGuard.js +33 -0
  16. package/dist/config/stageWriteGuard.js.map +1 -0
  17. package/dist/core/DeliverMessageHandler.d.ts +33 -0
  18. package/dist/core/DeliverMessageHandler.d.ts.map +1 -0
  19. package/dist/core/DeliverMessageHandler.js +34 -0
  20. package/dist/core/DeliverMessageHandler.js.map +1 -0
  21. package/dist/core/LeaseCoordinator.d.ts +39 -2
  22. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  23. package/dist/core/LeaseCoordinator.js +45 -9
  24. package/dist/core/LeaseCoordinator.js.map +1 -1
  25. package/dist/core/MachineIdentity.d.ts +28 -1
  26. package/dist/core/MachineIdentity.d.ts.map +1 -1
  27. package/dist/core/MachineIdentity.js +81 -1
  28. package/dist/core/MachineIdentity.js.map +1 -1
  29. package/dist/core/MachineLoadBroker.d.ts +94 -0
  30. package/dist/core/MachineLoadBroker.d.ts.map +1 -0
  31. package/dist/core/MachineLoadBroker.js +87 -0
  32. package/dist/core/MachineLoadBroker.js.map +1 -0
  33. package/dist/core/MachinePoolRegistry.d.ts +94 -0
  34. package/dist/core/MachinePoolRegistry.d.ts.map +1 -0
  35. package/dist/core/MachinePoolRegistry.js +150 -0
  36. package/dist/core/MachinePoolRegistry.js.map +1 -0
  37. package/dist/core/MeshRpc.d.ts +165 -0
  38. package/dist/core/MeshRpc.d.ts.map +1 -0
  39. package/dist/core/MeshRpc.js +156 -0
  40. package/dist/core/MeshRpc.js.map +1 -0
  41. package/dist/core/MeshRpcClient.d.ts +59 -0
  42. package/dist/core/MeshRpcClient.d.ts.map +1 -0
  43. package/dist/core/MeshRpcClient.js +58 -0
  44. package/dist/core/MeshRpcClient.js.map +1 -0
  45. package/dist/core/MultiMachineCoordinator.d.ts +11 -0
  46. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  47. package/dist/core/MultiMachineCoordinator.js +13 -0
  48. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  49. package/dist/core/NicknameAssigner.d.ts +51 -0
  50. package/dist/core/NicknameAssigner.d.ts.map +1 -0
  51. package/dist/core/NicknameAssigner.js +91 -0
  52. package/dist/core/NicknameAssigner.js.map +1 -0
  53. package/dist/core/NicknameCommand.d.ts +28 -0
  54. package/dist/core/NicknameCommand.d.ts.map +1 -0
  55. package/dist/core/NicknameCommand.js +69 -0
  56. package/dist/core/NicknameCommand.js.map +1 -0
  57. package/dist/core/PlacementExecutor.d.ts +71 -0
  58. package/dist/core/PlacementExecutor.d.ts.map +1 -0
  59. package/dist/core/PlacementExecutor.js +156 -0
  60. package/dist/core/PlacementExecutor.js.map +1 -0
  61. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  62. package/dist/core/PostUpdateMigrator.js +21 -0
  63. package/dist/core/PostUpdateMigrator.js.map +1 -1
  64. package/dist/core/RebalancePlanner.d.ts +48 -0
  65. package/dist/core/RebalancePlanner.d.ts.map +1 -0
  66. package/dist/core/RebalancePlanner.js +71 -0
  67. package/dist/core/RebalancePlanner.js.map +1 -0
  68. package/dist/core/SessionOwnership.d.ts +103 -0
  69. package/dist/core/SessionOwnership.d.ts.map +1 -0
  70. package/dist/core/SessionOwnership.js +0 -0
  71. package/dist/core/SessionOwnership.js.map +1 -0
  72. package/dist/core/SessionOwnershipRegistry.d.ts +115 -0
  73. package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -0
  74. package/dist/core/SessionOwnershipRegistry.js +128 -0
  75. package/dist/core/SessionOwnershipRegistry.js.map +1 -0
  76. package/dist/core/SessionPoolE2EResultStore.d.ts +45 -0
  77. package/dist/core/SessionPoolE2EResultStore.d.ts.map +1 -0
  78. package/dist/core/SessionPoolE2EResultStore.js +66 -0
  79. package/dist/core/SessionPoolE2EResultStore.js.map +1 -0
  80. package/dist/core/SessionRouter.d.ts +95 -0
  81. package/dist/core/SessionRouter.d.ts.map +1 -0
  82. package/dist/core/SessionRouter.js +151 -0
  83. package/dist/core/SessionRouter.js.map +1 -0
  84. package/dist/core/StageAdvancer.d.ts +47 -0
  85. package/dist/core/StageAdvancer.d.ts.map +1 -0
  86. package/dist/core/StageAdvancer.js +70 -0
  87. package/dist/core/StageAdvancer.js.map +1 -0
  88. package/dist/core/TransferByNickname.d.ts +51 -0
  89. package/dist/core/TransferByNickname.d.ts.map +1 -0
  90. package/dist/core/TransferByNickname.js +50 -0
  91. package/dist/core/TransferByNickname.js.map +1 -0
  92. package/dist/core/TransferOrchestrator.d.ts +113 -0
  93. package/dist/core/TransferOrchestrator.d.ts.map +1 -0
  94. package/dist/core/TransferOrchestrator.js +79 -0
  95. package/dist/core/TransferOrchestrator.js.map +1 -0
  96. package/dist/core/types.d.ts +177 -1
  97. package/dist/core/types.d.ts.map +1 -1
  98. package/dist/core/types.js.map +1 -1
  99. package/dist/scaffold/templates.d.ts.map +1 -1
  100. package/dist/scaffold/templates.js +5 -0
  101. package/dist/scaffold/templates.js.map +1 -1
  102. package/dist/server/AgentServer.d.ts +8 -0
  103. package/dist/server/AgentServer.d.ts.map +1 -1
  104. package/dist/server/AgentServer.js +4 -0
  105. package/dist/server/AgentServer.js.map +1 -1
  106. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  107. package/dist/server/CapabilityIndex.js +14 -0
  108. package/dist/server/CapabilityIndex.js.map +1 -1
  109. package/dist/server/routes.d.ts +12 -0
  110. package/dist/server/routes.d.ts.map +1 -1
  111. package/dist/server/routes.js +91 -0
  112. package/dist/server/routes.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/data/builtin-manifest.json +64 -64
  115. package/src/scaffold/templates.ts +5 -0
  116. package/upgrades/1.3.92.md +63 -0
  117. package/upgrades/side-effects/session-pool-capability-classification.md +16 -0
  118. package/upgrades/side-effects/session-pool-review-fixes.md +23 -0
  119. package/upgrades/side-effects/session-pool-track-a-router-lease.md +29 -0
  120. package/upgrades/side-effects/session-pool-track-b-agent-awareness.md +29 -0
  121. package/upgrades/side-effects/session-pool-track-b-nicknames.md +35 -0
  122. package/upgrades/side-effects/session-pool-track-b-pool-api.md +36 -0
  123. package/upgrades/side-effects/session-pool-track-b-registry.md +31 -0
  124. package/upgrades/side-effects/session-pool-track-b-wiring.md +39 -0
  125. package/upgrades/side-effects/session-pool-track-c-dispatcher.md +24 -0
  126. package/upgrades/side-effects/session-pool-track-c-meshrpc.md +35 -0
  127. package/upgrades/side-effects/session-pool-track-c-route-wiring.md +31 -0
  128. package/upgrades/side-effects/session-pool-track-d-ownership-fsm.md +31 -0
  129. package/upgrades/side-effects/session-pool-track-d-registry.md +27 -0
  130. package/upgrades/side-effects/session-pool-track-d-wiring.md +30 -0
  131. package/upgrades/side-effects/session-pool-track-e-boot-wiring.md +20 -0
  132. package/upgrades/side-effects/session-pool-track-e-placement-core.md +17 -0
  133. package/upgrades/side-effects/session-pool-track-e-router.md +23 -0
  134. package/upgrades/side-effects/session-pool-track-f-config.md +12 -0
  135. package/upgrades/side-effects/session-pool-track-f-handoff-core.md +17 -0
  136. package/upgrades/side-effects/session-pool-track-g-load-broker.md +16 -0
  137. package/upgrades/side-effects/session-pool-track-h-gate-wiring.md +17 -0
  138. package/upgrades/side-effects/session-pool-track-h-mesh-client.md +15 -0
  139. package/upgrades/side-effects/session-pool-track-h-rebalance.md +13 -0
  140. package/upgrades/side-effects/session-pool-track-h-rollout-gate.md +17 -0
  141. package/upgrades/side-effects/session-pool-track-h-stage-guard.md +16 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * RebalancePlanner — the pure Stage-3 load-rebalance decision (Multi-Machine Session
3
+ * Pool §L4 "Load-rebalance trigger conditions"). Given the machine capacities + the
4
+ * current session→machine assignments, it proposes a BOUNDED set of transfers off
5
+ * over-saturated machines. It is evaluated ONLY on the heartbeat interval by the
6
+ * caller (never per message), and is deliberately conservative to avoid a cascade:
7
+ *
8
+ * - A machine is a rebalance SOURCE only if its session ratio exceeds
9
+ * `rebalanceThresholdPercent` (default 0.85 of maxSessions).
10
+ * - Only NON-pinned, low-priority sessions that are off cool-down
11
+ * (`now - lastTransferredAt > placementCooldownMs`) are movable; a hard pin or a
12
+ * critical session is NEVER proactively rebalanced.
13
+ * - At most ONE move per source per cycle (capped overall by `maxMovesPerCycle`), and
14
+ * a working copy of capacities is updated as moves are proposed so the planner never
15
+ * piles every session onto one target.
16
+ * - The TARGET is chosen by the same `PlacementExecutor.decide(reason:'rebalance')`
17
+ * the router uses — a move is proposed only if it lands on a DIFFERENT, eligible
18
+ * machine. No move ⇒ empty result (rebalance never forces a churn).
19
+ *
20
+ * Pure over its inputs; the caller performs the actual transfers (TransferOrchestrator).
21
+ */
22
+ import type { PlacementExecutor, TopicPlacement } from './PlacementExecutor.js';
23
+ import type { MachineCapacity } from './types.js';
24
+ export interface RebalanceSession {
25
+ sessionKey: string;
26
+ ownerMachineId: string;
27
+ pinned?: boolean;
28
+ lowPriority?: boolean;
29
+ lastTransferredAt?: number;
30
+ topicMetadata?: TopicPlacement;
31
+ }
32
+ export interface RebalanceInput {
33
+ machines: MachineCapacity[];
34
+ sessions: RebalanceSession[];
35
+ rebalanceThresholdPercent: number;
36
+ placementCooldownMs: number;
37
+ now: number;
38
+ /** Hard cap on moves proposed per cycle (default = number of saturated sources). */
39
+ maxMovesPerCycle?: number;
40
+ }
41
+ export interface RebalanceMove {
42
+ sessionKey: string;
43
+ fromMachine: string;
44
+ toMachine: string;
45
+ reason: string;
46
+ }
47
+ export declare function planRebalance(input: RebalanceInput, placement: PlacementExecutor): RebalanceMove[];
48
+ //# sourceMappingURL=RebalancePlanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RebalancePlanner.d.ts","sourceRoot":"","sources":["../../src/core/RebalancePlanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,cAAc,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,iBAAiB,GAAG,aAAa,EAAE,CA+ClG"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * RebalancePlanner — the pure Stage-3 load-rebalance decision (Multi-Machine Session
3
+ * Pool §L4 "Load-rebalance trigger conditions"). Given the machine capacities + the
4
+ * current session→machine assignments, it proposes a BOUNDED set of transfers off
5
+ * over-saturated machines. It is evaluated ONLY on the heartbeat interval by the
6
+ * caller (never per message), and is deliberately conservative to avoid a cascade:
7
+ *
8
+ * - A machine is a rebalance SOURCE only if its session ratio exceeds
9
+ * `rebalanceThresholdPercent` (default 0.85 of maxSessions).
10
+ * - Only NON-pinned, low-priority sessions that are off cool-down
11
+ * (`now - lastTransferredAt > placementCooldownMs`) are movable; a hard pin or a
12
+ * critical session is NEVER proactively rebalanced.
13
+ * - At most ONE move per source per cycle (capped overall by `maxMovesPerCycle`), and
14
+ * a working copy of capacities is updated as moves are proposed so the planner never
15
+ * piles every session onto one target.
16
+ * - The TARGET is chosen by the same `PlacementExecutor.decide(reason:'rebalance')`
17
+ * the router uses — a move is proposed only if it lands on a DIFFERENT, eligible
18
+ * machine. No move ⇒ empty result (rebalance never forces a churn).
19
+ *
20
+ * Pure over its inputs; the caller performs the actual transfers (TransferOrchestrator).
21
+ */
22
+ function sessionRatio(m) {
23
+ return m.maxSessions && m.maxSessions > 0 ? (m.activeSessionCount ?? 0) / m.maxSessions : 0;
24
+ }
25
+ export function planRebalance(input, placement) {
26
+ // Working copy of capacities (mutated as moves are proposed → no pile-on).
27
+ const caps = new Map(input.machines.map((m) => [m.machineId, { ...m }]));
28
+ const threshold = input.rebalanceThresholdPercent;
29
+ const saturated = (id) => {
30
+ const m = caps.get(id);
31
+ return !!m && m.online && sessionRatio(m) > threshold;
32
+ };
33
+ const sources = input.machines.filter((m) => saturated(m.machineId)).sort((a, b) => sessionRatio(caps.get(b.machineId)) - sessionRatio(caps.get(a.machineId)));
34
+ const cap = input.maxMovesPerCycle ?? sources.length;
35
+ const moves = [];
36
+ const movedSessions = new Set();
37
+ for (const src of sources) {
38
+ if (moves.length >= cap)
39
+ break;
40
+ if (!saturated(src.machineId))
41
+ continue; // a prior move may have relieved it
42
+ // Eligible movable sessions on this source.
43
+ const candidates = input.sessions.filter((s) => s.ownerMachineId === src.machineId &&
44
+ !movedSessions.has(s.sessionKey) &&
45
+ !s.pinned && !s.topicMetadata?.pinned &&
46
+ (s.lowPriority ?? true) &&
47
+ input.now - (s.lastTransferredAt ?? 0) > input.placementCooldownMs);
48
+ for (const s of candidates) {
49
+ // One move per source per cycle (cascade guard).
50
+ const decision = placement.decide({
51
+ sessionKey: s.sessionKey,
52
+ topicMetadata: s.topicMetadata ?? {},
53
+ machineRegistry: [...caps.values()],
54
+ currentOwner: undefined, // rebalance ignores stickiness
55
+ reason: 'rebalance',
56
+ });
57
+ if (decision.outcome === 'placed' && decision.chosenMachine && decision.chosenMachine !== src.machineId) {
58
+ moves.push({ sessionKey: s.sessionKey, fromMachine: src.machineId, toMachine: decision.chosenMachine, reason: 'load-rebalance' });
59
+ movedSessions.add(s.sessionKey);
60
+ // Update the working copy: source −1, target +1.
61
+ const from = caps.get(src.machineId);
62
+ const to = caps.get(decision.chosenMachine);
63
+ from.activeSessionCount = Math.max(0, (from.activeSessionCount ?? 0) - 1);
64
+ to.activeSessionCount = (to.activeSessionCount ?? 0) + 1;
65
+ break; // one move per source this cycle
66
+ }
67
+ }
68
+ }
69
+ return moves;
70
+ }
71
+ //# sourceMappingURL=RebalancePlanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RebalancePlanner.js","sourceRoot":"","sources":["../../src/core/RebalancePlanner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SessionOwnership — per-session ownership state machine for the Multi-Machine
3
+ * Session Pool (spec §L3). Exactly one machine runs a session at any instant;
4
+ * ownership is movable and fenced by a (status, epoch) pair, advanced by a
5
+ * per-session CAS at `ownershipEpoch+1`.
6
+ *
7
+ * This module is PURE LOGIC — the legal transitions, the run-fence (who may run
8
+ * the agent), the output-exclusion contract (who may write to the user channel
9
+ * during a transfer), and the per-session nonce scoping. The durable CAS (the
10
+ * per-session git single-ref fast-forward push of §L−1) and the distributed
11
+ * registry sit on top and call these to validate every ownership change.
12
+ *
13
+ * The handoff is **claim-before-release, gated by the state machine** — NOT
14
+ * release-before-claim. Authoritative sequence for a transfer of session K from
15
+ * source S to target T:
16
+ * active(owner=S, e) → transferring(from=S, to=T, e+1) → active(owner=T, e+2) → S tears down (local)
17
+ * Because the fence is (status, epoch) — not the physical release — there is no
18
+ * double-run (only one machine ever sees itself active at the top epoch) and no
19
+ * no-owner gap (transferring still names S as the draining owner until T is active).
20
+ */
21
+ export type SessionOwnershipStatus = 'placing' | 'active' | 'transferring' | 'released';
22
+ export interface SessionOwnershipRecord {
23
+ sessionKey: string;
24
+ ownerMachineId: string;
25
+ ownershipEpoch: number;
26
+ status: SessionOwnershipStatus;
27
+ /** During `transferring`: the target machine the session is moving to. */
28
+ transferTo?: string;
29
+ nonce: string;
30
+ timestamp: number;
31
+ updatedAt: string;
32
+ signature?: string;
33
+ }
34
+ export type OwnershipAction = {
35
+ type: 'place';
36
+ machineId: string;
37
+ } | {
38
+ type: 'claim';
39
+ machineId: string;
40
+ } | {
41
+ type: 'transfer';
42
+ to: string;
43
+ } | {
44
+ type: 'release';
45
+ machineId: string;
46
+ };
47
+ export type OwnershipReason = 'ok' | 'already-placed' | 'claim-out-of-sequence' | 'claim-wrong-machine' | 'transfer-not-active' | 'release-not-owner' | 'release-requires-active' | 'no-record';
48
+ /**
49
+ * Pure transition. Given the CURRENT record (or null for a never-seen session)
50
+ * and an action, returns the next record (the CAS candidate, at epoch+1) or a
51
+ * typed rejection. Epoch arithmetic + legal sequencing are enforced here; the
52
+ * CAS layer then attempts to land `next` via a fast-forward push.
53
+ */
54
+ export declare function applyOwnershipAction(current: SessionOwnershipRecord | null, action: OwnershipAction, ctx: {
55
+ sessionKey: string;
56
+ nonce: string;
57
+ now: number;
58
+ }): {
59
+ ok: true;
60
+ next: SessionOwnershipRecord;
61
+ } | {
62
+ ok: false;
63
+ reason: OwnershipReason;
64
+ };
65
+ /**
66
+ * Run-fence (spec §L3): a worker may RUN the agent for a session ONLY while it
67
+ * observes itself as the `active` owner at the current epoch. During
68
+ * `transferring` NO machine runs new turns (the draining owner finishes via the
69
+ * output-exclusion contract; the target is not yet active).
70
+ */
71
+ export declare function mayRun(record: SessionOwnershipRecord | null, machineId: string, effectiveEpoch: number): boolean;
72
+ export interface EmitDecision {
73
+ /** May this machine write to the user channel at all right now? */
74
+ mayEmit: boolean;
75
+ /** If mayEmit, may it emit NEW output (vs only drain an in-flight reply)? */
76
+ newOutputAllowed: boolean;
77
+ }
78
+ /**
79
+ * Output-exclusion contract (spec §L3) — distinct from the run-fence. During a
80
+ * transfer the draining source and the freshly-active target overlap in time, so
81
+ * channel emission is gated separately so the user never sees interleaved/double
82
+ * output:
83
+ * - The draining source (record `transferring`, from === machine) MAY emit the
84
+ * TAIL of an in-flight reply within [transferringStartedAt, +cutoffMs], but no
85
+ * NEW output, and nothing past the cutoff.
86
+ * - The target (record `active`, owner === machine, after its CAS) emits only
87
+ * once the cutoff has elapsed since `transferringStartedAt` (so the two
88
+ * windows are disjoint by construction).
89
+ * - The steady-state active owner (no transfer in flight) emits freely.
90
+ */
91
+ export declare function mayEmit(record: SessionOwnershipRecord | null, machineId: string, opts: {
92
+ now: number;
93
+ transferringStartedAt?: number;
94
+ cutoffMs?: number;
95
+ }): EmitDecision;
96
+ /**
97
+ * Per-session nonce scoping (spec §L3): the replay-guard key is the tuple
98
+ * {sessionKey, sender, ownershipEpoch} — NOT a global nonce space. The same
99
+ * nonce value used in two different sessions does not collide; a replay WITHIN a
100
+ * session's CAS sequence is caught.
101
+ */
102
+ export declare function ownershipNonceKey(sessionKey: string, sender: string, ownershipEpoch: number, nonce: string): string;
103
+ //# sourceMappingURL=SessionOwnership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionOwnership.d.ts","sourceRoot":"","sources":["../../src/core/SessionOwnership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAC;AAExF,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,MAAM,MAAM,eAAe,GACvB,IAAI,GACJ,gBAAgB,GAChB,uBAAuB,GACvB,qBAAqB,GACrB,qBAAqB,GACrB,mBAAmB,GACnB,yBAAyB,GACzB,WAAW,CAAC;AAEhB;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,GAAG,IAAI,EACtC,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,sBAAsB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAqDrF;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAGhH;AAED,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,sBAAsB,GAAG,IAAI,EACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,YAAY,CAkBd;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnH"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionOwnership.js","sourceRoot":"","sources":["../../src/core/SessionOwnership.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SessionOwnershipRegistry — the distributed per-session ownership registry
3
+ * (Multi-Machine Session Pool §L3). Holds a SessionOwnershipRecord per session;
4
+ * answers "which machine holds session X"; and mutates ownership via a per-session
5
+ * CAS at `ownershipEpoch+1`, reusing the §L−1 single-ref fast-forward discipline
6
+ * (the same pattern as GitLeaseStore/LeaseCoordinator) but at per-session
7
+ * granularity — thousands of independent CAS points, one ref-file per session.
8
+ *
9
+ * The dangerous parts (CAS contention, per-session replay) are driven through
10
+ * injected seams so they are unit-testable with in-memory fakes:
11
+ * - `store` — the durable CAS substrate (git single-ref fast-forward push per
12
+ * session). `casWrite(candidate)` returns ok:true if the candidate landed
13
+ * (fast-forwarded from the current epoch), or ok:false + the freshly-observed
14
+ * record after a reject+reread (a peer won the race). MUST NOT force-push.
15
+ * - `seenNonce`/`recordNonce` — the per-session-scoped replay guard
16
+ * (keyed on {sessionKey, sender, epoch} — see SessionOwnership.ownershipNonceKey).
17
+ *
18
+ * The FSM (legal transitions, run-fence, output-exclusion) lives in
19
+ * SessionOwnership.ts; this module enforces the CAS + replay + retry-backoff +
20
+ * lifecycle around it.
21
+ */
22
+ import { type OwnershipAction, type OwnershipReason, type SessionOwnershipRecord } from './SessionOwnership.js';
23
+ /**
24
+ * In-memory per-session ownership store with fast-forward CAS — correct for a
25
+ * single machine (no cross-machine contention) and for the dark v0.1 state.
26
+ * The CROSS-MACHINE durable store (git single-ref-per-session push, mirroring
27
+ * GitLeaseStore) swaps in for the Track-H real-hardware proof; the registry/FSM/
28
+ * CAS logic above is store-agnostic. A candidate lands only if it fast-forwards
29
+ * from the current epoch (`candidate.ownershipEpoch === current.epoch + 1`).
30
+ */
31
+ export declare class InMemorySessionOwnershipStore {
32
+ private recs;
33
+ read(sessionKey: string): SessionOwnershipRecord | null;
34
+ casWrite(candidate: import('./SessionOwnership.js').SessionOwnershipRecord): {
35
+ ok: boolean;
36
+ observed: SessionOwnershipRecord | null;
37
+ };
38
+ all(): SessionOwnershipRecord[];
39
+ }
40
+ /** Durable per-session CAS substrate (git single-ref fast-forward push). */
41
+ export interface SessionOwnershipStore {
42
+ /** Read the current committed record for a session (null if none). */
43
+ read(sessionKey: string): SessionOwnershipRecord | null;
44
+ /**
45
+ * Attempt to land `candidate` as the new record via a fast-forward push from
46
+ * the current epoch. ok:true if it landed; ok:false + observed (the
47
+ * freshly-reread record) if a peer advanced first (non-fast-forward reject).
48
+ */
49
+ casWrite(candidate: SessionOwnershipRecord): {
50
+ ok: boolean;
51
+ observed: SessionOwnershipRecord | null;
52
+ };
53
+ }
54
+ export interface SessionOwnershipRegistryDeps {
55
+ store: SessionOwnershipStore;
56
+ /** Has this per-session nonce been seen (replay guard)? */
57
+ seenNonce: (key: string) => boolean;
58
+ /** Record a per-session nonce as seen (called only on a successful CAS). */
59
+ recordNonce: (key: string) => void;
60
+ now?: () => number;
61
+ /** Eviction age (ms) for `released` records. Default 86400000 (24h). */
62
+ releasedEvictionMs?: number;
63
+ logger?: (msg: string) => void;
64
+ }
65
+ export type CasResult = {
66
+ ok: true;
67
+ record: SessionOwnershipRecord;
68
+ } | {
69
+ ok: false;
70
+ reason: OwnershipReason | 'replayed-nonce' | 'cas-lost';
71
+ observed: SessionOwnershipRecord | null;
72
+ };
73
+ export declare class SessionOwnershipRegistry {
74
+ private readonly d;
75
+ private casConflicts;
76
+ private casRetryExhaustions;
77
+ constructor(deps: SessionOwnershipRegistryDeps);
78
+ private now;
79
+ read(sessionKey: string): SessionOwnershipRecord | null;
80
+ /** The current owner machine of a session (null if none / released). */
81
+ ownerOf(sessionKey: string): string | null;
82
+ /**
83
+ * The machine the router last assigned for a session (§L4 RBAC `claim` check):
84
+ * the placed-owner while `placing`, or the transfer target while `transferring`.
85
+ */
86
+ placementTargetOf(sessionKey: string): string | null;
87
+ /**
88
+ * Apply an ownership action via CAS (§L3). Runs the FSM transition, the
89
+ * per-session replay check, then the durable fast-forward CAS. On a lost CAS
90
+ * (a peer advanced the epoch first) returns `cas-lost` + the observed record —
91
+ * the caller backs off (ownershipCasRetryBackoffMs) or stands down. The nonce
92
+ * is recorded ONLY on a landed CAS (a rejected attempt never burns a nonce).
93
+ */
94
+ cas(action: OwnershipAction, ctx: {
95
+ sessionKey: string;
96
+ sender: string;
97
+ nonce: string;
98
+ }): CasResult;
99
+ /** Observability counters (spec §L3: /pool ownership.casConflicts/casRetryExhaustions). */
100
+ noteRetryExhaustion(): void;
101
+ metrics(): {
102
+ casConflicts: number;
103
+ casRetryExhaustions: number;
104
+ };
105
+ }
106
+ /**
107
+ * Exponential-jitter retry delay for a lost CAS (spec §L3): `ownershipCasRetryBackoffMs`
108
+ * 50→500ms, biased so the lowest-machineId contender retries first (a client-side
109
+ * ordering hint ONLY — never the CAS arbiter; the remote ref-update decides the winner).
110
+ */
111
+ export declare function ownershipRetryDelayMs(retryCount: number, selfMachineId: string, contenderMachineId: string, opts?: {
112
+ baseMs?: number;
113
+ maxMs?: number;
114
+ }): number;
115
+ //# sourceMappingURL=SessionOwnershipRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionOwnershipRegistry.d.ts","sourceRoot":"","sources":["../../src/core/SessionOwnershipRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;GAOG;AACH,qBAAa,6BAA6B;IACxC,OAAO,CAAC,IAAI,CAA6E;IACzF,IAAI,CAAC,UAAU,EAAE,MAAM;IAGvB,QAAQ,CAAC,SAAS,EAAE,OAAO,uBAAuB,EAAE,sBAAsB;;;;IAS1E,GAAG;CAGJ;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,sBAAsB,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAA;KAAE,CAAC;CACvG;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,qBAAqB,CAAC;IAC7B,2DAA2D;IAC3D,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,4EAA4E;IAC5E,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAA;CAAE,CAAC;AAEpH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA+B;IACjD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,mBAAmB,CAAK;gBAEpB,IAAI,EAAE,4BAA4B;IAG9C,OAAO,CAAC,GAAG;IAIX,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAIvD,wEAAwE;IACxE,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAM1C;;;OAGG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQpD;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAkBnG,2FAA2F;IAC3F,mBAAmB,IAAI,IAAI;IAG3B,OAAO,IAAI;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;CAGjE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,MAAM,CAOR"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SessionOwnershipRegistry — the distributed per-session ownership registry
3
+ * (Multi-Machine Session Pool §L3). Holds a SessionOwnershipRecord per session;
4
+ * answers "which machine holds session X"; and mutates ownership via a per-session
5
+ * CAS at `ownershipEpoch+1`, reusing the §L−1 single-ref fast-forward discipline
6
+ * (the same pattern as GitLeaseStore/LeaseCoordinator) but at per-session
7
+ * granularity — thousands of independent CAS points, one ref-file per session.
8
+ *
9
+ * The dangerous parts (CAS contention, per-session replay) are driven through
10
+ * injected seams so they are unit-testable with in-memory fakes:
11
+ * - `store` — the durable CAS substrate (git single-ref fast-forward push per
12
+ * session). `casWrite(candidate)` returns ok:true if the candidate landed
13
+ * (fast-forwarded from the current epoch), or ok:false + the freshly-observed
14
+ * record after a reject+reread (a peer won the race). MUST NOT force-push.
15
+ * - `seenNonce`/`recordNonce` — the per-session-scoped replay guard
16
+ * (keyed on {sessionKey, sender, epoch} — see SessionOwnership.ownershipNonceKey).
17
+ *
18
+ * The FSM (legal transitions, run-fence, output-exclusion) lives in
19
+ * SessionOwnership.ts; this module enforces the CAS + replay + retry-backoff +
20
+ * lifecycle around it.
21
+ */
22
+ import { applyOwnershipAction, ownershipNonceKey, } from './SessionOwnership.js';
23
+ /**
24
+ * In-memory per-session ownership store with fast-forward CAS — correct for a
25
+ * single machine (no cross-machine contention) and for the dark v0.1 state.
26
+ * The CROSS-MACHINE durable store (git single-ref-per-session push, mirroring
27
+ * GitLeaseStore) swaps in for the Track-H real-hardware proof; the registry/FSM/
28
+ * CAS logic above is store-agnostic. A candidate lands only if it fast-forwards
29
+ * from the current epoch (`candidate.ownershipEpoch === current.epoch + 1`).
30
+ */
31
+ export class InMemorySessionOwnershipStore {
32
+ recs = new Map();
33
+ read(sessionKey) {
34
+ return this.recs.get(sessionKey) ?? null;
35
+ }
36
+ casWrite(candidate) {
37
+ const current = this.recs.get(candidate.sessionKey) ?? null;
38
+ const curEpoch = current?.ownershipEpoch ?? 0;
39
+ if (candidate.ownershipEpoch === curEpoch + 1) {
40
+ this.recs.set(candidate.sessionKey, candidate);
41
+ return { ok: true, observed: candidate };
42
+ }
43
+ return { ok: false, observed: current };
44
+ }
45
+ all() {
46
+ return [...this.recs.values()];
47
+ }
48
+ }
49
+ export class SessionOwnershipRegistry {
50
+ d;
51
+ casConflicts = 0;
52
+ casRetryExhaustions = 0;
53
+ constructor(deps) {
54
+ this.d = deps;
55
+ }
56
+ now() {
57
+ return (this.d.now ?? Date.now)();
58
+ }
59
+ read(sessionKey) {
60
+ return this.d.store.read(sessionKey);
61
+ }
62
+ /** The current owner machine of a session (null if none / released). */
63
+ ownerOf(sessionKey) {
64
+ const r = this.d.store.read(sessionKey);
65
+ if (!r || r.status === 'released')
66
+ return null;
67
+ return r.ownerMachineId;
68
+ }
69
+ /**
70
+ * The machine the router last assigned for a session (§L4 RBAC `claim` check):
71
+ * the placed-owner while `placing`, or the transfer target while `transferring`.
72
+ */
73
+ placementTargetOf(sessionKey) {
74
+ const r = this.d.store.read(sessionKey);
75
+ if (!r)
76
+ return null;
77
+ if (r.status === 'placing')
78
+ return r.ownerMachineId;
79
+ if (r.status === 'transferring')
80
+ return r.transferTo ?? null;
81
+ return null;
82
+ }
83
+ /**
84
+ * Apply an ownership action via CAS (§L3). Runs the FSM transition, the
85
+ * per-session replay check, then the durable fast-forward CAS. On a lost CAS
86
+ * (a peer advanced the epoch first) returns `cas-lost` + the observed record —
87
+ * the caller backs off (ownershipCasRetryBackoffMs) or stands down. The nonce
88
+ * is recorded ONLY on a landed CAS (a rejected attempt never burns a nonce).
89
+ */
90
+ cas(action, ctx) {
91
+ const current = this.d.store.read(ctx.sessionKey);
92
+ const t = applyOwnershipAction(current, action, { sessionKey: ctx.sessionKey, nonce: ctx.nonce, now: this.now() });
93
+ if (!t.ok)
94
+ return { ok: false, reason: t.reason, observed: current };
95
+ const nkey = ownershipNonceKey(ctx.sessionKey, ctx.sender, t.next.ownershipEpoch, ctx.nonce);
96
+ if (this.d.seenNonce(nkey))
97
+ return { ok: false, reason: 'replayed-nonce', observed: current };
98
+ const res = this.d.store.casWrite(t.next);
99
+ if (!res.ok) {
100
+ this.casConflicts++;
101
+ this.d.logger?.(`[ownership] CAS lost for ${ctx.sessionKey} → observed epoch ${res.observed?.ownershipEpoch ?? 0}`);
102
+ return { ok: false, reason: 'cas-lost', observed: res.observed };
103
+ }
104
+ this.d.recordNonce(nkey);
105
+ return { ok: true, record: t.next };
106
+ }
107
+ /** Observability counters (spec §L3: /pool ownership.casConflicts/casRetryExhaustions). */
108
+ noteRetryExhaustion() {
109
+ this.casRetryExhaustions++;
110
+ }
111
+ metrics() {
112
+ return { casConflicts: this.casConflicts, casRetryExhaustions: this.casRetryExhaustions };
113
+ }
114
+ }
115
+ /**
116
+ * Exponential-jitter retry delay for a lost CAS (spec §L3): `ownershipCasRetryBackoffMs`
117
+ * 50→500ms, biased so the lowest-machineId contender retries first (a client-side
118
+ * ordering hint ONLY — never the CAS arbiter; the remote ref-update decides the winner).
119
+ */
120
+ export function ownershipRetryDelayMs(retryCount, selfMachineId, contenderMachineId, opts = {}) {
121
+ const base = opts.baseMs ?? 50;
122
+ const max = opts.maxMs ?? 500;
123
+ const backoff = Math.min(max, base * 2 ** Math.max(0, retryCount - 1));
124
+ // Lowest machineId retries first (shorter delay) — ordering hint, not arbitration.
125
+ const yieldBias = selfMachineId > contenderMachineId ? backoff : Math.floor(backoff / 2);
126
+ return Math.min(max, yieldBias);
127
+ }
128
+ //# sourceMappingURL=SessionOwnershipRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionOwnershipRegistry.js","sourceRoot":"","sources":["../../src/core/SessionOwnershipRegistry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SessionPoolE2EResultStore — the durable, signed, append-only record of each
3
+ * rollout stage's Tier-3 E2E outcome (Multi-Machine Session Pool §Rollout). It is
4
+ * the mechanical evidence the StageAdvancer gates on: a stage does NOT activate
5
+ * until its prior stage's E2E recorded `green` for the current commit. Per
6
+ * "Structure > Willpower" the gate is grounded in this implemented record, not a
7
+ * human's say-so.
8
+ *
9
+ * Append-only + signed (tamper-evident, AuditTrail-style): a reversion or a new run
10
+ * APPENDS a row, NEVER overwrites a prior one, so the full green/red history per
11
+ * commit is preserved. The Tier-3 E2E harness is the ONLY writer (`recordResult`).
12
+ */
13
+ export type StageE2EOutcome = 'green' | 'red';
14
+ export interface StageE2EResult {
15
+ stage: number;
16
+ result: StageE2EOutcome;
17
+ commitSha: string;
18
+ ranAt: string;
19
+ evidenceRef: string;
20
+ signature: string;
21
+ }
22
+ export interface SessionPoolE2EResultStoreDeps {
23
+ /** Absolute path to the append-only results file. */
24
+ filePath: string;
25
+ /** Sign the canonical (signature-excluded) row. Production: HMAC/Ed25519 over a state secret. */
26
+ sign: (canonical: string) => string;
27
+ /** Verify a signature against the canonical row. */
28
+ verifySig: (canonical: string, signature: string) => boolean;
29
+ now?: () => number;
30
+ }
31
+ /** Canonical bytes a row's signature covers — field-ordered, signature EXCLUDED. */
32
+ export declare function canonicalE2ERow(r: Omit<StageE2EResult, 'signature'>): string;
33
+ export declare class SessionPoolE2EResultStore {
34
+ private readonly d;
35
+ constructor(deps: SessionPoolE2EResultStoreDeps);
36
+ /** Append a signed result row. The ONLY writer (called by the Tier-3 E2E harness). */
37
+ recordResult(stage: number, result: StageE2EOutcome, commitSha: string, evidenceRef: string): StageE2EResult;
38
+ /** All rows (oldest→newest). Tolerant of a missing file / a torn trailing line. */
39
+ all(): StageE2EResult[];
40
+ /** The MOST RECENT row for a stage (a later `red` supersedes an earlier `green`). */
41
+ getLatestForStage(stage: number): StageE2EResult | null;
42
+ /** Verify a row's signature (tamper check). */
43
+ verify(row: StageE2EResult): boolean;
44
+ }
45
+ //# sourceMappingURL=SessionPoolE2EResultStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionPoolE2EResultStore.d.ts","sourceRoot":"","sources":["../../src/core/SessionPoolE2EResultStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,oDAAoD;IACpD,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,GAAG,MAAM,CAE5E;AAED,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgC;gBACtC,IAAI,EAAE,6BAA6B;IAI/C,sFAAsF;IACtF,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc;IAS5G,mFAAmF;IACnF,GAAG,IAAI,cAAc,EAAE;IAWvB,qFAAqF;IACrF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAMvD,+CAA+C;IAC/C,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO;CAGrC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * SessionPoolE2EResultStore — the durable, signed, append-only record of each
3
+ * rollout stage's Tier-3 E2E outcome (Multi-Machine Session Pool §Rollout). It is
4
+ * the mechanical evidence the StageAdvancer gates on: a stage does NOT activate
5
+ * until its prior stage's E2E recorded `green` for the current commit. Per
6
+ * "Structure > Willpower" the gate is grounded in this implemented record, not a
7
+ * human's say-so.
8
+ *
9
+ * Append-only + signed (tamper-evident, AuditTrail-style): a reversion or a new run
10
+ * APPENDS a row, NEVER overwrites a prior one, so the full green/red history per
11
+ * commit is preserved. The Tier-3 E2E harness is the ONLY writer (`recordResult`).
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ /** Canonical bytes a row's signature covers — field-ordered, signature EXCLUDED. */
16
+ export function canonicalE2ERow(r) {
17
+ return JSON.stringify([r.stage, r.result, r.commitSha, r.ranAt, r.evidenceRef]);
18
+ }
19
+ export class SessionPoolE2EResultStore {
20
+ d;
21
+ constructor(deps) {
22
+ this.d = deps;
23
+ }
24
+ /** Append a signed result row. The ONLY writer (called by the Tier-3 E2E harness). */
25
+ recordResult(stage, result, commitSha, evidenceRef) {
26
+ const ranAt = new Date(this.d.now ? this.d.now() : Date.now()).toISOString();
27
+ const unsigned = { stage, result, commitSha, ranAt, evidenceRef };
28
+ const row = { ...unsigned, signature: this.d.sign(canonicalE2ERow(unsigned)) };
29
+ fs.mkdirSync(path.dirname(this.d.filePath), { recursive: true });
30
+ fs.appendFileSync(this.d.filePath, JSON.stringify(row) + '\n');
31
+ return row;
32
+ }
33
+ /** All rows (oldest→newest). Tolerant of a missing file / a torn trailing line. */
34
+ all() {
35
+ let content;
36
+ try {
37
+ content = fs.readFileSync(this.d.filePath, 'utf8');
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ const out = [];
43
+ for (const line of content.split('\n')) {
44
+ if (!line.trim())
45
+ continue;
46
+ try {
47
+ out.push(JSON.parse(line));
48
+ }
49
+ catch { /* skip a torn line */ }
50
+ }
51
+ return out;
52
+ }
53
+ /** The MOST RECENT row for a stage (a later `red` supersedes an earlier `green`). */
54
+ getLatestForStage(stage) {
55
+ let latest = null;
56
+ for (const r of this.all())
57
+ if (r.stage === stage)
58
+ latest = r;
59
+ return latest;
60
+ }
61
+ /** Verify a row's signature (tamper check). */
62
+ verify(row) {
63
+ return this.d.verifySig(canonicalE2ERow(row), row.signature);
64
+ }
65
+ }
66
+ //# sourceMappingURL=SessionPoolE2EResultStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionPoolE2EResultStore.js","sourceRoot":"","sources":["../../src/core/SessionPoolE2EResultStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAuB7B,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAAC,CAAoC;IAClE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,OAAO,yBAAyB;IACnB,CAAC,CAAgC;IAClD,YAAY,IAAmC;QAC7C,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;IAChB,CAAC;IAED,sFAAsF;IACtF,YAAY,CAAC,KAAa,EAAE,MAAuB,EAAE,SAAiB,EAAE,WAAmB;QACzF,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7E,MAAM,QAAQ,GAAsC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QACrG,MAAM,GAAG,GAAmB,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC/F,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D,OAAO,GAAG,CAAC;IACb,CAAC;IAED,mFAAmF;IACnF,GAAG;QACD,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,EAAE,CAAC;QAAC,CAAC;QAChF,MAAM,GAAG,GAAqB,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,IAAI,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC;QACxF,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,qFAAqF;IACrF,iBAAiB,CAAC,KAAa;QAC7B,IAAI,MAAM,GAA0B,IAAI,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE;YAAE,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK;gBAAE,MAAM,GAAG,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,+CAA+C;IAC/C,MAAM,CAAC,GAAmB;QACxB,OAAO,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/D,CAAC;CACF"}