@indigoai-us/hq-cli 5.345.128 → 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 +12 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/lanes.js +26 -14
- package/dist/commands/mesh.js +32 -19
- 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/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/daemon-reconcile.d.ts +3 -0
- package/dist/lib/lanes/daemon-reconcile.js +8 -0
- package/dist/lib/lanes/reconcile-result.d.ts +10 -0
- package/dist/lib/lanes/reconcile-result.js +11 -0
- 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 +6 -0
- package/package.json +1 -1
|
@@ -87,7 +87,7 @@ export function messagesStreamResult(stdout, runtimeName) {
|
|
|
87
87
|
if (!sawJson)
|
|
88
88
|
throw new RuntimeError(`${runtimeName} returned non-JSON output`, stdout.trim().slice(-400), null, { excerptIsOutputTail: true });
|
|
89
89
|
if (result?.is_error === true) {
|
|
90
|
-
const detail = typeof result.result === "string" && result.result ? result.result : String(result.subtype ?? "error");
|
|
90
|
+
const detail = typeof result.result === "string" && result.result ? result.result : (resultErrors(result) ?? String(result.subtype ?? "error"));
|
|
91
91
|
throw new RuntimeError(`${runtimeName} reported an error: ${detail.slice(0, 200)}`, detail.slice(0, 400), null);
|
|
92
92
|
}
|
|
93
93
|
const text = (typeof result?.result === "string" && result.result.trim() ? result.result : lastText) ?? "";
|
|
@@ -143,8 +143,12 @@ export function messagesStreamFailure(stdout) {
|
|
|
143
143
|
let reason;
|
|
144
144
|
for (const line of stdout.split("\n")) {
|
|
145
145
|
const ev = parseJsonLine(line);
|
|
146
|
-
if (ev?.type === "result" && ev.is_error === true
|
|
147
|
-
|
|
146
|
+
if (ev?.type === "result" && ev.is_error === true) {
|
|
147
|
+
if (typeof ev.result === "string" && ev.result.trim())
|
|
148
|
+
reason = ev.result.trim();
|
|
149
|
+
else
|
|
150
|
+
reason = resultErrors(ev) ?? reason;
|
|
151
|
+
}
|
|
148
152
|
if (ev?.type === "error" && typeof ev.message === "string" && ev.message.trim())
|
|
149
153
|
reason = ev.message.trim();
|
|
150
154
|
}
|
|
@@ -187,8 +191,32 @@ export function prettyJsonFailure(stdout) {
|
|
|
187
191
|
return ev.result.trim();
|
|
188
192
|
if (typeof ev.message === "string" && ev.message.trim())
|
|
189
193
|
return ev.message.trim();
|
|
194
|
+
const errors = resultErrors(ev);
|
|
195
|
+
if (errors)
|
|
196
|
+
return errors;
|
|
190
197
|
if (typeof ev.subtype === "string" && ev.subtype.trim() && ev.subtype !== "success")
|
|
191
198
|
return ev.subtype.trim();
|
|
192
199
|
return undefined;
|
|
193
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The `errors` array on a Claude result envelope. A failed `--resume` ends
|
|
203
|
+
* with `{"subtype":"error_during_execution","is_error":true,"errors":["No
|
|
204
|
+
* conversation found with session ID: …"]}` and no `result` text; without
|
|
205
|
+
* this the failure reads as the bare subtype, the stale-session check never
|
|
206
|
+
* matches, and every turn on that scope fails the same way.
|
|
207
|
+
*/
|
|
208
|
+
function resultErrors(ev) {
|
|
209
|
+
if (!Array.isArray(ev.errors))
|
|
210
|
+
return undefined;
|
|
211
|
+
const parts = ev.errors.flatMap((e) => {
|
|
212
|
+
if (typeof e === "string")
|
|
213
|
+
return e.trim() ? [e.trim()] : [];
|
|
214
|
+
if (e && typeof e === "object" && typeof e.message === "string") {
|
|
215
|
+
const m = e.message.trim();
|
|
216
|
+
return m ? [m] : [];
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
});
|
|
220
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
221
|
+
}
|
|
194
222
|
//# sourceMappingURL=messages-stream.js.map
|
|
@@ -109,6 +109,10 @@ export interface PlanContext {
|
|
|
109
109
|
syncRunnerProcesses: readonly SyncRunnerProcess[] | null;
|
|
110
110
|
/** PID recorded in the watch-owner status, when it is readable. */
|
|
111
111
|
syncRunnerOwnerPid: number | null;
|
|
112
|
+
/** The daemon's in-process lane watcher owns reconcile while its kill switch is on. */
|
|
113
|
+
lanesReconcileInProcess?: boolean;
|
|
114
|
+
/** The active hq-cloud sync runner owns post-sync QMD indexing. */
|
|
115
|
+
searchIndexOwnedBySyncRunner?: boolean;
|
|
112
116
|
/** Sync runner children already started by this daemon before their lease is written. */
|
|
113
117
|
daemonSyncRunnerPids?: ReadonlySet<number>;
|
|
114
118
|
/** The protocol-event heartbeat watchdog timeout for the sync runner. */
|
|
@@ -227,14 +227,15 @@ function planService(service, ctx) {
|
|
|
227
227
|
}),
|
|
228
228
|
];
|
|
229
229
|
case "lanes-reconcile":
|
|
230
|
+
if (ctx.lanesReconcileInProcess)
|
|
231
|
+
return [];
|
|
230
232
|
return [
|
|
231
233
|
base(service, {
|
|
232
234
|
kind: "scheduled",
|
|
233
235
|
// Polls every 15 s so local keep-alive resumes are admitted on time (#1396).
|
|
234
236
|
// Each pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS), so
|
|
235
|
-
// desktops
|
|
236
|
-
//
|
|
237
|
-
// cadence, or no run, when no lane can need the 15 s poll.
|
|
237
|
+
// desktops reconcile in-process on lane-store changes with a bounded
|
|
238
|
+
// timer for due keep-alive work, avoiding a fresh CLI process per poll.
|
|
238
239
|
intervalSec: 15,
|
|
239
240
|
initialDelaySec: 30,
|
|
240
241
|
command: { type: "hq", args: ["lanes", "reconcile"] },
|
|
@@ -246,6 +247,8 @@ function planService(service, ctx) {
|
|
|
246
247
|
}),
|
|
247
248
|
];
|
|
248
249
|
case "search-index":
|
|
250
|
+
if (ctx.searchIndexOwnedBySyncRunner)
|
|
251
|
+
return [];
|
|
249
252
|
return [
|
|
250
253
|
base(service, {
|
|
251
254
|
kind: "scheduled",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface BoundedDaemonFlagMonitorOptions {
|
|
2
2
|
read: () => Promise<boolean>;
|
|
3
|
+
/** Registry default, also used for absent rows and stale reads. */
|
|
4
|
+
fallback?: boolean;
|
|
3
5
|
now?: () => number;
|
|
4
6
|
onChange?: (enabled: boolean) => void;
|
|
5
7
|
onReadError?: (error: unknown) => void;
|
|
@@ -15,7 +17,8 @@ export interface BoundedDaemonFlagMonitor {
|
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Refreshes a long-running daemon's flag independently of incoming requests.
|
|
18
|
-
* A failed read keeps
|
|
20
|
+
* A failed read keeps a default-off setting only until its 60s age limit; a
|
|
21
|
+
* default-on kill switch immediately returns to its registered safe default.
|
|
19
22
|
*/
|
|
20
23
|
export declare function createBoundedDaemonFlagMonitor(options: BoundedDaemonFlagMonitorOptions): BoundedDaemonFlagMonitor;
|
|
21
24
|
//# sourceMappingURL=flag-monitor.d.ts.map
|
|
@@ -3,11 +3,13 @@ const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30_000;
|
|
|
3
3
|
const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60_000;
|
|
4
4
|
/**
|
|
5
5
|
* Refreshes a long-running daemon's flag independently of incoming requests.
|
|
6
|
-
* A failed read keeps
|
|
6
|
+
* A failed read keeps a default-off setting only until its 60s age limit; a
|
|
7
|
+
* default-on kill switch immediately returns to its registered safe default.
|
|
7
8
|
*/
|
|
8
9
|
export function createBoundedDaemonFlagMonitor(options) {
|
|
9
10
|
const now = options.now ?? (() => performance.now());
|
|
10
|
-
|
|
11
|
+
const fallback = options.fallback ?? false;
|
|
12
|
+
let value = fallback ? true : undefined;
|
|
11
13
|
let lastSuccessAt;
|
|
12
14
|
let lastAttemptAt;
|
|
13
15
|
let lastReadFailed = false;
|
|
@@ -23,7 +25,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
23
25
|
};
|
|
24
26
|
const expireIfStale = () => {
|
|
25
27
|
if (!closed && lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
|
|
26
|
-
publish(
|
|
28
|
+
publish(fallback);
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
31
|
const refresh = () => {
|
|
@@ -44,13 +46,17 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
44
46
|
// Expire from the elapsed timer itself, not a wall-clock comparison.
|
|
45
47
|
expiryTimer = setTimeout(() => {
|
|
46
48
|
if (!closed && successVersion === thisSuccessVersion)
|
|
47
|
-
publish(
|
|
49
|
+
publish(fallback);
|
|
48
50
|
}, HQ_ANYWHERE_FLAG_MAX_STALENESS_MS);
|
|
49
51
|
expiryTimer.unref?.();
|
|
50
52
|
return next;
|
|
51
53
|
}).catch((error) => {
|
|
52
54
|
lastReadFailed = true;
|
|
53
55
|
options.onReadError?.(error);
|
|
56
|
+
if (fallback) {
|
|
57
|
+
publish(true);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
54
60
|
expireIfStale();
|
|
55
61
|
return value ?? false;
|
|
56
62
|
}).finally(() => {
|
|
@@ -69,7 +75,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
69
75
|
expireIfStale();
|
|
70
76
|
if (lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
|
|
71
77
|
void refresh();
|
|
72
|
-
return
|
|
78
|
+
return fallback;
|
|
73
79
|
}
|
|
74
80
|
if (value === undefined)
|
|
75
81
|
return refresh();
|
|
@@ -82,7 +88,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
82
88
|
const age = lastSuccessAt === undefined ? null : Math.max(0, now() - lastSuccessAt);
|
|
83
89
|
const fresh = age !== null && age < HQ_ANYWHERE_FLAG_MAX_STALENESS_MS;
|
|
84
90
|
return {
|
|
85
|
-
enabled: value === true && fresh,
|
|
91
|
+
enabled: value === true && (fresh || fallback),
|
|
86
92
|
lastSuccessfulReadAgeMs: age,
|
|
87
93
|
heldThroughReadError: lastReadFailed && fresh,
|
|
88
94
|
};
|
|
@@ -17,6 +17,8 @@ export interface DaemonFlagClient {
|
|
|
17
17
|
}
|
|
18
18
|
export interface ReadDaemonFlagOptions {
|
|
19
19
|
env?: NodeJS.ProcessEnv;
|
|
20
|
+
/** Registry default for this key; new kill switches use true. */
|
|
21
|
+
fallback?: boolean;
|
|
20
22
|
/** Controls how this read combines a local person setting with the registry value. */
|
|
21
23
|
localOverrideMode?: "respect" | "off-only" | "ignore";
|
|
22
24
|
createClient?: (options: FlagClientOptions) => DaemonFlagClient;
|
package/dist/lib/daemon/flag.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
17
17
|
const env = options.env ?? process.env;
|
|
18
18
|
if (flagKey === HQ_ANYWHERE_RUNTIME_FLAG && hqAnywhereRuntimeLocallyDisabled(env))
|
|
19
19
|
return false;
|
|
20
|
-
const fallback = flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
|
|
20
|
+
const fallback = options.fallback ?? flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
|
|
21
21
|
const override = readLocalOverride(flagKey, env);
|
|
22
22
|
const localOverrideMode = options.localOverrideMode ?? "respect";
|
|
23
23
|
if (override !== null && (localOverrideMode === "respect" || (localOverrideMode === "off-only" && !override)))
|
|
@@ -53,8 +53,11 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
53
53
|
onError(error);
|
|
54
54
|
}
|
|
55
55
|
const resolution = client.explain(flagKey, { fallback });
|
|
56
|
-
if (failed
|
|
57
|
-
|
|
56
|
+
if (failed) {
|
|
57
|
+
if (options.throwOnError && !fallback) {
|
|
58
|
+
throw new Error("hq-flags registry read failed", { cause: reportedError });
|
|
59
|
+
}
|
|
60
|
+
return fallback;
|
|
58
61
|
}
|
|
59
62
|
if (resolution.source === "snapshot")
|
|
60
63
|
return resolution.value === true;
|
|
@@ -63,8 +66,11 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
63
66
|
catch (error) {
|
|
64
67
|
if (!failed)
|
|
65
68
|
onError(error);
|
|
66
|
-
if (
|
|
67
|
-
|
|
69
|
+
if (failed) {
|
|
70
|
+
if (options.throwOnError && !fallback) {
|
|
71
|
+
throw new Error("hq-flags registry read failed", { cause: reportedError ?? error });
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
68
74
|
}
|
|
69
75
|
return fallback;
|
|
70
76
|
}
|
|
@@ -137,9 +137,11 @@ function relaySignal(signal) {
|
|
|
137
137
|
throw new Error(`Unable to relay ${signal}`);
|
|
138
138
|
}
|
|
139
139
|
async function run() {
|
|
140
|
-
const [prefix, version, ...
|
|
140
|
+
const [prefix, version, ...rawArgs] = process.argv.slice(2);
|
|
141
141
|
if (!prefix || !version)
|
|
142
142
|
throw new Error("The daemon hq bootstrap arguments are invalid");
|
|
143
|
+
const watchHandoff = rawArgs[0] !== "--handoff-watch=off";
|
|
144
|
+
const args = watchHandoff ? rawArgs : rawArgs.slice(1);
|
|
143
145
|
const lease = await acquireBootstrapLease(prefix, version);
|
|
144
146
|
const token = randomBytes(16).toString("hex");
|
|
145
147
|
const markerPathFor = (pid) => path.join(lease.directory, `handoff-${pid}-${token}.handoff`);
|
|
@@ -170,16 +172,54 @@ async function run() {
|
|
|
170
172
|
const exit = await new Promise((resolve, reject) => {
|
|
171
173
|
if (!child)
|
|
172
174
|
return reject(new Error("The HQ CLI child did not start"));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
175
|
+
let poll;
|
|
176
|
+
let watcher;
|
|
177
|
+
const closeHandoffWatch = () => {
|
|
178
|
+
if (poll)
|
|
179
|
+
clearInterval(poll);
|
|
180
|
+
poll = undefined;
|
|
181
|
+
watcher?.close();
|
|
182
|
+
watcher = undefined;
|
|
183
|
+
};
|
|
184
|
+
const releaseOnHandoff = () => {
|
|
176
185
|
if (!handoffMarker || leaseReleased || !fs.existsSync(handoffMarker))
|
|
177
186
|
return;
|
|
178
187
|
fs.rmSync(handoffMarker, { force: true });
|
|
179
188
|
releaseLease();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
closeHandoffWatch();
|
|
190
|
+
};
|
|
191
|
+
const startHandoffPolling = () => {
|
|
192
|
+
if (!poll && !leaseReleased)
|
|
193
|
+
poll = setInterval(releaseOnHandoff, 20);
|
|
194
|
+
};
|
|
195
|
+
child.once("error", reject);
|
|
196
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
197
|
+
child.once("exit", closeHandoffWatch);
|
|
198
|
+
child.once("error", closeHandoffWatch);
|
|
199
|
+
if (watchHandoff && handoffMarker) {
|
|
200
|
+
try {
|
|
201
|
+
watcher = fs.watch(lease.directory, (_event, filename) => {
|
|
202
|
+
if (filename === null || filename.toString() === path.basename(handoffMarker))
|
|
203
|
+
releaseOnHandoff();
|
|
204
|
+
});
|
|
205
|
+
watcher.on("error", (error) => {
|
|
206
|
+
process.stderr.write(`hq daemon child bootstrap handoff watch failed (${error.code ?? error.name}); using the recovery poll\n`);
|
|
207
|
+
watcher?.close();
|
|
208
|
+
watcher = undefined;
|
|
209
|
+
startHandoffPolling();
|
|
210
|
+
});
|
|
211
|
+
// Close the create-before-watch race after the watcher is attached.
|
|
212
|
+
releaseOnHandoff();
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
process.stderr.write(`hq daemon child bootstrap handoff watch unavailable (${error.code ?? "UnknownError"}); using the recovery poll\n`);
|
|
216
|
+
startHandoffPolling();
|
|
217
|
+
releaseOnHandoff();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
startHandoffPolling();
|
|
222
|
+
}
|
|
183
223
|
});
|
|
184
224
|
if (!leaseReleased)
|
|
185
225
|
releaseLease();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG = "daemon.child-bootstrap-handoff-watch";
|
|
2
|
+
export declare const DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG = "daemon.lanes-reconcile-file-watch";
|
|
3
|
+
export declare const DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG = "daemon.sync-process-scan-on-missing";
|
|
4
|
+
export declare const DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG = "daemon.search-index-runner-owns-index";
|
|
5
|
+
//# sourceMappingURL=kill-switch-flags.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG = "daemon.child-bootstrap-handoff-watch";
|
|
2
|
+
export const DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG = "daemon.lanes-reconcile-file-watch";
|
|
3
|
+
export const DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG = "daemon.sync-process-scan-on-missing";
|
|
4
|
+
export const DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG = "daemon.search-index-runner-owns-index";
|
|
5
|
+
//# sourceMappingURL=kill-switch-flags.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface DaemonLanesReconcileRegistrationOptions {
|
|
2
|
+
hqRoot: () => string;
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
}
|
|
5
|
+
/** Load the lazy lanes command root only when the daemon needs its reconciler. */
|
|
6
|
+
export declare function ensureDaemonLanesReconcileHandler(options: DaemonLanesReconcileRegistrationOptions): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=lanes-reconcile-loader.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
|
|
2
|
+
async function registerFromLanesCommand(options) {
|
|
3
|
+
const [{ Command }, { registerLanesCommand }] = await Promise.all([
|
|
4
|
+
import("commander"),
|
|
5
|
+
import("../../commands/lanes.js"),
|
|
6
|
+
]);
|
|
7
|
+
const env = { ...options.env };
|
|
8
|
+
delete env.HQ_ROOT;
|
|
9
|
+
registerLanesCommand(new Command(), {
|
|
10
|
+
hqRoot: options.hqRoot,
|
|
11
|
+
cwd: options.hqRoot,
|
|
12
|
+
env,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Load the lazy lanes command root only when the daemon needs its reconciler. */
|
|
16
|
+
export async function ensureDaemonLanesReconcileHandler(options) {
|
|
17
|
+
if (daemonLanesReconcileHandler())
|
|
18
|
+
return;
|
|
19
|
+
await registerFromLanesCommand(options);
|
|
20
|
+
if (!daemonLanesReconcileHandler()) {
|
|
21
|
+
throw new Error("lanes command did not register its daemon reconcile handler");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=lanes-reconcile-loader.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
type Watcher = Pick<fs.FSWatcher, "close">;
|
|
3
|
+
export interface LanesReconcileWatcher {
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Reconcile at startup, on lane-store changes, and on a bounded in-process timer. */
|
|
7
|
+
export declare function createLanesReconcileWatcher(options: {
|
|
8
|
+
directories: readonly string[];
|
|
9
|
+
reconcile: () => Promise<void>;
|
|
10
|
+
debounceMs?: number;
|
|
11
|
+
intervalMs?: number;
|
|
12
|
+
watch?: (directory: string, listener: (event: string, filename: string | Buffer | null) => void) => Watcher;
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
14
|
+
onWatchError?: (error: unknown) => void;
|
|
15
|
+
}): LanesReconcileWatcher;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=lanes-reconcile-watcher.d.ts.map
|
|
@@ -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;
|