instar 1.3.42 → 1.3.44

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 (111) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +292 -3
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/HandoffReceiver.d.ts +56 -0
  5. package/dist/core/HandoffReceiver.d.ts.map +1 -0
  6. package/dist/core/HandoffReceiver.js +103 -0
  7. package/dist/core/HandoffReceiver.js.map +1 -0
  8. package/dist/core/HandoffWireTransport.d.ts +61 -0
  9. package/dist/core/HandoffWireTransport.d.ts.map +1 -0
  10. package/dist/core/HandoffWireTransport.js +114 -0
  11. package/dist/core/HandoffWireTransport.js.map +1 -0
  12. package/dist/core/HttpLeaseTransport.d.ts +68 -0
  13. package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
  14. package/dist/core/HttpLeaseTransport.js +97 -0
  15. package/dist/core/HttpLeaseTransport.js.map +1 -0
  16. package/dist/core/HttpLiveTailTransport.d.ts +88 -0
  17. package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
  18. package/dist/core/HttpLiveTailTransport.js +95 -0
  19. package/dist/core/HttpLiveTailTransport.js.map +1 -0
  20. package/dist/core/LeaseCoordinator.d.ts +14 -0
  21. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  22. package/dist/core/LeaseCoordinator.js +38 -0
  23. package/dist/core/LeaseCoordinator.js.map +1 -1
  24. package/dist/core/LiveTailSource.d.ts +65 -0
  25. package/dist/core/LiveTailSource.d.ts.map +1 -0
  26. package/dist/core/LiveTailSource.js +87 -0
  27. package/dist/core/LiveTailSource.js.map +1 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts +14 -0
  29. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  30. package/dist/core/MultiMachineCoordinator.js +22 -0
  31. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  32. package/dist/core/ReplyMarkerTransport.d.ts +58 -0
  33. package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
  34. package/dist/core/ReplyMarkerTransport.js +66 -0
  35. package/dist/core/ReplyMarkerTransport.js.map +1 -0
  36. package/dist/core/SafeGitExecutor.d.ts.map +1 -1
  37. package/dist/core/SafeGitExecutor.js +21 -2
  38. package/dist/core/SafeGitExecutor.js.map +1 -1
  39. package/dist/core/featureRolloutScan.d.ts.map +1 -1
  40. package/dist/core/featureRolloutScan.js +5 -4
  41. package/dist/core/featureRolloutScan.js.map +1 -1
  42. package/dist/core/handoffReceiverWiring.d.ts +57 -0
  43. package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
  44. package/dist/core/handoffReceiverWiring.js +65 -0
  45. package/dist/core/handoffReceiverWiring.js.map +1 -0
  46. package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
  47. package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
  48. package/dist/core/handoffSentinelBootWiring.js +52 -0
  49. package/dist/core/handoffSentinelBootWiring.js.map +1 -0
  50. package/dist/core/handoffSentinelWiring.d.ts +60 -0
  51. package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
  52. package/dist/core/handoffSentinelWiring.js +59 -0
  53. package/dist/core/handoffSentinelWiring.js.map +1 -0
  54. package/dist/core/seamlessnessConfig.d.ts +8 -0
  55. package/dist/core/seamlessnessConfig.d.ts.map +1 -1
  56. package/dist/core/seamlessnessConfig.js +1 -0
  57. package/dist/core/seamlessnessConfig.js.map +1 -1
  58. package/dist/core/types.d.ts +6 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js.map +1 -1
  61. package/dist/messaging/TelegramAdapter.d.ts +6 -0
  62. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  63. package/dist/messaging/TelegramAdapter.js +8 -0
  64. package/dist/messaging/TelegramAdapter.js.map +1 -1
  65. package/dist/messaging/ingressDedup.d.ts +55 -0
  66. package/dist/messaging/ingressDedup.d.ts.map +1 -0
  67. package/dist/messaging/ingressDedup.js +69 -0
  68. package/dist/messaging/ingressDedup.js.map +1 -0
  69. package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
  70. package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
  71. package/dist/messaging/stuckMessageRecovery.js +52 -0
  72. package/dist/messaging/stuckMessageRecovery.js.map +1 -0
  73. package/dist/monitoring/releaseReadinessWiring.d.ts.map +1 -1
  74. package/dist/monitoring/releaseReadinessWiring.js +4 -2
  75. package/dist/monitoring/releaseReadinessWiring.js.map +1 -1
  76. package/dist/server/AgentServer.d.ts +64 -0
  77. package/dist/server/AgentServer.d.ts.map +1 -1
  78. package/dist/server/AgentServer.js +24 -0
  79. package/dist/server/AgentServer.js.map +1 -1
  80. package/dist/server/handoffInitiateRoutes.d.ts +40 -0
  81. package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
  82. package/dist/server/handoffInitiateRoutes.js +57 -0
  83. package/dist/server/handoffInitiateRoutes.js.map +1 -0
  84. package/dist/server/machineRoutes.d.ts +47 -0
  85. package/dist/server/machineRoutes.d.ts.map +1 -1
  86. package/dist/server/machineRoutes.js +138 -0
  87. package/dist/server/machineRoutes.js.map +1 -1
  88. package/dist/server/routes.d.ts +20 -0
  89. package/dist/server/routes.d.ts.map +1 -1
  90. package/dist/server/routes.js +65 -0
  91. package/dist/server/routes.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/data/builtin-manifest.json +49 -49
  94. package/upgrades/1.3.41.md +9 -5
  95. package/upgrades/1.3.43.md +21 -0
  96. package/upgrades/1.3.44.md +72 -0
  97. package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
  98. package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
  99. package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
  100. package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
  101. package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
  102. package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
  103. package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
  104. package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
  105. package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
  106. package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
  107. package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
  108. package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
  109. package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
  110. package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
  111. package/upgrades/side-effects/source-tree-guard-readsync-bypass.md +34 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkRH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAg6MtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA+RH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyrNtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -64,6 +64,18 @@ import { assertSeamlessnessInvariants } from '../core/seamlessnessConfig.js';
64
64
  import { FencedLease } from '../core/FencedLease.js';
65
65
  import { GitLeaseStore } from '../core/GitLeaseStore.js';
66
66
  import { LeaseCoordinator } from '../core/LeaseCoordinator.js';
67
+ import { HttpLeaseTransport } from '../core/HttpLeaseTransport.js';
68
+ import { HttpLiveTailTransport } from '../core/HttpLiveTailTransport.js';
69
+ import { LiveTailBuffer } from '../core/LiveTailBuffer.js';
70
+ import { LiveTailSource } from '../core/LiveTailSource.js';
71
+ import { HandoffWireTransport } from '../core/HandoffWireTransport.js';
72
+ import { createHandoffReceiverWiring } from '../core/handoffReceiverWiring.js';
73
+ import { createHandoffSentinelBootWiring } from '../core/handoffSentinelBootWiring.js';
74
+ import { MessageProcessingLedger } from '../messaging/MessageProcessingLedger.js';
75
+ import { recoverStuckMessages } from '../messaging/stuckMessageRecovery.js';
76
+ import { ReplyMarkerTransport } from '../core/ReplyMarkerTransport.js';
77
+ import { decryptFromSync, encryptForSync } from '../core/SecretStore.js';
78
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
67
79
  import { sign as signEd25519, verify as verifyEd25519 } from '../core/MachineIdentity.js';
68
80
  import { ProjectMapper } from '../core/ProjectMapper.js';
69
81
  import { CapabilityMapper } from '../core/CapabilityMapper.js';
@@ -2155,6 +2167,12 @@ export async function startServer(options) {
2155
2167
  // Standalone agents don't have git repos unless the user opted into cloud backup.
2156
2168
  let gitSync;
2157
2169
  let registrySyncDebouncer;
2170
+ let leaseTransport;
2171
+ let liveTailBuffer;
2172
+ let liveTailSendTransport;
2173
+ let handoffWireTransport;
2174
+ let replyMarkerTransport;
2175
+ let liveTailReceiver;
2158
2176
  const isGitRepo = fs.existsSync(path.join(config.projectDir, '.git'));
2159
2177
  const gitBackupEnabled = config.gitBackup?.enabled !== false;
2160
2178
  // Construct gitSync for BOTH roles when this is a git-backed mesh machine:
@@ -2234,11 +2252,29 @@ export async function startServer(options) {
2234
2252
  commitAndPush: (msg, paths) => gitSyncRef.commitAndPush(msg, paths),
2235
2253
  logger: (m) => console.log(pc.dim(m)),
2236
2254
  });
2255
+ // Lease wire transport (spec §6) — the low-latency authoritative copy
2256
+ // travels over the existing authenticated machine channel. For a
2257
+ // single-machine mesh (no peers) broadcast is a no-op and isReachable()
2258
+ // stays true, so the lease behaves exactly as git-only. Multi-machine
2259
+ // meshes get RTT-bounded acquisition + the renewal-requires-medium rule.
2260
+ let leaseSeq = Date.now();
2261
+ leaseTransport = new HttpLeaseTransport({
2262
+ selfMachineId,
2263
+ signingKeyPem: idMgr.loadSigningKey(),
2264
+ peers: () => {
2265
+ const reg = idMgr.loadRegistry();
2266
+ return Object.entries(reg.machines ?? {})
2267
+ .filter(([id, e]) => id !== selfMachineId && !!e.lastKnownUrl && !e.revokedAt)
2268
+ .map(([id, e]) => ({ machineId: id, url: e.lastKnownUrl }));
2269
+ },
2270
+ nextSequence: () => ++leaseSeq,
2271
+ reachabilityWindowMs: seamlessness.leaseTtlMs,
2272
+ logger: (m) => console.log(pc.dim(m)),
2273
+ });
2237
2274
  const leaseCoordinator = new LeaseCoordinator({
2238
2275
  lease: fencedLease,
2239
2276
  store: leaseStore,
2240
- // tunnel accelerator omitted for the launch increment (git-only is
2241
- // correct; tunnel transport tracked for the follow-on).
2277
+ tunnel: leaseTransport,
2242
2278
  presumedDeadHolders: () => {
2243
2279
  const reg = idMgr.loadRegistry();
2244
2280
  const nowMs = Date.now();
@@ -2260,6 +2296,109 @@ export async function startServer(options) {
2260
2296
  coordinator.attachLeaseCoordinator(leaseCoordinator);
2261
2297
  await coordinator.initializeLease();
2262
2298
  console.log(pc.dim(` Fenced lease active (epoch ${leaseCoordinator.currentEpoch()}, holder=${leaseCoordinator.currentHolder() ?? 'none'})`));
2299
+ // ── Handoff ack/yield wire (spec §8 G3d/G3e) ───────────────
2300
+ // The point-to-point channel the two machines use to negotiate a
2301
+ // verified, lease-safe planned handoff. The /api/handoff/ack route
2302
+ // delivers the incoming machine's caught-up echo via recordAck (resolves
2303
+ // the outgoing's awaitAck); /api/handoff/yield delivers the explicit
2304
+ // yield via recordYield (fires the incoming's registered handler → lease
2305
+ // CAS). A handoff is strictly 1:1, so peer() resolves the single
2306
+ // reachable counterpart. Solo agent (no peer) → sends are reachable
2307
+ // no-ops, so a single-machine mesh behaves exactly as before.
2308
+ let handoffSeq = Date.now();
2309
+ handoffWireTransport = new HandoffWireTransport({
2310
+ selfMachineId,
2311
+ signingKeyPem: idMgr.loadSigningKey(),
2312
+ peer: () => {
2313
+ const reg = idMgr.loadRegistry();
2314
+ for (const [id, e] of Object.entries(reg.machines ?? {})) {
2315
+ if (id === selfMachineId || !e.lastKnownUrl || e.revokedAt)
2316
+ continue;
2317
+ return { machineId: id, url: e.lastKnownUrl };
2318
+ }
2319
+ return null;
2320
+ },
2321
+ nextSequence: () => ++handoffSeq,
2322
+ logger: (m) => console.log(pc.dim(m)),
2323
+ });
2324
+ // Cross-machine reply-marker propagation (spec §8 G3a) — only when the
2325
+ // exactly-once ledger is active. Broadcasts "this event was answered" to
2326
+ // standby peers so a post-handoff redelivery is deduped on the new holder.
2327
+ if (seamlessness.exactlyOnceIngress) {
2328
+ let markerSeq = Date.now();
2329
+ replyMarkerTransport = new ReplyMarkerTransport({
2330
+ selfMachineId,
2331
+ signingKeyPem: idMgr.loadSigningKey(),
2332
+ peers: () => {
2333
+ const reg = idMgr.loadRegistry();
2334
+ const out = [];
2335
+ for (const [id, e] of Object.entries(reg.machines ?? {})) {
2336
+ if (id === selfMachineId || !e.lastKnownUrl || e.revokedAt)
2337
+ continue;
2338
+ out.push({ machineId: id, url: e.lastKnownUrl });
2339
+ }
2340
+ return out;
2341
+ },
2342
+ nextSequence: () => ++markerSeq,
2343
+ logger: (m) => console.log(pc.dim(m)),
2344
+ });
2345
+ }
2346
+ console.log(pc.dim(' Handoff ack/yield wire active (ack→recordAck, yield→recordYield)'));
2347
+ // ── Live-tail RECEIVER (spec §8 G3b/c) ─────────────────────
2348
+ // The standby receives the holder's redacted+encrypted live-tail flushes
2349
+ // at /api/live-tail, decrypts them with THIS machine's X25519 private key,
2350
+ // and sequence-dedups them into a persisted buffer so a failover resumes
2351
+ // from a durable (not merely in-memory) copy. The HOLDER-side sender
2352
+ // (HttpLiveTailTransport.broadcast) is driven by the flush producer wired
2353
+ // in the inbound-dispatch + handoff integration (next increment piece).
2354
+ // Solo agent (no peers ever POST here) → the receiver simply never fires.
2355
+ if (seamlessness.liveTailTransport === 'tunnel') {
2356
+ liveTailBuffer = new LiveTailBuffer({
2357
+ outOfOrderTimeoutMs: seamlessness.liveTailOutOfOrderTimeoutMs,
2358
+ maxBytesPerTopic: seamlessness.liveTailMaxBytesPerTopic,
2359
+ logger: (m) => console.log(pc.dim(m)),
2360
+ });
2361
+ const liveTailBufferRef = liveTailBuffer;
2362
+ // Decrypt with THIS machine's X25519 private key, then apply
2363
+ // (sequence-deduped). Throws on a bad payload → the route returns 400.
2364
+ const ownEncryptionKey = createPrivateKey(idMgr.loadEncryptionKey());
2365
+ liveTailReceiver = (flush) => {
2366
+ const decrypted = decryptFromSync(flush.enc, ownEncryptionKey);
2367
+ const content = typeof decrypted.content === 'string' ? decrypted.content : '';
2368
+ return liveTailBufferRef.applyFlush({ topic: flush.topic, seq: flush.seq, content });
2369
+ };
2370
+ console.log(pc.dim(' Live-tail receiver active (standby decrypts + sequence-dedups holder stream)'));
2371
+ // Holder-side SENDER transport (the LiveTailSource that drives it is
2372
+ // constructed after the Telegram adapter is up — it needs the content
2373
+ // provider). Built here because it needs idMgr (signing key + peer
2374
+ // registry). No peers → broadcast is a reachable no-op.
2375
+ let liveTailWireSeq = Date.now();
2376
+ liveTailSendTransport = new HttpLiveTailTransport({
2377
+ selfMachineId,
2378
+ signingKeyPem: idMgr.loadSigningKey(),
2379
+ peers: () => {
2380
+ const reg = idMgr.loadRegistry();
2381
+ const out = [];
2382
+ for (const [id, e] of Object.entries(reg.machines ?? {})) {
2383
+ if (id === selfMachineId || !e.lastKnownUrl || e.revokedAt)
2384
+ continue;
2385
+ const pem = idMgr.getEncryptionPublicKeyPem(id);
2386
+ if (!pem)
2387
+ continue; // can't encrypt for a peer whose X25519 key we lack
2388
+ try {
2389
+ const encB64 = createPublicKey(pem).export({ type: 'spki', format: 'der' }).toString('base64');
2390
+ out.push({ machineId: id, url: e.lastKnownUrl, encryptionPublicKey: encB64 });
2391
+ }
2392
+ catch { /* skip a peer with an unusable key */ }
2393
+ }
2394
+ return out;
2395
+ },
2396
+ nextSequence: () => ++liveTailWireSeq,
2397
+ encryptFor: (content, recipientEncPubB64) => encryptForSync({ content }, recipientEncPubB64),
2398
+ reachabilityWindowMs: seamlessness.leaseTtlMs,
2399
+ logger: (m) => console.log(pc.dim(m)),
2400
+ });
2401
+ }
2263
2402
  }
2264
2403
  catch (err) {
2265
2404
  // @silent-fallback-ok — git sync disabled gracefully
@@ -7558,6 +7697,113 @@ export async function startServer(options) {
7558
7697
  ? ' SessionReaper enabled (idle-session reaper — LIVE)'
7559
7698
  : ' SessionReaper enabled (idle-session reaper — dry-run, logs only)'));
7560
7699
  }
7700
+ // ── Live-tail STREAMING (spec §8 G3b) ──────────────────────────
7701
+ // The holder pushes the live conversation tail to the standby on a cadence,
7702
+ // keeping the standby's persisted copy fresh (RPO = liveTailMaxStalenessMs).
7703
+ // Gated on holding the lease, so only the awake machine streams. Solo agent
7704
+ // (no peers) → the transport's broadcast is a reachable no-op. The sender
7705
+ // transport was built in the lease block; the source + cadence are wired here
7706
+ // because the content provider needs the Telegram adapter.
7707
+ //
7708
+ // The outgoing-side planned-handoff trigger (spec §8 G3e) is assigned INSIDE
7709
+ // this block too: the sentinel must drive liveTailSource.pushTick to make the
7710
+ // standby current before it flushes the manifest, so liveTailSource has to be
7711
+ // in scope. Declared out here so the AgentServer mount (POST /handoff/initiate)
7712
+ // can read it; undefined on a solo agent / multi-machine off.
7713
+ let handoffInitiate;
7714
+ let handoffSentinelInProgress;
7715
+ if (liveTailSendTransport && telegram && coordinator.enabled) {
7716
+ const sendTransport = liveTailSendTransport;
7717
+ const liveTailSource = new LiveTailSource({
7718
+ // Full current tail for a topic — recent history formatted append-only.
7719
+ // (A window shift past the limit triggers a one-off full resend, which the
7720
+ // standby buffer dedups by seq + caps by bytes — correct, just occasional.)
7721
+ getTopicContent: (topic) => {
7722
+ const entries = telegram.getTopicHistory(Number(topic), 500);
7723
+ return entries.map((e) => `[${e.timestamp}] ${e.text}`).join('\n') + (entries.length ? '\n' : '');
7724
+ },
7725
+ activeTopics: () => telegram.getKnownTopicIds().map((id) => String(id)),
7726
+ transport: sendTransport,
7727
+ logger: (m) => console.log(pc.dim(m)),
7728
+ });
7729
+ const liveTailTimer = setInterval(() => {
7730
+ // Only the lease holder streams (mirrors the scheduler/sentinel gating).
7731
+ if (!coordinator.holdsLease())
7732
+ return;
7733
+ liveTailSource.pushTick().catch((err) => {
7734
+ console.error(`[live-tail] push tick failed: ${err instanceof Error ? err.message : err}`);
7735
+ });
7736
+ }, seamlessness.liveTailPushRateMs);
7737
+ if (liveTailTimer.unref)
7738
+ liveTailTimer.unref();
7739
+ console.log(pc.dim(` Live-tail streaming active (holder pushes every ${seamlessness.liveTailPushRateMs}ms when peers present)`));
7740
+ // ── Outgoing-side planned-handoff sentinel (spec §8 G3e) ──
7741
+ // The conductor: flush the live tail → POST the begin manifest → await +
7742
+ // VERIFY the incoming's caught-up echo → validate → yield → demote. The
7743
+ // CRITICAL invariant lives in HandoffSentinel: it NEVER yields the lease
7744
+ // unless the echo verifies AND validation passes — on any mismatch/timeout
7745
+ // it aborts and stays awake (no two-holders window). This block only binds
7746
+ // the ops to the live components. Additionally gated on handoffWireTransport
7747
+ // (the signed begin/ack/yield channel). The trigger is the explicit
7748
+ // POST /handoff/initiate route (no sleep auto-trigger — SleepWakeDetector
7749
+ // emits only 'wake', so there is no pre-sleep hook for v1).
7750
+ if (handoffWireTransport) {
7751
+ // The active-topic selection + dep binding live in the extracted
7752
+ // createHandoffSentinelBootWiring factory so the boot glue is unit-tested
7753
+ // (wiring-integrity), not inline here.
7754
+ const sentinelWiring = createHandoffSentinelBootWiring({
7755
+ telegram,
7756
+ coordinator,
7757
+ liveTailSource,
7758
+ wire: handoffWireTransport,
7759
+ handoffAckTimeoutMs: seamlessness.handoffAckTimeoutMs,
7760
+ minHandoffIntervalMs: seamlessness.minHandoffIntervalMs,
7761
+ logger: (m) => console.log(pc.dim(` ${m}`)),
7762
+ });
7763
+ handoffInitiate = sentinelWiring.initiate;
7764
+ handoffSentinelInProgress = () => sentinelWiring.sentinel.inProgress;
7765
+ console.log(pc.dim(' Handoff sentinel active (operator trigger: POST /handoff/initiate)'));
7766
+ }
7767
+ }
7768
+ // ── Incoming-side planned-handoff receiver (spec §8 G3d/G3e) ──
7769
+ // Constructed on every mesh machine; it acts only while this machine is the
7770
+ // standby being handed to. The begin route hands us the outgoing's flush
7771
+ // manifest; buildAck echoes its tailSeq + ingressPosition and recomputes the
7772
+ // thread-history hash from OUR own synced state (matches iff the live-tail
7773
+ // kept us caught up — same hash function the outgoing's flush uses). The lease
7774
+ // CAS is attempted ONLY on the explicit yield, never on the ack.
7775
+ let onHandoffBegin;
7776
+ if (handoffWireTransport && telegram && coordinator.enabled) {
7777
+ const hwt = handoffWireTransport;
7778
+ const handoffWiring = createHandoffReceiverWiring({
7779
+ sendAck: (ack) => hwt.sendAck(ack),
7780
+ acquireLeaseOnConsent: (from) => coordinator.acquireLeaseOnConsent(from),
7781
+ getTopicHistory: (topic, limit) => telegram.getTopicHistory(topic, limit),
7782
+ logger: (m) => console.log(pc.dim(` ${m}`)),
7783
+ });
7784
+ // The yield route → transport.recordYield → this handler → receiver.onYield → lease CAS.
7785
+ hwt.onYield(handoffWiring.yieldHandler);
7786
+ // The begin route → store the manifest + drive the caught-up ack build/send.
7787
+ onHandoffBegin = handoffWiring.onBegin;
7788
+ console.log(pc.dim(' Handoff receiver active (begin→ack, yield→lease CAS)'));
7789
+ }
7790
+ // ── Exactly-once ingress ledger (spec §8 G3a) ──────────────────
7791
+ // Constructed ONLY when multiMachine.exactlyOnceIngress is enabled (default
7792
+ // off). When absent the inbound/outbound message path is byte-for-byte
7793
+ // unchanged — this ships dark and is flipped on only after a live
7794
+ // test-as-self confirms no false-drops on the most critical path.
7795
+ let messageLedger;
7796
+ let currentInboundByTopic;
7797
+ if (seamlessness.exactlyOnceIngress) {
7798
+ try {
7799
+ messageLedger = MessageProcessingLedger.open(config.projectName, config.stateDir);
7800
+ currentInboundByTopic = new Map();
7801
+ console.log(pc.dim(' Exactly-once ingress ledger ACTIVE (spec §8 G3a — inbound dedup + reply commit)'));
7802
+ }
7803
+ catch (err) {
7804
+ console.error(`[exactly-once] ledger open failed, gate stays dark: ${err instanceof Error ? err.message : err}`);
7805
+ }
7806
+ }
7561
7807
  // ── ReleaseReadinessSentinel (Layer B of release-readiness-visibility) ──
7562
7808
  // Repo-gated dev-environment watchdog: only constructed when the install has
7563
7809
  // an analyzable instar git repo AND the feature is enabled in config. On a
@@ -7598,7 +7844,7 @@ export async function startServer(options) {
7598
7844
  }
7599
7845
  }
7600
7846
  }
7601
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, proxyCoordinator, topicIntentStore, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, unjustifiedStopGate, stopGateDb, stopNotifier });
7847
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, proxyCoordinator, topicIntentStore, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, unjustifiedStopGate, stopGateDb, stopNotifier });
7602
7848
  // Boot-recovery (tunnel-failure-resilience spec Part 6): if the agent
7603
7849
  // died mid-relay-episode, the persisted tunnel.json carries
7604
7850
  // rotationPending=true. Rotate the dashboard PIN + authToken BEFORE
@@ -7612,6 +7858,49 @@ export async function startServer(options) {
7612
7858
  void taskFlowSweeper;
7613
7859
  void taskFlowDueWaker;
7614
7860
  void divergenceChecker;
7861
+ // ── No-LOSS recovery: re-run inbound events stranded in 'processing' (spec §8 G3a) ──
7862
+ // The complement to the inbound dedup gate: an event claimed but never
7863
+ // reply_committed (the holder crashed or was fenced mid-turn) is re-run by the
7864
+ // current lease holder from its stored input, via the SAME onTopicMessage path a
7865
+ // fresh forward uses — so the lost reply is produced. Only active when the
7866
+ // exactly-once ledger is wired (flag on). Lease-gated (a standby never injects),
7867
+ // attempts-capped (an unanswered message is not re-run forever). Telegram-only for
7868
+ // v1 (the --no-telegram sessionManager path is a tracked refinement).
7869
+ if (messageLedger && currentInboundByTopic && telegram) {
7870
+ const ledgerForRecovery = messageLedger;
7871
+ const inboundMap = currentInboundByTopic;
7872
+ const reinjectStuck = (topicId, dedupeKey, replayText) => {
7873
+ inboundMap.set(topicId, dedupeKey); // so the eventual reply commits THIS entry
7874
+ void telegram.onTopicMessage?.({
7875
+ id: `replay-${dedupeKey}`,
7876
+ userId: 'unknown',
7877
+ content: replayText,
7878
+ channel: { type: 'telegram', identifier: topicId },
7879
+ receivedAt: new Date().toISOString(),
7880
+ metadata: { messageThreadId: Number(topicId), viaLifeline: true, replay: true },
7881
+ });
7882
+ };
7883
+ const runStuckRecovery = () => {
7884
+ try {
7885
+ recoverStuckMessages({
7886
+ ledger: ledgerForRecovery,
7887
+ holdsLease: () => coordinator.holdsLease(),
7888
+ epoch: coordinator.getLeaseEpoch(),
7889
+ maxProcessingMs: seamlessness.maxProcessingMs,
7890
+ reinject: reinjectStuck,
7891
+ logger: (m) => console.log(pc.dim(` ${m}`)),
7892
+ });
7893
+ }
7894
+ catch (err) {
7895
+ console.error(`[stuck-recovery] ${err instanceof Error ? err.message : err}`);
7896
+ }
7897
+ };
7898
+ runStuckRecovery(); // boot: re-run anything a prior crash left mid-turn
7899
+ const stuckTimer = setInterval(runStuckRecovery, Math.max(30_000, seamlessness.maxProcessingMs));
7900
+ if (stuckTimer.unref)
7901
+ stuckTimer.unref();
7902
+ console.log(pc.dim(' Stuck-message recovery active (no-loss half, spec §8 G3a)'));
7903
+ }
7615
7904
  // Connect DegradationReporter downstream systems now that everything is initialized.
7616
7905
  // Any degradation events queued during startup will drain to feedback + telegram.
7617
7906
  {