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,95 @@
1
+ /**
2
+ * SessionRouter — the L4 inbound-message dispatch engine (Multi-Machine Session
3
+ * Pool §L4 "Message Routing to Owner" + "Ownership CAS at Dispatch"). For every
4
+ * inbound message the router resolves session ownership and dispatches:
5
+ *
6
+ * 1. Owned + owner alive + owner == self → handle locally (no MeshRpc hop).
7
+ * 2. Owned + owner alive + owner != self → forward via deliverMessage over MeshRpc
8
+ * (signed, recipient-bound, idempotent on messageId; ACK before offset advance).
9
+ * 3. Owned + owner DEAD → owner-dead re-placement (re-place + claim).
10
+ * 4. Transient (placing/transferring) → queue (ownership-contention).
11
+ * 5. Unowned (new) → PlacementExecutor.decide() → SYNCHRONOUS
12
+ * CAS-claim → spawn on the winner (or, on CAS loss, forward to the winner / queue).
13
+ *
14
+ * Per-session ordering: messages for ONE sessionKey are dispatched strictly in
15
+ * inbound order, at-most-one-in-flight (a per-session promise chain). Different
16
+ * sessions dispatch concurrently. All I/O is injected so the dispatch logic itself
17
+ * is deterministic + unit-testable; the deliverMessage dep owns its own per-attempt
18
+ * timeout (it throws on timeout) — the router owns the retry/backoff/fallback loop.
19
+ */
20
+ import type { PlacementExecutor, TopicPlacement } from './PlacementExecutor.js';
21
+ import type { MachineCapacity } from './types.js';
22
+ export interface OwnershipView {
23
+ owner: string | null;
24
+ epoch: number;
25
+ status: 'active' | 'placing' | 'transferring' | null;
26
+ target?: string;
27
+ }
28
+ export interface DeliverAck {
29
+ messageId: string;
30
+ accepted: 'queued' | 'duplicate' | 'stale-ownership';
31
+ }
32
+ export interface InboundMessage {
33
+ sessionKey: string;
34
+ messageId: string;
35
+ payload: unknown;
36
+ topicMetadata?: TopicPlacement;
37
+ }
38
+ export type RouteAction = 'handled-locally' | 'forwarded' | 'spawned' | 'queued' | 'duplicate' | 'owner-dead-replaced' | 'placement-blocked';
39
+ export interface RouteOutcome {
40
+ action: RouteAction;
41
+ owner?: string | null;
42
+ detail?: string;
43
+ /** True once the inbound is durably accepted (ledger ACK or local handling) — the
44
+ * caller may advance the platform offset ONLY when this is true (§L4 ACK protocol). */
45
+ acked: boolean;
46
+ }
47
+ export interface SessionRouterConfig {
48
+ deliverMessageMaxRetries: number;
49
+ deliverMessageRetryBackoffStartMs: number;
50
+ deliverMessageRetryBackoffMaxMs: number;
51
+ /** Bound on stale-ownership re-resolution to avoid an unbounded chase. */
52
+ maxReResolveDepth: number;
53
+ }
54
+ export declare const DEFAULT_ROUTER_CONFIG: SessionRouterConfig;
55
+ export interface SessionRouterDeps {
56
+ selfMachineId: string;
57
+ placement: PlacementExecutor;
58
+ machineRegistry: () => MachineCapacity[];
59
+ resolveOwnership: (sessionKey: string) => OwnershipView;
60
+ isMachineAlive: (machineId: string) => boolean;
61
+ /** SYNCHRONOUS per-session CAS-claim (the §L−1 single-ref fast-forward push). */
62
+ casClaimOwnership: (sessionKey: string, machineId: string, expectedEpoch: number) => {
63
+ ok: boolean;
64
+ epoch: number;
65
+ };
66
+ /** ONE deliverMessage attempt over MeshRpc; throws on transport error/timeout. */
67
+ deliverMessage: (target: string, env: {
68
+ sessionKey: string;
69
+ messageId: string;
70
+ payload: unknown;
71
+ ownershipEpoch: number;
72
+ }) => Promise<DeliverAck>;
73
+ /** Router == owner: process the message on this machine. */
74
+ handleLocally: (msg: InboundMessage) => Promise<void>;
75
+ /** Instruct the chosen machine to spawn/resume the session. */
76
+ spawnOnMachine: (machineId: string, msg: InboundMessage) => Promise<void>;
77
+ queueMessage: (msg: InboundMessage, reason: string) => void;
78
+ raiseAttention: (title: string, body: string) => void;
79
+ markOwnerSuspect?: (machineId: string) => void;
80
+ sleep: (ms: number) => Promise<void>;
81
+ log?: (line: string) => void;
82
+ }
83
+ export declare class SessionRouter {
84
+ private readonly deps;
85
+ private readonly cfg;
86
+ private readonly chains;
87
+ constructor(deps: SessionRouterDeps, cfg?: SessionRouterConfig);
88
+ /** Dispatch an inbound message; serialized per-session (in-order, one-in-flight). */
89
+ route(msg: InboundMessage): Promise<RouteOutcome>;
90
+ private backoff;
91
+ private dispatchOne;
92
+ private forwardToOwner;
93
+ private placeAndClaim;
94
+ }
95
+ //# sourceMappingURL=SessionRouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRouter.d.ts","sourceRoot":"","sources":["../../src/core/SessionRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * SessionRouter — the L4 inbound-message dispatch engine (Multi-Machine Session
3
+ * Pool §L4 "Message Routing to Owner" + "Ownership CAS at Dispatch"). For every
4
+ * inbound message the router resolves session ownership and dispatches:
5
+ *
6
+ * 1. Owned + owner alive + owner == self → handle locally (no MeshRpc hop).
7
+ * 2. Owned + owner alive + owner != self → forward via deliverMessage over MeshRpc
8
+ * (signed, recipient-bound, idempotent on messageId; ACK before offset advance).
9
+ * 3. Owned + owner DEAD → owner-dead re-placement (re-place + claim).
10
+ * 4. Transient (placing/transferring) → queue (ownership-contention).
11
+ * 5. Unowned (new) → PlacementExecutor.decide() → SYNCHRONOUS
12
+ * CAS-claim → spawn on the winner (or, on CAS loss, forward to the winner / queue).
13
+ *
14
+ * Per-session ordering: messages for ONE sessionKey are dispatched strictly in
15
+ * inbound order, at-most-one-in-flight (a per-session promise chain). Different
16
+ * sessions dispatch concurrently. All I/O is injected so the dispatch logic itself
17
+ * is deterministic + unit-testable; the deliverMessage dep owns its own per-attempt
18
+ * timeout (it throws on timeout) — the router owns the retry/backoff/fallback loop.
19
+ */
20
+ export const DEFAULT_ROUTER_CONFIG = {
21
+ deliverMessageMaxRetries: 3,
22
+ deliverMessageRetryBackoffStartMs: 250,
23
+ deliverMessageRetryBackoffMaxMs: 2000,
24
+ maxReResolveDepth: 3,
25
+ };
26
+ export class SessionRouter {
27
+ deps;
28
+ cfg;
29
+ chains = new Map();
30
+ constructor(deps, cfg = DEFAULT_ROUTER_CONFIG) {
31
+ this.deps = deps;
32
+ this.cfg = cfg;
33
+ }
34
+ /** Dispatch an inbound message; serialized per-session (in-order, one-in-flight). */
35
+ route(msg) {
36
+ const prior = this.chains.get(msg.sessionKey) ?? Promise.resolve();
37
+ const next = prior.catch(() => undefined).then(() => this.dispatchOne(msg));
38
+ // Track the tail so the next message on this session waits for this one to settle.
39
+ this.chains.set(msg.sessionKey, next.then(() => undefined, () => undefined));
40
+ return next;
41
+ }
42
+ backoff(attempt) {
43
+ return Math.min(this.cfg.deliverMessageRetryBackoffStartMs * 2 ** attempt, this.cfg.deliverMessageRetryBackoffMaxMs);
44
+ }
45
+ async dispatchOne(msg) {
46
+ const own = this.deps.resolveOwnership(msg.sessionKey);
47
+ if (own.status === 'active' && own.owner) {
48
+ if (own.owner === this.deps.selfMachineId) {
49
+ await this.deps.handleLocally(msg);
50
+ return { action: 'handled-locally', owner: own.owner, acked: true };
51
+ }
52
+ if (this.deps.isMachineAlive(own.owner)) {
53
+ return this.forwardToOwner(msg, own.owner, own.epoch, 0);
54
+ }
55
+ // Owner is not alive → owner-dead re-placement (§L4 fallback).
56
+ this.deps.markOwnerSuspect?.(own.owner);
57
+ return this.placeAndClaim(msg, 'failover', true);
58
+ }
59
+ if (own.status === 'placing' || own.status === 'transferring') {
60
+ this.deps.queueMessage(msg, 'ownership-contention');
61
+ return { action: 'queued', detail: own.status, acked: false };
62
+ }
63
+ // Unowned → place + claim.
64
+ return this.placeAndClaim(msg, 'new', false);
65
+ }
66
+ async forwardToOwner(msg, owner, epoch, reResolveDepth) {
67
+ for (let attempt = 0; attempt <= this.cfg.deliverMessageMaxRetries; attempt++) {
68
+ try {
69
+ const ack = await this.deps.deliverMessage(owner, { sessionKey: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: epoch });
70
+ if (ack.accepted === 'duplicate')
71
+ return { action: 'duplicate', owner, acked: true };
72
+ if (ack.accepted === 'queued')
73
+ return { action: 'forwarded', owner, acked: true };
74
+ // stale-ownership → re-resolve (bounded) and route to the current owner.
75
+ if (reResolveDepth >= this.cfg.maxReResolveDepth) {
76
+ return this.placeAndClaim(msg, 'failover', true);
77
+ }
78
+ const own2 = this.deps.resolveOwnership(msg.sessionKey);
79
+ if (own2.status === 'active' && own2.owner === this.deps.selfMachineId) {
80
+ await this.deps.handleLocally(msg);
81
+ return { action: 'handled-locally', owner: own2.owner, detail: 'after-stale', acked: true };
82
+ }
83
+ // Re-forward to the current owner when ownership is still active+alive AND
84
+ // something actually changed — a DIFFERENT owner, or the SAME owner whose
85
+ // epoch advanced (the stale-ownership ACK meant our epoch view was behind;
86
+ // re-deliver at the corrected epoch rather than needlessly re-placing). A
87
+ // same-owner/same-epoch stale ACK is spurious → fall through to re-place.
88
+ // Bounded by maxReResolveDepth. (2026-05-29 pre-merge review #7.)
89
+ if (own2.status === 'active' && own2.owner && this.deps.isMachineAlive(own2.owner) && (own2.owner !== owner || own2.epoch !== epoch)) {
90
+ return this.forwardToOwner(msg, own2.owner, own2.epoch, reResolveDepth + 1);
91
+ }
92
+ // Ownership moved to a transient/dead state (or a spurious same-epoch stale) → re-place.
93
+ return this.placeAndClaim(msg, 'failover', true);
94
+ }
95
+ catch (err) {
96
+ this.deps.log?.(`deliverMessage attempt ${attempt} to ${owner} failed: ${String(err?.message ?? err)}`);
97
+ if (attempt < this.cfg.deliverMessageMaxRetries) {
98
+ await this.deps.sleep(this.backoff(attempt));
99
+ continue;
100
+ }
101
+ }
102
+ }
103
+ // Retries exhausted → owner is unreachable → owner-dead re-placement.
104
+ this.deps.markOwnerSuspect?.(owner);
105
+ return this.placeAndClaim(msg, 'failover', true);
106
+ }
107
+ async placeAndClaim(msg, reason, fromDead) {
108
+ const decision = this.deps.placement.decide({
109
+ sessionKey: msg.sessionKey,
110
+ topicMetadata: msg.topicMetadata ?? {},
111
+ machineRegistry: this.deps.machineRegistry(),
112
+ currentOwner: undefined,
113
+ reason,
114
+ });
115
+ if (decision.outcome === 'placement-blocked') {
116
+ this.deps.raiseAttention('Session placement blocked', `${msg.sessionKey}: ${decision.escalationReason ?? decision.reason}`);
117
+ this.deps.queueMessage(msg, `placement-blocked:${decision.escalationReason ?? decision.reason}`);
118
+ return { action: 'placement-blocked', detail: decision.escalationReason ?? decision.reason, acked: false };
119
+ }
120
+ if (decision.outcome === 'queued' || !decision.chosenMachine) {
121
+ this.deps.raiseAttention('No machine available for session', `${msg.sessionKey}: ${decision.escalationReason ?? decision.reason}`);
122
+ this.deps.queueMessage(msg, decision.escalationReason ?? 'no-capable-machine');
123
+ return { action: 'queued', detail: decision.escalationReason ?? decision.reason, acked: false };
124
+ }
125
+ // Synchronous CAS-claim (the dispatch BLOCKS on this — Invariant #2).
126
+ const pre = this.deps.resolveOwnership(msg.sessionKey);
127
+ const cas = this.deps.casClaimOwnership(msg.sessionKey, decision.chosenMachine, pre.epoch);
128
+ if (!cas.ok) {
129
+ // Contention — re-read; route to the confirmed winner or queue if transient.
130
+ const own2 = this.deps.resolveOwnership(msg.sessionKey);
131
+ if (own2.status === 'active' && own2.owner === this.deps.selfMachineId) {
132
+ await this.deps.handleLocally(msg);
133
+ return { action: 'handled-locally', owner: own2.owner, detail: 'cas-lost-self', acked: true };
134
+ }
135
+ if (own2.status === 'active' && own2.owner && this.deps.isMachineAlive(own2.owner)) {
136
+ return this.forwardToOwner(msg, own2.owner, own2.epoch, 0);
137
+ }
138
+ this.deps.queueMessage(msg, 'ownership-contention');
139
+ return { action: 'queued', detail: 'ownership-contention', acked: false };
140
+ }
141
+ // CAS won → spawn on the winner (or handle locally if that's us).
142
+ const action = fromDead ? 'owner-dead-replaced' : 'spawned';
143
+ if (decision.chosenMachine === this.deps.selfMachineId) {
144
+ await this.deps.handleLocally(msg);
145
+ return { action: fromDead ? 'owner-dead-replaced' : 'handled-locally', owner: decision.chosenMachine, detail: 'placed-self', acked: true };
146
+ }
147
+ await this.deps.spawnOnMachine(decision.chosenMachine, msg);
148
+ return { action, owner: decision.chosenMachine, acked: true };
149
+ }
150
+ }
151
+ //# sourceMappingURL=SessionRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRouter.js","sourceRoot":"","sources":["../../src/core/SessionRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * StageAdvancer — the SOLE writer of `multiMachine.sessionPool.stage` and the
3
+ * mechanical rollout gate (Multi-Machine Session Pool §Rollout). A stage activates
4
+ * ONLY when the prior stage's Tier-3 E2E recorded `green` for the CURRENT commit;
5
+ * otherwise the advance is refused and audited. If a live stage later records `red`,
6
+ * the advancer mechanically REVERTS to the prior (safe) stage. Per "Structure >
7
+ * Willpower" the gate is code, not prose: no other path may flip the stage (enforced
8
+ * by a Config.ts write-guard that only honors writes carrying this module's token).
9
+ */
10
+ import type { SessionPoolE2EResultStore } from './SessionPoolE2EResultStore.js';
11
+ export declare const STAGES: readonly ["dark", "shadow", "live-transfer", "rebalance"];
12
+ export type SessionPoolStage = (typeof STAGES)[number];
13
+ export declare function stageIndex(stage: SessionPoolStage): number;
14
+ export type AdvanceResult = {
15
+ ok: true;
16
+ stage: SessionPoolStage;
17
+ } | {
18
+ ok: false;
19
+ reason: 'e2e-gate-not-passed' | 'already-at-or-past' | 'invalid-stage';
20
+ detail?: string;
21
+ };
22
+ export interface StageAdvancerDeps {
23
+ resultStore: SessionPoolE2EResultStore;
24
+ /** The commit the running build is on — the E2E result must match it. */
25
+ currentCommitSha: () => string;
26
+ /** Read the current configured stage. */
27
+ readStage: () => SessionPoolStage;
28
+ /** The ONLY stage-config write path. Config.ts must reject writes that don't come through here. */
29
+ writeStageConfig: (stage: SessionPoolStage) => void;
30
+ audit?: (event: string, detail: Record<string, unknown>) => void;
31
+ }
32
+ export declare class StageAdvancer {
33
+ private readonly d;
34
+ constructor(d: StageAdvancerDeps);
35
+ /**
36
+ * Advance to `targetStage`, gated on a matching `green` E2E for the PRIOR stage at
37
+ * the current commit. A missing/`red`/stale-commit/tampered prior result → refused.
38
+ */
39
+ advanceTo(targetStage: SessionPoolStage): AdvanceResult;
40
+ /**
41
+ * Reconcile on each cycle: if the CURRENT stage's latest E2E is `red` (regression),
42
+ * mechanically revert to the prior stage until a fresh `green` is recorded. Returns
43
+ * the stage after reconciliation.
44
+ */
45
+ reconcile(): SessionPoolStage;
46
+ }
47
+ //# sourceMappingURL=StageAdvancer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StageAdvancer.d.ts","sourceRoot":"","sources":["../../src/core/StageAdvancer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAEhF,eAAO,MAAM,MAAM,2DAA4D,CAAC;AAChF,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAE1D;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,GAAG,oBAAoB,GAAG,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3G,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,yBAAyB,CAAC;IACvC,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,MAAM,CAAC;IAC/B,yCAAyC;IACzC,SAAS,EAAE,MAAM,gBAAgB,CAAC;IAClC,mGAAmG;IACnG,gBAAgB,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACpD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClE;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAAD,CAAC,EAAE,iBAAiB;IAEjD;;;OAGG;IACH,SAAS,CAAC,WAAW,EAAE,gBAAgB,GAAG,aAAa;IAwBvD;;;;OAIG;IACH,SAAS,IAAI,gBAAgB;CAiB9B"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * StageAdvancer — the SOLE writer of `multiMachine.sessionPool.stage` and the
3
+ * mechanical rollout gate (Multi-Machine Session Pool §Rollout). A stage activates
4
+ * ONLY when the prior stage's Tier-3 E2E recorded `green` for the CURRENT commit;
5
+ * otherwise the advance is refused and audited. If a live stage later records `red`,
6
+ * the advancer mechanically REVERTS to the prior (safe) stage. Per "Structure >
7
+ * Willpower" the gate is code, not prose: no other path may flip the stage (enforced
8
+ * by a Config.ts write-guard that only honors writes carrying this module's token).
9
+ */
10
+ export const STAGES = ['dark', 'shadow', 'live-transfer', 'rebalance'];
11
+ export function stageIndex(stage) {
12
+ return STAGES.indexOf(stage);
13
+ }
14
+ export class StageAdvancer {
15
+ d;
16
+ constructor(d) {
17
+ this.d = d;
18
+ }
19
+ /**
20
+ * Advance to `targetStage`, gated on a matching `green` E2E for the PRIOR stage at
21
+ * the current commit. A missing/`red`/stale-commit/tampered prior result → refused.
22
+ */
23
+ advanceTo(targetStage) {
24
+ const targetIdx = stageIndex(targetStage);
25
+ if (targetIdx < 0)
26
+ return { ok: false, reason: 'invalid-stage', detail: String(targetStage) };
27
+ const current = this.d.readStage();
28
+ if (stageIndex(current) >= targetIdx) {
29
+ return { ok: false, reason: 'already-at-or-past', detail: `current=${current}` };
30
+ }
31
+ // 'dark' (index 0) is the floor — there is no prior stage to gate on; you never
32
+ // "advance to dark", you revert to it. So any real advance targets index ≥ 1.
33
+ const priorStageIdx = targetIdx - 1;
34
+ const prior = this.d.resultStore.getLatestForStage(priorStageIdx);
35
+ const sha = this.d.currentCommitSha();
36
+ const gateOk = !!prior && prior.result === 'green' && prior.commitSha === sha && this.d.resultStore.verify(prior);
37
+ if (!gateOk) {
38
+ const detail = !prior ? 'no-result' : prior.result !== 'green' ? `result=${prior.result}` : prior.commitSha !== sha ? 'stale-commit' : 'bad-signature';
39
+ this.d.audit?.('stage-advance-refused', { targetStage, priorStage: STAGES[priorStageIdx], reason: 'e2e-gate-not-passed', detail, commitSha: sha });
40
+ return { ok: false, reason: 'e2e-gate-not-passed', detail };
41
+ }
42
+ this.d.writeStageConfig(targetStage);
43
+ this.d.audit?.('stage-advanced', { from: current, to: targetStage, commitSha: sha, evidenceRef: prior.evidenceRef });
44
+ return { ok: true, stage: targetStage };
45
+ }
46
+ /**
47
+ * Reconcile on each cycle: if the CURRENT stage's latest E2E is `red` (regression),
48
+ * mechanically revert to the prior stage until a fresh `green` is recorded. Returns
49
+ * the stage after reconciliation.
50
+ */
51
+ reconcile() {
52
+ const current = this.d.readStage();
53
+ const idx = stageIndex(current);
54
+ if (idx <= 0)
55
+ return current; // 'dark' is the floor — nothing to revert to
56
+ const latest = this.d.resultStore.getLatestForStage(idx);
57
+ // Only revert on a red for the CURRENT commit — a stale red from a prior commit
58
+ // is not a regression of the running build and must not trigger a revert.
59
+ // (2026-05-29 pre-merge review.)
60
+ const regressed = !!latest && latest.result === 'red' && latest.commitSha === this.d.currentCommitSha() && this.d.resultStore.verify(latest);
61
+ if (regressed) {
62
+ const reverted = STAGES[idx - 1];
63
+ this.d.writeStageConfig(reverted);
64
+ this.d.audit?.('stage-reverted', { from: current, to: reverted, reason: 'e2e-red', evidenceRef: latest.evidenceRef });
65
+ return reverted;
66
+ }
67
+ return current;
68
+ }
69
+ }
70
+ //# sourceMappingURL=StageAdvancer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StageAdvancer.js","sourceRoot":"","sources":["../../src/core/StageAdvancer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TransferByNickname — the pure planner behind the headline "move this to
3
+ * <nickname>" mid-conversation swap (Multi-Machine Session Pool §L4 "Topic
4
+ * Placement Updates" + §L5). It turns a recognized NicknameCommand into a gated,
5
+ * validated TransferPlan: resolve the nickname → machineId, enforce the rate limit,
6
+ * the confirmation gate (offline target / mid-reply), and the already-there no-op.
7
+ * Per "Structure > Willpower" a transfer is high-impact, so this is NOT free-text
8
+ * inference — it operates on an ALREADY-recognized command + the registry state,
9
+ * and an unknown/ambiguous nickname is REJECTED (listing valid nicknames), never
10
+ * silently mis-routed.
11
+ */
12
+ import type { NicknameCommand } from './NicknameCommand.js';
13
+ export interface TransferByNicknameState {
14
+ /** Resolve a nickname → machineId (case-insensitive exact match), or null if unknown. */
15
+ resolveNickname: (nickname: string) => string | null;
16
+ /** The full set of valid nicknames (returned on an unknown-nickname rejection). */
17
+ validNicknames: () => string[];
18
+ isOnline: (machineId: string) => boolean;
19
+ currentOwnerOf: (sessionKey: string) => string | null;
20
+ /** Is the session mid-reply (a turn in flight)? Confirmation is required to interrupt it. */
21
+ isMidReply: (sessionKey: string) => boolean;
22
+ /** When the topic last had a placement update (for the rate limit), or null. */
23
+ lastPlacementUpdateAt: (sessionKey: string) => number | null;
24
+ now: () => number;
25
+ /** Minimum ms between placement updates per topic. Default 10000. */
26
+ minUpdateIntervalMs?: number;
27
+ }
28
+ export type TransferPlanAction = 'transfer' | 'noop' | 'confirm-required' | 'reject';
29
+ export interface TransferPlan {
30
+ action: TransferPlanAction;
31
+ sessionKey: string;
32
+ /** Resolved target machine (present for transfer / confirm-required / noop). */
33
+ targetMachine?: string;
34
+ /** Set the topic's preferredMachine + pinned:true (a hard pin — §L4). */
35
+ setPin?: boolean;
36
+ /** Human-facing confirmation prompt (action === 'confirm-required'). */
37
+ confirmationPrompt?: string;
38
+ /** Rejection reason (action === 'reject'). */
39
+ rejectReason?: 'unknown-machine-nickname' | 'rate-limited';
40
+ /** On unknown-nickname: the valid nicknames to surface to the user. */
41
+ validNicknames?: string[];
42
+ detail?: string;
43
+ }
44
+ /**
45
+ * Plan a transfer-by-nickname. Pure over (command, state). Both "move this to
46
+ * <nick>" and "run this on <nick>" set a hard pin (pinned:true) per §L4; the only
47
+ * intent difference is cosmetic. Confirmation is required when the target is offline,
48
+ * or when it differs from the current owner AND the session is mid-reply (§L4 line 456).
49
+ */
50
+ export declare function planTransferByNickname(command: NicknameCommand, state: TransferByNicknameState, sessionKey: string): TransferPlan;
51
+ //# sourceMappingURL=TransferByNickname.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransferByNickname.d.ts","sourceRoot":"","sources":["../../src/core/TransferByNickname.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACrD,mFAAmF;IACnF,cAAc,EAAE,MAAM,MAAM,EAAE,CAAC;IAC/B,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACtD,6FAA6F;IAC7F,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,gFAAgF;IAChF,qBAAqB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC7D,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,qEAAqE;IACrE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,MAAM,GAAG,kBAAkB,GAAG,QAAQ,CAAC;AAErF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA8C;IAC9C,YAAY,CAAC,EAAE,0BAA0B,GAAG,cAAc,CAAC;IAC3D,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,CAmCjI"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TransferByNickname — the pure planner behind the headline "move this to
3
+ * <nickname>" mid-conversation swap (Multi-Machine Session Pool §L4 "Topic
4
+ * Placement Updates" + §L5). It turns a recognized NicknameCommand into a gated,
5
+ * validated TransferPlan: resolve the nickname → machineId, enforce the rate limit,
6
+ * the confirmation gate (offline target / mid-reply), and the already-there no-op.
7
+ * Per "Structure > Willpower" a transfer is high-impact, so this is NOT free-text
8
+ * inference — it operates on an ALREADY-recognized command + the registry state,
9
+ * and an unknown/ambiguous nickname is REJECTED (listing valid nicknames), never
10
+ * silently mis-routed.
11
+ */
12
+ const DEFAULT_MIN_UPDATE_INTERVAL_MS = 10000;
13
+ /**
14
+ * Plan a transfer-by-nickname. Pure over (command, state). Both "move this to
15
+ * <nick>" and "run this on <nick>" set a hard pin (pinned:true) per §L4; the only
16
+ * intent difference is cosmetic. Confirmation is required when the target is offline,
17
+ * or when it differs from the current owner AND the session is mid-reply (§L4 line 456).
18
+ */
19
+ export function planTransferByNickname(command, state, sessionKey) {
20
+ const target = state.resolveNickname(command.nickname);
21
+ if (!target) {
22
+ return { action: 'reject', sessionKey, rejectReason: 'unknown-machine-nickname', validNicknames: state.validNicknames(), detail: command.nickname };
23
+ }
24
+ // Rate limit — at most one placement update per topic per interval (defeats rapid-fire transfers).
25
+ const minInterval = state.minUpdateIntervalMs ?? DEFAULT_MIN_UPDATE_INTERVAL_MS;
26
+ const last = state.lastPlacementUpdateAt(sessionKey);
27
+ if (last != null && state.now() - last < minInterval) {
28
+ return { action: 'reject', sessionKey, targetMachine: target, rejectReason: 'rate-limited' };
29
+ }
30
+ const owner = state.currentOwnerOf(sessionKey);
31
+ // Already on the target → no transfer needed (still applies the pin if requested).
32
+ if (owner === target) {
33
+ return { action: 'noop', sessionKey, targetMachine: target, setPin: true, detail: 'already-on-target' };
34
+ }
35
+ // Confirmation gate (§L4): offline target, OR a different owner while mid-reply.
36
+ if (!state.isOnline(target)) {
37
+ return {
38
+ action: 'confirm-required', sessionKey, targetMachine: target, setPin: true,
39
+ confirmationPrompt: `${command.nickname} is offline right now. Move this conversation there anyway? It'll pick up like a fresh-session catch-up once it's back.`,
40
+ };
41
+ }
42
+ if (owner != null && owner !== target && state.isMidReply(sessionKey)) {
43
+ return {
44
+ action: 'confirm-required', sessionKey, targetMachine: target, setPin: true,
45
+ confirmationPrompt: `Move this to ${command.nickname}? I'm mid-reply — it'll be like a fresh-session catch-up on the other machine.`,
46
+ };
47
+ }
48
+ return { action: 'transfer', sessionKey, targetMachine: target, setPin: true };
49
+ }
50
+ //# sourceMappingURL=TransferByNickname.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransferByNickname.js","sourceRoot":"","sources":["../../src/core/TransferByNickname.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsCH,MAAM,8BAA8B,GAAG,KAAK,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAwB,EAAE,KAA8B,EAAE,UAAkB;IACjH,MAAM,MAAM,GAAG,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IACtJ,CAAC;IAED,mGAAmG;IACnG,MAAM,WAAW,GAAG,KAAK,CAAC,mBAAmB,IAAI,8BAA8B,CAAC;IAChF,MAAM,IAAI,GAAG,KAAK,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,WAAW,EAAE,CAAC;QACrD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/F,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IAE/C,mFAAmF;IACnF,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC1G,CAAC;IAED,iFAAiF;IACjF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI;YAC3E,kBAAkB,EAAE,GAAG,OAAO,CAAC,QAAQ,yHAAyH;SACjK,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtE,OAAO;YACL,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI;YAC3E,kBAAkB,EAAE,gBAAgB,OAAO,CAAC,QAAQ,gFAAgF;SACrI,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACjF,CAAC"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * TransferOrchestrator — drives a planned session handoff through the §L3/§L5
3
+ * ordered sequence: active(S) → transferring(e+1) → [drain, flush ledger, target
4
+ * pulls+verifies] → active(T, e+2) → S-release. Claim-before-release, fenced by
5
+ * (status, epoch): there is no double-run (only one machine is `active` at the top
6
+ * epoch) and no no-owner gap (`transferring` still names S as the draining owner).
7
+ *
8
+ * The orchestrator enforces the two §L5 timing contracts:
9
+ * - Drain bound (`transferDrainTimeoutMs`): a long reply/tool-call must NOT block a
10
+ * transfer; on timeout the source abandons its partial output (NOT emitted).
11
+ * - Output exclusion (`transferOutputCutoffMs`): T holds its CONTINUATION until the
12
+ * cutoff has elapsed since the `transferring` write, so S's drain window and T's
13
+ * emission window are disjoint by construction — exactly one continuation per turn.
14
+ *
15
+ * All I/O (CAS, drain, ledger flush, mesh transfer, claim, clock) is injected so the
16
+ * sequence + timing logic is deterministic and unit-testable; the real wiring (real
17
+ * SessionMigrator drain, real git ledger flush, real mesh client) lands in Track H.
18
+ */
19
+ export type LedgerEntryStatus = 'reply_committed' | 'cursor_advanced' | 'in_flight';
20
+ export interface LedgerSnapshot {
21
+ sessionKey: string;
22
+ generatedAt: number;
23
+ entries: Array<{
24
+ messageId: string;
25
+ status: LedgerEntryStatus;
26
+ replyMarker?: string;
27
+ updatedAt: number;
28
+ }>;
29
+ snapshotSha256: string;
30
+ }
31
+ /**
32
+ * Verify a ledger snapshot at the transfer handoff (§L5): the SHA256 must match the
33
+ * SyncManifest-covered value AND every entry must be terminal (nothing `in_flight`).
34
+ * An `in_flight` entry means a turn was still processing at flush time — the target
35
+ * must NOT resume from it (it waits for a terminal re-flush, or treats it as the
36
+ * interrupted-drain case on failover). Pure.
37
+ */
38
+ export declare function verifyLedgerSnapshot(snapshot: LedgerSnapshot, expectedSha256: string, computeSha256: (s: LedgerSnapshot) => string): {
39
+ ok: true;
40
+ } | {
41
+ ok: false;
42
+ reason: 'sha-mismatch' | 'in-flight-entry';
43
+ };
44
+ export interface TransferRequest {
45
+ sessionKey: string;
46
+ source: string;
47
+ target: string;
48
+ reason: 'pin' | 'rebalance' | 'failover';
49
+ /** Whether the source machine is alive (planned transfer) or gone (failover). */
50
+ sourceAlive: boolean;
51
+ /** The ownership epoch the orchestrator observed before starting. */
52
+ baseEpoch: number;
53
+ }
54
+ export interface TransferOrchestratorConfig {
55
+ transferDrainTimeoutMs: number;
56
+ transferOutputCutoffMs: number;
57
+ }
58
+ export declare const DEFAULT_TRANSFER_CONFIG: TransferOrchestratorConfig;
59
+ export interface TransferOrchestratorDeps {
60
+ /** CAS the record source→transferring(epoch+1). */
61
+ casToTransferring: (sessionKey: string, target: string, expectedEpoch: number) => {
62
+ ok: boolean;
63
+ epoch: number;
64
+ };
65
+ /** Quiesce + drain the in-flight reply on the source, bounded by timeoutMs. */
66
+ drain: (sessionKey: string, timeoutMs: number) => Promise<{
67
+ drained: boolean;
68
+ abandonedPartial: boolean;
69
+ }>;
70
+ /** Source flushes its ledger snapshot synchronously (returns the snapshot + its ref). */
71
+ flushLedger: (sessionKey: string) => Promise<{
72
+ snapshot: LedgerSnapshot;
73
+ ledgerSnapshotRef: string;
74
+ syncManifestRef: string;
75
+ }>;
76
+ /** Send the transfer MeshRpc to the target; the target pulls + verifies the snapshot. */
77
+ sendTransferRpc: (target: string, refs: {
78
+ sessionKey: string;
79
+ ledgerSnapshotRef: string;
80
+ syncManifestRef: string;
81
+ }) => Promise<{
82
+ ok: boolean;
83
+ verified: boolean;
84
+ reason?: string;
85
+ }>;
86
+ /** Target CASes itself to active(owner=T, epoch+2). */
87
+ targetClaim: (sessionKey: string, expectedEpoch: number) => {
88
+ ok: boolean;
89
+ epoch: number;
90
+ };
91
+ /** Cleanup: source releases (NOT a precondition for T's claim — runs after). */
92
+ releaseSource: (sessionKey: string) => void;
93
+ /** Raise a deduped Attention item on sync corruption / unrecoverable handoff. */
94
+ raiseAttention?: (title: string, body: string) => void;
95
+ now: () => number;
96
+ sleep: (ms: number) => Promise<void>;
97
+ log?: (line: string) => void;
98
+ }
99
+ export type TransferStatus = 'transferred' | 'aborted-cas-lost' | 'sync-corrupted' | 'target-claim-failed';
100
+ export interface TransferOutcome {
101
+ ok: boolean;
102
+ status: TransferStatus;
103
+ drainAbandoned?: boolean;
104
+ finalEpoch?: number;
105
+ detail?: string;
106
+ }
107
+ export declare class TransferOrchestrator {
108
+ private readonly deps;
109
+ private readonly cfg;
110
+ constructor(deps: TransferOrchestratorDeps, cfg?: TransferOrchestratorConfig);
111
+ transfer(req: TransferRequest): Promise<TransferOutcome>;
112
+ }
113
+ //# sourceMappingURL=TransferOrchestrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransferOrchestrator.d.ts","sourceRoot":"","sources":["../../src/core/TransferOrchestrator.ts"],"names":[],"mappings":"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"}