instar 1.3.1344 → 1.3.1346

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 (85) hide show
  1. package/dashboard/index.html +50 -0
  2. package/dashboard/pairing.js +179 -0
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +105 -4
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  7. package/dist/core/PostUpdateMigrator.js +42 -12
  8. package/dist/core/PostUpdateMigrator.js.map +1 -1
  9. package/dist/core/SpawnAdmission.d.ts +1 -1
  10. package/dist/core/SpawnAdmission.d.ts.map +1 -1
  11. package/dist/core/SpawnAdmission.js.map +1 -1
  12. package/dist/core/SpawningTopicsRegistry.d.ts +13 -0
  13. package/dist/core/SpawningTopicsRegistry.d.ts.map +1 -1
  14. package/dist/core/SpawningTopicsRegistry.js +19 -0
  15. package/dist/core/SpawningTopicsRegistry.js.map +1 -1
  16. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  17. package/dist/core/WriteDomainRegistry.js +7 -0
  18. package/dist/core/WriteDomainRegistry.js.map +1 -1
  19. package/dist/data/standards-guard-index.json +1 -1
  20. package/dist/data/standards-guard-index.meta.json +2 -2
  21. package/dist/data/standards-registry.meta.json +1 -1
  22. package/dist/scaffold/templates.d.ts.map +1 -1
  23. package/dist/scaffold/templates.js +6 -2
  24. package/dist/scaffold/templates.js.map +1 -1
  25. package/dist/server/routes.d.ts.map +1 -1
  26. package/dist/server/routes.js +191 -28
  27. package/dist/server/routes.js.map +1 -1
  28. package/dist/threadline/AgentTrustManager.d.ts +31 -0
  29. package/dist/threadline/AgentTrustManager.d.ts.map +1 -1
  30. package/dist/threadline/AgentTrustManager.js +68 -0
  31. package/dist/threadline/AgentTrustManager.js.map +1 -1
  32. package/dist/threadline/CredentialShareGate.d.ts +31 -10
  33. package/dist/threadline/CredentialShareGate.d.ts.map +1 -1
  34. package/dist/threadline/CredentialShareGate.js +29 -4
  35. package/dist/threadline/CredentialShareGate.js.map +1 -1
  36. package/dist/threadline/PairVerifyReceipt.d.ts +14 -2
  37. package/dist/threadline/PairVerifyReceipt.d.ts.map +1 -1
  38. package/dist/threadline/PairVerifyReceipt.js +49 -10
  39. package/dist/threadline/PairVerifyReceipt.js.map +1 -1
  40. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  41. package/dist/threadline/ThreadlineBootstrap.js +13 -0
  42. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  43. package/dist/threadline/ThreadlineCrypto.d.ts +15 -0
  44. package/dist/threadline/ThreadlineCrypto.d.ts.map +1 -1
  45. package/dist/threadline/ThreadlineCrypto.js +105 -0
  46. package/dist/threadline/ThreadlineCrypto.js.map +1 -1
  47. package/dist/threadline/ThreadlineMCPServer.d.ts +17 -0
  48. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  49. package/dist/threadline/ThreadlineMCPServer.js +25 -6
  50. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  51. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  52. package/dist/threadline/ThreadlineRouter.js +43 -16
  53. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  54. package/dist/threadline/TopicLinkageHandler.d.ts +31 -1
  55. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
  56. package/dist/threadline/TopicLinkageHandler.js +34 -3
  57. package/dist/threadline/TopicLinkageHandler.js.map +1 -1
  58. package/dist/threadline/autoAck.d.ts +28 -0
  59. package/dist/threadline/autoAck.d.ts.map +1 -1
  60. package/dist/threadline/autoAck.js +35 -2
  61. package/dist/threadline/autoAck.js.map +1 -1
  62. package/dist/threadline/client/ThreadlineClient.d.ts +60 -0
  63. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  64. package/dist/threadline/client/ThreadlineClient.js +179 -4
  65. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  66. package/dist/threadline/mcp-http-client.d.ts +11 -0
  67. package/dist/threadline/mcp-http-client.d.ts.map +1 -1
  68. package/dist/threadline/mcp-http-client.js +29 -0
  69. package/dist/threadline/mcp-http-client.js.map +1 -1
  70. package/dist/threadline/mcp-stdio-entry.js +5 -1
  71. package/dist/threadline/mcp-stdio-entry.js.map +1 -1
  72. package/dist/threadline/topicRespawnHook.d.ts +67 -0
  73. package/dist/threadline/topicRespawnHook.d.ts.map +1 -0
  74. package/dist/threadline/topicRespawnHook.js +98 -0
  75. package/dist/threadline/topicRespawnHook.js.map +1 -0
  76. package/package.json +1 -1
  77. package/src/data/builtin-manifest.json +63 -63
  78. package/src/data/standards-guard-index.json +1 -1
  79. package/src/data/standards-guard-index.meta.json +2 -2
  80. package/src/data/standards-registry.meta.json +1 -1
  81. package/src/scaffold/templates.ts +6 -2
  82. package/upgrades/1.3.1345.md +38 -0
  83. package/upgrades/1.3.1346.md +27 -0
  84. package/upgrades/side-effects/threadline-reaped-topic-respawn.md +111 -0
  85. package/upgrades/side-effects/threadline-verified-pairing-v2.md +141 -0
@@ -1042,6 +1042,24 @@
1042
1042
  .mnd-reason-cell { color: var(--text-dim); }
1043
1043
  .mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
1044
1044
  .mnd-dim { font-size: 13px; color: var(--text-dim); }
1045
+ .pair-card { border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-top: 10px; }
1046
+ .pair-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
1047
+ .pair-state { font-size: 13px; color: var(--text-dim); }
1048
+ .pair-state-verification-failed { color: #f85149; }
1049
+ .pair-state-mutual-verified, .pair-state-identity-verified { color: #3fb950; }
1050
+ .pair-step { margin-top: 10px; }
1051
+ .pair-step-title { font-size: 13px; margin-bottom: 6px; }
1052
+ .pair-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 6px 0; }
1053
+ .pair-own-words { columns: 3; font-family: ui-monospace, monospace; font-size: 15px; margin: 6px 0; }
1054
+ .pair-words { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
1055
+ .pair-word { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-dim); }
1056
+ .pair-word input { flex: 1; min-width: 0; padding: 6px; font-family: ui-monospace, monospace; font-size: 15px; }
1057
+ .pair-meta { font-size: 11px; color: var(--text-dim); margin-top: 8px; }
1058
+ @media (max-width: 640px) {
1059
+ .pair-own-words { columns: 2; }
1060
+ .pair-words { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1061
+ .pair-row .mnd-pin, .pair-row .mnd-btn { flex: 1 1 100%; width: 100%; }
1062
+ }
1045
1063
  /* Mobile-first (item 5): at phone width the 5-column audit table stacks into
1046
1064
  labelled rows so the reason — the useful column — is never truncated. */
1047
1065
  @media (max-width: 640px) {
@@ -4208,6 +4226,17 @@
4208
4226
  <div id="tlBridgeError" style="color:var(--err, #c44);font-size:12px;display:none"></div>
4209
4227
  </div>
4210
4228
 
4229
+ <!-- Verified pairing — the 12-word check (docs/specs/secure-a2a-verified-pairing.md §3.6) -->
4230
+ <div id="pairCard" class="mnd-card" style="border:1px solid var(--border);border-radius:8px;padding:16px">
4231
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap">
4232
+ <strong>Verified pairing</strong>
4233
+ <button onclick="startPairingPanel()" class="mnd-btn mnd-btn-quiet" style="padding:6px 12px">Refresh</button>
4234
+ </div>
4235
+ <div class="mnd-hint">Confirm another agent is really who it says it is: compare 12 words with its operator before it can be sent credentials.</div>
4236
+ <div id="pairNotice" class="mnd-notice"></div>
4237
+ <div id="pairList"></div>
4238
+ </div>
4239
+
4211
4240
  <!-- Conversation observability — threads list + message stream + search -->
4212
4241
  <div style="border:1px solid var(--border);border-radius:8px;padding:0;display:flex;flex-direction:column;min-height:480px">
4213
4242
  <!-- Toolbar -->
@@ -5990,6 +6019,7 @@
5990
6019
  onActivate: () => {
5991
6020
  if (typeof loadThreadlineBridgeConfig === 'function') loadThreadlineBridgeConfig();
5992
6021
  if (typeof tlObsLoadThreads === 'function') tlObsLoadThreads();
6022
+ if (typeof startPairingPanel === 'function') startPairingPanel();
5993
6023
  },
5994
6024
  },
5995
6025
  {
@@ -10413,6 +10443,26 @@
10413
10443
  } catch { host.innerHTML = '<div style="opacity:.7">Throughput data is temporarily unavailable.</div>'; }
10414
10444
  }
10415
10445
 
10446
+ // ── Verified pairing panel (Threadline tab; secure-a2a-verified-pairing §3.6) ──
10447
+ // Lazy-loaded like Mandates. The PIN typed here is sent once per action, never stored.
10448
+ let __pairController = null;
10449
+ let __pairModule = null;
10450
+ async function startPairingPanel() {
10451
+ if (!__pairModule) {
10452
+ try { __pairModule = await import('/dashboard/pairing.js'); }
10453
+ catch (e) { console.error('[pairing] module load failed', e); return; }
10454
+ }
10455
+ if (!__pairController) {
10456
+ const els = { list: document.getElementById('pairList'), notice: document.getElementById('pairNotice') };
10457
+ const fetchImpl = (url, opts = {}) => fetch(url, {
10458
+ ...opts,
10459
+ headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
10460
+ });
10461
+ __pairController = __pairModule.createController({ doc: document, els, fetchImpl });
10462
+ }
10463
+ __pairController.start();
10464
+ }
10465
+
10416
10466
  // ── Mandates tab (coordination-mandate spec, decision 2A) ──
10417
10467
  // Same lazy dynamic-import pattern as Process Health. The PIN typed in this
10418
10468
  // tab is sent once per action and never stored anywhere.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * pairing.js — the Verified Pairing panel on the dashboard's Threadline tab
3
+ * (docs/specs/secure-a2a-verified-pairing.md §3.6, Mobile-Complete operator action).
4
+ *
5
+ * The operator's surface for the 12-word check: list pairings; for a pending one,
6
+ * enter the dashboard PIN to see THIS side's 12 words, then type the 12 words the
7
+ * OTHER side shows and confirm (the server compares all 12 in order; any difference
8
+ * is a denial). A denied pairing can be cleared with the PIN. The PIN is typed at
9
+ * action time, sent once, and never stored (no localStorage, no module state).
10
+ *
11
+ * Same shape as mandates.js: pure renderers + createController({ doc, els, fetchImpl }).
12
+ */
13
+
14
+ const REFRESH_MS = 30_000;
15
+ const WORD_COUNT = 12;
16
+
17
+ function esc(s) {
18
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
19
+ }
20
+
21
+ const STATE_LABEL = {
22
+ 'pending-verification': 'Waiting for you to compare the words',
23
+ 'mutual-verified': 'Verified',
24
+ 'identity-verified': 'Verified on another of my machines',
25
+ 'verification-failed': 'Words did not match — blocked',
26
+ };
27
+
28
+ /** One pairing card. Primary action first; the fingerprint is support metadata. */
29
+ export function renderPairing(p) {
30
+ const fp = esc(p.peerFp);
31
+ const name = esc(p.peerName || 'Unknown agent');
32
+ const state = p.state;
33
+ const label = esc(STATE_LABEL[state] || state || 'Unknown');
34
+ let actions = '';
35
+ if (state === 'pending-verification') {
36
+ const inputs = Array.from({ length: WORD_COUNT }, (_, i) =>
37
+ `<label class="pair-word"><span>${i + 1}</span><input type="text" autocomplete="off" autocapitalize="off" spellcheck="false" data-their-word="${fp}" data-idx="${i}" aria-label="Their word ${i + 1}" /></label>`).join('');
38
+ actions = `
39
+ <div class="pair-step">
40
+ <div class="pair-step-title">1. See the words on this side</div>
41
+ <div class="pair-row"><input type="password" class="mnd-pin" data-show-pin="${fp}" placeholder="Your PIN" autocomplete="off" />
42
+ <button class="mnd-btn mnd-btn-primary" data-show-words="${fp}">Show my words</button></div>
43
+ <ol class="pair-own-words" data-own-words="${fp}"></ol>
44
+ </div>
45
+ <div class="pair-step">
46
+ <div class="pair-step-title">2. Type the 12 words the OTHER agent's operator sees</div>
47
+ <div class="pair-words">${inputs}</div>
48
+ <div class="pair-row"><input type="password" class="mnd-pin" data-confirm-pin="${fp}" placeholder="Your PIN" autocomplete="off" />
49
+ <button class="mnd-btn mnd-btn-primary" data-confirm="${fp}">Confirm they match</button></div>
50
+ <div class="mnd-hint">Compare over a call or in person, not through the agents. Any wrong word blocks this pairing.</div>
51
+ <button class="mnd-btn mnd-btn-quiet mnd-btn-danger" data-deny="${fp}">They don't match</button>
52
+ </div>`;
53
+ } else if (state === 'verification-failed') {
54
+ actions = `
55
+ <div class="pair-step">
56
+ <div class="mnd-hint">Clear this only once you know why the words were different.</div>
57
+ <div class="pair-row"><input type="password" class="mnd-pin" data-clear-pin="${fp}" placeholder="Your PIN" autocomplete="off" />
58
+ <button class="mnd-btn mnd-btn-quiet" data-clear="${fp}">Clear and start again</button></div>
59
+ </div>`;
60
+ }
61
+ return `<div class="pair-card" data-pairing="${fp}">
62
+ <div class="pair-head"><strong>${name}</strong> <span class="pair-state pair-state-${esc(state)}">${label}</span></div>
63
+ ${actions}
64
+ <div class="pair-meta">Agent ${fp.slice(0, 12)}…</div>
65
+ </div>`;
66
+ }
67
+
68
+ export function renderPairings(list) {
69
+ if (!Array.isArray(list) || list.length === 0) {
70
+ return '<div class="mnd-hint">No pairings yet. An agent starts one with <code>threadline_pair</code> start.</div>';
71
+ }
72
+ return list.map(renderPairing).join('');
73
+ }
74
+
75
+ /** Words typed into the 12 inputs for one pairing, in order. */
76
+ export function collectTypedWords(root, fp) {
77
+ const inputs = Array.from(root.querySelectorAll(`[data-their-word="${fp}"]`));
78
+ inputs.sort((a, b) => Number(a.dataset.idx) - Number(b.dataset.idx));
79
+ return inputs.map((i) => String(i.value || '').trim().toLowerCase());
80
+ }
81
+
82
+ export function createController({ doc, els, fetchImpl }) {
83
+ let timer = null;
84
+
85
+ async function fetchJson(url, opts) {
86
+ const res = await fetchImpl(url, opts);
87
+ let body = null;
88
+ try { body = await res.json(); } catch { body = null; }
89
+ return { status: res.status, body };
90
+ }
91
+
92
+ function note(msg, kind) {
93
+ if (!els.notice) return;
94
+ els.notice.textContent = msg;
95
+ els.notice.className = kind === 'error' ? 'mnd-notice mnd-notice-err' : kind === 'success' ? 'mnd-notice mnd-notice-ok' : 'mnd-notice';
96
+ }
97
+
98
+ // A timed refresh must not wipe words or a PIN the operator is typing.
99
+ function operatorIsTyping() {
100
+ const inputs = els.list ? Array.from(els.list.querySelectorAll('input')) : [];
101
+ return inputs.some((i) => i.value);
102
+ }
103
+
104
+ async function load(opts = {}) {
105
+ if (opts.timed && operatorIsTyping()) return;
106
+ try {
107
+ const { status, body } = await fetchJson('/threadline/pairing');
108
+ if (status === 503) { els.list.innerHTML = '<div class="mnd-hint">Verified pairing is turned off on this agent.</div>'; return; }
109
+ if (status !== 200) { note(`Could not load pairings: ${body?.error ?? `HTTP ${status}`}`, 'error'); return; }
110
+ els.list.innerHTML = renderPairings(body?.pairings ?? []);
111
+ } catch (e) {
112
+ note(`Could not reach the agent: ${e?.message ?? e}`, 'error');
113
+ }
114
+ }
115
+
116
+ async function post(url, payload) {
117
+ return fetchJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
118
+ }
119
+
120
+ async function onClick(ev) {
121
+ const t = ev.target;
122
+ if (!t || !t.dataset) return;
123
+ const root = els.list;
124
+ const take = (attr, fp) => { const el = root.querySelector(`[${attr}="${fp}"]`); const v = el ? el.value : ''; if (el) el.value = ''; return v; };
125
+ try {
126
+ if (t.dataset.showWords) {
127
+ const fp = t.dataset.showWords;
128
+ const pin = take('data-show-pin', fp);
129
+ if (!pin) { note('Enter your PIN to see the words.', 'error'); return; }
130
+ const res = await fetchImpl(`/threadline/pairing/${fp}`, { headers: { 'X-Dashboard-Pin': pin } });
131
+ const body = await res.json().catch(() => null);
132
+ const words = body?.pairing?.sasWords;
133
+ if (!Array.isArray(words)) { note(res.status === 200 ? 'The PIN was not accepted, or this pairing is no longer waiting.' : `Refused: ${body?.error ?? `HTTP ${res.status}`}`, 'error'); return; }
134
+ const ol = root.querySelector(`[data-own-words="${fp}"]`);
135
+ if (ol) ol.innerHTML = words.map((w) => `<li>${esc(w)}</li>`).join('');
136
+ note('These are the words on this side. Read them to the other operator and type theirs below.', 'info');
137
+ } else if (t.dataset.confirm) {
138
+ const fp = t.dataset.confirm;
139
+ const words = collectTypedWords(root, fp);
140
+ const pin = take('data-confirm-pin', fp);
141
+ if (words.some((w) => !w)) { note(`Type all ${WORD_COUNT} of the other side's words first.`, 'error'); return; }
142
+ if (!pin) { note('Enter your PIN to confirm.', 'error'); return; }
143
+ const { status, body } = await post(`/threadline/pairing/${fp}/verify`, { words, pin });
144
+ if (status !== 200) { note(`Not confirmed — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error'); return; }
145
+ note(body?.state === 'mutual-verified' ? 'Verified. Every word matched.' : 'The words did not match. This pairing is now blocked.', body?.state === 'mutual-verified' ? 'success' : 'error');
146
+ await load();
147
+ } else if (t.dataset.deny) {
148
+ const fp = t.dataset.deny;
149
+ const pin = take('data-confirm-pin', fp);
150
+ if (!pin) { note('Enter your PIN (in the field above) to record that the words did not match.', 'error'); return; }
151
+ const { status, body } = await post(`/threadline/pairing/${fp}/verify`, { match: false, pin });
152
+ if (status !== 200) { note(`Not recorded — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error'); return; }
153
+ note('Recorded: the words did not match. This pairing is blocked.', 'success');
154
+ await load();
155
+ } else if (t.dataset.clear) {
156
+ const fp = t.dataset.clear;
157
+ const pin = take('data-clear-pin', fp);
158
+ if (!pin) { note('Enter your PIN to clear this pairing.', 'error'); return; }
159
+ const { status, body } = await post(`/threadline/pairing/${fp}/start`, { clearFailed: true, pin });
160
+ if (status !== 200) { note(`Not cleared — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error'); return; }
161
+ note('Cleared. Compare the words again.', 'success');
162
+ await load();
163
+ }
164
+ } catch (e) {
165
+ note(`Could not reach the agent: ${e?.message ?? e}`, 'error');
166
+ }
167
+ }
168
+
169
+ return {
170
+ start() {
171
+ if (els.list && !els.list.__pairWired) { els.list.addEventListener('click', onClick); els.list.__pairWired = true; }
172
+ load();
173
+ if (!timer) timer = setInterval(() => load({ timed: true }), REFRESH_MS);
174
+ },
175
+ stop() { if (timer) { clearInterval(timer); timer = null; } },
176
+ load,
177
+ onClick,
178
+ };
179
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
@@ -242,7 +242,8 @@ import { ConversationStore } from '../threadline/ConversationStore.js';
242
242
  import { ThreadLog } from '../threadline/ThreadLog.js';
243
243
  import { ThreadMessageRecorder } from '../threadline/recordThreadMessage.js';
244
244
  import { recordInboundAck } from '../threadline/recordInboundAck.js';
245
- import { isAutoAckInbound, autoAckRecognition, looksLikeAutoAckText, createAckRateLimiter, runRelayAckStage } from '../threadline/autoAck.js';
245
+ import { isAutoAckInbound, autoAckRecognition, looksLikeAutoAckText, createAckRateLimiter, runRelayAckStage, settleDeferredRelayAck } from '../threadline/autoAck.js';
246
+ import { createThreadlineTopicRespawn } from '../threadline/topicRespawnHook.js';
246
247
  import { WarrantsReplyGate, evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
247
248
  import { CollaborationSurfacer } from '../threadline/CollaborationSurfacer.js';
248
249
  import { ListenerSessionManager } from '../threadline/ListenerSessionManager.js';
@@ -586,6 +587,12 @@ let _sweptInboundStore = null;
586
587
  // The drain's local-delivery tail (§3.1 via:'drain') — assigned inside
587
588
  // wireTelegramRouting where the session primitives live in scope.
588
589
  let _drainLocalDeliver = null;
590
+ // ACT-091: wake a topic's dead/reaped session for a topic-bound Threadline
591
+ // reply — assigned inside wireTelegramRouting where the respawn primitives
592
+ // (and the per-topic spawn guard) live in scope. Null ⇒ not wired.
593
+ let _threadlineTopicRespawn = null;
594
+ /** ACT-091: "is a spawn for this topic in flight?" — the spawningTopics read, for the handler. */
595
+ let _threadlineTopicSpawning = null;
589
596
  // Emergency-stop integration (§3.6): marks a topic stopped for the drain's
590
597
  // pass/batch/chokepoint consults AND settles its custody (terminal
591
598
  // operator-stop + PIS cleanup + loss report). Set with the engine.
@@ -3049,6 +3056,72 @@ getHubClassifierDeps) {
3049
3056
  spawningTopics.clear(topicId, _spawnTokD);
3050
3057
  }
3051
3058
  };
3059
+ // ── ACT-091: topic-bound Threadline reply into a dead/reaped topic session ──
3060
+ // The decision lives in createThreadlineTopicRespawn (testable); this binds
3061
+ // it to the REAL primitives the Telegram dead-session path uses: ownership,
3062
+ // the G3 lease gate, the death classification, SpawnAdmission (its own
3063
+ // journal-only callsite), the spawningTopics guard, respawnSessionForTopic
3064
+ // (--resume) / spawnSessionForTopic, each awaited through the initial
3065
+ // injection. A spawn already in flight is WAITED for, then the reply is typed
3066
+ // into the live session — a second reply inside one respawn is never lost.
3067
+ {
3068
+ _threadlineTopicSpawning = (topicId) => spawningTopics.has(topicId);
3069
+ _threadlineTopicRespawn = createThreadlineTopicRespawn({
3070
+ spawning: spawningTopics,
3071
+ ownedElsewhere: (topicId) => {
3072
+ if (!_ownershipReadForDrain || !_meshSelfId)
3073
+ return false;
3074
+ const ownRec = _ownershipReadForDrain(String(topicId));
3075
+ return !!ownRec && ownRec.status === 'active' && ownRec.ownerMachineId !== _meshSelfId;
3076
+ },
3077
+ registeredSession: (topicId) => telegram.getSessionForTopic(topicId),
3078
+ isSessionAlive: (name) => sessionManager.isSessionAlive(name),
3079
+ injectConfirmed: (name, text) => sessionManager.injectPasteNotificationConfirmed(name, text),
3080
+ leaseGate: (topicId) => g3ShouldSpawnLocally(topicId),
3081
+ classifyDeath: (name) => {
3082
+ try {
3083
+ const output = sessionManager.captureOutput(name, 100);
3084
+ if (!output)
3085
+ return null;
3086
+ const death = classifySessionDeath(output, quotaTracker?.getState() ?? null);
3087
+ if (death.cause === 'quota_exhaustion' && death.confidence !== 'low')
3088
+ return 'quota';
3089
+ if (death.cause === 'context_exhausted')
3090
+ return 'context-exhausted';
3091
+ return 'other';
3092
+ }
3093
+ catch {
3094
+ return null; /* @silent-fallback-ok: classification failed — respawn as the Telegram dead path does */
3095
+ }
3096
+ },
3097
+ admit: (topicId) => {
3098
+ if (!_spawnAdmission)
3099
+ return { allow: true };
3100
+ try {
3101
+ const d = _spawnAdmission.admit({ sessionKey: String(topicId), callsite: 'threadline-topic-respawn' });
3102
+ if (d.allow)
3103
+ _ladderDryRunConsult(d, String(topicId), topicId);
3104
+ return { allow: d.allow, reason: d.reason };
3105
+ }
3106
+ catch (err) {
3107
+ // Same posture as the Telegram seam: an admission ERROR fails open.
3108
+ console.warn(`[spawn-admission] threadline-topic-respawn admit error (fail-open): ${err instanceof Error ? err.message : String(err)}`);
3109
+ return { allow: true };
3110
+ }
3111
+ },
3112
+ respawn: async (name, topicId, text, fresh) => {
3113
+ if (fresh)
3114
+ _topicResumeMap?.remove(topicId);
3115
+ await respawnSessionForTopic(sessionManager, telegram, name, topicId, text, topicMemory, undefined, undefined, { silent: true, awaitInitialInjection: true });
3116
+ },
3117
+ coldSpawn: async (topicId, text) => {
3118
+ const topicName = telegram.getTopicName(topicId) || `topic-${topicId}`;
3119
+ const newSessionName = await spawnSessionForTopic(sessionManager, telegram, topicName, topicId, text, topicMemory, undefined, undefined, undefined, { awaitInitialInjection: true });
3120
+ telegram.registerTopicSession(topicId, newSessionName, topicName);
3121
+ return newSessionName;
3122
+ },
3123
+ });
3124
+ }
3052
3125
  }
3053
3126
  /**
3054
3127
  * Wire WhatsApp message routing: incoming messages → Claude sessions.
@@ -16856,6 +16929,12 @@ export async function startServer(options) {
16856
16929
  getSessionForTopic: telegram
16857
16930
  ? (topicId) => telegram.getSessionForTopic(topicId)
16858
16931
  : undefined,
16932
+ // ACT-091: a reply into a reaped topic session wakes it (late-bound —
16933
+ // Telegram routing is wired after this handler is constructed).
16934
+ respawnTopicSession: (args) => _threadlineTopicRespawn
16935
+ ? _threadlineTopicRespawn(args)
16936
+ : Promise.resolve({ delivered: false, reason: 'telegram-routing-not-wired' }),
16937
+ isTopicSpawning: (topicId) => _threadlineTopicSpawning?.(topicId) === true,
16859
16938
  // A2A cross-machine route §5: a topic-bound reply is typed into the
16860
16939
  // topic's live session on whichever machine has it. Late-bound: the
16861
16940
  // mesh client exists only after the multi-machine wiring below.
@@ -17375,13 +17454,22 @@ export async function startServer(options) {
17375
17454
  // Telegram mirror and the gate. Any other message gets our own ack
17376
17455
  // (post-trust-verification, never for status messages, rate-limited).
17377
17456
  const recordRelayDelivery = () => recordInboundAck({ a2aDeliveryTracker }, { threadId: msg.threadId, senderFingerprint, senderName: senderName ?? null });
17378
- const ackStage = runRelayAckStage({
17457
+ // ACT-091: a topic-bound reply can reach no live session (its topic
17458
+ // session was reaped), so its ack — which promises a reply — waits for
17459
+ // the routing outcome (settleDeferredRelayAck below).
17460
+ const isTopicBoundReply = !!msg.threadId && threadResumeMap.get(msg.threadId)?.originTopicId !== undefined;
17461
+ const relayAckDeps = {
17379
17462
  autoAckEnabled: config.threadline?.autoAck !== false,
17380
17463
  autoAckMessage: config.threadline?.autoAckMessage,
17381
17464
  isAckRateLimited,
17382
17465
  sendAck: (to, text, threadId) => { threadlineRelayClient.sendAck(to, text, threadId); },
17383
17466
  recordDelivery: recordRelayDelivery,
17384
- }, { isAutoAck, trustLevel, msgType, senderFingerprint, threadId: msg.threadId, ticket: ledgerTicket });
17467
+ };
17468
+ const relayAckInput = {
17469
+ isAutoAck, trustLevel, msgType, senderFingerprint, threadId: msg.threadId, ticket: ledgerTicket,
17470
+ deferSend: isTopicBoundReply,
17471
+ };
17472
+ const ackStage = runRelayAckStage(relayAckDeps, relayAckInput);
17385
17473
  if (ackStage === 'ack-consumed') {
17386
17474
  console.log(`[relay] delivery ack from ${senderName} (thread: ${msg.threadId ?? 'none'}, by ${autoAckRecognition({ type: msgType, text: textContent })}) recorded — not acked, not routed`);
17387
17475
  return;
@@ -17462,6 +17550,11 @@ export async function startServer(options) {
17462
17550
  else
17463
17551
  ledgerTicket?.recordNoReply();
17464
17552
  console.log(`[relay] warrants-reply gate suppressed reply (${decision.verdict.signal}) for ${senderName} thread ${gateThreadId.slice(0, 8)}`);
17553
+ // ACT-091: a deferred ack on a terminal no-reply exit becomes the
17554
+ // non-promising "Message received." — received, deliberately not
17555
+ // answered — so the sender's tracker never reads a stuck channel.
17556
+ if (relayAckInput.deferSend)
17557
+ settleDeferredRelayAck(relayAckDeps, relayAckInput, { delivered: false, noReply: true });
17465
17558
  // On budget exhaustion, surface ONE status notice — never silently
17466
17559
  // drop. CMT-519: route it to the SILENT Threadline hub (not a
17467
17560
  // per-event attention topic, not the parent topic the operator is
@@ -17551,7 +17644,6 @@ export async function startServer(options) {
17551
17644
  // for the user even though transport succeeded. (The pipe-spawn branch
17552
17645
  // above is already excluded for topic-bound threads via its
17553
17646
  // `!threadResumeMap.get(...)` guard, now that get() no longer nulls them.)
17554
- const isTopicBoundReply = !!msg.threadId && threadResumeMap.get(msg.threadId)?.originTopicId !== undefined;
17555
17647
  if (listenerManager && listenerManager.shouldUseListener(trustLevel, textContent.length) && !isTopicBoundReply) {
17556
17648
  listenerManager.writeToInbox({ from: senderFingerprint, senderName, trustLevel, threadId: msg.threadId ?? getSyntheticThreadId(senderFingerprint), text: ledgerNotice ? `[server notice: ${ledgerNotice}]\n\n${textContent}` : textContent });
17557
17649
  ledgerTicket?.recordHandoff('listener');
@@ -17596,6 +17688,15 @@ export async function startServer(options) {
17596
17688
  console.log(`[relay] Spawned session for ${senderName} (trust: ${trustLevel}, thread: ${result.threadId})`);
17597
17689
  if (result.resumed)
17598
17690
  console.log(`[relay] Resumed session for ${senderName} (thread: ${result.threadId})`);
17691
+ if (result.handled && result.delivered === false) {
17692
+ console.warn(`[relay] Message from ${senderName} (thread: ${result.threadId}) accepted but not delivered to a live session${result.queued ? ' (queued)' : ''}`);
17693
+ }
17694
+ if (relayAckInput.deferSend) {
17695
+ const ackOutcome = settleDeferredRelayAck(relayAckDeps, relayAckInput, { delivered: result.delivered === true });
17696
+ if (ackOutcome === 'skipped-undelivered') {
17697
+ console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${result.threadId}) — no live session received the message`);
17698
+ }
17699
+ }
17599
17700
  }
17600
17701
  // Relay client is passed to AgentServer → RouteContext for the /threadline/relay-send endpoint
17601
17702
  // Report the client's ACTUAL state. This line used to say "connected"