@indigoai-us/hq-cli 5.345.127 → 5.345.129
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/CHANGELOG.md +47 -0
- package/assets/scaffold/core/scripts/handoff-knowledge-commit.sh +123 -0
- package/assets/scaffold/core/scripts/hq-detach.sh +169 -0
- package/dist/command-catalog.generated.d.ts +40 -2
- package/dist/command-catalog.generated.js +48 -2
- package/dist/commands/auth.js +19 -2
- package/dist/commands/core-bucket-c-handoff-knowledge-fast.d.ts +2 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-fast.js +10 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +46 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-native.js +284 -0
- package/dist/commands/core-bucket-c-hq-detach-fast.d.ts +2 -0
- package/dist/commands/core-bucket-c-hq-detach-fast.js +10 -0
- package/dist/commands/core-bucket-c-hq-detach-native.d.ts +43 -0
- package/dist/commands/core-bucket-c-hq-detach-native.js +241 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
- package/dist/commands/core.js +12 -0
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/lanes-accounts.js +3 -1
- package/dist/commands/lanes-shared.d.ts +7 -0
- package/dist/commands/lanes.js +139 -17
- package/dist/commands/mesh.js +32 -19
- package/dist/commands/skill.js +9 -4
- package/dist/fast-scaffold.js +28 -0
- package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +3 -5
- package/dist/lib/bot/inbox-poll-backoff-flag.js +20 -12
- package/dist/lib/bot/inbox-wake.d.ts +18 -0
- package/dist/lib/bot/inbox-wake.js +125 -0
- package/dist/lib/bot/run.d.ts +8 -0
- package/dist/lib/bot/run.js +51 -7
- package/dist/lib/bot/runtime/messages-stream.js +31 -3
- package/dist/lib/capacity/lane-login.js +1 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +6 -3
- package/dist/lib/daemon/flag-monitor.d.ts +4 -1
- package/dist/lib/daemon/flag-monitor.js +12 -6
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +11 -5
- package/dist/lib/daemon/hq-child-bootstrap.mjs +47 -7
- package/dist/lib/daemon/kill-switch-flags.d.ts +5 -0
- package/dist/lib/daemon/kill-switch-flags.js +5 -0
- package/dist/lib/daemon/lanes-reconcile-loader.d.ts +7 -0
- package/dist/lib/daemon/lanes-reconcile-loader.js +24 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.d.ts +17 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.js +80 -0
- package/dist/lib/daemon/remote-control.d.ts +8 -5
- package/dist/lib/daemon/remote-control.js +8 -7
- package/dist/lib/daemon/run.d.ts +4 -1
- package/dist/lib/daemon/run.js +133 -20
- package/dist/lib/doctor/json-output.d.ts +15 -3
- package/dist/lib/doctor/json-output.js +13 -1
- package/dist/lib/lanes/accounts.js +2 -2
- package/dist/lib/lanes/daemon-reconcile.d.ts +3 -0
- package/dist/lib/lanes/daemon-reconcile.js +8 -0
- package/dist/lib/lanes/host-capacity.d.ts +4 -0
- package/dist/lib/lanes/host-capacity.js +10 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/loop.d.ts +8 -0
- package/dist/lib/lanes/loop.js +35 -0
- package/dist/lib/lanes/quota-notice.d.ts +11 -11
- package/dist/lib/lanes/quota-notice.js +20 -29
- package/dist/lib/lanes/quota.d.ts +69 -50
- package/dist/lib/lanes/quota.js +146 -127
- package/dist/lib/lanes/reconcile-result.d.ts +10 -0
- package/dist/lib/lanes/reconcile-result.js +11 -0
- package/dist/lib/lanes/reconcile.d.ts +14 -0
- package/dist/lib/lanes/reconcile.js +77 -23
- package/dist/lib/lanes/watcher.d.ts +8 -0
- package/dist/lib/lanes/watcher.js +62 -20
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.js +8 -1
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/mesh/story-status.d.ts +2 -2
- package/dist/lib/mesh/story-status.js +2 -1
- package/dist/utils/resident-package-preload.js +18 -4
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
/** Reconcile at startup, on lane-store changes, and on a bounded in-process timer. */
|
|
3
|
+
export function createLanesReconcileWatcher(options) {
|
|
4
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
5
|
+
const intervalMs = options.intervalMs ?? 60_000;
|
|
6
|
+
const watch = options.watch ?? ((directory, listener) => fs.watch(directory, listener));
|
|
7
|
+
const watchers = [];
|
|
8
|
+
let timer;
|
|
9
|
+
let running = false;
|
|
10
|
+
let rerun = false;
|
|
11
|
+
let closed = false;
|
|
12
|
+
const run = async () => {
|
|
13
|
+
if (closed)
|
|
14
|
+
return;
|
|
15
|
+
if (running) {
|
|
16
|
+
rerun = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
running = true;
|
|
20
|
+
try {
|
|
21
|
+
await options.reconcile();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
options.onError?.(error);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
running = false;
|
|
28
|
+
if (rerun && !closed) {
|
|
29
|
+
rerun = false;
|
|
30
|
+
schedule();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
schedule(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const schedule = (delayMs = debounceMs) => {
|
|
38
|
+
if (closed)
|
|
39
|
+
return;
|
|
40
|
+
if (timer)
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
timer = undefined;
|
|
44
|
+
void run();
|
|
45
|
+
}, delayMs);
|
|
46
|
+
};
|
|
47
|
+
const watchFailure = (error) => {
|
|
48
|
+
options.onWatchError?.(error);
|
|
49
|
+
close();
|
|
50
|
+
};
|
|
51
|
+
const close = () => {
|
|
52
|
+
if (closed)
|
|
53
|
+
return;
|
|
54
|
+
closed = true;
|
|
55
|
+
if (timer)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = undefined;
|
|
58
|
+
for (const watcher of watchers.splice(0))
|
|
59
|
+
watcher.close();
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
for (const directory of options.directories) {
|
|
63
|
+
const watcher = watch(directory, (_event, filename) => {
|
|
64
|
+
if (filename !== null && !filename.toString().endsWith(".json"))
|
|
65
|
+
return;
|
|
66
|
+
schedule();
|
|
67
|
+
});
|
|
68
|
+
const eventSource = watcher;
|
|
69
|
+
eventSource.on?.("error", watchFailure);
|
|
70
|
+
watchers.push(watcher);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
close();
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
schedule(0);
|
|
78
|
+
return { close };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=lanes-reconcile-watcher.js.map
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* backoff (replacing the runner's `while true` loop and its watchdogs);
|
|
12
12
|
* - the daemon stops the host -> the host kills the session and exits 0;
|
|
13
13
|
* - a new version of the tool is installed (the updater, or the tool's own
|
|
14
|
-
* auto-updater) ->
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* auto-updater) -> standalone hosts restart after 10 quiet minutes or 2
|
|
15
|
+
* hours at the latest. Shared Remote Control pools never restart solely for
|
|
16
|
+
* an update, because their terminal activity cannot establish that all
|
|
17
|
+
* connected client sessions are idle. A failed pool host is still restarted
|
|
18
|
+
* by the daemon. Reinstalling the same version and hq-cli releases leave
|
|
19
|
+
* running sessions alone.
|
|
19
20
|
*
|
|
20
21
|
* The tmux server is a private socket (`-L hq-daemon`), so it never touches a
|
|
21
22
|
* user's own tmux sessions.
|
|
@@ -67,6 +68,8 @@ export interface RemoteControlHostDeps {
|
|
|
67
68
|
label: string;
|
|
68
69
|
/** Identity of the tool's binary on disk; a change means it was updated. Null when it cannot be read. */
|
|
69
70
|
binaryFingerprint: () => string | null;
|
|
71
|
+
/** Shared pools must keep their clients connected across tool updates. */
|
|
72
|
+
restartOnToolUpdate?: boolean;
|
|
70
73
|
now: () => number;
|
|
71
74
|
run: RunCommand;
|
|
72
75
|
sleep: (ms: number) => Promise<void>;
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* backoff (replacing the runner's `while true` loop and its watchdogs);
|
|
12
12
|
* - the daemon stops the host -> the host kills the session and exits 0;
|
|
13
13
|
* - a new version of the tool is installed (the updater, or the tool's own
|
|
14
|
-
* auto-updater) ->
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* auto-updater) -> standalone hosts restart after 10 quiet minutes or 2
|
|
15
|
+
* hours at the latest. Shared Remote Control pools never restart solely for
|
|
16
|
+
* an update, because their terminal activity cannot establish that all
|
|
17
|
+
* connected client sessions are idle. A failed pool host is still restarted
|
|
18
|
+
* by the daemon. Reinstalling the same version and hq-cli releases leave
|
|
19
|
+
* running sessions alone.
|
|
19
20
|
*
|
|
20
21
|
* The tmux server is a private socket (`-L hq-daemon`), so it never touches a
|
|
21
22
|
* user's own tmux sessions.
|
|
@@ -317,7 +318,7 @@ export async function runRemoteControlHost(deps) {
|
|
|
317
318
|
return 1;
|
|
318
319
|
}
|
|
319
320
|
deps.log(`${deps.label} started in ${JSON.stringify(deps.cwd)} (creator pid ${invokerPid})`);
|
|
320
|
-
const startedWith = deps.binaryFingerprint();
|
|
321
|
+
const startedWith = deps.restartOnToolUpdate === false ? null : deps.binaryFingerprint();
|
|
321
322
|
let updatedAt = null;
|
|
322
323
|
for (;;) {
|
|
323
324
|
if (deps.signal.aborted) {
|
|
@@ -338,7 +339,7 @@ export async function runRemoteControlHost(deps) {
|
|
|
338
339
|
return 1;
|
|
339
340
|
}
|
|
340
341
|
const now = deps.now();
|
|
341
|
-
if (updatedAt === null) {
|
|
342
|
+
if (deps.restartOnToolUpdate !== false && updatedAt === null) {
|
|
342
343
|
const current = deps.binaryFingerprint();
|
|
343
344
|
if (startedWith !== null && current !== null && current !== startedWith) {
|
|
344
345
|
updatedAt = now;
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -86,7 +86,10 @@ export declare function daemonPlanCanReleasePackageUseLease(plans: readonly Unit
|
|
|
86
86
|
* PRIO_DARWIN_BG is inherited by everything the unit spawns (qmd included).
|
|
87
87
|
*/
|
|
88
88
|
export declare function backgroundQosCommand(command: [string, string[]], backgroundQos: boolean, platform?: NodeJS.Platform | string, exists?: (file: string) => boolean): [string, string[]];
|
|
89
|
-
export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
|
|
89
|
+
export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null, handoffWatchEnabled?: boolean): [string, string[]];
|
|
90
|
+
export declare function daemonSyncUnitIsPresent(units: readonly {
|
|
91
|
+
id: string;
|
|
92
|
+
}[]): boolean;
|
|
90
93
|
export declare function resolveSyncRunnerPath(): string | null;
|
|
91
94
|
/** The hosted desktop app uses this env value when planning daemon services. */
|
|
92
95
|
export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -24,7 +24,12 @@ import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits }
|
|
|
24
24
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
25
25
|
import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
|
|
26
26
|
import { loadLanesConfig } from "../lanes/config.js";
|
|
27
|
+
import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
|
|
28
|
+
import { ensureDaemonLanesReconcileHandler } from "./lanes-reconcile-loader.js";
|
|
27
29
|
import { SUPERVISOR_HANDOFF_FLAG, readDaemonFlag } from "./flag.js";
|
|
30
|
+
import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
|
|
31
|
+
import { DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG, DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG, DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG, DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG, } from "./kill-switch-flags.js";
|
|
32
|
+
import { createLanesReconcileWatcher } from "./lanes-reconcile-watcher.js";
|
|
28
33
|
import { findLegacyUnits, legacyMeshUnitsToRetire, legacyOwnerLookup } from "./legacy.js";
|
|
29
34
|
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
30
35
|
import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLock } from "./lock.js";
|
|
@@ -216,11 +221,14 @@ export function backgroundQosCommand(command, backgroundQos, platform = process.
|
|
|
216
221
|
return command;
|
|
217
222
|
return [DARWIN_TASKPOLICY, ["-b", command[0], ...command[1]]];
|
|
218
223
|
}
|
|
219
|
-
export function daemonHqChildCommand(nodeBinary, hqBinary, args, bootstrapPath, packagePrefix) {
|
|
224
|
+
export function daemonHqChildCommand(nodeBinary, hqBinary, args, bootstrapPath, packagePrefix, handoffWatchEnabled = true) {
|
|
220
225
|
return bootstrapPath && packagePrefix
|
|
221
|
-
? [nodeBinary, [bootstrapPath, packagePrefix, CLI_VERSION, ...args]]
|
|
226
|
+
? [nodeBinary, [bootstrapPath, packagePrefix, CLI_VERSION, ...(handoffWatchEnabled ? [] : ["--handoff-watch=off"]), ...args]]
|
|
222
227
|
: [nodeBinary, [hqBinary, ...args]];
|
|
223
228
|
}
|
|
229
|
+
export function daemonSyncUnitIsPresent(units) {
|
|
230
|
+
return units.some((unit) => unit.id === "sync");
|
|
231
|
+
}
|
|
224
232
|
export function resolveSyncRunnerPath() {
|
|
225
233
|
try {
|
|
226
234
|
const require = createRequire(import.meta.url);
|
|
@@ -674,11 +682,104 @@ export async function runDaemon(opts) {
|
|
|
674
682
|
};
|
|
675
683
|
const daemonSyncRunnerPids = new Set();
|
|
676
684
|
let supervisor = null;
|
|
685
|
+
const killSwitchMonitor = (flagKey) => createBoundedDaemonFlagMonitor({
|
|
686
|
+
fallback: true,
|
|
687
|
+
read: () => readDaemonFlag(flagKey, {
|
|
688
|
+
env,
|
|
689
|
+
fallback: true,
|
|
690
|
+
localOverrideMode: "ignore",
|
|
691
|
+
throwOnError: true,
|
|
692
|
+
}),
|
|
693
|
+
onReadError: (error) => log("warn", `could not read ${flagKey}; keeping its default-on behavior (${error instanceof Error ? error.name : "UnknownError"})`),
|
|
694
|
+
});
|
|
695
|
+
const childBootstrapHandoffWatchFlag = killSwitchMonitor(DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG);
|
|
696
|
+
const lanesReconcileFileWatchFlag = killSwitchMonitor(DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG);
|
|
697
|
+
const searchIndexRunnerOwnsIndexFlag = killSwitchMonitor(DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG);
|
|
698
|
+
const syncProcessScanOnMissingFlag = killSwitchMonitor(DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG);
|
|
699
|
+
let laneReconcileWatcher = null;
|
|
700
|
+
let laneReconcileWatcherRoot = null;
|
|
701
|
+
let laneReconcileWatchFailedRoot = null;
|
|
702
|
+
let laneReconcileHandlerLoadInFlight = false;
|
|
703
|
+
let laneReconcileHandlerLoadFailed = false;
|
|
704
|
+
const loadLaneReconcileHandler = (initialRoot) => {
|
|
705
|
+
if (daemonLanesReconcileHandler() || laneReconcileHandlerLoadInFlight || laneReconcileHandlerLoadFailed)
|
|
706
|
+
return;
|
|
707
|
+
laneReconcileHandlerLoadInFlight = true;
|
|
708
|
+
const resolveRoot = () => {
|
|
709
|
+
const configured = readDaemonConfig(paths.configPath).hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
|
|
710
|
+
return configured && fs.existsSync(configured) ? configured : initialRoot;
|
|
711
|
+
};
|
|
712
|
+
void ensureDaemonLanesReconcileHandler({ hqRoot: resolveRoot, env }).then(() => {
|
|
713
|
+
supervisor?.reconcile();
|
|
714
|
+
}).catch((error) => {
|
|
715
|
+
laneReconcileHandlerLoadFailed = true;
|
|
716
|
+
log("error", `could not load the in-process lanes reconciler; retaining the scheduled unit (${error instanceof Error ? error.message : String(error)})`);
|
|
717
|
+
}).finally(() => {
|
|
718
|
+
laneReconcileHandlerLoadInFlight = false;
|
|
719
|
+
});
|
|
720
|
+
};
|
|
721
|
+
const closeRuntimeWatchers = () => {
|
|
722
|
+
laneReconcileWatcher?.close();
|
|
723
|
+
laneReconcileWatcher = null;
|
|
724
|
+
laneReconcileWatcherRoot = null;
|
|
725
|
+
childBootstrapHandoffWatchFlag.close();
|
|
726
|
+
lanesReconcileFileWatchFlag.close();
|
|
727
|
+
searchIndexRunnerOwnsIndexFlag.close();
|
|
728
|
+
syncProcessScanOnMissingFlag.close();
|
|
729
|
+
};
|
|
677
730
|
const plan = () => {
|
|
678
731
|
const config = readDaemonConfig(paths.configPath);
|
|
679
732
|
const legacy = legacyUnits();
|
|
680
733
|
const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
|
|
681
734
|
const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
|
|
735
|
+
if (laneReconcileWatcherRoot && laneReconcileWatcherRoot !== hqRoot) {
|
|
736
|
+
laneReconcileWatcher?.close();
|
|
737
|
+
laneReconcileWatcher = null;
|
|
738
|
+
laneReconcileWatcherRoot = null;
|
|
739
|
+
laneReconcileWatchFailedRoot = null;
|
|
740
|
+
}
|
|
741
|
+
const lanesReconcileEnabled = lanesReconcileFileWatchFlag.status().enabled;
|
|
742
|
+
if (!lanesReconcileEnabled) {
|
|
743
|
+
laneReconcileWatcher?.close();
|
|
744
|
+
laneReconcileWatcher = null;
|
|
745
|
+
laneReconcileWatcherRoot = null;
|
|
746
|
+
laneReconcileWatchFailedRoot = null;
|
|
747
|
+
}
|
|
748
|
+
else if (hqRoot) {
|
|
749
|
+
if (!daemonLanesReconcileHandler())
|
|
750
|
+
loadLaneReconcileHandler(hqRoot);
|
|
751
|
+
const laneReconcileHandler = daemonLanesReconcileHandler();
|
|
752
|
+
if (laneReconcileHandler && laneReconcileWatcherRoot !== hqRoot && laneReconcileWatchFailedRoot !== hqRoot) {
|
|
753
|
+
laneReconcileWatcher?.close();
|
|
754
|
+
laneReconcileWatcher = null;
|
|
755
|
+
laneReconcileWatcherRoot = null;
|
|
756
|
+
const laneStore = path.join(hqRoot, "workspace", "lanes");
|
|
757
|
+
const laneDirectories = [path.join(laneStore, "lanes"), path.join(laneStore, "questions")];
|
|
758
|
+
try {
|
|
759
|
+
for (const directory of laneDirectories)
|
|
760
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
761
|
+
laneReconcileWatcher = createLanesReconcileWatcher({
|
|
762
|
+
directories: laneDirectories,
|
|
763
|
+
reconcile: laneReconcileHandler,
|
|
764
|
+
intervalMs: 15_000,
|
|
765
|
+
onError: (error) => log("error", `in-process lanes reconcile failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
766
|
+
onWatchError: (error) => {
|
|
767
|
+
laneReconcileWatcher = null;
|
|
768
|
+
laneReconcileWatcherRoot = null;
|
|
769
|
+
laneReconcileWatchFailedRoot = hqRoot;
|
|
770
|
+
log("error", `lane-store watch failed; restoring the scheduled reconcile unit (${error instanceof Error ? error.message : String(error)})`);
|
|
771
|
+
supervisor?.reconcile();
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
laneReconcileWatcherRoot = hqRoot;
|
|
775
|
+
laneReconcileWatchFailedRoot = null;
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
laneReconcileWatchFailedRoot = hqRoot;
|
|
779
|
+
log("error", `could not watch the lane store; retaining the scheduled reconcile unit (${error instanceof Error ? error.message : String(error)})`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
682
783
|
let remoteControlPools = { version: 1, pools: [] };
|
|
683
784
|
let remoteControlPoolsError;
|
|
684
785
|
try {
|
|
@@ -810,21 +911,30 @@ export async function runDaemon(opts) {
|
|
|
810
911
|
!config.syncPaused &&
|
|
811
912
|
hqRoot &&
|
|
812
913
|
isSignedIn) {
|
|
813
|
-
const
|
|
814
|
-
if (!
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
syncRunnerProcesses = processList.processes;
|
|
820
|
-
try {
|
|
821
|
-
syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
|
|
914
|
+
const syncUnitPresent = daemonSyncUnitIsPresent(supervisor?.state().units ?? []);
|
|
915
|
+
if (!syncProcessScanOnMissingFlag.status().enabled || !syncUnitPresent) {
|
|
916
|
+
const processList = listSyncRunnerProcesses(platform);
|
|
917
|
+
if (!processList.ok) {
|
|
918
|
+
syncRunnerProcesses = null;
|
|
919
|
+
log("warn", `${processList.error}; sync will wait before taking ownership`);
|
|
822
920
|
}
|
|
823
|
-
|
|
824
|
-
|
|
921
|
+
else {
|
|
922
|
+
syncRunnerProcesses = processList.processes;
|
|
923
|
+
try {
|
|
924
|
+
syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
|
|
928
|
+
}
|
|
825
929
|
}
|
|
826
930
|
}
|
|
827
931
|
}
|
|
932
|
+
const appManagesSync = desktopAppManagesSync({
|
|
933
|
+
machineType,
|
|
934
|
+
host: opts.host,
|
|
935
|
+
legacy,
|
|
936
|
+
appRunning: () => desktopAppRunning(platform),
|
|
937
|
+
});
|
|
828
938
|
const plans = planUnits(services, {
|
|
829
939
|
machineType,
|
|
830
940
|
platform,
|
|
@@ -840,15 +950,15 @@ export async function runDaemon(opts) {
|
|
|
840
950
|
kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
|
|
841
951
|
syncPaused: config.syncPaused === true,
|
|
842
952
|
syncOwner,
|
|
843
|
-
desktopAppManagesSync:
|
|
844
|
-
machineType,
|
|
845
|
-
host: opts.host,
|
|
846
|
-
legacy,
|
|
847
|
-
appRunning: () => desktopAppRunning(platform),
|
|
848
|
-
}),
|
|
953
|
+
desktopAppManagesSync: appManagesSync,
|
|
849
954
|
syncRunnerProcesses,
|
|
850
955
|
syncRunnerOwnerPid,
|
|
851
956
|
daemonSyncRunnerPids,
|
|
957
|
+
lanesReconcileInProcess: Boolean(laneReconcileWatcher && laneReconcileWatcherRoot === hqRoot),
|
|
958
|
+
searchIndexOwnedBySyncRunner: searchIndexRunnerOwnsIndexFlag.status().enabled &&
|
|
959
|
+
hqCloud !== null && !hqCloudVersionOlder(hqCloud.version, "6.18.75") &&
|
|
960
|
+
syncOwner === "daemon" && services.includes("sync") && !config.syncPaused &&
|
|
961
|
+
!appManagesSync && Boolean(hqRoot && isSignedIn && runners.syncRunner),
|
|
852
962
|
syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
|
|
853
963
|
bots: listLocalBots(),
|
|
854
964
|
legacyOwner: legacyOwnerLookup(legacy),
|
|
@@ -884,7 +994,7 @@ export async function runDaemon(opts) {
|
|
|
884
994
|
if (!command || command.type === "internal")
|
|
885
995
|
throw new Error(`${unit.id} has no process to start`);
|
|
886
996
|
let [file, args] = command.type === "hq"
|
|
887
|
-
? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix)
|
|
997
|
+
? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix, childBootstrapHandoffWatchFlag.status().enabled)
|
|
888
998
|
: command.type === "node"
|
|
889
999
|
? [opts.nodeBinary, [command.script, ...command.args]]
|
|
890
1000
|
: [command.file, command.args];
|
|
@@ -1112,6 +1222,7 @@ export async function runDaemon(opts) {
|
|
|
1112
1222
|
stopping = true;
|
|
1113
1223
|
log("info", `${why}; stopping services`);
|
|
1114
1224
|
closeHqd();
|
|
1225
|
+
closeRuntimeWatchers();
|
|
1115
1226
|
// Keep shutdown bounded, but let a stopping sync runner reach its persist deadline.
|
|
1116
1227
|
const cap = createShutdownCap();
|
|
1117
1228
|
extendSyncShutdownCap = cap.extendForSyncRunnerPersist;
|
|
@@ -1139,6 +1250,7 @@ export async function runDaemon(opts) {
|
|
|
1139
1250
|
return;
|
|
1140
1251
|
stopping = true;
|
|
1141
1252
|
closeHqd();
|
|
1253
|
+
closeRuntimeWatchers();
|
|
1142
1254
|
const released = supervisor.release();
|
|
1143
1255
|
// What these readers have not read stays in the FIFO for the next daemon.
|
|
1144
1256
|
for (const tail of protocolTails)
|
|
@@ -1207,6 +1319,7 @@ export async function runDaemon(opts) {
|
|
|
1207
1319
|
catch (err) {
|
|
1208
1320
|
log("error", `supervisor failed to start: ${err.message}`);
|
|
1209
1321
|
closeHqd();
|
|
1322
|
+
closeRuntimeWatchers();
|
|
1210
1323
|
releaseDaemonLock(paths.lockPath);
|
|
1211
1324
|
resolve(1);
|
|
1212
1325
|
}
|
|
@@ -106,9 +106,10 @@ export interface BuildDoctorJsonInput {
|
|
|
106
106
|
clientHealthCheckStatusEnabledCheckIds?: ReadonlySet<string>;
|
|
107
107
|
/**
|
|
108
108
|
* JSON consumed by the SessionStart health hook (`--client-health-report`).
|
|
109
|
-
* Ordinary hq-core upgrade WARNs
|
|
110
|
-
* are omitted from every JSON document
|
|
111
|
-
* including `summary` tallies: `--fix`
|
|
109
|
+
* Ordinary hq-core upgrade WARNs, leftover `sync.journal.personal` WARNs, and
|
|
110
|
+
* leftover `sync.manifest.personal` WARNs are omitted from every JSON document
|
|
111
|
+
* (`--json` and `--client-health-report`), including `summary` tallies: `--fix`
|
|
112
|
+
* cannot apply a release or upload personal manifests, and the leftover
|
|
112
113
|
* personal shard is not a live sync target. Company journals this `--fix`
|
|
113
114
|
* pass did not try to refresh are omitted the same way: the repair cap leaves
|
|
114
115
|
* them for a later session, and older hooks that only call `hq doctor --json`
|
|
@@ -141,6 +142,17 @@ export declare function isLeftoverPersonalJournalWarn(result: {
|
|
|
141
142
|
checkId: string;
|
|
142
143
|
status: DoctorStatus;
|
|
143
144
|
}): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* True when doctor still WARNs on a stale personal sync manifest.
|
|
147
|
+
* `--fix` does not upload manifests (network, auth, vault write), so the row
|
|
148
|
+
* stays WARN after a repair pass. JSON omits the WARN so `--json` health hooks
|
|
149
|
+
* do not file it as a failed repair. FAIL stays visible, and human
|
|
150
|
+
* `hq doctor` still prints the WARN.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isLeftoverPersonalManifestWarn(result: {
|
|
153
|
+
checkId: string;
|
|
154
|
+
status: DoctorStatus;
|
|
155
|
+
}): boolean;
|
|
144
156
|
/** Assemble the machine-readable document from a completed run. */
|
|
145
157
|
export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
|
|
146
158
|
/** Serialise the document as pretty-printed JSON with a trailing newline. */
|
|
@@ -50,8 +50,20 @@ export function isOrdinaryCoreUpdateWarn(result) {
|
|
|
50
50
|
export function isLeftoverPersonalJournalWarn(result) {
|
|
51
51
|
return result.checkId === "sync.journal.personal" && result.status === "WARN";
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* True when doctor still WARNs on a stale personal sync manifest.
|
|
55
|
+
* `--fix` does not upload manifests (network, auth, vault write), so the row
|
|
56
|
+
* stays WARN after a repair pass. JSON omits the WARN so `--json` health hooks
|
|
57
|
+
* do not file it as a failed repair. FAIL stays visible, and human
|
|
58
|
+
* `hq doctor` still prints the WARN.
|
|
59
|
+
*/
|
|
60
|
+
export function isLeftoverPersonalManifestWarn(result) {
|
|
61
|
+
return result.checkId === "sync.manifest.personal" && result.status === "WARN";
|
|
62
|
+
}
|
|
53
63
|
function omitFromDoctorJson(result) {
|
|
54
|
-
return isOrdinaryCoreUpdateWarn(result) ||
|
|
64
|
+
return (isOrdinaryCoreUpdateWarn(result) ||
|
|
65
|
+
isLeftoverPersonalJournalWarn(result) ||
|
|
66
|
+
isLeftoverPersonalManifestWarn(result));
|
|
55
67
|
}
|
|
56
68
|
/** Tally the results that actually ship in the JSON document. */
|
|
57
69
|
function summarizeJsonResults(results) {
|
|
@@ -280,7 +280,7 @@ function throwIfQuotaBlocked(block, provider, accountId, identity) {
|
|
|
280
280
|
const label = accountLabel(accountId, identity);
|
|
281
281
|
const identityFields = identity ? { identity } : {};
|
|
282
282
|
if (block.reason === "account_model_unavailable") {
|
|
283
|
-
throw new AccountResolutionError("account_model_unavailable", `${label} cannot run model ${model}: ${block.marker ?? "needs credits"}`, {
|
|
283
|
+
throw new AccountResolutionError("account_model_unavailable", `${label} cannot run model ${model}: ${block.marker ?? "needs credits"}. The account's model needs credits; once they are added, clear the marker with: ${clearQuotaCommand(provider, accountId)}`, {
|
|
284
284
|
provider,
|
|
285
285
|
account: accountId,
|
|
286
286
|
model: block.model ?? null,
|
|
@@ -289,7 +289,7 @@ function throwIfQuotaBlocked(block, provider, accountId, identity) {
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
const clear = clearQuotaCommand(provider, accountId);
|
|
292
|
-
throw new AccountResolutionError("account_quota_exhausted", `${label} is
|
|
292
|
+
throw new AccountResolutionError("account_quota_exhausted", `${label} is marked unavailable for model ${model} (the account's model needs credits). Clear the marker with: ${clear}`, {
|
|
293
293
|
provider,
|
|
294
294
|
account: accountId,
|
|
295
295
|
model: block.model ?? null,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
let daemonReconcileHandler = null;
|
|
2
|
+
export function registerDaemonLanesReconcileHandler(handler) {
|
|
3
|
+
daemonReconcileHandler = handler;
|
|
4
|
+
}
|
|
5
|
+
export function daemonLanesReconcileHandler() {
|
|
6
|
+
return daemonReconcileHandler;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=daemon-reconcile.js.map
|
|
@@ -12,6 +12,10 @@ export interface HostCapacityAccount {
|
|
|
12
12
|
/** Account-wide quota status when this snapshot is produced on its host. */
|
|
13
13
|
quota_held?: boolean;
|
|
14
14
|
quota_reset_at?: string;
|
|
15
|
+
/** Last recorded provider usage limit on this account. Informational only. */
|
|
16
|
+
last_quota_hit_at?: string;
|
|
17
|
+
/** The provider's reset time from that limit, when it named one. Informational only. */
|
|
18
|
+
provider_reset_at?: string;
|
|
15
19
|
/** Exact lane states used for Claude account balancing; absent on older Outposts. */
|
|
16
20
|
lane_counts?: {
|
|
17
21
|
running: number;
|
|
@@ -2,7 +2,7 @@ import { accountStatus } from "./accounts.js";
|
|
|
2
2
|
import { accountMaxRunning } from "./config.js";
|
|
3
3
|
import { laneOccupiesRunningSlot, laneRunsOnHost, } from "./admission.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
-
import { quotaBlockFor } from "./quota.js";
|
|
5
|
+
import { quotaBlockFor, quotaHitInfo } from "./quota.js";
|
|
6
6
|
function admissionHost(host) {
|
|
7
7
|
return host.kind === "outpost"
|
|
8
8
|
? { hostname: "", outpostId: host.id }
|
|
@@ -38,6 +38,7 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
38
38
|
? quotaBlockFor(input.quotaState, provider, id, undefined, input.now)
|
|
39
39
|
: undefined;
|
|
40
40
|
const quotaResetAt = input.quotaState?.providers[provider]?.[id]?.all_models_quota_reset_at;
|
|
41
|
+
const hit = input.quotaState ? quotaHitInfo(input.quotaState, provider, id, input.now) : undefined;
|
|
41
42
|
accounts[id] = {
|
|
42
43
|
enabled: account.enabled,
|
|
43
44
|
logged_in: status.logged_in,
|
|
@@ -51,6 +52,8 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
51
52
|
...(quotaResetAt ? { quota_reset_at: quotaResetAt } : {}),
|
|
52
53
|
}
|
|
53
54
|
: {}),
|
|
55
|
+
...(hit?.last_quota_hit_at ? { last_quota_hit_at: hit.last_quota_hit_at } : {}),
|
|
56
|
+
...(hit?.provider_reset_at ? { provider_reset_at: hit.provider_reset_at } : {}),
|
|
54
57
|
lane_counts: {
|
|
55
58
|
running: liveLanes.filter((lane) => lane.state === "running").length,
|
|
56
59
|
queued: liveLanes.filter((lane) => lane.state === "queued").length,
|
|
@@ -142,6 +145,12 @@ export function parseHostCapacitySnapshot(raw) {
|
|
|
142
145
|
...(account.quota_reset_at !== undefined
|
|
143
146
|
? { quota_reset_at: account.quota_reset_at }
|
|
144
147
|
: {}),
|
|
148
|
+
...(typeof account.last_quota_hit_at === "string" && Number.isFinite(Date.parse(account.last_quota_hit_at))
|
|
149
|
+
? { last_quota_hit_at: account.last_quota_hit_at }
|
|
150
|
+
: {}),
|
|
151
|
+
...(typeof account.provider_reset_at === "string" && Number.isFinite(Date.parse(account.provider_reset_at))
|
|
152
|
+
? { provider_reset_at: account.provider_reset_at }
|
|
153
|
+
: {}),
|
|
145
154
|
...(account.lane_counts !== undefined
|
|
146
155
|
? { lane_counts: parseLaneCounts(account.lane_counts, `${provider}.${id}.lane_counts`) }
|
|
147
156
|
: {}),
|
|
@@ -44,8 +44,8 @@ export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PRO
|
|
|
44
44
|
export type { AccountResolveContext, AccountStatus, AuthState, ClaudeIdentity, ResolvedAccount, } from "./accounts.js";
|
|
45
45
|
export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
|
|
46
46
|
export type { LivenessDeps, LivenessEventKind, LivenessIndex, SessionIndexEntry, } from "./liveness.js";
|
|
47
|
-
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, wakeAnsweredLaneNow, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
48
|
-
export type { AnsweredLaneWakeResult, ReconcileChange, ReconcileDeps, ReconcileHost, ReconcileSkip, ReconcileSummary, } from "./reconcile.js";
|
|
47
|
+
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, wakeAnsweredLaneNow, admitQueuedNow, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
48
|
+
export type { AnsweredLaneWakeResult, AdmitQueuedNowResult, ReconcileChange, ReconcileDeps, ReconcileHost, ReconcileSkip, ReconcileSummary, } from "./reconcile.js";
|
|
49
49
|
export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
|
|
50
50
|
export type { ProviderPathEnv, ReconcileInstallOpts, ReconcileInstallResult, RunCommand, RunCommandResult, } from "./reconcile-install.js";
|
|
51
51
|
export { unitPathEnv } from "./reconcile-install.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO,
|
|
|
24
24
|
export { effectiveAccountHome, } from "./account-home.js";
|
|
25
25
|
export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
|
|
26
26
|
export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
|
|
27
|
-
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, wakeAnsweredLaneNow, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
27
|
+
export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, wakeAnsweredLaneNow, admitQueuedNow, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
|
|
28
28
|
export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
|
|
29
29
|
export { unitPathEnv } from "./reconcile-install.js";
|
|
30
30
|
export { PROVIDER_REJECTED_REASON, PROVIDER_REJECTION_MESSAGE_CAP, parseClaudeResultRejection, parseCodexTurnFailed, parseGrokErrorOutput, parseProviderRejection, readProviderRejection, retryableFromStatus, } from "./provider-rejection.js";
|
package/dist/lib/lanes/loop.d.ts
CHANGED
|
@@ -178,6 +178,14 @@ export declare function applyLoopStallAnswer(input: {
|
|
|
178
178
|
action: "requeued" | "parked" | "already_applied";
|
|
179
179
|
} | undefined;
|
|
180
180
|
export declare function loopStatus(runDir: string, laneId: string): LoopStatus;
|
|
181
|
+
/**
|
|
182
|
+
* Commit a stop envelope that is the only thing left in an idle loop lane's
|
|
183
|
+
* queue: no phase is active and the stop is the FIFO head. The loop
|
|
184
|
+
* controller would do exactly this on its next claim; doing it in-process
|
|
185
|
+
* lets a queued loop lane end without waiting for admission. Returns false
|
|
186
|
+
* and changes nothing when the queue holds anything ahead of the stop.
|
|
187
|
+
*/
|
|
188
|
+
export declare function commitIdleLoopStop(runDir: string, laneId: string, now?: () => Date): boolean;
|
|
181
189
|
export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined): void;
|
|
182
190
|
export declare function clearLoopControllerPid(runDir: string, laneId: string, pid: number): void;
|
|
183
191
|
export declare function loopControllerIsAlive(runDir: string): boolean;
|
package/dist/lib/lanes/loop.js
CHANGED
|
@@ -596,6 +596,41 @@ export function loopStatus(runDir, laneId) {
|
|
|
596
596
|
updated_at: typeof loop.updated_at === "string" ? loop.updated_at : new Date(0).toISOString(),
|
|
597
597
|
};
|
|
598
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Commit a stop envelope that is the only thing left in an idle loop lane's
|
|
601
|
+
* queue: no phase is active and the stop is the FIFO head. The loop
|
|
602
|
+
* controller would do exactly this on its next claim; doing it in-process
|
|
603
|
+
* lets a queued loop lane end without waiting for admission. Returns false
|
|
604
|
+
* and changes nothing when the queue holds anything ahead of the stop.
|
|
605
|
+
*/
|
|
606
|
+
export function commitIdleLoopStop(runDir, laneId, now = () => new Date()) {
|
|
607
|
+
if (envelopeFiles(activeDir(runDir)).length > 0)
|
|
608
|
+
return false;
|
|
609
|
+
const head = envelopeFiles(pendingDir(runDir))[0];
|
|
610
|
+
if (!head)
|
|
611
|
+
return false;
|
|
612
|
+
let headRecord;
|
|
613
|
+
try {
|
|
614
|
+
headRecord = fileEnvelope(head);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if (headRecord.envelope.kind !== "stop")
|
|
620
|
+
return false;
|
|
621
|
+
const claimed = claimNextLoopEnvelope(runDir);
|
|
622
|
+
if (!claimed)
|
|
623
|
+
return false;
|
|
624
|
+
if (claimed.record.envelope.kind !== "stop") {
|
|
625
|
+
// Another claimer moved the queue between the check and the claim; put it back.
|
|
626
|
+
fs.renameSync(claimed.file, path.join(pendingDir(runDir), path.basename(claimed.file)));
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
const stopped = { schema_version: LOOP_SCHEMA, envelope_id: claimed.record.envelope_id, status: "stopped", started_at: claimed.record.queued_at, finished_at: now().toISOString() };
|
|
630
|
+
writeResultAt(runDir, claimed.file, claimed.record, stopped);
|
|
631
|
+
persistStatus(runDir, laneId, { state: "stopped", queue_depth: listLoopQueue(runDir).length, active_envelope_id: null, active_phase: null });
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
599
634
|
export function setLoopControllerPid(runDir, laneId, pid) {
|
|
600
635
|
persistStatus(runDir, laneId, { pid });
|
|
601
636
|
}
|