instar 1.3.90 → 1.3.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) 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 +214 -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/SessionManager.d.ts +20 -3
  69. package/dist/core/SessionManager.d.ts.map +1 -1
  70. package/dist/core/SessionManager.js +54 -6
  71. package/dist/core/SessionManager.js.map +1 -1
  72. package/dist/core/SessionOwnership.d.ts +103 -0
  73. package/dist/core/SessionOwnership.d.ts.map +1 -0
  74. package/dist/core/SessionOwnership.js +0 -0
  75. package/dist/core/SessionOwnership.js.map +1 -0
  76. package/dist/core/SessionOwnershipRegistry.d.ts +115 -0
  77. package/dist/core/SessionOwnershipRegistry.d.ts.map +1 -0
  78. package/dist/core/SessionOwnershipRegistry.js +128 -0
  79. package/dist/core/SessionOwnershipRegistry.js.map +1 -0
  80. package/dist/core/SessionPoolE2EResultStore.d.ts +45 -0
  81. package/dist/core/SessionPoolE2EResultStore.d.ts.map +1 -0
  82. package/dist/core/SessionPoolE2EResultStore.js +66 -0
  83. package/dist/core/SessionPoolE2EResultStore.js.map +1 -0
  84. package/dist/core/SessionRouter.d.ts +95 -0
  85. package/dist/core/SessionRouter.d.ts.map +1 -0
  86. package/dist/core/SessionRouter.js +151 -0
  87. package/dist/core/SessionRouter.js.map +1 -0
  88. package/dist/core/StageAdvancer.d.ts +47 -0
  89. package/dist/core/StageAdvancer.d.ts.map +1 -0
  90. package/dist/core/StageAdvancer.js +70 -0
  91. package/dist/core/StageAdvancer.js.map +1 -0
  92. package/dist/core/TransferByNickname.d.ts +51 -0
  93. package/dist/core/TransferByNickname.d.ts.map +1 -0
  94. package/dist/core/TransferByNickname.js +50 -0
  95. package/dist/core/TransferByNickname.js.map +1 -0
  96. package/dist/core/TransferOrchestrator.d.ts +113 -0
  97. package/dist/core/TransferOrchestrator.d.ts.map +1 -0
  98. package/dist/core/TransferOrchestrator.js +79 -0
  99. package/dist/core/TransferOrchestrator.js.map +1 -0
  100. package/dist/core/types.d.ts +177 -1
  101. package/dist/core/types.d.ts.map +1 -1
  102. package/dist/core/types.js.map +1 -1
  103. package/dist/monitoring/RateLimitSentinel.d.ts +24 -2
  104. package/dist/monitoring/RateLimitSentinel.d.ts.map +1 -1
  105. package/dist/monitoring/RateLimitSentinel.js +25 -14
  106. package/dist/monitoring/RateLimitSentinel.js.map +1 -1
  107. package/dist/scaffold/templates.d.ts.map +1 -1
  108. package/dist/scaffold/templates.js +5 -0
  109. package/dist/scaffold/templates.js.map +1 -1
  110. package/dist/server/AgentServer.d.ts +8 -0
  111. package/dist/server/AgentServer.d.ts.map +1 -1
  112. package/dist/server/AgentServer.js +4 -0
  113. package/dist/server/AgentServer.js.map +1 -1
  114. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  115. package/dist/server/CapabilityIndex.js +14 -0
  116. package/dist/server/CapabilityIndex.js.map +1 -1
  117. package/dist/server/routes.d.ts +12 -0
  118. package/dist/server/routes.d.ts.map +1 -1
  119. package/dist/server/routes.js +91 -0
  120. package/dist/server/routes.js.map +1 -1
  121. package/package.json +1 -1
  122. package/src/data/builtin-manifest.json +65 -65
  123. package/src/scaffold/templates.ts +5 -0
  124. package/upgrades/1.3.91.md +54 -0
  125. package/upgrades/1.3.92.md +63 -0
  126. package/upgrades/side-effects/session-error-nudge-rearm.md +17 -0
  127. package/upgrades/side-effects/session-pool-capability-classification.md +16 -0
  128. package/upgrades/side-effects/session-pool-review-fixes.md +23 -0
  129. package/upgrades/side-effects/session-pool-track-a-router-lease.md +29 -0
  130. package/upgrades/side-effects/session-pool-track-b-agent-awareness.md +29 -0
  131. package/upgrades/side-effects/session-pool-track-b-nicknames.md +35 -0
  132. package/upgrades/side-effects/session-pool-track-b-pool-api.md +36 -0
  133. package/upgrades/side-effects/session-pool-track-b-registry.md +31 -0
  134. package/upgrades/side-effects/session-pool-track-b-wiring.md +39 -0
  135. package/upgrades/side-effects/session-pool-track-c-dispatcher.md +24 -0
  136. package/upgrades/side-effects/session-pool-track-c-meshrpc.md +35 -0
  137. package/upgrades/side-effects/session-pool-track-c-route-wiring.md +31 -0
  138. package/upgrades/side-effects/session-pool-track-d-ownership-fsm.md +31 -0
  139. package/upgrades/side-effects/session-pool-track-d-registry.md +27 -0
  140. package/upgrades/side-effects/session-pool-track-d-wiring.md +30 -0
  141. package/upgrades/side-effects/session-pool-track-e-boot-wiring.md +20 -0
  142. package/upgrades/side-effects/session-pool-track-e-placement-core.md +17 -0
  143. package/upgrades/side-effects/session-pool-track-e-router.md +23 -0
  144. package/upgrades/side-effects/session-pool-track-f-config.md +12 -0
  145. package/upgrades/side-effects/session-pool-track-f-handoff-core.md +17 -0
  146. package/upgrades/side-effects/session-pool-track-g-load-broker.md +16 -0
  147. package/upgrades/side-effects/session-pool-track-h-gate-wiring.md +17 -0
  148. package/upgrades/side-effects/session-pool-track-h-mesh-client.md +15 -0
  149. package/upgrades/side-effects/session-pool-track-h-rebalance.md +13 -0
  150. package/upgrades/side-effects/session-pool-track-h-rollout-gate.md +17 -0
  151. package/upgrades/side-effects/session-pool-track-h-stage-guard.md +16 -0
  152. package/upgrades/side-effects/transient-api-error-recovery-generalization.md +18 -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,CAghOtE;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;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAqtOtE;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"}
@@ -5245,6 +5245,13 @@ export async function startServer(options) {
5245
5245
  sessionManager.on('rateLimitedAtIdle', (sessionName) => {
5246
5246
  rateLimitSentinel.report(sessionName, 'idle-error');
5247
5247
  });
5248
+ // Generic transient API errors (500/502/503, timeout, connection drop) ride the
5249
+ // SAME backoff→verify→escalate lifecycle as throttles, with a faster backoff
5250
+ // schedule (they usually clear in seconds). Generalizing the proven recovery to
5251
+ // the whole transient-API class is the 2026-05-29 future-proofing ask (topic 13481).
5252
+ sessionManager.on('apiErrorAtIdle', (sessionName) => {
5253
+ rateLimitSentinel.report(sessionName, 'idle-error', { errorClass: 'transient-api' });
5254
+ });
5248
5255
  if (rlsCfg.enabled !== false) {
5249
5256
  console.log(pc.green(' RateLimitSentinel enabled (server-throttle backoff + check-ins)'));
5250
5257
  }
@@ -8160,7 +8167,213 @@ export async function startServer(options) {
8160
8167
  }
8161
8168
  }
8162
8169
  }
8163
- 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 });
8170
+ // ── Multi-Machine Session Pool registry (§L2) — live MachineCapacity view for GET /pool ──
8171
+ // Always instantiated (cheap); the feature stays dark behind the sessionPool stage gate.
8172
+ // Self-attests hardware + records a self-heartbeat so GET /pool + the Machines tab show
8173
+ // this machine online with its specs; peer liveness is fed from MachineHeartbeat.
8174
+ let machinePoolRegistry;
8175
+ try {
8176
+ const poolMod = await import('../core/MachinePoolRegistry.js');
8177
+ const osMod = await import('node:os');
8178
+ const poolIdMgr = coordinator?.managers?.identityManager;
8179
+ if (poolIdMgr) {
8180
+ const failoverThresholdMs = (config.multiMachine?.failoverTimeoutMinutes ?? 15) * 60_000;
8181
+ const clockSkewToleranceMs = config.multiMachine?.sessionPool?.clockSkewToleranceMs ?? 300_000;
8182
+ machinePoolRegistry = new poolMod.MachinePoolRegistry({
8183
+ listMachines: () => poolIdMgr.getActiveMachines().map(({ machineId, entry }) => ({
8184
+ machineId,
8185
+ nickname: entry.nickname,
8186
+ hardware: entry.hardware,
8187
+ })),
8188
+ clockSkewToleranceMs,
8189
+ failoverThresholdMs,
8190
+ logger: (m) => console.log(pc.dim(` [pool] ${m}`)),
8191
+ });
8192
+ const poolSelfId = machineHeartbeat?.config?.machineId ??
8193
+ (poolIdMgr.hasIdentity() ? poolIdMgr.loadIdentity().machineId : null);
8194
+ if (poolSelfId) {
8195
+ try {
8196
+ poolIdMgr.recordSelfHardware(poolSelfId, poolMod.captureHardware());
8197
+ }
8198
+ catch { /* best-effort hardware self-attest */ }
8199
+ const refreshPool = () => {
8200
+ try {
8201
+ machinePoolRegistry.recordHeartbeat({
8202
+ machineId: poolSelfId,
8203
+ selfReportedLastSeen: new Date().toISOString(),
8204
+ loadAvg: osMod.loadavg()[0],
8205
+ });
8206
+ const hbApi = machineHeartbeat?.api;
8207
+ if (hbApi) {
8208
+ for (const r of hbApi.listAll()) {
8209
+ if (r.machineId !== poolSelfId) {
8210
+ machinePoolRegistry.recordHeartbeat({ machineId: r.machineId, selfReportedLastSeen: r.lastHeartbeatAt });
8211
+ }
8212
+ }
8213
+ }
8214
+ }
8215
+ catch { /* best-effort pool refresh */ }
8216
+ };
8217
+ refreshPool();
8218
+ const poolTimer = setInterval(refreshPool, 30_000);
8219
+ if (typeof poolTimer.unref === 'function')
8220
+ poolTimer.unref();
8221
+ }
8222
+ }
8223
+ }
8224
+ catch (err) {
8225
+ console.log(pc.dim(` [pool] registry not wired: ${err instanceof Error ? err.message : String(err)}`));
8226
+ }
8227
+ // ── MeshRpc dispatcher (§L0) + SessionOwnership registry (§L3) ──
8228
+ // Built when there's a machine identity. The ownership registry (L3) feeds
8229
+ // the dispatcher's RBAC (ownerOf/placementTargetOf) + the place/claim/
8230
+ // transfer/release handlers; read-class handlers (capacity/session-status)
8231
+ // are live too. Single-machine uses the in-memory store; the cross-machine
8232
+ // git-backed store swaps in for the Track-H proof (the registry is store-agnostic).
8233
+ let meshRpcDispatcher;
8234
+ let sessionOwnershipRegistry;
8235
+ try {
8236
+ const meshMod = await import('../core/MeshRpc.js');
8237
+ const idMod = await import('../core/MachineIdentity.js');
8238
+ const meshIdMgr = coordinator?.managers?.identityManager;
8239
+ const meshSelfId = machineHeartbeat?.config?.machineId
8240
+ ?? (meshIdMgr?.hasIdentity() ? meshIdMgr.loadIdentity().machineId : null);
8241
+ if (meshIdMgr && meshSelfId) {
8242
+ const meshClockToleranceMs = config.multiMachine?.sessionPool?.meshRpcClockToleranceMs ?? 30000;
8243
+ const seenMeshNonces = new Map(); // `${sender}:${nonce}` → ts, age-pruned
8244
+ const meshPruneMs = Math.max(meshClockToleranceMs * 4, 120_000);
8245
+ // SessionOwnership registry (§L3) — in-memory store (single-machine correct;
8246
+ // git-backed cross-machine store is the Track-H swap). Per-session nonce set.
8247
+ const ownMod = await import('../core/SessionOwnershipRegistry.js');
8248
+ const seenOwnNonces = new Set();
8249
+ sessionOwnershipRegistry = new ownMod.SessionOwnershipRegistry({
8250
+ store: new ownMod.InMemorySessionOwnershipStore(),
8251
+ seenNonce: (k) => seenOwnNonces.has(k),
8252
+ recordNonce: (k) => seenOwnNonces.add(k),
8253
+ logger: (m) => console.log(pc.dim(` ${m}`)),
8254
+ });
8255
+ const ownReg = sessionOwnershipRegistry;
8256
+ // The §L3 ownership commands, routed from MeshRpc to the registry CAS.
8257
+ const ownAction = (cmd, sender, env) => {
8258
+ if (cmd.type === 'place')
8259
+ return ownReg.cas({ type: 'place', machineId: cmd.machine }, { sessionKey: cmd.session, sender, nonce: env.nonce });
8260
+ if (cmd.type === 'claim')
8261
+ return ownReg.cas({ type: 'claim', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
8262
+ if (cmd.type === 'transfer')
8263
+ return ownReg.cas({ type: 'transfer', to: cmd.target }, { sessionKey: cmd.session, sender, nonce: env.nonce });
8264
+ if (cmd.type === 'release')
8265
+ return ownReg.cas({ type: 'release', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
8266
+ return { ok: false, reason: 'unsupported' };
8267
+ };
8268
+ // The §L4 owner-side deliverMessage receive handler (shared factory — same
8269
+ // code path the tests exercise). Durable-receipt-before-processing with
8270
+ // idempotent dedupe on messageId + the stale-ownership fence. Returns the
8271
+ // deliverMessageAck the router waits on before advancing the platform offset.
8272
+ // (Local processing hand-off to SessionManager is the Track-H staged
8273
+ // activation; the durable receipt + ACK is the complete dark-phase contract.)
8274
+ const deliverMod = await import('../core/DeliverMessageHandler.js');
8275
+ const deliverSeenFallback = new Set(); // used only if the SQLite ledger is unavailable
8276
+ const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
8277
+ ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
8278
+ recordReceipt: (messageId, session) => {
8279
+ if (messageLedger)
8280
+ return messageLedger.record(messageId, { platform: 'mesh', topic: session }).firstSeen;
8281
+ if (deliverSeenFallback.has(messageId))
8282
+ return false;
8283
+ deliverSeenFallback.add(messageId);
8284
+ return true;
8285
+ },
8286
+ });
8287
+ meshRpcDispatcher = new meshMod.MeshRpcDispatcher({
8288
+ verify: {
8289
+ selfMachineId: meshSelfId,
8290
+ verify: (canonical, signature, sender) => {
8291
+ const pem = meshIdMgr.getSigningPublicKeyPem(sender);
8292
+ if (!pem)
8293
+ return false;
8294
+ try {
8295
+ return idMod.verify(canonical, signature, pem);
8296
+ }
8297
+ catch {
8298
+ return false;
8299
+ }
8300
+ },
8301
+ isRegisteredPeer: (s) => meshIdMgr.isMachineActive(s),
8302
+ seenNonce: (s, n) => seenMeshNonces.has(`${s}:${n}`),
8303
+ now: () => Date.now(),
8304
+ clockToleranceMs: meshClockToleranceMs,
8305
+ },
8306
+ rbac: {
8307
+ routerHolder: () => coordinator?.getSyncStatus().leaseHolder ?? null,
8308
+ ownerOf: (s) => ownReg.ownerOf(s),
8309
+ placementTargetOf: (s) => ownReg.placementTargetOf(s),
8310
+ },
8311
+ recordNonce: (s, n) => {
8312
+ const t = Date.now();
8313
+ seenMeshNonces.set(`${s}:${n}`, t);
8314
+ if (seenMeshNonces.size > 5000) {
8315
+ for (const [k, v] of seenMeshNonces)
8316
+ if (t - v > meshPruneMs)
8317
+ seenMeshNonces.delete(k);
8318
+ }
8319
+ },
8320
+ handlers: {
8321
+ 'capacity-report': () => machinePoolRegistry?.getCapacities() ?? [],
8322
+ 'session-status': () => machinePoolRegistry?.getCapacity(meshSelfId) ?? { machineId: meshSelfId },
8323
+ place: ownAction,
8324
+ claim: ownAction,
8325
+ transfer: ownAction,
8326
+ release: ownAction,
8327
+ deliverMessage: deliverMessageHandler,
8328
+ },
8329
+ logger: (m) => console.log(pc.dim(` ${m}`)),
8330
+ });
8331
+ }
8332
+ }
8333
+ catch (err) {
8334
+ console.log(pc.dim(` [mesh-rpc] dispatcher not wired: ${err instanceof Error ? err.message : String(err)}`));
8335
+ }
8336
+ // ── Rollout gate (§Rollout): signed E2E result store + StageAdvancer ──────────
8337
+ // The E2E result store backs GET /session-pool/e2e-results (observable gate state);
8338
+ // StageAdvancer is the SOLE writer of multiMachine.sessionPool.stage (it passes the
8339
+ // stageWriteGuard token; any other write throws stage-write-not-permitted). Always
8340
+ // instantiated (cheap); inert until the pool activates past 'dark'.
8341
+ let sessionPoolE2EResultStore;
8342
+ try {
8343
+ const e2eMod = await import('../core/SessionPoolE2EResultStore.js');
8344
+ const stageMod = await import('../core/StageAdvancer.js');
8345
+ const guardMod = await import('../config/stageWriteGuard.js');
8346
+ const crypto = await import('node:crypto');
8347
+ // HMAC over the agent's authToken — tamper-evident without a separate keystore.
8348
+ const hmacKey = String(config.authToken ?? 'instar-session-pool-e2e');
8349
+ const hmac = (c) => crypto.createHmac('sha256', hmacKey).update(c).digest('hex');
8350
+ sessionPoolE2EResultStore = new e2eMod.SessionPoolE2EResultStore({
8351
+ filePath: path.join(config.stateDir, 'session-pool-e2e-results.json'),
8352
+ sign: hmac,
8353
+ verifySig: (c, s) => {
8354
+ try {
8355
+ const exp = hmac(c);
8356
+ return s.length === exp.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(exp));
8357
+ }
8358
+ catch {
8359
+ return false;
8360
+ }
8361
+ },
8362
+ });
8363
+ // StageAdvancer: the sole stage writer. Held for the rollout job/route to drive;
8364
+ // constructed here so the write path (liveConfig + token) is wired in one place.
8365
+ void new stageMod.StageAdvancer({
8366
+ resultStore: sessionPoolE2EResultStore,
8367
+ currentCommitSha: () => process.env.INSTAR_COMMIT_SHA ?? process.env.GITHUB_SHA ?? 'unknown',
8368
+ readStage: () => liveConfig.get('multiMachine.sessionPool.stage', 'dark'),
8369
+ writeStageConfig: (s) => liveConfig.set(guardMod.STAGE_CONFIG_PATH, s, { stageWriteToken: guardMod.STAGE_WRITE_TOKEN }),
8370
+ audit: (event, detail) => console.log(pc.dim(` [stage-advancer] ${event} ${JSON.stringify(detail)}`)),
8371
+ });
8372
+ }
8373
+ catch (err) {
8374
+ console.log(pc.dim(` [session-pool] rollout gate not wired: ${err instanceof Error ? err.message : String(err)}`));
8375
+ }
8376
+ 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 });
8164
8377
  // Boot-recovery (tunnel-failure-resilience spec Part 6): if the agent
8165
8378
  // died mid-relay-episode, the persisted tunnel.json carries
8166
8379
  // rotationPending=true. Rotate the dashboard PIN + authToken BEFORE