@adhdev/daemon-core 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/cli-adapter-types.d.ts +7 -0
- package/dist/commands/upgrade-helper.d.ts +14 -0
- package/dist/index.js +623 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -84
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +69 -16
- package/dist/providers/spec/cli-adapter.d.ts +7 -0
- package/dist/providers/spec/fsm-driver.d.ts +35 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +28 -2
- package/dist/providers/spec/fsm-types.d.ts +19 -1
- package/dist/providers/spec/signal-envelope.d.ts +59 -0
- package/dist/providers/transcript-signal-source.d.ts +59 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.ts +7 -0
- package/src/commands/low-family/daemon-lifecycle.ts +41 -0
- package/src/commands/med-family/mesh-restart.ts +233 -30
- package/src/commands/upgrade-helper.ts +34 -3
- package/src/providers/cli-provider-instance.ts +204 -87
- package/src/providers/spec/cli-adapter.ts +10 -0
- package/src/providers/spec/fsm-driver.ts +62 -1
- package/src/providers/spec/fsm-evaluator.ts +110 -5
- package/src/providers/spec/fsm-loader.ts +7 -0
- package/src/providers/spec/fsm-types.ts +20 -0
- package/src/providers/spec/signal-envelope.ts +136 -0
- package/src/providers/transcript-signal-source.ts +179 -0
package/dist/index.mjs
CHANGED
|
@@ -786,10 +786,10 @@ function readInjected(value) {
|
|
|
786
786
|
}
|
|
787
787
|
function getDaemonBuildInfo() {
|
|
788
788
|
if (cached) return cached;
|
|
789
|
-
const commit = readInjected(true ? "
|
|
790
|
-
const commitShort = readInjected(true ? "
|
|
791
|
-
const version = readInjected(true ? "1.0.28-rc.
|
|
792
|
-
const builtAt = readInjected(true ? "2026-07-
|
|
789
|
+
const commit = readInjected(true ? "6f38d1460a02018ef53a3ecdf306004005b44310" : void 0) ?? "unknown";
|
|
790
|
+
const commitShort = readInjected(true ? "6f38d146" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
|
|
791
|
+
const version = readInjected(true ? "1.0.28-rc.12" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
|
|
792
|
+
const builtAt = readInjected(true ? "2026-07-26T08:24:03.072Z" : void 0);
|
|
793
793
|
cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
|
|
794
794
|
return cached;
|
|
795
795
|
}
|
|
@@ -27269,6 +27269,11 @@ function validateCondition(c, sectionIds, path46) {
|
|
|
27269
27269
|
return errs;
|
|
27270
27270
|
}
|
|
27271
27271
|
if ("cursor_above" in w && "changed" in w) return errs;
|
|
27272
|
+
if ("signal" in w) {
|
|
27273
|
+
if (typeof w.signal !== "string" || !w.signal.trim()) errs.push(`${path46}.signal must be a non-empty string`);
|
|
27274
|
+
if (w.equals !== void 0 && typeof w.equals !== "boolean") errs.push(`${path46}.equals must be a boolean`);
|
|
27275
|
+
return errs;
|
|
27276
|
+
}
|
|
27272
27277
|
if ("elapsed_ms" in w) {
|
|
27273
27278
|
if (typeof w.elapsed_ms !== "number") errs.push(`${path46}.elapsed_ms must be a number`);
|
|
27274
27279
|
return errs;
|
|
@@ -27583,6 +27588,51 @@ var init_evaluator = __esm({
|
|
|
27583
27588
|
}
|
|
27584
27589
|
});
|
|
27585
27590
|
|
|
27591
|
+
// src/providers/spec/signal-envelope.ts
|
|
27592
|
+
function unavailableSignalSnapshot(now, reason, profile) {
|
|
27593
|
+
return {
|
|
27594
|
+
kind: SIGNAL_SNAPSHOT_KIND,
|
|
27595
|
+
sampledAt: now,
|
|
27596
|
+
available: false,
|
|
27597
|
+
unavailableReason: reason,
|
|
27598
|
+
...profile ? { profile } : {},
|
|
27599
|
+
signals: { final_assistant_present: null, in_turn_progress: null, transcript_growing: null },
|
|
27600
|
+
detail: { msgCount: 0, sourceMtimeMs: 0, ageMs: null }
|
|
27601
|
+
};
|
|
27602
|
+
}
|
|
27603
|
+
function evaluateSignalLeaf(cond, snapshot) {
|
|
27604
|
+
const expected = cond.equals ?? true;
|
|
27605
|
+
if (!snapshot || !snapshot.available) {
|
|
27606
|
+
const why = !snapshot ? "no snapshot" : `unavailable(${snapshot.unavailableReason ?? "unknown"})`;
|
|
27607
|
+
return {
|
|
27608
|
+
value: null,
|
|
27609
|
+
shadowResult: null,
|
|
27610
|
+
detail: `signal ${cond.signal} ${why} \u2192 fail-open`
|
|
27611
|
+
};
|
|
27612
|
+
}
|
|
27613
|
+
const raw = Object.prototype.hasOwnProperty.call(snapshot.signals, cond.signal) ? snapshot.signals[cond.signal] : null;
|
|
27614
|
+
if (raw === null || raw === void 0) {
|
|
27615
|
+
return {
|
|
27616
|
+
value: null,
|
|
27617
|
+
shadowResult: null,
|
|
27618
|
+
detail: `signal ${cond.signal} unknown \u2192 fail-open`
|
|
27619
|
+
};
|
|
27620
|
+
}
|
|
27621
|
+
const shadowResult = raw === expected;
|
|
27622
|
+
return {
|
|
27623
|
+
value: raw,
|
|
27624
|
+
shadowResult,
|
|
27625
|
+
detail: `signal ${cond.signal}=${raw} expected=${expected} \u2192 ${shadowResult}`
|
|
27626
|
+
};
|
|
27627
|
+
}
|
|
27628
|
+
var SIGNAL_SNAPSHOT_KIND;
|
|
27629
|
+
var init_signal_envelope = __esm({
|
|
27630
|
+
"src/providers/spec/signal-envelope.ts"() {
|
|
27631
|
+
"use strict";
|
|
27632
|
+
SIGNAL_SNAPSHOT_KIND = "adhdev:fsm/signal-snapshot@0";
|
|
27633
|
+
}
|
|
27634
|
+
});
|
|
27635
|
+
|
|
27586
27636
|
// src/providers/spec/fsm-evaluator.ts
|
|
27587
27637
|
var fsm_evaluator_exports = {};
|
|
27588
27638
|
__export(fsm_evaluator_exports, {
|
|
@@ -27616,24 +27666,74 @@ function isAny(c) {
|
|
|
27616
27666
|
function isNot(c) {
|
|
27617
27667
|
return "not" in c;
|
|
27618
27668
|
}
|
|
27619
|
-
function
|
|
27669
|
+
function isSignal(c) {
|
|
27670
|
+
return "signal" in c;
|
|
27671
|
+
}
|
|
27672
|
+
function containsSignal(c) {
|
|
27673
|
+
if (isSignal(c)) return true;
|
|
27674
|
+
if (isAll(c)) return c.all.some(containsSignal);
|
|
27675
|
+
if (isAny(c)) return c.any.some(containsSignal);
|
|
27676
|
+
if (isNot(c)) return containsSignal(c.not);
|
|
27677
|
+
return false;
|
|
27678
|
+
}
|
|
27679
|
+
function foldShadow(cond) {
|
|
27680
|
+
if (cond.kind === "signal") {
|
|
27681
|
+
const s2 = cond.signal;
|
|
27682
|
+
if (s2 && s2.shadowResult !== null && s2.shadowResult !== void 0) {
|
|
27683
|
+
return { value: s2.shadowResult, unknown: false };
|
|
27684
|
+
}
|
|
27685
|
+
return { value: true, unknown: true };
|
|
27686
|
+
}
|
|
27687
|
+
if (cond.kind === "not" && cond.children?.length) {
|
|
27688
|
+
const c = foldShadow(cond.children[0]);
|
|
27689
|
+
return { value: !c.value, unknown: c.unknown };
|
|
27690
|
+
}
|
|
27691
|
+
if (cond.kind === "all" && cond.children) {
|
|
27692
|
+
const kids = cond.children.map(foldShadow);
|
|
27693
|
+
if (kids.some((k) => !k.unknown && !k.value)) return { value: false, unknown: false };
|
|
27694
|
+
if (kids.some((k) => k.unknown)) return { value: true, unknown: true };
|
|
27695
|
+
return { value: true, unknown: false };
|
|
27696
|
+
}
|
|
27697
|
+
if (cond.kind === "any" && cond.children) {
|
|
27698
|
+
const kids = cond.children.map(foldShadow);
|
|
27699
|
+
if (kids.some((k) => !k.unknown && k.value)) return { value: true, unknown: false };
|
|
27700
|
+
if (kids.some((k) => k.unknown)) return { value: false, unknown: true };
|
|
27701
|
+
return { value: false, unknown: false };
|
|
27702
|
+
}
|
|
27703
|
+
return { value: cond.result, unknown: false };
|
|
27704
|
+
}
|
|
27705
|
+
function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot) {
|
|
27620
27706
|
if (isAll(cond)) {
|
|
27621
|
-
const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
|
|
27707
|
+
const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
|
|
27622
27708
|
const result = children.every((c) => c.result);
|
|
27623
27709
|
const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
|
|
27624
27710
|
return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
|
|
27625
27711
|
}
|
|
27626
27712
|
if (isAny(cond)) {
|
|
27627
|
-
const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
|
|
27713
|
+
const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
|
|
27628
27714
|
const result = children.some((c) => c.result);
|
|
27629
27715
|
const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
|
|
27630
27716
|
const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
|
|
27631
27717
|
return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
|
|
27632
27718
|
}
|
|
27633
27719
|
if (isNot(cond)) {
|
|
27634
|
-
const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
|
|
27720
|
+
const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot);
|
|
27635
27721
|
return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
|
|
27636
27722
|
}
|
|
27723
|
+
if (isSignal(cond)) {
|
|
27724
|
+
const leaf = evaluateSignalLeaf(cond, signalSnapshot);
|
|
27725
|
+
return {
|
|
27726
|
+
kind: "signal",
|
|
27727
|
+
result: true,
|
|
27728
|
+
detail: `${leaf.detail} (stage0 shadow \u2014 pass-through)`,
|
|
27729
|
+
signal: {
|
|
27730
|
+
name: cond.signal,
|
|
27731
|
+
available: !!signalSnapshot?.available,
|
|
27732
|
+
value: leaf.value,
|
|
27733
|
+
shadowResult: leaf.shadowResult
|
|
27734
|
+
}
|
|
27735
|
+
};
|
|
27736
|
+
}
|
|
27637
27737
|
if (isElapsed(cond)) {
|
|
27638
27738
|
const age = clock.now - clock.stateEnteredAt;
|
|
27639
27739
|
const result = age >= cond.elapsed_ms;
|
|
@@ -27672,7 +27772,7 @@ function fromLabel(t) {
|
|
|
27672
27772
|
const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
|
|
27673
27773
|
return t.label ?? `${from}\u2192${t.to}`;
|
|
27674
27774
|
}
|
|
27675
|
-
function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
|
|
27775
|
+
function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock, signalSnapshot) {
|
|
27676
27776
|
const legacyTrace = [];
|
|
27677
27777
|
const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
|
|
27678
27778
|
const cleanScreen = lines.join("\n");
|
|
@@ -27688,7 +27788,7 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
|
|
|
27688
27788
|
let cond;
|
|
27689
27789
|
let condResult = true;
|
|
27690
27790
|
if (t.when) {
|
|
27691
|
-
cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}
|
|
27791
|
+
cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`, signalSnapshot);
|
|
27692
27792
|
condResult = cond.result;
|
|
27693
27793
|
}
|
|
27694
27794
|
const fires = holdSatisfied && condResult;
|
|
@@ -27703,6 +27803,14 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
|
|
|
27703
27803
|
fires,
|
|
27704
27804
|
priority: t.priority ?? 0
|
|
27705
27805
|
};
|
|
27806
|
+
if (t.when && containsSignal(t.when) && cond) {
|
|
27807
|
+
const folded = foldShadow(cond);
|
|
27808
|
+
te.shadow = {
|
|
27809
|
+
condResult: folded.value,
|
|
27810
|
+
fires: holdSatisfied && folded.value,
|
|
27811
|
+
unknown: folded.unknown
|
|
27812
|
+
};
|
|
27813
|
+
}
|
|
27706
27814
|
transitions.push(te);
|
|
27707
27815
|
if (fires && !fired) fired = te;
|
|
27708
27816
|
}
|
|
@@ -27725,6 +27833,7 @@ var init_fsm_evaluator = __esm({
|
|
|
27725
27833
|
"use strict";
|
|
27726
27834
|
init_evaluator();
|
|
27727
27835
|
init_fsm_types();
|
|
27836
|
+
init_signal_envelope();
|
|
27728
27837
|
WHOLE_SCREEN = -1;
|
|
27729
27838
|
}
|
|
27730
27839
|
});
|
|
@@ -45073,12 +45182,21 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45073
45182
|
appendUpgradeLog(`Waiting for parent pid ${payload.parentPid} to exit`);
|
|
45074
45183
|
await waitForPidExit(payload.parentPid, 15e3);
|
|
45075
45184
|
}
|
|
45076
|
-
if (process.platform === "win32") {
|
|
45185
|
+
if (process.platform === "win32" || payload.killSessionHost === true) {
|
|
45077
45186
|
await stopSessionHostProcesses(sessionHostAppName);
|
|
45078
45187
|
} else {
|
|
45079
45188
|
appendUpgradeLog("POSIX \u2014 session-host left running (survives upgrade; sessions rebind on next boot)");
|
|
45080
45189
|
}
|
|
45081
45190
|
removeDaemonPidFile();
|
|
45191
|
+
if (payload.skipInstall) {
|
|
45192
|
+
appendUpgradeLog("Restart-only mode \u2014 package install skipped, re-spawning daemon");
|
|
45193
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
45194
|
+
try {
|
|
45195
|
+
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45196
|
+
} catch {
|
|
45197
|
+
}
|
|
45198
|
+
return;
|
|
45199
|
+
}
|
|
45082
45200
|
const instanceDir = resolveInstanceDir();
|
|
45083
45201
|
const windowsInstallerLayout = resolveWindowsInstallerLayout({
|
|
45084
45202
|
homeDir: os14.homedir(),
|
|
@@ -45197,6 +45315,13 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45197
45315
|
cleanupStaleGlobalInstallDirs(payload.packageName, installCommand.surface);
|
|
45198
45316
|
appendUpgradeLog("Post-install staging cleanup complete");
|
|
45199
45317
|
}
|
|
45318
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
45319
|
+
try {
|
|
45320
|
+
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45321
|
+
} catch {
|
|
45322
|
+
}
|
|
45323
|
+
}
|
|
45324
|
+
function spawnDetachedDaemonRestart(restartArgv, cwd) {
|
|
45200
45325
|
if (restartArgv.length > 0) {
|
|
45201
45326
|
const env = { ...process.env };
|
|
45202
45327
|
delete env[UPGRADE_HELPER_ENV];
|
|
@@ -45205,17 +45330,13 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45205
45330
|
detached: true,
|
|
45206
45331
|
stdio: "ignore",
|
|
45207
45332
|
windowsHide: true,
|
|
45208
|
-
cwd:
|
|
45333
|
+
cwd: cwd || process.cwd(),
|
|
45209
45334
|
env
|
|
45210
45335
|
});
|
|
45211
45336
|
child.unref();
|
|
45212
45337
|
} else {
|
|
45213
45338
|
appendUpgradeLog("No restart argv provided; upgrade completed without restart");
|
|
45214
45339
|
}
|
|
45215
|
-
try {
|
|
45216
|
-
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45217
|
-
} catch {
|
|
45218
|
-
}
|
|
45219
45340
|
}
|
|
45220
45341
|
async function maybeRunDaemonUpgradeHelperFromEnv() {
|
|
45221
45342
|
const raw = process.env[UPGRADE_HELPER_ENV];
|
|
@@ -45308,6 +45429,41 @@ var daemonLifecycleHandlers = {
|
|
|
45308
45429
|
return { success: false, error: e.message };
|
|
45309
45430
|
}
|
|
45310
45431
|
},
|
|
45432
|
+
// Restart-only path (mesh restart_daemon_node mode="restart"): re-spawn the
|
|
45433
|
+
// daemon WITHOUT the npm reinstall daemon_upgrade performs. Used to reset
|
|
45434
|
+
// daemon state (memory leaks, zombie sessions, wedged internals) when the
|
|
45435
|
+
// version is already correct — downtime drops to the detached re-spawn.
|
|
45436
|
+
// killSessionHost is an explicit opt-in hard refresh (see upgrade-helper).
|
|
45437
|
+
daemon_restart: async (ctx, args) => {
|
|
45438
|
+
LOG.info("Restart", "Restart-only requested (no package reinstall)");
|
|
45439
|
+
try {
|
|
45440
|
+
const isStandalone = ctx.deps.packageName === "@adhdev/daemon-standalone" || process.argv[1]?.includes("daemon-standalone");
|
|
45441
|
+
const pkgName = isStandalone ? "@adhdev/daemon-standalone" : "adhdev";
|
|
45442
|
+
const killSessionHost = args?.killSessionHost === true;
|
|
45443
|
+
if (killSessionHost) {
|
|
45444
|
+
LOG.warn("Restart", "killSessionHost requested \u2014 session-host will be stopped and ALL hosted sessions destroyed");
|
|
45445
|
+
}
|
|
45446
|
+
spawnDetachedDaemonUpgradeHelper({
|
|
45447
|
+
packageName: pkgName,
|
|
45448
|
+
targetVersion: "",
|
|
45449
|
+
parentPid: process.pid,
|
|
45450
|
+
restartArgv: process.argv.slice(1),
|
|
45451
|
+
cwd: process.cwd(),
|
|
45452
|
+
sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || "adhdev",
|
|
45453
|
+
skipInstall: true,
|
|
45454
|
+
killSessionHost
|
|
45455
|
+
});
|
|
45456
|
+
LOG.info("Restart", "Scheduled detached restart-only helper");
|
|
45457
|
+
setTimeout(() => {
|
|
45458
|
+
LOG.info("Restart", "Exiting daemon so detached helper can re-spawn it...");
|
|
45459
|
+
process.exit(0);
|
|
45460
|
+
}, 3e3);
|
|
45461
|
+
return { success: true, restarted: true, restarting: true, mode: "restart", killSessionHost };
|
|
45462
|
+
} catch (e) {
|
|
45463
|
+
LOG.error("Restart", `Failed: ${e.message}`);
|
|
45464
|
+
return { success: false, error: e.message };
|
|
45465
|
+
}
|
|
45466
|
+
},
|
|
45311
45467
|
set_machine_nickname: async (_ctx, args) => {
|
|
45312
45468
|
const nickname = args?.nickname;
|
|
45313
45469
|
updateConfig({ machineNickname: nickname || null });
|
|
@@ -45804,6 +45960,93 @@ import * as fs24 from "fs";
|
|
|
45804
45960
|
init_hash();
|
|
45805
45961
|
init_transcript_evidence();
|
|
45806
45962
|
|
|
45963
|
+
// src/providers/transcript-signal-source.ts
|
|
45964
|
+
init_logger();
|
|
45965
|
+
init_signal_envelope();
|
|
45966
|
+
var TranscriptSignalSource = class {
|
|
45967
|
+
constructor(opts) {
|
|
45968
|
+
this.opts = opts;
|
|
45969
|
+
}
|
|
45970
|
+
/** msgCount seen at the previous update — drives in_turn_progress. */
|
|
45971
|
+
prevMsgCount = -1;
|
|
45972
|
+
/** Fingerprint of the last LOGGED snapshot, so the shadow log fires on
|
|
45973
|
+
* change only (a quiet session must not spam one line per read). */
|
|
45974
|
+
lastLoggedFingerprint = "";
|
|
45975
|
+
/**
|
|
45976
|
+
* Normalize one daemon read into a SignalSnapshot and emit the Stage-0
|
|
45977
|
+
* shadow log when the observation changed. Pure w.r.t. the outside world:
|
|
45978
|
+
* the only side effect is the (change-gated) log line. Returns the
|
|
45979
|
+
* snapshot the caller should inject into the FSM driver.
|
|
45980
|
+
*/
|
|
45981
|
+
update(sample, now = Date.now()) {
|
|
45982
|
+
const snapshot = this.buildSnapshot(sample, now);
|
|
45983
|
+
this.logOnChange(snapshot);
|
|
45984
|
+
return snapshot;
|
|
45985
|
+
}
|
|
45986
|
+
/** Pure normalization — separated from update() so tests can assert the
|
|
45987
|
+
* envelope without touching the log path. */
|
|
45988
|
+
buildSnapshot(sample, now) {
|
|
45989
|
+
const profileRef = { class: this.opts.profile.class, timing: this.opts.profile.timing };
|
|
45990
|
+
if (this.opts.profile.class !== "native-source") {
|
|
45991
|
+
return unavailableSignalSnapshot(now, "no_native_source", profileRef);
|
|
45992
|
+
}
|
|
45993
|
+
if (sample.error) {
|
|
45994
|
+
return unavailableSignalSnapshot(now, "error", profileRef);
|
|
45995
|
+
}
|
|
45996
|
+
const probe = sample.probe;
|
|
45997
|
+
if (!probe || !Array.isArray(sample.messages)) {
|
|
45998
|
+
return unavailableSignalSnapshot(now, "unresolved", profileRef);
|
|
45999
|
+
}
|
|
46000
|
+
const msgCount = typeof probe.msgCount === "number" && Number.isFinite(probe.msgCount) ? probe.msgCount : sample.messages.length;
|
|
46001
|
+
const sourceMtimeMs = typeof probe.sourceMtimeMs === "number" && Number.isFinite(probe.sourceMtimeMs) ? probe.sourceMtimeMs : 0;
|
|
46002
|
+
const ageMs = sourceMtimeMs > 0 ? Math.max(0, now - sourceMtimeMs) : null;
|
|
46003
|
+
let turnStartedAt;
|
|
46004
|
+
try {
|
|
46005
|
+
turnStartedAt = this.opts.turnStartedAt?.();
|
|
46006
|
+
} catch {
|
|
46007
|
+
turnStartedAt = void 0;
|
|
46008
|
+
}
|
|
46009
|
+
let finalAssistant = null;
|
|
46010
|
+
try {
|
|
46011
|
+
finalAssistant = this.opts.finalAssistantPresent(sample.messages, turnStartedAt);
|
|
46012
|
+
} catch {
|
|
46013
|
+
finalAssistant = null;
|
|
46014
|
+
}
|
|
46015
|
+
const countAdvanced = this.prevMsgCount >= 0 && msgCount > this.prevMsgCount;
|
|
46016
|
+
const fresh = ageMs !== null && ageMs < this.opts.growthQuietMs;
|
|
46017
|
+
const inTurnProgress = countAdvanced || fresh;
|
|
46018
|
+
const transcriptGrowing = ageMs === null ? null : fresh;
|
|
46019
|
+
this.prevMsgCount = msgCount;
|
|
46020
|
+
return {
|
|
46021
|
+
kind: SIGNAL_SNAPSHOT_KIND,
|
|
46022
|
+
sampledAt: now,
|
|
46023
|
+
available: true,
|
|
46024
|
+
profile: profileRef,
|
|
46025
|
+
signals: {
|
|
46026
|
+
final_assistant_present: finalAssistant,
|
|
46027
|
+
in_turn_progress: inTurnProgress,
|
|
46028
|
+
transcript_growing: transcriptGrowing
|
|
46029
|
+
},
|
|
46030
|
+
detail: { msgCount, sourceMtimeMs, ageMs }
|
|
46031
|
+
};
|
|
46032
|
+
}
|
|
46033
|
+
/** Stage-0 shadow log: emit one line when the normalized observation
|
|
46034
|
+
* CHANGES. This log is the Stage 1-3 judgment input ("which signals were
|
|
46035
|
+
* actually observable, and when"), so it carries the full signal set —
|
|
46036
|
+
* but only on change, so a steady-state session stays quiet. */
|
|
46037
|
+
logOnChange(snapshot) {
|
|
46038
|
+
const s2 = snapshot.signals;
|
|
46039
|
+
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"}`;
|
|
46040
|
+
if (fingerprint === this.lastLoggedFingerprint) return;
|
|
46041
|
+
this.lastLoggedFingerprint = fingerprint;
|
|
46042
|
+
const age = snapshot.detail.ageMs === null ? "n/a" : `${snapshot.detail.ageMs}ms`;
|
|
46043
|
+
LOG.info(
|
|
46044
|
+
"TranscriptSignalSource",
|
|
46045
|
+
`[${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"}`)
|
|
46046
|
+
);
|
|
46047
|
+
}
|
|
46048
|
+
};
|
|
46049
|
+
|
|
45807
46050
|
// src/providers/spec/route.ts
|
|
45808
46051
|
init_provider_cli_adapter();
|
|
45809
46052
|
import * as fs22 from "fs";
|
|
@@ -46168,6 +46411,15 @@ var FsmDriver = class {
|
|
|
46168
46411
|
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
46169
46412
|
* for the single most recent evaluation. Separate from stateHistory. */
|
|
46170
46413
|
fsmSnapshotHistory = [];
|
|
46414
|
+
/** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
|
|
46415
|
+
* Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
|
|
46416
|
+
* the Stage-0 pass-through in the evaluator means it can never alter
|
|
46417
|
+
* which transition fires. */
|
|
46418
|
+
signalObservation = null;
|
|
46419
|
+
/** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
|
|
46420
|
+
* the shadow verdict currently disagrees with the real one), so the
|
|
46421
|
+
* shadow log emits on FLIP only, not every frame. */
|
|
46422
|
+
shadowDivergenceLast = /* @__PURE__ */ new Map();
|
|
46171
46423
|
subscribe(listener) {
|
|
46172
46424
|
this.listeners.add(listener);
|
|
46173
46425
|
return () => {
|
|
@@ -46240,6 +46492,16 @@ var FsmDriver = class {
|
|
|
46240
46492
|
updateMeta(meta, replace = false) {
|
|
46241
46493
|
this.adapter.updateMeta(meta, replace);
|
|
46242
46494
|
}
|
|
46495
|
+
/**
|
|
46496
|
+
* TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
|
|
46497
|
+
* observation. The driver stores it verbatim — it does NOT poll, parse,
|
|
46498
|
+
* or read anything itself (generic-PTY-engine boundary). The observation
|
|
46499
|
+
* only feeds the shadow verdict of `signal` conditions; the evaluator's
|
|
46500
|
+
* Stage-0 pass-through guarantees it cannot change a transition.
|
|
46501
|
+
*/
|
|
46502
|
+
setSignalObservation(snapshot) {
|
|
46503
|
+
this.signalObservation = snapshot ?? null;
|
|
46504
|
+
}
|
|
46243
46505
|
snapshot() {
|
|
46244
46506
|
return this.adapter.snapshot();
|
|
46245
46507
|
}
|
|
@@ -46430,7 +46692,35 @@ var FsmDriver = class {
|
|
|
46430
46692
|
}
|
|
46431
46693
|
evalFsmNow(screen, cursor, now) {
|
|
46432
46694
|
const prev = this.prevScreenLines.length > 0 ? this.prevScreenLines : void 0;
|
|
46433
|
-
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now));
|
|
46695
|
+
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now), this.signalObservation);
|
|
46696
|
+
}
|
|
46697
|
+
/**
|
|
46698
|
+
* TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
|
|
46699
|
+
* counterfactual verdict against the real (PTY-only) one and log on FLIP.
|
|
46700
|
+
* This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
|
|
46701
|
+
* evidence for whether signal gating would have changed any verdict, and
|
|
46702
|
+
* in which direction. Read-only: runs after the evaluation is complete
|
|
46703
|
+
* and never feeds back into it.
|
|
46704
|
+
*/
|
|
46705
|
+
logShadowDivergence(ev) {
|
|
46706
|
+
for (const t of ev.transitions) {
|
|
46707
|
+
if (!t.shadow) continue;
|
|
46708
|
+
const key2 = `${this.currentStateId}\u2192${t.to}`;
|
|
46709
|
+
const diverges = t.shadow.fires !== t.fires;
|
|
46710
|
+
if ((this.shadowDivergenceLast.get(key2) ?? false) === diverges) continue;
|
|
46711
|
+
this.shadowDivergenceLast.set(key2, diverges);
|
|
46712
|
+
if (diverges) {
|
|
46713
|
+
LOG.info(
|
|
46714
|
+
"FsmDriver",
|
|
46715
|
+
`[${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" : ""})`
|
|
46716
|
+
);
|
|
46717
|
+
} else {
|
|
46718
|
+
LOG.info(
|
|
46719
|
+
"FsmDriver",
|
|
46720
|
+
`[${this.specTag()}] [shadow] ${key2} (${t.label}): shadow verdict realigned with real fires=${t.fires}`
|
|
46721
|
+
);
|
|
46722
|
+
}
|
|
46723
|
+
}
|
|
46434
46724
|
}
|
|
46435
46725
|
reevaluate(forceEmit = false) {
|
|
46436
46726
|
const now = Date.now();
|
|
@@ -46441,6 +46731,7 @@ var FsmDriver = class {
|
|
|
46441
46731
|
const ev = this.evalFsmNow(screen, cursor, now);
|
|
46442
46732
|
this.lastFsmEval = ev;
|
|
46443
46733
|
this.prevScreenLines = currentLines;
|
|
46734
|
+
this.logShadowDivergence(ev);
|
|
46444
46735
|
if (ev.fired) {
|
|
46445
46736
|
this.commitTransition(ev.fired, now, ev);
|
|
46446
46737
|
this.emitStateChanged(forceEmit);
|
|
@@ -49089,6 +49380,18 @@ var SpecCliAdapter = class _SpecCliAdapter {
|
|
|
49089
49380
|
}
|
|
49090
49381
|
refreshProviderDefinition() {
|
|
49091
49382
|
}
|
|
49383
|
+
/**
|
|
49384
|
+
* TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
|
|
49385
|
+
* observation into the FSM driver. Observation-only — failures here must
|
|
49386
|
+
* never break the adapter, and the driver treats the envelope as a pure
|
|
49387
|
+
* injected value (no reads cross the engine boundary).
|
|
49388
|
+
*/
|
|
49389
|
+
setSignalObservation(snapshot) {
|
|
49390
|
+
try {
|
|
49391
|
+
this.driver.setSignalObservation?.(snapshot);
|
|
49392
|
+
} catch {
|
|
49393
|
+
}
|
|
49394
|
+
}
|
|
49092
49395
|
handleEvent(ev) {
|
|
49093
49396
|
this.lastEvent = ev;
|
|
49094
49397
|
switch (ev.kind) {
|
|
@@ -50134,18 +50437,20 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
50134
50437
|
// MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
|
|
50135
50438
|
// anchor re-arms (the per-anchor guard only covers a single continuous stall).
|
|
50136
50439
|
meshStallLastFiredAt = -1;
|
|
50137
|
-
// KIMI-MESH-COMPLETION-EMIT (axis 1)
|
|
50138
|
-
//
|
|
50139
|
-
//
|
|
50140
|
-
//
|
|
50141
|
-
//
|
|
50142
|
-
//
|
|
50143
|
-
//
|
|
50144
|
-
//
|
|
50145
|
-
//
|
|
50146
|
-
// handling can lead to the worker being stopped mid-work → completion
|
|
50147
|
-
// emitted).
|
|
50148
|
-
|
|
50440
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 1) — TX-FSM Stage 1: whether the stall
|
|
50441
|
+
// watchdog has consumed a usable native-transcript SIGNAL SNAPSHOT for the
|
|
50442
|
+
// current stall episode. A pure-PTY native-source worker running a long,
|
|
50443
|
+
// screen-quiet tool (no viewport bytes for minutes) looks stalled by the
|
|
50444
|
+
// lastOutputAt clock even though its transcript file is still growing.
|
|
50445
|
+
// Before firing the stall, the watchdog consults the shared
|
|
50446
|
+
// TranscriptSignalSource's in_turn_progress signal; if the transcript is
|
|
50447
|
+
// advancing, the "stall" is a false positive of PTY-render stasis, so we
|
|
50448
|
+
// re-arm the anchor instead of paging the coordinator (whose no_progress
|
|
50449
|
+
// handling can lead to the worker being stopped mid-work → completion
|
|
50450
|
+
// never emitted). The FIRST usable sample of an episode still re-arms
|
|
50451
|
+
// unconditionally (no episode-scoped baseline exists yet to prove stasis
|
|
50452
|
+
// against) — the historical `!prev → advanced` semantics, preserved.
|
|
50453
|
+
meshStallTranscriptSignalSampled = false;
|
|
50149
50454
|
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
50150
50455
|
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
50151
50456
|
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
@@ -50943,6 +51248,17 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
50943
51248
|
completedDebounceTimer = null;
|
|
50944
51249
|
completedDebouncePending = null;
|
|
50945
51250
|
lastExternalCompletionProbe = null;
|
|
51251
|
+
/** TX-FSM: lazily-created transcript signal normalizer. Fed ONLY by
|
|
51252
|
+
* transcript reads this instance already performs — it adds zero I/O.
|
|
51253
|
+
* Stage 0: its output was a pure shadow observation for the FSM driver.
|
|
51254
|
+
* Stage 1: the instance's own stall/growth-hold judgments consume the
|
|
51255
|
+
* normalized snapshot too (single source of truth). */
|
|
51256
|
+
transcriptSignalSource = null;
|
|
51257
|
+
/** TX-FSM Stage 1: the latest snapshot the source produced (set inside
|
|
51258
|
+
* publishTranscriptSignalObservation). Consumed the same tick by the
|
|
51259
|
+
* stall-path / growth-hold judgments via probeNativeTranscriptSignals —
|
|
51260
|
+
* never treated as fresh across ticks. */
|
|
51261
|
+
lastTranscriptSignalSnapshot = null;
|
|
50946
51262
|
/**
|
|
50947
51263
|
* The final assistant summary of the last completed turn, cached at
|
|
50948
51264
|
* completion-emit time. For a native-source provider (antigravity) whose
|
|
@@ -51108,6 +51424,7 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51108
51424
|
});
|
|
51109
51425
|
if (restoredHistory.source !== "provider-native") {
|
|
51110
51426
|
this.lastExternalCompletionProbe = null;
|
|
51427
|
+
this.publishTranscriptSignalObservation(null);
|
|
51111
51428
|
return null;
|
|
51112
51429
|
}
|
|
51113
51430
|
this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
|
|
@@ -51115,8 +51432,55 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51115
51432
|
restoredHistory.sourcePath,
|
|
51116
51433
|
restoredHistory.sourceMtimeMs
|
|
51117
51434
|
);
|
|
51435
|
+
this.publishTranscriptSignalObservation(restoredHistory.messages);
|
|
51118
51436
|
return restoredHistory.messages;
|
|
51119
51437
|
}
|
|
51438
|
+
/**
|
|
51439
|
+
* TX-FSM: normalize the transcript read that JUST happened into a
|
|
51440
|
+
* SignalSnapshot. Stage 0 injected it into the FSM driver as a pure
|
|
51441
|
+
* shadow observation (daemon → SpecCliAdapter → FsmDriver); Stage 1
|
|
51442
|
+
* additionally caches it (lastTranscriptSignalSnapshot) so the instance's
|
|
51443
|
+
* OWN stall/growth-hold judgments consume the SAME normalized snapshot
|
|
51444
|
+
* instead of re-running private transcript scans. Fed ONLY by reads this
|
|
51445
|
+
* method's caller already performs — it adds zero I/O, so the getState()
|
|
51446
|
+
* zero-native-read invariant and the stall-path read cadence are
|
|
51447
|
+
* untouched. The source update runs regardless of the adapter hook so a
|
|
51448
|
+
* non-spec provider still produces the instance-side snapshot (the FSM
|
|
51449
|
+
* injection is simply skipped there). Fail-open end to end — an
|
|
51450
|
+
* unresolved transcript or any throw degrades to "no observation", never
|
|
51451
|
+
* to a wedge.
|
|
51452
|
+
*/
|
|
51453
|
+
publishTranscriptSignalObservation(messages, error = false) {
|
|
51454
|
+
try {
|
|
51455
|
+
if (!this.transcriptSignalSource) {
|
|
51456
|
+
this.transcriptSignalSource = new TranscriptSignalSource({
|
|
51457
|
+
label: this.type,
|
|
51458
|
+
// Choke point: class/timing come from the P0 profile
|
|
51459
|
+
// resolver, never from raw predicates or provider names.
|
|
51460
|
+
profile: resolveTranscriptAuthorityProfile(this.provider),
|
|
51461
|
+
turnStartedAt: () => {
|
|
51462
|
+
const t = this.adapter?.currentTurnStartedAt;
|
|
51463
|
+
if (typeof t === "number" && Number.isFinite(t)) return t;
|
|
51464
|
+
return this.meshTaskInjectedAt > 0 ? this.meshTaskInjectedAt : void 0;
|
|
51465
|
+
},
|
|
51466
|
+
// Reuse the exact completion machinery (I1) for the
|
|
51467
|
+
// final_assistant_present signal rather than duplicating
|
|
51468
|
+
// the message scan.
|
|
51469
|
+
finalAssistantPresent: (msgs, ts2) => this.completionHasFinalAssistantMessage(msgs, ts2),
|
|
51470
|
+
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS
|
|
51471
|
+
});
|
|
51472
|
+
}
|
|
51473
|
+
const snapshot = this.transcriptSignalSource.update(
|
|
51474
|
+
{ messages, probe: this.lastExternalCompletionProbe, error }
|
|
51475
|
+
);
|
|
51476
|
+
this.lastTranscriptSignalSnapshot = snapshot;
|
|
51477
|
+
const adapter = this.adapter;
|
|
51478
|
+
if (typeof adapter?.setSignalObservation === "function") {
|
|
51479
|
+
adapter.setSignalObservation(snapshot);
|
|
51480
|
+
}
|
|
51481
|
+
} catch {
|
|
51482
|
+
}
|
|
51483
|
+
}
|
|
51120
51484
|
/**
|
|
51121
51485
|
* The content of the LAST visible assistant bubble in a message list, or ''
|
|
51122
51486
|
* when the tail is not an assistant reply. Skips trailing system/tool/activity
|
|
@@ -51606,17 +51970,17 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51606
51970
|
const threshold = turnActive ? _CliProviderInstance.MESH_WORKER_STALL_TURN_THRESHOLD_MS : _CliProviderInstance.MESH_WORKER_STALL_IDLE_THRESHOLD_MS;
|
|
51607
51971
|
const stalledMs = now - this.meshStallAnchorAt;
|
|
51608
51972
|
if (stalledMs < threshold) return;
|
|
51609
|
-
const
|
|
51610
|
-
if (
|
|
51611
|
-
const
|
|
51612
|
-
|
|
51613
|
-
|
|
51614
|
-
if (
|
|
51973
|
+
const transcriptSignals = this.probeNativeTranscriptSignals();
|
|
51974
|
+
if (transcriptSignals?.snapshot?.available === true) {
|
|
51975
|
+
const firstSampleThisEpisode = !this.meshStallTranscriptSignalSampled;
|
|
51976
|
+
this.meshStallTranscriptSignalSampled = true;
|
|
51977
|
+
const signalDetail = transcriptSignals.snapshot.detail;
|
|
51978
|
+
if (firstSampleThisEpisode || transcriptSignals.snapshot.signals.in_turn_progress === true) {
|
|
51615
51979
|
if (this.isMeshWorkerSession()) {
|
|
51616
51980
|
traceMeshEventDrop(
|
|
51617
51981
|
"mesh_worker_stall_transcript_advancing",
|
|
51618
51982
|
this.meshTraceCtx("monitor:no_progress"),
|
|
51619
|
-
`msgCount=${
|
|
51983
|
+
`msgCount=${signalDetail.msgCount} sourceMtime=${signalDetail.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1e3)}s but transcript advancing)`
|
|
51620
51984
|
);
|
|
51621
51985
|
}
|
|
51622
51986
|
this.meshStallAnchorAt = now;
|
|
@@ -51624,7 +51988,7 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51624
51988
|
return;
|
|
51625
51989
|
}
|
|
51626
51990
|
}
|
|
51627
|
-
if (this.tryReconcileTranscriptCompletionForStall(observedStatus)) {
|
|
51991
|
+
if (this.tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals)) {
|
|
51628
51992
|
this.meshStallEmittedForAnchor = true;
|
|
51629
51993
|
return;
|
|
51630
51994
|
}
|
|
@@ -51664,38 +52028,45 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51664
52028
|
this.meshStallEmittedForAnchor = false;
|
|
51665
52029
|
this.meshStallTurnActiveLast = void 0;
|
|
51666
52030
|
this.meshStallLastFiredAt = -1;
|
|
51667
|
-
this.
|
|
52031
|
+
this.meshStallTranscriptSignalSampled = false;
|
|
51668
52032
|
}
|
|
52033
|
+
/** The result of one native-transcript signal probe: the normalized
|
|
52034
|
+
* snapshot the shared TranscriptSignalSource produced from the read, and
|
|
52035
|
+
* the very messages it was normalized from (so a judgment site can pull
|
|
52036
|
+
* a payload — e.g. the final summary — from the SAME read with zero
|
|
52037
|
+
* added I/O). */
|
|
51669
52038
|
/**
|
|
51670
|
-
*
|
|
51671
|
-
*
|
|
51672
|
-
*
|
|
51673
|
-
*
|
|
51674
|
-
*
|
|
51675
|
-
*
|
|
51676
|
-
*
|
|
51677
|
-
*
|
|
51678
|
-
*
|
|
52039
|
+
* TX-FSM Stage 1 — the single native-transcript signal probe (replaces
|
|
52040
|
+
* the Stage-0 sampleNativeTranscriptProgress fingerprint sampler). For a
|
|
52041
|
+
* native-source provider (its authoritative history is an on-disk
|
|
52042
|
+
* transcript file, e.g. kimi's wire.jsonl), perform the read this
|
|
52043
|
+
* judgment point already owns — SAME cadence as before, one
|
|
52044
|
+
* readExternalCompletionMessages() per call, never more — and return the
|
|
52045
|
+
* NORMALIZED SignalSnapshot the shared TranscriptSignalSource produced
|
|
52046
|
+
* from it (publishTranscriptSignalObservation runs inside the read), plus
|
|
52047
|
+
* the messages that read returned. Judgment sites (the stall watchdog's
|
|
52048
|
+
* transcript-advancing axis, the completion growth-hold, the stall-path
|
|
52049
|
+
* completion rescue) consume the snapshot's SIGNALS instead of running
|
|
52050
|
+
* their own fingerprint/freshness/final-assistant scans — one source of
|
|
52051
|
+
* truth for "what does the transcript say right now".
|
|
51679
52052
|
*
|
|
51680
|
-
*
|
|
51681
|
-
*
|
|
51682
|
-
*
|
|
51683
|
-
* (
|
|
52053
|
+
* Class gating goes through resolveTranscriptAuthorityProfile ONLY.
|
|
52054
|
+
* Returns null for a non-native-source class (nothing to signal from) and
|
|
52055
|
+
* a null snapshot when the read threw — callers keep their fail-open
|
|
52056
|
+
* fallbacks ("couldn't tell" never blocks an idle verdict and never
|
|
52057
|
+
* fabricates a completion). Cheap enough for the stall path: it runs
|
|
52058
|
+
* only at the stall threshold (≥180s of PTY stasis) or during an armed
|
|
52059
|
+
* completion-debounce retry, never on the routine 5s tick.
|
|
51684
52060
|
*/
|
|
51685
|
-
|
|
51686
|
-
if (
|
|
52061
|
+
probeNativeTranscriptSignals() {
|
|
52062
|
+
if (resolveTranscriptAuthorityProfile(this.provider).class !== "native-source") return null;
|
|
51687
52063
|
let messages = null;
|
|
51688
52064
|
try {
|
|
51689
52065
|
messages = this.readExternalCompletionMessages();
|
|
51690
52066
|
} catch {
|
|
51691
|
-
return null;
|
|
52067
|
+
return { snapshot: null, messages: null };
|
|
51692
52068
|
}
|
|
51693
|
-
|
|
51694
|
-
if (!probe) return null;
|
|
51695
|
-
return {
|
|
51696
|
-
msgCount: typeof probe.msgCount === "number" ? probe.msgCount : Array.isArray(messages) ? messages.length : 0,
|
|
51697
|
-
sourceMtimeMs: typeof probe.sourceMtimeMs === "number" ? probe.sourceMtimeMs : 0
|
|
51698
|
-
};
|
|
52069
|
+
return { snapshot: this.lastTranscriptSignalSnapshot, messages };
|
|
51699
52070
|
}
|
|
51700
52071
|
/**
|
|
51701
52072
|
* KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
|
|
@@ -51784,8 +52155,22 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51784
52155
|
* false for a daemon-owned provider and for a genuinely mid-turn / wedged
|
|
51785
52156
|
* worker with no in-turn final assistant, so a real stall still fires
|
|
51786
52157
|
* unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
|
|
52158
|
+
*
|
|
52159
|
+
* TX-FSM Stage 1 (FIX 3 probe delegation): for a native-source class the
|
|
52160
|
+
* completion VERDICT is the shared TranscriptSignalSource's
|
|
52161
|
+
* final_assistant_present signal, normalized from the ONE transcript read
|
|
52162
|
+
* the stall watchdog already performed this tick (passed in as
|
|
52163
|
+
* `transcriptSignals`) — not a second private read + scan. The emit
|
|
52164
|
+
* payload (finalSummary) is extracted from the SAME messages the signal
|
|
52165
|
+
* was normalized from, with the SAME turn boundary the legacy path used,
|
|
52166
|
+
* so the extraction re-proves the turn scope the signal checked. Fail-open
|
|
52167
|
+
* both ways: no usable snapshot (read failed / unresolved / a pure-PTY
|
|
52168
|
+
* class, which has no native signal by construction) → the legacy
|
|
52169
|
+
* class-appropriate evidence path (completionFinalSummary), unchanged; and
|
|
52170
|
+
* a present-but-unextractable signal yields false rather than a
|
|
52171
|
+
* payload-less emit.
|
|
51787
52172
|
*/
|
|
51788
|
-
tryReconcileTranscriptCompletionForStall(observedStatus) {
|
|
52173
|
+
tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals) {
|
|
51789
52174
|
const profile = resolveTranscriptAuthorityProfile(this.provider);
|
|
51790
52175
|
if (profile.class === "daemon-owned") return false;
|
|
51791
52176
|
if (observedStatus !== "idle") return false;
|
|
@@ -51803,10 +52188,19 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51803
52188
|
parsedMessages = void 0;
|
|
51804
52189
|
}
|
|
51805
52190
|
let finalSummary;
|
|
51806
|
-
|
|
51807
|
-
|
|
51808
|
-
|
|
51809
|
-
finalSummary =
|
|
52191
|
+
const signalSnapshot = transcriptSignals?.snapshot;
|
|
52192
|
+
if (profile.class === "native-source" && signalSnapshot?.available === true) {
|
|
52193
|
+
if (signalSnapshot.signals.final_assistant_present !== true) return false;
|
|
52194
|
+
finalSummary = extractFinalSummaryFromMessagesAfter(
|
|
52195
|
+
Array.isArray(transcriptSignals?.messages) ? transcriptSignals.messages : [],
|
|
52196
|
+
turnStartedAt
|
|
52197
|
+
) || void 0;
|
|
52198
|
+
} else {
|
|
52199
|
+
try {
|
|
52200
|
+
finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
|
|
52201
|
+
} catch {
|
|
52202
|
+
finalSummary = void 0;
|
|
52203
|
+
}
|
|
51810
52204
|
}
|
|
51811
52205
|
if (!finalSummary) return false;
|
|
51812
52206
|
const diagnosticSource = profile.class === "pure-pty" ? "stall_pure_pty_transcript_completion" : "stall_native_source_transcript_completion";
|
|
@@ -52073,24 +52467,25 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52073
52467
|
const isTranscriptEvidenceGate = block2.allowTimeout === true;
|
|
52074
52468
|
LOG.debug("CLI", `[${this.type}] finalization block: reason=${blockReason} terminal=${block2.terminal} allowTimeout=${isTranscriptEvidenceGate} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
52075
52469
|
if (blockReason === "missing_final_assistant" && block2.noExternalTranscriptSource === true) {
|
|
52076
|
-
let
|
|
52470
|
+
let growthSnapshot = null;
|
|
52077
52471
|
try {
|
|
52078
|
-
|
|
52472
|
+
growthSnapshot = this.probeNativeTranscriptSignals()?.snapshot ?? null;
|
|
52079
52473
|
} catch {
|
|
52080
|
-
|
|
52474
|
+
growthSnapshot = null;
|
|
52081
52475
|
}
|
|
52082
|
-
|
|
52083
|
-
|
|
52476
|
+
if (growthSnapshot?.available === true && growthSnapshot.signals.transcript_growing === true) {
|
|
52477
|
+
const growthDetail = growthSnapshot.detail;
|
|
52478
|
+
const mtimeAgeMs = growthDetail.ageMs ?? 0;
|
|
52084
52479
|
if (pending.loggedBlockReason !== "native_transcript_advancing") {
|
|
52085
|
-
LOG.info("CLI", `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${
|
|
52480
|
+
LOG.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`);
|
|
52086
52481
|
if (this.isMeshWorkerSession()) {
|
|
52087
|
-
traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${
|
|
52482
|
+
traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms`);
|
|
52088
52483
|
}
|
|
52089
52484
|
if (this.completionTraceOn()) this.recordCompletionGateTrace("hold", {
|
|
52090
52485
|
blockReason: "native_transcript_advancing",
|
|
52091
52486
|
latestVisibleStatus,
|
|
52092
|
-
msgCount:
|
|
52093
|
-
sourceMtimeAgeMs:
|
|
52487
|
+
msgCount: growthDetail.msgCount,
|
|
52488
|
+
sourceMtimeAgeMs: mtimeAgeMs,
|
|
52094
52489
|
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
|
|
52095
52490
|
waitedMs
|
|
52096
52491
|
});
|
|
@@ -62591,17 +62986,134 @@ var fastForwardHandlers = {
|
|
|
62591
62986
|
|
|
62592
62987
|
// src/commands/med-family/mesh-restart.ts
|
|
62593
62988
|
init_dist();
|
|
62989
|
+
init_logger();
|
|
62594
62990
|
var RESTART_BLOCKING_STATES = /* @__PURE__ */ new Set(["generating", "waiting_approval", "starting"]);
|
|
62595
|
-
|
|
62991
|
+
var DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
62992
|
+
var DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
62993
|
+
var DEFERRED_RESTART_POLL_MS = 1e4;
|
|
62994
|
+
function collectBlockingSessions(ctx, meshId) {
|
|
62995
|
+
const blocking = [];
|
|
62596
62996
|
const states = ctx.deps.instanceManager.collectAllStates();
|
|
62997
|
+
const consider = (state) => {
|
|
62998
|
+
const status = String(state?.status || "");
|
|
62999
|
+
if (!RESTART_BLOCKING_STATES.has(status)) return;
|
|
63000
|
+
blocking.push({
|
|
63001
|
+
instanceId: typeof state?.instanceId === "string" ? state.instanceId : null,
|
|
63002
|
+
status,
|
|
63003
|
+
// The coordinator marker is stamped by mesh_coordinator_launch and
|
|
63004
|
+
// re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
|
|
63005
|
+
// coordinator sessions have no marker and are NOT waived by selfOnly.
|
|
63006
|
+
selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId
|
|
63007
|
+
});
|
|
63008
|
+
};
|
|
62597
63009
|
for (const state of states) {
|
|
62598
|
-
|
|
63010
|
+
consider(state);
|
|
62599
63011
|
const childStates = "extensions" in state && Array.isArray(state.extensions) ? state.extensions : [];
|
|
62600
|
-
for (const child of childStates)
|
|
62601
|
-
|
|
62602
|
-
|
|
63012
|
+
for (const child of childStates) consider(child);
|
|
63013
|
+
}
|
|
63014
|
+
return blocking;
|
|
63015
|
+
}
|
|
63016
|
+
var pendingDeferredRestart = null;
|
|
63017
|
+
function deferredRestartInfo() {
|
|
63018
|
+
if (!pendingDeferredRestart) return null;
|
|
63019
|
+
return {
|
|
63020
|
+
meshId: pendingDeferredRestart.meshId,
|
|
63021
|
+
nodeId: pendingDeferredRestart.nodeId,
|
|
63022
|
+
mode: pendingDeferredRestart.mode,
|
|
63023
|
+
killSessionHost: pendingDeferredRestart.killSessionHost,
|
|
63024
|
+
scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
|
|
63025
|
+
expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
|
|
63026
|
+
runCondition: "executes automatically once no session is generating / waiting_approval / starting"
|
|
63027
|
+
};
|
|
63028
|
+
}
|
|
63029
|
+
function clearPendingDeferredRestart() {
|
|
63030
|
+
if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
|
|
63031
|
+
pendingDeferredRestart = null;
|
|
63032
|
+
}
|
|
63033
|
+
function normalizeRestartMode(value) {
|
|
63034
|
+
return value === "restart" ? "restart" : "upgrade";
|
|
63035
|
+
}
|
|
63036
|
+
function restartWarnings(args) {
|
|
63037
|
+
const warnings = [];
|
|
63038
|
+
if (args.forced) {
|
|
63039
|
+
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).");
|
|
63040
|
+
}
|
|
63041
|
+
if (args.killSessionHost) {
|
|
63042
|
+
warnings.push("killSessionHost: the session-host process was stopped \u2014 ALL hosted CLI sessions on this machine are destroyed (hard refresh).");
|
|
63043
|
+
}
|
|
63044
|
+
if (process.platform === "win32") {
|
|
63045
|
+
warnings.push("Windows: the daemon restart/upgrade path stops the session-host regardless of options \u2014 all hosted sessions terminate.");
|
|
63046
|
+
} else {
|
|
63047
|
+
warnings.push("POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).");
|
|
63048
|
+
}
|
|
63049
|
+
return warnings;
|
|
63050
|
+
}
|
|
63051
|
+
async function executeRestart(ctx, args, opts) {
|
|
63052
|
+
const mode = normalizeRestartMode(args?.mode);
|
|
63053
|
+
const killSessionHost = args?.killSessionHost === true;
|
|
63054
|
+
const result = mode === "restart" ? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost }) : await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
|
|
63055
|
+
const restarting = result?.restarting === true;
|
|
63056
|
+
return {
|
|
63057
|
+
...result,
|
|
63058
|
+
mode,
|
|
63059
|
+
restarted: restarting,
|
|
63060
|
+
...restarting ? {
|
|
63061
|
+
warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
|
|
63062
|
+
// MCP IPC has no automatic reconnect/retry — tell the caller when to
|
|
63063
|
+
// talk to this daemon again.
|
|
63064
|
+
clientHint: "daemon restarting \u2014 retry your next call in ~10s"
|
|
63065
|
+
} : {}
|
|
63066
|
+
};
|
|
63067
|
+
}
|
|
63068
|
+
function scheduleDeferredRestart(ctx, args, meshId, nodeId) {
|
|
63069
|
+
clearPendingDeferredRestart();
|
|
63070
|
+
const requestedTimeout = typeof args?.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? args.timeoutMs : DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
|
|
63071
|
+
const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
|
|
63072
|
+
const record = {
|
|
63073
|
+
meshId,
|
|
63074
|
+
nodeId,
|
|
63075
|
+
mode: normalizeRestartMode(args?.mode),
|
|
63076
|
+
killSessionHost: args?.killSessionHost === true,
|
|
63077
|
+
scheduledAt: Date.now(),
|
|
63078
|
+
expiresAt: Date.now() + timeoutMs,
|
|
63079
|
+
timer: setInterval(() => {
|
|
63080
|
+
void deferredRestartTick(ctx);
|
|
63081
|
+
}, DEFERRED_RESTART_POLL_MS)
|
|
63082
|
+
};
|
|
63083
|
+
record.timer.unref?.();
|
|
63084
|
+
pendingDeferredRestart = record;
|
|
63085
|
+
LOG.info("MeshRestart", `Deferred restart scheduled for node ${nodeId} (mode=${record.mode}, expires in ${Math.round(timeoutMs / 6e4)}min)`);
|
|
63086
|
+
return {
|
|
63087
|
+
success: true,
|
|
63088
|
+
restarted: false,
|
|
63089
|
+
scheduled: true,
|
|
63090
|
+
code: "restart_scheduled_when_idle",
|
|
63091
|
+
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.",
|
|
63092
|
+
deferredRestart: deferredRestartInfo()
|
|
63093
|
+
};
|
|
63094
|
+
}
|
|
63095
|
+
async function deferredRestartTick(ctx) {
|
|
63096
|
+
const record = pendingDeferredRestart;
|
|
63097
|
+
if (!record) return;
|
|
63098
|
+
if (Date.now() >= record.expiresAt) {
|
|
63099
|
+
LOG.warn("MeshRestart", `Deferred restart for node ${record.nodeId} expired without reaching an idle point; dropping the schedule`);
|
|
63100
|
+
clearPendingDeferredRestart();
|
|
63101
|
+
return;
|
|
63102
|
+
}
|
|
63103
|
+
const blocking = collectBlockingSessions(ctx, record.meshId);
|
|
63104
|
+
if (blocking.length > 0) return;
|
|
63105
|
+
LOG.info("MeshRestart", `Deferred restart for node ${record.nodeId} executing \u2014 daemon is idle`);
|
|
63106
|
+
clearPendingDeferredRestart();
|
|
63107
|
+
try {
|
|
63108
|
+
await executeRestart(ctx, {
|
|
63109
|
+
meshId: record.meshId,
|
|
63110
|
+
nodeId: record.nodeId,
|
|
63111
|
+
mode: record.mode,
|
|
63112
|
+
killSessionHost: record.killSessionHost
|
|
63113
|
+
}, { forced: false });
|
|
63114
|
+
} catch (e) {
|
|
63115
|
+
LOG.error("MeshRestart", `Deferred restart execution failed: ${e?.message || String(e)}`);
|
|
62603
63116
|
}
|
|
62604
|
-
return false;
|
|
62605
63117
|
}
|
|
62606
63118
|
var meshRestartHandlers = {
|
|
62607
63119
|
restart_daemon_node: async (ctx, args) => {
|
|
@@ -62622,16 +63134,43 @@ var meshRestartHandlers = {
|
|
|
62622
63134
|
});
|
|
62623
63135
|
return forwarded ?? { success: false, error: "no response from remote node" };
|
|
62624
63136
|
}
|
|
62625
|
-
if (
|
|
63137
|
+
if (args?.cancelWhenIdle === true) {
|
|
63138
|
+
const had = pendingDeferredRestart !== null;
|
|
63139
|
+
clearPendingDeferredRestart();
|
|
63140
|
+
return { success: true, restarted: false, cancelled: had, deferredRestart: null };
|
|
63141
|
+
}
|
|
63142
|
+
if (args?.whenIdleStatus === true) {
|
|
63143
|
+
return { success: true, restarted: false, deferredRestart: deferredRestartInfo() };
|
|
63144
|
+
}
|
|
63145
|
+
const blocking = collectBlockingSessions(ctx, meshId);
|
|
63146
|
+
if (blocking.length > 0) {
|
|
63147
|
+
if (args?.force === true) {
|
|
63148
|
+
LOG.warn("MeshRestart", `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || "?"}(${b.status})`).join(", ")} \u2014 pendingOutboundQueue will be lost`);
|
|
63149
|
+
return executeRestart(ctx, args, { forced: true });
|
|
63150
|
+
}
|
|
63151
|
+
const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
|
|
63152
|
+
if (args?.selfOnly === true && foreignBlocking.length === 0) {
|
|
63153
|
+
LOG.info("MeshRestart", `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
|
|
63154
|
+
return executeRestart(ctx, args, { forced: false });
|
|
63155
|
+
}
|
|
63156
|
+
if (args?.whenIdle === true) {
|
|
63157
|
+
return scheduleDeferredRestart(ctx, args, meshId, nodeId);
|
|
63158
|
+
}
|
|
62626
63159
|
return {
|
|
62627
63160
|
success: false,
|
|
62628
63161
|
restarted: false,
|
|
62629
63162
|
code: "blocking_sessions",
|
|
62630
|
-
reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle."
|
|
63163
|
+
reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.",
|
|
63164
|
+
blockingSessions: blocking,
|
|
63165
|
+
options: {
|
|
63166
|
+
selfOnly: "waive only this mesh's own coordinator session (settings.meshCoordinatorFor === meshId)",
|
|
63167
|
+
force: "bypass the idle-gate entirely \u2014 kills in-flight turns and loses the unpersisted pendingOutboundQueue",
|
|
63168
|
+
whenIdle: "schedule the restart to run automatically once the daemon goes idle (safest)"
|
|
63169
|
+
},
|
|
63170
|
+
deferredRestart: deferredRestartInfo()
|
|
62631
63171
|
};
|
|
62632
63172
|
}
|
|
62633
|
-
|
|
62634
|
-
return { ...result, restarted: result?.restarting === true };
|
|
63173
|
+
return executeRestart(ctx, args, { forced: false });
|
|
62635
63174
|
}
|
|
62636
63175
|
};
|
|
62637
63176
|
|