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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +292 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/HandoffReceiver.d.ts +56 -0
- package/dist/core/HandoffReceiver.d.ts.map +1 -0
- package/dist/core/HandoffReceiver.js +103 -0
- package/dist/core/HandoffReceiver.js.map +1 -0
- package/dist/core/HandoffWireTransport.d.ts +61 -0
- package/dist/core/HandoffWireTransport.d.ts.map +1 -0
- package/dist/core/HandoffWireTransport.js +114 -0
- package/dist/core/HandoffWireTransport.js.map +1 -0
- package/dist/core/HttpLeaseTransport.d.ts +68 -0
- package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
- package/dist/core/HttpLeaseTransport.js +97 -0
- package/dist/core/HttpLeaseTransport.js.map +1 -0
- package/dist/core/HttpLiveTailTransport.d.ts +88 -0
- package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
- package/dist/core/HttpLiveTailTransport.js +95 -0
- package/dist/core/HttpLiveTailTransport.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +14 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +38 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/LiveTailSource.d.ts +65 -0
- package/dist/core/LiveTailSource.d.ts.map +1 -0
- package/dist/core/LiveTailSource.js +87 -0
- package/dist/core/LiveTailSource.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +14 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +22 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/ReplyMarkerTransport.d.ts +58 -0
- package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
- package/dist/core/ReplyMarkerTransport.js +66 -0
- package/dist/core/ReplyMarkerTransport.js.map +1 -0
- package/dist/core/SafeGitExecutor.d.ts.map +1 -1
- package/dist/core/SafeGitExecutor.js +21 -2
- package/dist/core/SafeGitExecutor.js.map +1 -1
- package/dist/core/featureRolloutScan.d.ts.map +1 -1
- package/dist/core/featureRolloutScan.js +5 -4
- package/dist/core/featureRolloutScan.js.map +1 -1
- package/dist/core/handoffReceiverWiring.d.ts +57 -0
- package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
- package/dist/core/handoffReceiverWiring.js +65 -0
- package/dist/core/handoffReceiverWiring.js.map +1 -0
- package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
- package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelBootWiring.js +52 -0
- package/dist/core/handoffSentinelBootWiring.js.map +1 -0
- package/dist/core/handoffSentinelWiring.d.ts +60 -0
- package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelWiring.js +59 -0
- package/dist/core/handoffSentinelWiring.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +8 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +1 -0
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +6 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +8 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts +55 -0
- package/dist/messaging/ingressDedup.d.ts.map +1 -0
- package/dist/messaging/ingressDedup.js +69 -0
- package/dist/messaging/ingressDedup.js.map +1 -0
- package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
- package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
- package/dist/messaging/stuckMessageRecovery.js +52 -0
- package/dist/messaging/stuckMessageRecovery.js.map +1 -0
- package/dist/monitoring/releaseReadinessWiring.d.ts.map +1 -1
- package/dist/monitoring/releaseReadinessWiring.js +4 -2
- package/dist/monitoring/releaseReadinessWiring.js.map +1 -1
- package/dist/server/AgentServer.d.ts +64 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +24 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/handoffInitiateRoutes.d.ts +40 -0
- package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
- package/dist/server/handoffInitiateRoutes.js +57 -0
- package/dist/server/handoffInitiateRoutes.js.map +1 -0
- package/dist/server/machineRoutes.d.ts +47 -0
- package/dist/server/machineRoutes.d.ts.map +1 -1
- package/dist/server/machineRoutes.js +138 -0
- package/dist/server/machineRoutes.js.map +1 -1
- package/dist/server/routes.d.ts +20 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +65 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +49 -49
- package/upgrades/1.3.41.md +9 -5
- package/upgrades/1.3.43.md +21 -0
- package/upgrades/1.3.44.md +72 -0
- package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
- package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
- package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
- package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
- 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;
|
|
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"}
|
package/dist/commands/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
{
|