@adhdev/daemon-standalone 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
@@ -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 ? "5cd04eec1e043188c3ef97821372ad5ff615cd04" : void 0) ?? "unknown";
33266
+ const commitShort = readInjected(true ? "5cd04eec" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
33267
+ const version2 = readInjected(true ? "1.0.28-rc.11" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
33268
+ const builtAt = readInjected(true ? "2026-07-26T07:08:53.585Z" : 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) {
@@ -83846,6 +84145,10 @@ ${body}
83846
84145
  completedDebounceTimer = null;
83847
84146
  completedDebouncePending = null;
83848
84147
  lastExternalCompletionProbe = null;
84148
+ /** TX-FSM Stage 0 (shadow): lazily-created transcript signal normalizer.
84149
+ * Fed ONLY by transcript reads this instance already performs — it adds
84150
+ * zero I/O and its output never feeds back into any verdict. */
84151
+ transcriptSignalSource = null;
83849
84152
  /**
83850
84153
  * The final assistant summary of the last completed turn, cached at
83851
84154
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -84011,6 +84314,7 @@ ${body}
84011
84314
  });
84012
84315
  if (restoredHistory.source !== "provider-native") {
84013
84316
  this.lastExternalCompletionProbe = null;
84317
+ this.publishTranscriptSignalObservation(null);
84014
84318
  return null;
84015
84319
  }
84016
84320
  this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
@@ -84018,8 +84322,48 @@ ${body}
84018
84322
  restoredHistory.sourcePath,
84019
84323
  restoredHistory.sourceMtimeMs
84020
84324
  );
84325
+ this.publishTranscriptSignalObservation(restoredHistory.messages);
84021
84326
  return restoredHistory.messages;
84022
84327
  }
84328
+ /**
84329
+ * TX-FSM Stage 0 (shadow): normalize the transcript read that JUST
84330
+ * happened into a SignalSnapshot and inject it into the FSM driver as a
84331
+ * pure observation (daemon → SpecCliAdapter → FsmDriver). Fed ONLY by
84332
+ * reads this method's caller already performs — it adds zero I/O, so the
84333
+ * getState() zero-native-read invariant and the stall-path read cadence
84334
+ * are untouched. The snapshot is shadow data: nothing on this path feeds
84335
+ * back into completion/stall/redrive verdicts. Fail-open end to end — a
84336
+ * missing adapter hook (non-spec providers), an unresolved transcript, or
84337
+ * any throw degrades to "no observation", never to a wedge.
84338
+ */
84339
+ publishTranscriptSignalObservation(messages, error48 = false) {
84340
+ const adapter = this.adapter;
84341
+ if (typeof adapter?.setSignalObservation !== "function") return;
84342
+ try {
84343
+ if (!this.transcriptSignalSource) {
84344
+ this.transcriptSignalSource = new TranscriptSignalSource({
84345
+ label: this.type,
84346
+ // Choke point: class/timing come from the P0 profile
84347
+ // resolver, never from raw predicates or provider names.
84348
+ profile: resolveTranscriptAuthorityProfile(this.provider),
84349
+ turnStartedAt: () => {
84350
+ const t = this.adapter?.currentTurnStartedAt;
84351
+ return typeof t === "number" && Number.isFinite(t) ? t : void 0;
84352
+ },
84353
+ // Reuse the exact completion machinery (I1) for the
84354
+ // final_assistant_present signal rather than duplicating
84355
+ // the message scan.
84356
+ finalAssistantPresent: (msgs, ts2) => this.completionHasFinalAssistantMessage(msgs, ts2),
84357
+ growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS
84358
+ });
84359
+ }
84360
+ const snapshot = this.transcriptSignalSource.update(
84361
+ { messages, probe: this.lastExternalCompletionProbe, error: error48 }
84362
+ );
84363
+ adapter.setSignalObservation(snapshot);
84364
+ } catch {
84365
+ }
84366
+ }
84023
84367
  /**
84024
84368
  * The content of the LAST visible assistant bubble in a message list, or ''
84025
84369
  * when the tail is not an assistant reply. Skips trailing system/tool/activity
@@ -95437,17 +95781,134 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
95437
95781
  }
95438
95782
  };
95439
95783
  init_dist();
95784
+ init_logger();
95440
95785
  var RESTART_BLOCKING_STATES = /* @__PURE__ */ new Set(["generating", "waiting_approval", "starting"]);
95441
- function hasBlockingSessions(ctx) {
95786
+ var DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
95787
+ var DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
95788
+ var DEFERRED_RESTART_POLL_MS = 1e4;
95789
+ function collectBlockingSessions(ctx, meshId) {
95790
+ const blocking = [];
95442
95791
  const states = ctx.deps.instanceManager.collectAllStates();
95792
+ const consider = (state) => {
95793
+ const status = String(state?.status || "");
95794
+ if (!RESTART_BLOCKING_STATES.has(status)) return;
95795
+ blocking.push({
95796
+ instanceId: typeof state?.instanceId === "string" ? state.instanceId : null,
95797
+ status,
95798
+ // The coordinator marker is stamped by mesh_coordinator_launch and
95799
+ // re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
95800
+ // coordinator sessions have no marker and are NOT waived by selfOnly.
95801
+ selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId
95802
+ });
95803
+ };
95443
95804
  for (const state of states) {
95444
- if (RESTART_BLOCKING_STATES.has(String(state.status || ""))) return true;
95805
+ consider(state);
95445
95806
  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
- }
95807
+ for (const child of childStates) consider(child);
95808
+ }
95809
+ return blocking;
95810
+ }
95811
+ var pendingDeferredRestart = null;
95812
+ function deferredRestartInfo() {
95813
+ if (!pendingDeferredRestart) return null;
95814
+ return {
95815
+ meshId: pendingDeferredRestart.meshId,
95816
+ nodeId: pendingDeferredRestart.nodeId,
95817
+ mode: pendingDeferredRestart.mode,
95818
+ killSessionHost: pendingDeferredRestart.killSessionHost,
95819
+ scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
95820
+ expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
95821
+ runCondition: "executes automatically once no session is generating / waiting_approval / starting"
95822
+ };
95823
+ }
95824
+ function clearPendingDeferredRestart() {
95825
+ if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
95826
+ pendingDeferredRestart = null;
95827
+ }
95828
+ function normalizeRestartMode(value) {
95829
+ return value === "restart" ? "restart" : "upgrade";
95830
+ }
95831
+ function restartWarnings(args) {
95832
+ const warnings = [];
95833
+ if (args.forced) {
95834
+ 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).");
95835
+ }
95836
+ if (args.killSessionHost) {
95837
+ warnings.push("killSessionHost: the session-host process was stopped \u2014 ALL hosted CLI sessions on this machine are destroyed (hard refresh).");
95838
+ }
95839
+ if (process.platform === "win32") {
95840
+ warnings.push("Windows: the daemon restart/upgrade path stops the session-host regardless of options \u2014 all hosted sessions terminate.");
95841
+ } else {
95842
+ warnings.push("POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).");
95843
+ }
95844
+ return warnings;
95845
+ }
95846
+ async function executeRestart(ctx, args, opts) {
95847
+ const mode = normalizeRestartMode(args?.mode);
95848
+ const killSessionHost = args?.killSessionHost === true;
95849
+ const result = mode === "restart" ? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost }) : await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
95850
+ const restarting = result?.restarting === true;
95851
+ return {
95852
+ ...result,
95853
+ mode,
95854
+ restarted: restarting,
95855
+ ...restarting ? {
95856
+ warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
95857
+ // MCP IPC has no automatic reconnect/retry — tell the caller when to
95858
+ // talk to this daemon again.
95859
+ clientHint: "daemon restarting \u2014 retry your next call in ~10s"
95860
+ } : {}
95861
+ };
95862
+ }
95863
+ function scheduleDeferredRestart(ctx, args, meshId, nodeId) {
95864
+ clearPendingDeferredRestart();
95865
+ const requestedTimeout = typeof args?.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? args.timeoutMs : DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
95866
+ const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
95867
+ const record2 = {
95868
+ meshId,
95869
+ nodeId,
95870
+ mode: normalizeRestartMode(args?.mode),
95871
+ killSessionHost: args?.killSessionHost === true,
95872
+ scheduledAt: Date.now(),
95873
+ expiresAt: Date.now() + timeoutMs,
95874
+ timer: setInterval(() => {
95875
+ void deferredRestartTick(ctx);
95876
+ }, DEFERRED_RESTART_POLL_MS)
95877
+ };
95878
+ record2.timer.unref?.();
95879
+ pendingDeferredRestart = record2;
95880
+ LOG2.info("MeshRestart", `Deferred restart scheduled for node ${nodeId} (mode=${record2.mode}, expires in ${Math.round(timeoutMs / 6e4)}min)`);
95881
+ return {
95882
+ success: true,
95883
+ restarted: false,
95884
+ scheduled: true,
95885
+ code: "restart_scheduled_when_idle",
95886
+ 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.",
95887
+ deferredRestart: deferredRestartInfo()
95888
+ };
95889
+ }
95890
+ async function deferredRestartTick(ctx) {
95891
+ const record2 = pendingDeferredRestart;
95892
+ if (!record2) return;
95893
+ if (Date.now() >= record2.expiresAt) {
95894
+ LOG2.warn("MeshRestart", `Deferred restart for node ${record2.nodeId} expired without reaching an idle point; dropping the schedule`);
95895
+ clearPendingDeferredRestart();
95896
+ return;
95897
+ }
95898
+ const blocking = collectBlockingSessions(ctx, record2.meshId);
95899
+ if (blocking.length > 0) return;
95900
+ LOG2.info("MeshRestart", `Deferred restart for node ${record2.nodeId} executing \u2014 daemon is idle`);
95901
+ clearPendingDeferredRestart();
95902
+ try {
95903
+ await executeRestart(ctx, {
95904
+ meshId: record2.meshId,
95905
+ nodeId: record2.nodeId,
95906
+ mode: record2.mode,
95907
+ killSessionHost: record2.killSessionHost
95908
+ }, { forced: false });
95909
+ } catch (e) {
95910
+ LOG2.error("MeshRestart", `Deferred restart execution failed: ${e?.message || String(e)}`);
95449
95911
  }
95450
- return false;
95451
95912
  }
95452
95913
  var meshRestartHandlers = {
95453
95914
  restart_daemon_node: async (ctx, args) => {
@@ -95468,16 +95929,43 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
95468
95929
  });
95469
95930
  return forwarded ?? { success: false, error: "no response from remote node" };
95470
95931
  }
95471
- if (hasBlockingSessions(ctx)) {
95932
+ if (args?.cancelWhenIdle === true) {
95933
+ const had = pendingDeferredRestart !== null;
95934
+ clearPendingDeferredRestart();
95935
+ return { success: true, restarted: false, cancelled: had, deferredRestart: null };
95936
+ }
95937
+ if (args?.whenIdleStatus === true) {
95938
+ return { success: true, restarted: false, deferredRestart: deferredRestartInfo() };
95939
+ }
95940
+ const blocking = collectBlockingSessions(ctx, meshId);
95941
+ if (blocking.length > 0) {
95942
+ if (args?.force === true) {
95943
+ LOG2.warn("MeshRestart", `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || "?"}(${b.status})`).join(", ")} \u2014 pendingOutboundQueue will be lost`);
95944
+ return executeRestart(ctx, args, { forced: true });
95945
+ }
95946
+ const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
95947
+ if (args?.selfOnly === true && foreignBlocking.length === 0) {
95948
+ LOG2.info("MeshRestart", `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
95949
+ return executeRestart(ctx, args, { forced: false });
95950
+ }
95951
+ if (args?.whenIdle === true) {
95952
+ return scheduleDeferredRestart(ctx, args, meshId, nodeId);
95953
+ }
95472
95954
  return {
95473
95955
  success: false,
95474
95956
  restarted: false,
95475
95957
  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."
95958
+ reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.",
95959
+ blockingSessions: blocking,
95960
+ options: {
95961
+ selfOnly: "waive only this mesh's own coordinator session (settings.meshCoordinatorFor === meshId)",
95962
+ force: "bypass the idle-gate entirely \u2014 kills in-flight turns and loses the unpersisted pendingOutboundQueue",
95963
+ whenIdle: "schedule the restart to run automatically once the daemon goes idle (safest)"
95964
+ },
95965
+ deferredRestart: deferredRestartInfo()
95477
95966
  };
95478
95967
  }
95479
- const result = await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
95480
- return { ...result, restarted: result?.restarting === true };
95968
+ return executeRestart(ctx, args, { forced: false });
95481
95969
  }
95482
95970
  };
95483
95971
  var medFamilyRegistry = new Map(