@logicseek/hello-cc 1.0.0 → 1.0.2-dsh.3

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 (123) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +36 -0
  3. package/README.zh-CN.md +35 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/guide.md +58 -0
  11. package/docs/guide.zh-CN.md +55 -0
  12. package/docs/mcp.md +9 -0
  13. package/docs/mcp.zh-CN.md +23 -0
  14. package/docs/native.md +295 -0
  15. package/docs/native.zh-CN.md +253 -0
  16. package/docs/web-handoff.zh-CN.md +53 -0
  17. package/lib/cli/app.mjs +27 -6
  18. package/lib/cli/commands/coordination.mjs +3 -1
  19. package/lib/cli/commands/doctor.mjs +17 -2
  20. package/lib/cli/commands/dsh.mjs +79 -0
  21. package/lib/cli/commands/gc.mjs +182 -32
  22. package/lib/cli/commands/hook.mjs +241 -90
  23. package/lib/cli/commands/lock.mjs +34 -19
  24. package/lib/cli/commands/mcp.mjs +28 -0
  25. package/lib/cli/commands/misc.mjs +15 -0
  26. package/lib/cli/commands/native.mjs +130 -0
  27. package/lib/cli/commands/peer.mjs +21 -5
  28. package/lib/cli/commands/run.mjs +66 -21
  29. package/lib/cli/commands/task.mjs +6 -3
  30. package/lib/cli-args.mjs +4 -0
  31. package/lib/coordination-state.mjs +15 -1
  32. package/lib/core/coordination/event-retention.mjs +14 -2
  33. package/lib/core/coordination/gc-plan.mjs +3 -13
  34. package/lib/core/coordination/locks.mjs +45 -0
  35. package/lib/core/coordination/results.mjs +79 -0
  36. package/lib/core/peers/bindings.mjs +9 -3
  37. package/lib/diagnostics/codex.mjs +121 -0
  38. package/lib/integrations/codex-interactions.mjs +114 -0
  39. package/lib/integrations/dsh-collaboration.mjs +170 -0
  40. package/lib/integrations/dsh-cordis.d.ts +19 -0
  41. package/lib/integrations/dsh-cordis.mjs +177 -0
  42. package/lib/integrations/dsh.bundle.yml +7 -0
  43. package/lib/integrations/dsh.mjs +287 -0
  44. package/lib/integrations/native/claude.mjs +451 -0
  45. package/lib/integrations/native/codex.mjs +242 -0
  46. package/lib/integrations/native/dsh-acp.mjs +343 -0
  47. package/lib/integrations/native/index.mjs +19 -0
  48. package/lib/integrations/native/interactions.mjs +53 -0
  49. package/lib/integrations/native/jsonrpc.mjs +185 -0
  50. package/lib/integrations/peers/identity.mjs +36 -10
  51. package/lib/integrations/peers/processes.mjs +22 -8
  52. package/lib/integrations/providers.mjs +6 -2
  53. package/lib/mcp/scope.mjs +104 -0
  54. package/lib/mcp/stdio.mjs +80 -0
  55. package/lib/mcp/tools.mjs +197 -0
  56. package/lib/process/identity.mjs +33 -1
  57. package/lib/release/release-notes.mjs +9 -0
  58. package/lib/runtime/buffer-directory-lease.mjs +2 -1
  59. package/lib/runtime/client.mjs +3 -0
  60. package/lib/runtime/native/client.mjs +38 -0
  61. package/lib/runtime/native/service.mjs +511 -0
  62. package/lib/runtime/native/store.mjs +121 -0
  63. package/lib/shared/file-lock.mjs +11 -4
  64. package/lib/shared/socket-lock-worker.mjs +73 -24
  65. package/lib/terminal/tmux.mjs +53 -23
  66. package/lib/ui/help.mjs +5 -2
  67. package/lib/web/adoption-state.mjs +55 -0
  68. package/lib/web/auto-attach.mjs +18 -2
  69. package/lib/web/buffer-gc-runtime.mjs +2 -2
  70. package/lib/web/codex-app-server.mjs +557 -0
  71. package/lib/web/codex-sessions.mjs +287 -0
  72. package/lib/web/control-lease.mjs +204 -0
  73. package/lib/web/http-routes.mjs +197 -15
  74. package/lib/web/http.mjs +5 -4
  75. package/lib/web/native-sessions.mjs +286 -0
  76. package/lib/web/project-contexts.mjs +8 -2
  77. package/lib/web/pty-sessions.mjs +60 -33
  78. package/lib/web/runtime-main.mjs +101 -6
  79. package/lib/web/session-serialize.mjs +0 -0
  80. package/lib/web/startup.mjs +4 -2
  81. package/lib/web/task-results.mjs +76 -0
  82. package/lib/web/tmux-sessions.mjs +92 -11
  83. package/lib/web/tmux-stream.mjs +2 -2
  84. package/lib/web/ui-codex.mjs +550 -0
  85. package/lib/web/ui-command-palette.mjs +119 -0
  86. package/lib/web/ui-handoff.mjs +102 -0
  87. package/lib/web/ui-history.mjs +118 -0
  88. package/lib/web/ui-i18n.mjs +841 -0
  89. package/lib/web/ui-interactions.mjs +78 -0
  90. package/lib/web/ui-login.mjs +101 -0
  91. package/lib/web/ui-native.mjs +165 -0
  92. package/lib/web/ui-preferences.mjs +278 -0
  93. package/lib/web/ui-review.mjs +132 -0
  94. package/lib/web/ui-template.mjs +1106 -576
  95. package/lib/web/ui-terminal-find.mjs +98 -0
  96. package/lib/web/ui-workbench.mjs +94 -0
  97. package/lib/web/ui-workspace.mjs +222 -0
  98. package/package.json +12 -4
  99. package/scripts/check-free-identifiers.mjs +67 -8
  100. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  101. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  102. package/scripts/dsh-installed-acceptance.mjs +247 -0
  103. package/scripts/dsh-official-acceptance.mjs +180 -0
  104. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  105. package/scripts/github-release.mjs +3 -1
  106. package/scripts/native-live-acceptance.mjs +369 -0
  107. package/scripts/regression.mjs +338 -187
  108. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  109. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  110. package/scripts/web-native-model-acceptance.mjs +138 -0
  111. package/docs/architecture-split-review.md +0 -217
  112. package/docs/current-state-review-2026-08.md +0 -341
  113. package/docs/defect-review-2026-07.md +0 -382
  114. package/docs/layout-split-stack-audit.md +0 -298
  115. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
  116. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
  117. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
  118. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
  119. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
  120. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
  121. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
  122. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
  123. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
@@ -26,6 +26,7 @@ const outDir = path.join(os.tmpdir(), `hcc-reg-out-${testId}`);
26
26
  const tmuxSession = `hcc-reg-${process.pid}`;
27
27
  const tmuxSocketName = `hcc-reg-${testId}`.replace(/[^A-Za-z0-9_-]/g, '-');
28
28
  const secondProjectRoot = path.join(root, 'second-project');
29
+ const TERMINAL_WEBSOCKET_TIMEOUT_MS = 15_000;
29
30
  const realHome = process.env.HOME || os.homedir();
30
31
  const realRegistryFile = path.join(realHome, '.hello-cc', 'projects.json');
31
32
  const realTmuxBin = spawnSync('sh', ['-lc', 'command -v tmux || true'], {
@@ -432,6 +433,13 @@ async function assertTmuxGcPolicy() {
432
433
  managedTmuxSessions.add(rootEventSession);
433
434
  managedTmuxSessions.add(manualSession);
434
435
 
436
+ // Create the one runtime-managed authority before the bulk synthetic tmux
437
+ // fixtures. Leaving the runtime up while those sessions are created lets its
438
+ // five-second orphan scan adopt them and contend with this start on slow CI.
439
+ const eventActivePane = parsePane(hcc(['peer', 'start', eventPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
440
+ const eventActiveSession = run('tmux', ['display-message', '-p', '-t', eventActivePane, '#{session_name}']).trim();
441
+ await stopRuntime();
442
+
435
443
  run('tmux', ['new-session', '-d', '-s', staleSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
436
444
  run('tmux', ['new-session', '-d', '-s', peerFilterSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
437
445
  run('tmux', ['new-session', '-d', '-s', attachedSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
@@ -489,10 +497,27 @@ async function assertTmuxGcPolicy() {
489
497
  insertRuntimeTargetBinding(deadBindingPeer, 'shell', 'tmux-gc-binding-dead-session', deadBindingPane);
490
498
  insertRuntimeTargetBinding(reusedBindingPeer, 'shell', 'tmux-gc-binding-reused-session', reusedBindingPane);
491
499
  insertRuntimeTargetBinding(clientUnknownBindingPeer, 'shell', 'tmux-gc-binding-client-unknown-session', clientUnknownBindingPane);
492
- const eventActivePane = parsePane(hcc(['peer', 'start', eventPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
493
- const eventActiveSession = run('tmux', ['display-message', '-p', '-t', eventActivePane, '#{session_name}']).trim();
494
500
  withMeshDb((db) => {
495
501
  const t = Math.floor(Date.now() / 1000) - 30 * 86400;
502
+ for (const [peer, pane] of [
503
+ [stalePeer, stalePane],
504
+ [peerFilterPeer, peerFilterPane],
505
+ [attachedPeer, attachedPane],
506
+ [deadBindingPeer, deadBindingPane],
507
+ [reusedBindingPeer, reusedBindingPane],
508
+ [clientUnknownBindingPeer, clientUnknownBindingPane]
509
+ ]) {
510
+ db.prepare(`
511
+ UPDATE peer_bindings
512
+ SET transport = 'tmux', runtime_target = ?, updated_at = ?
513
+ WHERE peer = ?
514
+ `).run(pane, t, peer);
515
+ }
516
+ db.prepare(`
517
+ UPDATE peer_bindings
518
+ SET transport = 'tmux', runtime_target = ?
519
+ WHERE peer = ?
520
+ `).run(eventActivePane, eventPeer);
496
521
  db.prepare('UPDATE peers SET last_seen_at = ? WHERE id IN (?, ?, ?)').run(t, stalePeer, peerFilterPeer, attachedPeer);
497
522
  db.prepare("UPDATE peers SET status = 'exited' WHERE id IN (?, ?)").run(stalePeer, peerFilterPeer);
498
523
  db.prepare('UPDATE peer_bindings SET updated_at = ? WHERE peer IN (?, ?, ?)').run(t, stalePeer, peerFilterPeer, attachedPeer);
@@ -1581,32 +1606,82 @@ async function expectWebSocketMarker(peer, marker) {
1581
1606
  await new Promise((resolve, reject) => {
1582
1607
  let sawSnapshot = false;
1583
1608
  let sawMarker = false;
1584
- const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1585
- const timer = setTimeout(() => reject(new Error(`${peer} websocket timeout`)), 5000);
1586
- ws.on('open', () => {
1587
- const result = hccMaybe(['inject', peer, `echo ${marker}`]);
1588
- if (result.status !== 0) reject(new Error(result.stderr || result.stdout || 'inject failed'));
1589
- });
1609
+ const ws = trackTerminalControl(new WebSocket(runtimeWsUrl(peer), runtimeWsOptions()));
1610
+ const timer = setTimeout(() => {
1611
+ try { ws.terminate(); } catch {}
1612
+ reject(new Error(`${peer} websocket timeout`));
1613
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1590
1614
  ws.on('message', (raw) => {
1591
1615
  const msg = JSON.parse(String(raw));
1592
- if (msg.type === 'snapshot') sawSnapshot = true;
1616
+ if (msg.type === 'snapshot') {
1617
+ sawSnapshot = true;
1618
+ claimTerminalControl(ws).then(() => ws.send(JSON.stringify({
1619
+ type: 'input', data: `echo ${marker}\r`, action_token: ws.hccActionToken,
1620
+ epoch: ws.hccControl.epoch
1621
+ }))).catch(reject);
1622
+ }
1593
1623
  if (['snapshot', 'data', 'replace'].includes(msg.type) && String(msg.data || '').includes(marker)) {
1594
1624
  sawMarker = true;
1595
1625
  }
1596
1626
  if (sawSnapshot && sawMarker) {
1597
1627
  clearTimeout(timer);
1628
+ releaseTerminalControl(ws);
1598
1629
  ws.close();
1599
1630
  resolve();
1600
1631
  }
1601
1632
  });
1602
- ws.on('error', reject);
1633
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1634
+ });
1635
+ }
1636
+
1637
+ // Keep the private token and public fencing epoch on each test connection.
1638
+ // Reconnecting helpers explicitly claim control before an authorized write.
1639
+ function trackTerminalControl(ws) {
1640
+ ws.on('message', (raw) => {
1641
+ let message;
1642
+ try { message = JSON.parse(String(raw)); } catch { return; }
1643
+ if (message.action_token) ws.hccActionToken = message.action_token;
1644
+ if (message.control) ws.hccControl = message.control;
1645
+ });
1646
+ return ws;
1647
+ }
1648
+
1649
+ async function claimTerminalControl(ws) {
1650
+ if (ws.hccControl?.can_control) return ws.hccControl;
1651
+ return new Promise((resolve, reject) => {
1652
+ const timer = setTimeout(() => finish(new Error('terminal control claim timeout')), 5000);
1653
+ const finish = (error = null) => {
1654
+ clearTimeout(timer);
1655
+ ws.off('message', onMessage);
1656
+ if (error) reject(error);
1657
+ else resolve(ws.hccControl);
1658
+ };
1659
+ const onMessage = (raw) => {
1660
+ let message;
1661
+ try { message = JSON.parse(String(raw)); } catch { return; }
1662
+ if (message.type === 'error') finish(new Error(`terminal control claim failed: ${JSON.stringify(message.error)}`));
1663
+ else if (message.type === 'control' && message.control?.can_control) finish();
1664
+ };
1665
+ ws.on('message', onMessage);
1666
+ ws.send(JSON.stringify({ type: 'control', action: 'claim', action_token: ws.hccActionToken,
1667
+ epoch: ws.hccControl?.epoch, force: true }));
1603
1668
  });
1604
1669
  }
1605
1670
 
1671
+ function releaseTerminalControl(ws) {
1672
+ if (ws?.readyState === WebSocket.OPEN && ws.hccControl?.can_control) {
1673
+ ws.send(JSON.stringify({ type: 'control', action: 'release', action_token: ws.hccActionToken,
1674
+ epoch: ws.hccControl.epoch }));
1675
+ }
1676
+ }
1677
+
1606
1678
  async function openTerminalWebSocket(peer) {
1607
1679
  return new Promise((resolve, reject) => {
1608
1680
  const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1609
- const timer = setTimeout(() => reject(new Error(`${peer} websocket open timeout`)), 5000);
1681
+ const timer = setTimeout(() => {
1682
+ try { ws.terminate(); } catch {}
1683
+ reject(new Error(`${peer} websocket open timeout`));
1684
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1610
1685
  ws.on('open', () => {
1611
1686
  clearTimeout(timer);
1612
1687
  resolve(ws);
@@ -1626,13 +1701,16 @@ async function openSessionActionChannel(peer, params = {}) {
1626
1701
  for (const [key, value] of Object.entries(params)) {
1627
1702
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
1628
1703
  }
1629
- const ws = new WebSocket(url, runtimeWsOptions());
1630
- const timer = setTimeout(() => reject(new Error(`${peer} action token fetch timeout`)), 5000);
1704
+ const ws = trackTerminalControl(new WebSocket(url, runtimeWsOptions()));
1705
+ const timer = setTimeout(() => {
1706
+ try { ws.terminate(); } catch {}
1707
+ reject(new Error(`${peer} action token fetch timeout`));
1708
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1631
1709
  ws.on('message', (raw) => {
1632
1710
  const msg = JSON.parse(String(raw));
1633
1711
  if (msg.type === 'snapshot') {
1634
1712
  clearTimeout(timer);
1635
- resolve({ token: msg.action_token || '', ws });
1713
+ resolve({ token: msg.action_token || '', ws, get epoch() { return ws.hccControl?.epoch; } });
1636
1714
  }
1637
1715
  });
1638
1716
  ws.on('error', (err) => { clearTimeout(timer); reject(err); });
@@ -1643,8 +1721,23 @@ async function fetchSessionActionToken(peer, params = {}) {
1643
1721
  return (await openSessionActionChannel(peer, params)).token;
1644
1722
  }
1645
1723
 
1724
+ async function stopControlledSession(id, payload = {}, projectRoot = root) {
1725
+ const channel = await openSessionActionChannel(id, { root: projectRoot });
1726
+ try {
1727
+ await claimTerminalControl(channel.ws);
1728
+ return await runtimeFetch(`/api/sessions/${encodeURIComponent(id)}/stop`, {
1729
+ method: 'POST',
1730
+ headers: { 'Content-Type': 'application/json' },
1731
+ body: JSON.stringify({ ...payload, action_token: channel.token, epoch: channel.epoch })
1732
+ }, { root: projectRoot });
1733
+ } finally {
1734
+ await closeTerminalWebSocket(channel.ws);
1735
+ }
1736
+ }
1737
+
1646
1738
  async function closeTerminalWebSocket(ws) {
1647
1739
  if (!ws || ws.readyState === WebSocket.CLOSED) return;
1740
+ releaseTerminalControl(ws);
1648
1741
  await new Promise((resolve) => {
1649
1742
  const timer = setTimeout(() => {
1650
1743
  try { ws.terminate(); } catch {}
@@ -1667,12 +1760,16 @@ async function fetchTerminalSnapshot(peer, params = {}) {
1667
1760
  for (const [key, value] of Object.entries(params)) {
1668
1761
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
1669
1762
  }
1670
- const ws = new WebSocket(url, runtimeWsOptions());
1671
- const timer = setTimeout(() => reject(new Error(`${peer} terminal snapshot timeout`)), 5000);
1763
+ const ws = trackTerminalControl(new WebSocket(url, runtimeWsOptions()));
1764
+ const timer = setTimeout(() => {
1765
+ try { ws.terminate(); } catch {}
1766
+ reject(new Error(`${peer} terminal snapshot timeout`));
1767
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1672
1768
  ws.on('message', (raw) => {
1673
1769
  const msg = JSON.parse(String(raw));
1674
1770
  if (msg.type !== 'snapshot') return;
1675
1771
  clearTimeout(timer);
1772
+ releaseTerminalControl(ws);
1676
1773
  try { ws.close(); } catch {}
1677
1774
  resolve(String(msg.data || ''));
1678
1775
  });
@@ -1759,12 +1856,12 @@ async function openCookieTerminalWebSocket(peer, sid, params = {}) {
1759
1856
  }
1760
1857
  return new Promise((resolve, reject) => {
1761
1858
  let settled = false;
1762
- const ws = new WebSocket(url, {
1859
+ const ws = trackTerminalControl(new WebSocket(url, {
1763
1860
  headers: {
1764
1861
  Cookie: `hcc_sid=${sid}`,
1765
1862
  Origin: new URL(baseUrl).origin
1766
1863
  }
1767
- });
1864
+ }));
1768
1865
  const timer = setTimeout(() => {
1769
1866
  if (settled) return;
1770
1867
  settled = true;
@@ -1885,25 +1982,30 @@ async function assertEvictionClosesCookieWebSocket(peer, params = {}) {
1885
1982
  async function expectResizeReplaceSnapshot(peer, marker) {
1886
1983
  await new Promise((resolve, reject) => {
1887
1984
  let sawSnapshot = false;
1888
- const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1889
- const timer = setTimeout(() => reject(new Error(`${peer} resize replace timeout`)), 5000);
1890
- ws.on('open', () => {
1891
- const result = hccMaybe(['inject', peer, `echo ${marker}`]);
1892
- if (result.status !== 0) reject(new Error(result.stderr || result.stdout || 'inject failed'));
1893
- });
1985
+ const ws = trackTerminalControl(new WebSocket(runtimeWsUrl(peer), runtimeWsOptions()));
1986
+ const timer = setTimeout(() => {
1987
+ try { ws.terminate(); } catch {}
1988
+ reject(new Error(`${peer} resize replace timeout`));
1989
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1894
1990
  ws.on('message', (raw) => {
1895
1991
  const msg = JSON.parse(String(raw));
1896
1992
  if (msg.type === 'snapshot') {
1897
1993
  sawSnapshot = true;
1898
- ws.send(JSON.stringify({ type: 'resize', cols: 96, rows: 28 }));
1994
+ claimTerminalControl(ws).then(() => {
1995
+ ws.send(JSON.stringify({ type: 'input', data: `echo ${marker}\r`,
1996
+ action_token: ws.hccActionToken, epoch: ws.hccControl.epoch }));
1997
+ ws.send(JSON.stringify({ type: 'resize', cols: 96, rows: 28,
1998
+ action_token: ws.hccActionToken, epoch: ws.hccControl.epoch }));
1999
+ }).catch(reject);
1899
2000
  }
1900
2001
  if (sawSnapshot && msg.type === 'replace' && String(msg.data || '').includes(marker)) {
1901
2002
  clearTimeout(timer);
2003
+ releaseTerminalControl(ws);
1902
2004
  ws.close();
1903
2005
  resolve();
1904
2006
  }
1905
2007
  });
1906
- ws.on('error', reject);
2008
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1907
2009
  });
1908
2010
  }
1909
2011
 
@@ -1913,18 +2015,21 @@ async function expectWebSocketInputVisible(peer, marker) {
1913
2015
  let sent = false;
1914
2016
  let sawMarkerAfterInput = false;
1915
2017
  let sawFrameAfterInput = false;
1916
- let actionToken = '';
1917
- const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1918
- const timer = setTimeout(() => reject(new Error(`${peer} websocket input visibility timeout`)), 5000);
2018
+ const ws = trackTerminalControl(new WebSocket(runtimeWsUrl(peer), runtimeWsOptions()));
2019
+ const timer = setTimeout(() => {
2020
+ try { ws.terminate(); } catch {}
2021
+ reject(new Error(`${peer} websocket input visibility timeout`));
2022
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1919
2023
  ws.on('message', (raw) => {
1920
2024
  const msg = JSON.parse(String(raw));
1921
2025
  const data = String(msg.data || '');
1922
2026
  if (msg.type === 'snapshot') {
1923
2027
  sawSnapshot = true;
1924
- if (msg.action_token) actionToken = msg.action_token;
1925
2028
  if (!sent) {
1926
2029
  sent = true;
1927
- ws.send(JSON.stringify({ type: 'input', data: `echo ${marker}\r`, action_token: actionToken }));
2030
+ claimTerminalControl(ws).then(() => ws.send(JSON.stringify({ type: 'input',
2031
+ data: `echo ${marker}\r`, action_token: ws.hccActionToken,
2032
+ epoch: ws.hccControl.epoch }))).catch(reject);
1928
2033
  }
1929
2034
  return;
1930
2035
  }
@@ -1934,11 +2039,12 @@ async function expectWebSocketInputVisible(peer, marker) {
1934
2039
  }
1935
2040
  if (sawSnapshot && sawFrameAfterInput && sawMarkerAfterInput) {
1936
2041
  clearTimeout(timer);
2042
+ releaseTerminalControl(ws);
1937
2043
  ws.close();
1938
2044
  resolve();
1939
2045
  }
1940
2046
  });
1941
- ws.on('error', reject);
2047
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1942
2048
  });
1943
2049
  }
1944
2050
 
@@ -1948,7 +2054,7 @@ async function assertTerminalInputTokenRejected(peer, suppliedToken, params = {}
1948
2054
  for (const [key, value] of Object.entries(params)) {
1949
2055
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
1950
2056
  }
1951
- const ws = new WebSocket(url, runtimeWsOptions());
2057
+ const ws = trackTerminalControl(new WebSocket(url, runtimeWsOptions()));
1952
2058
  const rejectedMarker = `REJECTED_${testId}_${Math.random().toString(16).slice(2)}`;
1953
2059
  const acceptedMarker = `ACCEPTED_${testId}_${Math.random().toString(16).slice(2)}`;
1954
2060
  let snapshotToken = '';
@@ -1965,6 +2071,7 @@ async function assertTerminalInputTokenRejected(peer, suppliedToken, params = {}
1965
2071
  if (settled) return;
1966
2072
  settled = true;
1967
2073
  clearTimeout(timer);
2074
+ releaseTerminalControl(ws);
1968
2075
  try { ws.close(); } catch {}
1969
2076
  if (err) reject(err);
1970
2077
  else resolve();
@@ -1975,7 +2082,7 @@ async function assertTerminalInputTokenRejected(peer, suppliedToken, params = {}
1975
2082
  output += String(message.data || '');
1976
2083
  if (message.type === 'snapshot' && !snapshotToken) {
1977
2084
  snapshotToken = message.action_token || '';
1978
- const frame = { type: 'input', data: `echo ${rejectedMarker}\r` };
2085
+ const frame = { type: 'input', data: `echo ${rejectedMarker}\r`, epoch: ws.hccControl.epoch };
1979
2086
  if (suppliedToken !== undefined) frame.action_token = suppliedToken;
1980
2087
  ws.send(JSON.stringify(frame));
1981
2088
  setTimeout(() => {
@@ -1984,11 +2091,12 @@ async function assertTerminalInputTokenRejected(peer, suppliedToken, params = {}
1984
2091
  return;
1985
2092
  }
1986
2093
  checkedRejected = true;
1987
- ws.send(JSON.stringify({
2094
+ claimTerminalControl(ws).then(() => ws.send(JSON.stringify({
1988
2095
  type: 'input',
1989
2096
  data: `echo ${acceptedMarker}\r`,
1990
- action_token: snapshotToken
1991
- }));
2097
+ action_token: snapshotToken,
2098
+ epoch: ws.hccControl.epoch
2099
+ }))).catch(finish);
1992
2100
  }, 350);
1993
2101
  }
1994
2102
  if (checkedRejected && output.includes(acceptedMarker)) finish();
@@ -2495,7 +2603,8 @@ async function cookieSessionExpiryWorkflow() {
2495
2603
  ws.send(JSON.stringify({
2496
2604
  type: 'input',
2497
2605
  data: `echo ${marker}\r`,
2498
- action_token: ws.hccActionToken
2606
+ action_token: ws.hccActionToken,
2607
+ epoch: ws.hccControl.epoch
2499
2608
  }));
2500
2609
  const closeResult = await closed;
2501
2610
  if (closeResult.code !== 4001 || !closeResult.reason.includes('session expired')) {
@@ -2515,7 +2624,7 @@ async function cookieSessionExpiryWorkflow() {
2515
2624
  try { ws.terminate(); } catch {}
2516
2625
  }
2517
2626
  try {
2518
- await runtimeFetch(`/api/sessions/${encodeURIComponent(expirySessionId)}/stop`, { method: 'POST' }, { root });
2627
+ await stopControlledSession(expirySessionId);
2519
2628
  } catch {}
2520
2629
  await stopRuntime();
2521
2630
  }
@@ -2564,7 +2673,8 @@ async function webSecretRedactionWorkflow() {
2564
2673
  }, { root });
2565
2674
  if (!create.ok) fail(`redaction PTY create failed with status ${create.status}`);
2566
2675
  terminalWs = await openCookieTerminalWebSocket(sessionId, sid, { root });
2567
- terminalWs.send(JSON.stringify({ type: 'resize', cols: 90, rows: 28 }));
2676
+ terminalWs.send(JSON.stringify({ type: 'resize', cols: 90, rows: 28,
2677
+ action_token: terminalWs.hccActionToken, epoch: terminalWs.hccControl.epoch }));
2568
2678
 
2569
2679
  const badProject = await fetch(new URL(`/api/projects?token=${encodeURIComponent(secret)}`, runtime.base_url), {
2570
2680
  method: 'POST',
@@ -2609,7 +2719,7 @@ async function webSecretRedactionWorkflow() {
2609
2719
  try { terminalWs.terminate(); } catch {}
2610
2720
  }
2611
2721
  try {
2612
- await runtimeFetch(`/api/sessions/${encodeURIComponent(sessionId)}/stop`, { method: 'POST' }, { root });
2722
+ await stopControlledSession(sessionId);
2613
2723
  } catch {}
2614
2724
  await stopRuntime();
2615
2725
  }
@@ -4031,6 +4141,10 @@ async function multiProjectWebWorkflow() {
4031
4141
  !arbitraryRuntime.process_identity?.commandHash) {
4032
4142
  fail(`project API runtime pointer omitted the shared runtime fingerprint:\n${JSON.stringify(arbitraryRuntime, null, 2)}`);
4033
4143
  }
4144
+ const arbitraryBufs = path.join(arbitraryRoot, '.hello-cc', 'bufs');
4145
+ if (fs.existsSync(arbitraryBufs)) {
4146
+ fail('project registration eagerly created the optional buffer directory');
4147
+ }
4034
4148
  fs.rmSync(path.join(arbitraryRoot, '.hello-cc'), { recursive: true, force: true });
4035
4149
  await sleep(1250);
4036
4150
  if (fs.existsSync(path.join(arbitraryRoot, '.hello-cc'))) {
@@ -4322,17 +4436,16 @@ async function multiProjectWebWorkflow() {
4322
4436
  }
4323
4437
  for (const expected of [
4324
4438
  'id="langSelect"',
4325
- "localStorage.getItem('hcc.lang')",
4326
- "localStorage.setItem('hcc.lang', lang)",
4327
- "document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en'",
4328
- "zh: {",
4329
- "language: '语言'",
4330
- "projectState: '项目状态'",
4331
- "noSessionSelected: '未选择会话'",
4332
- "sendMessage: '发送消息'",
4333
- "peers: '协作方'",
4334
- "noPeers: '没有协作方。'",
4335
- "'status.active': '活跃'",
4439
+ 'window.hccUi',
4440
+ 'hccUi.language',
4441
+ 'hccUi.tr(key',
4442
+ '"language":"语言"',
4443
+ '"projectState":"项目状态"',
4444
+ '"noSessionSelected":"未选择会话"',
4445
+ '"sendMessage":"发送消息"',
4446
+ '"peers":"协作方"',
4447
+ '"noPeers":"没有协作方。"',
4448
+ '"status.active":"活跃"',
4336
4449
  "data-i18n=\"language\"",
4337
4450
  "data-i18n-placeholder=\"projectPathPlaceholder\"",
4338
4451
  "data-i18n-title=\"collapseSidebar\"",
@@ -4379,7 +4492,7 @@ async function multiProjectWebWorkflow() {
4379
4492
  'setPointerCapture',
4380
4493
  'sideIsCollapsed(opposite) ? 0',
4381
4494
  'Math.abs(delta) <= 3',
4382
- "localStorage.setItem('hcc.collapse.' + side, on ? '1' : '0');\n applySideWidths();",
4495
+ "hccUi.safeSet('hcc.collapse.' + side, on ? '1' : '0');\n applySideWidths();",
4383
4496
  'cursor: col-resize'
4384
4497
  ]) {
4385
4498
  if (!html.includes(expected)) fail(`web layout missing resizable sidebar support: ${expected}`);
@@ -4389,13 +4502,13 @@ async function multiProjectWebWorkflow() {
4389
4502
  }
4390
4503
  for (const expected of [
4391
4504
  'function stateCardCollapsed(section)',
4392
- "localStorage.getItem('hcc.stateCard.' + section + '.collapsed')",
4505
+ "hccUi.safeGet('hcc.stateCard.' + section + '.collapsed')",
4393
4506
  'function stateCardHtml(section, title, count, bodyHtml)',
4394
4507
  'state-card-toggle',
4395
4508
  'aria-expanded=',
4396
4509
  'state-card-collapsed',
4397
4510
  'function bindStateCardToggles()',
4398
- "localStorage.setItem('hcc.stateCard.' + section + '.collapsed'",
4511
+ "hccUi.safeSet('hcc.stateCard.' + section + '.collapsed'",
4399
4512
  "stateCardHtml('automation'",
4400
4513
  "stateCardHtml('timeline'",
4401
4514
  "stateCardHtml('messages'",
@@ -4427,11 +4540,18 @@ async function multiProjectWebWorkflow() {
4427
4540
  for (const expected of [
4428
4541
  'term.onData((data) => {',
4429
4542
  'sendTerminalInput(data);',
4430
- 'function flushPendingTerminalInput(id, socket)',
4431
- "socket.send(JSON.stringify({ type: 'input', data, action_token: actionToken }))"
4543
+ "function sendTerminalInput(data, inputId = '')",
4544
+ 'epoch: controlEpoch()',
4545
+ "...(inputId ? { input_id: inputId } : {})",
4546
+ "msg.type === 'input_ack' && handoffStore.acknowledge(currentProject, id, msg.input_id)",
4547
+ 'handoffStore.uncertain(currentProject, id)',
4548
+ 'Connection recovery never replays terminal input or a draft.'
4432
4549
  ]) {
4433
4550
  if (!html.includes(expected)) fail(`web terminal input forwarding missing: ${expected}`);
4434
4551
  }
4552
+ if (html.includes('flushPendingTerminalInput') || html.includes('pendingTerminalInput')) {
4553
+ fail('web terminal recovery must not buffer or replay terminal input');
4554
+ }
4435
4555
  if (!html.includes("stateCardHtml('timeline'") || !html.includes('renderTimelineItem') || !html.includes('bodyPinned') || !html.includes('refreshCurrentState')) {
4436
4556
  fail('web state panel missing collaboration timeline or refresh routing');
4437
4557
  }
@@ -4483,14 +4603,7 @@ async function multiProjectWebWorkflow() {
4483
4603
  return json.session;
4484
4604
  };
4485
4605
  const stopSession = async (id, payload = null, projectRoot = root) => {
4486
- const options = payload
4487
- ? {
4488
- method: 'POST',
4489
- headers: { 'Content-Type': 'application/json' },
4490
- body: JSON.stringify(payload)
4491
- }
4492
- : { method: 'POST' };
4493
- const response = await runtimeFetch(`/api/sessions/${encodeURIComponent(id)}/stop`, options, { root: projectRoot });
4606
+ const response = await stopControlledSession(id, payload || {}, projectRoot);
4494
4607
  const json = await response.json();
4495
4608
  if (!response.ok) fail(`web provider session stop failed: ${JSON.stringify(json)}`);
4496
4609
  return json.session;
@@ -4638,7 +4751,8 @@ async function multiProjectWebWorkflow() {
4638
4751
  const inputResponse = await cookieRuntimeFetch(inputRoute, cookieAdmin, {
4639
4752
  method: 'POST',
4640
4753
  headers: { 'Content-Type': 'application/json' },
4641
- body: JSON.stringify({ data: `echo ${marker}\r` })
4754
+ body: JSON.stringify({ data: `echo ${marker}\r`, action_token: cookieAdminWs.hccActionToken,
4755
+ epoch: cookieAdminWs.hccControl.epoch })
4642
4756
  }, { root });
4643
4757
  if (!inputResponse.ok) fail(`same-origin cookie administrator input returned ${inputResponse.status}`);
4644
4758
  });
@@ -4661,7 +4775,7 @@ async function multiProjectWebWorkflow() {
4661
4775
  const cookieStop = await cookieRuntimeFetch(stopRoute, cookieAdmin, {
4662
4776
  method: 'POST',
4663
4777
  headers: { 'Content-Type': 'application/json' },
4664
- body: '{}'
4778
+ body: JSON.stringify({ action_token: cookieAdminWs.hccActionToken, epoch: cookieAdminWs.hccControl.epoch })
4665
4779
  }, { root });
4666
4780
  if (!cookieStop.ok) fail(`same-origin cookie administrator stop returned ${cookieStop.status}`);
4667
4781
  } finally {
@@ -4758,14 +4872,16 @@ async function multiProjectWebWorkflow() {
4758
4872
  kind: 'shell',
4759
4873
  command: 'bash --noprofile --norc'
4760
4874
  });
4761
- const encodedActionToken = await fetchSessionActionToken(encodedActionId, { root });
4875
+ const encodedActionChannel = await openSessionActionChannel(encodedActionId, { root });
4876
+ const encodedActionToken = encodedActionChannel.token;
4762
4877
  const siblingActionId = `web-action-sibling-${testId}`;
4763
4878
  const siblingActionSession = await startProvider({
4764
4879
  id: siblingActionId,
4765
4880
  kind: 'shell',
4766
4881
  command: 'bash --noprofile --norc'
4767
4882
  }, otherRoot);
4768
- const siblingActionToken = await fetchSessionActionToken(siblingActionId, { root: otherRoot });
4883
+ const siblingActionChannel = await openSessionActionChannel(siblingActionId, { root: otherRoot });
4884
+ const siblingActionToken = siblingActionChannel.token;
4769
4885
  if (!encodedActionToken || !siblingActionToken) {
4770
4886
  fail(`alternate sessions did not deliver action tokens:\n${JSON.stringify({ encodedActionSession, siblingActionSession }, null, 2)}`);
4771
4887
  }
@@ -4798,10 +4914,12 @@ async function multiProjectWebWorkflow() {
4798
4914
  }
4799
4915
  await closeTerminalWebSocket(secondManagedChannel.ws);
4800
4916
  await expectActionTokenRejected(secondManagedChannel.token, 'closed-connection');
4917
+ await claimTerminalControl(managedActionChannel.ws);
4918
+ await claimTerminalControl(encodedActionChannel.ws);
4801
4919
  const encodedPeerAction = await runtimeFetch(`/api/peers/${encodeURIComponent(encodedActionId)}/actions/heartbeat`, {
4802
4920
  method: 'POST',
4803
4921
  headers: { 'Content-Type': 'application/json' },
4804
- body: JSON.stringify({ action_token: encodedActionToken, renew_locks: false })
4922
+ body: JSON.stringify({ action_token: encodedActionToken, epoch: encodedActionChannel.epoch, renew_locks: false })
4805
4923
  }, { root });
4806
4924
  if (!encodedPeerAction.ok) {
4807
4925
  fail(`URL-encoded peer action rejected its own token:\n${await encodedPeerAction.text()}`);
@@ -4810,11 +4928,13 @@ async function multiProjectWebWorkflow() {
4810
4928
  const actionNext = await (await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionSession.peer_id || managedActionSession.id)}/actions/task-next`, {
4811
4929
  method: 'POST',
4812
4930
  headers: { 'Content-Type': 'application/json' },
4813
- body: JSON.stringify({ action_token: managedActionToken })
4931
+ body: JSON.stringify({ action_token: managedActionToken, epoch: managedActionChannel.epoch })
4814
4932
  }, { root })).json();
4815
4933
  if (!actionNext.ok || actionNext.action !== 'task-next' || String(actionNext.data?.task?.id) !== taskMatch[1]) {
4816
4934
  fail(`web task-next action did not claim pending task #${taskMatch[1]}:\n${JSON.stringify(actionNext, null, 2)}`);
4817
4935
  }
4936
+ await closeTerminalWebSocket(encodedActionChannel.ws);
4937
+ await closeTerminalWebSocket(siblingActionChannel.ws);
4818
4938
  await stopSession(encodedActionSession.id);
4819
4939
  await stopSession(siblingActionSession.id, null, otherRoot);
4820
4940
  // Cookie-authenticated terminal sockets are tied to the opaque browser
@@ -4828,7 +4948,7 @@ async function multiProjectWebWorkflow() {
4828
4948
  method: 'POST',
4829
4949
  headers: { 'Content-Type': 'application/json' },
4830
4950
  body: JSON.stringify({
4831
- action_token: managedActionToken,
4951
+ action_token: managedActionToken, epoch: managedActionChannel.epoch,
4832
4952
  resource: webTtlResource,
4833
4953
  task: Number(taskMatch[1]),
4834
4954
  ttl: 60
@@ -4846,7 +4966,7 @@ async function multiProjectWebWorkflow() {
4846
4966
  method: 'POST',
4847
4967
  headers: { 'Content-Type': 'application/json' },
4848
4968
  body: JSON.stringify({
4849
- action_token: managedActionToken,
4969
+ action_token: managedActionToken, epoch: managedActionChannel.epoch,
4850
4970
  renew_locks: true,
4851
4971
  ...(ttlOverride === null ? {} : { ttl: ttlOverride })
4852
4972
  })
@@ -4890,7 +5010,7 @@ async function multiProjectWebWorkflow() {
4890
5010
  const unknownHeartbeatResponse = await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionPeer)}/actions/heartbeat`, {
4891
5011
  method: 'POST',
4892
5012
  headers: { 'Content-Type': 'application/json' },
4893
- body: JSON.stringify({ action_token: managedActionToken, renew_locks: true })
5013
+ body: JSON.stringify({ action_token: managedActionToken, epoch: managedActionChannel.epoch, renew_locks: true })
4894
5014
  }, { root });
4895
5015
  const unknownHeartbeat = await unknownHeartbeatResponse.json();
4896
5016
  if (!unknownHeartbeatResponse.ok || unknownHeartbeat.data?.renewed !== 0) {
@@ -4906,7 +5026,7 @@ async function multiProjectWebWorkflow() {
4906
5026
  const graceHeartbeatResponse = await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionPeer)}/actions/heartbeat`, {
4907
5027
  method: 'POST',
4908
5028
  headers: { 'Content-Type': 'application/json' },
4909
- body: JSON.stringify({ action_token: managedActionToken, renew_locks: true })
5029
+ body: JSON.stringify({ action_token: managedActionToken, epoch: managedActionChannel.epoch, renew_locks: true })
4910
5030
  }, { root });
4911
5031
  const graceHeartbeat = await graceHeartbeatResponse.json();
4912
5032
  const graceHeartbeatState = withMeshDb((db) => ({
@@ -4986,7 +5106,7 @@ async function multiProjectWebWorkflow() {
4986
5106
  method: 'POST',
4987
5107
  headers: { 'Content-Type': 'application/json' },
4988
5108
  body: JSON.stringify({
4989
- action_token: managedActionToken,
5109
+ action_token: managedActionToken, epoch: managedActionChannel.epoch,
4990
5110
  id: evidenceTaskId,
4991
5111
  reason: 'owner process identity was reused',
4992
5112
  policy: 'stale',
@@ -5047,7 +5167,7 @@ async function multiProjectWebWorkflow() {
5047
5167
  const liveLockResponse = await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionPeer)}/actions/lock-acquire`, {
5048
5168
  method: 'POST',
5049
5169
  headers: { 'Content-Type': 'application/json' },
5050
- body: JSON.stringify({ action_token: managedActionToken, resource: liveExpiredResource, ttl: 60 })
5170
+ body: JSON.stringify({ action_token: managedActionToken, epoch: managedActionChannel.epoch, resource: liveExpiredResource, ttl: 60 })
5051
5171
  }, { root });
5052
5172
  const liveLockResult = await liveLockResponse.json();
5053
5173
  if (liveLockResponse.ok || liveLockResult.error?.code !== 'LOCK_HELD') {
@@ -5075,7 +5195,7 @@ async function multiProjectWebWorkflow() {
5075
5195
  const deadLockResponse = await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionPeer)}/actions/lock-acquire`, {
5076
5196
  method: 'POST',
5077
5197
  headers: { 'Content-Type': 'application/json' },
5078
- body: JSON.stringify({ action_token: managedActionToken, resource: deadLockResource, ttl: 60 })
5198
+ body: JSON.stringify({ action_token: managedActionToken, epoch: managedActionChannel.epoch, resource: deadLockResource, ttl: 60 })
5079
5199
  }, { root });
5080
5200
  const deadLockResult = await deadLockResponse.json();
5081
5201
  if (!deadLockResponse.ok || !deadLockResult.ok || deadLockResult.data?.lock?.owner !== managedActionPeer) {
@@ -5104,7 +5224,7 @@ async function multiProjectWebWorkflow() {
5104
5224
  method: 'POST',
5105
5225
  headers: { 'Content-Type': 'application/json' },
5106
5226
  body: JSON.stringify({
5107
- action_token: managedActionToken,
5227
+ action_token: managedActionToken, epoch: managedActionChannel.epoch,
5108
5228
  resource: 'web/identity-spoof-lock',
5109
5229
  task: Number(ownerTaskMatch[1])
5110
5230
  })
@@ -5277,50 +5397,6 @@ async function statusServer(status) {
5277
5397
  };
5278
5398
  }
5279
5399
 
5280
- async function stalledRuntimeServer(phase) {
5281
- const child = spawn(process.execPath, ['-e', `
5282
- const http = require('node:http');
5283
- const sockets = new Set();
5284
- const server = http.createServer((req, res) => {
5285
- if (${JSON.stringify(phase)} === 'body') {
5286
- res.writeHead(200, { 'Content-Type': 'application/json' });
5287
- res.write('{"deleted":0');
5288
- }
5289
- });
5290
- server.on('connection', (socket) => {
5291
- sockets.add(socket);
5292
- socket.on('close', () => sockets.delete(socket));
5293
- });
5294
- server.listen(0, '127.0.0.1', () => process.stdout.write(String(server.address().port) + '\\n'));
5295
- process.on('SIGTERM', () => {
5296
- for (const socket of sockets) socket.destroy();
5297
- server.close(() => process.exit(0));
5298
- });
5299
- `], { stdio: ['ignore', 'pipe', 'inherit'] });
5300
- const port = await new Promise((resolve, reject) => {
5301
- let output = '';
5302
- const timer = setTimeout(() => reject(new Error(`timed out starting ${phase} stalled runtime`)), 5000);
5303
- child.stdout.on('data', (chunk) => {
5304
- output += chunk;
5305
- const match = output.match(/^(\d+)\n/);
5306
- if (!match) return;
5307
- clearTimeout(timer);
5308
- resolve(Number(match[1]));
5309
- });
5310
- child.once('exit', (code) => {
5311
- clearTimeout(timer);
5312
- reject(new Error(`${phase} stalled runtime exited early with ${code}`));
5313
- });
5314
- });
5315
- return {
5316
- baseUrl: `http://127.0.0.1:${port}`,
5317
- close: async () => {
5318
- child.kill('SIGTERM');
5319
- try { await waitForProcessExit(child.pid, `${phase} stalled runtime exit`, 3000); } catch {}
5320
- }
5321
- };
5322
- }
5323
-
5324
5400
  function bufferGcCanonicalizationRaceWorkflow() {
5325
5401
  for (const phase of ['before', 'after']) {
5326
5402
  const raceRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-realpath-${phase}-${testId}-`));
@@ -5445,7 +5521,10 @@ async function bufferGcArbitrationWorkflow() {
5445
5521
  if (!firstApply.ok || replayApply.ok) fail('buffer GC token was not exactly once');
5446
5522
 
5447
5523
  const cutoffMs = Date.now() - 60_000;
5448
- const oldTime = new Date(cutoffMs - 60_000);
5524
+ // Ordinary deletion checks use a non-zero retention window so crossing a
5525
+ // wall-clock second cannot legitimately activate clock-jump protection.
5526
+ const stableGcRetentionDays = 1;
5527
+ const oldTime = new Date(Date.now() - 2 * 86400_000);
5449
5528
  const rootBufs = path.join(root, '.hello-cc', 'bufs');
5450
5529
  const siblingBufs = path.join(secondProjectRoot, '.hello-cc', 'bufs');
5451
5530
  fs.mkdirSync(rootBufs, { recursive: true });
@@ -5586,7 +5665,9 @@ async function bufferGcArbitrationWorkflow() {
5586
5665
  const parentAlias = path.join(aliasContainer, 'parent');
5587
5666
  fs.symlinkSync(path.dirname(secondProjectRoot), parentAlias);
5588
5667
  const siblingAliasRoot = path.join(parentAlias, path.basename(secondProjectRoot));
5589
- const gcOutput = hccFrom(['--json', 'gc', '--older-than', '0', '--yes'], siblingAliasRoot);
5668
+ const gcOutput = hccFrom([
5669
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5670
+ ], siblingAliasRoot);
5590
5671
  fs.rmSync(aliasContainer, { recursive: true, force: true });
5591
5672
  const gcPayload = JSON.parse(gcOutput);
5592
5673
  const result = gcPayload.data || {};
@@ -5609,7 +5690,9 @@ async function bufferGcArbitrationWorkflow() {
5609
5690
  } finally {
5610
5691
  rootGcClockDb.close();
5611
5692
  }
5612
- const rootGcPayload = JSON.parse(hcc(['--json', 'gc', '--older-than', '0', '--yes']));
5693
+ const rootGcPayload = JSON.parse(hcc([
5694
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5695
+ ]));
5613
5696
  const rootGcResult = rootGcPayload.data || {};
5614
5697
  if (rootGcResult.buf_files !== 1 || rootGcResult.protected_buf_files < liveFiles.length ||
5615
5698
  rootGcResult.deferred_buf_files < legacyFiles.length || fs.existsSync(rootOrphan)) {
@@ -5735,7 +5818,9 @@ async function bufferGcArbitrationWorkflow() {
5735
5818
  const normalOrphan = path.join(siblingBufs, `gc-actual-dir-normal-${testId}.out`);
5736
5819
  fs.writeFileSync(normalOrphan, 'actual directory normal baseline');
5737
5820
  fs.utimesSync(normalOrphan, oldTime, oldTime);
5738
- const normalGc = JSON.parse(hccFrom(['--json', 'gc', '--older-than', '0', '--yes'], secondProjectRoot)).data;
5821
+ const normalGc = JSON.parse(hccFrom([
5822
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5823
+ ], secondProjectRoot)).data;
5739
5824
  if (fs.existsSync(gapOrphan) || fs.existsSync(normalOrphan) || Number(normalGc.buf_files || 0) < 2) {
5740
5825
  fail(`actual runtime buffer directory did not delete against a live baseline: ${JSON.stringify(normalGc)}`);
5741
5826
  }
@@ -5812,42 +5897,6 @@ async function bufferGcArbitrationWorkflow() {
5812
5897
  }
5813
5898
  }
5814
5899
 
5815
- for (const phase of ['headers', 'body']) {
5816
- const isolatedRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-stall-${phase}-${testId}-`));
5817
- const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-stall-home-${phase}-${testId}-`));
5818
- let server = null;
5819
- try {
5820
- const isolatedEnv = { ...env, HOME: isolatedHome };
5821
- run(process.execPath, [hccBin, '--root', isolatedRoot, 'init', '--no-guidance'], { env: isolatedEnv });
5822
- const directory = path.join(isolatedRoot, '.hello-cc', 'bufs');
5823
- fs.mkdirSync(directory, { recursive: true });
5824
- const orphan = path.join(directory, 'old-orphan.out');
5825
- fs.writeFileSync(orphan, 'orphan');
5826
- fs.utimesSync(orphan, oldTime, oldTime);
5827
- server = await stalledRuntimeServer(phase);
5828
- fs.writeFileSync(path.join(isolatedRoot, '.hello-cc', 'runtime.json'), JSON.stringify({
5829
- pid: process.pid,
5830
- base_url: server.baseUrl,
5831
- token: 'regression-token'
5832
- }));
5833
-
5834
- const startedAt = Date.now();
5835
- const gc = run(process.execPath, [hccBin, '--root', isolatedRoot, '--json', 'gc', '--older-than', '0', '--yes'], { env: isolatedEnv });
5836
- const elapsedMs = Date.now() - startedAt;
5837
- const payload = JSON.parse(gc);
5838
- if (elapsedMs >= 9000 ||
5839
- Number(payload.data?.buf_files || 0) !== 0 ||
5840
- Number(payload.data?.deferred_buf_files || 0) < 1 ||
5841
- !fs.existsSync(orphan)) {
5842
- fail(`runtime ${phase} stall did not fail closed within deadline (${elapsedMs}ms): ${gc}`);
5843
- }
5844
- } finally {
5845
- await server?.close();
5846
- fs.rmSync(isolatedRoot, { recursive: true, force: true });
5847
- fs.rmSync(isolatedHome, { recursive: true, force: true });
5848
- }
5849
- }
5850
-
5851
5900
  const localRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-local-${testId}-`));
5852
5901
  const localHome = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-local-home-${testId}-`));
5853
5902
  try {
@@ -5991,9 +6040,24 @@ async function tmuxBackedStartWorkflow() {
5991
6040
  if (!restoredAuthority?.payload?.tmux_session_created || !restoredAuthority?.payload?.tmux_session_id) {
5992
6041
  fail(`startup auto GC left no complete immutable tmux authority: ${JSON.stringify({ authorityBindingAfterStop, restoredAuthority })}`);
5993
6042
  }
5994
- withMeshDb((db) => db.prepare('UPDATE events SET created_at = ? WHERE id = ?')
5995
- .run(oldAuthorityAt, restoredAuthority.id));
5996
- hcc(['gc', '--older-than', '14', '--yes']);
6043
+ const authorityGcNow = Math.floor(Date.now() / 1000);
6044
+ withMeshDb((db) => {
6045
+ db.prepare('UPDATE events SET created_at = ? WHERE id = ?')
6046
+ .run(oldAuthorityAt, restoredAuthority.id);
6047
+ // Backdating the fixture must not itself look like a real wall-clock jump.
6048
+ // Establish the same current baseline used by the other explicit GC tests.
6049
+ db.prepare("DELETE FROM meta WHERE key IN ('clock_grace_until', 'clock_pending_gap')").run();
6050
+ db.prepare(`
6051
+ INSERT INTO meta(key, value) VALUES ('clock_last_observed_at', ?)
6052
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
6053
+ `).run(String(authorityGcNow));
6054
+ });
6055
+ // This assertion exercises manual history pruning. Request it explicitly
6056
+ // instead of racing the runtime's asynchronous startup auto-GC.
6057
+ const authorityGc = JSON.parse(hcc(['--json', 'gc', '--older-than', '14', '--history', '--yes']));
6058
+ if (authorityGc.data?.deferred_age_based || Number(authorityGc.data?.deferred_old_events || 0) !== 0) {
6059
+ fail(`tmux authority history GC was unexpectedly deferred: ${JSON.stringify(authorityGc.data)}`);
6060
+ }
5997
6061
  const authorityRowsAfterFullGc = withMeshDb((db) => db.prepare(`
5998
6062
  SELECT id FROM events
5999
6063
  WHERE type = 'tmux.session.attached'
@@ -6476,10 +6540,36 @@ function identityEnforcementWorkflow() {
6476
6540
  db2.close();
6477
6541
  }
6478
6542
 
6479
- hccFrom(['run', '--peer', 'fake-runner', '--', process.execPath, '-e', 'process.exit(0)'], identityRoot, { env: identityEnv });
6543
+ const readRunOwner = () => {
6544
+ const ownerDb = new DatabaseSync(dbPath);
6545
+ try {
6546
+ return {
6547
+ peer: ownerDb.prepare('SELECT * FROM peers WHERE id = ?').get('real-peer'),
6548
+ binding: ownerDb.prepare('SELECT * FROM peer_bindings WHERE peer = ?').get('real-peer') || null,
6549
+ runEvents: ownerDb.prepare("SELECT * FROM events WHERE actor = 'real-peer' AND type LIKE 'run.session.%' ORDER BY id").all()
6550
+ };
6551
+ } finally {
6552
+ ownerDb.close();
6553
+ }
6554
+ };
6555
+ // Earlier identity commands registered real-peer with the live regression
6556
+ // parent as owner. Correcting --peer must preserve that occupied identity.
6557
+ const ownerBeforeRun = readRunOwner();
6558
+ const occupiedRun = hccFromMaybe(['--json', 'run', '--peer', 'fake-runner', '--', process.execPath, '-e', 'process.exit(0)'], identityRoot, { env: identityEnv });
6559
+ let occupiedError;
6560
+ try { occupiedError = JSON.parse(occupiedRun.stderr).error; } catch {}
6561
+ if (occupiedRun.status === 0 || occupiedError?.code !== 'PEER_SESSION_EXISTS' || occupiedError?.peer !== 'real-peer') {
6562
+ fail(`run did not protect the existing system peer owner:\nstdout=${occupiedRun.stdout}\nstderr=${occupiedRun.stderr}`);
6563
+ }
6564
+ if (JSON.stringify(readRunOwner()) !== JSON.stringify(ownerBeforeRun)) {
6565
+ fail('rejected run changed the existing system peer, binding, or run events');
6566
+ }
6567
+
6568
+ const runnerEnv = { ...identityEnv, HCC_PEER: 'real-runner' };
6569
+ hccFrom(['run', '--peer', 'fake-runner', '--', process.execPath, '-e', 'process.exit(0)'], identityRoot, { env: runnerEnv });
6480
6570
  const db3 = new DatabaseSync(dbPath);
6481
6571
  try {
6482
- const realRunner = db3.prepare('SELECT id, status FROM peers WHERE id = ?').get('real-peer');
6572
+ const realRunner = db3.prepare('SELECT id, status FROM peers WHERE id = ?').get('real-runner');
6483
6573
  const fakeRunner = db3.prepare('SELECT id FROM peers WHERE id = ?').get('fake-runner');
6484
6574
  if (!realRunner || realRunner.status !== 'exited' || fakeRunner) {
6485
6575
  fail(`run did not enforce system peer identity:\n${JSON.stringify({ realRunner, fakeRunner }, null, 2)}`);
@@ -6487,6 +6577,9 @@ function identityEnforcementWorkflow() {
6487
6577
  } finally {
6488
6578
  db3.close();
6489
6579
  }
6580
+ if (JSON.stringify(readRunOwner()) !== JSON.stringify(ownerBeforeRun)) {
6581
+ fail('fresh run changed the occupied system peer owner');
6582
+ }
6490
6583
 
6491
6584
  hccFrom(['peer', 'stop', 'fake-target'], identityRoot, { env: identityEnv });
6492
6585
  const stopRequested = eventPayloads('peer.stop.requested', 5, dbPath)
@@ -6758,21 +6851,28 @@ async function syntaxAndHelp() {
6758
6851
  'function detectedPeerCanStop(peer)',
6759
6852
  "if (['exited', 'detached'].includes(status)) return false;",
6760
6853
  'const canStop = detectedPeerCanStop(p);',
6761
- '${canStop ?',
6854
+ "${dshCoordinationPeer(p) ? '' : canStop ?",
6762
6855
  'function providerSessionKnown(session)',
6763
6856
  "tr('providerSession') + '=' + sessionProvider(session) + ':' + (value || tr('unknown'))",
6764
- 'sessionCardDetailText(s)',
6857
+ "sessionDetailsHtml('managed:' + s.id",
6858
+ "[tr('runtime'), sessionRuntimeTarget(s)]",
6859
+ "[tr('providerSession'), sessionProvider(s) + ':' + (sessionProviderSessionValue(s) || tr('unknown'))]",
6860
+ "[tr('command'), s.command]",
6861
+ "[tr('cwd'), path]",
6765
6862
  'const activeDetectedPeers = visibleDetected.filter((p) => peerIsActive(p));',
6766
6863
  'const staleDetectedPeers = visibleDetected.filter((p) => !peerIsActive(p));',
6767
- "localStorage.setItem('hcc.showStaleDetected'",
6864
+ "hccUi.safeSet('hcc.showStaleDetected'",
6768
6865
  'id="toggleStaleDetected"',
6769
- "if (e.target.closest('[data-action]')) return;",
6866
+ "if (e.target.closest('details, [data-action]')) return;",
6770
6867
  'id="stopKillLabel" data-i18n="dialog.killTmux"',
6771
6868
  'id="stopCancelBtn" type="button" data-i18n="dialog.cancel"',
6772
6869
  'id="stopConfirmBtn" type="button" data-i18n="stop"'
6773
6870
  ]) {
6774
6871
  if (!webUiTemplateSource.includes(expected)) fail(`web display regression guard missing: ${expected}`);
6775
6872
  }
6873
+ // Execute the shipped renderer as well: a helper name alone does not prove
6874
+ // session identities remain visible or disclosure/action clicks stay local.
6875
+ run(process.execPath, ['--test', path.join(repoRoot, 'test', 'web-ui-session-display.test.mjs')]);
6776
6876
  if (webUiTemplateSource.includes("p.status === 'running' ?")) {
6777
6877
  fail('detected peer action rendering still depends on status === running instead of liveness');
6778
6878
  }
@@ -9280,8 +9380,9 @@ async function syntaxAndHelp() {
9280
9380
  };
9281
9381
  const html = webUiTemplate.webIndexHtml({ nonce: 'regression-template-nonce' });
9282
9382
  if (!html.includes('<!doctype html>') ||
9283
- !html.includes('<div class="app">') ||
9284
- !html.includes('<script src="/assets/xterm.js"></script>')) {
9383
+ !html.includes('<div class="app" data-view="terminal">') ||
9384
+ !html.includes('<script src="/assets/xterm.js"></script>') ||
9385
+ !html.includes('<script src="/assets/addon-fit.js"></script>')) {
9285
9386
  fail('web UI template module did not render the expected shell HTML');
9286
9387
  }
9287
9388
  const jsonReq = Readable.from(['{"ok":true}']);
@@ -9529,12 +9630,15 @@ async function syntaxAndHelp() {
9529
9630
  expectEqual(webRuntime.publicRuntimeUrl(ipv6WildcardRuntime, '/tmp/hcc project'), 'http://<machine-ip>:8788/?token=tok&project=%2Ftmp%2Fhcc%20project', 'publicRuntimeUrl ipv6 wildcard');
9530
9631
  expectEqual(webRuntime.localRuntimeUrl(localRuntime, null), 'http://127.0.0.1:8789/?token=tok', 'localRuntimeUrl no project');
9531
9632
  const mainHelp = run(process.execPath, [hccBin, '--help']);
9532
- if (mainHelp.includes('setup') || mainHelp.includes('--web-managed')) {
9633
+ if (/^\s*setup(?:\s|$)/m.test(mainHelp) || mainHelp.includes('--web-managed')) {
9533
9634
  fail(`public help exposes maintenance or removed commands:\n${mainHelp}`);
9534
9635
  }
9535
9636
  if (!mainHelp.includes(' update Update the global npm install of hello-cc')) {
9536
9637
  fail(`main help missing update command:\n${mainHelp}`);
9537
9638
  }
9639
+ if (!mainHelp.includes(' dsh <setup|status|web> Connect DeepSeek Harness through project-local hooks')) {
9640
+ fail(`main help missing dsh command:\n${mainHelp}`);
9641
+ }
9538
9642
  if (!mainHelp.includes(' state [--peer ID] Show timeline and next coordination action')) {
9539
9643
  fail(`main help missing state command:\n${mainHelp}`);
9540
9644
  }
@@ -9726,6 +9830,10 @@ function uninstallWorkflow() {
9726
9830
 
9727
9831
  async function processEvidenceWorkflow() {
9728
9832
  log('process evidence: live, reused, legacy, dead, root mismatch');
9833
+ // These fixtures deliberately backdate peers beyond the production reaper's
9834
+ // grace window. Keep the runtime stopped until the external-adoption cases so
9835
+ // a 30-second scheduler tick cannot turn an evidence snapshot into exited.
9836
+ await stopRuntime();
9729
9837
 
9730
9838
  const createOwnedTask = (peer, title) => {
9731
9839
  const output = hcc(['task', 'create', '--from', 'human', '--to', peer, '--title', title]);
@@ -9831,6 +9939,8 @@ async function processEvidenceWorkflow() {
9831
9939
  cleanupBindingPeers(rootPeer);
9832
9940
  }
9833
9941
 
9942
+ startRuntime();
9943
+ await waitRuntime();
9834
9944
  const externalDir = path.join(root, '.hello-cc', 'bufs');
9835
9945
  const shortExternalId = `evidence-external-short-${testId}`;
9836
9946
  const shortProducer = spawn(process.execPath, [
@@ -10711,17 +10821,18 @@ function manualGcRetentionContractWorkflow() {
10711
10821
  }
10712
10822
  }
10713
10823
 
10714
- // Name-authoritative re-adoption + dead-peer reaper (session leak hardening).
10824
+ // Intentional detach persists; unpaused orphans recover by name and dead peers
10825
+ // are reaped without treating terminal detachment as process death.
10715
10826
  async function sessionRecoveryWorkflow() {
10716
10827
  if (!tmuxAvailable()) {
10717
10828
  log('session recovery skipped (tmux not installed)');
10718
10829
  return;
10719
10830
  }
10720
- log('session recovery: re-adopt orphan tmux + reap dead peer');
10831
+ log('session recovery: persistent detach + explicit resume + orphan adoption + dead peer reaping');
10721
10832
  if (!fs.existsSync(path.join(root, '.hello-cc', 'runtime.json'))) startRuntime();
10722
10833
  await waitRuntime();
10723
10834
 
10724
- // ── Default Stop (no kill) must not strand a live managed session ──
10835
+ // Default Stop pauses Web adoption while leaving the local terminal alive.
10725
10836
  const readoptPeer = 'readopt-shell';
10726
10837
  const pane = parsePane(hcc(['peer', 'start', readoptPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
10727
10838
  const sessionName = tmuxManagedSession(root, readoptPeer);
@@ -10753,22 +10864,57 @@ async function sessionRecoveryWorkflow() {
10753
10864
  detachedTakeover.status === 0) {
10754
10865
  fail(`live detached tmux pane did not block takeover: ${JSON.stringify(detachedRow)}\n${detachedTakeover.stdout}\n${detachedTakeover.stderr}`);
10755
10866
  }
10756
- // Within a few poll cycles the name-sweep must re-adopt the still-live session.
10867
+ const pauseEvent = eventPayloads('web.adoption.paused')
10868
+ .find((event) => event.payload?.peer === readoptPeer && event.payload?.pane === pane);
10869
+ if (!pauseEvent) fail('default detach did not persist its adoption pause');
10870
+ // A genuinely orphaned binding has no intentional pause. Simulate runtime
10871
+ // state loss while keeping the managed tmux name and process identity.
10872
+ const orphanPeer = 'unpaused-orphan-shell';
10873
+ const orphanPane = parsePane(hcc(['peer', 'start', orphanPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
10874
+ const orphanSessionName = tmuxManagedSession(root, orphanPeer);
10875
+ await stopRuntime();
10876
+ withMeshDb((db) => {
10877
+ db.prepare('UPDATE peer_bindings SET runtime_target = NULL WHERE peer = ?').run(orphanPeer);
10878
+ db.prepare("UPDATE peers SET status = 'detached' WHERE id = ?").run(orphanPeer);
10879
+ });
10880
+ startRuntime();
10881
+ await waitRuntime();
10757
10882
  await waitFor(() => {
10758
- const row = peerBindingRow(readoptPeer);
10759
- return Boolean(row && row.runtime_target === pane);
10760
- }, 'orphan managed tmux session re-adoption', 20000);
10883
+ const row = peerBindingRow(orphanPeer);
10884
+ return Boolean(row && row.runtime_target === orphanPane);
10885
+ }, 'unpaused orphan managed tmux session re-adoption', 20000);
10886
+ // Cover another auto-attach cycle after restart: it must not override a pause.
10887
+ await sleep(5500);
10888
+ binding = peerBindingRow(readoptPeer);
10889
+ if (!binding || binding.runtime_target !== null ||
10890
+ runMaybe('tmux', ['has-session', '-t', sessionName]).status !== 0) {
10891
+ fail(`runtime restart or polling overrode an intentional live-terminal detach:\n${JSON.stringify(binding, null, 2)}`);
10892
+ }
10893
+ const pausedSessions = await (await runtimeFetch('/api/sessions', {}, { root })).json();
10894
+ if ((pausedSessions.sessions || []).some((session) => session.id === readoptPeer && session.status === 'running')) {
10895
+ fail(`intentionally detached session was automatically re-adopted:\n${JSON.stringify(pausedSessions, null, 2)}`);
10896
+ }
10897
+ const resumeResponse = await runtimeFetch('/api/sessions/attach', {
10898
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
10899
+ body: JSON.stringify({ id: readoptPeer, kind: 'shell', pane, force: true })
10900
+ }, { root });
10901
+ const resumed = await resumeResponse.json();
10902
+ if (!resumeResponse.ok || resumed.session?.pane !== pane) {
10903
+ fail(`explicit attach did not resume the same detached terminal:\n${JSON.stringify(resumed, null, 2)}`);
10904
+ }
10761
10905
  const restoredStatus = withMeshDb((db) => db.prepare('SELECT status FROM peers WHERE id = ?').get(readoptPeer)?.status);
10762
- if (restoredStatus !== 'running') fail(`re-adopted peer not marked running (status=${restoredStatus})`);
10763
- // Re-enterability: the runtime now exposes the session again.
10906
+ if (restoredStatus !== 'running') fail(`explicitly resumed peer not marked running (status=${restoredStatus})`);
10907
+ // Both explicit resume and automatic orphan recovery expose a live session.
10764
10908
  const sessions = await (await runtimeFetch('/api/sessions', {}, { root })).json();
10765
10909
  if (!(sessions.sessions || []).some((s) => s.id === readoptPeer && s.status === 'running')) {
10766
- fail(`re-adopted session not exposed by /api/sessions:\n${JSON.stringify(sessions, null, 2)}`);
10910
+ fail(`explicitly resumed session not exposed by /api/sessions:\n${JSON.stringify(sessions, null, 2)}`);
10911
+ }
10912
+ if (!(sessions.sessions || []).some((session) => session.id === orphanPeer && session.pane === orphanPane && session.status === 'running')) {
10913
+ fail(`unpaused orphan session not exposed by /api/sessions:\n${JSON.stringify(sessions, null, 2)}`);
10767
10914
  }
10768
10915
  hcc(['task', 'takeover', '--peer', 'human', '--id', detachedTaskId, '--reason', 'detached evidence cleanup', '--force']);
10769
10916
  hcc(['task', 'update', '--peer', 'human', '--id', detachedTaskId, '--status', 'abandoned', '--summary', 'detached evidence cleanup']);
10770
- // Destroying the live session is the real teardown path (CLI peer stop has
10771
- // no kill flag; the web API's kill_tmux is the equivalent). Kill it directly
10917
+ // Destroying the live session is the real teardown path. Kill it directly
10772
10918
  // so it does not leak into the final tmux-session leak assertion.
10773
10919
  runMaybe('tmux', ['kill-session', '-t', sessionName]);
10774
10920
  if (runMaybe('tmux', ['has-session', '-t', sessionName]).status === 0) {
@@ -10778,6 +10924,11 @@ async function sessionRecoveryWorkflow() {
10778
10924
  db.prepare('DELETE FROM peer_bindings WHERE peer = ?').run(readoptPeer);
10779
10925
  db.prepare('DELETE FROM peers WHERE id = ?').run(readoptPeer);
10780
10926
  });
10927
+ runMaybe('tmux', ['kill-session', '-t', orphanSessionName]);
10928
+ withMeshDb((db) => {
10929
+ db.prepare('DELETE FROM peer_bindings WHERE peer = ?').run(orphanPeer);
10930
+ db.prepare('DELETE FROM peers WHERE id = ?').run(orphanPeer);
10931
+ });
10781
10932
 
10782
10933
  // ── A peer whose process is gone must be reaped, not stuck at 'running' ──
10783
10934
  const reaped = spawnSync('sleep', ['10'], { stdio: 'ignore' });