@indigoai-us/hq-cli 5.345.147 → 5.345.148
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 +30 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +297 -3
- package/dist/command-catalog.generated.js +368 -3
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
- package/dist/commands/core-refresh-vault-access-native.js +214 -19
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +9 -2
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +7 -0
- package/dist/commands/install-global.js +129 -35
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +36 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
- package/dist/lib/doctor/checks/anywhere.js +23 -8
- package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
- package/dist/lib/doctor/checks/cli-path.js +45 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +16 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +10 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/user-path.d.ts +48 -0
- package/dist/lib/windows/user-path.js +114 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { LanesError } from "./errors.js";
|
|
6
|
+
export const LANE_HOST_SCHEMA = "hq-lane-host.v1";
|
|
7
|
+
export const LANE_HOST_FILENAME = "lane-host.json";
|
|
8
|
+
const MAX_ALIASES = 32;
|
|
9
|
+
/**
|
|
10
|
+
* mDNS and DHCP rename a machine by swapping only this suffix
|
|
11
|
+
* (`Mac.localdomain` → `Mac.lan`). The label before the first dot must match,
|
|
12
|
+
* and the entire remainder must be one of these. `ec2.internal` and public
|
|
13
|
+
* DNS names are not in the set.
|
|
14
|
+
*
|
|
15
|
+
* A lane stamped with the bare label (`Mac`) matches a current or recorded
|
|
16
|
+
* name whose first label is that same word (`Mac.lan`). Two different
|
|
17
|
+
* machines that share a short label and a lane store can still collide for
|
|
18
|
+
* an old hostname-stamped lane. New lanes record the stable id below, and a
|
|
19
|
+
* copied home directory remints that id when the hardware id differs.
|
|
20
|
+
*/
|
|
21
|
+
const LINK_LOCAL_SUFFIXES = new Set(["local", "localdomain", "lan", "home"]);
|
|
22
|
+
const HOST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
23
|
+
export function laneHostIdentityPath(homedir) {
|
|
24
|
+
return path.join(homedir, ".hq", LANE_HOST_FILENAME);
|
|
25
|
+
}
|
|
26
|
+
function corruptIdentity(file, problem, details = {}) {
|
|
27
|
+
return new LanesError("host_identity_corrupt", `${problem} The lane host file is ${file}. Restore that file, or delete it if no lane's host id came from it, then retry. Do not edit a lane's host.id by hand.`, { path: file, ...details });
|
|
28
|
+
}
|
|
29
|
+
function assertHostId(file, id) {
|
|
30
|
+
const trimmed = id.trim();
|
|
31
|
+
if (!HOST_ID_PATTERN.test(trimmed)) {
|
|
32
|
+
throw corruptIdentity(file, `Lane host id ${JSON.stringify(id)} is not a stable host id.`);
|
|
33
|
+
}
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
function linkLocalLabel(name) {
|
|
37
|
+
const trimmed = name.trim();
|
|
38
|
+
const dot = trimmed.indexOf(".");
|
|
39
|
+
if (dot <= 0)
|
|
40
|
+
return null;
|
|
41
|
+
const suffix = trimmed.slice(dot + 1);
|
|
42
|
+
if (!LINK_LOCAL_SUFFIXES.has(suffix))
|
|
43
|
+
return null;
|
|
44
|
+
const label = trimmed.slice(0, dot);
|
|
45
|
+
return label.length > 0 ? label : null;
|
|
46
|
+
}
|
|
47
|
+
function linkLocalSameMachine(recorded, current) {
|
|
48
|
+
const recordedLabel = linkLocalLabel(recorded);
|
|
49
|
+
const currentLabel = linkLocalLabel(current);
|
|
50
|
+
return recordedLabel !== null && recordedLabel === currentLabel;
|
|
51
|
+
}
|
|
52
|
+
function firstLabel(name) {
|
|
53
|
+
const trimmed = name.trim();
|
|
54
|
+
const dot = trimmed.indexOf(".");
|
|
55
|
+
return dot === -1 ? trimmed : trimmed.slice(0, dot);
|
|
56
|
+
}
|
|
57
|
+
/** A bare stamp (`Mac`) and a longer name with that first label (`Mac.lan`) are the same machine. */
|
|
58
|
+
function bareLabelSameMachine(recorded, current) {
|
|
59
|
+
const left = recorded.trim();
|
|
60
|
+
const right = current.trim();
|
|
61
|
+
if (!left || !right || left === right)
|
|
62
|
+
return false;
|
|
63
|
+
const leftBare = !left.includes(".");
|
|
64
|
+
const rightBare = !right.includes(".");
|
|
65
|
+
if (leftBare === rightBare)
|
|
66
|
+
return false;
|
|
67
|
+
const bare = leftBare ? left : right;
|
|
68
|
+
const dotted = leftBare ? right : left;
|
|
69
|
+
return bare === firstLabel(dotted);
|
|
70
|
+
}
|
|
71
|
+
function sameMachineName(recorded, current) {
|
|
72
|
+
return linkLocalSameMachine(recorded, current) || bareLabelSameMachine(recorded, current);
|
|
73
|
+
}
|
|
74
|
+
/** True when `recordedId` is this machine's stable id, current hostname, or a known earlier hostname. */
|
|
75
|
+
export function localHostIdsReferToSameMachine(recordedId, host) {
|
|
76
|
+
const recorded = recordedId.trim();
|
|
77
|
+
if (!recorded)
|
|
78
|
+
return false;
|
|
79
|
+
const hostname = host.hostname.trim();
|
|
80
|
+
if (hostname !== "" && recorded === hostname)
|
|
81
|
+
return true;
|
|
82
|
+
const machineId = host.machineId?.trim();
|
|
83
|
+
if (machineId && recorded === machineId)
|
|
84
|
+
return true;
|
|
85
|
+
const aliases = host.aliases ?? [];
|
|
86
|
+
if (aliases.some((alias) => alias.trim() === recorded))
|
|
87
|
+
return true;
|
|
88
|
+
if (hostname !== "" && sameMachineName(recorded, hostname))
|
|
89
|
+
return true;
|
|
90
|
+
return aliases.some((alias) => sameMachineName(recorded, alias));
|
|
91
|
+
}
|
|
92
|
+
function cleanedAliases(file, aliases) {
|
|
93
|
+
if (!Array.isArray(aliases)) {
|
|
94
|
+
throw corruptIdentity(file, "Lane host identity aliases must be a list.");
|
|
95
|
+
}
|
|
96
|
+
const cleaned = [];
|
|
97
|
+
for (const alias of aliases) {
|
|
98
|
+
if (typeof alias !== "string") {
|
|
99
|
+
throw corruptIdentity(file, "Lane host identity aliases must be strings.");
|
|
100
|
+
}
|
|
101
|
+
const trimmed = alias.trim();
|
|
102
|
+
if (!trimmed || cleaned.includes(trimmed))
|
|
103
|
+
continue;
|
|
104
|
+
cleaned.push(trimmed);
|
|
105
|
+
}
|
|
106
|
+
return cleaned;
|
|
107
|
+
}
|
|
108
|
+
function withHostname(aliases, hostname) {
|
|
109
|
+
const host = hostname.trim();
|
|
110
|
+
const next = [...aliases];
|
|
111
|
+
if (host && !next.includes(host))
|
|
112
|
+
next.push(host);
|
|
113
|
+
if (next.length <= MAX_ALIASES)
|
|
114
|
+
return next;
|
|
115
|
+
const kept = next.slice(next.length - MAX_ALIASES);
|
|
116
|
+
if (host && !kept.includes(host)) {
|
|
117
|
+
kept.shift();
|
|
118
|
+
kept.push(host);
|
|
119
|
+
}
|
|
120
|
+
return kept;
|
|
121
|
+
}
|
|
122
|
+
function normalizeHardwareId(value) {
|
|
123
|
+
if (typeof value !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
if (!trimmed || trimmed.length > 128 || /\s/.test(trimmed))
|
|
127
|
+
return null;
|
|
128
|
+
return trimmed;
|
|
129
|
+
}
|
|
130
|
+
function parseIdentity(file, raw) {
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(raw);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw corruptIdentity(file, "Lane host identity file is not valid JSON.");
|
|
137
|
+
}
|
|
138
|
+
if (!parsed || typeof parsed !== "object") {
|
|
139
|
+
throw corruptIdentity(file, "Lane host identity file is not an object.");
|
|
140
|
+
}
|
|
141
|
+
const record = parsed;
|
|
142
|
+
if (record.schema_version !== LANE_HOST_SCHEMA) {
|
|
143
|
+
throw corruptIdentity(file, "Lane host identity file has the wrong schema.", {
|
|
144
|
+
schema_version: record.schema_version ?? null,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (typeof record.id !== "string") {
|
|
148
|
+
throw corruptIdentity(file, "Lane host identity file is missing an id.");
|
|
149
|
+
}
|
|
150
|
+
let hardwareId;
|
|
151
|
+
if (record.hardware_id !== undefined) {
|
|
152
|
+
const normalized = normalizeHardwareId(record.hardware_id);
|
|
153
|
+
if (!normalized) {
|
|
154
|
+
throw corruptIdentity(file, "Lane host identity hardware id is not usable.");
|
|
155
|
+
}
|
|
156
|
+
hardwareId = normalized;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
schema_version: LANE_HOST_SCHEMA,
|
|
160
|
+
id: assertHostId(file, record.id),
|
|
161
|
+
aliases: cleanedAliases(file, record.aliases),
|
|
162
|
+
...(hardwareId ? { hardware_id: hardwareId } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function serialize(identity) {
|
|
166
|
+
return `${JSON.stringify(identity, null, 2)}\n`;
|
|
167
|
+
}
|
|
168
|
+
function readIdentityFile(file) {
|
|
169
|
+
let raw;
|
|
170
|
+
try {
|
|
171
|
+
raw = fs.readFileSync(file, "utf8");
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (err.code === "ENOENT")
|
|
175
|
+
return null;
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
return parseIdentity(file, raw);
|
|
179
|
+
}
|
|
180
|
+
function sameAliases(left, right) {
|
|
181
|
+
return left.length === right.length && left.every((alias, index) => alias === right[index]);
|
|
182
|
+
}
|
|
183
|
+
function writeIdentity(file, identity) {
|
|
184
|
+
const dir = path.dirname(file);
|
|
185
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
186
|
+
const tmp = path.join(dir, `.${LANE_HOST_FILENAME}.${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
187
|
+
fs.writeFileSync(tmp, serialize(identity));
|
|
188
|
+
fs.renameSync(tmp, file);
|
|
189
|
+
}
|
|
190
|
+
function readTextFile(file) {
|
|
191
|
+
try {
|
|
192
|
+
const text = fs.readFileSync(file, "utf8").trim();
|
|
193
|
+
return text || null;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
let cachedHardwareId;
|
|
200
|
+
function readPlatformHardwareId() {
|
|
201
|
+
if (process.platform === "darwin") {
|
|
202
|
+
const result = spawnSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
timeout: 2000,
|
|
205
|
+
});
|
|
206
|
+
if (result.error || result.status !== 0)
|
|
207
|
+
return null;
|
|
208
|
+
const match = (result.stdout ?? "").match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
209
|
+
return match?.[1]?.trim() || null;
|
|
210
|
+
}
|
|
211
|
+
if (process.platform === "linux") {
|
|
212
|
+
return readTextFile("/etc/machine-id") ?? readTextFile("/var/lib/dbus/machine-id");
|
|
213
|
+
}
|
|
214
|
+
if (process.platform === "win32") {
|
|
215
|
+
const result = spawnSync("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"], {
|
|
216
|
+
encoding: "utf8",
|
|
217
|
+
timeout: 2000,
|
|
218
|
+
});
|
|
219
|
+
if (result.error || result.status !== 0)
|
|
220
|
+
return null;
|
|
221
|
+
const match = (result.stdout ?? "").match(/MachineGuid\s+REG_SZ\s+(\S+)/);
|
|
222
|
+
return match?.[1]?.trim() || null;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function currentHardwareId(readHardwareId) {
|
|
227
|
+
if (readHardwareId)
|
|
228
|
+
return normalizeHardwareId(safeRead(readHardwareId));
|
|
229
|
+
if (cachedHardwareId !== undefined)
|
|
230
|
+
return cachedHardwareId;
|
|
231
|
+
cachedHardwareId = normalizeHardwareId(safeRead(readPlatformHardwareId));
|
|
232
|
+
return cachedHardwareId;
|
|
233
|
+
}
|
|
234
|
+
function safeRead(read) {
|
|
235
|
+
try {
|
|
236
|
+
return read();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function mintIdentity(file, hostname, hardwareId, createId) {
|
|
243
|
+
return {
|
|
244
|
+
schema_version: LANE_HOST_SCHEMA,
|
|
245
|
+
id: assertHostId(file, (createId ?? (() => crypto.randomUUID()))()),
|
|
246
|
+
aliases: hostname ? [hostname] : [],
|
|
247
|
+
...(hardwareId ? { hardware_id: hardwareId } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function alignStoredIdentity(file, existing, hostname, hardwareId, createId) {
|
|
251
|
+
if (hardwareId && existing.hardware_id && existing.hardware_id !== hardwareId) {
|
|
252
|
+
const reminted = mintIdentity(file, hostname, hardwareId, createId);
|
|
253
|
+
writeIdentity(file, reminted);
|
|
254
|
+
return reminted;
|
|
255
|
+
}
|
|
256
|
+
const aliases = withHostname(existing.aliases, hostname);
|
|
257
|
+
const next = {
|
|
258
|
+
...existing,
|
|
259
|
+
aliases,
|
|
260
|
+
...(hardwareId && !existing.hardware_id ? { hardware_id: hardwareId } : {}),
|
|
261
|
+
};
|
|
262
|
+
if (next.hardware_id === existing.hardware_id && sameAliases(aliases, existing.aliases))
|
|
263
|
+
return existing;
|
|
264
|
+
writeIdentity(file, next);
|
|
265
|
+
return next;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Read the stable id, creating it once. A lost race re-reads the winner
|
|
269
|
+
* instead of minting a second id that would strand the lanes just created.
|
|
270
|
+
* A stored hardware id that does not match this machine mints a new id so a
|
|
271
|
+
* cloned home directory does not keep the source machine's id.
|
|
272
|
+
*/
|
|
273
|
+
export function resolveLocalHostIdentity(input) {
|
|
274
|
+
const hostname = input.hostname.trim();
|
|
275
|
+
if (!hostname) {
|
|
276
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host for lane placement", {});
|
|
277
|
+
}
|
|
278
|
+
const file = laneHostIdentityPath(input.homedir);
|
|
279
|
+
const hardwareId = currentHardwareId(input.readHardwareId);
|
|
280
|
+
const existing = readIdentityFile(file);
|
|
281
|
+
if (existing)
|
|
282
|
+
return alignStoredIdentity(file, existing, hostname, hardwareId, input.createId);
|
|
283
|
+
const created = mintIdentity(file, hostname, hardwareId, input.createId);
|
|
284
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
285
|
+
try {
|
|
286
|
+
fs.writeFileSync(file, serialize(created), { flag: "wx" });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err.code !== "EEXIST")
|
|
290
|
+
throw err;
|
|
291
|
+
const raced = readIdentityFile(file);
|
|
292
|
+
if (!raced) {
|
|
293
|
+
throw corruptIdentity(file, "Lane host identity file disappeared while it was created.");
|
|
294
|
+
}
|
|
295
|
+
return alignStoredIdentity(file, raced, hostname, hardwareId, input.createId);
|
|
296
|
+
}
|
|
297
|
+
return created;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Read an existing identity. Does not create the file, record a hostname,
|
|
301
|
+
* or replace an id. A hardware mismatch returns null so a cloned id is not
|
|
302
|
+
* treated as this machine.
|
|
303
|
+
*/
|
|
304
|
+
export function observeLocalHostIdentity(input) {
|
|
305
|
+
const file = laneHostIdentityPath(input.homedir);
|
|
306
|
+
const existing = readIdentityFile(file);
|
|
307
|
+
if (!existing)
|
|
308
|
+
return null;
|
|
309
|
+
const hardwareId = currentHardwareId(input.readHardwareId);
|
|
310
|
+
if (hardwareId && existing.hardware_id && existing.hardware_id !== hardwareId)
|
|
311
|
+
return null;
|
|
312
|
+
return existing;
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=local-host-identity.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LaneRecord } from "./types.js";
|
|
2
2
|
import { type LegacyPoolFinding } from "./legacy-pool.js";
|
|
3
3
|
export declare const DEFAULT_REAP_MIN_AGE_MINUTES = 120;
|
|
4
4
|
export declare const REAP_SINGLE_FLIGHT_LOCK_STALE_MS: number;
|
package/dist/lib/lanes/reaper.js
CHANGED
|
@@ -8,6 +8,7 @@ import { laneRunsOnHost } from "./admission.js";
|
|
|
8
8
|
import { laneRunDir, isPidRecordAlive, readPidRecord, readProcStarttime } from "./spawn.js";
|
|
9
9
|
import { readAllLanes, readLane } from "./store.js";
|
|
10
10
|
import { LanesError } from "./errors.js";
|
|
11
|
+
import { isSubagentLane } from "./types.js";
|
|
11
12
|
import { findLegacyPoolLanes, stopLegacyPoolLane } from "./legacy-pool.js";
|
|
12
13
|
export const DEFAULT_REAP_MIN_AGE_MINUTES = 120;
|
|
13
14
|
export const REAP_SINGLE_FLIGHT_LOCK_STALE_MS = 60 * 60 * 1000;
|
|
@@ -220,6 +221,12 @@ export async function classifyLocalLanes(root, now, minAgeMinutes, deps = {}) {
|
|
|
220
221
|
const ageMs = now.getTime() - Date.parse(lane.created_at);
|
|
221
222
|
const age = Number.isFinite(ageMs) ? Math.max(0, Math.floor(ageMs / 60_000)) : 0;
|
|
222
223
|
const finding = { lane_id: lane.lane_id, orphaned: false, reason: "not_a_candidate", age_minutes: age };
|
|
224
|
+
if (isSubagentLane(lane)) {
|
|
225
|
+
// A subagent lane has no process tree to reap; its senior session owns it.
|
|
226
|
+
finding.reason = "subagent_runner";
|
|
227
|
+
result.push(finding);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
223
230
|
const laneRunDirectory = laneRunDir(root, lane.lane_id);
|
|
224
231
|
const workerRecord = readPidRecord(path.join(laneRunDirectory, "worker.pid"));
|
|
225
232
|
const watcherRecord = readPidRecord(path.join(laneRunDirectory, "watcher.pid"));
|
|
@@ -87,6 +87,10 @@ export interface ReconcileSummary {
|
|
|
87
87
|
export interface ReconcileHost {
|
|
88
88
|
hostname: string;
|
|
89
89
|
outpostId: string | null;
|
|
90
|
+
/** Stable id from ~/.hq/lane-host.json, when this process has read one. */
|
|
91
|
+
machineId?: string;
|
|
92
|
+
/** Hostnames this machine has already used. */
|
|
93
|
+
aliases?: readonly string[];
|
|
90
94
|
}
|
|
91
95
|
export interface ReconcileDeps {
|
|
92
96
|
now: () => Date;
|
|
@@ -22,7 +22,7 @@ import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./gr
|
|
|
22
22
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
|
|
23
23
|
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, nextEnvelopePath, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
24
24
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, retryUpdateLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
25
|
-
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled,
|
|
25
|
+
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readMemorySignal, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
26
26
|
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "./registry-flag.js";
|
|
27
27
|
import { LANE_BLOCKED_ESCALATION_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, LANE_LEAD_ADMISSION_PRIORITY_FLAG, LANE_QUOTA_ACCOUNT_FAILOVER_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "./flag-keys.js";
|
|
28
28
|
import { checkLaneDiskSpace, configuredCodexRolloutHomes, laneDiskAccount, retainCodexRollouts } from "./disk-guard.js";
|
|
@@ -32,7 +32,7 @@ import { monitorSessionDir } from "../monitor.js";
|
|
|
32
32
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
33
33
|
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, dueKeepAliveNextCheckMs, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
34
34
|
import { isFailedQuotaKeepAliveLane, providerSupportsSessionResume } from "./delivery.js";
|
|
35
|
-
import { LANE_PROVIDERS, isIsoDateTime, isTerminalLaneState } from "./types.js";
|
|
35
|
+
import { LANE_PROVIDERS, isIsoDateTime, isSubagentLane, isTerminalLaneState } from "./types.js";
|
|
36
36
|
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
37
37
|
import { deliverQuotaNotice, quotaLaneNotice } from "./quota-notice.js";
|
|
38
38
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -1586,6 +1586,10 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1586
1586
|
for (const lane of lanes) {
|
|
1587
1587
|
if (lane.state !== "running")
|
|
1588
1588
|
continue;
|
|
1589
|
+
// No HQ process backs a subagent lane, so a missing pid is not a dead
|
|
1590
|
+
// worker. Its senior session reports progress and the final envelope.
|
|
1591
|
+
if (isSubagentLane(lane))
|
|
1592
|
+
continue;
|
|
1589
1593
|
const local = laneRunsHere(lane, deps.host);
|
|
1590
1594
|
if (local) {
|
|
1591
1595
|
const runDir = laneRunDir(hqRoot, lane.lane_id);
|
|
@@ -2097,11 +2101,12 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
2097
2101
|
const busy = deps.cpuBusyFraction !== undefined
|
|
2098
2102
|
? deps.cpuBusyFraction
|
|
2099
2103
|
: await resolveCpuBusyFraction(throttleDeps);
|
|
2100
|
-
const
|
|
2104
|
+
const memory = readMemorySignal(throttleDeps);
|
|
2101
2105
|
const nowMs = now.getTime();
|
|
2102
2106
|
const decision = evaluateThrottle({
|
|
2103
2107
|
busy,
|
|
2104
|
-
availableMemMb,
|
|
2108
|
+
availableMemMb: memory?.available_mb ?? null,
|
|
2109
|
+
memory,
|
|
2105
2110
|
cpuCheckEnabled: isCpuCheckEnabled(throttleDeps.env),
|
|
2106
2111
|
state: throttleState,
|
|
2107
2112
|
throttle: config.throttle,
|
|
@@ -3570,11 +3575,14 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
3570
3575
|
if (deps.dryRun) {
|
|
3571
3576
|
const loaded = readThrottleState(throttleStatePath(hqRoot), throttleDeps);
|
|
3572
3577
|
const state = loaded === "missing" ? emptyThrottleState() : loaded;
|
|
3578
|
+
const busy = deps.cpuBusyFraction !== undefined
|
|
3579
|
+
? deps.cpuBusyFraction
|
|
3580
|
+
: deps.cpuObservation?.busy ?? await resolveCpuBusyFraction(throttleDeps);
|
|
3581
|
+
const memory = readMemorySignal(throttleDeps);
|
|
3573
3582
|
throttleDecision = evaluateThrottle({
|
|
3574
|
-
busy
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
availableMemMb: readAvailableMemMb(throttleDeps),
|
|
3583
|
+
busy,
|
|
3584
|
+
availableMemMb: memory?.available_mb ?? null,
|
|
3585
|
+
memory,
|
|
3578
3586
|
cpuCheckEnabled: isCpuCheckEnabled(throttleDeps.env),
|
|
3579
3587
|
state,
|
|
3580
3588
|
throttle: config.throttle,
|
|
@@ -190,6 +190,17 @@ export declare const STANDING_AUTHORIZATION: string;
|
|
|
190
190
|
* turn while it was still running; nothing in the brief said not to.
|
|
191
191
|
*/
|
|
192
192
|
export declare const FOREGROUND_ONLY_RULE: string;
|
|
193
|
+
/**
|
|
194
|
+
* Temp-home rule carried in every composed brief, for every provider. Lanes
|
|
195
|
+
* that ran tests under a raw mktemp HOME re-downloaded the Rust toolchain and
|
|
196
|
+
* pnpm store (1.4-1.8 GB) per run and never deleted the folder; on 2026-10-18
|
|
197
|
+
* that refilled about 45 GB of disk.
|
|
198
|
+
*
|
|
199
|
+
* The wrapper path is absolute because a worker's cwd is often a repo, not
|
|
200
|
+
* the HQ root. The wrapper ships in hq-core (hq-core-staging#1420); until an
|
|
201
|
+
* install has it, the command fails with "No such file".
|
|
202
|
+
*/
|
|
203
|
+
export declare function tempHomeRule(hqRoot: string): string;
|
|
193
204
|
/**
|
|
194
205
|
* The answers as written by the reconcile pass (`answered-questions.json`),
|
|
195
206
|
* reduced to id + choice. A malformed file reads as no inline answers; the
|
|
@@ -288,6 +299,15 @@ export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonR
|
|
|
288
299
|
* Must run before the worker is spawned.
|
|
289
300
|
*/
|
|
290
301
|
export declare function refuseOutpostSelfTarget(hqRoot: string, lane: LaneRecord, deps: SpawnLaneDeps): void;
|
|
302
|
+
/**
|
|
303
|
+
* The hq CLI entry a watcher runs internal `hq lanes` commands through. A
|
|
304
|
+
* watcher admits queued lanes and retries accounts from its own process,
|
|
305
|
+
* where process.argv[1] is watcher-main.js rather than the CLI. Recording that
|
|
306
|
+
* path made envelope repair run `watcher-main.js lanes _spawn-envelope-repair`,
|
|
307
|
+
* which started a watcher for a lane named "lanes". A runner script in argv[1]
|
|
308
|
+
* is replaced by the CLI entry that ships beside this module.
|
|
309
|
+
*/
|
|
310
|
+
export declare function resolveHqCliEntry(override: string | undefined, argvEntry?: string | undefined, existsSync?: (file: string) => boolean): string | undefined;
|
|
291
311
|
/** Arm (or re-arm) the detached per-lane watcher. */
|
|
292
312
|
export declare function armLaneWatcher(hqRoot: string, laneId: string, deps?: SpawnLaneDeps): number;
|
|
293
313
|
export interface SpawnManifestFile {
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -33,6 +33,7 @@ import { managerStatusGateHeartbeat } from "./manager-status-gate.js";
|
|
|
33
33
|
import { LANE_QUOTA_ACCOUNT_FAILOVER_FLAG } from "./flag-keys.js";
|
|
34
34
|
import { readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
35
35
|
import { LANE_WORKER_EXIT_PATH_ENV } from "./worker-process.js";
|
|
36
|
+
import { isLaneRunnerEntry } from "./envelope-repair-dispatch.js";
|
|
36
37
|
import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
37
38
|
import { LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT } from "./keep-alive.js";
|
|
38
39
|
import { copySessionFiles, planSessionCopy } from "./move-account.js";
|
|
@@ -45,7 +46,7 @@ import { resolveAdmissionHost } from "./admission-host.js";
|
|
|
45
46
|
import { offlineFingerprint } from "../fingerprint/offline.js";
|
|
46
47
|
import { SESSION_ID_ENV_VARS } from "../fingerprint/self-core.js";
|
|
47
48
|
import { checkLaneDiskSpace, laneDiskAccount } from "./disk-guard.js";
|
|
48
|
-
import { LANE_ENVELOPE_JSON_SCHEMA, ENVELOPE_INVALID_REASON, parseLaneProvider, } from "./types.js";
|
|
49
|
+
import { LANE_ENVELOPE_JSON_SCHEMA, ENVELOPE_INVALID_REASON, isSubagentLane, parseLaneProvider, } from "./types.js";
|
|
49
50
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
50
51
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
51
52
|
export const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
|
|
@@ -1225,6 +1226,12 @@ function proveDetachedViaPs(pid) {
|
|
|
1225
1226
|
return null;
|
|
1226
1227
|
}
|
|
1227
1228
|
}
|
|
1229
|
+
/** Refuse to start a provider process or watcher for a lane a subagent runs. */
|
|
1230
|
+
function assertProcessRunner(lane) {
|
|
1231
|
+
if (!isSubagentLane(lane))
|
|
1232
|
+
return;
|
|
1233
|
+
throw new LanesError("subagent_lane_not_spawnable", `Lane ${lane.lane_id} is run by an in-session subagent; HQ does not start a process for it`, { lane_id: lane.lane_id, runner: "subagent" });
|
|
1234
|
+
}
|
|
1228
1235
|
export function nextEnvelopePath(runDir) {
|
|
1229
1236
|
const dir = path.join(runDir, "envelopes");
|
|
1230
1237
|
let names = new Set();
|
|
@@ -1280,6 +1287,25 @@ export const FOREGROUND_ONLY_RULE = "FOREGROUND ONLY. Run every command, test su
|
|
|
1280
1287
|
"Never background a process and end the turn while it is still running. " +
|
|
1281
1288
|
"A turn that ends before the envelope is written is classified interrupted, " +
|
|
1282
1289
|
"and whatever the background process produces afterwards is not read.";
|
|
1290
|
+
/**
|
|
1291
|
+
* Temp-home rule carried in every composed brief, for every provider. Lanes
|
|
1292
|
+
* that ran tests under a raw mktemp HOME re-downloaded the Rust toolchain and
|
|
1293
|
+
* pnpm store (1.4-1.8 GB) per run and never deleted the folder; on 2026-10-18
|
|
1294
|
+
* that refilled about 45 GB of disk.
|
|
1295
|
+
*
|
|
1296
|
+
* The wrapper path is absolute because a worker's cwd is often a repo, not
|
|
1297
|
+
* the HQ root. The wrapper ships in hq-core (hq-core-staging#1420); until an
|
|
1298
|
+
* install has it, the command fails with "No such file".
|
|
1299
|
+
*/
|
|
1300
|
+
export function tempHomeRule(hqRoot) {
|
|
1301
|
+
const wrapper = path.join(path.resolve(hqRoot), "core", "scripts", "with-temp-home.sh");
|
|
1302
|
+
const quoted = /^[\w./-]+$/.test(wrapper) ? wrapper : `'${wrapper.replace(/'/g, `'\\''`)}'`;
|
|
1303
|
+
return ("TEMP HOME. When a command needs a throwaway $HOME, run it as " +
|
|
1304
|
+
`\`bash ${quoted} -- <command>\`. ` +
|
|
1305
|
+
"Never assign HOME from mktemp directly (HOME=$(mktemp -d) <command>, or export HOME the same way): " +
|
|
1306
|
+
"each fresh home re-downloads the Rust toolchain and pnpm store and is never deleted. " +
|
|
1307
|
+
"The wrapper keeps the real caches and removes the temp home when the command exits.");
|
|
1308
|
+
}
|
|
1283
1309
|
/**
|
|
1284
1310
|
* The answers as written by the reconcile pass (`answered-questions.json`),
|
|
1285
1311
|
* reduced to id + choice. A malformed file reads as no inline answers; the
|
|
@@ -2036,15 +2062,34 @@ function writeSpawnManifest(runDir, input) {
|
|
|
2036
2062
|
: {}),
|
|
2037
2063
|
}, null, 2)}\n`);
|
|
2038
2064
|
}
|
|
2065
|
+
/**
|
|
2066
|
+
* The hq CLI entry a watcher runs internal `hq lanes` commands through. A
|
|
2067
|
+
* watcher admits queued lanes and retries accounts from its own process,
|
|
2068
|
+
* where process.argv[1] is watcher-main.js rather than the CLI. Recording that
|
|
2069
|
+
* path made envelope repair run `watcher-main.js lanes _spawn-envelope-repair`,
|
|
2070
|
+
* which started a watcher for a lane named "lanes". A runner script in argv[1]
|
|
2071
|
+
* is replaced by the CLI entry that ships beside this module.
|
|
2072
|
+
*/
|
|
2073
|
+
export function resolveHqCliEntry(override, argvEntry = process.argv[1], existsSync = fs.existsSync) {
|
|
2074
|
+
const explicit = override?.trim();
|
|
2075
|
+
if (explicit)
|
|
2076
|
+
return explicit;
|
|
2077
|
+
const fromArgv = argvEntry?.trim();
|
|
2078
|
+
if (fromArgv && !isLaneRunnerEntry(fromArgv))
|
|
2079
|
+
return fromArgv;
|
|
2080
|
+
const packaged = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../dist/index.js");
|
|
2081
|
+
return existsSync(packaged) ? packaged : undefined;
|
|
2082
|
+
}
|
|
2039
2083
|
/** Arm (or re-arm) the detached per-lane watcher. */
|
|
2040
2084
|
export function armLaneWatcher(hqRoot, laneId, deps = {}) {
|
|
2041
2085
|
const env = deps.env ?? process.env;
|
|
2042
2086
|
const spawnProcess = deps.spawnProcess ?? defaultSpawnProcess;
|
|
2043
2087
|
const proveDetached = deps.proveDetached ?? defaultProveDetached;
|
|
2088
|
+
assertProcessRunner(readLane(hqRoot, laneId));
|
|
2044
2089
|
const runDir = laneRunDir(hqRoot, laneId);
|
|
2045
2090
|
const logsDir = path.join(runDir, "logs");
|
|
2046
2091
|
ensureDir(logsDir);
|
|
2047
|
-
const cliEntry = deps.hqCliEntry
|
|
2092
|
+
const cliEntry = resolveHqCliEntry(deps.hqCliEntry);
|
|
2048
2093
|
if (!cliEntry) {
|
|
2049
2094
|
throw new LanesError("watcher_cli_entry_unavailable", "Cannot arm the lane watcher without the current hq CLI entrypoint", { lane_id: laneId });
|
|
2050
2095
|
}
|
|
@@ -2167,6 +2212,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2167
2212
|
const runDir = laneRunDir(hqRoot, lane.lane_id);
|
|
2168
2213
|
const previousSpawn = readSpawnManifest(runDir);
|
|
2169
2214
|
const stored = readLane(hqRoot, lane.lane_id);
|
|
2215
|
+
assertProcessRunner(stored);
|
|
2170
2216
|
const recoveredSessionId = (typeof stored.provider_session_id === "string" && stored.provider_session_id.trim()
|
|
2171
2217
|
? stored.provider_session_id
|
|
2172
2218
|
: undefined) ??
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { assertNoSecretText } from "./text-scan.js";
|
|
4
4
|
import { compareEnvelopeNames } from "./envelope-sequence.js";
|
|
5
|
-
import { isTerminalLaneState } from "./types.js";
|
|
5
|
+
import { isSubagentLane, isTerminalLaneState } from "./types.js";
|
|
6
6
|
const OUTPUT_TAIL_BYTES = 64 * 1024;
|
|
7
7
|
const INBOX_TAIL_BYTES = 32 * 1024;
|
|
8
8
|
const PR_URL = /https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+/g;
|
|
@@ -115,6 +115,11 @@ function startTimeMs(lane, runDir) {
|
|
|
115
115
|
if (Number.isFinite(parsed) && parsed > 0)
|
|
116
116
|
return parsed;
|
|
117
117
|
}
|
|
118
|
+
if (isSubagentLane(lane)) {
|
|
119
|
+
// No worker.pid exists; the subagent started when its lane was registered.
|
|
120
|
+
const created = Date.parse(lane.created_at);
|
|
121
|
+
return Number.isFinite(created) ? created : null;
|
|
122
|
+
}
|
|
118
123
|
try {
|
|
119
124
|
return fs.statSync(path.join(runDir, "worker.pid")).mtimeMs;
|
|
120
125
|
}
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -16,7 +16,7 @@ import { generateUlid } from "../mesh/live/ulid.js";
|
|
|
16
16
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
17
17
|
import { readOutpostIdentity } from "./admission-host.js";
|
|
18
18
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
19
|
-
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
19
|
+
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_RUNNERS, LANE_WRITERS, } from "./types.js";
|
|
20
20
|
import { laneStories } from "./stories.js";
|
|
21
21
|
import { laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
22
22
|
export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
@@ -433,6 +433,13 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
433
433
|
value: raw.remote_control,
|
|
434
434
|
});
|
|
435
435
|
}
|
|
436
|
+
if (raw.runner !== undefined && raw.runner !== "subagent") {
|
|
437
|
+
throw new LaneSchemaError(`Unknown lane runner ${JSON.stringify(raw.runner)}. Accepted: ${LANE_RUNNERS.join(", ")}`, {
|
|
438
|
+
path: filePath,
|
|
439
|
+
field: "runner",
|
|
440
|
+
value: raw.runner,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
436
443
|
if (raw.remote_control_title !== undefined && (raw.remote_control !== true ||
|
|
437
444
|
typeof raw.remote_control_title !== "string" ||
|
|
438
445
|
raw.remote_control_title.trim() !== raw.remote_control_title ||
|