instar 1.3.339 → 1.3.341

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 (64) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +178 -10
  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 +17 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/AutonomousSessions.d.ts +17 -2
  8. package/dist/core/AutonomousSessions.d.ts.map +1 -1
  9. package/dist/core/AutonomousSessions.js +30 -4
  10. package/dist/core/AutonomousSessions.js.map +1 -1
  11. package/dist/core/CoherenceJournal.d.ts +326 -0
  12. package/dist/core/CoherenceJournal.d.ts.map +1 -0
  13. package/dist/core/CoherenceJournal.js +999 -0
  14. package/dist/core/CoherenceJournal.js.map +1 -0
  15. package/dist/core/CoherenceJournalReader.d.ts +158 -0
  16. package/dist/core/CoherenceJournalReader.d.ts.map +1 -0
  17. package/dist/core/CoherenceJournalReader.js +450 -0
  18. package/dist/core/CoherenceJournalReader.js.map +1 -0
  19. package/dist/core/JournalSyncApplier.d.ts +272 -0
  20. package/dist/core/JournalSyncApplier.d.ts.map +1 -0
  21. package/dist/core/JournalSyncApplier.js +835 -0
  22. package/dist/core/JournalSyncApplier.js.map +1 -0
  23. package/dist/core/PendingInjectStore.d.ts +54 -0
  24. package/dist/core/PendingInjectStore.d.ts.map +1 -0
  25. package/dist/core/PendingInjectStore.js +152 -0
  26. package/dist/core/PendingInjectStore.js.map +1 -0
  27. package/dist/core/SessionManager.d.ts +17 -0
  28. package/dist/core/SessionManager.d.ts.map +1 -1
  29. package/dist/core/SessionManager.js +70 -1
  30. package/dist/core/SessionManager.js.map +1 -1
  31. package/dist/core/StateManager.d.ts +45 -0
  32. package/dist/core/StateManager.d.ts.map +1 -1
  33. package/dist/core/StateManager.js +106 -0
  34. package/dist/core/StateManager.js.map +1 -1
  35. package/dist/core/types.d.ts +35 -0
  36. package/dist/core/types.d.ts.map +1 -1
  37. package/dist/core/types.js.map +1 -1
  38. package/dist/messaging/TelegramAdapter.d.ts +14 -0
  39. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  40. package/dist/messaging/TelegramAdapter.js +16 -1
  41. package/dist/messaging/TelegramAdapter.js.map +1 -1
  42. package/dist/scaffold/templates.d.ts.map +1 -1
  43. package/dist/scaffold/templates.js +7 -0
  44. package/dist/scaffold/templates.js.map +1 -1
  45. package/dist/server/routes.d.ts.map +1 -1
  46. package/dist/server/routes.js +54 -3
  47. package/dist/server/routes.js.map +1 -1
  48. package/package.json +2 -2
  49. package/scripts/lint-cas-emit-placement.js +87 -0
  50. package/scripts/lint-journal-actuation-ban.js +63 -0
  51. package/scripts/lint-state-registry.js +313 -0
  52. package/src/data/builtin-manifest.json +47 -47
  53. package/src/data/state-coherence-registry.json +880 -0
  54. package/src/scaffold/templates.ts +7 -0
  55. package/upgrades/1.3.340.md +132 -0
  56. package/upgrades/1.3.341.md +41 -0
  57. package/upgrades/coherence-journal-p1-1.eli16.md +11 -0
  58. package/upgrades/coherence-journal-p1-2.eli16.md +11 -0
  59. package/upgrades/coherence-journal-p1-3.eli16.md +13 -0
  60. package/upgrades/side-effects/coherence-journal-p1-1.md +68 -0
  61. package/upgrades/side-effects/coherence-journal-p1-2.md +54 -0
  62. package/upgrades/side-effects/coherence-journal-p1-3.md +49 -0
  63. package/upgrades/side-effects/pending-inject-durability.md +58 -0
  64. package/upgrades/1.3.339.md +0 -48
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA6SH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAuxDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgxRtE;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;AA6SH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAuxDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA06RtE;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"}
@@ -2304,6 +2304,94 @@ export async function startServer(options) {
2304
2304
  console.log(pc.yellow(' Standby mode — processing gated, writes disabled'));
2305
2305
  }
2306
2306
  }
2307
+ // ── Coherence Journal (COHERENCE-JOURNAL-SPEC §3.1/§3.3, P1.1) ─────────
2308
+ // Per-machine append-only event streams (topic-placement / session-
2309
+ // lifecycle / autonomous-run). Dark-ship: `enabled ?? !!developmentAgent`.
2310
+ // Constructed HERE — after the coordinator (machineId available), before
2311
+ // the reaper/ownership wiring that emits into it. Writer runs even on a
2312
+ // single-machine agent (locally useful); replication is P1.3.
2313
+ let coherenceJournal;
2314
+ {
2315
+ const cjCfg = config.multiMachine?.coherenceJournal;
2316
+ const cjEnabled = cjCfg?.enabled ?? !!config.developmentAgent;
2317
+ if (cjEnabled) {
2318
+ try {
2319
+ const cjMod = await import('../core/CoherenceJournal.js');
2320
+ // Stable machine id: mesh identity when present; deterministic
2321
+ // hostname-derived fallback for single-machine agents (sanitized by
2322
+ // the journal's own percent-encode rule either way).
2323
+ const cjMachineId = coordinator.identity?.machineId ?? `m_host_${os.hostname()}`;
2324
+ coherenceJournal = new cjMod.CoherenceJournal({
2325
+ stateDir: config.stateDir,
2326
+ machineId: cjMachineId,
2327
+ flushIntervalMs: cjCfg?.flushIntervalMs,
2328
+ retention: cjCfg?.retention,
2329
+ // The §3.1 standby-safe seam: the flusher asks StateManager before
2330
+ // each append batch; the prefix allowlist lives there.
2331
+ guardWrite: (p) => state.guardJournalWrite(p),
2332
+ logger: (m) => console.log(pc.dim(` [coherence-journal] ${m}`)),
2333
+ });
2334
+ coherenceJournal.open();
2335
+ // Lifecycle funnel (§3.3): every session status transition flows
2336
+ // through StateManager.saveSession; the diff-derived emit lives there.
2337
+ state.setCoherenceJournal(coherenceJournal);
2338
+ // §3.3 autonomous-run journal scanner — observation-based start/stop
2339
+ // (no single .local.md write funnel exists; polling is the structural
2340
+ // choice). P19 brakes, declared: constant per-tick cost (bounded by
2341
+ // maxConcurrent active runs); a throwing read skips the tick and
2342
+ // never compounds; the seen-set evicts a run after its stopped emit
2343
+ // (bounded by active runs, not history); emits inherit the writer's
2344
+ // rate cap. Sub-scan-interval runs are not observed (stated spec
2345
+ // limitation). Op-key dedupe collapses scanner + stop-funnel emits.
2346
+ {
2347
+ const journal = coherenceJournal;
2348
+ const cjScannerMs = cjCfg?.scannerIntervalMs ?? 60000;
2349
+ const seenRuns = new Map();
2350
+ const cjScan = async () => {
2351
+ try {
2352
+ const { activeAutonomousJobs, autonomousRunId } = await import('../core/AutonomousSessions.js');
2353
+ const active = activeAutonomousJobs(config.stateDir);
2354
+ const liveTopics = new Set();
2355
+ for (const j of active) {
2356
+ if (j.topic == null)
2357
+ continue; // legacy single-file job: not topic-scoped
2358
+ liveTopics.add(j.topic);
2359
+ const topicNum = Number(j.topic);
2360
+ if (!Number.isFinite(topicNum))
2361
+ continue;
2362
+ if (!seenRuns.has(j.topic)) {
2363
+ const runId = autonomousRunId(j.startedAt, j.topic);
2364
+ seenRuns.set(j.topic, { runId, file: j.file });
2365
+ journal.emitAutonomousRun(topicNum, { action: 'started', runId, artifactPaths: [j.file] });
2366
+ }
2367
+ }
2368
+ for (const [topicKey, run] of [...seenRuns]) {
2369
+ if (liveTopics.has(topicKey))
2370
+ continue;
2371
+ const topicNum = Number(topicKey);
2372
+ if (Number.isFinite(topicNum)) {
2373
+ // observed-stopped: covers deaths outside the stop funnels
2374
+ // (crash / reboot / reaper kill) — no phantom-live runs.
2375
+ journal.emitAutonomousRun(topicNum, { action: 'stopped', runId: run.runId, artifactPaths: [run.file] });
2376
+ }
2377
+ seenRuns.delete(topicKey);
2378
+ }
2379
+ }
2380
+ catch { /* @silent-fallback-ok: journal observability must never endanger the observed operation (COHERENCE-JOURNAL-SPEC §3.1) */ /* skip-tick; never compounds (P19) */ }
2381
+ };
2382
+ const cjScanTimer = setInterval(() => { void cjScan(); }, cjScannerMs);
2383
+ cjScanTimer.unref?.();
2384
+ void cjScan(); // prime at boot (re-emits dedupe via op keys)
2385
+ }
2386
+ console.log(pc.dim(` Coherence journal: writer active (${cjMachineId.slice(0, 16)}…)`));
2387
+ }
2388
+ catch (err) { /* @silent-fallback-ok: journal observability must never endanger the observed operation (COHERENCE-JOURNAL-SPEC §3.1) */
2389
+ // The journal must never endanger boot (§3.1 inverted at startup).
2390
+ console.warn(pc.yellow(` Coherence journal failed to start (continuing without): ${err instanceof Error ? err.message : String(err)}`));
2391
+ coherenceJournal = undefined;
2392
+ }
2393
+ }
2394
+ }
2307
2395
  // Cross-Machine Seamlessness (spec §9) — resolve + validate the tunable
2308
2396
  // knobs at startup. A violating config (e.g. a widened ingressHeartbeatMs
2309
2397
  // that breaks the RPO bound) is REJECTED here with a clear message rather
@@ -3577,6 +3665,16 @@ export async function startServer(options) {
3577
3665
  console.error(`[server] Failed to ensure Agent Updates topic: ${err}`);
3578
3666
  });
3579
3667
  }
3668
+ // ── Coherence Journal × Telegram emergency-stop seam (COHERENCE-JOURNAL §3.3)
3669
+ // The adapter's sentinel emergency-stop path clears a topic's autonomous job
3670
+ // via stopAutonomousTopic but holds no StateManager, so it cannot reach the
3671
+ // wired journal on its own. Inject the journal as the AutonomousJournalSeam so
3672
+ // a sentinel-driven stop emits the autonomous-run `stopped` event like every
3673
+ // other stop funnel. Placed AFTER both adapter-setup blocks (so `telegram` is
3674
+ // assigned in either mode) and only when the journal is wired.
3675
+ if (telegram && coherenceJournal) {
3676
+ telegram.setCoherenceJournalSeam(coherenceJournal);
3677
+ }
3580
3678
  // Agent worktree convention (Layer 4) — lifeline detector.
3581
3679
  //
3582
3680
  // Runs once per server boot. Inspects the canonical instar repo's
@@ -4301,6 +4399,19 @@ export async function startServer(options) {
4301
4399
  origin: e.origin,
4302
4400
  });
4303
4401
  reapNotifier.onReaped({ session: e.session, reason: e.reason, disposition: e.disposition, origin: e.origin });
4402
+ // Coherence journal 'reaped' (§3.3): emitted HERE, alongside the
4403
+ // reap-log append it references — never derived in the saveSession
4404
+ // funnel (which records the plain killed/completed transition).
4405
+ try {
4406
+ const m = /(?:^|[-_])(?:topic|telegram)[-_]?(\d+)(?:$|[-_])/.exec(e.session.name ?? '');
4407
+ coherenceJournal?.emitLifecycle({
4408
+ sessionId: e.session.id,
4409
+ status: 'reaped',
4410
+ reapReason: e.reason,
4411
+ reapLogRef: `logs/reap-log.jsonl:${e.session.name}`,
4412
+ }, m ? Number(m[1]) : undefined);
4413
+ }
4414
+ catch { /* observability never endangers the observed */ }
4304
4415
  });
4305
4416
  sessionManager.on('reapBlocked', (e) => {
4306
4417
  reapLog.recordSkipped({
@@ -4317,6 +4428,18 @@ export async function startServer(options) {
4317
4428
  // lifeline to restart the server in a tight loop.
4318
4429
  await sessionManager.purgeDeadSessions();
4319
4430
  sessionManager.startMonitoring();
4431
+ // Pending-inject recovery (finding 8d300555): re-deliver initial messages
4432
+ // orphaned by the previous server process dying in the spawn→ready→inject
4433
+ // window (the auto-updater restart race). Runs AFTER the purge so dead
4434
+ // sessions are already settled, and in the background — the ready-waits
4435
+ // inside can take up to 90s per session and must not block boot.
4436
+ void sessionManager.recoverPendingInjects().catch((err) => {
4437
+ // @silent-fallback-ok boot recovery is a backstop that must NEVER crash
4438
+ // boot — its own internal failures already route to DegradationReporter
4439
+ // (sweepPendingInjects.reportLoss); this outer guard only catches an
4440
+ // unexpected throw from the sweep harness itself.
4441
+ console.error(`[server] Pending-inject recovery failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
4442
+ });
4320
4443
  // StuckInputSentinel — persistent, restart-safe recovery for tmux prompts
4321
4444
  // that hold text but never submitted Enter. Complements the in-process
4322
4445
  // verifyInjection timers (PR #159) which die when the server crashes.
@@ -9307,16 +9430,55 @@ export async function startServer(options) {
9307
9430
  logger: (m) => console.log(pc.dim(` ${m}`)),
9308
9431
  });
9309
9432
  const ownReg = sessionOwnershipRegistry;
9433
+ // Coherence journal §3.3: the emit is a thin wrapper at every CAS call
9434
+ // site — `reason` is caller knowledge (cas() is a storage primitive and
9435
+ // cannot know WHY). A mesh-applied action records the coarse reason;
9436
+ // the ORIGINATING machine's own emit carries the precise one. The
9437
+ // cas-pairing lint (scripts/lint-cas-emit-placement.js) fails CI on
9438
+ // any cas( call site missing this pairing.
9439
+ const emitPlacement = (sessionKey, r, reason, prevOwner) => {
9440
+ try {
9441
+ if (!coherenceJournal || !r?.ok)
9442
+ return;
9443
+ const rec = r.record;
9444
+ const topicNum = Number(sessionKey);
9445
+ if (!Number.isFinite(topicNum) || rec?.ownershipEpoch == null)
9446
+ return;
9447
+ coherenceJournal.emitPlacement(topicNum, {
9448
+ owner: rec.ownerMachineId ?? '',
9449
+ ...(prevOwner ? { prevOwner } : {}),
9450
+ epoch: rec.ownershipEpoch,
9451
+ reason,
9452
+ });
9453
+ }
9454
+ catch { /* observability never endangers the observed */ }
9455
+ };
9310
9456
  // The §L3 ownership commands, routed from MeshRpc to the registry CAS.
9311
9457
  const ownAction = (cmd, sender, env) => {
9312
- if (cmd.type === 'place')
9313
- return ownReg.cas({ type: 'place', machineId: cmd.machine }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9314
- if (cmd.type === 'claim')
9315
- return ownReg.cas({ type: 'claim', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9316
- if (cmd.type === 'transfer')
9317
- return ownReg.cas({ type: 'transfer', to: cmd.target }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9318
- if (cmd.type === 'release')
9319
- return ownReg.cas({ type: 'release', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9458
+ if (cmd.type === 'place') {
9459
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9460
+ const r = ownReg.cas({ type: 'place', machineId: cmd.machine }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9461
+ emitPlacement(cmd.session, r, 'placed', prev);
9462
+ return r;
9463
+ }
9464
+ if (cmd.type === 'claim') {
9465
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9466
+ const r = ownReg.cas({ type: 'claim', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9467
+ emitPlacement(cmd.session, r, cmd.failover ? 'failover' : 'placed', prev);
9468
+ return r;
9469
+ }
9470
+ if (cmd.type === 'transfer') {
9471
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9472
+ const r = ownReg.cas({ type: 'transfer', to: cmd.target }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9473
+ emitPlacement(cmd.session, r, 'user-move', prev);
9474
+ return r;
9475
+ }
9476
+ if (cmd.type === 'release') {
9477
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9478
+ const r = ownReg.cas({ type: 'release', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9479
+ emitPlacement(cmd.session, r, 'released', prev);
9480
+ return r;
9481
+ }
9320
9482
  return { ok: false, reason: 'unsupported' };
9321
9483
  };
9322
9484
  // The §L4 owner-side deliverMessage receive handler (shared factory — same
@@ -9702,7 +9864,9 @@ export async function startServer(options) {
9702
9864
  markOwnerSuspect: (m) => ownerSuspectBreaker.markSuspect(m),
9703
9865
  onOwnerResponsive: (m) => ownerSuspectBreaker.recordSuccess(m),
9704
9866
  casClaimOwnership: (sk, machineId) => {
9867
+ const prevOwner = ownReg.read(sk)?.ownerMachineId;
9705
9868
  const r = ownReg.cas({ type: 'place', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:c:${++routerNonce}` });
9869
+ emitPlacement(sk, r, 'placed', prevOwner);
9706
9870
  return { ok: r.ok, epoch: ownReg.read(sk)?.ownershipEpoch ?? 0 };
9707
9871
  },
9708
9872
  // bug #11: confirm the remote owner (placing → active) after the spawn is
@@ -9711,7 +9875,9 @@ export async function startServer(options) {
9711
9875
  // owner, so the router confirms on the target's behalf. Without this the
9712
9876
  // record stays 'placing' and every later message for the session queues.
9713
9877
  confirmClaim: (sk, machineId) => {
9714
- ownReg.cas({ type: 'claim', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:cl:${++routerNonce}` });
9878
+ const prevOwner = ownReg.read(sk)?.ownerMachineId;
9879
+ const r = ownReg.cas({ type: 'claim', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:cl:${++routerNonce}` });
9880
+ emitPlacement(sk, r, 'placed', prevOwner); // placing→active confirmation: a real epoch bump in the registry
9715
9881
  },
9716
9882
  deliverMessage: async (target, env) => {
9717
9883
  const url = peerUrl(target);
@@ -9879,7 +10045,9 @@ export async function startServer(options) {
9879
10045
  // If THIS machine actively owns the topic, release so the next message re-places to the pin.
9880
10046
  try {
9881
10047
  if (ownReg.ownerOf(sessionKey) === meshSelfId) {
9882
- ownReg.cas({ type: 'release', machineId: meshSelfId }, { sessionKey, sender: meshSelfId, nonce: `${meshSelfId}:rel:${sessionKey}:${Math.round(performance.now())}` });
10048
+ const prevOwner = ownReg.read(sessionKey)?.ownerMachineId;
10049
+ const r = ownReg.cas({ type: 'release', machineId: meshSelfId }, { sessionKey, sender: meshSelfId, nonce: `${meshSelfId}:rel:${sessionKey}:${Math.round(performance.now())}` });
10050
+ emitPlacement(sessionKey, r, 'user-move', prevOwner); // the explicit move's release half
9883
10051
  }
9884
10052
  }
9885
10053
  catch { /* best-effort; route() re-places regardless once the owner is cleared */ }