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.
- package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +17 -0
- package/dashboard/index.html +108 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +207 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +30 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/config/LiveConfig.d.ts +3 -1
- package/dist/config/LiveConfig.d.ts.map +1 -1
- package/dist/config/LiveConfig.js +4 -1
- package/dist/config/LiveConfig.js.map +1 -1
- package/dist/config/stageWriteGuard.d.ts +23 -0
- package/dist/config/stageWriteGuard.d.ts.map +1 -0
- package/dist/config/stageWriteGuard.js +33 -0
- package/dist/config/stageWriteGuard.js.map +1 -0
- package/dist/core/DeliverMessageHandler.d.ts +33 -0
- package/dist/core/DeliverMessageHandler.d.ts.map +1 -0
- package/dist/core/DeliverMessageHandler.js +34 -0
- package/dist/core/DeliverMessageHandler.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +39 -2
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +45 -9
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/MachineIdentity.d.ts +28 -1
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +81 -1
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/MachineLoadBroker.d.ts +94 -0
- package/dist/core/MachineLoadBroker.d.ts.map +1 -0
- package/dist/core/MachineLoadBroker.js +87 -0
- package/dist/core/MachineLoadBroker.js.map +1 -0
- package/dist/core/MachinePoolRegistry.d.ts +94 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -0
- package/dist/core/MachinePoolRegistry.js +150 -0
- package/dist/core/MachinePoolRegistry.js.map +1 -0
- package/dist/core/MeshRpc.d.ts +165 -0
- package/dist/core/MeshRpc.d.ts.map +1 -0
- package/dist/core/MeshRpc.js +156 -0
- package/dist/core/MeshRpc.js.map +1 -0
- package/dist/core/MeshRpcClient.d.ts +59 -0
- package/dist/core/MeshRpcClient.d.ts.map +1 -0
- package/dist/core/MeshRpcClient.js +58 -0
- package/dist/core/MeshRpcClient.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +11 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +13 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/NicknameAssigner.d.ts +51 -0
- package/dist/core/NicknameAssigner.d.ts.map +1 -0
- package/dist/core/NicknameAssigner.js +91 -0
- package/dist/core/NicknameAssigner.js.map +1 -0
- package/dist/core/NicknameCommand.d.ts +28 -0
- package/dist/core/NicknameCommand.d.ts.map +1 -0
- package/dist/core/NicknameCommand.js +69 -0
- package/dist/core/NicknameCommand.js.map +1 -0
- package/dist/core/PlacementExecutor.d.ts +71 -0
- package/dist/core/PlacementExecutor.d.ts.map +1 -0
- package/dist/core/PlacementExecutor.js +156 -0
- package/dist/core/PlacementExecutor.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +21 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RebalancePlanner.d.ts +48 -0
- package/dist/core/RebalancePlanner.d.ts.map +1 -0
- package/dist/core/RebalancePlanner.js +71 -0
- package/dist/core/RebalancePlanner.js.map +1 -0
- package/dist/core/SessionOwnership.d.ts +103 -0
- package/dist/core/SessionOwnership.d.ts.map +1 -0
- package/dist/core/SessionOwnership.js +0 -0
- package/dist/core/SessionOwnership.js.map +1 -0
- package/dist/core/SessionOwnershipRegistry.d.ts +115 -0
- package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -0
- package/dist/core/SessionOwnershipRegistry.js +128 -0
- package/dist/core/SessionOwnershipRegistry.js.map +1 -0
- package/dist/core/SessionPoolE2EResultStore.d.ts +45 -0
- package/dist/core/SessionPoolE2EResultStore.d.ts.map +1 -0
- package/dist/core/SessionPoolE2EResultStore.js +66 -0
- package/dist/core/SessionPoolE2EResultStore.js.map +1 -0
- package/dist/core/SessionRouter.d.ts +95 -0
- package/dist/core/SessionRouter.d.ts.map +1 -0
- package/dist/core/SessionRouter.js +151 -0
- package/dist/core/SessionRouter.js.map +1 -0
- package/dist/core/StageAdvancer.d.ts +47 -0
- package/dist/core/StageAdvancer.d.ts.map +1 -0
- package/dist/core/StageAdvancer.js +70 -0
- package/dist/core/StageAdvancer.js.map +1 -0
- package/dist/core/TransferByNickname.d.ts +51 -0
- package/dist/core/TransferByNickname.d.ts.map +1 -0
- package/dist/core/TransferByNickname.js +50 -0
- package/dist/core/TransferByNickname.js.map +1 -0
- package/dist/core/TransferOrchestrator.d.ts +113 -0
- package/dist/core/TransferOrchestrator.d.ts.map +1 -0
- package/dist/core/TransferOrchestrator.js +79 -0
- package/dist/core/TransferOrchestrator.js.map +1 -0
- package/dist/core/types.d.ts +177 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +8 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +4 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +14 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +12 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +91 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +5 -0
- package/upgrades/1.3.92.md +63 -0
- package/upgrades/side-effects/session-pool-capability-classification.md +16 -0
- package/upgrades/side-effects/session-pool-review-fixes.md +23 -0
- package/upgrades/side-effects/session-pool-track-a-router-lease.md +29 -0
- package/upgrades/side-effects/session-pool-track-b-agent-awareness.md +29 -0
- package/upgrades/side-effects/session-pool-track-b-nicknames.md +35 -0
- package/upgrades/side-effects/session-pool-track-b-pool-api.md +36 -0
- package/upgrades/side-effects/session-pool-track-b-registry.md +31 -0
- package/upgrades/side-effects/session-pool-track-b-wiring.md +39 -0
- package/upgrades/side-effects/session-pool-track-c-dispatcher.md +24 -0
- package/upgrades/side-effects/session-pool-track-c-meshrpc.md +35 -0
- package/upgrades/side-effects/session-pool-track-c-route-wiring.md +31 -0
- package/upgrades/side-effects/session-pool-track-d-ownership-fsm.md +31 -0
- package/upgrades/side-effects/session-pool-track-d-registry.md +27 -0
- package/upgrades/side-effects/session-pool-track-d-wiring.md +30 -0
- package/upgrades/side-effects/session-pool-track-e-boot-wiring.md +20 -0
- package/upgrades/side-effects/session-pool-track-e-placement-core.md +17 -0
- package/upgrades/side-effects/session-pool-track-e-router.md +23 -0
- package/upgrades/side-effects/session-pool-track-f-config.md +12 -0
- package/upgrades/side-effects/session-pool-track-f-handoff-core.md +17 -0
- package/upgrades/side-effects/session-pool-track-g-load-broker.md +16 -0
- package/upgrades/side-effects/session-pool-track-h-gate-wiring.md +17 -0
- package/upgrades/side-effects/session-pool-track-h-mesh-client.md +15 -0
- package/upgrades/side-effects/session-pool-track-h-rebalance.md +13 -0
- package/upgrades/side-effects/session-pool-track-h-rollout-gate.md +17 -0
- 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"}
|