instar 1.3.91 → 1.3.93
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 +337 -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 +27 -0
- package/upgrades/1.3.93.md +28 -0
- package/upgrades/side-effects/session-pool-activation-wiring.md +22 -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
|
@@ -30,6 +30,23 @@ set -uo pipefail # NOTE: -e intentionally omitted; field lookups for optional
|
|
|
30
30
|
# Read hook input from stdin
|
|
31
31
|
HOOK_INPUT=$(cat)
|
|
32
32
|
|
|
33
|
+
# ── Anchor to the agent home, NOT the session's CWD ───────────────────
|
|
34
|
+
# All state paths below are relative (.instar/autonomous/<topic>.local.md, the
|
|
35
|
+
# registry, the legacy file). The Stop hook inherits the session's working
|
|
36
|
+
# directory — and a session working inside a git worktree (~/.instar/agents/
|
|
37
|
+
# <name>/.worktrees/<slug>) would resolve those paths against the WORKTREE,
|
|
38
|
+
# which has no autonomous state, so the hook sees "no active job" and lets the
|
|
39
|
+
# session exit — silently stranding the autonomous loop. Anchor to the agent
|
|
40
|
+
# home (where this hook is installed) so the paths resolve regardless of CWD.
|
|
41
|
+
# (Root cause of the 2026-05-29 strand: CWD was a worktree.)
|
|
42
|
+
# Anchor to CLAUDE_PROJECT_DIR — Claude Code always sets it for hooks to the
|
|
43
|
+
# agent home (the hook itself is wired as `${CLAUDE_PROJECT_DIR}/.claude/...`).
|
|
44
|
+
# Guard on its `.instar` so a misconfigured value can't send us somewhere wrong;
|
|
45
|
+
# when unset (e.g. an isolated test harness) we stay in the caller's CWD.
|
|
46
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -d "${CLAUDE_PROJECT_DIR}/.instar" ]]; then
|
|
47
|
+
cd "$CLAUDE_PROJECT_DIR" || true
|
|
48
|
+
fi
|
|
49
|
+
|
|
33
50
|
REGISTRY_FILE=".instar/topic-session-registry.json"
|
|
34
51
|
RECOVERY_AUDIT=".instar/autonomous-recovery.jsonl"
|
|
35
52
|
LEGACY_STATE=".instar/autonomous-state.local.md"
|
package/dashboard/index.html
CHANGED
|
@@ -801,6 +801,18 @@
|
|
|
801
801
|
/* Page intro — one-sentence grounding right under the title so terms like
|
|
802
802
|
"it" / "the system" later in the page have a clear referent. */
|
|
803
803
|
.ph-intro { font-size: 17px; color: var(--text); line-height: 1.55; margin: 0; max-width: 720px; }
|
|
804
|
+
/* ── Machines tab (Multi-Machine Session Pool §L2) — reuses .ph-root calm type ── */
|
|
805
|
+
.machine-card { border: 1px solid var(--border, #2a2a2a); border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; max-width: 720px; background: var(--panel-bg, transparent); }
|
|
806
|
+
.machine-head { display: flex; align-items: center; gap: 10px; }
|
|
807
|
+
.machine-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
|
|
808
|
+
.machine-dot.ok { background: var(--accent); } /* online */
|
|
809
|
+
.machine-dot.warn { background: #f59e0b; } /* clock out of sync — paused */
|
|
810
|
+
.machine-dot.off { background: var(--text-dim); } /* offline */
|
|
811
|
+
.machine-nick { font-size: 19px; font-weight: 600; color: var(--text-bright); background: transparent; border: 1px solid transparent; border-radius: 6px; padding: 3px 6px; min-width: 120px; }
|
|
812
|
+
.machine-nick:hover { border-color: var(--border, #2a2a2a); }
|
|
813
|
+
.machine-nick:focus { border-color: var(--accent); outline: none; background: rgba(255,255,255,0.03); }
|
|
814
|
+
.machine-badge { font-size: 12px; font-weight: 600; color: var(--accent); border: 1px solid var(--accent-dim); border-radius: 999px; padding: 2px 9px; }
|
|
815
|
+
.machine-meta { font-size: 15px; color: var(--text-dim); line-height: 1.55; margin-top: 6px; }
|
|
804
816
|
/* Status color tokens — calm, never alarming. The dot is a scan-aid:
|
|
805
817
|
glance the collapsed list, see which problems are still being worked
|
|
806
818
|
on (amber), cause known (blue), or worked out (green). */
|
|
@@ -2624,6 +2636,7 @@
|
|
|
2624
2636
|
<button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
|
|
2625
2637
|
<button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
|
|
2626
2638
|
<button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
|
|
2639
|
+
<button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
|
|
2627
2640
|
</nav>
|
|
2628
2641
|
</div>
|
|
2629
2642
|
<div class="vital-signs" id="vitalSigns">
|
|
@@ -3136,6 +3149,21 @@
|
|
|
3136
3149
|
</details>
|
|
3137
3150
|
</div>
|
|
3138
3151
|
|
|
3152
|
+
<!-- machines tab (Multi-Machine Session Pool §L2) -->
|
|
3153
|
+
<div id="machinesPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
|
|
3154
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
|
3155
|
+
<h2 class="ph-title">Machines</h2>
|
|
3156
|
+
<div id="machinesStamp" class="ph-stamp" aria-live="polite"></div>
|
|
3157
|
+
</div>
|
|
3158
|
+
<p class="ph-intro">Every computer this agent is set up on. Each one gets a friendly name you can change — and once it has a name, you can tell the agent "move this conversation to the mini" and it knows which machine you mean. The agent shares its conversations across these machines so it can handle more at once.</p>
|
|
3159
|
+
<div id="machinesHeadline" class="ph-headline-wrap" aria-live="polite"></div>
|
|
3160
|
+
<section class="ph-section">
|
|
3161
|
+
<h3 class="ph-h">The machines</h3>
|
|
3162
|
+
<p class="ph-h-sub">Click a name to rename it. The "dispatcher" is the one machine currently handing out new conversations.</p>
|
|
3163
|
+
<div id="machinesList"></div>
|
|
3164
|
+
</section>
|
|
3165
|
+
</div>
|
|
3166
|
+
|
|
3139
3167
|
<div id="evidenceTab" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
|
|
3140
3168
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
3141
3169
|
<h2 style="margin:0">Evidence</h2>
|
|
@@ -4375,6 +4403,13 @@
|
|
|
4375
4403
|
onActivate: () => { if (typeof startProcessHealth === 'function') startProcessHealth(); },
|
|
4376
4404
|
onDeactivate: () => { if (typeof stopProcessHealth === 'function') stopProcessHealth(); },
|
|
4377
4405
|
},
|
|
4406
|
+
{
|
|
4407
|
+
id: 'machines',
|
|
4408
|
+
panels: ['machinesPanel'],
|
|
4409
|
+
display: ['flex'],
|
|
4410
|
+
onActivate: () => { if (typeof startMachines === 'function') startMachines(); },
|
|
4411
|
+
onDeactivate: () => { if (typeof stopMachines === 'function') stopMachines(); },
|
|
4412
|
+
},
|
|
4378
4413
|
{
|
|
4379
4414
|
id: 'secrets',
|
|
4380
4415
|
panels: ['secretsPanel'],
|
|
@@ -4530,6 +4565,79 @@
|
|
|
4530
4565
|
return resp.json();
|
|
4531
4566
|
}
|
|
4532
4567
|
|
|
4568
|
+
// ── Machines Tab (Multi-Machine Session Pool §L2) ─────────────────
|
|
4569
|
+
let __machinesTimer = null;
|
|
4570
|
+
function startMachines() { loadMachines(); if (!__machinesTimer) __machinesTimer = setInterval(loadMachines, 15000); }
|
|
4571
|
+
function stopMachines() { if (__machinesTimer) { clearInterval(__machinesTimer); __machinesTimer = null; } }
|
|
4572
|
+
function machineSpecLine(hw) {
|
|
4573
|
+
if (!hw) return 'specs not reported yet';
|
|
4574
|
+
const chip = (hw.cpuModel && hw.cpuModel !== 'unknown') ? hw.cpuModel : ((hw.platform || '') + (hw.arch ? ' ' + hw.arch : '')).trim();
|
|
4575
|
+
const cores = hw.cpuCores ? hw.cpuCores + ' cores' : '';
|
|
4576
|
+
const ram = hw.totalMemBytes ? Math.round(hw.totalMemBytes / 1073741824) + ' GB' : '';
|
|
4577
|
+
return [chip, cores, ram].filter(Boolean).join(' · ') || 'specs not reported yet';
|
|
4578
|
+
}
|
|
4579
|
+
function machineBusyWord(m) {
|
|
4580
|
+
if (m.loadAvg == null) return '';
|
|
4581
|
+
if (m.loadAvg < 1) return 'idle';
|
|
4582
|
+
if (m.loadAvg < 4) return 'working';
|
|
4583
|
+
return 'busy';
|
|
4584
|
+
}
|
|
4585
|
+
function renderMachineCard(m, routerHolder) {
|
|
4586
|
+
const online = !!m.online;
|
|
4587
|
+
const dot = !online ? 'off' : (m.clockSkewStatus === 'suspect-clock-removed' ? 'warn' : 'ok');
|
|
4588
|
+
const statusWord = !online ? 'offline' : (m.clockSkewStatus === 'suspect-clock-removed' ? 'clock out of sync — paused for new conversations' : 'online');
|
|
4589
|
+
const isRouter = !!(routerHolder && m.machineId === routerHolder);
|
|
4590
|
+
const nick = m.nickname || m.machineId;
|
|
4591
|
+
const busy = machineBusyWord(m);
|
|
4592
|
+
const sessions = (m.activeSessionCount != null) ? (m.activeSessionCount + (m.maxSessions ? '/' + m.maxSessions : '') + ' conversation' + (m.activeSessionCount === 1 ? '' : 's')) : '';
|
|
4593
|
+
const tail = [statusWord, busy, sessions].filter(Boolean).map(s => escapeHtml(s)).join(' · ');
|
|
4594
|
+
return '<div class="machine-card">' +
|
|
4595
|
+
'<div class="machine-head">' +
|
|
4596
|
+
'<span class="machine-dot ' + dot + '" title="' + escapeHtml(statusWord) + '"></span>' +
|
|
4597
|
+
'<input class="machine-nick" value="' + escapeHtml(nick) + '" data-mid="' + escapeHtml(m.machineId) + '" ' +
|
|
4598
|
+
'onkeydown="if(event.key===\'Enter\'){this.blur();}" onblur="saveMachineNickname(this)" aria-label="machine nickname" />' +
|
|
4599
|
+
(isRouter ? '<span class="machine-badge">dispatcher</span>' : '') +
|
|
4600
|
+
'</div>' +
|
|
4601
|
+
'<div class="machine-meta">' + escapeHtml(machineSpecLine(m.hardware)) + '</div>' +
|
|
4602
|
+
'<div class="machine-meta">' + tail + '</div>' +
|
|
4603
|
+
'</div>';
|
|
4604
|
+
}
|
|
4605
|
+
async function saveMachineNickname(input) {
|
|
4606
|
+
const mid = input.getAttribute('data-mid');
|
|
4607
|
+
const nickname = input.value.trim();
|
|
4608
|
+
try {
|
|
4609
|
+
await apiFetch('/pool/machines/' + encodeURIComponent(mid), {
|
|
4610
|
+
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname }),
|
|
4611
|
+
});
|
|
4612
|
+
loadMachines();
|
|
4613
|
+
} catch (e) {
|
|
4614
|
+
const hl = document.getElementById('machinesHeadline');
|
|
4615
|
+
if (hl) hl.innerHTML = '<div class="ph-headline">Couldn\'t rename that machine: ' + escapeHtml(e.message) + '</div>';
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
async function loadMachines() {
|
|
4619
|
+
const stamp = document.getElementById('machinesStamp');
|
|
4620
|
+
const list = document.getElementById('machinesList');
|
|
4621
|
+
const headline = document.getElementById('machinesHeadline');
|
|
4622
|
+
if (!list) return;
|
|
4623
|
+
try {
|
|
4624
|
+
const data = await apiFetch('/pool');
|
|
4625
|
+
if (stamp) stamp.textContent = new Date().toLocaleTimeString();
|
|
4626
|
+
if (!data.enabled) {
|
|
4627
|
+
if (headline) headline.innerHTML = '<div class="ph-headline">Running on a single machine right now — nothing to spread across yet.</div>';
|
|
4628
|
+
list.innerHTML = '';
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
const machines = data.machines || [];
|
|
4632
|
+
const onlineCount = machines.filter(m => m.online).length;
|
|
4633
|
+
if (headline) headline.innerHTML = '<div class="ph-headline">' + onlineCount + ' of ' + machines.length + ' machine' + (machines.length === 1 ? '' : 's') + ' online</div>';
|
|
4634
|
+
const routerHolder = data.router && data.router.holder;
|
|
4635
|
+
list.innerHTML = machines.length ? machines.map(m => renderMachineCard(m, routerHolder)).join('') : '<div class="machine-meta">No machines paired yet.</div>';
|
|
4636
|
+
} catch (e) {
|
|
4637
|
+
if (headline) headline.innerHTML = '<div class="ph-headline">Couldn\'t load machines: ' + escapeHtml(e.message) + '</div>';
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4533
4641
|
// ── Tokens Tab ──────────────────────────────────────────────────
|
|
4534
4642
|
function fmtTokens(n) {
|
|
4535
4643
|
if (n == null) return '0';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAquDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8yOtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -298,6 +298,15 @@ let _fixDeps = null;
|
|
|
298
298
|
// Module-level reference for session resume mapping.
|
|
299
299
|
// Set once in startServer() and used by spawnSessionForTopic/respawnSessionForTopic.
|
|
300
300
|
let _topicResumeMap = null;
|
|
301
|
+
// ── Multi-Machine Session Pool (§L4) activation refs ──────────────────────
|
|
302
|
+
// The SessionRouter is constructed in startServer()'s mesh block, but the inbound
|
|
303
|
+
// dispatch handler that consults it is defined ABOVE startServer — so the router is
|
|
304
|
+
// shared via this module-level ref (the same pattern as _orphanReaper/_topicResumeMap).
|
|
305
|
+
// `_sessionPoolStage()` reads the live rollout stage; the inbound interception only
|
|
306
|
+
// routes through the pool when it returns a non-'dark' stage, so DARK (the default)
|
|
307
|
+
// is byte-identical to today's always-local dispatch. Set once in startServer().
|
|
308
|
+
let _sessionRouter = null;
|
|
309
|
+
let _sessionPoolStage = () => 'dark';
|
|
301
310
|
/** Per-topic framework override (claude-code | codex-cli). Populated from
|
|
302
311
|
* `config.topicFrameworks` at server boot. Boot-immutable; runtime
|
|
303
312
|
* mutations go through `_topicFrameworksStore` instead so they persist
|
|
@@ -1156,6 +1165,31 @@ getHubDeps) {
|
|
|
1156
1165
|
const pipeline = messageToPipeline(msg, storedTopicName);
|
|
1157
1166
|
// Route message to corresponding session
|
|
1158
1167
|
const targetSession = telegram.getSessionForTopic(topicId);
|
|
1168
|
+
// ── Multi-Machine Session Pool (§L4): route through the pool when active ──
|
|
1169
|
+
// When the rollout stage is past 'dark', consult the SessionRouter: it may
|
|
1170
|
+
// forward this topic's message to the machine that OWNS the session (over the
|
|
1171
|
+
// mesh) instead of handling it locally. DARK (the default) skips this block
|
|
1172
|
+
// entirely → byte-identical to single-machine dispatch. Any error falls back
|
|
1173
|
+
// to the local path below (fail-safe). The live-transfer behavior is gated by
|
|
1174
|
+
// the staged rollout (StageAdvancer); shadow records ownership but stays local.
|
|
1175
|
+
if (_sessionRouter && _sessionPoolStage() !== 'dark') {
|
|
1176
|
+
try {
|
|
1177
|
+
const outcome = await _sessionRouter.route({
|
|
1178
|
+
sessionKey: String(topicId),
|
|
1179
|
+
messageId: String(msg.id),
|
|
1180
|
+
payload: text,
|
|
1181
|
+
});
|
|
1182
|
+
if (outcome.action === 'forwarded' || outcome.action === 'duplicate') {
|
|
1183
|
+
console.log(`[session-pool] topic ${topicId} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
// 'handled-locally' / 'spawned'(self) / 'owner-dead-replaced' / 'queued' /
|
|
1187
|
+
// 'placement-blocked' → fall through to the existing local dispatch below.
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
console.warn(`[session-pool] route error for topic ${topicId} — falling back to local dispatch: ${err instanceof Error ? err.message : String(err)}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1159
1193
|
if (targetSession) {
|
|
1160
1194
|
// Session is mapped — check if it's alive, inject or respawn
|
|
1161
1195
|
if (sessionManager.isSessionAlive(targetSession)) {
|
|
@@ -8167,7 +8201,309 @@ export async function startServer(options) {
|
|
|
8167
8201
|
}
|
|
8168
8202
|
}
|
|
8169
8203
|
}
|
|
8170
|
-
|
|
8204
|
+
// ── Multi-Machine Session Pool registry (§L2) — live MachineCapacity view for GET /pool ──
|
|
8205
|
+
// Always instantiated (cheap); the feature stays dark behind the sessionPool stage gate.
|
|
8206
|
+
// Self-attests hardware + records a self-heartbeat so GET /pool + the Machines tab show
|
|
8207
|
+
// this machine online with its specs; peer liveness is fed from MachineHeartbeat.
|
|
8208
|
+
let machinePoolRegistry;
|
|
8209
|
+
try {
|
|
8210
|
+
const poolMod = await import('../core/MachinePoolRegistry.js');
|
|
8211
|
+
const osMod = await import('node:os');
|
|
8212
|
+
const poolIdMgr = coordinator?.managers?.identityManager;
|
|
8213
|
+
if (poolIdMgr) {
|
|
8214
|
+
const failoverThresholdMs = (config.multiMachine?.failoverTimeoutMinutes ?? 15) * 60_000;
|
|
8215
|
+
const clockSkewToleranceMs = config.multiMachine?.sessionPool?.clockSkewToleranceMs ?? 300_000;
|
|
8216
|
+
machinePoolRegistry = new poolMod.MachinePoolRegistry({
|
|
8217
|
+
listMachines: () => poolIdMgr.getActiveMachines().map(({ machineId, entry }) => ({
|
|
8218
|
+
machineId,
|
|
8219
|
+
nickname: entry.nickname,
|
|
8220
|
+
hardware: entry.hardware,
|
|
8221
|
+
})),
|
|
8222
|
+
clockSkewToleranceMs,
|
|
8223
|
+
failoverThresholdMs,
|
|
8224
|
+
logger: (m) => console.log(pc.dim(` [pool] ${m}`)),
|
|
8225
|
+
});
|
|
8226
|
+
const poolSelfId = machineHeartbeat?.config?.machineId ??
|
|
8227
|
+
(poolIdMgr.hasIdentity() ? poolIdMgr.loadIdentity().machineId : null);
|
|
8228
|
+
if (poolSelfId) {
|
|
8229
|
+
try {
|
|
8230
|
+
poolIdMgr.recordSelfHardware(poolSelfId, poolMod.captureHardware());
|
|
8231
|
+
}
|
|
8232
|
+
catch { /* best-effort hardware self-attest */ }
|
|
8233
|
+
const refreshPool = () => {
|
|
8234
|
+
try {
|
|
8235
|
+
machinePoolRegistry.recordHeartbeat({
|
|
8236
|
+
machineId: poolSelfId,
|
|
8237
|
+
selfReportedLastSeen: new Date().toISOString(),
|
|
8238
|
+
loadAvg: osMod.loadavg()[0],
|
|
8239
|
+
});
|
|
8240
|
+
const hbApi = machineHeartbeat?.api;
|
|
8241
|
+
if (hbApi) {
|
|
8242
|
+
for (const r of hbApi.listAll()) {
|
|
8243
|
+
if (r.machineId !== poolSelfId) {
|
|
8244
|
+
machinePoolRegistry.recordHeartbeat({ machineId: r.machineId, selfReportedLastSeen: r.lastHeartbeatAt });
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
catch { /* best-effort pool refresh */ }
|
|
8250
|
+
};
|
|
8251
|
+
refreshPool();
|
|
8252
|
+
const poolTimer = setInterval(refreshPool, 30_000);
|
|
8253
|
+
if (typeof poolTimer.unref === 'function')
|
|
8254
|
+
poolTimer.unref();
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
catch (err) {
|
|
8259
|
+
console.log(pc.dim(` [pool] registry not wired: ${err instanceof Error ? err.message : String(err)}`));
|
|
8260
|
+
}
|
|
8261
|
+
// ── MeshRpc dispatcher (§L0) + SessionOwnership registry (§L3) ──
|
|
8262
|
+
// Built when there's a machine identity. The ownership registry (L3) feeds
|
|
8263
|
+
// the dispatcher's RBAC (ownerOf/placementTargetOf) + the place/claim/
|
|
8264
|
+
// transfer/release handlers; read-class handlers (capacity/session-status)
|
|
8265
|
+
// are live too. Single-machine uses the in-memory store; the cross-machine
|
|
8266
|
+
// git-backed store swaps in for the Track-H proof (the registry is store-agnostic).
|
|
8267
|
+
let meshRpcDispatcher;
|
|
8268
|
+
let sessionOwnershipRegistry;
|
|
8269
|
+
try {
|
|
8270
|
+
const meshMod = await import('../core/MeshRpc.js');
|
|
8271
|
+
const idMod = await import('../core/MachineIdentity.js');
|
|
8272
|
+
const meshIdMgr = coordinator?.managers?.identityManager;
|
|
8273
|
+
const meshSelfId = machineHeartbeat?.config?.machineId
|
|
8274
|
+
?? (meshIdMgr?.hasIdentity() ? meshIdMgr.loadIdentity().machineId : null);
|
|
8275
|
+
if (meshIdMgr && meshSelfId) {
|
|
8276
|
+
const meshClockToleranceMs = config.multiMachine?.sessionPool?.meshRpcClockToleranceMs ?? 30000;
|
|
8277
|
+
const seenMeshNonces = new Map(); // `${sender}:${nonce}` → ts, age-pruned
|
|
8278
|
+
const meshPruneMs = Math.max(meshClockToleranceMs * 4, 120_000);
|
|
8279
|
+
// SessionOwnership registry (§L3) — in-memory store (single-machine correct;
|
|
8280
|
+
// git-backed cross-machine store is the Track-H swap). Per-session nonce set.
|
|
8281
|
+
const ownMod = await import('../core/SessionOwnershipRegistry.js');
|
|
8282
|
+
const seenOwnNonces = new Set();
|
|
8283
|
+
sessionOwnershipRegistry = new ownMod.SessionOwnershipRegistry({
|
|
8284
|
+
store: new ownMod.InMemorySessionOwnershipStore(),
|
|
8285
|
+
seenNonce: (k) => seenOwnNonces.has(k),
|
|
8286
|
+
recordNonce: (k) => seenOwnNonces.add(k),
|
|
8287
|
+
logger: (m) => console.log(pc.dim(` ${m}`)),
|
|
8288
|
+
});
|
|
8289
|
+
const ownReg = sessionOwnershipRegistry;
|
|
8290
|
+
// The §L3 ownership commands, routed from MeshRpc to the registry CAS.
|
|
8291
|
+
const ownAction = (cmd, sender, env) => {
|
|
8292
|
+
if (cmd.type === 'place')
|
|
8293
|
+
return ownReg.cas({ type: 'place', machineId: cmd.machine }, { sessionKey: cmd.session, sender, nonce: env.nonce });
|
|
8294
|
+
if (cmd.type === 'claim')
|
|
8295
|
+
return ownReg.cas({ type: 'claim', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
|
|
8296
|
+
if (cmd.type === 'transfer')
|
|
8297
|
+
return ownReg.cas({ type: 'transfer', to: cmd.target }, { sessionKey: cmd.session, sender, nonce: env.nonce });
|
|
8298
|
+
if (cmd.type === 'release')
|
|
8299
|
+
return ownReg.cas({ type: 'release', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
|
|
8300
|
+
return { ok: false, reason: 'unsupported' };
|
|
8301
|
+
};
|
|
8302
|
+
// The §L4 owner-side deliverMessage receive handler (shared factory — same
|
|
8303
|
+
// code path the tests exercise). Durable-receipt-before-processing with
|
|
8304
|
+
// idempotent dedupe on messageId + the stale-ownership fence. Returns the
|
|
8305
|
+
// deliverMessageAck the router waits on before advancing the platform offset.
|
|
8306
|
+
// (Local processing hand-off to SessionManager is the Track-H staged
|
|
8307
|
+
// activation; the durable receipt + ACK is the complete dark-phase contract.)
|
|
8308
|
+
const deliverMod = await import('../core/DeliverMessageHandler.js');
|
|
8309
|
+
const deliverSeenFallback = new Set(); // used only if the SQLite ledger is unavailable
|
|
8310
|
+
const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
|
|
8311
|
+
ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
|
|
8312
|
+
recordReceipt: (messageId, session) => {
|
|
8313
|
+
if (messageLedger)
|
|
8314
|
+
return messageLedger.record(messageId, { platform: 'mesh', topic: session }).firstSeen;
|
|
8315
|
+
if (deliverSeenFallback.has(messageId))
|
|
8316
|
+
return false;
|
|
8317
|
+
deliverSeenFallback.add(messageId);
|
|
8318
|
+
return true;
|
|
8319
|
+
},
|
|
8320
|
+
// Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
|
|
8321
|
+
// the local session for the topic so the conversation continues on THIS machine.
|
|
8322
|
+
// Only fires for a FIRST-seen forwarded deliverMessage (the ledger dedupes
|
|
8323
|
+
// redeliveries), and a deliverMessage only arrives from a router peer — but we
|
|
8324
|
+
// double-gate on stage!=='dark' to be safe. Fire-and-forget: the durable receipt
|
|
8325
|
+
// is already recorded + ACKed before this runs.
|
|
8326
|
+
onAccepted: (cmd) => {
|
|
8327
|
+
if (_sessionPoolStage() === 'dark' || !telegram)
|
|
8328
|
+
return;
|
|
8329
|
+
const tg = telegram;
|
|
8330
|
+
const topicId = Number(cmd.session);
|
|
8331
|
+
if (!Number.isFinite(topicId))
|
|
8332
|
+
return;
|
|
8333
|
+
const text = typeof cmd.payload === 'string'
|
|
8334
|
+
? cmd.payload
|
|
8335
|
+
: (cmd.payload && typeof cmd.payload === 'object' && 'text' in cmd.payload)
|
|
8336
|
+
? String(cmd.payload.text)
|
|
8337
|
+
: undefined;
|
|
8338
|
+
const sessionName = tg.getSessionForTopic(topicId) ?? `topic-${topicId}`;
|
|
8339
|
+
void spawnSessionForTopic(sessionManager, tg, sessionName, topicId, text, undefined, undefined)
|
|
8340
|
+
.then((name) => {
|
|
8341
|
+
tg.registerTopicSession(topicId, name, sessionName);
|
|
8342
|
+
console.log(pc.green(` [session-pool] owner-side resume for forwarded topic ${topicId} → ${name}`));
|
|
8343
|
+
})
|
|
8344
|
+
.catch((err) => console.warn(` [session-pool] owner-side resume failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8345
|
+
},
|
|
8346
|
+
});
|
|
8347
|
+
meshRpcDispatcher = new meshMod.MeshRpcDispatcher({
|
|
8348
|
+
verify: {
|
|
8349
|
+
selfMachineId: meshSelfId,
|
|
8350
|
+
verify: (canonical, signature, sender) => {
|
|
8351
|
+
const pem = meshIdMgr.getSigningPublicKeyPem(sender);
|
|
8352
|
+
if (!pem)
|
|
8353
|
+
return false;
|
|
8354
|
+
try {
|
|
8355
|
+
return idMod.verify(canonical, signature, pem);
|
|
8356
|
+
}
|
|
8357
|
+
catch {
|
|
8358
|
+
return false;
|
|
8359
|
+
}
|
|
8360
|
+
},
|
|
8361
|
+
isRegisteredPeer: (s) => meshIdMgr.isMachineActive(s),
|
|
8362
|
+
seenNonce: (s, n) => seenMeshNonces.has(`${s}:${n}`),
|
|
8363
|
+
now: () => Date.now(),
|
|
8364
|
+
clockToleranceMs: meshClockToleranceMs,
|
|
8365
|
+
},
|
|
8366
|
+
rbac: {
|
|
8367
|
+
routerHolder: () => coordinator?.getSyncStatus().leaseHolder ?? null,
|
|
8368
|
+
ownerOf: (s) => ownReg.ownerOf(s),
|
|
8369
|
+
placementTargetOf: (s) => ownReg.placementTargetOf(s),
|
|
8370
|
+
},
|
|
8371
|
+
recordNonce: (s, n) => {
|
|
8372
|
+
const t = Date.now();
|
|
8373
|
+
seenMeshNonces.set(`${s}:${n}`, t);
|
|
8374
|
+
if (seenMeshNonces.size > 5000) {
|
|
8375
|
+
for (const [k, v] of seenMeshNonces)
|
|
8376
|
+
if (t - v > meshPruneMs)
|
|
8377
|
+
seenMeshNonces.delete(k);
|
|
8378
|
+
}
|
|
8379
|
+
},
|
|
8380
|
+
handlers: {
|
|
8381
|
+
'capacity-report': () => machinePoolRegistry?.getCapacities() ?? [],
|
|
8382
|
+
'session-status': () => machinePoolRegistry?.getCapacity(meshSelfId) ?? { machineId: meshSelfId },
|
|
8383
|
+
place: ownAction,
|
|
8384
|
+
claim: ownAction,
|
|
8385
|
+
transfer: ownAction,
|
|
8386
|
+
release: ownAction,
|
|
8387
|
+
deliverMessage: deliverMessageHandler,
|
|
8388
|
+
},
|
|
8389
|
+
logger: (m) => console.log(pc.dim(` ${m}`)),
|
|
8390
|
+
});
|
|
8391
|
+
// ── L4 SessionRouter (activation transport) — shared via _sessionRouter ──
|
|
8392
|
+
// Constructed with the real registry/ownership/placement + the outbound
|
|
8393
|
+
// MeshRpcClient so it can reach peers. INERT until the rollout stage advances
|
|
8394
|
+
// past 'dark': the inbound dispatch interception only consults it when
|
|
8395
|
+
// _sessionPoolStage() !== 'dark'. Single-machine → placement keeps everything local.
|
|
8396
|
+
try {
|
|
8397
|
+
const routerMod = await import('../core/SessionRouter.js');
|
|
8398
|
+
const placeMod = await import('../core/PlacementExecutor.js');
|
|
8399
|
+
const clientMod = await import('../core/MeshRpcClient.js');
|
|
8400
|
+
let routerNonce = 0;
|
|
8401
|
+
const meshClient = new clientMod.MeshRpcClient({
|
|
8402
|
+
selfMachineId: meshSelfId,
|
|
8403
|
+
sign: (c) => idMod.sign(c, localSigningKeyPem),
|
|
8404
|
+
nonce: () => `${meshSelfId}:r:${Date.now()}:${++routerNonce}`,
|
|
8405
|
+
now: () => Date.now(),
|
|
8406
|
+
});
|
|
8407
|
+
const peerUrl = (machineId) => meshIdMgr.getActiveMachines().find((m) => m.machineId === machineId)?.entry.lastKnownUrl ?? null;
|
|
8408
|
+
_sessionRouter = new routerMod.SessionRouter({
|
|
8409
|
+
selfMachineId: meshSelfId,
|
|
8410
|
+
placement: new placeMod.PlacementExecutor(),
|
|
8411
|
+
machineRegistry: () => machinePoolRegistry?.getCapacities() ?? [],
|
|
8412
|
+
resolveOwnership: (sk) => {
|
|
8413
|
+
const r = ownReg.read(sk);
|
|
8414
|
+
if (!r)
|
|
8415
|
+
return { owner: null, epoch: 0, status: null };
|
|
8416
|
+
const status = r.status === 'released' ? null : r.status;
|
|
8417
|
+
return { owner: ownReg.ownerOf(sk), epoch: r.ownershipEpoch, status, target: ownReg.placementTargetOf(sk) ?? undefined };
|
|
8418
|
+
},
|
|
8419
|
+
isMachineAlive: (m) => m === meshSelfId || (machinePoolRegistry?.getCapacity(m)?.online ?? false),
|
|
8420
|
+
casClaimOwnership: (sk, machineId) => {
|
|
8421
|
+
const r = ownReg.cas({ type: 'place', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:c:${++routerNonce}` });
|
|
8422
|
+
return { ok: r.ok, epoch: ownReg.read(sk)?.ownershipEpoch ?? 0 };
|
|
8423
|
+
},
|
|
8424
|
+
deliverMessage: async (target, env) => {
|
|
8425
|
+
const url = peerUrl(target);
|
|
8426
|
+
if (!url)
|
|
8427
|
+
throw new Error(`no peer url for ${target}`);
|
|
8428
|
+
const res = await meshClient.send({ machineId: target, url }, { type: 'deliverMessage', session: env.sessionKey, messageId: env.messageId, payload: env.payload, ownershipEpoch: env.ownershipEpoch }, env.ownershipEpoch);
|
|
8429
|
+
if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
|
|
8430
|
+
return res.result;
|
|
8431
|
+
}
|
|
8432
|
+
throw new Error(`deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
|
|
8433
|
+
},
|
|
8434
|
+
spawnOnMachine: async (machineId, msg) => {
|
|
8435
|
+
const url = peerUrl(machineId);
|
|
8436
|
+
if (!url)
|
|
8437
|
+
throw new Error(`no peer url for ${machineId}`);
|
|
8438
|
+
await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: 0 }, 0);
|
|
8439
|
+
},
|
|
8440
|
+
handleLocally: async () => { },
|
|
8441
|
+
queueMessage: () => { },
|
|
8442
|
+
raiseAttention: (title, body) => console.log(pc.dim(` [session-router] attention: ${title} — ${body}`)),
|
|
8443
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
8444
|
+
log: (line) => console.log(pc.dim(` [session-router] ${line}`)),
|
|
8445
|
+
});
|
|
8446
|
+
_sessionPoolStage = () => {
|
|
8447
|
+
try {
|
|
8448
|
+
const fallback = (config.multiMachine?.sessionPool ?? {});
|
|
8449
|
+
const live = liveConfig.get('multiMachine.sessionPool', fallback);
|
|
8450
|
+
return live?.enabled && live?.stage ? String(live.stage) : 'dark';
|
|
8451
|
+
}
|
|
8452
|
+
catch {
|
|
8453
|
+
return 'dark';
|
|
8454
|
+
}
|
|
8455
|
+
};
|
|
8456
|
+
console.log(pc.green(' SessionRouter wired (L4) — inert until rollout stage advances past dark'));
|
|
8457
|
+
}
|
|
8458
|
+
catch (err) {
|
|
8459
|
+
console.log(pc.dim(` [session-router] not wired: ${err instanceof Error ? err.message : String(err)}`));
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
catch (err) {
|
|
8464
|
+
console.log(pc.dim(` [mesh-rpc] dispatcher not wired: ${err instanceof Error ? err.message : String(err)}`));
|
|
8465
|
+
}
|
|
8466
|
+
// ── Rollout gate (§Rollout): signed E2E result store + StageAdvancer ──────────
|
|
8467
|
+
// The E2E result store backs GET /session-pool/e2e-results (observable gate state);
|
|
8468
|
+
// StageAdvancer is the SOLE writer of multiMachine.sessionPool.stage (it passes the
|
|
8469
|
+
// stageWriteGuard token; any other write throws stage-write-not-permitted). Always
|
|
8470
|
+
// instantiated (cheap); inert until the pool activates past 'dark'.
|
|
8471
|
+
let sessionPoolE2EResultStore;
|
|
8472
|
+
try {
|
|
8473
|
+
const e2eMod = await import('../core/SessionPoolE2EResultStore.js');
|
|
8474
|
+
const stageMod = await import('../core/StageAdvancer.js');
|
|
8475
|
+
const guardMod = await import('../config/stageWriteGuard.js');
|
|
8476
|
+
const crypto = await import('node:crypto');
|
|
8477
|
+
// HMAC over the agent's authToken — tamper-evident without a separate keystore.
|
|
8478
|
+
const hmacKey = String(config.authToken ?? 'instar-session-pool-e2e');
|
|
8479
|
+
const hmac = (c) => crypto.createHmac('sha256', hmacKey).update(c).digest('hex');
|
|
8480
|
+
sessionPoolE2EResultStore = new e2eMod.SessionPoolE2EResultStore({
|
|
8481
|
+
filePath: path.join(config.stateDir, 'session-pool-e2e-results.json'),
|
|
8482
|
+
sign: hmac,
|
|
8483
|
+
verifySig: (c, s) => {
|
|
8484
|
+
try {
|
|
8485
|
+
const exp = hmac(c);
|
|
8486
|
+
return s.length === exp.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(exp));
|
|
8487
|
+
}
|
|
8488
|
+
catch {
|
|
8489
|
+
return false;
|
|
8490
|
+
}
|
|
8491
|
+
},
|
|
8492
|
+
});
|
|
8493
|
+
// StageAdvancer: the sole stage writer. Held for the rollout job/route to drive;
|
|
8494
|
+
// constructed here so the write path (liveConfig + token) is wired in one place.
|
|
8495
|
+
void new stageMod.StageAdvancer({
|
|
8496
|
+
resultStore: sessionPoolE2EResultStore,
|
|
8497
|
+
currentCommitSha: () => process.env.INSTAR_COMMIT_SHA ?? process.env.GITHUB_SHA ?? 'unknown',
|
|
8498
|
+
readStage: () => liveConfig.get('multiMachine.sessionPool.stage', 'dark'),
|
|
8499
|
+
writeStageConfig: (s) => liveConfig.set(guardMod.STAGE_CONFIG_PATH, s, { stageWriteToken: guardMod.STAGE_WRITE_TOKEN }),
|
|
8500
|
+
audit: (event, detail) => console.log(pc.dim(` [stage-advancer] ${event} ${JSON.stringify(detail)}`)),
|
|
8501
|
+
});
|
|
8502
|
+
}
|
|
8503
|
+
catch (err) {
|
|
8504
|
+
console.log(pc.dim(` [session-pool] rollout gate not wired: ${err instanceof Error ? err.message : String(err)}`));
|
|
8505
|
+
}
|
|
8506
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, sessionOwnershipRegistry, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, reapLog, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
|
|
8171
8507
|
// Boot-recovery (tunnel-failure-resilience spec Part 6): if the agent
|
|
8172
8508
|
// died mid-relay-episode, the persisted tunnel.json carries
|
|
8173
8509
|
// rotationPending=true. Rotate the dashboard PIN + authToken BEFORE
|