@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.js
CHANGED
|
@@ -791,10 +791,10 @@ function readInjected(value) {
|
|
|
791
791
|
}
|
|
792
792
|
function getDaemonBuildInfo() {
|
|
793
793
|
if (cached) return cached;
|
|
794
|
-
const commit = readInjected(true ? "
|
|
795
|
-
const commitShort = readInjected(true ? "
|
|
796
|
-
const version = readInjected(true ? "1.0.28-rc.
|
|
797
|
-
const builtAt = readInjected(true ? "2026-07-
|
|
794
|
+
const commit = readInjected(true ? "6f38d1460a02018ef53a3ecdf306004005b44310" : void 0) ?? "unknown";
|
|
795
|
+
const commitShort = readInjected(true ? "6f38d146" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
|
|
796
|
+
const version = readInjected(true ? "1.0.28-rc.12" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
|
|
797
|
+
const builtAt = readInjected(true ? "2026-07-26T08:24:03.072Z" : void 0);
|
|
798
798
|
cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
|
|
799
799
|
return cached;
|
|
800
800
|
}
|
|
@@ -27260,6 +27260,11 @@ function validateCondition(c, sectionIds, path46) {
|
|
|
27260
27260
|
return errs;
|
|
27261
27261
|
}
|
|
27262
27262
|
if ("cursor_above" in w && "changed" in w) return errs;
|
|
27263
|
+
if ("signal" in w) {
|
|
27264
|
+
if (typeof w.signal !== "string" || !w.signal.trim()) errs.push(`${path46}.signal must be a non-empty string`);
|
|
27265
|
+
if (w.equals !== void 0 && typeof w.equals !== "boolean") errs.push(`${path46}.equals must be a boolean`);
|
|
27266
|
+
return errs;
|
|
27267
|
+
}
|
|
27263
27268
|
if ("elapsed_ms" in w) {
|
|
27264
27269
|
if (typeof w.elapsed_ms !== "number") errs.push(`${path46}.elapsed_ms must be a number`);
|
|
27265
27270
|
return errs;
|
|
@@ -27576,6 +27581,51 @@ var init_evaluator = __esm({
|
|
|
27576
27581
|
}
|
|
27577
27582
|
});
|
|
27578
27583
|
|
|
27584
|
+
// src/providers/spec/signal-envelope.ts
|
|
27585
|
+
function unavailableSignalSnapshot(now, reason, profile) {
|
|
27586
|
+
return {
|
|
27587
|
+
kind: SIGNAL_SNAPSHOT_KIND,
|
|
27588
|
+
sampledAt: now,
|
|
27589
|
+
available: false,
|
|
27590
|
+
unavailableReason: reason,
|
|
27591
|
+
...profile ? { profile } : {},
|
|
27592
|
+
signals: { final_assistant_present: null, in_turn_progress: null, transcript_growing: null },
|
|
27593
|
+
detail: { msgCount: 0, sourceMtimeMs: 0, ageMs: null }
|
|
27594
|
+
};
|
|
27595
|
+
}
|
|
27596
|
+
function evaluateSignalLeaf(cond, snapshot) {
|
|
27597
|
+
const expected = cond.equals ?? true;
|
|
27598
|
+
if (!snapshot || !snapshot.available) {
|
|
27599
|
+
const why = !snapshot ? "no snapshot" : `unavailable(${snapshot.unavailableReason ?? "unknown"})`;
|
|
27600
|
+
return {
|
|
27601
|
+
value: null,
|
|
27602
|
+
shadowResult: null,
|
|
27603
|
+
detail: `signal ${cond.signal} ${why} \u2192 fail-open`
|
|
27604
|
+
};
|
|
27605
|
+
}
|
|
27606
|
+
const raw = Object.prototype.hasOwnProperty.call(snapshot.signals, cond.signal) ? snapshot.signals[cond.signal] : null;
|
|
27607
|
+
if (raw === null || raw === void 0) {
|
|
27608
|
+
return {
|
|
27609
|
+
value: null,
|
|
27610
|
+
shadowResult: null,
|
|
27611
|
+
detail: `signal ${cond.signal} unknown \u2192 fail-open`
|
|
27612
|
+
};
|
|
27613
|
+
}
|
|
27614
|
+
const shadowResult = raw === expected;
|
|
27615
|
+
return {
|
|
27616
|
+
value: raw,
|
|
27617
|
+
shadowResult,
|
|
27618
|
+
detail: `signal ${cond.signal}=${raw} expected=${expected} \u2192 ${shadowResult}`
|
|
27619
|
+
};
|
|
27620
|
+
}
|
|
27621
|
+
var SIGNAL_SNAPSHOT_KIND;
|
|
27622
|
+
var init_signal_envelope = __esm({
|
|
27623
|
+
"src/providers/spec/signal-envelope.ts"() {
|
|
27624
|
+
"use strict";
|
|
27625
|
+
SIGNAL_SNAPSHOT_KIND = "adhdev:fsm/signal-snapshot@0";
|
|
27626
|
+
}
|
|
27627
|
+
});
|
|
27628
|
+
|
|
27579
27629
|
// src/providers/spec/fsm-evaluator.ts
|
|
27580
27630
|
var fsm_evaluator_exports = {};
|
|
27581
27631
|
__export(fsm_evaluator_exports, {
|
|
@@ -27609,24 +27659,74 @@ function isAny(c) {
|
|
|
27609
27659
|
function isNot(c) {
|
|
27610
27660
|
return "not" in c;
|
|
27611
27661
|
}
|
|
27612
|
-
function
|
|
27662
|
+
function isSignal(c) {
|
|
27663
|
+
return "signal" in c;
|
|
27664
|
+
}
|
|
27665
|
+
function containsSignal(c) {
|
|
27666
|
+
if (isSignal(c)) return true;
|
|
27667
|
+
if (isAll(c)) return c.all.some(containsSignal);
|
|
27668
|
+
if (isAny(c)) return c.any.some(containsSignal);
|
|
27669
|
+
if (isNot(c)) return containsSignal(c.not);
|
|
27670
|
+
return false;
|
|
27671
|
+
}
|
|
27672
|
+
function foldShadow(cond) {
|
|
27673
|
+
if (cond.kind === "signal") {
|
|
27674
|
+
const s2 = cond.signal;
|
|
27675
|
+
if (s2 && s2.shadowResult !== null && s2.shadowResult !== void 0) {
|
|
27676
|
+
return { value: s2.shadowResult, unknown: false };
|
|
27677
|
+
}
|
|
27678
|
+
return { value: true, unknown: true };
|
|
27679
|
+
}
|
|
27680
|
+
if (cond.kind === "not" && cond.children?.length) {
|
|
27681
|
+
const c = foldShadow(cond.children[0]);
|
|
27682
|
+
return { value: !c.value, unknown: c.unknown };
|
|
27683
|
+
}
|
|
27684
|
+
if (cond.kind === "all" && cond.children) {
|
|
27685
|
+
const kids = cond.children.map(foldShadow);
|
|
27686
|
+
if (kids.some((k) => !k.unknown && !k.value)) return { value: false, unknown: false };
|
|
27687
|
+
if (kids.some((k) => k.unknown)) return { value: true, unknown: true };
|
|
27688
|
+
return { value: true, unknown: false };
|
|
27689
|
+
}
|
|
27690
|
+
if (cond.kind === "any" && cond.children) {
|
|
27691
|
+
const kids = cond.children.map(foldShadow);
|
|
27692
|
+
if (kids.some((k) => !k.unknown && k.value)) return { value: true, unknown: false };
|
|
27693
|
+
if (kids.some((k) => k.unknown)) return { value: false, unknown: true };
|
|
27694
|
+
return { value: false, unknown: false };
|
|
27695
|
+
}
|
|
27696
|
+
return { value: cond.result, unknown: false };
|
|
27697
|
+
}
|
|
27698
|
+
function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot) {
|
|
27613
27699
|
if (isAll(cond)) {
|
|
27614
|
-
const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
|
|
27700
|
+
const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
|
|
27615
27701
|
const result = children.every((c) => c.result);
|
|
27616
27702
|
const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
|
|
27617
27703
|
return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
|
|
27618
27704
|
}
|
|
27619
27705
|
if (isAny(cond)) {
|
|
27620
|
-
const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
|
|
27706
|
+
const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot));
|
|
27621
27707
|
const result = children.some((c) => c.result);
|
|
27622
27708
|
const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
|
|
27623
27709
|
const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
|
|
27624
27710
|
return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
|
|
27625
27711
|
}
|
|
27626
27712
|
if (isNot(cond)) {
|
|
27627
|
-
const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
|
|
27713
|
+
const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId, signalSnapshot);
|
|
27628
27714
|
return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
|
|
27629
27715
|
}
|
|
27716
|
+
if (isSignal(cond)) {
|
|
27717
|
+
const leaf = evaluateSignalLeaf(cond, signalSnapshot);
|
|
27718
|
+
return {
|
|
27719
|
+
kind: "signal",
|
|
27720
|
+
result: true,
|
|
27721
|
+
detail: `${leaf.detail} (stage0 shadow \u2014 pass-through)`,
|
|
27722
|
+
signal: {
|
|
27723
|
+
name: cond.signal,
|
|
27724
|
+
available: !!signalSnapshot?.available,
|
|
27725
|
+
value: leaf.value,
|
|
27726
|
+
shadowResult: leaf.shadowResult
|
|
27727
|
+
}
|
|
27728
|
+
};
|
|
27729
|
+
}
|
|
27630
27730
|
if (isElapsed(cond)) {
|
|
27631
27731
|
const age = clock.now - clock.stateEnteredAt;
|
|
27632
27732
|
const result = age >= cond.elapsed_ms;
|
|
@@ -27665,7 +27765,7 @@ function fromLabel(t) {
|
|
|
27665
27765
|
const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
|
|
27666
27766
|
return t.label ?? `${from}\u2192${t.to}`;
|
|
27667
27767
|
}
|
|
27668
|
-
function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
|
|
27768
|
+
function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock, signalSnapshot) {
|
|
27669
27769
|
const legacyTrace = [];
|
|
27670
27770
|
const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
|
|
27671
27771
|
const cleanScreen = lines.join("\n");
|
|
@@ -27681,7 +27781,7 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
|
|
|
27681
27781
|
let cond;
|
|
27682
27782
|
let condResult = true;
|
|
27683
27783
|
if (t.when) {
|
|
27684
|
-
cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}
|
|
27784
|
+
cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`, signalSnapshot);
|
|
27685
27785
|
condResult = cond.result;
|
|
27686
27786
|
}
|
|
27687
27787
|
const fires = holdSatisfied && condResult;
|
|
@@ -27696,6 +27796,14 @@ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock)
|
|
|
27696
27796
|
fires,
|
|
27697
27797
|
priority: t.priority ?? 0
|
|
27698
27798
|
};
|
|
27799
|
+
if (t.when && containsSignal(t.when) && cond) {
|
|
27800
|
+
const folded = foldShadow(cond);
|
|
27801
|
+
te.shadow = {
|
|
27802
|
+
condResult: folded.value,
|
|
27803
|
+
fires: holdSatisfied && folded.value,
|
|
27804
|
+
unknown: folded.unknown
|
|
27805
|
+
};
|
|
27806
|
+
}
|
|
27699
27807
|
transitions.push(te);
|
|
27700
27808
|
if (fires && !fired) fired = te;
|
|
27701
27809
|
}
|
|
@@ -27718,6 +27826,7 @@ var init_fsm_evaluator = __esm({
|
|
|
27718
27826
|
"use strict";
|
|
27719
27827
|
init_evaluator();
|
|
27720
27828
|
init_fsm_types();
|
|
27829
|
+
init_signal_envelope();
|
|
27721
27830
|
WHOLE_SCREEN = -1;
|
|
27722
27831
|
}
|
|
27723
27832
|
});
|
|
@@ -45515,12 +45624,21 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45515
45624
|
appendUpgradeLog(`Waiting for parent pid ${payload.parentPid} to exit`);
|
|
45516
45625
|
await waitForPidExit(payload.parentPid, 15e3);
|
|
45517
45626
|
}
|
|
45518
|
-
if (process.platform === "win32") {
|
|
45627
|
+
if (process.platform === "win32" || payload.killSessionHost === true) {
|
|
45519
45628
|
await stopSessionHostProcesses(sessionHostAppName);
|
|
45520
45629
|
} else {
|
|
45521
45630
|
appendUpgradeLog("POSIX \u2014 session-host left running (survives upgrade; sessions rebind on next boot)");
|
|
45522
45631
|
}
|
|
45523
45632
|
removeDaemonPidFile();
|
|
45633
|
+
if (payload.skipInstall) {
|
|
45634
|
+
appendUpgradeLog("Restart-only mode \u2014 package install skipped, re-spawning daemon");
|
|
45635
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
45636
|
+
try {
|
|
45637
|
+
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45638
|
+
} catch {
|
|
45639
|
+
}
|
|
45640
|
+
return;
|
|
45641
|
+
}
|
|
45524
45642
|
const instanceDir = resolveInstanceDir();
|
|
45525
45643
|
const windowsInstallerLayout = resolveWindowsInstallerLayout({
|
|
45526
45644
|
homeDir: os14.homedir(),
|
|
@@ -45639,6 +45757,13 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45639
45757
|
cleanupStaleGlobalInstallDirs(payload.packageName, installCommand.surface);
|
|
45640
45758
|
appendUpgradeLog("Post-install staging cleanup complete");
|
|
45641
45759
|
}
|
|
45760
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
45761
|
+
try {
|
|
45762
|
+
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45763
|
+
} catch {
|
|
45764
|
+
}
|
|
45765
|
+
}
|
|
45766
|
+
function spawnDetachedDaemonRestart(restartArgv, cwd) {
|
|
45642
45767
|
if (restartArgv.length > 0) {
|
|
45643
45768
|
const env = { ...process.env };
|
|
45644
45769
|
delete env[UPGRADE_HELPER_ENV];
|
|
@@ -45647,17 +45772,13 @@ async function runDaemonUpgradeHelper(payload) {
|
|
|
45647
45772
|
detached: true,
|
|
45648
45773
|
stdio: "ignore",
|
|
45649
45774
|
windowsHide: true,
|
|
45650
|
-
cwd:
|
|
45775
|
+
cwd: cwd || process.cwd(),
|
|
45651
45776
|
env
|
|
45652
45777
|
});
|
|
45653
45778
|
child.unref();
|
|
45654
45779
|
} else {
|
|
45655
45780
|
appendUpgradeLog("No restart argv provided; upgrade completed without restart");
|
|
45656
45781
|
}
|
|
45657
|
-
try {
|
|
45658
|
-
fs15.unlinkSync(getUpgradeFailureNoticePath());
|
|
45659
|
-
} catch {
|
|
45660
|
-
}
|
|
45661
45782
|
}
|
|
45662
45783
|
async function maybeRunDaemonUpgradeHelperFromEnv() {
|
|
45663
45784
|
const raw = process.env[UPGRADE_HELPER_ENV];
|
|
@@ -45750,6 +45871,41 @@ var daemonLifecycleHandlers = {
|
|
|
45750
45871
|
return { success: false, error: e.message };
|
|
45751
45872
|
}
|
|
45752
45873
|
},
|
|
45874
|
+
// Restart-only path (mesh restart_daemon_node mode="restart"): re-spawn the
|
|
45875
|
+
// daemon WITHOUT the npm reinstall daemon_upgrade performs. Used to reset
|
|
45876
|
+
// daemon state (memory leaks, zombie sessions, wedged internals) when the
|
|
45877
|
+
// version is already correct — downtime drops to the detached re-spawn.
|
|
45878
|
+
// killSessionHost is an explicit opt-in hard refresh (see upgrade-helper).
|
|
45879
|
+
daemon_restart: async (ctx, args) => {
|
|
45880
|
+
LOG.info("Restart", "Restart-only requested (no package reinstall)");
|
|
45881
|
+
try {
|
|
45882
|
+
const isStandalone = ctx.deps.packageName === "@adhdev/daemon-standalone" || process.argv[1]?.includes("daemon-standalone");
|
|
45883
|
+
const pkgName = isStandalone ? "@adhdev/daemon-standalone" : "adhdev";
|
|
45884
|
+
const killSessionHost = args?.killSessionHost === true;
|
|
45885
|
+
if (killSessionHost) {
|
|
45886
|
+
LOG.warn("Restart", "killSessionHost requested \u2014 session-host will be stopped and ALL hosted sessions destroyed");
|
|
45887
|
+
}
|
|
45888
|
+
spawnDetachedDaemonUpgradeHelper({
|
|
45889
|
+
packageName: pkgName,
|
|
45890
|
+
targetVersion: "",
|
|
45891
|
+
parentPid: process.pid,
|
|
45892
|
+
restartArgv: process.argv.slice(1),
|
|
45893
|
+
cwd: process.cwd(),
|
|
45894
|
+
sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || "adhdev",
|
|
45895
|
+
skipInstall: true,
|
|
45896
|
+
killSessionHost
|
|
45897
|
+
});
|
|
45898
|
+
LOG.info("Restart", "Scheduled detached restart-only helper");
|
|
45899
|
+
setTimeout(() => {
|
|
45900
|
+
LOG.info("Restart", "Exiting daemon so detached helper can re-spawn it...");
|
|
45901
|
+
process.exit(0);
|
|
45902
|
+
}, 3e3);
|
|
45903
|
+
return { success: true, restarted: true, restarting: true, mode: "restart", killSessionHost };
|
|
45904
|
+
} catch (e) {
|
|
45905
|
+
LOG.error("Restart", `Failed: ${e.message}`);
|
|
45906
|
+
return { success: false, error: e.message };
|
|
45907
|
+
}
|
|
45908
|
+
},
|
|
45753
45909
|
set_machine_nickname: async (_ctx, args) => {
|
|
45754
45910
|
const nickname = args?.nickname;
|
|
45755
45911
|
updateConfig({ machineNickname: nickname || null });
|
|
@@ -46246,6 +46402,93 @@ init_provider_input_support();
|
|
|
46246
46402
|
init_hash();
|
|
46247
46403
|
init_transcript_evidence();
|
|
46248
46404
|
|
|
46405
|
+
// src/providers/transcript-signal-source.ts
|
|
46406
|
+
init_logger();
|
|
46407
|
+
init_signal_envelope();
|
|
46408
|
+
var TranscriptSignalSource = class {
|
|
46409
|
+
constructor(opts) {
|
|
46410
|
+
this.opts = opts;
|
|
46411
|
+
}
|
|
46412
|
+
/** msgCount seen at the previous update — drives in_turn_progress. */
|
|
46413
|
+
prevMsgCount = -1;
|
|
46414
|
+
/** Fingerprint of the last LOGGED snapshot, so the shadow log fires on
|
|
46415
|
+
* change only (a quiet session must not spam one line per read). */
|
|
46416
|
+
lastLoggedFingerprint = "";
|
|
46417
|
+
/**
|
|
46418
|
+
* Normalize one daemon read into a SignalSnapshot and emit the Stage-0
|
|
46419
|
+
* shadow log when the observation changed. Pure w.r.t. the outside world:
|
|
46420
|
+
* the only side effect is the (change-gated) log line. Returns the
|
|
46421
|
+
* snapshot the caller should inject into the FSM driver.
|
|
46422
|
+
*/
|
|
46423
|
+
update(sample, now = Date.now()) {
|
|
46424
|
+
const snapshot = this.buildSnapshot(sample, now);
|
|
46425
|
+
this.logOnChange(snapshot);
|
|
46426
|
+
return snapshot;
|
|
46427
|
+
}
|
|
46428
|
+
/** Pure normalization — separated from update() so tests can assert the
|
|
46429
|
+
* envelope without touching the log path. */
|
|
46430
|
+
buildSnapshot(sample, now) {
|
|
46431
|
+
const profileRef = { class: this.opts.profile.class, timing: this.opts.profile.timing };
|
|
46432
|
+
if (this.opts.profile.class !== "native-source") {
|
|
46433
|
+
return unavailableSignalSnapshot(now, "no_native_source", profileRef);
|
|
46434
|
+
}
|
|
46435
|
+
if (sample.error) {
|
|
46436
|
+
return unavailableSignalSnapshot(now, "error", profileRef);
|
|
46437
|
+
}
|
|
46438
|
+
const probe = sample.probe;
|
|
46439
|
+
if (!probe || !Array.isArray(sample.messages)) {
|
|
46440
|
+
return unavailableSignalSnapshot(now, "unresolved", profileRef);
|
|
46441
|
+
}
|
|
46442
|
+
const msgCount = typeof probe.msgCount === "number" && Number.isFinite(probe.msgCount) ? probe.msgCount : sample.messages.length;
|
|
46443
|
+
const sourceMtimeMs = typeof probe.sourceMtimeMs === "number" && Number.isFinite(probe.sourceMtimeMs) ? probe.sourceMtimeMs : 0;
|
|
46444
|
+
const ageMs = sourceMtimeMs > 0 ? Math.max(0, now - sourceMtimeMs) : null;
|
|
46445
|
+
let turnStartedAt;
|
|
46446
|
+
try {
|
|
46447
|
+
turnStartedAt = this.opts.turnStartedAt?.();
|
|
46448
|
+
} catch {
|
|
46449
|
+
turnStartedAt = void 0;
|
|
46450
|
+
}
|
|
46451
|
+
let finalAssistant = null;
|
|
46452
|
+
try {
|
|
46453
|
+
finalAssistant = this.opts.finalAssistantPresent(sample.messages, turnStartedAt);
|
|
46454
|
+
} catch {
|
|
46455
|
+
finalAssistant = null;
|
|
46456
|
+
}
|
|
46457
|
+
const countAdvanced = this.prevMsgCount >= 0 && msgCount > this.prevMsgCount;
|
|
46458
|
+
const fresh = ageMs !== null && ageMs < this.opts.growthQuietMs;
|
|
46459
|
+
const inTurnProgress = countAdvanced || fresh;
|
|
46460
|
+
const transcriptGrowing = ageMs === null ? null : fresh;
|
|
46461
|
+
this.prevMsgCount = msgCount;
|
|
46462
|
+
return {
|
|
46463
|
+
kind: SIGNAL_SNAPSHOT_KIND,
|
|
46464
|
+
sampledAt: now,
|
|
46465
|
+
available: true,
|
|
46466
|
+
profile: profileRef,
|
|
46467
|
+
signals: {
|
|
46468
|
+
final_assistant_present: finalAssistant,
|
|
46469
|
+
in_turn_progress: inTurnProgress,
|
|
46470
|
+
transcript_growing: transcriptGrowing
|
|
46471
|
+
},
|
|
46472
|
+
detail: { msgCount, sourceMtimeMs, ageMs }
|
|
46473
|
+
};
|
|
46474
|
+
}
|
|
46475
|
+
/** Stage-0 shadow log: emit one line when the normalized observation
|
|
46476
|
+
* CHANGES. This log is the Stage 1-3 judgment input ("which signals were
|
|
46477
|
+
* actually observable, and when"), so it carries the full signal set —
|
|
46478
|
+
* but only on change, so a steady-state session stays quiet. */
|
|
46479
|
+
logOnChange(snapshot) {
|
|
46480
|
+
const s2 = snapshot.signals;
|
|
46481
|
+
const fingerprint = snapshot.available ? `1|fa=${s2.final_assistant_present}|tp=${s2.in_turn_progress}|tg=${s2.transcript_growing}|n=${snapshot.detail.msgCount}` : `0|${snapshot.unavailableReason ?? "unknown"}`;
|
|
46482
|
+
if (fingerprint === this.lastLoggedFingerprint) return;
|
|
46483
|
+
this.lastLoggedFingerprint = fingerprint;
|
|
46484
|
+
const age = snapshot.detail.ageMs === null ? "n/a" : `${snapshot.detail.ageMs}ms`;
|
|
46485
|
+
LOG.info(
|
|
46486
|
+
"TranscriptSignalSource",
|
|
46487
|
+
`[${this.opts.label}] [shadow] signals available=${snapshot.available}` + (snapshot.available ? ` final_assistant_present=${s2.final_assistant_present} in_turn_progress=${s2.in_turn_progress} transcript_growing=${s2.transcript_growing} msgCount=${snapshot.detail.msgCount} age=${age}` : ` reason=${snapshot.unavailableReason ?? "unknown"}`)
|
|
46488
|
+
);
|
|
46489
|
+
}
|
|
46490
|
+
};
|
|
46491
|
+
|
|
46249
46492
|
// src/providers/spec/route.ts
|
|
46250
46493
|
var fs22 = __toESM(require("fs"));
|
|
46251
46494
|
var path26 = __toESM(require("path"));
|
|
@@ -46610,6 +46853,15 @@ var FsmDriver = class {
|
|
|
46610
46853
|
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
46611
46854
|
* for the single most recent evaluation. Separate from stateHistory. */
|
|
46612
46855
|
fsmSnapshotHistory = [];
|
|
46856
|
+
/** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
|
|
46857
|
+
* Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
|
|
46858
|
+
* the Stage-0 pass-through in the evaluator means it can never alter
|
|
46859
|
+
* which transition fires. */
|
|
46860
|
+
signalObservation = null;
|
|
46861
|
+
/** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
|
|
46862
|
+
* the shadow verdict currently disagrees with the real one), so the
|
|
46863
|
+
* shadow log emits on FLIP only, not every frame. */
|
|
46864
|
+
shadowDivergenceLast = /* @__PURE__ */ new Map();
|
|
46613
46865
|
subscribe(listener) {
|
|
46614
46866
|
this.listeners.add(listener);
|
|
46615
46867
|
return () => {
|
|
@@ -46682,6 +46934,16 @@ var FsmDriver = class {
|
|
|
46682
46934
|
updateMeta(meta, replace = false) {
|
|
46683
46935
|
this.adapter.updateMeta(meta, replace);
|
|
46684
46936
|
}
|
|
46937
|
+
/**
|
|
46938
|
+
* TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
|
|
46939
|
+
* observation. The driver stores it verbatim — it does NOT poll, parse,
|
|
46940
|
+
* or read anything itself (generic-PTY-engine boundary). The observation
|
|
46941
|
+
* only feeds the shadow verdict of `signal` conditions; the evaluator's
|
|
46942
|
+
* Stage-0 pass-through guarantees it cannot change a transition.
|
|
46943
|
+
*/
|
|
46944
|
+
setSignalObservation(snapshot) {
|
|
46945
|
+
this.signalObservation = snapshot ?? null;
|
|
46946
|
+
}
|
|
46685
46947
|
snapshot() {
|
|
46686
46948
|
return this.adapter.snapshot();
|
|
46687
46949
|
}
|
|
@@ -46872,7 +47134,35 @@ var FsmDriver = class {
|
|
|
46872
47134
|
}
|
|
46873
47135
|
evalFsmNow(screen, cursor, now) {
|
|
46874
47136
|
const prev = this.prevScreenLines.length > 0 ? this.prevScreenLines : void 0;
|
|
46875
|
-
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now));
|
|
47137
|
+
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now), this.signalObservation);
|
|
47138
|
+
}
|
|
47139
|
+
/**
|
|
47140
|
+
* TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
|
|
47141
|
+
* counterfactual verdict against the real (PTY-only) one and log on FLIP.
|
|
47142
|
+
* This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
|
|
47143
|
+
* evidence for whether signal gating would have changed any verdict, and
|
|
47144
|
+
* in which direction. Read-only: runs after the evaluation is complete
|
|
47145
|
+
* and never feeds back into it.
|
|
47146
|
+
*/
|
|
47147
|
+
logShadowDivergence(ev) {
|
|
47148
|
+
for (const t of ev.transitions) {
|
|
47149
|
+
if (!t.shadow) continue;
|
|
47150
|
+
const key2 = `${this.currentStateId}\u2192${t.to}`;
|
|
47151
|
+
const diverges = t.shadow.fires !== t.fires;
|
|
47152
|
+
if ((this.shadowDivergenceLast.get(key2) ?? false) === diverges) continue;
|
|
47153
|
+
this.shadowDivergenceLast.set(key2, diverges);
|
|
47154
|
+
if (diverges) {
|
|
47155
|
+
LOG.info(
|
|
47156
|
+
"FsmDriver",
|
|
47157
|
+
`[${this.specTag()}] [shadow] ${key2} (${t.label}): real fires=${t.fires} but signal-gated verdict would be fires=${t.shadow.fires} (shadowCond=${t.shadow.condResult}${t.shadow.unknown ? ", signal unknown/fail-open" : ""})`
|
|
47158
|
+
);
|
|
47159
|
+
} else {
|
|
47160
|
+
LOG.info(
|
|
47161
|
+
"FsmDriver",
|
|
47162
|
+
`[${this.specTag()}] [shadow] ${key2} (${t.label}): shadow verdict realigned with real fires=${t.fires}`
|
|
47163
|
+
);
|
|
47164
|
+
}
|
|
47165
|
+
}
|
|
46876
47166
|
}
|
|
46877
47167
|
reevaluate(forceEmit = false) {
|
|
46878
47168
|
const now = Date.now();
|
|
@@ -46883,6 +47173,7 @@ var FsmDriver = class {
|
|
|
46883
47173
|
const ev = this.evalFsmNow(screen, cursor, now);
|
|
46884
47174
|
this.lastFsmEval = ev;
|
|
46885
47175
|
this.prevScreenLines = currentLines;
|
|
47176
|
+
this.logShadowDivergence(ev);
|
|
46886
47177
|
if (ev.fired) {
|
|
46887
47178
|
this.commitTransition(ev.fired, now, ev);
|
|
46888
47179
|
this.emitStateChanged(forceEmit);
|
|
@@ -49531,6 +49822,18 @@ var SpecCliAdapter = class _SpecCliAdapter {
|
|
|
49531
49822
|
}
|
|
49532
49823
|
refreshProviderDefinition() {
|
|
49533
49824
|
}
|
|
49825
|
+
/**
|
|
49826
|
+
* TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
|
|
49827
|
+
* observation into the FSM driver. Observation-only — failures here must
|
|
49828
|
+
* never break the adapter, and the driver treats the envelope as a pure
|
|
49829
|
+
* injected value (no reads cross the engine boundary).
|
|
49830
|
+
*/
|
|
49831
|
+
setSignalObservation(snapshot) {
|
|
49832
|
+
try {
|
|
49833
|
+
this.driver.setSignalObservation?.(snapshot);
|
|
49834
|
+
} catch {
|
|
49835
|
+
}
|
|
49836
|
+
}
|
|
49534
49837
|
handleEvent(ev) {
|
|
49535
49838
|
this.lastEvent = ev;
|
|
49536
49839
|
switch (ev.kind) {
|
|
@@ -50576,18 +50879,20 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
50576
50879
|
// MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
|
|
50577
50880
|
// anchor re-arms (the per-anchor guard only covers a single continuous stall).
|
|
50578
50881
|
meshStallLastFiredAt = -1;
|
|
50579
|
-
// KIMI-MESH-COMPLETION-EMIT (axis 1)
|
|
50580
|
-
//
|
|
50581
|
-
//
|
|
50582
|
-
//
|
|
50583
|
-
//
|
|
50584
|
-
//
|
|
50585
|
-
//
|
|
50586
|
-
//
|
|
50587
|
-
//
|
|
50588
|
-
// handling can lead to the worker being stopped mid-work → completion
|
|
50589
|
-
// emitted).
|
|
50590
|
-
|
|
50882
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 1) — TX-FSM Stage 1: whether the stall
|
|
50883
|
+
// watchdog has consumed a usable native-transcript SIGNAL SNAPSHOT for the
|
|
50884
|
+
// current stall episode. A pure-PTY native-source worker running a long,
|
|
50885
|
+
// screen-quiet tool (no viewport bytes for minutes) looks stalled by the
|
|
50886
|
+
// lastOutputAt clock even though its transcript file is still growing.
|
|
50887
|
+
// Before firing the stall, the watchdog consults the shared
|
|
50888
|
+
// TranscriptSignalSource's in_turn_progress signal; if the transcript is
|
|
50889
|
+
// advancing, the "stall" is a false positive of PTY-render stasis, so we
|
|
50890
|
+
// re-arm the anchor instead of paging the coordinator (whose no_progress
|
|
50891
|
+
// handling can lead to the worker being stopped mid-work → completion
|
|
50892
|
+
// never emitted). The FIRST usable sample of an episode still re-arms
|
|
50893
|
+
// unconditionally (no episode-scoped baseline exists yet to prove stasis
|
|
50894
|
+
// against) — the historical `!prev → advanced` semantics, preserved.
|
|
50895
|
+
meshStallTranscriptSignalSampled = false;
|
|
50591
50896
|
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
50592
50897
|
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
50593
50898
|
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
@@ -51385,6 +51690,17 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51385
51690
|
completedDebounceTimer = null;
|
|
51386
51691
|
completedDebouncePending = null;
|
|
51387
51692
|
lastExternalCompletionProbe = null;
|
|
51693
|
+
/** TX-FSM: lazily-created transcript signal normalizer. Fed ONLY by
|
|
51694
|
+
* transcript reads this instance already performs — it adds zero I/O.
|
|
51695
|
+
* Stage 0: its output was a pure shadow observation for the FSM driver.
|
|
51696
|
+
* Stage 1: the instance's own stall/growth-hold judgments consume the
|
|
51697
|
+
* normalized snapshot too (single source of truth). */
|
|
51698
|
+
transcriptSignalSource = null;
|
|
51699
|
+
/** TX-FSM Stage 1: the latest snapshot the source produced (set inside
|
|
51700
|
+
* publishTranscriptSignalObservation). Consumed the same tick by the
|
|
51701
|
+
* stall-path / growth-hold judgments via probeNativeTranscriptSignals —
|
|
51702
|
+
* never treated as fresh across ticks. */
|
|
51703
|
+
lastTranscriptSignalSnapshot = null;
|
|
51388
51704
|
/**
|
|
51389
51705
|
* The final assistant summary of the last completed turn, cached at
|
|
51390
51706
|
* completion-emit time. For a native-source provider (antigravity) whose
|
|
@@ -51550,6 +51866,7 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51550
51866
|
});
|
|
51551
51867
|
if (restoredHistory.source !== "provider-native") {
|
|
51552
51868
|
this.lastExternalCompletionProbe = null;
|
|
51869
|
+
this.publishTranscriptSignalObservation(null);
|
|
51553
51870
|
return null;
|
|
51554
51871
|
}
|
|
51555
51872
|
this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
|
|
@@ -51557,8 +51874,55 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
51557
51874
|
restoredHistory.sourcePath,
|
|
51558
51875
|
restoredHistory.sourceMtimeMs
|
|
51559
51876
|
);
|
|
51877
|
+
this.publishTranscriptSignalObservation(restoredHistory.messages);
|
|
51560
51878
|
return restoredHistory.messages;
|
|
51561
51879
|
}
|
|
51880
|
+
/**
|
|
51881
|
+
* TX-FSM: normalize the transcript read that JUST happened into a
|
|
51882
|
+
* SignalSnapshot. Stage 0 injected it into the FSM driver as a pure
|
|
51883
|
+
* shadow observation (daemon → SpecCliAdapter → FsmDriver); Stage 1
|
|
51884
|
+
* additionally caches it (lastTranscriptSignalSnapshot) so the instance's
|
|
51885
|
+
* OWN stall/growth-hold judgments consume the SAME normalized snapshot
|
|
51886
|
+
* instead of re-running private transcript scans. Fed ONLY by reads this
|
|
51887
|
+
* method's caller already performs — it adds zero I/O, so the getState()
|
|
51888
|
+
* zero-native-read invariant and the stall-path read cadence are
|
|
51889
|
+
* untouched. The source update runs regardless of the adapter hook so a
|
|
51890
|
+
* non-spec provider still produces the instance-side snapshot (the FSM
|
|
51891
|
+
* injection is simply skipped there). Fail-open end to end — an
|
|
51892
|
+
* unresolved transcript or any throw degrades to "no observation", never
|
|
51893
|
+
* to a wedge.
|
|
51894
|
+
*/
|
|
51895
|
+
publishTranscriptSignalObservation(messages, error = false) {
|
|
51896
|
+
try {
|
|
51897
|
+
if (!this.transcriptSignalSource) {
|
|
51898
|
+
this.transcriptSignalSource = new TranscriptSignalSource({
|
|
51899
|
+
label: this.type,
|
|
51900
|
+
// Choke point: class/timing come from the P0 profile
|
|
51901
|
+
// resolver, never from raw predicates or provider names.
|
|
51902
|
+
profile: resolveTranscriptAuthorityProfile(this.provider),
|
|
51903
|
+
turnStartedAt: () => {
|
|
51904
|
+
const t = this.adapter?.currentTurnStartedAt;
|
|
51905
|
+
if (typeof t === "number" && Number.isFinite(t)) return t;
|
|
51906
|
+
return this.meshTaskInjectedAt > 0 ? this.meshTaskInjectedAt : void 0;
|
|
51907
|
+
},
|
|
51908
|
+
// Reuse the exact completion machinery (I1) for the
|
|
51909
|
+
// final_assistant_present signal rather than duplicating
|
|
51910
|
+
// the message scan.
|
|
51911
|
+
finalAssistantPresent: (msgs, ts2) => this.completionHasFinalAssistantMessage(msgs, ts2),
|
|
51912
|
+
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS
|
|
51913
|
+
});
|
|
51914
|
+
}
|
|
51915
|
+
const snapshot = this.transcriptSignalSource.update(
|
|
51916
|
+
{ messages, probe: this.lastExternalCompletionProbe, error }
|
|
51917
|
+
);
|
|
51918
|
+
this.lastTranscriptSignalSnapshot = snapshot;
|
|
51919
|
+
const adapter = this.adapter;
|
|
51920
|
+
if (typeof adapter?.setSignalObservation === "function") {
|
|
51921
|
+
adapter.setSignalObservation(snapshot);
|
|
51922
|
+
}
|
|
51923
|
+
} catch {
|
|
51924
|
+
}
|
|
51925
|
+
}
|
|
51562
51926
|
/**
|
|
51563
51927
|
* The content of the LAST visible assistant bubble in a message list, or ''
|
|
51564
51928
|
* when the tail is not an assistant reply. Skips trailing system/tool/activity
|
|
@@ -52048,17 +52412,17 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52048
52412
|
const threshold = turnActive ? _CliProviderInstance.MESH_WORKER_STALL_TURN_THRESHOLD_MS : _CliProviderInstance.MESH_WORKER_STALL_IDLE_THRESHOLD_MS;
|
|
52049
52413
|
const stalledMs = now - this.meshStallAnchorAt;
|
|
52050
52414
|
if (stalledMs < threshold) return;
|
|
52051
|
-
const
|
|
52052
|
-
if (
|
|
52053
|
-
const
|
|
52054
|
-
|
|
52055
|
-
|
|
52056
|
-
if (
|
|
52415
|
+
const transcriptSignals = this.probeNativeTranscriptSignals();
|
|
52416
|
+
if (transcriptSignals?.snapshot?.available === true) {
|
|
52417
|
+
const firstSampleThisEpisode = !this.meshStallTranscriptSignalSampled;
|
|
52418
|
+
this.meshStallTranscriptSignalSampled = true;
|
|
52419
|
+
const signalDetail = transcriptSignals.snapshot.detail;
|
|
52420
|
+
if (firstSampleThisEpisode || transcriptSignals.snapshot.signals.in_turn_progress === true) {
|
|
52057
52421
|
if (this.isMeshWorkerSession()) {
|
|
52058
52422
|
traceMeshEventDrop(
|
|
52059
52423
|
"mesh_worker_stall_transcript_advancing",
|
|
52060
52424
|
this.meshTraceCtx("monitor:no_progress"),
|
|
52061
|
-
`msgCount=${
|
|
52425
|
+
`msgCount=${signalDetail.msgCount} sourceMtime=${signalDetail.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1e3)}s but transcript advancing)`
|
|
52062
52426
|
);
|
|
52063
52427
|
}
|
|
52064
52428
|
this.meshStallAnchorAt = now;
|
|
@@ -52066,7 +52430,7 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52066
52430
|
return;
|
|
52067
52431
|
}
|
|
52068
52432
|
}
|
|
52069
|
-
if (this.tryReconcileTranscriptCompletionForStall(observedStatus)) {
|
|
52433
|
+
if (this.tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals)) {
|
|
52070
52434
|
this.meshStallEmittedForAnchor = true;
|
|
52071
52435
|
return;
|
|
52072
52436
|
}
|
|
@@ -52106,38 +52470,45 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52106
52470
|
this.meshStallEmittedForAnchor = false;
|
|
52107
52471
|
this.meshStallTurnActiveLast = void 0;
|
|
52108
52472
|
this.meshStallLastFiredAt = -1;
|
|
52109
|
-
this.
|
|
52473
|
+
this.meshStallTranscriptSignalSampled = false;
|
|
52110
52474
|
}
|
|
52475
|
+
/** The result of one native-transcript signal probe: the normalized
|
|
52476
|
+
* snapshot the shared TranscriptSignalSource produced from the read, and
|
|
52477
|
+
* the very messages it was normalized from (so a judgment site can pull
|
|
52478
|
+
* a payload — e.g. the final summary — from the SAME read with zero
|
|
52479
|
+
* added I/O). */
|
|
52111
52480
|
/**
|
|
52112
|
-
*
|
|
52113
|
-
*
|
|
52114
|
-
*
|
|
52115
|
-
*
|
|
52116
|
-
*
|
|
52117
|
-
*
|
|
52118
|
-
*
|
|
52119
|
-
*
|
|
52120
|
-
*
|
|
52481
|
+
* TX-FSM Stage 1 — the single native-transcript signal probe (replaces
|
|
52482
|
+
* the Stage-0 sampleNativeTranscriptProgress fingerprint sampler). For a
|
|
52483
|
+
* native-source provider (its authoritative history is an on-disk
|
|
52484
|
+
* transcript file, e.g. kimi's wire.jsonl), perform the read this
|
|
52485
|
+
* judgment point already owns — SAME cadence as before, one
|
|
52486
|
+
* readExternalCompletionMessages() per call, never more — and return the
|
|
52487
|
+
* NORMALIZED SignalSnapshot the shared TranscriptSignalSource produced
|
|
52488
|
+
* from it (publishTranscriptSignalObservation runs inside the read), plus
|
|
52489
|
+
* the messages that read returned. Judgment sites (the stall watchdog's
|
|
52490
|
+
* transcript-advancing axis, the completion growth-hold, the stall-path
|
|
52491
|
+
* completion rescue) consume the snapshot's SIGNALS instead of running
|
|
52492
|
+
* their own fingerprint/freshness/final-assistant scans — one source of
|
|
52493
|
+
* truth for "what does the transcript say right now".
|
|
52121
52494
|
*
|
|
52122
|
-
*
|
|
52123
|
-
*
|
|
52124
|
-
*
|
|
52125
|
-
* (
|
|
52495
|
+
* Class gating goes through resolveTranscriptAuthorityProfile ONLY.
|
|
52496
|
+
* Returns null for a non-native-source class (nothing to signal from) and
|
|
52497
|
+
* a null snapshot when the read threw — callers keep their fail-open
|
|
52498
|
+
* fallbacks ("couldn't tell" never blocks an idle verdict and never
|
|
52499
|
+
* fabricates a completion). Cheap enough for the stall path: it runs
|
|
52500
|
+
* only at the stall threshold (≥180s of PTY stasis) or during an armed
|
|
52501
|
+
* completion-debounce retry, never on the routine 5s tick.
|
|
52126
52502
|
*/
|
|
52127
|
-
|
|
52128
|
-
if (
|
|
52503
|
+
probeNativeTranscriptSignals() {
|
|
52504
|
+
if (resolveTranscriptAuthorityProfile(this.provider).class !== "native-source") return null;
|
|
52129
52505
|
let messages = null;
|
|
52130
52506
|
try {
|
|
52131
52507
|
messages = this.readExternalCompletionMessages();
|
|
52132
52508
|
} catch {
|
|
52133
|
-
return null;
|
|
52509
|
+
return { snapshot: null, messages: null };
|
|
52134
52510
|
}
|
|
52135
|
-
|
|
52136
|
-
if (!probe) return null;
|
|
52137
|
-
return {
|
|
52138
|
-
msgCount: typeof probe.msgCount === "number" ? probe.msgCount : Array.isArray(messages) ? messages.length : 0,
|
|
52139
|
-
sourceMtimeMs: typeof probe.sourceMtimeMs === "number" ? probe.sourceMtimeMs : 0
|
|
52140
|
-
};
|
|
52511
|
+
return { snapshot: this.lastTranscriptSignalSnapshot, messages };
|
|
52141
52512
|
}
|
|
52142
52513
|
/**
|
|
52143
52514
|
* KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
|
|
@@ -52226,8 +52597,22 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52226
52597
|
* false for a daemon-owned provider and for a genuinely mid-turn / wedged
|
|
52227
52598
|
* worker with no in-turn final assistant, so a real stall still fires
|
|
52228
52599
|
* unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
|
|
52600
|
+
*
|
|
52601
|
+
* TX-FSM Stage 1 (FIX 3 probe delegation): for a native-source class the
|
|
52602
|
+
* completion VERDICT is the shared TranscriptSignalSource's
|
|
52603
|
+
* final_assistant_present signal, normalized from the ONE transcript read
|
|
52604
|
+
* the stall watchdog already performed this tick (passed in as
|
|
52605
|
+
* `transcriptSignals`) — not a second private read + scan. The emit
|
|
52606
|
+
* payload (finalSummary) is extracted from the SAME messages the signal
|
|
52607
|
+
* was normalized from, with the SAME turn boundary the legacy path used,
|
|
52608
|
+
* so the extraction re-proves the turn scope the signal checked. Fail-open
|
|
52609
|
+
* both ways: no usable snapshot (read failed / unresolved / a pure-PTY
|
|
52610
|
+
* class, which has no native signal by construction) → the legacy
|
|
52611
|
+
* class-appropriate evidence path (completionFinalSummary), unchanged; and
|
|
52612
|
+
* a present-but-unextractable signal yields false rather than a
|
|
52613
|
+
* payload-less emit.
|
|
52229
52614
|
*/
|
|
52230
|
-
tryReconcileTranscriptCompletionForStall(observedStatus) {
|
|
52615
|
+
tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals) {
|
|
52231
52616
|
const profile = resolveTranscriptAuthorityProfile(this.provider);
|
|
52232
52617
|
if (profile.class === "daemon-owned") return false;
|
|
52233
52618
|
if (observedStatus !== "idle") return false;
|
|
@@ -52245,10 +52630,19 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52245
52630
|
parsedMessages = void 0;
|
|
52246
52631
|
}
|
|
52247
52632
|
let finalSummary;
|
|
52248
|
-
|
|
52249
|
-
|
|
52250
|
-
|
|
52251
|
-
finalSummary =
|
|
52633
|
+
const signalSnapshot = transcriptSignals?.snapshot;
|
|
52634
|
+
if (profile.class === "native-source" && signalSnapshot?.available === true) {
|
|
52635
|
+
if (signalSnapshot.signals.final_assistant_present !== true) return false;
|
|
52636
|
+
finalSummary = extractFinalSummaryFromMessagesAfter(
|
|
52637
|
+
Array.isArray(transcriptSignals?.messages) ? transcriptSignals.messages : [],
|
|
52638
|
+
turnStartedAt
|
|
52639
|
+
) || void 0;
|
|
52640
|
+
} else {
|
|
52641
|
+
try {
|
|
52642
|
+
finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
|
|
52643
|
+
} catch {
|
|
52644
|
+
finalSummary = void 0;
|
|
52645
|
+
}
|
|
52252
52646
|
}
|
|
52253
52647
|
if (!finalSummary) return false;
|
|
52254
52648
|
const diagnosticSource = profile.class === "pure-pty" ? "stall_pure_pty_transcript_completion" : "stall_native_source_transcript_completion";
|
|
@@ -52515,24 +52909,25 @@ var CliProviderInstance = class _CliProviderInstance {
|
|
|
52515
52909
|
const isTranscriptEvidenceGate = block2.allowTimeout === true;
|
|
52516
52910
|
LOG.debug("CLI", `[${this.type}] finalization block: reason=${blockReason} terminal=${block2.terminal} allowTimeout=${isTranscriptEvidenceGate} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
52517
52911
|
if (blockReason === "missing_final_assistant" && block2.noExternalTranscriptSource === true) {
|
|
52518
|
-
let
|
|
52912
|
+
let growthSnapshot = null;
|
|
52519
52913
|
try {
|
|
52520
|
-
|
|
52914
|
+
growthSnapshot = this.probeNativeTranscriptSignals()?.snapshot ?? null;
|
|
52521
52915
|
} catch {
|
|
52522
|
-
|
|
52916
|
+
growthSnapshot = null;
|
|
52523
52917
|
}
|
|
52524
|
-
|
|
52525
|
-
|
|
52918
|
+
if (growthSnapshot?.available === true && growthSnapshot.signals.transcript_growing === true) {
|
|
52919
|
+
const growthDetail = growthSnapshot.detail;
|
|
52920
|
+
const mtimeAgeMs = growthDetail.ageMs ?? 0;
|
|
52526
52921
|
if (pending.loggedBlockReason !== "native_transcript_advancing") {
|
|
52527
|
-
LOG.info("CLI", `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${
|
|
52922
|
+
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`);
|
|
52528
52923
|
if (this.isMeshWorkerSession()) {
|
|
52529
|
-
traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${
|
|
52924
|
+
traceMeshEventDrop("completion_gate_hold", this.meshTraceCtx(), `native_transcript_advancing msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms`);
|
|
52530
52925
|
}
|
|
52531
52926
|
if (this.completionTraceOn()) this.recordCompletionGateTrace("hold", {
|
|
52532
52927
|
blockReason: "native_transcript_advancing",
|
|
52533
52928
|
latestVisibleStatus,
|
|
52534
|
-
msgCount:
|
|
52535
|
-
sourceMtimeAgeMs:
|
|
52929
|
+
msgCount: growthDetail.msgCount,
|
|
52930
|
+
sourceMtimeAgeMs: mtimeAgeMs,
|
|
52536
52931
|
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
|
|
52537
52932
|
waitedMs
|
|
52538
52933
|
});
|
|
@@ -63028,17 +63423,134 @@ var fastForwardHandlers = {
|
|
|
63028
63423
|
|
|
63029
63424
|
// src/commands/med-family/mesh-restart.ts
|
|
63030
63425
|
init_dist();
|
|
63426
|
+
init_logger();
|
|
63031
63427
|
var RESTART_BLOCKING_STATES = /* @__PURE__ */ new Set(["generating", "waiting_approval", "starting"]);
|
|
63032
|
-
|
|
63428
|
+
var DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
63429
|
+
var DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
63430
|
+
var DEFERRED_RESTART_POLL_MS = 1e4;
|
|
63431
|
+
function collectBlockingSessions(ctx, meshId) {
|
|
63432
|
+
const blocking = [];
|
|
63033
63433
|
const states = ctx.deps.instanceManager.collectAllStates();
|
|
63434
|
+
const consider = (state) => {
|
|
63435
|
+
const status = String(state?.status || "");
|
|
63436
|
+
if (!RESTART_BLOCKING_STATES.has(status)) return;
|
|
63437
|
+
blocking.push({
|
|
63438
|
+
instanceId: typeof state?.instanceId === "string" ? state.instanceId : null,
|
|
63439
|
+
status,
|
|
63440
|
+
// The coordinator marker is stamped by mesh_coordinator_launch and
|
|
63441
|
+
// re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
|
|
63442
|
+
// coordinator sessions have no marker and are NOT waived by selfOnly.
|
|
63443
|
+
selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId
|
|
63444
|
+
});
|
|
63445
|
+
};
|
|
63034
63446
|
for (const state of states) {
|
|
63035
|
-
|
|
63447
|
+
consider(state);
|
|
63036
63448
|
const childStates = "extensions" in state && Array.isArray(state.extensions) ? state.extensions : [];
|
|
63037
|
-
for (const child of childStates)
|
|
63038
|
-
|
|
63039
|
-
|
|
63449
|
+
for (const child of childStates) consider(child);
|
|
63450
|
+
}
|
|
63451
|
+
return blocking;
|
|
63452
|
+
}
|
|
63453
|
+
var pendingDeferredRestart = null;
|
|
63454
|
+
function deferredRestartInfo() {
|
|
63455
|
+
if (!pendingDeferredRestart) return null;
|
|
63456
|
+
return {
|
|
63457
|
+
meshId: pendingDeferredRestart.meshId,
|
|
63458
|
+
nodeId: pendingDeferredRestart.nodeId,
|
|
63459
|
+
mode: pendingDeferredRestart.mode,
|
|
63460
|
+
killSessionHost: pendingDeferredRestart.killSessionHost,
|
|
63461
|
+
scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
|
|
63462
|
+
expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
|
|
63463
|
+
runCondition: "executes automatically once no session is generating / waiting_approval / starting"
|
|
63464
|
+
};
|
|
63465
|
+
}
|
|
63466
|
+
function clearPendingDeferredRestart() {
|
|
63467
|
+
if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
|
|
63468
|
+
pendingDeferredRestart = null;
|
|
63469
|
+
}
|
|
63470
|
+
function normalizeRestartMode(value) {
|
|
63471
|
+
return value === "restart" ? "restart" : "upgrade";
|
|
63472
|
+
}
|
|
63473
|
+
function restartWarnings(args) {
|
|
63474
|
+
const warnings = [];
|
|
63475
|
+
if (args.forced) {
|
|
63476
|
+
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).");
|
|
63477
|
+
}
|
|
63478
|
+
if (args.killSessionHost) {
|
|
63479
|
+
warnings.push("killSessionHost: the session-host process was stopped \u2014 ALL hosted CLI sessions on this machine are destroyed (hard refresh).");
|
|
63480
|
+
}
|
|
63481
|
+
if (process.platform === "win32") {
|
|
63482
|
+
warnings.push("Windows: the daemon restart/upgrade path stops the session-host regardless of options \u2014 all hosted sessions terminate.");
|
|
63483
|
+
} else {
|
|
63484
|
+
warnings.push("POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).");
|
|
63485
|
+
}
|
|
63486
|
+
return warnings;
|
|
63487
|
+
}
|
|
63488
|
+
async function executeRestart(ctx, args, opts) {
|
|
63489
|
+
const mode = normalizeRestartMode(args?.mode);
|
|
63490
|
+
const killSessionHost = args?.killSessionHost === true;
|
|
63491
|
+
const result = mode === "restart" ? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost }) : await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
|
|
63492
|
+
const restarting = result?.restarting === true;
|
|
63493
|
+
return {
|
|
63494
|
+
...result,
|
|
63495
|
+
mode,
|
|
63496
|
+
restarted: restarting,
|
|
63497
|
+
...restarting ? {
|
|
63498
|
+
warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
|
|
63499
|
+
// MCP IPC has no automatic reconnect/retry — tell the caller when to
|
|
63500
|
+
// talk to this daemon again.
|
|
63501
|
+
clientHint: "daemon restarting \u2014 retry your next call in ~10s"
|
|
63502
|
+
} : {}
|
|
63503
|
+
};
|
|
63504
|
+
}
|
|
63505
|
+
function scheduleDeferredRestart(ctx, args, meshId, nodeId) {
|
|
63506
|
+
clearPendingDeferredRestart();
|
|
63507
|
+
const requestedTimeout = typeof args?.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? args.timeoutMs : DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
|
|
63508
|
+
const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
|
|
63509
|
+
const record = {
|
|
63510
|
+
meshId,
|
|
63511
|
+
nodeId,
|
|
63512
|
+
mode: normalizeRestartMode(args?.mode),
|
|
63513
|
+
killSessionHost: args?.killSessionHost === true,
|
|
63514
|
+
scheduledAt: Date.now(),
|
|
63515
|
+
expiresAt: Date.now() + timeoutMs,
|
|
63516
|
+
timer: setInterval(() => {
|
|
63517
|
+
void deferredRestartTick(ctx);
|
|
63518
|
+
}, DEFERRED_RESTART_POLL_MS)
|
|
63519
|
+
};
|
|
63520
|
+
record.timer.unref?.();
|
|
63521
|
+
pendingDeferredRestart = record;
|
|
63522
|
+
LOG.info("MeshRestart", `Deferred restart scheduled for node ${nodeId} (mode=${record.mode}, expires in ${Math.round(timeoutMs / 6e4)}min)`);
|
|
63523
|
+
return {
|
|
63524
|
+
success: true,
|
|
63525
|
+
restarted: false,
|
|
63526
|
+
scheduled: true,
|
|
63527
|
+
code: "restart_scheduled_when_idle",
|
|
63528
|
+
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.",
|
|
63529
|
+
deferredRestart: deferredRestartInfo()
|
|
63530
|
+
};
|
|
63531
|
+
}
|
|
63532
|
+
async function deferredRestartTick(ctx) {
|
|
63533
|
+
const record = pendingDeferredRestart;
|
|
63534
|
+
if (!record) return;
|
|
63535
|
+
if (Date.now() >= record.expiresAt) {
|
|
63536
|
+
LOG.warn("MeshRestart", `Deferred restart for node ${record.nodeId} expired without reaching an idle point; dropping the schedule`);
|
|
63537
|
+
clearPendingDeferredRestart();
|
|
63538
|
+
return;
|
|
63539
|
+
}
|
|
63540
|
+
const blocking = collectBlockingSessions(ctx, record.meshId);
|
|
63541
|
+
if (blocking.length > 0) return;
|
|
63542
|
+
LOG.info("MeshRestart", `Deferred restart for node ${record.nodeId} executing \u2014 daemon is idle`);
|
|
63543
|
+
clearPendingDeferredRestart();
|
|
63544
|
+
try {
|
|
63545
|
+
await executeRestart(ctx, {
|
|
63546
|
+
meshId: record.meshId,
|
|
63547
|
+
nodeId: record.nodeId,
|
|
63548
|
+
mode: record.mode,
|
|
63549
|
+
killSessionHost: record.killSessionHost
|
|
63550
|
+
}, { forced: false });
|
|
63551
|
+
} catch (e) {
|
|
63552
|
+
LOG.error("MeshRestart", `Deferred restart execution failed: ${e?.message || String(e)}`);
|
|
63040
63553
|
}
|
|
63041
|
-
return false;
|
|
63042
63554
|
}
|
|
63043
63555
|
var meshRestartHandlers = {
|
|
63044
63556
|
restart_daemon_node: async (ctx, args) => {
|
|
@@ -63059,16 +63571,43 @@ var meshRestartHandlers = {
|
|
|
63059
63571
|
});
|
|
63060
63572
|
return forwarded ?? { success: false, error: "no response from remote node" };
|
|
63061
63573
|
}
|
|
63062
|
-
if (
|
|
63574
|
+
if (args?.cancelWhenIdle === true) {
|
|
63575
|
+
const had = pendingDeferredRestart !== null;
|
|
63576
|
+
clearPendingDeferredRestart();
|
|
63577
|
+
return { success: true, restarted: false, cancelled: had, deferredRestart: null };
|
|
63578
|
+
}
|
|
63579
|
+
if (args?.whenIdleStatus === true) {
|
|
63580
|
+
return { success: true, restarted: false, deferredRestart: deferredRestartInfo() };
|
|
63581
|
+
}
|
|
63582
|
+
const blocking = collectBlockingSessions(ctx, meshId);
|
|
63583
|
+
if (blocking.length > 0) {
|
|
63584
|
+
if (args?.force === true) {
|
|
63585
|
+
LOG.warn("MeshRestart", `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || "?"}(${b.status})`).join(", ")} \u2014 pendingOutboundQueue will be lost`);
|
|
63586
|
+
return executeRestart(ctx, args, { forced: true });
|
|
63587
|
+
}
|
|
63588
|
+
const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
|
|
63589
|
+
if (args?.selfOnly === true && foreignBlocking.length === 0) {
|
|
63590
|
+
LOG.info("MeshRestart", `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
|
|
63591
|
+
return executeRestart(ctx, args, { forced: false });
|
|
63592
|
+
}
|
|
63593
|
+
if (args?.whenIdle === true) {
|
|
63594
|
+
return scheduleDeferredRestart(ctx, args, meshId, nodeId);
|
|
63595
|
+
}
|
|
63063
63596
|
return {
|
|
63064
63597
|
success: false,
|
|
63065
63598
|
restarted: false,
|
|
63066
63599
|
code: "blocking_sessions",
|
|
63067
|
-
reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle."
|
|
63600
|
+
reason: "Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.",
|
|
63601
|
+
blockingSessions: blocking,
|
|
63602
|
+
options: {
|
|
63603
|
+
selfOnly: "waive only this mesh's own coordinator session (settings.meshCoordinatorFor === meshId)",
|
|
63604
|
+
force: "bypass the idle-gate entirely \u2014 kills in-flight turns and loses the unpersisted pendingOutboundQueue",
|
|
63605
|
+
whenIdle: "schedule the restart to run automatically once the daemon goes idle (safest)"
|
|
63606
|
+
},
|
|
63607
|
+
deferredRestart: deferredRestartInfo()
|
|
63068
63608
|
};
|
|
63069
63609
|
}
|
|
63070
|
-
|
|
63071
|
-
return { ...result, restarted: result?.restarting === true };
|
|
63610
|
+
return executeRestart(ctx, args, { forced: false });
|
|
63072
63611
|
}
|
|
63073
63612
|
};
|
|
63074
63613
|
|