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.
Files changed (143) hide show
  1. package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +17 -0
  2. package/dashboard/index.html +108 -0
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +337 -1
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +30 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/config/LiveConfig.d.ts +3 -1
  10. package/dist/config/LiveConfig.d.ts.map +1 -1
  11. package/dist/config/LiveConfig.js +4 -1
  12. package/dist/config/LiveConfig.js.map +1 -1
  13. package/dist/config/stageWriteGuard.d.ts +23 -0
  14. package/dist/config/stageWriteGuard.d.ts.map +1 -0
  15. package/dist/config/stageWriteGuard.js +33 -0
  16. package/dist/config/stageWriteGuard.js.map +1 -0
  17. package/dist/core/DeliverMessageHandler.d.ts +33 -0
  18. package/dist/core/DeliverMessageHandler.d.ts.map +1 -0
  19. package/dist/core/DeliverMessageHandler.js +34 -0
  20. package/dist/core/DeliverMessageHandler.js.map +1 -0
  21. package/dist/core/LeaseCoordinator.d.ts +39 -2
  22. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  23. package/dist/core/LeaseCoordinator.js +45 -9
  24. package/dist/core/LeaseCoordinator.js.map +1 -1
  25. package/dist/core/MachineIdentity.d.ts +28 -1
  26. package/dist/core/MachineIdentity.d.ts.map +1 -1
  27. package/dist/core/MachineIdentity.js +81 -1
  28. package/dist/core/MachineIdentity.js.map +1 -1
  29. package/dist/core/MachineLoadBroker.d.ts +94 -0
  30. package/dist/core/MachineLoadBroker.d.ts.map +1 -0
  31. package/dist/core/MachineLoadBroker.js +87 -0
  32. package/dist/core/MachineLoadBroker.js.map +1 -0
  33. package/dist/core/MachinePoolRegistry.d.ts +94 -0
  34. package/dist/core/MachinePoolRegistry.d.ts.map +1 -0
  35. package/dist/core/MachinePoolRegistry.js +150 -0
  36. package/dist/core/MachinePoolRegistry.js.map +1 -0
  37. package/dist/core/MeshRpc.d.ts +165 -0
  38. package/dist/core/MeshRpc.d.ts.map +1 -0
  39. package/dist/core/MeshRpc.js +156 -0
  40. package/dist/core/MeshRpc.js.map +1 -0
  41. package/dist/core/MeshRpcClient.d.ts +59 -0
  42. package/dist/core/MeshRpcClient.d.ts.map +1 -0
  43. package/dist/core/MeshRpcClient.js +58 -0
  44. package/dist/core/MeshRpcClient.js.map +1 -0
  45. package/dist/core/MultiMachineCoordinator.d.ts +11 -0
  46. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  47. package/dist/core/MultiMachineCoordinator.js +13 -0
  48. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  49. package/dist/core/NicknameAssigner.d.ts +51 -0
  50. package/dist/core/NicknameAssigner.d.ts.map +1 -0
  51. package/dist/core/NicknameAssigner.js +91 -0
  52. package/dist/core/NicknameAssigner.js.map +1 -0
  53. package/dist/core/NicknameCommand.d.ts +28 -0
  54. package/dist/core/NicknameCommand.d.ts.map +1 -0
  55. package/dist/core/NicknameCommand.js +69 -0
  56. package/dist/core/NicknameCommand.js.map +1 -0
  57. package/dist/core/PlacementExecutor.d.ts +71 -0
  58. package/dist/core/PlacementExecutor.d.ts.map +1 -0
  59. package/dist/core/PlacementExecutor.js +156 -0
  60. package/dist/core/PlacementExecutor.js.map +1 -0
  61. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  62. package/dist/core/PostUpdateMigrator.js +21 -0
  63. package/dist/core/PostUpdateMigrator.js.map +1 -1
  64. package/dist/core/RebalancePlanner.d.ts +48 -0
  65. package/dist/core/RebalancePlanner.d.ts.map +1 -0
  66. package/dist/core/RebalancePlanner.js +71 -0
  67. package/dist/core/RebalancePlanner.js.map +1 -0
  68. package/dist/core/SessionOwnership.d.ts +103 -0
  69. package/dist/core/SessionOwnership.d.ts.map +1 -0
  70. package/dist/core/SessionOwnership.js +0 -0
  71. package/dist/core/SessionOwnership.js.map +1 -0
  72. package/dist/core/SessionOwnershipRegistry.d.ts +115 -0
  73. package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -0
  74. package/dist/core/SessionOwnershipRegistry.js +128 -0
  75. package/dist/core/SessionOwnershipRegistry.js.map +1 -0
  76. package/dist/core/SessionPoolE2EResultStore.d.ts +45 -0
  77. package/dist/core/SessionPoolE2EResultStore.d.ts.map +1 -0
  78. package/dist/core/SessionPoolE2EResultStore.js +66 -0
  79. package/dist/core/SessionPoolE2EResultStore.js.map +1 -0
  80. package/dist/core/SessionRouter.d.ts +95 -0
  81. package/dist/core/SessionRouter.d.ts.map +1 -0
  82. package/dist/core/SessionRouter.js +151 -0
  83. package/dist/core/SessionRouter.js.map +1 -0
  84. package/dist/core/StageAdvancer.d.ts +47 -0
  85. package/dist/core/StageAdvancer.d.ts.map +1 -0
  86. package/dist/core/StageAdvancer.js +70 -0
  87. package/dist/core/StageAdvancer.js.map +1 -0
  88. package/dist/core/TransferByNickname.d.ts +51 -0
  89. package/dist/core/TransferByNickname.d.ts.map +1 -0
  90. package/dist/core/TransferByNickname.js +50 -0
  91. package/dist/core/TransferByNickname.js.map +1 -0
  92. package/dist/core/TransferOrchestrator.d.ts +113 -0
  93. package/dist/core/TransferOrchestrator.d.ts.map +1 -0
  94. package/dist/core/TransferOrchestrator.js +79 -0
  95. package/dist/core/TransferOrchestrator.js.map +1 -0
  96. package/dist/core/types.d.ts +177 -1
  97. package/dist/core/types.d.ts.map +1 -1
  98. package/dist/core/types.js.map +1 -1
  99. package/dist/scaffold/templates.d.ts.map +1 -1
  100. package/dist/scaffold/templates.js +5 -0
  101. package/dist/scaffold/templates.js.map +1 -1
  102. package/dist/server/AgentServer.d.ts +8 -0
  103. package/dist/server/AgentServer.d.ts.map +1 -1
  104. package/dist/server/AgentServer.js +4 -0
  105. package/dist/server/AgentServer.js.map +1 -1
  106. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  107. package/dist/server/CapabilityIndex.js +14 -0
  108. package/dist/server/CapabilityIndex.js.map +1 -1
  109. package/dist/server/routes.d.ts +12 -0
  110. package/dist/server/routes.d.ts.map +1 -1
  111. package/dist/server/routes.js +91 -0
  112. package/dist/server/routes.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/data/builtin-manifest.json +64 -64
  115. package/src/scaffold/templates.ts +5 -0
  116. package/upgrades/1.3.92.md +27 -0
  117. package/upgrades/1.3.93.md +28 -0
  118. package/upgrades/side-effects/session-pool-activation-wiring.md +22 -0
  119. package/upgrades/side-effects/session-pool-capability-classification.md +16 -0
  120. package/upgrades/side-effects/session-pool-review-fixes.md +23 -0
  121. package/upgrades/side-effects/session-pool-track-a-router-lease.md +29 -0
  122. package/upgrades/side-effects/session-pool-track-b-agent-awareness.md +29 -0
  123. package/upgrades/side-effects/session-pool-track-b-nicknames.md +35 -0
  124. package/upgrades/side-effects/session-pool-track-b-pool-api.md +36 -0
  125. package/upgrades/side-effects/session-pool-track-b-registry.md +31 -0
  126. package/upgrades/side-effects/session-pool-track-b-wiring.md +39 -0
  127. package/upgrades/side-effects/session-pool-track-c-dispatcher.md +24 -0
  128. package/upgrades/side-effects/session-pool-track-c-meshrpc.md +35 -0
  129. package/upgrades/side-effects/session-pool-track-c-route-wiring.md +31 -0
  130. package/upgrades/side-effects/session-pool-track-d-ownership-fsm.md +31 -0
  131. package/upgrades/side-effects/session-pool-track-d-registry.md +27 -0
  132. package/upgrades/side-effects/session-pool-track-d-wiring.md +30 -0
  133. package/upgrades/side-effects/session-pool-track-e-boot-wiring.md +20 -0
  134. package/upgrades/side-effects/session-pool-track-e-placement-core.md +17 -0
  135. package/upgrades/side-effects/session-pool-track-e-router.md +23 -0
  136. package/upgrades/side-effects/session-pool-track-f-config.md +12 -0
  137. package/upgrades/side-effects/session-pool-track-f-handoff-core.md +17 -0
  138. package/upgrades/side-effects/session-pool-track-g-load-broker.md +16 -0
  139. package/upgrades/side-effects/session-pool-track-h-gate-wiring.md +17 -0
  140. package/upgrades/side-effects/session-pool-track-h-mesh-client.md +15 -0
  141. package/upgrades/side-effects/session-pool-track-h-rebalance.md +13 -0
  142. package/upgrades/side-effects/session-pool-track-h-rollout-gate.md +17 -0
  143. 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"
@@ -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;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuhOtE;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"}
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"}
@@ -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
- 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, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, reapLog, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
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