instar 1.3.339 → 1.3.340

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 (54) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +166 -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/StateManager.d.ts +41 -0
  24. package/dist/core/StateManager.d.ts.map +1 -1
  25. package/dist/core/StateManager.js +100 -0
  26. package/dist/core/StateManager.js.map +1 -1
  27. package/dist/core/types.d.ts +35 -0
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/core/types.js.map +1 -1
  30. package/dist/messaging/TelegramAdapter.d.ts +14 -0
  31. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  32. package/dist/messaging/TelegramAdapter.js +16 -1
  33. package/dist/messaging/TelegramAdapter.js.map +1 -1
  34. package/dist/scaffold/templates.d.ts.map +1 -1
  35. package/dist/scaffold/templates.js +7 -0
  36. package/dist/scaffold/templates.js.map +1 -1
  37. package/dist/server/routes.d.ts.map +1 -1
  38. package/dist/server/routes.js +54 -3
  39. package/dist/server/routes.js.map +1 -1
  40. package/package.json +2 -2
  41. package/scripts/lint-cas-emit-placement.js +87 -0
  42. package/scripts/lint-journal-actuation-ban.js +63 -0
  43. package/scripts/lint-state-registry.js +313 -0
  44. package/src/data/builtin-manifest.json +46 -46
  45. package/src/data/state-coherence-registry.json +880 -0
  46. package/src/scaffold/templates.ts +7 -0
  47. package/upgrades/1.3.340.md +132 -0
  48. package/upgrades/coherence-journal-p1-1.eli16.md +11 -0
  49. package/upgrades/coherence-journal-p1-2.eli16.md +11 -0
  50. package/upgrades/coherence-journal-p1-3.eli16.md +13 -0
  51. package/upgrades/side-effects/coherence-journal-p1-1.md +68 -0
  52. package/upgrades/side-effects/coherence-journal-p1-2.md +54 -0
  53. package/upgrades/side-effects/coherence-journal-p1-3.md +49 -0
  54. 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,CA65RtE;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({
@@ -9307,16 +9418,55 @@ export async function startServer(options) {
9307
9418
  logger: (m) => console.log(pc.dim(` ${m}`)),
9308
9419
  });
9309
9420
  const ownReg = sessionOwnershipRegistry;
9421
+ // Coherence journal §3.3: the emit is a thin wrapper at every CAS call
9422
+ // site — `reason` is caller knowledge (cas() is a storage primitive and
9423
+ // cannot know WHY). A mesh-applied action records the coarse reason;
9424
+ // the ORIGINATING machine's own emit carries the precise one. The
9425
+ // cas-pairing lint (scripts/lint-cas-emit-placement.js) fails CI on
9426
+ // any cas( call site missing this pairing.
9427
+ const emitPlacement = (sessionKey, r, reason, prevOwner) => {
9428
+ try {
9429
+ if (!coherenceJournal || !r?.ok)
9430
+ return;
9431
+ const rec = r.record;
9432
+ const topicNum = Number(sessionKey);
9433
+ if (!Number.isFinite(topicNum) || rec?.ownershipEpoch == null)
9434
+ return;
9435
+ coherenceJournal.emitPlacement(topicNum, {
9436
+ owner: rec.ownerMachineId ?? '',
9437
+ ...(prevOwner ? { prevOwner } : {}),
9438
+ epoch: rec.ownershipEpoch,
9439
+ reason,
9440
+ });
9441
+ }
9442
+ catch { /* observability never endangers the observed */ }
9443
+ };
9310
9444
  // The §L3 ownership commands, routed from MeshRpc to the registry CAS.
9311
9445
  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 });
9446
+ if (cmd.type === 'place') {
9447
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9448
+ const r = ownReg.cas({ type: 'place', machineId: cmd.machine }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9449
+ emitPlacement(cmd.session, r, 'placed', prev);
9450
+ return r;
9451
+ }
9452
+ if (cmd.type === 'claim') {
9453
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9454
+ const r = ownReg.cas({ type: 'claim', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9455
+ emitPlacement(cmd.session, r, cmd.failover ? 'failover' : 'placed', prev);
9456
+ return r;
9457
+ }
9458
+ if (cmd.type === 'transfer') {
9459
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9460
+ const r = ownReg.cas({ type: 'transfer', to: cmd.target }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9461
+ emitPlacement(cmd.session, r, 'user-move', prev);
9462
+ return r;
9463
+ }
9464
+ if (cmd.type === 'release') {
9465
+ const prev = ownReg.read(cmd.session)?.ownerMachineId;
9466
+ const r = ownReg.cas({ type: 'release', machineId: sender }, { sessionKey: cmd.session, sender, nonce: env.nonce });
9467
+ emitPlacement(cmd.session, r, 'released', prev);
9468
+ return r;
9469
+ }
9320
9470
  return { ok: false, reason: 'unsupported' };
9321
9471
  };
9322
9472
  // The §L4 owner-side deliverMessage receive handler (shared factory — same
@@ -9702,7 +9852,9 @@ export async function startServer(options) {
9702
9852
  markOwnerSuspect: (m) => ownerSuspectBreaker.markSuspect(m),
9703
9853
  onOwnerResponsive: (m) => ownerSuspectBreaker.recordSuccess(m),
9704
9854
  casClaimOwnership: (sk, machineId) => {
9855
+ const prevOwner = ownReg.read(sk)?.ownerMachineId;
9705
9856
  const r = ownReg.cas({ type: 'place', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:c:${++routerNonce}` });
9857
+ emitPlacement(sk, r, 'placed', prevOwner);
9706
9858
  return { ok: r.ok, epoch: ownReg.read(sk)?.ownershipEpoch ?? 0 };
9707
9859
  },
9708
9860
  // bug #11: confirm the remote owner (placing → active) after the spawn is
@@ -9711,7 +9863,9 @@ export async function startServer(options) {
9711
9863
  // owner, so the router confirms on the target's behalf. Without this the
9712
9864
  // record stays 'placing' and every later message for the session queues.
9713
9865
  confirmClaim: (sk, machineId) => {
9714
- ownReg.cas({ type: 'claim', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:cl:${++routerNonce}` });
9866
+ const prevOwner = ownReg.read(sk)?.ownerMachineId;
9867
+ const r = ownReg.cas({ type: 'claim', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:cl:${++routerNonce}` });
9868
+ emitPlacement(sk, r, 'placed', prevOwner); // placing→active confirmation: a real epoch bump in the registry
9715
9869
  },
9716
9870
  deliverMessage: async (target, env) => {
9717
9871
  const url = peerUrl(target);
@@ -9879,7 +10033,9 @@ export async function startServer(options) {
9879
10033
  // If THIS machine actively owns the topic, release so the next message re-places to the pin.
9880
10034
  try {
9881
10035
  if (ownReg.ownerOf(sessionKey) === meshSelfId) {
9882
- ownReg.cas({ type: 'release', machineId: meshSelfId }, { sessionKey, sender: meshSelfId, nonce: `${meshSelfId}:rel:${sessionKey}:${Math.round(performance.now())}` });
10036
+ const prevOwner = ownReg.read(sessionKey)?.ownerMachineId;
10037
+ const r = ownReg.cas({ type: 'release', machineId: meshSelfId }, { sessionKey, sender: meshSelfId, nonce: `${meshSelfId}:rel:${sessionKey}:${Math.round(performance.now())}` });
10038
+ emitPlacement(sessionKey, r, 'user-move', prevOwner); // the explicit move's release half
9883
10039
  }
9884
10040
  }
9885
10041
  catch { /* best-effort; route() re-places regardless once the owner is cleared */ }