@adhdev/daemon-core 1.0.28-rc.10 → 1.0.28-rc.11

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/index.js CHANGED
@@ -791,10 +791,10 @@ function readInjected(value) {
791
791
  }
792
792
  function getDaemonBuildInfo() {
793
793
  if (cached) return cached;
794
- const commit = readInjected(true ? "e4b8cfd97ab9fbe458b61198b091a9051da87a6e" : void 0) ?? "unknown";
795
- const commitShort = readInjected(true ? "e4b8cfd9" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
796
- const version = readInjected(true ? "1.0.28-rc.10" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
797
- const builtAt = readInjected(true ? "2026-07-26T03:08:22.566Z" : void 0);
794
+ const commit = readInjected(true ? "5cd04eec1e043188c3ef97821372ad5ff615cd04" : void 0) ?? "unknown";
795
+ const commitShort = readInjected(true ? "5cd04eec" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
796
+ const version = readInjected(true ? "1.0.28-rc.11" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
797
+ const builtAt = readInjected(true ? "2026-07-26T07:08:19.328Z" : void 0);
798
798
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
799
799
  return cached;
800
800
  }
@@ -27260,6 +27260,11 @@ function validateCondition(c, sectionIds, path46) {
27260
27260
  return errs;
27261
27261
  }
27262
27262
  if ("cursor_above" in w && "changed" in w) return errs;
27263
+ if ("signal" in w) {
27264
+ if (typeof w.signal !== "string" || !w.signal.trim()) errs.push(`${path46}.signal must be a non-empty string`);
27265
+ if (w.equals !== void 0 && typeof w.equals !== "boolean") errs.push(`${path46}.equals must be a boolean`);
27266
+ return errs;
27267
+ }
27263
27268
  if ("elapsed_ms" in w) {
27264
27269
  if (typeof w.elapsed_ms !== "number") errs.push(`${path46}.elapsed_ms must be a number`);
27265
27270
  return errs;
@@ -27576,6 +27581,51 @@ var init_evaluator = __esm({
27576
27581
  }
27577
27582
  });
27578
27583
 
27584
+ // src/providers/spec/signal-envelope.ts
27585
+ function unavailableSignalSnapshot(now, reason, profile) {
27586
+ return {
27587
+ kind: SIGNAL_SNAPSHOT_KIND,
27588
+ sampledAt: now,
27589
+ available: false,
27590
+ unavailableReason: reason,
27591
+ ...profile ? { profile } : {},
27592
+ signals: { final_assistant_present: null, in_turn_progress: null, transcript_growing: null },
27593
+ detail: { msgCount: 0, sourceMtimeMs: 0, ageMs: null }
27594
+ };
27595
+ }
27596
+ function evaluateSignalLeaf(cond, snapshot) {
27597
+ const expected = cond.equals ?? true;
27598
+ if (!snapshot || !snapshot.available) {
27599
+ const why = !snapshot ? "no snapshot" : `unavailable(${snapshot.unavailableReason ?? "unknown"})`;
27600
+ return {
27601
+ value: null,
27602
+ shadowResult: null,
27603
+ detail: `signal ${cond.signal} ${why} \u2192 fail-open`
27604
+ };
27605
+ }
27606
+ const raw = Object.prototype.hasOwnProperty.call(snapshot.signals, cond.signal) ? snapshot.signals[cond.signal] : null;
27607
+ if (raw === null || raw === void 0) {
27608
+ return {
27609
+ value: null,
27610
+ shadowResult: null,
27611
+ detail: `signal ${cond.signal} unknown \u2192 fail-open`
27612
+ };
27613
+ }
27614
+ const shadowResult = raw === expected;
27615
+ return {
27616
+ value: raw,
27617
+ shadowResult,
27618
+ detail: `signal ${cond.signal}=${raw} expected=${expected} \u2192 ${shadowResult}`
27619
+ };
27620
+ }
27621
+ var SIGNAL_SNAPSHOT_KIND;
27622
+ var init_signal_envelope = __esm({
27623
+ "src/providers/spec/signal-envelope.ts"() {
27624
+ "use strict";
27625
+ SIGNAL_SNAPSHOT_KIND = "adhdev:fsm/signal-snapshot@0";
27626
+ }
27627
+ });
27628
+
27579
27629
  // src/providers/spec/fsm-evaluator.ts
27580
27630
  var fsm_evaluator_exports = {};
27581
27631
  __export(fsm_evaluator_exports, {
@@ -27609,24 +27659,74 @@ function isAny(c) {
27609
27659
  function isNot(c) {
27610
27660
  return "not" in c;
27611
27661
  }
27612
- function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId) {
27662
+ function isSignal(c) {
27663
+ return "signal" in c;
27664
+ }
27665
+ function containsSignal(c) {
27666
+ if (isSignal(c)) return true;
27667
+ if (isAll(c)) return c.all.some(containsSignal);
27668
+ if (isAny(c)) return c.any.some(containsSignal);
27669
+ if (isNot(c)) return containsSignal(c.not);
27670
+ return false;
27671
+ }
27672
+ function foldShadow(cond) {
27673
+ if (cond.kind === "signal") {
27674
+ const s2 = cond.signal;
27675
+ if (s2 && s2.shadowResult !== null && s2.shadowResult !== void 0) {
27676
+ return { value: s2.shadowResult, unknown: false };
27677
+ }
27678
+ return { value: true, unknown: true };
27679
+ }
27680
+ if (cond.kind === "not" && cond.children?.length) {
27681
+ const c = foldShadow(cond.children[0]);
27682
+ return { value: !c.value, unknown: c.unknown };
27683
+ }
27684
+ if (cond.kind === "all" && cond.children) {
27685
+ const kids = cond.children.map(foldShadow);
27686
+ if (kids.some((k) => !k.unknown && !k.value)) return { value: false, unknown: false };
27687
+ if (kids.some((k) => k.unknown)) return { value: true, unknown: true };
27688
+ return { value: true, unknown: false };
27689
+ }
27690
+ if (cond.kind === "any" && cond.children) {
27691
+ const kids = cond.children.map(foldShadow);
27692
+ if (kids.some((k) => !k.unknown && k.value)) return { value: true, unknown: false };
27693
+ if (kids.some((k) => k.unknown)) return { value: false, unknown: true };
27694
+ return { value: false, unknown: false };
27695
+ }
27696
+ return { value: cond.result, unknown: false };
27697
+ }
27698
+ function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot) {
27613
27699
  if (isAll(cond)) {
27614
- const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
27700
+ const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
27615
27701
  const result = children.every((c) => c.result);
27616
27702
  const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
27617
27703
  return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
27618
27704
  }
27619
27705
  if (isAny(cond)) {
27620
- const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
27706
+ const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
27621
27707
  const result = children.some((c) => c.result);
27622
27708
  const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
27623
27709
  const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
27624
27710
  return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
27625
27711
  }
27626
27712
  if (isNot(cond)) {
27627
- const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
27713
+ const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot);
27628
27714
  return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
27629
27715
  }
27716
+ if (isSignal(cond)) {
27717
+ const leaf = evaluateSignalLeaf(cond, signalSnapshot);
27718
+ return {
27719
+ kind: "signal",
27720
+ result: true,
27721
+ detail: `${leaf.detail} (stage0 shadow \u2014 pass-through)`,
27722
+ signal: {
27723
+ name: cond.signal,
27724
+ available: !!signalSnapshot?.available,
27725
+ value: leaf.value,
27726
+ shadowResult: leaf.shadowResult
27727
+ }
27728
+ };
27729
+ }
27630
27730
  if (isElapsed(cond)) {
27631
27731
  const age = clock.now - clock.stateEnteredAt;
27632
27732
  const result = age >= cond.elapsed_ms;
@@ -27665,7 +27765,7 @@ function fromLabel(t) {
27665
27765
  const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
27666
27766
  return t.label ?? `${from}\u2192${t.to}`;
27667
27767
  }
27668
- function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
27768
+ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock, signalSnapshot) {
27669
27769
  const legacyTrace = [];
27670
27770
  const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
27671
27771
  const cleanScreen = lines.join("\n");
@@ -27681,7 +27781,7 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
27681
27781
  let cond;
27682
27782
  let condResult = true;
27683
27783
  if (t.when) {
27684
- cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`);
27784
+ cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`, signalSnapshot);
27685
27785
  condResult = cond.result;
27686
27786
  }
27687
27787
  const fires = holdSatisfied && condResult;
@@ -27696,6 +27796,14 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
27696
27796
  fires,
27697
27797
  priority: t.priority ?? 0
27698
27798
  };
27799
+ if (t.when && containsSignal(t.when) && cond) {
27800
+ const folded = foldShadow(cond);
27801
+ te.shadow = {
27802
+ condResult: folded.value,
27803
+ fires: holdSatisfied && folded.value,
27804
+ unknown: folded.unknown
27805
+ };
27806
+ }
27699
27807
  transitions.push(te);
27700
27808
  if (fires && !fired) fired = te;
27701
27809
  }
@@ -27718,6 +27826,7 @@ var init_fsm_evaluator = __esm({
27718
27826
  "use strict";
27719
27827
  init_evaluator();
27720
27828
  init_fsm_types();
27829
+ init_signal_envelope();
27721
27830
  WHOLE_SCREEN = -1;
27722
27831
  }
27723
27832
  });
@@ -45515,12 +45624,21 @@ async function runDaemonUpgradeHelper(payload) {
45515
45624
  appendUpgradeLog(`Waiting for parent pid ${payload.parentPid} to exit`);
45516
45625
  await waitForPidExit(payload.parentPid, 15e3);
45517
45626
  }
45518
- if (process.platform === "win32") {
45627
+ if (process.platform === "win32" || payload.killSessionHost === true) {
45519
45628
  await stopSessionHostProcesses(sessionHostAppName);
45520
45629
  } else {
45521
45630
  appendUpgradeLog("POSIX \u2014 session-host left running (survives upgrade; sessions rebind on next boot)");
45522
45631
  }
45523
45632
  removeDaemonPidFile();
45633
+ if (payload.skipInstall) {
45634
+ appendUpgradeLog("Restart-only mode \u2014 package install skipped, re-spawning daemon");
45635
+ spawnDetachedDaemonRestart(restartArgv, payload.cwd);
45636
+ try {
45637
+ fs15.unlinkSync(getUpgradeFailureNoticePath());
45638
+ } catch {
45639
+ }
45640
+ return;
45641
+ }
45524
45642
  const instanceDir = resolveInstanceDir();
45525
45643
  const windowsInstallerLayout = resolveWindowsInstallerLayout({
45526
45644
  homeDir: os14.homedir(),
@@ -45639,6 +45757,13 @@ async function runDaemonUpgradeHelper(payload) {
45639
45757
  cleanupStaleGlobalInstallDirs(payload.packageName, installCommand.surface);
45640
45758
  appendUpgradeLog("Post-install staging cleanup complete");
45641
45759
  }
45760
+ spawnDetachedDaemonRestart(restartArgv, payload.cwd);
45761
+ try {
45762
+ fs15.unlinkSync(getUpgradeFailureNoticePath());
45763
+ } catch {
45764
+ }
45765
+ }
45766
+ function spawnDetachedDaemonRestart(restartArgv, cwd) {
45642
45767
  if (restartArgv.length > 0) {
45643
45768
  const env = { ...process.env };
45644
45769
  delete env[UPGRADE_HELPER_ENV];
@@ -45647,17 +45772,13 @@ async function runDaemonUpgradeHelper(payload) {
45647
45772
  detached: true,
45648
45773
  stdio: "ignore",
45649
45774
  windowsHide: true,
45650
- cwd: payload.cwd || process.cwd(),
45775
+ cwd: cwd || process.cwd(),
45651
45776
  env
45652
45777
  });
45653
45778
  child.unref();
45654
45779
  } else {
45655
45780
  appendUpgradeLog("No restart argv provided; upgrade completed without restart");
45656
45781
  }
45657
- try {
45658
- fs15.unlinkSync(getUpgradeFailureNoticePath());
45659
- } catch {
45660
- }
45661
45782
  }
45662
45783
  async function maybeRunDaemonUpgradeHelperFromEnv() {
45663
45784
  const raw = process.env[UPGRADE_HELPER_ENV];
@@ -45750,6 +45871,41 @@ var daemonLifecycleHandlers = {
45750
45871
  return { success: false, error: e.message };
45751
45872
  }
45752
45873
  },
45874
+ // Restart-only path (mesh restart_daemon_node mode="restart"): re-spawn the
45875
+ // daemon WITHOUT the npm reinstall daemon_upgrade performs. Used to reset
45876
+ // daemon state (memory leaks, zombie sessions, wedged internals) when the
45877
+ // version is already correct — downtime drops to the detached re-spawn.
45878
+ // killSessionHost is an explicit opt-in hard refresh (see upgrade-helper).
45879
+ daemon_restart: async (ctx, args) => {
45880
+ LOG.info("Restart", "Restart-only requested (no package reinstall)");
45881
+ try {
45882
+ const isStandalone = ctx.deps.packageName === "@adhdev/daemon-standalone" || process.argv[1]?.includes("daemon-standalone");
45883
+ const pkgName = isStandalone ? "@adhdev/daemon-standalone" : "adhdev";
45884
+ const killSessionHost = args?.killSessionHost === true;
45885
+ if (killSessionHost) {
45886
+ LOG.warn("Restart", "killSessionHost requested \u2014 session-host will be stopped and ALL hosted sessions destroyed");
45887
+ }
45888
+ spawnDetachedDaemonUpgradeHelper({
45889
+ packageName: pkgName,
45890
+ targetVersion: "",
45891
+ parentPid: process.pid,
45892
+ restartArgv: process.argv.slice(1),
45893
+ cwd: process.cwd(),
45894
+ sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || "adhdev",
45895
+ skipInstall: true,
45896
+ killSessionHost
45897
+ });
45898
+ LOG.info("Restart", "Scheduled detached restart-only helper");
45899
+ setTimeout(() => {
45900
+ LOG.info("Restart", "Exiting daemon so detached helper can re-spawn it...");
45901
+ process.exit(0);
45902
+ }, 3e3);
45903
+ return { success: true, restarted: true, restarting: true, mode: "restart", killSessionHost };
45904
+ } catch (e) {
45905
+ LOG.error("Restart", `Failed: ${e.message}`);
45906
+ return { success: false, error: e.message };
45907
+ }
45908
+ },
45753
45909
  set_machine_nickname: async (_ctx, args) => {
45754
45910
  const nickname = args?.nickname;
45755
45911
  updateConfig({ machineNickname: nickname || null });
@@ -46246,6 +46402,93 @@ init_provider_input_support();
46246
46402
  init_hash();
46247
46403
  init_transcript_evidence();
46248
46404
 
46405
+ // src/providers/transcript-signal-source.ts
46406
+ init_logger();
46407
+ init_signal_envelope();
46408
+ var TranscriptSignalSource = class {
46409
+ constructor(opts) {
46410
+ this.opts = opts;
46411
+ }
46412
+ /** msgCount seen at the previous update — drives in_turn_progress. */
46413
+ prevMsgCount = -1;
46414
+ /** Fingerprint of the last LOGGED snapshot, so the shadow log fires on
46415
+ * change only (a quiet session must not spam one line per read). */
46416
+ lastLoggedFingerprint = "";
46417
+ /**
46418
+ * Normalize one daemon read into a SignalSnapshot and emit the Stage-0
46419
+ * shadow log when the observation changed. Pure w.r.t. the outside world:
46420
+ * the only side effect is the (change-gated) log line. Returns the
46421
+ * snapshot the caller should inject into the FSM driver.
46422
+ */
46423
+ update(sample, now = Date.now()) {
46424
+ const snapshot = this.buildSnapshot(sample, now);
46425
+ this.logOnChange(snapshot);
46426
+ return snapshot;
46427
+ }
46428
+ /** Pure normalization — separated from update() so tests can assert the
46429
+ * envelope without touching the log path. */
46430
+ buildSnapshot(sample, now) {
46431
+ const profileRef = { class: this.opts.profile.class, timing: this.opts.profile.timing };
46432
+ if (this.opts.profile.class !== "native-source") {
46433
+ return unavailableSignalSnapshot(now, "no_native_source", profileRef);
46434
+ }
46435
+ if (sample.error) {
46436
+ return unavailableSignalSnapshot(now, "error", profileRef);
46437
+ }
46438
+ const probe = sample.probe;
46439
+ if (!probe || !Array.isArray(sample.messages)) {
46440
+ return unavailableSignalSnapshot(now, "unresolved", profileRef);
46441
+ }
46442
+ const msgCount = typeof probe.msgCount === "number" && Number.isFinite(probe.msgCount) ? probe.msgCount : sample.messages.length;
46443
+ const sourceMtimeMs = typeof probe.sourceMtimeMs === "number" && Number.isFinite(probe.sourceMtimeMs) ? probe.sourceMtimeMs : 0;
46444
+ const ageMs = sourceMtimeMs > 0 ? Math.max(0, now - sourceMtimeMs) : null;
46445
+ let turnStartedAt;
46446
+ try {
46447
+ turnStartedAt = this.opts.turnStartedAt?.();
46448
+ } catch {
46449
+ turnStartedAt = void 0;
46450
+ }
46451
+ let finalAssistant = null;
46452
+ try {
46453
+ finalAssistant = this.opts.finalAssistantPresent(sample.messages, turnStartedAt);
46454
+ } catch {
46455
+ finalAssistant = null;
46456
+ }
46457
+ const countAdvanced = this.prevMsgCount >= 0 && msgCount > this.prevMsgCount;
46458
+ const fresh = ageMs !== null && ageMs < this.opts.growthQuietMs;
46459
+ const inTurnProgress = countAdvanced || fresh;
46460
+ const transcriptGrowing = ageMs === null ? null : fresh;
46461
+ this.prevMsgCount = msgCount;
46462
+ return {
46463
+ kind: SIGNAL_SNAPSHOT_KIND,
46464
+ sampledAt: now,
46465
+ available: true,
46466
+ profile: profileRef,
46467
+ signals: {
46468
+ final_assistant_present: finalAssistant,
46469
+ in_turn_progress: inTurnProgress,
46470
+ transcript_growing: transcriptGrowing
46471
+ },
46472
+ detail: { msgCount, sourceMtimeMs, ageMs }
46473
+ };
46474
+ }
46475
+ /** Stage-0 shadow log: emit one line when the normalized observation
46476
+ * CHANGES. This log is the Stage 1-3 judgment input ("which signals were
46477
+ * actually observable, and when"), so it carries the full signal set —
46478
+ * but only on change, so a steady-state session stays quiet. */
46479
+ logOnChange(snapshot) {
46480
+ const s2 = snapshot.signals;
46481
+ const fingerprint = snapshot.available ? `1|fa=${s2.final_assistant_present}|tp=${s2.in_turn_progress}|tg=${s2.transcript_growing}|n=${snapshot.detail.msgCount}` : `0|${snapshot.unavailableReason ?? "unknown"}`;
46482
+ if (fingerprint === this.lastLoggedFingerprint) return;
46483
+ this.lastLoggedFingerprint = fingerprint;
46484
+ const age = snapshot.detail.ageMs === null ? "n/a" : `${snapshot.detail.ageMs}ms`;
46485
+ LOG.info(
46486
+ "TranscriptSignalSource",
46487
+ `[${this.opts.label}] [shadow] signals available=${snapshot.available}` + (snapshot.available ? ` final_assistant_present=${s2.final_assistant_present} in_turn_progress=${s2.in_turn_progress} transcript_growing=${s2.transcript_growing} msgCount=${snapshot.detail.msgCount} age=${age}` : ` reason=${snapshot.unavailableReason ?? "unknown"}`)
46488
+ );
46489
+ }
46490
+ };
46491
+
46249
46492
  // src/providers/spec/route.ts
46250
46493
  var fs22 = __toESM(require("fs"));
46251
46494
  var path26 = __toESM(require("path"));
@@ -46610,6 +46853,15 @@ var FsmDriver = class {
46610
46853
  * transition — the rich pre-transition table that lastFsmEval only keeps
46611
46854
  * for the single most recent evaluation. Separate from stateHistory. */
46612
46855
  fsmSnapshotHistory = [];
46856
+ /** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
46857
+ * Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
46858
+ * the Stage-0 pass-through in the evaluator means it can never alter
46859
+ * which transition fires. */
46860
+ signalObservation = null;
46861
+ /** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
46862
+ * the shadow verdict currently disagrees with the real one), so the
46863
+ * shadow log emits on FLIP only, not every frame. */
46864
+ shadowDivergenceLast = /* @__PURE__ */ new Map();
46613
46865
  subscribe(listener) {
46614
46866
  this.listeners.add(listener);
46615
46867
  return () => {
@@ -46682,6 +46934,16 @@ var FsmDriver = class {
46682
46934
  updateMeta(meta, replace = false) {
46683
46935
  this.adapter.updateMeta(meta, replace);
46684
46936
  }
46937
+ /**
46938
+ * TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
46939
+ * observation. The driver stores it verbatim — it does NOT poll, parse,
46940
+ * or read anything itself (generic-PTY-engine boundary). The observation
46941
+ * only feeds the shadow verdict of `signal` conditions; the evaluator's
46942
+ * Stage-0 pass-through guarantees it cannot change a transition.
46943
+ */
46944
+ setSignalObservation(snapshot) {
46945
+ this.signalObservation = snapshot ?? null;
46946
+ }
46685
46947
  snapshot() {
46686
46948
  return this.adapter.snapshot();
46687
46949
  }
@@ -46872,7 +47134,35 @@ var FsmDriver = class {
46872
47134
  }
46873
47135
  evalFsmNow(screen, cursor, now) {
46874
47136
  const prev = this.prevScreenLines.length > 0 ? this.prevScreenLines : void 0;
46875
- return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now));
47137
+ return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now), this.signalObservation);
47138
+ }
47139
+ /**
47140
+ * TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
47141
+ * counterfactual verdict against the real (PTY-only) one and log on FLIP.
47142
+ * This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
47143
+ * evidence for whether signal gating would have changed any verdict, and
47144
+ * in which direction. Read-only: runs after the evaluation is complete
47145
+ * and never feeds back into it.
47146
+ */
47147
+ logShadowDivergence(ev) {
47148
+ for (const t of ev.transitions) {
47149
+ if (!t.shadow) continue;
47150
+ const key2 = `${this.currentStateId}\u2192${t.to}`;
47151
+ const diverges = t.shadow.fires !== t.fires;
47152
+ if ((this.shadowDivergenceLast.get(key2) ?? false) === diverges) continue;
47153
+ this.shadowDivergenceLast.set(key2, diverges);
47154
+ if (diverges) {
47155
+ LOG.info(
47156
+ "FsmDriver",
47157
+ `[${this.specTag()}] [shadow] ${key2} (${t.label}): real fires=${t.fires} but signal-gated verdict would be fires=${t.shadow.fires} (shadowCond=${t.shadow.condResult}${t.shadow.unknown ? ", signal unknown/fail-open" : ""})`
47158
+ );
47159
+ } else {
47160
+ LOG.info(
47161
+ "FsmDriver",
47162
+ `[${this.specTag()}] [shadow] ${key2} (${t.label}): shadow verdict realigned with real fires=${t.fires}`
47163
+ );
47164
+ }
47165
+ }
46876
47166
  }
46877
47167
  reevaluate(forceEmit = false) {
46878
47168
  const now = Date.now();
@@ -46883,6 +47173,7 @@ var FsmDriver = class {
46883
47173
  const ev = this.evalFsmNow(screen, cursor, now);
46884
47174
  this.lastFsmEval = ev;
46885
47175
  this.prevScreenLines = currentLines;
47176
+ this.logShadowDivergence(ev);
46886
47177
  if (ev.fired) {
46887
47178
  this.commitTransition(ev.fired, now, ev);
46888
47179
  this.emitStateChanged(forceEmit);
@@ -49531,6 +49822,18 @@ var SpecCliAdapter = class _SpecCliAdapter {
49531
49822
  }
49532
49823
  refreshProviderDefinition() {
49533
49824
  }
49825
+ /**
49826
+ * TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
49827
+ * observation into the FSM driver. Observation-only — failures here must
49828
+ * never break the adapter, and the driver treats the envelope as a pure
49829
+ * injected value (no reads cross the engine boundary).
49830
+ */
49831
+ setSignalObservation(snapshot) {
49832
+ try {
49833
+ this.driver.setSignalObservation?.(snapshot);
49834
+ } catch {
49835
+ }
49836
+ }
49534
49837
  handleEvent(ev) {
49535
49838
  this.lastEvent = ev;
49536
49839
  switch (ev.kind) {
@@ -51385,6 +51688,10 @@ var CliProviderInstance = class _CliProviderInstance {
51385
51688
  completedDebounceTimer = null;
51386
51689
  completedDebouncePending = null;
51387
51690
  lastExternalCompletionProbe = null;
51691
+ /** TX-FSM Stage 0 (shadow): lazily-created transcript signal normalizer.
51692
+ * Fed ONLY by transcript reads this instance already performs — it adds
51693
+ * zero I/O and its output never feeds back into any verdict. */
51694
+ transcriptSignalSource = null;
51388
51695
  /**
51389
51696
  * The final assistant summary of the last completed turn, cached at
51390
51697
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -51550,6 +51857,7 @@ var CliProviderInstance = class _CliProviderInstance {
51550
51857
  });
51551
51858
  if (restoredHistory.source !== "provider-native") {
51552
51859
  this.lastExternalCompletionProbe = null;
51860
+ this.publishTranscriptSignalObservation(null);
51553
51861
  return null;
51554
51862
  }
51555
51863
  this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
@@ -51557,8 +51865,48 @@ var CliProviderInstance = class _CliProviderInstance {
51557
51865
  restoredHistory.sourcePath,
51558
51866
  restoredHistory.sourceMtimeMs
51559
51867
  );
51868
+ this.publishTranscriptSignalObservation(restoredHistory.messages);
51560
51869
  return restoredHistory.messages;
51561
51870
  }
51871
+ /**
51872
+ * TX-FSM Stage 0 (shadow): normalize the transcript read that JUST
51873
+ * happened into a SignalSnapshot and inject it into the FSM driver as a
51874
+ * pure observation (daemon → SpecCliAdapter → FsmDriver). Fed ONLY by
51875
+ * reads this method's caller already performs — it adds zero I/O, so the
51876
+ * getState() zero-native-read invariant and the stall-path read cadence
51877
+ * are untouched. The snapshot is shadow data: nothing on this path feeds
51878
+ * back into completion/stall/redrive verdicts. Fail-open end to end — a
51879
+ * missing adapter hook (non-spec providers), an unresolved transcript, or
51880
+ * any throw degrades to "no observation", never to a wedge.
51881
+ */
51882
+ publishTranscriptSignalObservation(messages, error = false) {
51883
+ const adapter = this.adapter;
51884
+ if (typeof adapter?.setSignalObservation !== "function") return;
51885
+ try {
51886
+ if (!this.transcriptSignalSource) {
51887
+ this.transcriptSignalSource = new TranscriptSignalSource({
51888
+ label: this.type,
51889
+ // Choke point: class/timing come from the P0 profile
51890
+ // resolver, never from raw predicates or provider names.
51891
+ profile: resolveTranscriptAuthorityProfile(this.provider),
51892
+ turnStartedAt: () => {
51893
+ const t = this.adapter?.currentTurnStartedAt;
51894
+ return typeof t === "number" && Number.isFinite(t) ? t : void 0;
51895
+ },
51896
+ // Reuse the exact completion machinery (I1) for the
51897
+ // final_assistant_present signal rather than duplicating
51898
+ // the message scan.
51899
+ finalAssistantPresent: (msgs, ts2) => this.completionHasFinalAssistantMessage(msgs, ts2),
51900
+ growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS
51901
+ });
51902
+ }
51903
+ const snapshot = this.transcriptSignalSource.update(
51904
+ { messages, probe: this.lastExternalCompletionProbe, error }
51905
+ );
51906
+ adapter.setSignalObservation(snapshot);
51907
+ } catch {
51908
+ }
51909
+ }
51562
51910
  /**
51563
51911
  * The content of the LAST visible assistant bubble in a message list, or ''
51564
51912
  * when the tail is not an assistant reply. Skips trailing system/tool/activity
@@ -63028,17 +63376,134 @@ var fastForwardHandlers = {
63028
63376
 
63029
63377
  // src/commands/med-family/mesh-restart.ts
63030
63378
  init_dist();
63379
+ init_logger();
63031
63380
  var RESTART_BLOCKING_STATES = /* @__PURE__ */ new Set(["generating", "waiting_approval", "starting"]);
63032
- function hasBlockingSessions(ctx) {
63381
+ var DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
63382
+ var DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
63383
+ var DEFERRED_RESTART_POLL_MS = 1e4;
63384
+ function collectBlockingSessions(ctx, meshId) {
63385
+ const blocking = [];
63033
63386
  const states = ctx.deps.instanceManager.collectAllStates();
63387
+ const consider = (state) => {
63388
+ const status = String(state?.status || "");
63389
+ if (!RESTART_BLOCKING_STATES.has(status)) return;
63390
+ blocking.push({
63391
+ instanceId: typeof state?.instanceId === "string" ? state.instanceId : null,
63392
+ status,
63393
+ // The coordinator marker is stamped by mesh_coordinator_launch and
63394
+ // re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
63395
+ // coordinator sessions have no marker and are NOT waived by selfOnly.
63396
+ selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId
63397
+ });
63398
+ };
63034
63399
  for (const state of states) {
63035
- if (RESTART_BLOCKING_STATES.has(String(state.status || ""))) return true;
63400
+ consider(state);
63036
63401
  const childStates = "extensions" in state && Array.isArray(state.extensions) ? state.extensions : [];
63037
- for (const child of childStates) {
63038
- if (RESTART_BLOCKING_STATES.has(String(child?.status || ""))) return true;
63039
- }
63402
+ for (const child of childStates) consider(child);
63403
+ }
63404
+ return blocking;
63405
+ }
63406
+ var pendingDeferredRestart = null;
63407
+ function deferredRestartInfo() {
63408
+ if (!pendingDeferredRestart) return null;
63409
+ return {
63410
+ meshId: pendingDeferredRestart.meshId,
63411
+ nodeId: pendingDeferredRestart.nodeId,
63412
+ mode: pendingDeferredRestart.mode,
63413
+ killSessionHost: pendingDeferredRestart.killSessionHost,
63414
+ scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
63415
+ expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
63416
+ runCondition: "executes automatically once no session is generating / waiting_approval / starting"
63417
+ };
63418
+ }
63419
+ function clearPendingDeferredRestart() {
63420
+ if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
63421
+ pendingDeferredRestart = null;
63422
+ }
63423
+ function normalizeRestartMode(value) {
63424
+ return value === "restart" ? "restart" : "upgrade";
63425
+ }
63426
+ function restartWarnings(args) {
63427
+ const warnings = [];
63428
+ if (args.forced) {
63429
+ warnings.push("forced restart over active sessions \u2014 in-flight turns were interrupted and the in-memory pendingOutboundQueue is permanently lost (no persistence/restore path).");
63430
+ }
63431
+ if (args.killSessionHost) {
63432
+ warnings.push("killSessionHost: the session-host process was stopped \u2014 ALL hosted CLI sessions on this machine are destroyed (hard refresh).");
63433
+ }
63434
+ if (process.platform === "win32") {
63435
+ warnings.push("Windows: the daemon restart/upgrade path stops the session-host regardless of options \u2014 all hosted sessions terminate.");
63436
+ } else {
63437
+ warnings.push("POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).");
63438
+ }
63439
+ return warnings;
63440
+ }
63441
+ async function executeRestart(ctx, args, opts) {
63442
+ const mode = normalizeRestartMode(args?.mode);
63443
+ const killSessionHost = args?.killSessionHost === true;
63444
+ const result = mode === "restart" ? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost }) : await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
63445
+ const restarting = result?.restarting === true;
63446
+ return {
63447
+ ...result,
63448
+ mode,
63449
+ restarted: restarting,
63450
+ ...restarting ? {
63451
+ warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
63452
+ // MCP IPC has no automatic reconnect/retry — tell the caller when to
63453
+ // talk to this daemon again.
63454
+ clientHint: "daemon restarting \u2014 retry your next call in ~10s"
63455
+ } : {}
63456
+ };
63457
+ }
63458
+ function scheduleDeferredRestart(ctx, args, meshId, nodeId) {
63459
+ clearPendingDeferredRestart();
63460
+ const requestedTimeout = typeof args?.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? args.timeoutMs : DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
63461
+ const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
63462
+ const record = {
63463
+ meshId,
63464
+ nodeId,
63465
+ mode: normalizeRestartMode(args?.mode),
63466
+ killSessionHost: args?.killSessionHost === true,
63467
+ scheduledAt: Date.now(),
63468
+ expiresAt: Date.now() + timeoutMs,
63469
+ timer: setInterval(() => {
63470
+ void deferredRestartTick(ctx);
63471
+ }, DEFERRED_RESTART_POLL_MS)
63472
+ };
63473
+ record.timer.unref?.();
63474
+ pendingDeferredRestart = record;
63475
+ LOG.info("MeshRestart", `Deferred restart scheduled for node ${nodeId} (mode=${record.mode}, expires in ${Math.round(timeoutMs / 6e4)}min)`);
63476
+ return {
63477
+ success: true,
63478
+ restarted: false,
63479
+ scheduled: true,
63480
+ code: "restart_scheduled_when_idle",
63481
+ reason: "Restart scheduled \u2014 it will execute automatically as soon as no session on this daemon is generating / waiting_approval / starting. Running at an idle point also avoids pendingOutboundQueue loss, making this the safest restart path.",
63482
+ deferredRestart: deferredRestartInfo()
63483
+ };
63484
+ }
63485
+ async function deferredRestartTick(ctx) {
63486
+ const record = pendingDeferredRestart;
63487
+ if (!record) return;
63488
+ if (Date.now() >= record.expiresAt) {
63489
+ LOG.warn("MeshRestart", `Deferred restart for node ${record.nodeId} expired without reaching an idle point; dropping the schedule`);
63490
+ clearPendingDeferredRestart();
63491
+ return;
63492
+ }
63493
+ const blocking = collectBlockingSessions(ctx, record.meshId);
63494
+ if (blocking.length > 0) return;
63495
+ LOG.info("MeshRestart", `Deferred restart for node ${record.nodeId} executing \u2014 daemon is idle`);
63496
+ clearPendingDeferredRestart();
63497
+ try {
63498
+ await executeRestart(ctx, {
63499
+ meshId: record.meshId,
63500
+ nodeId: record.nodeId,
63501
+ mode: record.mode,
63502
+ killSessionHost: record.killSessionHost
63503
+ }, { forced: false });
63504
+ } catch (e) {
63505
+ LOG.error("MeshRestart", `Deferred restart execution failed: ${e?.message || String(e)}`);
63040
63506
  }
63041
- return false;
63042
63507
  }
63043
63508
  var meshRestartHandlers = {
63044
63509
  restart_daemon_node: async (ctx, args) => {
@@ -63059,16 +63524,43 @@ var meshRestartHandlers = {
63059
63524
  });
63060
63525
  return forwarded ?? { success: false, error: "no response from remote node" };
63061
63526
  }
63062
- if (hasBlockingSessions(ctx)) {
63527
+ if (args?.cancelWhenIdle === true) {
63528
+ const had = pendingDeferredRestart !== null;
63529
+ clearPendingDeferredRestart();
63530
+ return { success: true, restarted: false, cancelled: had, deferredRestart: null };
63531
+ }
63532
+ if (args?.whenIdleStatus === true) {
63533
+ return { success: true, restarted: false, deferredRestart: deferredRestartInfo() };
63534
+ }
63535
+ const blocking = collectBlockingSessions(ctx, meshId);
63536
+ if (blocking.length > 0) {
63537
+ if (args?.force === true) {
63538
+ LOG.warn("MeshRestart", `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || "?"}(${b.status})`).join(", ")} \u2014 pendingOutboundQueue will be lost`);
63539
+ return executeRestart(ctx, args, { forced: true });
63540
+ }
63541
+ const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
63542
+ if (args?.selfOnly === true && foreignBlocking.length === 0) {
63543
+ LOG.info("MeshRestart", `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
63544
+ return executeRestart(ctx, args, { forced: false });
63545
+ }
63546
+ if (args?.whenIdle === true) {
63547
+ return scheduleDeferredRestart(ctx, args, meshId, nodeId);
63548
+ }
63063
63549
  return {
63064
63550
  success: false,
63065
63551
  restarted: false,
63066
63552
  code: "blocking_sessions",
63067
- reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle."
63553
+ reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.",
63554
+ blockingSessions: blocking,
63555
+ options: {
63556
+ selfOnly: "waive only this mesh's own coordinator session (settings.meshCoordinatorFor === meshId)",
63557
+ force: "bypass the idle-gate entirely \u2014 kills in-flight turns and loses the unpersisted pendingOutboundQueue",
63558
+ whenIdle: "schedule the restart to run automatically once the daemon goes idle (safest)"
63559
+ },
63560
+ deferredRestart: deferredRestartInfo()
63068
63561
  };
63069
63562
  }
63070
- const result = await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
63071
- return { ...result, restarted: result?.restarting === true };
63563
+ return executeRestart(ctx, args, { forced: false });
63072
63564
  }
63073
63565
  };
63074
63566