instar 1.3.501 → 1.3.502

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 (75) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +537 -9
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +41 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/BackupManager.d.ts.map +1 -1
  8. package/dist/core/BackupManager.js +7 -0
  9. package/dist/core/BackupManager.js.map +1 -1
  10. package/dist/core/DeliverMessageHandler.d.ts +13 -1
  11. package/dist/core/DeliverMessageHandler.d.ts.map +1 -1
  12. package/dist/core/DeliverMessageHandler.js +7 -0
  13. package/dist/core/DeliverMessageHandler.js.map +1 -1
  14. package/dist/core/MachinePoolRegistry.d.ts +3 -0
  15. package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
  16. package/dist/core/MachinePoolRegistry.js +1 -0
  17. package/dist/core/MachinePoolRegistry.js.map +1 -1
  18. package/dist/core/OwnerSuspectBreaker.d.ts +37 -0
  19. package/dist/core/OwnerSuspectBreaker.d.ts.map +1 -1
  20. package/dist/core/OwnerSuspectBreaker.js +58 -0
  21. package/dist/core/OwnerSuspectBreaker.js.map +1 -1
  22. package/dist/core/PendingInboundStore.d.ts +310 -0
  23. package/dist/core/PendingInboundStore.d.ts.map +1 -0
  24. package/dist/core/PendingInboundStore.js +715 -0
  25. package/dist/core/PendingInboundStore.js.map +1 -0
  26. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  27. package/dist/core/PostUpdateMigrator.js +13 -0
  28. package/dist/core/PostUpdateMigrator.js.map +1 -1
  29. package/dist/core/QueueDrainLoop.d.ts +247 -0
  30. package/dist/core/QueueDrainLoop.d.ts.map +1 -0
  31. package/dist/core/QueueDrainLoop.js +0 -0
  32. package/dist/core/QueueDrainLoop.js.map +1 -0
  33. package/dist/core/SessionRouter.d.ts +36 -2
  34. package/dist/core/SessionRouter.d.ts.map +1 -1
  35. package/dist/core/SessionRouter.js +34 -9
  36. package/dist/core/SessionRouter.js.map +1 -1
  37. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  38. package/dist/core/devGatedFeatures.js +10 -0
  39. package/dist/core/devGatedFeatures.js.map +1 -1
  40. package/dist/core/inboundQueueBootSweep.d.ts +61 -0
  41. package/dist/core/inboundQueueBootSweep.d.ts.map +1 -0
  42. package/dist/core/inboundQueueBootSweep.js +207 -0
  43. package/dist/core/inboundQueueBootSweep.js.map +1 -0
  44. package/dist/core/inboundQueueConfig.d.ts +78 -0
  45. package/dist/core/inboundQueueConfig.d.ts.map +1 -0
  46. package/dist/core/inboundQueueConfig.js +135 -0
  47. package/dist/core/inboundQueueConfig.js.map +1 -0
  48. package/dist/core/types.d.ts +23 -0
  49. package/dist/core/types.d.ts.map +1 -1
  50. package/dist/core/types.js.map +1 -1
  51. package/dist/messaging/TelegramAdapter.d.ts +4 -0
  52. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  53. package/dist/messaging/TelegramAdapter.js +12 -0
  54. package/dist/messaging/TelegramAdapter.js.map +1 -1
  55. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  56. package/dist/monitoring/guardManifest.js +27 -0
  57. package/dist/monitoring/guardManifest.js.map +1 -1
  58. package/dist/scaffold/templates.d.ts.map +1 -1
  59. package/dist/scaffold/templates.js +6 -0
  60. package/dist/scaffold/templates.js.map +1 -1
  61. package/dist/server/AgentServer.d.ts +2 -0
  62. package/dist/server/AgentServer.d.ts.map +1 -1
  63. package/dist/server/AgentServer.js +1 -0
  64. package/dist/server/AgentServer.js.map +1 -1
  65. package/dist/server/routes.d.ts +4 -0
  66. package/dist/server/routes.d.ts.map +1 -1
  67. package/dist/server/routes.js +25 -0
  68. package/dist/server/routes.js.map +1 -1
  69. package/package.json +1 -1
  70. package/scripts/lint-guard-manifest.js +2 -0
  71. package/src/data/builtin-manifest.json +65 -65
  72. package/src/scaffold/templates.ts +6 -0
  73. package/upgrades/1.3.502.md +75 -0
  74. package/upgrades/side-effects/1.3.492.md +20 -0
  75. package/upgrades/side-effects/durable-inbound-message-queue.md +213 -0
@@ -366,6 +366,20 @@ let _topicResumeMap = null;
366
366
  // is byte-identical to today's always-local dispatch. Set once in startServer().
367
367
  let _sessionRouter = null;
368
368
  let _sessionPoolStage = () => 'dark';
369
+ // ── Durable Inbound Message Queue (docs/specs/durable-inbound-message-queue.md) ──
370
+ // The custody engine (null = feature dark / gate failed / invariants violated —
371
+ // every consumer treats null as "refused → today's fall-through").
372
+ let _inboundQueue = null;
373
+ // The store the unconditional boot sweep opened when the queue will run this
374
+ // boot — adopted by the engine construction (one open handle, single-writer).
375
+ let _sweptInboundStore = null;
376
+ // The drain's local-delivery tail (§3.1 via:'drain') — assigned inside
377
+ // wireTelegramRouting where the session primitives live in scope.
378
+ let _drainLocalDeliver = null;
379
+ // Emergency-stop integration (§3.6): marks a topic stopped for the drain's
380
+ // pass/batch/chokepoint consults AND settles its custody (terminal
381
+ // operator-stop + PIS cleanup + loss report). Set with the engine.
382
+ let _inboundQueueStop = null;
369
383
  /** This machine's mesh id — lets the inbound dispatch tell a REMOTE placement
370
384
  * (forward/spawn on another machine → must NOT also dispatch locally) from a
371
385
  * self placement. Set once in startServer()'s mesh block. */
@@ -1746,17 +1760,42 @@ getAttentionTopicId) {
1746
1760
  }
1747
1761
  }
1748
1762
  if (_sessionRouter && _sessionPoolStage() !== 'dark') {
1763
+ // Ordering gate (Durable Inbound Message Queue §2.3): a live message for
1764
+ // a session with queued custody enqueues BEHIND the existing entries —
1765
+ // injecting it now would deliver out of order. Gated on a live engine.
1766
+ if (_inboundQueue) {
1767
+ try {
1768
+ if (_inboundQueue.hasQueued(String(topicId))) {
1769
+ const ord = _inboundQueue.enqueueLive({
1770
+ sessionKey: String(topicId),
1771
+ messageId: String(msg.id),
1772
+ payload: text,
1773
+ senderEnvelope: { userId: telegramUserId || undefined, firstName: pipeline.sender.firstName },
1774
+ topicMetadata: _topicPinStore?.asTopicMetadata(String(topicId)),
1775
+ }, 'ordering-behind-queued');
1776
+ if (ord.result === 'queued' || ord.result === 'already-queued') {
1777
+ console.log(`[inbound-queue] topic ${topicId} msg ${msg.id} queued behind existing entries (ordering)`);
1778
+ return;
1779
+ }
1780
+ // refused → fall through to route() — delivery beats both loss and
1781
+ // silence; the ordering violation is counted by the engine.
1782
+ }
1783
+ }
1784
+ catch { /* gate is best-effort; route() owns the message */ }
1785
+ }
1749
1786
  try {
1750
1787
  const outcome = await _sessionRouter.route({
1751
1788
  sessionKey: String(topicId),
1752
1789
  messageId: String(msg.id),
1753
1790
  payload: text,
1754
1791
  topicMetadata: _topicPinStore?.asTopicMetadata(String(topicId)),
1792
+ // §2.2: sender identity captured at ingress, persisted with custody.
1793
+ senderEnvelope: { userId: telegramUserId || undefined, firstName: pipeline.sender.firstName },
1755
1794
  });
1756
1795
  // Routing-decision observability — the live transfer path is otherwise a black
1757
1796
  // box (the recognizer logs its pin, but route()'s actual placement/forward
1758
1797
  // decision was invisible; that hid the bug below from the first live test).
1759
- console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'}`);
1798
+ console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
1760
1799
  // Short-circuit local dispatch whenever the session ended up on ANOTHER machine
1761
1800
  // (forward/duplicate, OR a fresh remote 'spawned'/'owner-dead-replaced'). Before
1762
1801
  // this, only 'forwarded'/'duplicate' were caught, so a just-moved topic was
@@ -1766,10 +1805,30 @@ getAttentionTopicId) {
1766
1805
  console.log(`[session-pool] topic ${topicId} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
1767
1806
  return;
1768
1807
  }
1769
- // 'handled-locally' / 'spawned'(self) / 'owner-dead-replaced'(self) / 'queued' /
1770
- // 'placement-blocked' fall through to the existing local dispatch below.
1808
+ // Custody-ack short-circuit (§2.2): a queued/placement-blocked verdict
1809
+ // whose enqueue COMMITTED (acked) is the queue's message now — no local
1810
+ // fall-through. Un-custodied (refused/off/dry-run) keeps today's
1811
+ // fall-through. Wiring pins assert both directions.
1812
+ if ((outcome.action === 'queued' || outcome.action === 'placement-blocked') && outcome.acked) {
1813
+ console.log(`[inbound-queue] topic ${topicId} msg ${msg.id} in durable custody (${outcome.detail ?? outcome.action}) — drain will deliver`);
1814
+ return;
1815
+ }
1816
+ // 'handled-locally' / 'spawned'(self) / 'owner-dead-replaced'(self) /
1817
+ // un-acked 'queued'/'placement-blocked' → fall through to local dispatch.
1771
1818
  }
1772
1819
  catch (err) {
1820
+ // Route-throw fail-open is CUSTODY-AWARE (§2.2): a per-MESSAGE point
1821
+ // read against the store — a committed non-terminal row for THIS
1822
+ // message means the queue owns it (skip local dispatch); no row (or
1823
+ // engine dark) → today's fall-through. A point-read ERROR fails OPEN
1824
+ // to fall-through — the bounded duplicate window, §5-enumerated.
1825
+ try {
1826
+ if (_inboundQueue?.hasCommittedRow(String(topicId), String(msg.id))) {
1827
+ console.warn(`[session-pool] route error for topic ${topicId} but custody is committed — not dispatching locally: ${err instanceof Error ? err.message : String(err)}`);
1828
+ return;
1829
+ }
1830
+ }
1831
+ catch { /* point-read error → fail open (fall through) */ }
1773
1832
  console.warn(`[session-pool] route error for topic ${topicId} — falling back to local dispatch: ${err instanceof Error ? err.message : String(err)}`);
1774
1833
  }
1775
1834
  }
@@ -1893,6 +1952,99 @@ getAttentionTopicId) {
1893
1952
  });
1894
1953
  }
1895
1954
  };
1955
+ // ── Durable Inbound Message Queue: the drain's local-delivery tail ──
1956
+ // (§3.1 via:'drain'.) Built from the SAME primitives as the live tail above
1957
+ // (injectTelegramMessage / respawnSessionForTopic / spawnSessionForTopic +
1958
+ // the spawningTopics guard) with the drain contract's divergences, each
1959
+ // enumerated in the spec: bypasses the intercept stack (a stored message is
1960
+ // DATA — this function never re-interprets commands or re-binds operators),
1961
+ // bypasses the ingress ledger (the receipt is the at-most-once authority),
1962
+ // suppresses the per-message "✓ Delivered" confirmation, AWAITS the tail
1963
+ // through the receipt write, injects with the STORED sender envelope, and
1964
+ // paces same-session runs at 1s.
1965
+ _drainLocalDeliver = async (dmsg, handover) => {
1966
+ const topicId = Number(dmsg.sessionKey);
1967
+ if (!Number.isFinite(topicId))
1968
+ return { kind: 'failed', error: new Error(`non-numeric sessionKey ${dmsg.sessionKey}`) };
1969
+ if (!_sessionRouter)
1970
+ return { kind: 'un-routable', reason: 'router-not-constructed' };
1971
+ // Routing first — ownership may have moved while the entry waited.
1972
+ const outcome = await _sessionRouter.route({
1973
+ sessionKey: dmsg.sessionKey,
1974
+ messageId: dmsg.messageId,
1975
+ payload: dmsg.payload,
1976
+ topicMetadata: dmsg.topicMetadata
1977
+ ?? _topicPinStore?.asTopicMetadata(dmsg.sessionKey),
1978
+ senderEnvelope: dmsg.senderEnvelope,
1979
+ });
1980
+ if (outcome.action === 'forwarded' && outcome.detail === 'sender-rejected') {
1981
+ return { kind: 'sender-rejected' };
1982
+ }
1983
+ if (isRemotelyHandled(outcome, _meshSelfId)) {
1984
+ return { kind: 'remote-delivered' };
1985
+ }
1986
+ if (outcome.action === 'queued' || outcome.action === 'placement-blocked') {
1987
+ // The router re-queued our own entry ('already-queued' against the
1988
+ // claimed row) — un-routable: release + backoff + attempts++ (§3.1).
1989
+ return { kind: 'un-routable', reason: outcome.detail ?? outcome.action };
1990
+ }
1991
+ // Local delivery ('handled-locally' / self 'owner-dead-replaced').
1992
+ const sender = dmsg.senderEnvelope ?? undefined;
1993
+ const senderFirstName = sender?.firstName ?? 'User';
1994
+ const senderUserId = typeof sender?.userId === 'number' ? sender.userId : Number(sender?.userId ?? 0) || 0;
1995
+ const resolvedUser = senderUserId && userManager ? userManager.resolveFromTelegramUserId(senderUserId) : null;
1996
+ const targetSession = telegram.getSessionForTopic(topicId);
1997
+ const topicName = telegram.getTopicName(topicId) || `topic-${topicId}`;
1998
+ if (targetSession && sessionManager.isSessionAlive(targetSession)) {
1999
+ // Direct inject: receipt FIRST (the §3.4 handover point), stop re-check,
2000
+ // then the inject. A caught inject error AFTER the receipt is
2001
+ // local-delivered+injectError (never silent loss).
2002
+ if (!handover.commitReceipt())
2003
+ return { kind: 'handover-refused' };
2004
+ if (handover.stopRecheck())
2005
+ return { kind: 'stopped-before-inject' };
2006
+ try {
2007
+ sessionManager.injectTelegramMessage(targetSession, topicId, dmsg.payload, topicName, senderFirstName, senderUserId, Number(dmsg.messageId.replace(/^\D*/, '')) || undefined);
2008
+ telegram.trackMessageInjection(topicId, targetSession, dmsg.payload);
2009
+ // 1s inter-inject pacing for same-session runs (§3.1, pinned round-6).
2010
+ await new Promise((r) => setTimeout(r, 1000));
2011
+ return { kind: 'local-delivered' };
2012
+ }
2013
+ catch (err) {
2014
+ return { kind: 'local-delivered', injectError: err instanceof Error ? err.message : String(err) };
2015
+ }
2016
+ }
2017
+ // Respawn / auto-spawn path. The spawn-in-progress guard maps to
2018
+ // un-routable (§3.1) — never a silent skip.
2019
+ if (spawningTopics.has(topicId)) {
2020
+ return { kind: 'un-routable', reason: 'spawn-in-progress' };
2021
+ }
2022
+ if (!handover.commitReceipt())
2023
+ return { kind: 'handover-refused' };
2024
+ if (handover.stopRecheck())
2025
+ return { kind: 'stopped-before-inject' };
2026
+ spawningTopics.add(topicId);
2027
+ try {
2028
+ if (targetSession) {
2029
+ // Dead session — respawn, AWAITED through the spawn+inject (the PIS
2030
+ // record is written inside, AFTER our receipt — the round-7 order pin).
2031
+ await respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, dmsg.payload, topicMemory, resolvedUser ?? undefined);
2032
+ }
2033
+ else {
2034
+ const newSessionName = await spawnSessionForTopic(sessionManager, telegram, topicName, topicId, dmsg.payload, topicMemory, resolvedUser ?? undefined);
2035
+ telegram.registerTopicSession(topicId, newSessionName, topicName);
2036
+ }
2037
+ return { kind: 'local-delivered' };
2038
+ }
2039
+ catch (err) {
2040
+ // Receipt already committed — the at-most-once authority forbids a
2041
+ // re-inject; honest disposition: delivered-unconfirmed + report (§3.4).
2042
+ return { kind: 'local-delivered', injectError: err instanceof Error ? err.message : String(err) };
2043
+ }
2044
+ finally {
2045
+ spawningTopics.delete(topicId);
2046
+ }
2047
+ };
1896
2048
  }
1897
2049
  /**
1898
2050
  * Wire WhatsApp message routing: incoming messages → Claude sessions.
@@ -5885,6 +6037,104 @@ export async function startServer(options) {
5885
6037
  // lifeline to restart the server in a tight loop.
5886
6038
  await sessionManager.purgeDeadSessions();
5887
6039
  sessionManager.startMonitoring();
6040
+ // ── Durable Inbound Message Queue: unconditional boot sweep (§5.3) ──
6041
+ // MUST run BEFORE recoverPendingInjects (boot ordering, spec §3.4): the
6042
+ // sweep consults injection receipts and vetoes PIS records for
6043
+ // operator-stop rows — running PIS recovery first would replay an inject
6044
+ // the operator stopped. Keyed on store-file existence; fail-open
6045
+ // (quarantine) on a corrupt store; gate-expires all custody with a NAMED
6046
+ // reason when the drain will not run this boot. The mesh-identity gate is
6047
+ // resolved later (the mesh block) — when the wiring there finds no
6048
+ // identity, it runs the same expire-all with reason no-mesh-identity
6049
+ // before any drain could have started.
6050
+ try {
6051
+ const sweepMod = await import('../core/inboundQueueBootSweep.js');
6052
+ const pisMod = await import('../core/PendingInjectStore.js');
6053
+ const sweepPis = new pisMod.PendingInjectStore(path.join(config.stateDir, 'state'));
6054
+ const pisRecordsForTopic = (sessionKey) => sweepPis.list().records.filter((r) => String(r.telegramTopicId ?? '') === sessionKey);
6055
+ const qRaw = config.multiMachine?.sessionPool?.inboundQueue;
6056
+ const poolRaw = config.multiMachine?.sessionPool;
6057
+ const queueWillRun = qRaw?.enabled !== true ? { run: false, gateReason: 'feature-disabled' }
6058
+ : qRaw?.dryRun !== false ? { run: false, gateReason: 'dry-run' }
6059
+ : (poolRaw?.enabled !== true || (poolRaw?.stage ?? 'dark') === 'dark') ? { run: false, gateReason: 'pool-dark' }
6060
+ : { run: true };
6061
+ const sweepRes = sweepMod.runInboundQueueBootSweep({
6062
+ stateDir: config.stateDir,
6063
+ agentId: config.projectName ?? 'agent',
6064
+ queueWillRun,
6065
+ hasPisRecord: (sk) => pisRecordsForTopic(sk).length > 0,
6066
+ clearPisRecord: (sk) => { for (const r of pisRecordsForTopic(sk))
6067
+ sweepPis.clear(r.tmuxSession); },
6068
+ reportLoss: (items, reason) => {
6069
+ const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
6070
+ notify('SUMMARY', 'inbound-queue', `I didn't get to ${items.length} queued message(s) (${reason}; topics: ${topics}) — resend anything still needed.`);
6071
+ },
6072
+ reportPossiblyNotInjected: (items) => {
6073
+ const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
6074
+ notify('SUMMARY', 'inbound-queue', `${items.length} message(s) may not have been injected before a crash (topics: ${topics}) — if a message went unanswered, resend it.`);
6075
+ },
6076
+ raiseAttention: (title, body) => notify('IMMEDIATE', 'inbound-queue', `${title}: ${body}`),
6077
+ log: (line) => console.log(pc.dim(` ${line}`)),
6078
+ nowMs: () => Date.now(),
6079
+ });
6080
+ _sweptInboundStore = sweepRes.store;
6081
+ if (sweepRes.storePresent) {
6082
+ console.log(pc.dim(` [inbound-queue] boot sweep: gateExpired=${sweepRes.gateExpired} recovered=${sweepRes.recoveredToQueued} ` +
6083
+ `delivered=${sweepRes.settledDelivered} pni=${sweepRes.possiblyNotInjected} pisVetoed=${sweepRes.pisVetoed} quarantined=${sweepRes.quarantined}`));
6084
+ }
6085
+ // Engine-never-constructed backstop (second-pass concern 1 — the
6086
+ // no-mesh-identity gate, made unreachable-proof): the sweep ran with
6087
+ // run:true expecting the mesh block to construct the drain. If, 90s into
6088
+ // boot, the swept store is still unadopted (no mesh identity, or the
6089
+ // construction threw), expire its custody with the NAMED reason and
6090
+ // close the handle — custody is NEVER silently stranded (§5.3), through
6091
+ // ANY non-construction path. One-shot; a no-op when the engine is live.
6092
+ if (sweepRes.store) {
6093
+ const orphanTimer = setTimeout(() => {
6094
+ if (!_sweptInboundStore || _inboundQueue)
6095
+ return;
6096
+ try {
6097
+ const store = _sweptInboundStore;
6098
+ const nowIso = new Date().toISOString();
6099
+ const rows = store.listNonTerminal();
6100
+ const dropped = [];
6101
+ for (const row of rows) {
6102
+ if (store.transition(row.enqueue_seq, row.state, 'expired', { nowIso, terminalReason: 'gate:no-mesh-identity' })) {
6103
+ dropped.push(row.session_key);
6104
+ }
6105
+ }
6106
+ if (dropped.length > 0) {
6107
+ notify('SUMMARY', 'inbound-queue', `I didn't get to ${dropped.length} queued message(s) (the queue is enabled but this machine has no mesh identity, so the drain never started; topics: ${[...new Set(dropped)].join(', ')}) — resend anything still needed.`);
6108
+ }
6109
+ store.close();
6110
+ _sweptInboundStore = null;
6111
+ console.warn('[inbound-queue] swept store never adopted by an engine — custody gate-expired (no-mesh-identity) and store closed');
6112
+ }
6113
+ catch (e) {
6114
+ console.warn(`[inbound-queue] orphan-store backstop failed (next boot's sweep retries): ${e instanceof Error ? e.message : String(e)}`);
6115
+ }
6116
+ }, 90_000);
6117
+ orphanTimer.unref?.();
6118
+ }
6119
+ }
6120
+ catch (err) {
6121
+ // The sweep is a recovery backstop — its own failure must never block
6122
+ // boot. The next boot retries; rows are durable.
6123
+ console.error(`[server] inbound-queue boot sweep failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
6124
+ }
6125
+ // Hold-for-stability effective-state getter (§4.2) — registered on the
6126
+ // UNCONDITIONAL boot path (same path as the sweep above), as a closure
6127
+ // over the effective-state computation: always-failover default ⇒
6128
+ // enabled:false, so the orphaned-config case (hold on, queue off/dark)
6129
+ // derives /guards off-runtime-divergent instead of on-unverified.
6130
+ try {
6131
+ guardRegistry.register('multiMachine.sessionPool.holdForStability.enabled', () => ({
6132
+ enabled: config.multiMachine?.sessionPool?.holdForStability?.enabled === true &&
6133
+ _inboundQueue !== null,
6134
+ lastTickAt: Date.now(),
6135
+ }));
6136
+ }
6137
+ catch { /* posture observability never blocks boot */ }
5888
6138
  // Pending-inject recovery (finding 8d300555): re-deliver initial messages
5889
6139
  // orphaned by the previous server process dying in the spawn→ready→inject
5890
6140
  // window (the auto-updater restart race). Runs AFTER the purge so dead
@@ -8649,6 +8899,13 @@ export async function startServer(options) {
8649
8899
  semanticMemory?.checkpoint();
8650
8900
  }
8651
8901
  catch { /* non-critical */ }
8902
+ // Durable Inbound Message Queue §6: sleep-shift backoff deadlines by the
8903
+ // nap span + the nap clamp (stale rows expire reported). Low wake
8904
+ // confidence → the conservative branch (clamp without shift).
8905
+ try {
8906
+ void _inboundQueue?.onWake(event.sleepDurationSeconds * 1000, event.lowConfidence ? 'low' : 'high');
8907
+ }
8908
+ catch { /* the backstop tick covers a missed wake trigger */ }
8652
8909
  // Re-validate tmux sessions
8653
8910
  try {
8654
8911
  const tmuxPath = detectTmuxPath();
@@ -9027,6 +9284,13 @@ export async function startServer(options) {
9027
9284
  }
9028
9285
  return null; // Normal messages pass through
9029
9286
  };
9287
+ // Durable Inbound Message Queue §3.6: emergency stop reaches custody.
9288
+ telegram.onSentinelStopCustody = (topicId) => {
9289
+ try {
9290
+ _inboundQueueStop?.(String(topicId));
9291
+ }
9292
+ catch { /* best-effort */ }
9293
+ };
9030
9294
  telegram.onSentinelKillSession = (sessionName) => {
9031
9295
  // Save resume UUID before killing so respawn can --resume
9032
9296
  if (_topicResumeMap) {
@@ -11615,6 +11879,28 @@ export async function startServer(options) {
11615
11879
  loadAvg: osMod.loadavg()[0],
11616
11880
  quotaState: selfQuotaState(),
11617
11881
  guardPosture: selfGuardPosture(),
11882
+ // Durable Inbound Message Queue §5.1: depth + oldest + tenure +
11883
+ // bounded top-K — the survivor's loss-SUSPECTED item, capped
11884
+ // re-placement arm, and supersede-dedupe key all read these.
11885
+ // Absent while the queue is dark — depth honestly unknown.
11886
+ ...(() => {
11887
+ try {
11888
+ if (!_inboundQueue)
11889
+ return {};
11890
+ const snap = _inboundQueue.snapshot();
11891
+ return {
11892
+ inboundQueue: {
11893
+ queueDepth: snap.counts.queued + snap.counts.claimed,
11894
+ oldestQueuedAt: snap.counts.oldestQueuedAt,
11895
+ tenure: snap.tenure,
11896
+ topK: _inboundQueue.topKSessionDepths(10),
11897
+ },
11898
+ };
11899
+ }
11900
+ catch {
11901
+ return {};
11902
+ }
11903
+ })(),
11618
11904
  });
11619
11905
  const hbApi = machineHeartbeat?.api;
11620
11906
  if (hbApi) {
@@ -11719,6 +12005,15 @@ export async function startServer(options) {
11719
12005
  // cas-pairing lint (scripts/lint-cas-emit-placement.js) fails CI on
11720
12006
  // any cas( call site missing this pairing.
11721
12007
  const emitPlacement = (sessionKey, r, reason, prevOwner) => {
12008
+ // Durable Inbound Message Queue §3.2 event trigger: an ownership
12009
+ // transition for a session with queued entries makes its head due
12010
+ // now (scoped reset + a drain pass). Fire-and-forget; the engine
12011
+ // no-ops when the session has nothing queued.
12012
+ try {
12013
+ if (r?.ok)
12014
+ void _inboundQueue?.onOwnershipTransition(sessionKey);
12015
+ }
12016
+ catch { /* trigger is best-effort; the backstop tick covers it */ }
11722
12017
  try {
11723
12018
  if (!coherenceJournal || !r?.ok)
11724
12019
  return;
@@ -11774,6 +12069,15 @@ export async function startServer(options) {
11774
12069
  const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
11775
12070
  ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
11776
12071
  recordReceipt: (messageId, session) => {
12072
+ // Durable Inbound Message Queue §3.4 remote path: the queue store's
12073
+ // remote receipt (canonical-id keyed, carries the `injected` marker
12074
+ // that makes peer-crash-between-receipt-and-inject boot-detectable —
12075
+ // loss window 6). Recorded ALONGSIDE the existing ledger receipt;
12076
+ // engine dark → the prior posture, named in the spec's skew note.
12077
+ try {
12078
+ _inboundQueue?.recordRemoteReceipt(session, messageId);
12079
+ }
12080
+ catch { /* receipt best-effort; ledger is authoritative for dedupe */ }
11777
12081
  if (messageLedger)
11778
12082
  return messageLedger.record(messageId, { platform: 'mesh', topic: session }).firstSeen;
11779
12083
  if (deliverSeenFallback.has(messageId))
@@ -11781,6 +12085,23 @@ export async function startServer(options) {
11781
12085
  deliverSeenFallback.add(messageId);
11782
12086
  return true;
11783
12087
  },
12088
+ // §3.4 sender re-validation (per-machine registries can diverge during
12089
+ // a deauthorization): a carried envelope whose userId no longer
12090
+ // resolves on THIS machine NACKs `sender-rejected`. Envelope absent
12091
+ // (old peer / live local frame) → not consulted.
12092
+ validateSender: (envelope) => {
12093
+ const uid = Number(envelope.userId);
12094
+ if (!Number.isFinite(uid) || uid === 0)
12095
+ return true;
12096
+ // THIS machine's registry (file-backed; the telegram block's
12097
+ // instance is scoped there — same files, same truth).
12098
+ try {
12099
+ return new UserManager(config.stateDir, config.users).resolveFromTelegramUserId(uid) !== null;
12100
+ }
12101
+ catch {
12102
+ return true;
12103
+ }
12104
+ },
11784
12105
  // Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
11785
12106
  // the local session for the topic so the conversation continues on THIS machine.
11786
12107
  // Only fires for a FIRST-seen forwarded deliverMessage (the ledger dedupes
@@ -11824,8 +12145,17 @@ export async function startServer(options) {
11824
12145
  if (existing && sessionManager.isSessionAlive(existing)) {
11825
12146
  if (text) {
11826
12147
  console.log(pc.green(` [session-pool] owner-side inject for forwarded topic ${topicId} → ${existing}`));
11827
- sessionManager.injectTelegramMessage(existing, topicId, text, tg.getTopicName?.(topicId) ?? undefined);
11828
- tg.trackMessageInjection(topicId, existing, text);
12148
+ // Loss window 6 (Durable Inbound Message Queue §3.4 remote):
12149
+ // flip the receipt's injected marker on success; a CAUGHT
12150
+ // failure reports at error time — never silent.
12151
+ try {
12152
+ sessionManager.injectTelegramMessage(existing, topicId, text, tg.getTopicName?.(topicId) ?? undefined);
12153
+ tg.trackMessageInjection(topicId, existing, text);
12154
+ _inboundQueue?.markRemoteInjected(cmd.session, cmd.messageId);
12155
+ }
12156
+ catch (err) {
12157
+ _inboundQueue?.reportPeerInjectError(cmd.session, cmd.messageId, err instanceof Error ? err.message : String(err));
12158
+ }
11829
12159
  }
11830
12160
  return;
11831
12161
  }
@@ -11859,8 +12189,13 @@ export async function startServer(options) {
11859
12189
  .then((name) => {
11860
12190
  tg.registerTopicSession(topicId, name, spawnName);
11861
12191
  console.log(pc.green(` [session-pool] owner-side resume for forwarded topic ${topicId} → ${name}`));
12192
+ // Loss window 6: the forwarded message reached a real session.
12193
+ _inboundQueue?.markRemoteInjected(cmd.session, cmd.messageId);
11862
12194
  })
11863
- .catch((err) => console.warn(` [session-pool] owner-side resume failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`));
12195
+ .catch((err) => {
12196
+ console.warn(` [session-pool] owner-side resume failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`);
12197
+ _inboundQueue?.reportPeerInjectError(cmd.session, cmd.messageId, err instanceof Error ? err.message : String(err));
12198
+ });
11864
12199
  },
11865
12200
  });
11866
12201
  // ── Secret-sync inbound handler (cross-machine secret distribution, spec Phase 4) ──
@@ -12688,6 +13023,15 @@ export async function startServer(options) {
12688
13023
  // signal per episode.
12689
13024
  const ownerSuspectBreaker = new (await import('../core/OwnerSuspectBreaker.js')).OwnerSuspectBreaker({
12690
13025
  logger: (m) => console.log(pc.dim(m)),
13026
+ // Durable Inbound Message Queue §3.2: breaker close delivers held
13027
+ // rows instantly. Engine constructed later in this block — the
13028
+ // closure reads at fire time.
13029
+ onClose: () => { void _inboundQueue?.onBreakerClose(); },
13030
+ flapThresholdPerHour: config.multiMachine?.sessionPool?.holdForStability?.flapThresholdPerHour ?? 6,
13031
+ reportFlapEpisode: ({ machineId, episodesLastHour }) => {
13032
+ const nickname = machinePoolRegistry?.getCapacity(machineId)?.nickname ?? machineId;
13033
+ notify('SUMMARY', 'inbound-queue', `"${nickname}" is flapping (${episodesLastHour} suspect episodes in the last hour) — holds are disabled for it until it calms; its conversations move on the usual failover path.`);
13034
+ },
12691
13035
  reportSustained: ({ machineId, suspectForMs, marks }) => {
12692
13036
  DegradationReporter.getInstance().report({
12693
13037
  feature: 'SessionPool.ownerDelivery',
@@ -12753,7 +13097,10 @@ export async function startServer(options) {
12753
13097
  const url = peerUrl(target);
12754
13098
  if (!url)
12755
13099
  throw new Error(`no peer url for ${target}`);
12756
- const res = await meshClient.send({ machineId: target, url }, { type: 'deliverMessage', session: env.sessionKey, messageId: env.messageId, payload: env.payload, ownershipEpoch: env.ownershipEpoch }, env.ownershipEpoch);
13100
+ // senderEnvelope (Durable Inbound Message Queue §2.2): a drained
13101
+ // forward carries the STORED sender frame; an old peer ignores
13102
+ // the extra field (version skew named in the spec).
13103
+ const res = await meshClient.send({ machineId: target, url }, { type: 'deliverMessage', session: env.sessionKey, messageId: env.messageId, payload: env.payload, ownershipEpoch: env.ownershipEpoch, ...(env.senderEnvelope ? { senderEnvelope: env.senderEnvelope } : {}) }, env.ownershipEpoch);
12757
13104
  if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
12758
13105
  return res.result;
12759
13106
  }
@@ -12766,11 +13113,192 @@ export async function startServer(options) {
12766
13113
  await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: 0 }, 0);
12767
13114
  },
12768
13115
  handleLocally: async () => { },
12769
- queueMessage: () => { },
13116
+ // Durable Inbound Message Queue §2.2: tri-state custody taking.
13117
+ // Null engine (dark / gate failed / invariants violated) → 'refused'
13118
+ // → the router leaves acked:false → today's fall-through. enqueueLive
13119
+ // never throws (storage failure maps to 'refused' — fail-safe).
13120
+ queueMessage: (msg, reason) => {
13121
+ if (!_inboundQueue)
13122
+ return 'refused';
13123
+ const out = _inboundQueue.enqueueLive({
13124
+ sessionKey: msg.sessionKey,
13125
+ messageId: msg.messageId,
13126
+ payload: typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload ?? ''),
13127
+ senderEnvelope: msg.senderEnvelope ?? null,
13128
+ topicMetadata: msg.topicMetadata,
13129
+ }, reason);
13130
+ return out.result;
13131
+ },
12770
13132
  raiseAttention: (title, body) => console.log(pc.dim(` [session-router] attention: ${title} — ${body}`)),
12771
13133
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
12772
13134
  log: (line) => console.log(pc.dim(` [session-router] ${line}`)),
12773
13135
  });
13136
+ // ── Durable Inbound Message Queue: engine construction (gated) ──
13137
+ // Gate: pool active (this block) + mesh identity (this block) +
13138
+ // inboundQueue.enabled + !dryRun + the six config-seam invariants.
13139
+ // A violated invariant keeps the queue OFF for the boot — one loud
13140
+ // config-error per violated inequality + one attention item, never a
13141
+ // half-configured queue (spec §Config).
13142
+ try {
13143
+ const iqcMod = await import('../core/inboundQueueConfig.js');
13144
+ const qcfg = { ...iqcMod.DEFAULT_INBOUND_QUEUE_CONFIG, ...(config.multiMachine?.sessionPool?.inboundQueue ?? {}) };
13145
+ const hcfg = { ...iqcMod.DEFAULT_HOLD_FOR_STABILITY_CONFIG, ...(config.multiMachine?.sessionPool?.holdForStability ?? {}) };
13146
+ // Dry-run constructs the engine too (second-pass concern 2): the
13147
+ // §2.4 dry-run branch never takes custody, but its durable
13148
+ // wouldEnqueue/wouldHold/wouldRefuse counters ARE the promotion
13149
+ // evidence and /pool/queue must serve them. The boot sweep already
13150
+ // gate-expired any live→dry-run residual rows.
13151
+ if (qcfg.enabled && _sessionPoolStage() !== 'dark') {
13152
+ const inv = iqcMod.validateInboundQueueInvariants(qcfg, hcfg);
13153
+ if (!inv.ok) {
13154
+ for (const v of inv.violations) {
13155
+ console.error(`[inbound-queue] CONFIG ERROR — invariant ${v.invariant} (${v.name}): ${v.message}`);
13156
+ }
13157
+ notify('IMMEDIATE', 'inbound-queue', `Inbound queue NOT started — ${inv.violations.length} config invariant(s) violated (${inv.violations.map((v) => v.name).join(', ')}). The queue stays OFF; messages use today's delivery path.`);
13158
+ _sweptInboundStore?.close();
13159
+ _sweptInboundStore = null;
13160
+ }
13161
+ else {
13162
+ const qdlMod = await import('../core/QueueDrainLoop.js');
13163
+ const pisStoreMod = await import('../core/PendingInboundStore.js');
13164
+ const pisMod2 = await import('../core/PendingInjectStore.js');
13165
+ const queuePis = new pisMod2.PendingInjectStore(path.join(config.stateDir, 'state'));
13166
+ const store = _sweptInboundStore ?? pisStoreMod.PendingInboundStore.open(config.projectName ?? 'agent', config.stateDir);
13167
+ const bootSessionId = `${meshSelfId}:${Date.now()}`;
13168
+ // §4.2 hold verdict — effective state honesty: always-'failover'
13169
+ // when the policy is off or the queue is dry-run (unreachable
13170
+ // here: dryRun gates construction) — config-coupled both ways.
13171
+ const holdOn = hcfg.enabled === true;
13172
+ const holdVerdict = (sessionKey) => {
13173
+ if (!holdOn)
13174
+ return 'failover';
13175
+ const own = ownReg.read(sessionKey);
13176
+ const owner = own ? ownReg.ownerOf(sessionKey) : null;
13177
+ if (!owner || owner === meshSelfId)
13178
+ return 'deliver';
13179
+ const cap = machinePoolRegistry?.getCapacity(owner);
13180
+ if (!cap?.online)
13181
+ return 'failover'; // heartbeat offline — dead is dead
13182
+ if (ownerSuspectBreaker.isFlapping(owner))
13183
+ return 'failover'; // §4.4
13184
+ if (ownerSuspectBreaker.isSuspect(owner))
13185
+ return 'hold'; // suspect + online
13186
+ return 'deliver'; // not suspect (exhaustion site) — enqueue-and-drain
13187
+ };
13188
+ const stoppedTopics = new Set();
13189
+ _inboundQueue = new qdlMod.QueueDrainLoop({
13190
+ store,
13191
+ qcfg,
13192
+ hcfg,
13193
+ selfMachineId: meshSelfId,
13194
+ // The real serving-lease signal (second-pass concern 3): when
13195
+ // a lease coordinator exists, custody is gated on actually
13196
+ // HOLDING the lease (§2.2 — custody only where it can be
13197
+ // drained); a single-machine install with no coordinator
13198
+ // defaults true (the single-router topology).
13199
+ holdsLease: () => (leaseCoordinatorRef ? leaseCoordinatorRef.holdsLease() : true),
13200
+ isStopped: (sk) => stoppedTopics.has(sk),
13201
+ dispatchInbound: async (msg, handover) => {
13202
+ if (!_drainLocalDeliver)
13203
+ return { kind: 'un-routable', reason: 'local-tail-not-wired' };
13204
+ return _drainLocalDeliver(msg, handover);
13205
+ },
13206
+ forceReplace: async (msg) => {
13207
+ if (!_sessionRouter)
13208
+ return false;
13209
+ return _sessionRouter.forceReplace({
13210
+ sessionKey: msg.sessionKey,
13211
+ messageId: msg.messageId,
13212
+ payload: msg.payload,
13213
+ senderEnvelope: msg.senderEnvelope,
13214
+ topicMetadata: msg.topicMetadata,
13215
+ });
13216
+ },
13217
+ holdVerdict,
13218
+ clearPisRecord: (sk) => {
13219
+ for (const r of queuePis.list().records.filter((x) => String(x.telegramTopicId ?? '') === sk)) {
13220
+ queuePis.clear(r.tmuxSession);
13221
+ }
13222
+ },
13223
+ reportLoss: (items, reason) => {
13224
+ const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
13225
+ notify('SUMMARY', 'inbound-queue', `I didn't get to ${items.length} queued message(s) (${reason}; topics: ${topics}) — resend anything still needed.`);
13226
+ },
13227
+ reportPossiblyNotInjected: (items) => {
13228
+ const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
13229
+ notify('SUMMARY', 'inbound-queue', `${items.length} message(s) may not have been injected (topics: ${topics}) — if a message went unanswered, resend it.`);
13230
+ },
13231
+ log: (line) => console.log(pc.dim(` ${line}`)),
13232
+ reportDegradation: (reason) => {
13233
+ DegradationReporter.getInstance().report({
13234
+ feature: 'InboundQueue.drain',
13235
+ primary: 'Durable inbound custody drain',
13236
+ fallback: 'Rows remain durable; the Eternal-Sentinel tick keeps retrying',
13237
+ reason,
13238
+ impact: 'Queued inbound messages may deliver late until the tick recovers.',
13239
+ });
13240
+ },
13241
+ now: () => Date.now(),
13242
+ mono: () => performance.now(),
13243
+ bootSessionId,
13244
+ });
13245
+ _inboundQueue.onLeaseAcquired(null);
13246
+ // Expose the stop hook for the emergency-stop integration and
13247
+ // the engine for the /pool/queue route via module state.
13248
+ _inboundQueueStop = (sk) => { stoppedTopics.add(sk); _inboundQueue?.onOperatorStop(sk); };
13249
+ // Backstop tick (Eternal Sentinel — declared in QueueDrainLoop).
13250
+ const tickHandle = setInterval(() => { void _inboundQueue?.tick(); }, qcfg.drainTickMs);
13251
+ tickHandle.unref?.();
13252
+ console.log(pc.dim(` [inbound-queue] engine live — tick every ${qcfg.drainTickMs}ms, tenure ${_inboundQueue.currentTenure()}`));
13253
+ // Survivor arm (spec §5.1, loss window 1): the machine that
13254
+ // holds the routing role checks — once heartbeats have had a
13255
+ // cycle to repopulate — for OFFLINE peers whose last capacity
13256
+ // heartbeat reported nonzero queue depth. ONE loss-SUSPECTED
13257
+ // item per (machine + tenure) episode, then capped synthetic
13258
+ // re-placement for the top-K sessions THROUGH the router path
13259
+ // (PlacementExecutor honors pins; CAS; spawn on the chosen
13260
+ // machine) — SESSION RECOVERY WITHOUT MESSAGE REPLAY, framed
13261
+ // exactly so in the copy. A mesh-less/old peer carries no
13262
+ // depth field — honestly unknown, no item.
13263
+ const survivorEpisodesSeen = new Set();
13264
+ const survivorCheck = () => {
13265
+ try {
13266
+ for (const cap of machinePoolRegistry?.getCapacities() ?? []) {
13267
+ const iq = cap.inboundQueue;
13268
+ if (cap.machineId === meshSelfId || cap.online || !iq || iq.queueDepth <= 0)
13269
+ continue;
13270
+ const episodeKey = `${cap.machineId}|${iq.tenure ?? 'unknown'}`;
13271
+ if (survivorEpisodesSeen.has(episodeKey))
13272
+ continue;
13273
+ survivorEpisodesSeen.add(episodeKey);
13274
+ notify('IMMEDIATE', 'inbound-queue', `"${cap.nickname ?? cap.machineId}" went dark holding ~${iq.queueDepth} queued message(s) — SUSPECTED loss (it may have delivered some before going dark; last heartbeat ${cap.selfReportedLastSeen ?? 'unknown'}). I'm re-opening its top conversations on a healthy machine — session recovery WITHOUT message replay: the queued messages themselves are not recovered, so resend anything still needed.`);
13275
+ const respawnCap = Math.min(qcfg.maxFailoverRespawns, iq.topK.length);
13276
+ for (let i = 0; i < respawnCap; i++) {
13277
+ const sk = iq.topK[i].sessionKey;
13278
+ setTimeout(() => {
13279
+ void _sessionRouter?.forceReplace({ sessionKey: sk, messageId: `survivor-replace:${episodeKey}:${sk}`, payload: '', senderEnvelope: null })
13280
+ .catch(() => { });
13281
+ }, 2000 * i).unref?.(); // staggered
13282
+ }
13283
+ }
13284
+ }
13285
+ catch { /* survivor check is best-effort observability+recovery */ }
13286
+ };
13287
+ const survivorTimer = setInterval(survivorCheck, 60_000);
13288
+ survivorTimer.unref?.();
13289
+ }
13290
+ }
13291
+ else if (_sweptInboundStore) {
13292
+ // Sweep opened the store expecting a live engine but the stage/
13293
+ // dry-run gate says otherwise (mid-boot config nuance) — close it;
13294
+ // the sweep already settled rows per its own gate verdict.
13295
+ _sweptInboundStore.close();
13296
+ _sweptInboundStore = null;
13297
+ }
13298
+ }
13299
+ catch (err) {
13300
+ console.error(`[inbound-queue] engine construction failed (queue stays OFF): ${err instanceof Error ? err.message : String(err)}`);
13301
+ }
12774
13302
  // ── B (HTTP presence transport): pull each peer's self-capacity over
12775
13303
  // the signed /mesh/rpc channel and record it into the pool registry.
12776
13304
  // This is the credential-less presence path — a standby that cannot
@@ -13164,7 +13692,7 @@ export async function startServer(options) {
13164
13692
  carrier: _topicProfileCarrier,
13165
13693
  }
13166
13694
  : null;
13167
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
13695
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, getInboundQueue: () => _inboundQueue, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
13168
13696
  // Resolve the late-bound topic-operator getter (increment 2e): routing was
13169
13697
  // wired before the server existed; from here on inbound binds use the
13170
13698
  // server's own store instance.