@adhdev/daemon-standalone 1.0.28-rc.10 → 1.0.28-rc.12

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
@@ -33262,10 +33262,10 @@ var require_dist3 = __commonJS({
33262
33262
  }
33263
33263
  function getDaemonBuildInfo() {
33264
33264
  if (cached2) return cached2;
33265
- const commit = readInjected(true ? "e4b8cfd97ab9fbe458b61198b091a9051da87a6e" : void 0) ?? "unknown";
33266
- const commitShort = readInjected(true ? "e4b8cfd9" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
33267
- const version2 = readInjected(true ? "1.0.28-rc.10" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
33268
- const builtAt = readInjected(true ? "2026-07-26T03:08:59.459Z" : void 0);
33265
+ const commit = readInjected(true ? "6f38d1460a02018ef53a3ecdf306004005b44310" : void 0) ?? "unknown";
33266
+ const commitShort = readInjected(true ? "6f38d146" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
33267
+ const version2 = readInjected(true ? "1.0.28-rc.12" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
33268
+ const builtAt = readInjected(true ? "2026-07-26T08:24:40.824Z" : void 0);
33269
33269
  cached2 = builtAt ? { commit, commitShort, version: version2, builtAt } : { commit, commitShort, version: version2 };
33270
33270
  return cached2;
33271
33271
  }
@@ -59929,6 +59929,11 @@ ${cleanBody}`;
59929
59929
  return errs;
59930
59930
  }
59931
59931
  if ("cursor_above" in w && "changed" in w) return errs;
59932
+ if ("signal" in w) {
59933
+ if (typeof w.signal !== "string" || !w.signal.trim()) errs.push(`${path46}.signal must be a non-empty string`);
59934
+ if (w.equals !== void 0 && typeof w.equals !== "boolean") errs.push(`${path46}.equals must be a boolean`);
59935
+ return errs;
59936
+ }
59932
59937
  if ("elapsed_ms" in w) {
59933
59938
  if (typeof w.elapsed_ms !== "number") errs.push(`${path46}.elapsed_ms must be a number`);
59934
59939
  return errs;
@@ -60242,6 +60247,49 @@ ${cleanBody}`;
60242
60247
  "use strict";
60243
60248
  }
60244
60249
  });
60250
+ function unavailableSignalSnapshot(now, reason, profile) {
60251
+ return {
60252
+ kind: SIGNAL_SNAPSHOT_KIND,
60253
+ sampledAt: now,
60254
+ available: false,
60255
+ unavailableReason: reason,
60256
+ ...profile ? { profile } : {},
60257
+ signals: { final_assistant_present: null, in_turn_progress: null, transcript_growing: null },
60258
+ detail: { msgCount: 0, sourceMtimeMs: 0, ageMs: null }
60259
+ };
60260
+ }
60261
+ function evaluateSignalLeaf(cond, snapshot) {
60262
+ const expected = cond.equals ?? true;
60263
+ if (!snapshot || !snapshot.available) {
60264
+ const why = !snapshot ? "no snapshot" : `unavailable(${snapshot.unavailableReason ?? "unknown"})`;
60265
+ return {
60266
+ value: null,
60267
+ shadowResult: null,
60268
+ detail: `signal ${cond.signal} ${why} \u2192 fail-open`
60269
+ };
60270
+ }
60271
+ const raw = Object.prototype.hasOwnProperty.call(snapshot.signals, cond.signal) ? snapshot.signals[cond.signal] : null;
60272
+ if (raw === null || raw === void 0) {
60273
+ return {
60274
+ value: null,
60275
+ shadowResult: null,
60276
+ detail: `signal ${cond.signal} unknown \u2192 fail-open`
60277
+ };
60278
+ }
60279
+ const shadowResult = raw === expected;
60280
+ return {
60281
+ value: raw,
60282
+ shadowResult,
60283
+ detail: `signal ${cond.signal}=${raw} expected=${expected} \u2192 ${shadowResult}`
60284
+ };
60285
+ }
60286
+ var SIGNAL_SNAPSHOT_KIND;
60287
+ var init_signal_envelope = __esm2({
60288
+ "src/providers/spec/signal-envelope.ts"() {
60289
+ "use strict";
60290
+ SIGNAL_SNAPSHOT_KIND = "adhdev:fsm/signal-snapshot@0";
60291
+ }
60292
+ });
60245
60293
  var fsm_evaluator_exports = {};
60246
60294
  __export2(fsm_evaluator_exports, {
60247
60295
  evaluateConditionPreview: () => evaluateConditionPreview,
@@ -60274,24 +60322,74 @@ ${cleanBody}`;
60274
60322
  function isNot(c) {
60275
60323
  return "not" in c;
60276
60324
  }
60277
- function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId) {
60325
+ function isSignal(c) {
60326
+ return "signal" in c;
60327
+ }
60328
+ function containsSignal(c) {
60329
+ if (isSignal(c)) return true;
60330
+ if (isAll(c)) return c.all.some(containsSignal);
60331
+ if (isAny(c)) return c.any.some(containsSignal);
60332
+ if (isNot(c)) return containsSignal(c.not);
60333
+ return false;
60334
+ }
60335
+ function foldShadow(cond) {
60336
+ if (cond.kind === "signal") {
60337
+ const s2 = cond.signal;
60338
+ if (s2 && s2.shadowResult !== null && s2.shadowResult !== void 0) {
60339
+ return { value: s2.shadowResult, unknown: false };
60340
+ }
60341
+ return { value: true, unknown: true };
60342
+ }
60343
+ if (cond.kind === "not" && cond.children?.length) {
60344
+ const c = foldShadow(cond.children[0]);
60345
+ return { value: !c.value, unknown: c.unknown };
60346
+ }
60347
+ if (cond.kind === "all" && cond.children) {
60348
+ const kids = cond.children.map(foldShadow);
60349
+ if (kids.some((k) => !k.unknown && !k.value)) return { value: false, unknown: false };
60350
+ if (kids.some((k) => k.unknown)) return { value: true, unknown: true };
60351
+ return { value: true, unknown: false };
60352
+ }
60353
+ if (cond.kind === "any" && cond.children) {
60354
+ const kids = cond.children.map(foldShadow);
60355
+ if (kids.some((k) => !k.unknown && k.value)) return { value: true, unknown: false };
60356
+ if (kids.some((k) => k.unknown)) return { value: false, unknown: true };
60357
+ return { value: false, unknown: false };
60358
+ }
60359
+ return { value: cond.result, unknown: false };
60360
+ }
60361
+ function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot) {
60278
60362
  if (isAll(cond)) {
60279
- const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
60363
+ const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
60280
60364
  const result = children.every((c) => c.result);
60281
60365
  const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
60282
60366
  return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
60283
60367
  }
60284
60368
  if (isAny(cond)) {
60285
- const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
60369
+ const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
60286
60370
  const result = children.some((c) => c.result);
60287
60371
  const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
60288
60372
  const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
60289
60373
  return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
60290
60374
  }
60291
60375
  if (isNot(cond)) {
60292
- const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
60376
+ const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot);
60293
60377
  return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
60294
60378
  }
60379
+ if (isSignal(cond)) {
60380
+ const leaf = evaluateSignalLeaf(cond, signalSnapshot);
60381
+ return {
60382
+ kind: "signal",
60383
+ result: true,
60384
+ detail: `${leaf.detail} (stage0 shadow \u2014 pass-through)`,
60385
+ signal: {
60386
+ name: cond.signal,
60387
+ available: !!signalSnapshot?.available,
60388
+ value: leaf.value,
60389
+ shadowResult: leaf.shadowResult
60390
+ }
60391
+ };
60392
+ }
60295
60393
  if (isElapsed(cond)) {
60296
60394
  const age = clock.now - clock.stateEnteredAt;
60297
60395
  const result = age >= cond.elapsed_ms;
@@ -60330,7 +60428,7 @@ ${cleanBody}`;
60330
60428
  const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
60331
60429
  return t.label ?? `${from}\u2192${t.to}`;
60332
60430
  }
60333
- function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
60431
+ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock, signalSnapshot) {
60334
60432
  const legacyTrace = [];
60335
60433
  const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
60336
60434
  const cleanScreen = lines.join("\n");
@@ -60346,7 +60444,7 @@ ${cleanBody}`;
60346
60444
  let cond;
60347
60445
  let condResult = true;
60348
60446
  if (t.when) {
60349
- cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`);
60447
+ cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`, signalSnapshot);
60350
60448
  condResult = cond.result;
60351
60449
  }
60352
60450
  const fires = holdSatisfied && condResult;
@@ -60361,6 +60459,14 @@ ${cleanBody}`;
60361
60459
  fires,
60362
60460
  priority: t.priority ?? 0
60363
60461
  };
60462
+ if (t.when && containsSignal(t.when) && cond) {
60463
+ const folded = foldShadow(cond);
60464
+ te.shadow = {
60465
+ condResult: folded.value,
60466
+ fires: holdSatisfied && folded.value,
60467
+ unknown: folded.unknown
60468
+ };
60469
+ }
60364
60470
  transitions.push(te);
60365
60471
  if (fires && !fired) fired = te;
60366
60472
  }
@@ -60383,6 +60489,7 @@ ${cleanBody}`;
60383
60489
  "use strict";
60384
60490
  init_evaluator();
60385
60491
  init_fsm_types();
60492
+ init_signal_envelope();
60386
60493
  WHOLE_SCREEN = -1;
60387
60494
  }
60388
60495
  });
@@ -78044,12 +78151,21 @@ ${body}
78044
78151
  appendUpgradeLog(`Waiting for parent pid ${payload.parentPid} to exit`);
78045
78152
  await waitForPidExit(payload.parentPid, 15e3);
78046
78153
  }
78047
- if (process.platform === "win32") {
78154
+ if (process.platform === "win32" || payload.killSessionHost === true) {
78048
78155
  await stopSessionHostProcesses(sessionHostAppName);
78049
78156
  } else {
78050
78157
  appendUpgradeLog("POSIX \u2014 session-host left running (survives upgrade; sessions rebind on next boot)");
78051
78158
  }
78052
78159
  removeDaemonPidFile();
78160
+ if (payload.skipInstall) {
78161
+ appendUpgradeLog("Restart-only mode \u2014 package install skipped, re-spawning daemon");
78162
+ spawnDetachedDaemonRestart(restartArgv, payload.cwd);
78163
+ try {
78164
+ fs15.unlinkSync(getUpgradeFailureNoticePath());
78165
+ } catch {
78166
+ }
78167
+ return;
78168
+ }
78053
78169
  const instanceDir = resolveInstanceDir();
78054
78170
  const windowsInstallerLayout = resolveWindowsInstallerLayout({
78055
78171
  homeDir: os14.homedir(),
@@ -78168,6 +78284,13 @@ ${body}
78168
78284
  cleanupStaleGlobalInstallDirs(payload.packageName, installCommand.surface);
78169
78285
  appendUpgradeLog("Post-install staging cleanup complete");
78170
78286
  }
78287
+ spawnDetachedDaemonRestart(restartArgv, payload.cwd);
78288
+ try {
78289
+ fs15.unlinkSync(getUpgradeFailureNoticePath());
78290
+ } catch {
78291
+ }
78292
+ }
78293
+ function spawnDetachedDaemonRestart(restartArgv, cwd) {
78171
78294
  if (restartArgv.length > 0) {
78172
78295
  const env2 = { ...process.env };
78173
78296
  delete env2[UPGRADE_HELPER_ENV];
@@ -78176,17 +78299,13 @@ ${body}
78176
78299
  detached: true,
78177
78300
  stdio: "ignore",
78178
78301
  windowsHide: true,
78179
- cwd: payload.cwd || process.cwd(),
78302
+ cwd: cwd || process.cwd(),
78180
78303
  env: env2
78181
78304
  });
78182
78305
  child.unref();
78183
78306
  } else {
78184
78307
  appendUpgradeLog("No restart argv provided; upgrade completed without restart");
78185
78308
  }
78186
- try {
78187
- fs15.unlinkSync(getUpgradeFailureNoticePath());
78188
- } catch {
78189
- }
78190
78309
  }
78191
78310
  async function maybeRunDaemonUpgradeHelperFromEnv2() {
78192
78311
  const raw = process.env[UPGRADE_HELPER_ENV];
@@ -78277,6 +78396,41 @@ ${body}
78277
78396
  return { success: false, error: e.message };
78278
78397
  }
78279
78398
  },
78399
+ // Restart-only path (mesh restart_daemon_node mode="restart"): re-spawn the
78400
+ // daemon WITHOUT the npm reinstall daemon_upgrade performs. Used to reset
78401
+ // daemon state (memory leaks, zombie sessions, wedged internals) when the
78402
+ // version is already correct — downtime drops to the detached re-spawn.
78403
+ // killSessionHost is an explicit opt-in hard refresh (see upgrade-helper).
78404
+ daemon_restart: async (ctx, args) => {
78405
+ LOG2.info("Restart", "Restart-only requested (no package reinstall)");
78406
+ try {
78407
+ const isStandalone = ctx.deps.packageName === "@adhdev/daemon-standalone" || process.argv[1]?.includes("daemon-standalone");
78408
+ const pkgName = isStandalone ? "@adhdev/daemon-standalone" : "adhdev";
78409
+ const killSessionHost = args?.killSessionHost === true;
78410
+ if (killSessionHost) {
78411
+ LOG2.warn("Restart", "killSessionHost requested \u2014 session-host will be stopped and ALL hosted sessions destroyed");
78412
+ }
78413
+ spawnDetachedDaemonUpgradeHelper({
78414
+ packageName: pkgName,
78415
+ targetVersion: "",
78416
+ parentPid: process.pid,
78417
+ restartArgv: process.argv.slice(1),
78418
+ cwd: process.cwd(),
78419
+ sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || "adhdev",
78420
+ skipInstall: true,
78421
+ killSessionHost
78422
+ });
78423
+ LOG2.info("Restart", "Scheduled detached restart-only helper");
78424
+ setTimeout(() => {
78425
+ LOG2.info("Restart", "Exiting daemon so detached helper can re-spawn it...");
78426
+ process.exit(0);
78427
+ }, 3e3);
78428
+ return { success: true, restarted: true, restarting: true, mode: "restart", killSessionHost };
78429
+ } catch (e) {
78430
+ LOG2.error("Restart", `Failed: ${e.message}`);
78431
+ return { success: false, error: e.message };
78432
+ }
78433
+ },
78280
78434
  set_machine_nickname: async (_ctx, args) => {
78281
78435
  const nickname = args?.nickname;
78282
78436
  updateConfig({ machineNickname: nickname || null });
@@ -78754,6 +78908,91 @@ ${body}
78754
78908
  init_provider_input_support();
78755
78909
  init_hash();
78756
78910
  init_transcript_evidence();
78911
+ init_logger();
78912
+ init_signal_envelope();
78913
+ var TranscriptSignalSource = class {
78914
+ constructor(opts) {
78915
+ this.opts = opts;
78916
+ }
78917
+ /** msgCount seen at the previous update — drives in_turn_progress. */
78918
+ prevMsgCount = -1;
78919
+ /** Fingerprint of the last LOGGED snapshot, so the shadow log fires on
78920
+ * change only (a quiet session must not spam one line per read). */
78921
+ lastLoggedFingerprint = "";
78922
+ /**
78923
+ * Normalize one daemon read into a SignalSnapshot and emit the Stage-0
78924
+ * shadow log when the observation changed. Pure w.r.t. the outside world:
78925
+ * the only side effect is the (change-gated) log line. Returns the
78926
+ * snapshot the caller should inject into the FSM driver.
78927
+ */
78928
+ update(sample, now = Date.now()) {
78929
+ const snapshot = this.buildSnapshot(sample, now);
78930
+ this.logOnChange(snapshot);
78931
+ return snapshot;
78932
+ }
78933
+ /** Pure normalization — separated from update() so tests can assert the
78934
+ * envelope without touching the log path. */
78935
+ buildSnapshot(sample, now) {
78936
+ const profileRef = { class: this.opts.profile.class, timing: this.opts.profile.timing };
78937
+ if (this.opts.profile.class !== "native-source") {
78938
+ return unavailableSignalSnapshot(now, "no_native_source", profileRef);
78939
+ }
78940
+ if (sample.error) {
78941
+ return unavailableSignalSnapshot(now, "error", profileRef);
78942
+ }
78943
+ const probe = sample.probe;
78944
+ if (!probe || !Array.isArray(sample.messages)) {
78945
+ return unavailableSignalSnapshot(now, "unresolved", profileRef);
78946
+ }
78947
+ const msgCount = typeof probe.msgCount === "number" && Number.isFinite(probe.msgCount) ? probe.msgCount : sample.messages.length;
78948
+ const sourceMtimeMs = typeof probe.sourceMtimeMs === "number" && Number.isFinite(probe.sourceMtimeMs) ? probe.sourceMtimeMs : 0;
78949
+ const ageMs = sourceMtimeMs > 0 ? Math.max(0, now - sourceMtimeMs) : null;
78950
+ let turnStartedAt;
78951
+ try {
78952
+ turnStartedAt = this.opts.turnStartedAt?.();
78953
+ } catch {
78954
+ turnStartedAt = void 0;
78955
+ }
78956
+ let finalAssistant = null;
78957
+ try {
78958
+ finalAssistant = this.opts.finalAssistantPresent(sample.messages, turnStartedAt);
78959
+ } catch {
78960
+ finalAssistant = null;
78961
+ }
78962
+ const countAdvanced = this.prevMsgCount >= 0 && msgCount > this.prevMsgCount;
78963
+ const fresh = ageMs !== null && ageMs < this.opts.growthQuietMs;
78964
+ const inTurnProgress = countAdvanced || fresh;
78965
+ const transcriptGrowing = ageMs === null ? null : fresh;
78966
+ this.prevMsgCount = msgCount;
78967
+ return {
78968
+ kind: SIGNAL_SNAPSHOT_KIND,
78969
+ sampledAt: now,
78970
+ available: true,
78971
+ profile: profileRef,
78972
+ signals: {
78973
+ final_assistant_present: finalAssistant,
78974
+ in_turn_progress: inTurnProgress,
78975
+ transcript_growing: transcriptGrowing
78976
+ },
78977
+ detail: { msgCount, sourceMtimeMs, ageMs }
78978
+ };
78979
+ }
78980
+ /** Stage-0 shadow log: emit one line when the normalized observation
78981
+ * CHANGES. This log is the Stage 1-3 judgment input ("which signals were
78982
+ * actually observable, and when"), so it carries the full signal set —
78983
+ * but only on change, so a steady-state session stays quiet. */
78984
+ logOnChange(snapshot) {
78985
+ const s2 = snapshot.signals;
78986
+ 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"}`;
78987
+ if (fingerprint === this.lastLoggedFingerprint) return;
78988
+ this.lastLoggedFingerprint = fingerprint;
78989
+ const age = snapshot.detail.ageMs === null ? "n/a" : `${snapshot.detail.ageMs}ms`;
78990
+ LOG2.info(
78991
+ "TranscriptSignalSource",
78992
+ `[${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"}`)
78993
+ );
78994
+ }
78995
+ };
78757
78996
  var fs222 = __toESM2(require("fs"));
78758
78997
  var path26 = __toESM2(require("path"));
78759
78998
  init_provider_cli_adapter();
@@ -79107,6 +79346,15 @@ ${body}
79107
79346
  * transition — the rich pre-transition table that lastFsmEval only keeps
79108
79347
  * for the single most recent evaluation. Separate from stateHistory. */
79109
79348
  fsmSnapshotHistory = [];
79349
+ /** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
79350
+ * Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
79351
+ * the Stage-0 pass-through in the evaluator means it can never alter
79352
+ * which transition fires. */
79353
+ signalObservation = null;
79354
+ /** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
79355
+ * the shadow verdict currently disagrees with the real one), so the
79356
+ * shadow log emits on FLIP only, not every frame. */
79357
+ shadowDivergenceLast = /* @__PURE__ */ new Map();
79110
79358
  subscribe(listener) {
79111
79359
  this.listeners.add(listener);
79112
79360
  return () => {
@@ -79179,6 +79427,16 @@ ${body}
79179
79427
  updateMeta(meta3, replace = false) {
79180
79428
  this.adapter.updateMeta(meta3, replace);
79181
79429
  }
79430
+ /**
79431
+ * TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
79432
+ * observation. The driver stores it verbatim — it does NOT poll, parse,
79433
+ * or read anything itself (generic-PTY-engine boundary). The observation
79434
+ * only feeds the shadow verdict of `signal` conditions; the evaluator's
79435
+ * Stage-0 pass-through guarantees it cannot change a transition.
79436
+ */
79437
+ setSignalObservation(snapshot) {
79438
+ this.signalObservation = snapshot ?? null;
79439
+ }
79182
79440
  snapshot() {
79183
79441
  return this.adapter.snapshot();
79184
79442
  }
@@ -79369,7 +79627,35 @@ ${body}
79369
79627
  }
79370
79628
  evalFsmNow(screen, cursor, now) {
79371
79629
  const prev = this.prevScreenLines.length > 0 ? this.prevScreenLines : void 0;
79372
- return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now));
79630
+ return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now), this.signalObservation);
79631
+ }
79632
+ /**
79633
+ * TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
79634
+ * counterfactual verdict against the real (PTY-only) one and log on FLIP.
79635
+ * This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
79636
+ * evidence for whether signal gating would have changed any verdict, and
79637
+ * in which direction. Read-only: runs after the evaluation is complete
79638
+ * and never feeds back into it.
79639
+ */
79640
+ logShadowDivergence(ev) {
79641
+ for (const t of ev.transitions) {
79642
+ if (!t.shadow) continue;
79643
+ const key2 = `${this.currentStateId}\u2192${t.to}`;
79644
+ const diverges = t.shadow.fires !== t.fires;
79645
+ if ((this.shadowDivergenceLast.get(key2) ?? false) === diverges) continue;
79646
+ this.shadowDivergenceLast.set(key2, diverges);
79647
+ if (diverges) {
79648
+ LOG2.info(
79649
+ "FsmDriver",
79650
+ `[${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" : ""})`
79651
+ );
79652
+ } else {
79653
+ LOG2.info(
79654
+ "FsmDriver",
79655
+ `[${this.specTag()}] [shadow] ${key2} (${t.label}): shadow verdict realigned with real fires=${t.fires}`
79656
+ );
79657
+ }
79658
+ }
79373
79659
  }
79374
79660
  reevaluate(forceEmit = false) {
79375
79661
  const now = Date.now();
@@ -79380,6 +79666,7 @@ ${body}
79380
79666
  const ev = this.evalFsmNow(screen, cursor, now);
79381
79667
  this.lastFsmEval = ev;
79382
79668
  this.prevScreenLines = currentLines;
79669
+ this.logShadowDivergence(ev);
79383
79670
  if (ev.fired) {
79384
79671
  this.commitTransition(ev.fired, now, ev);
79385
79672
  this.emitStateChanged(forceEmit);
@@ -82016,6 +82303,18 @@ ${body}
82016
82303
  }
82017
82304
  refreshProviderDefinition() {
82018
82305
  }
82306
+ /**
82307
+ * TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
82308
+ * observation into the FSM driver. Observation-only — failures here must
82309
+ * never break the adapter, and the driver treats the envelope as a pure
82310
+ * injected value (no reads cross the engine boundary).
82311
+ */
82312
+ setSignalObservation(snapshot) {
82313
+ try {
82314
+ this.driver.setSignalObservation?.(snapshot);
82315
+ } catch {
82316
+ }
82317
+ }
82019
82318
  handleEvent(ev) {
82020
82319
  this.lastEvent = ev;
82021
82320
  switch (ev.kind) {
@@ -83037,18 +83336,20 @@ ${body}
83037
83336
  // MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
83038
83337
  // anchor re-arms (the per-anchor guard only covers a single continuous stall).
83039
83338
  meshStallLastFiredAt = -1;
83040
- // KIMI-MESH-COMPLETION-EMIT (axis 1): the native-source transcript progress
83041
- // fingerprint (record count + source mtime) observed the LAST time the stall
83042
- // watchdog checked a native-source provider (transcriptAuthority=provider e.g.
83043
- // kimi wire.jsonl). A pure-PTY native-source worker running a long, screen-quiet
83044
- // tool (no viewport bytes for minutes) looks stalled by the lastOutputAt clock
83045
- // even though its transcript file is still growing. Before firing the stall, we
83046
- // compare the current transcript fingerprint against this snapshot; if the
83047
- // transcript advanced, the "stall" is a false positive of PTY-render stasis, so
83048
- // we re-arm the anchor instead of paging the coordinator (whose no_progress
83049
- // handling can lead to the worker being stopped mid-work → completion never
83050
- // emitted). null = not yet sampled for this session/anchor.
83051
- meshStallNativeTranscriptSample = null;
83339
+ // KIMI-MESH-COMPLETION-EMIT (axis 1) TX-FSM Stage 1: whether the stall
83340
+ // watchdog has consumed a usable native-transcript SIGNAL SNAPSHOT for the
83341
+ // current stall episode. A pure-PTY native-source worker running a long,
83342
+ // screen-quiet tool (no viewport bytes for minutes) looks stalled by the
83343
+ // lastOutputAt clock even though its transcript file is still growing.
83344
+ // Before firing the stall, the watchdog consults the shared
83345
+ // TranscriptSignalSource's in_turn_progress signal; if the transcript is
83346
+ // advancing, the "stall" is a false positive of PTY-render stasis, so we
83347
+ // re-arm the anchor instead of paging the coordinator (whose no_progress
83348
+ // handling can lead to the worker being stopped mid-work → completion
83349
+ // never emitted). The FIRST usable sample of an episode still re-arms
83350
+ // unconditionally (no episode-scoped baseline exists yet to prove stasis
83351
+ // against) — the historical `!prev → advanced` semantics, preserved.
83352
+ meshStallTranscriptSignalSampled = false;
83052
83353
  // FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
83053
83354
  // phase (→generating or →waiting_approval). The completedDebouncePending snapshots
83054
83355
  // this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
@@ -83846,6 +84147,17 @@ ${body}
83846
84147
  completedDebounceTimer = null;
83847
84148
  completedDebouncePending = null;
83848
84149
  lastExternalCompletionProbe = null;
84150
+ /** TX-FSM: lazily-created transcript signal normalizer. Fed ONLY by
84151
+ * transcript reads this instance already performs — it adds zero I/O.
84152
+ * Stage 0: its output was a pure shadow observation for the FSM driver.
84153
+ * Stage 1: the instance's own stall/growth-hold judgments consume the
84154
+ * normalized snapshot too (single source of truth). */
84155
+ transcriptSignalSource = null;
84156
+ /** TX-FSM Stage 1: the latest snapshot the source produced (set inside
84157
+ * publishTranscriptSignalObservation). Consumed the same tick by the
84158
+ * stall-path / growth-hold judgments via probeNativeTranscriptSignals —
84159
+ * never treated as fresh across ticks. */
84160
+ lastTranscriptSignalSnapshot = null;
83849
84161
  /**
83850
84162
  * The final assistant summary of the last completed turn, cached at
83851
84163
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -84011,6 +84323,7 @@ ${body}
84011
84323
  });
84012
84324
  if (restoredHistory.source !== "provider-native") {
84013
84325
  this.lastExternalCompletionProbe = null;
84326
+ this.publishTranscriptSignalObservation(null);
84014
84327
  return null;
84015
84328
  }
84016
84329
  this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
@@ -84018,8 +84331,55 @@ ${body}
84018
84331
  restoredHistory.sourcePath,
84019
84332
  restoredHistory.sourceMtimeMs
84020
84333
  );
84334
+ this.publishTranscriptSignalObservation(restoredHistory.messages);
84021
84335
  return restoredHistory.messages;
84022
84336
  }
84337
+ /**
84338
+ * TX-FSM: normalize the transcript read that JUST happened into a
84339
+ * SignalSnapshot. Stage 0 injected it into the FSM driver as a pure
84340
+ * shadow observation (daemon → SpecCliAdapter → FsmDriver); Stage 1
84341
+ * additionally caches it (lastTranscriptSignalSnapshot) so the instance's
84342
+ * OWN stall/growth-hold judgments consume the SAME normalized snapshot
84343
+ * instead of re-running private transcript scans. Fed ONLY by reads this
84344
+ * method's caller already performs — it adds zero I/O, so the getState()
84345
+ * zero-native-read invariant and the stall-path read cadence are
84346
+ * untouched. The source update runs regardless of the adapter hook so a
84347
+ * non-spec provider still produces the instance-side snapshot (the FSM
84348
+ * injection is simply skipped there). Fail-open end to end — an
84349
+ * unresolved transcript or any throw degrades to "no observation", never
84350
+ * to a wedge.
84351
+ */
84352
+ publishTranscriptSignalObservation(messages, error48 = false) {
84353
+ try {
84354
+ if (!this.transcriptSignalSource) {
84355
+ this.transcriptSignalSource = new TranscriptSignalSource({
84356
+ label: this.type,
84357
+ // Choke point: class/timing come from the P0 profile
84358
+ // resolver, never from raw predicates or provider names.
84359
+ profile: resolveTranscriptAuthorityProfile(this.provider),
84360
+ turnStartedAt: () => {
84361
+ const t = this.adapter?.currentTurnStartedAt;
84362
+ if (typeof t === "number" && Number.isFinite(t)) return t;
84363
+ return this.meshTaskInjectedAt > 0 ? this.meshTaskInjectedAt : void 0;
84364
+ },
84365
+ // Reuse the exact completion machinery (I1) for the
84366
+ // final_assistant_present signal rather than duplicating
84367
+ // the message scan.
84368
+ finalAssistantPresent: (msgs, ts2) => this.completionHasFinalAssistantMessage(msgs, ts2),
84369
+ growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS
84370
+ });
84371
+ }
84372
+ const snapshot = this.transcriptSignalSource.update(
84373
+ { messages, probe: this.lastExternalCompletionProbe, error: error48 }
84374
+ );
84375
+ this.lastTranscriptSignalSnapshot = snapshot;
84376
+ const adapter = this.adapter;
84377
+ if (typeof adapter?.setSignalObservation === "function") {
84378
+ adapter.setSignalObservation(snapshot);
84379
+ }
84380
+ } catch {
84381
+ }
84382
+ }
84023
84383
  /**
84024
84384
  * The content of the LAST visible assistant bubble in a message list, or ''
84025
84385
  * when the tail is not an assistant reply. Skips trailing system/tool/activity
@@ -84509,17 +84869,17 @@ ${body}
84509
84869
  const threshold = turnActive ? _CliProviderInstance.MESH_WORKER_STALL_TURN_THRESHOLD_MS : _CliProviderInstance.MESH_WORKER_STALL_IDLE_THRESHOLD_MS;
84510
84870
  const stalledMs = now - this.meshStallAnchorAt;
84511
84871
  if (stalledMs < threshold) return;
84512
- const nativeSample = this.sampleNativeTranscriptProgress();
84513
- if (nativeSample) {
84514
- const prev = this.meshStallNativeTranscriptSample;
84515
- const advanced = !prev || nativeSample.msgCount > prev.msgCount || nativeSample.sourceMtimeMs > prev.sourceMtimeMs;
84516
- this.meshStallNativeTranscriptSample = nativeSample;
84517
- if (advanced) {
84872
+ const transcriptSignals = this.probeNativeTranscriptSignals();
84873
+ if (transcriptSignals?.snapshot?.available === true) {
84874
+ const firstSampleThisEpisode = !this.meshStallTranscriptSignalSampled;
84875
+ this.meshStallTranscriptSignalSampled = true;
84876
+ const signalDetail = transcriptSignals.snapshot.detail;
84877
+ if (firstSampleThisEpisode || transcriptSignals.snapshot.signals.in_turn_progress === true) {
84518
84878
  if (this.isMeshWorkerSession()) {
84519
84879
  traceMeshEventDrop(
84520
84880
  "mesh_worker_stall_transcript_advancing",
84521
84881
  this.meshTraceCtx("monitor:no_progress"),
84522
- `msgCount=${nativeSample.msgCount} sourceMtime=${nativeSample.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1e3)}s but transcript advancing)`
84882
+ `msgCount=${signalDetail.msgCount} sourceMtime=${signalDetail.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1e3)}s but transcript advancing)`
84523
84883
  );
84524
84884
  }
84525
84885
  this.meshStallAnchorAt = now;
@@ -84527,7 +84887,7 @@ ${body}
84527
84887
  return;
84528
84888
  }
84529
84889
  }
84530
- if (this.tryReconcileTranscriptCompletionForStall(observedStatus)) {
84890
+ if (this.tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals)) {
84531
84891
  this.meshStallEmittedForAnchor = true;
84532
84892
  return;
84533
84893
  }
@@ -84567,38 +84927,45 @@ ${body}
84567
84927
  this.meshStallEmittedForAnchor = false;
84568
84928
  this.meshStallTurnActiveLast = void 0;
84569
84929
  this.meshStallLastFiredAt = -1;
84570
- this.meshStallNativeTranscriptSample = null;
84930
+ this.meshStallTranscriptSignalSampled = false;
84571
84931
  }
84932
+ /** The result of one native-transcript signal probe: the normalized
84933
+ * snapshot the shared TranscriptSignalSource produced from the read, and
84934
+ * the very messages it was normalized from (so a judgment site can pull
84935
+ * a payload — e.g. the final summary — from the SAME read with zero
84936
+ * added I/O). */
84572
84937
  /**
84573
- * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
84574
- * (transcriptAuthority=provider its authoritative history is an on-disk
84575
- * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
84576
- * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
84577
- * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
84578
- * worker is still doing long tool work (transcript growing)" from a genuine
84579
- * wedge. Returns null for a pure-PTY provider (no native source) or when the
84580
- * source cannot be resolved this tick — the caller then falls back to the
84581
- * unchanged lastOutputAt-only judgment.
84938
+ * TX-FSM Stage 1 the single native-transcript signal probe (replaces
84939
+ * the Stage-0 sampleNativeTranscriptProgress fingerprint sampler). For a
84940
+ * native-source provider (its authoritative history is an on-disk
84941
+ * transcript file, e.g. kimi's wire.jsonl), perform the read this
84942
+ * judgment point already owns SAME cadence as before, one
84943
+ * readExternalCompletionMessages() per call, never more and return the
84944
+ * NORMALIZED SignalSnapshot the shared TranscriptSignalSource produced
84945
+ * from it (publishTranscriptSignalObservation runs inside the read), plus
84946
+ * the messages that read returned. Judgment sites (the stall watchdog's
84947
+ * transcript-advancing axis, the completion growth-hold, the stall-path
84948
+ * completion rescue) consume the snapshot's SIGNALS instead of running
84949
+ * their own fingerprint/freshness/final-assistant scans — one source of
84950
+ * truth for "what does the transcript say right now".
84582
84951
  *
84583
- * Generalized on the provider's native-source flag, NOT a hardcoded provider
84584
- * type, so every current and future pure-PTY long-tool native-source provider
84585
- * benefits. Cheap enough for the stall path: it runs only at the stall threshold
84586
- * (≥180s of PTY stasis), never on the routine 5s tick.
84952
+ * Class gating goes through resolveTranscriptAuthorityProfile ONLY.
84953
+ * Returns null for a non-native-source class (nothing to signal from) and
84954
+ * a null snapshot when the read threw callers keep their fail-open
84955
+ * fallbacks ("couldn't tell" never blocks an idle verdict and never
84956
+ * fabricates a completion). Cheap enough for the stall path: it runs
84957
+ * only at the stall threshold (≥180s of PTY stasis) or during an armed
84958
+ * completion-debounce retry, never on the routine 5s tick.
84587
84959
  */
84588
- sampleNativeTranscriptProgress() {
84589
- if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return null;
84960
+ probeNativeTranscriptSignals() {
84961
+ if (resolveTranscriptAuthorityProfile(this.provider).class !== "native-source") return null;
84590
84962
  let messages = null;
84591
84963
  try {
84592
84964
  messages = this.readExternalCompletionMessages();
84593
84965
  } catch {
84594
- return null;
84966
+ return { snapshot: null, messages: null };
84595
84967
  }
84596
- const probe = this.lastExternalCompletionProbe;
84597
- if (!probe) return null;
84598
- return {
84599
- msgCount: typeof probe.msgCount === "number" ? probe.msgCount : Array.isArray(messages) ? messages.length : 0,
84600
- sourceMtimeMs: typeof probe.sourceMtimeMs === "number" ? probe.sourceMtimeMs : 0
84601
- };
84968
+ return { snapshot: this.lastTranscriptSignalSnapshot, messages };
84602
84969
  }
84603
84970
  /**
84604
84971
  * KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
@@ -84687,8 +85054,22 @@ ${body}
84687
85054
  * false for a daemon-owned provider and for a genuinely mid-turn / wedged
84688
85055
  * worker with no in-turn final assistant, so a real stall still fires
84689
85056
  * unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
85057
+ *
85058
+ * TX-FSM Stage 1 (FIX 3 probe delegation): for a native-source class the
85059
+ * completion VERDICT is the shared TranscriptSignalSource's
85060
+ * final_assistant_present signal, normalized from the ONE transcript read
85061
+ * the stall watchdog already performed this tick (passed in as
85062
+ * `transcriptSignals`) — not a second private read + scan. The emit
85063
+ * payload (finalSummary) is extracted from the SAME messages the signal
85064
+ * was normalized from, with the SAME turn boundary the legacy path used,
85065
+ * so the extraction re-proves the turn scope the signal checked. Fail-open
85066
+ * both ways: no usable snapshot (read failed / unresolved / a pure-PTY
85067
+ * class, which has no native signal by construction) → the legacy
85068
+ * class-appropriate evidence path (completionFinalSummary), unchanged; and
85069
+ * a present-but-unextractable signal yields false rather than a
85070
+ * payload-less emit.
84690
85071
  */
84691
- tryReconcileTranscriptCompletionForStall(observedStatus) {
85072
+ tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals) {
84692
85073
  const profile = resolveTranscriptAuthorityProfile(this.provider);
84693
85074
  if (profile.class === "daemon-owned") return false;
84694
85075
  if (observedStatus !== "idle") return false;
@@ -84706,10 +85087,19 @@ ${body}
84706
85087
  parsedMessages = void 0;
84707
85088
  }
84708
85089
  let finalSummary;
84709
- try {
84710
- finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
84711
- } catch {
84712
- finalSummary = void 0;
85090
+ const signalSnapshot = transcriptSignals?.snapshot;
85091
+ if (profile.class === "native-source" && signalSnapshot?.available === true) {
85092
+ if (signalSnapshot.signals.final_assistant_present !== true) return false;
85093
+ finalSummary = extractFinalSummaryFromMessagesAfter(
85094
+ Array.isArray(transcriptSignals?.messages) ? transcriptSignals.messages : [],
85095
+ turnStartedAt
85096
+ ) || void 0;
85097
+ } else {
85098
+ try {
85099
+ finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
85100
+ } catch {
85101
+ finalSummary = void 0;
85102
+ }
84713
85103
  }
84714
85104
  if (!finalSummary) return false;
84715
85105
  const diagnosticSource = profile.class === "pure-pty" ? "stall_pure_pty_transcript_completion" : "stall_native_source_transcript_completion";
@@ -84976,24 +85366,25 @@ ${body}
84976
85366
  const isTranscriptEvidenceGate = block2.allowTimeout === true;
84977
85367
  LOG2.debug("CLI", `[${this.type}] finalization block: reason=${blockReason} terminal=${block2.terminal} allowTimeout=${isTranscriptEvidenceGate} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
84978
85368
  if (blockReason === "missing_final_assistant" && block2.noExternalTranscriptSource === true) {
84979
- let nativeSample = null;
85369
+ let growthSnapshot = null;
84980
85370
  try {
84981
- nativeSample = this.sampleNativeTranscriptProgress();
85371
+ growthSnapshot = this.probeNativeTranscriptSignals()?.snapshot ?? null;
84982
85372
  } catch {
84983
- nativeSample = null;
85373
+ growthSnapshot = null;
84984
85374
  }
84985
- const sourceMtimeMs = nativeSample?.sourceMtimeMs ?? 0;
84986
- if (nativeSample && sourceMtimeMs > 0 && Date.now() - sourceMtimeMs < MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS) {
85375
+ if (growthSnapshot?.available === true && growthSnapshot.signals.transcript_growing === true) {
85376
+ const growthDetail = growthSnapshot.detail;
85377
+ const mtimeAgeMs = growthDetail.ageMs ?? 0;
84987
85378
  if (pending.loggedBlockReason !== "native_transcript_advancing") {
84988
- LOG2.info("CLI", `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${nativeSample.msgCount} mtimeAge=${Date.now() - sourceMtimeMs}ms < ${MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS}ms) \u2014 transcript still growing, screen-idle verdict not trusted`);
85379
+ LOG2.info("CLI", `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms < ${MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS}ms) \u2014 transcript still growing, screen-idle verdict not trusted`);
84989
85380
  if (this.isMeshWorkerSession()) {
84990
- traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${nativeSample.msgCount} mtimeAge=${Date.now() - sourceMtimeMs}ms`);
85381
+ traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms`);
84991
85382
  }
84992
85383
  if (this.completionTraceOn()) this.recordCompletionGateTrace("hold", {
84993
85384
  blockReason: "native_transcript_advancing",
84994
85385
  latestVisibleStatus,
84995
- msgCount: nativeSample.msgCount,
84996
- sourceMtimeAgeMs: Date.now() - sourceMtimeMs,
85386
+ msgCount: growthDetail.msgCount,
85387
+ sourceMtimeAgeMs: mtimeAgeMs,
84997
85388
  growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
84998
85389
  waitedMs
84999
85390
  });
@@ -95437,17 +95828,134 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
95437
95828
  }
95438
95829
  };
95439
95830
  init_dist();
95831
+ init_logger();
95440
95832
  var RESTART_BLOCKING_STATES = /* @__PURE__ */ new Set(["generating", "waiting_approval", "starting"]);
95441
- function hasBlockingSessions(ctx) {
95833
+ var DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
95834
+ var DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
95835
+ var DEFERRED_RESTART_POLL_MS = 1e4;
95836
+ function collectBlockingSessions(ctx, meshId) {
95837
+ const blocking = [];
95442
95838
  const states = ctx.deps.instanceManager.collectAllStates();
95839
+ const consider = (state) => {
95840
+ const status = String(state?.status || "");
95841
+ if (!RESTART_BLOCKING_STATES.has(status)) return;
95842
+ blocking.push({
95843
+ instanceId: typeof state?.instanceId === "string" ? state.instanceId : null,
95844
+ status,
95845
+ // The coordinator marker is stamped by mesh_coordinator_launch and
95846
+ // re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
95847
+ // coordinator sessions have no marker and are NOT waived by selfOnly.
95848
+ selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId
95849
+ });
95850
+ };
95443
95851
  for (const state of states) {
95444
- if (RESTART_BLOCKING_STATES.has(String(state.status || ""))) return true;
95852
+ consider(state);
95445
95853
  const childStates = "extensions" in state && Array.isArray(state.extensions) ? state.extensions : [];
95446
- for (const child of childStates) {
95447
- if (RESTART_BLOCKING_STATES.has(String(child?.status || ""))) return true;
95448
- }
95854
+ for (const child of childStates) consider(child);
95855
+ }
95856
+ return blocking;
95857
+ }
95858
+ var pendingDeferredRestart = null;
95859
+ function deferredRestartInfo() {
95860
+ if (!pendingDeferredRestart) return null;
95861
+ return {
95862
+ meshId: pendingDeferredRestart.meshId,
95863
+ nodeId: pendingDeferredRestart.nodeId,
95864
+ mode: pendingDeferredRestart.mode,
95865
+ killSessionHost: pendingDeferredRestart.killSessionHost,
95866
+ scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
95867
+ expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
95868
+ runCondition: "executes automatically once no session is generating / waiting_approval / starting"
95869
+ };
95870
+ }
95871
+ function clearPendingDeferredRestart() {
95872
+ if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
95873
+ pendingDeferredRestart = null;
95874
+ }
95875
+ function normalizeRestartMode(value) {
95876
+ return value === "restart" ? "restart" : "upgrade";
95877
+ }
95878
+ function restartWarnings(args) {
95879
+ const warnings = [];
95880
+ if (args.forced) {
95881
+ 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).");
95882
+ }
95883
+ if (args.killSessionHost) {
95884
+ warnings.push("killSessionHost: the session-host process was stopped \u2014 ALL hosted CLI sessions on this machine are destroyed (hard refresh).");
95885
+ }
95886
+ if (process.platform === "win32") {
95887
+ warnings.push("Windows: the daemon restart/upgrade path stops the session-host regardless of options \u2014 all hosted sessions terminate.");
95888
+ } else {
95889
+ warnings.push("POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).");
95890
+ }
95891
+ return warnings;
95892
+ }
95893
+ async function executeRestart(ctx, args, opts) {
95894
+ const mode = normalizeRestartMode(args?.mode);
95895
+ const killSessionHost = args?.killSessionHost === true;
95896
+ const result = mode === "restart" ? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost }) : await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
95897
+ const restarting = result?.restarting === true;
95898
+ return {
95899
+ ...result,
95900
+ mode,
95901
+ restarted: restarting,
95902
+ ...restarting ? {
95903
+ warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
95904
+ // MCP IPC has no automatic reconnect/retry — tell the caller when to
95905
+ // talk to this daemon again.
95906
+ clientHint: "daemon restarting \u2014 retry your next call in ~10s"
95907
+ } : {}
95908
+ };
95909
+ }
95910
+ function scheduleDeferredRestart(ctx, args, meshId, nodeId) {
95911
+ clearPendingDeferredRestart();
95912
+ const requestedTimeout = typeof args?.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? args.timeoutMs : DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
95913
+ const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
95914
+ const record2 = {
95915
+ meshId,
95916
+ nodeId,
95917
+ mode: normalizeRestartMode(args?.mode),
95918
+ killSessionHost: args?.killSessionHost === true,
95919
+ scheduledAt: Date.now(),
95920
+ expiresAt: Date.now() + timeoutMs,
95921
+ timer: setInterval(() => {
95922
+ void deferredRestartTick(ctx);
95923
+ }, DEFERRED_RESTART_POLL_MS)
95924
+ };
95925
+ record2.timer.unref?.();
95926
+ pendingDeferredRestart = record2;
95927
+ LOG2.info("MeshRestart", `Deferred restart scheduled for node ${nodeId} (mode=${record2.mode}, expires in ${Math.round(timeoutMs / 6e4)}min)`);
95928
+ return {
95929
+ success: true,
95930
+ restarted: false,
95931
+ scheduled: true,
95932
+ code: "restart_scheduled_when_idle",
95933
+ 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.",
95934
+ deferredRestart: deferredRestartInfo()
95935
+ };
95936
+ }
95937
+ async function deferredRestartTick(ctx) {
95938
+ const record2 = pendingDeferredRestart;
95939
+ if (!record2) return;
95940
+ if (Date.now() >= record2.expiresAt) {
95941
+ LOG2.warn("MeshRestart", `Deferred restart for node ${record2.nodeId} expired without reaching an idle point; dropping the schedule`);
95942
+ clearPendingDeferredRestart();
95943
+ return;
95944
+ }
95945
+ const blocking = collectBlockingSessions(ctx, record2.meshId);
95946
+ if (blocking.length > 0) return;
95947
+ LOG2.info("MeshRestart", `Deferred restart for node ${record2.nodeId} executing \u2014 daemon is idle`);
95948
+ clearPendingDeferredRestart();
95949
+ try {
95950
+ await executeRestart(ctx, {
95951
+ meshId: record2.meshId,
95952
+ nodeId: record2.nodeId,
95953
+ mode: record2.mode,
95954
+ killSessionHost: record2.killSessionHost
95955
+ }, { forced: false });
95956
+ } catch (e) {
95957
+ LOG2.error("MeshRestart", `Deferred restart execution failed: ${e?.message || String(e)}`);
95449
95958
  }
95450
- return false;
95451
95959
  }
95452
95960
  var meshRestartHandlers = {
95453
95961
  restart_daemon_node: async (ctx, args) => {
@@ -95468,16 +95976,43 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
95468
95976
  });
95469
95977
  return forwarded ?? { success: false, error: "no response from remote node" };
95470
95978
  }
95471
- if (hasBlockingSessions(ctx)) {
95979
+ if (args?.cancelWhenIdle === true) {
95980
+ const had = pendingDeferredRestart !== null;
95981
+ clearPendingDeferredRestart();
95982
+ return { success: true, restarted: false, cancelled: had, deferredRestart: null };
95983
+ }
95984
+ if (args?.whenIdleStatus === true) {
95985
+ return { success: true, restarted: false, deferredRestart: deferredRestartInfo() };
95986
+ }
95987
+ const blocking = collectBlockingSessions(ctx, meshId);
95988
+ if (blocking.length > 0) {
95989
+ if (args?.force === true) {
95990
+ LOG2.warn("MeshRestart", `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || "?"}(${b.status})`).join(", ")} \u2014 pendingOutboundQueue will be lost`);
95991
+ return executeRestart(ctx, args, { forced: true });
95992
+ }
95993
+ const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
95994
+ if (args?.selfOnly === true && foreignBlocking.length === 0) {
95995
+ LOG2.info("MeshRestart", `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
95996
+ return executeRestart(ctx, args, { forced: false });
95997
+ }
95998
+ if (args?.whenIdle === true) {
95999
+ return scheduleDeferredRestart(ctx, args, meshId, nodeId);
96000
+ }
95472
96001
  return {
95473
96002
  success: false,
95474
96003
  restarted: false,
95475
96004
  code: "blocking_sessions",
95476
- reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle."
96005
+ reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.",
96006
+ blockingSessions: blocking,
96007
+ options: {
96008
+ selfOnly: "waive only this mesh's own coordinator session (settings.meshCoordinatorFor === meshId)",
96009
+ force: "bypass the idle-gate entirely \u2014 kills in-flight turns and loses the unpersisted pendingOutboundQueue",
96010
+ whenIdle: "schedule the restart to run automatically once the daemon goes idle (safest)"
96011
+ },
96012
+ deferredRestart: deferredRestartInfo()
95477
96013
  };
95478
96014
  }
95479
- const result = await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
95480
- return { ...result, restarted: result?.restarting === true };
96015
+ return executeRestart(ctx, args, { forced: false });
95481
96016
  }
95482
96017
  };
95483
96018
  var medFamilyRegistry = new Map(