@indigoai-us/hq-cli 5.345.147 → 5.345.149
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 +65 -0
- package/README.md +2 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +341 -11
- package/dist/command-catalog.generated.js +421 -11
- package/dist/command-help.js +2 -1
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
- package/dist/commands/core-refresh-vault-access-native.js +356 -28
- 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 +9 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +18 -3
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +8 -8
- package/dist/commands/install-global.js +139 -61
- 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/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/reindex.js +19 -0
- 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 +43 -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.d.ts +10 -0
- package/dist/index.js +76 -5
- package/dist/lazy-commands.js +2 -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/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -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/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
- package/dist/lib/doctor/checks/anywhere.js +77 -11
- package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
- package/dist/lib/doctor/checks/cli-path.js +46 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +17 -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/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -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/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +125 -0
- package/dist/lib/windows/user-path.js +275 -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/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- 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/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- 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
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 ||
|
|
@@ -17,6 +17,14 @@ export declare const STARVATION_MAX_DEFERRALS = 10;
|
|
|
17
17
|
export declare const STARVATION_MAX_MS: number;
|
|
18
18
|
export declare const PROC_MEMINFO = "/proc/meminfo";
|
|
19
19
|
export declare const VM_STAT_BIN = "/usr/bin/vm_stat";
|
|
20
|
+
export declare const SYSCTL_BIN = "/usr/sbin/sysctl";
|
|
21
|
+
/**
|
|
22
|
+
* The sysctls behind macOS `memory_pressure`: its "System-wide memory free
|
|
23
|
+
* percentage" is kern.memorystatus_level, and its pressure level is
|
|
24
|
+
* kern.memorystatus_vm_pressure_level (1 normal, 2 warn, 4 critical).
|
|
25
|
+
* hw.memsize turns the percentage into MiB for min_available_mem_mb.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MEMORY_PRESSURE_SYSCTLS: readonly ["kern.memorystatus_level", "kern.memorystatus_vm_pressure_level", "hw.memsize"];
|
|
20
28
|
export declare const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
21
29
|
export declare const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
22
30
|
export interface CpuTimes {
|
|
@@ -38,7 +46,8 @@ export interface ThrottleDeps {
|
|
|
38
46
|
freemem: () => number;
|
|
39
47
|
platform: () => NodeJS.Platform;
|
|
40
48
|
/**
|
|
41
|
-
* Darwin only: read
|
|
49
|
+
* Darwin only: read the memory_pressure sysctls (vm_stat as a fallback) when
|
|
50
|
+
* /proc/meminfo is absent. Unused on Linux.
|
|
42
51
|
*/
|
|
43
52
|
execFileSync: (file: string, args: readonly string[], options: {
|
|
44
53
|
encoding: "utf8";
|
|
@@ -82,16 +91,45 @@ export declare function recordCpuBusyObservation(state: ThrottleState, observati
|
|
|
82
91
|
export declare function parseMemAvailableKb(contents: string): number | null;
|
|
83
92
|
/**
|
|
84
93
|
* Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
|
|
85
|
-
* pages.
|
|
94
|
+
* pages. Only a fallback when the memory_pressure sysctls cannot be read: it
|
|
95
|
+
* leaves out purgeable and compressible memory, so it reads lower than
|
|
96
|
+
* `memory_pressure` on a Mac that is not under pressure.
|
|
86
97
|
*/
|
|
87
98
|
export declare function parseVmStatAvailableMb(contents: string): number | null;
|
|
99
|
+
export type MemoryPressureLevel = "normal" | "warn" | "critical";
|
|
100
|
+
export type MemorySignalSource = "memory_pressure" | "vm_stat" | "meminfo" | "freemem";
|
|
101
|
+
/** One memory reading and where it came from. */
|
|
102
|
+
export interface MemorySignal {
|
|
103
|
+
source: MemorySignalSource;
|
|
104
|
+
available_mb: number;
|
|
105
|
+
/** memory_pressure only: "System-wide memory free percentage". */
|
|
106
|
+
free_percent?: number;
|
|
107
|
+
/** memory_pressure only: kern.memorystatus_vm_pressure_level. */
|
|
108
|
+
pressure_level?: MemoryPressureLevel;
|
|
109
|
+
}
|
|
110
|
+
/** The memory gate's reason for holding admission, shown to the user. */
|
|
111
|
+
export interface MemoryHold extends MemorySignal {
|
|
112
|
+
threshold_mb: number;
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse `sysctl -n kern.memorystatus_level kern.memorystatus_vm_pressure_level
|
|
117
|
+
* hw.memsize`: one integer per line, in that order.
|
|
118
|
+
*/
|
|
119
|
+
export declare function parseMemoryPressureSysctl(contents: string): MemorySignal | null;
|
|
88
120
|
/**
|
|
89
|
-
*
|
|
90
|
-
* /proc/meminfo,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
121
|
+
* Current memory reading for the admission gate. Prefers kernel MemAvailable.
|
|
122
|
+
* On darwin without /proc/meminfo, reads the same values `memory_pressure`
|
|
123
|
+
* prints (free percentage and pressure level), falls back to vm_stat, and never
|
|
124
|
+
* uses os.freemem(). Elsewhere, falls back to os.freemem() only where
|
|
125
|
+
* MemAvailable is absent. Returning null skips the memory gate instead of
|
|
126
|
+
* holding every lane.
|
|
93
127
|
*/
|
|
128
|
+
export declare function readMemorySignal(deps?: ThrottleDeps): MemorySignal | null;
|
|
129
|
+
/** Available memory in MiB from readMemorySignal; null skips the memory gate. */
|
|
94
130
|
export declare function readAvailableMemMb(deps?: ThrottleDeps): number | null;
|
|
131
|
+
/** Plain explanation of a memory hold: which signal, its value, and the threshold. */
|
|
132
|
+
export declare function formatMemoryHoldMessage(signal: MemorySignal, thresholdMb: number): string;
|
|
95
133
|
export interface ThrottleState {
|
|
96
134
|
consecutiveDeferrals: number;
|
|
97
135
|
lastAdmissionAt: string | null;
|
|
@@ -100,6 +138,8 @@ export interface ThrottleState {
|
|
|
100
138
|
last_busy?: number | null;
|
|
101
139
|
busy_source?: CpuBusySource;
|
|
102
140
|
sampled_at?: string;
|
|
141
|
+
/** Set while the memory gate holds admission; cleared on the next admit or CPU hold. */
|
|
142
|
+
memory_hold?: MemoryHold;
|
|
103
143
|
}
|
|
104
144
|
export type ThrottleAdmitReason = "ok" | "starvation_bound" | "unreadable" | "cpu_check_disabled";
|
|
105
145
|
export type ThrottleDeferReason = "cpu" | "memory";
|
|
@@ -111,12 +151,15 @@ export type ThrottleDecision = {
|
|
|
111
151
|
reason: ThrottleDeferReason;
|
|
112
152
|
busy?: number;
|
|
113
153
|
availableMemMb?: number;
|
|
154
|
+
memory_hold?: MemoryHold;
|
|
114
155
|
};
|
|
115
156
|
export declare function emptyThrottleState(): ThrottleState;
|
|
116
157
|
export declare function starvationBoundReached(state: ThrottleState, nowMs: number): boolean;
|
|
117
158
|
export declare function evaluateThrottle(input: {
|
|
118
159
|
busy: number | null;
|
|
119
160
|
availableMemMb: number | null;
|
|
161
|
+
/** Where availableMemMb came from. Without it the reading is reported as MemAvailable. */
|
|
162
|
+
memory?: MemorySignal | null;
|
|
120
163
|
cpuCheckEnabled: boolean;
|
|
121
164
|
state: ThrottleState | "unreadable";
|
|
122
165
|
throttle: ThrottleConfig;
|
|
@@ -22,6 +22,18 @@ export const STARVATION_MAX_MS = 30 * 60 * 1000;
|
|
|
22
22
|
export const PROC_MEMINFO = "/proc/meminfo";
|
|
23
23
|
export const VM_STAT_BIN = "/usr/bin/vm_stat";
|
|
24
24
|
const VM_STAT_TIMEOUT_MS = 2_000;
|
|
25
|
+
export const SYSCTL_BIN = "/usr/sbin/sysctl";
|
|
26
|
+
/**
|
|
27
|
+
* The sysctls behind macOS `memory_pressure`: its "System-wide memory free
|
|
28
|
+
* percentage" is kern.memorystatus_level, and its pressure level is
|
|
29
|
+
* kern.memorystatus_vm_pressure_level (1 normal, 2 warn, 4 critical).
|
|
30
|
+
* hw.memsize turns the percentage into MiB for min_available_mem_mb.
|
|
31
|
+
*/
|
|
32
|
+
export const MEMORY_PRESSURE_SYSCTLS = [
|
|
33
|
+
"kern.memorystatus_level",
|
|
34
|
+
"kern.memorystatus_vm_pressure_level",
|
|
35
|
+
"hw.memsize",
|
|
36
|
+
];
|
|
25
37
|
export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
26
38
|
export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
27
39
|
function defaultSleep(ms) {
|
|
@@ -200,7 +212,9 @@ function parseVmStatPages(contents, label) {
|
|
|
200
212
|
}
|
|
201
213
|
/**
|
|
202
214
|
* Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
|
|
203
|
-
* pages.
|
|
215
|
+
* pages. Only a fallback when the memory_pressure sysctls cannot be read: it
|
|
216
|
+
* leaves out purgeable and compressible memory, so it reads lower than
|
|
217
|
+
* `memory_pressure` on a Mac that is not under pressure.
|
|
204
218
|
*/
|
|
205
219
|
export function parseVmStatAvailableMb(contents) {
|
|
206
220
|
const pageSizeMatch = /page size of (\d+) bytes/i.exec(contents);
|
|
@@ -214,7 +228,53 @@ export function parseVmStatAvailableMb(contents) {
|
|
|
214
228
|
return null;
|
|
215
229
|
return ((free + inactive + speculative) * pageSize) / (1024 * 1024);
|
|
216
230
|
}
|
|
217
|
-
|
|
231
|
+
const PRESSURE_LEVELS = {
|
|
232
|
+
1: "normal",
|
|
233
|
+
2: "warn",
|
|
234
|
+
4: "critical",
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Parse `sysctl -n kern.memorystatus_level kern.memorystatus_vm_pressure_level
|
|
238
|
+
* hw.memsize`: one integer per line, in that order.
|
|
239
|
+
*/
|
|
240
|
+
export function parseMemoryPressureSysctl(contents) {
|
|
241
|
+
const lines = contents.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
242
|
+
if (lines.length !== MEMORY_PRESSURE_SYSCTLS.length)
|
|
243
|
+
return null;
|
|
244
|
+
if (!lines.every((line) => /^\d+$/.test(line)))
|
|
245
|
+
return null;
|
|
246
|
+
const [freePercent, level, memsize] = lines.map(Number);
|
|
247
|
+
const pressureLevel = PRESSURE_LEVELS[level];
|
|
248
|
+
if (!Number.isSafeInteger(freePercent) || freePercent < 0 || freePercent > 100 ||
|
|
249
|
+
pressureLevel === undefined ||
|
|
250
|
+
!Number.isSafeInteger(memsize) || memsize <= 0) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
source: "memory_pressure",
|
|
255
|
+
available_mb: (memsize * (freePercent / 100)) / (1024 * 1024),
|
|
256
|
+
free_percent: freePercent,
|
|
257
|
+
pressure_level: pressureLevel,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function readDarwinMemoryPressure(deps) {
|
|
261
|
+
try {
|
|
262
|
+
const contents = deps.execFileSync(SYSCTL_BIN, ["-n", ...MEMORY_PRESSURE_SYSCTLS], {
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
timeout: VM_STAT_TIMEOUT_MS,
|
|
265
|
+
});
|
|
266
|
+
const signal = parseMemoryPressureSysctl(contents);
|
|
267
|
+
if (signal !== null)
|
|
268
|
+
return signal;
|
|
269
|
+
process.stderr.write("lanes throttle: darwin memory_pressure sysctls were unreadable; falling back to vm_stat\n");
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
process.stderr.write(`lanes throttle: darwin memory_pressure sysctls failed: ${err instanceof Error ? err.message : String(err)}; falling back to vm_stat\n`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function readDarwinVmStat(deps) {
|
|
218
278
|
try {
|
|
219
279
|
const contents = deps.execFileSync(VM_STAT_BIN, [], {
|
|
220
280
|
encoding: "utf8",
|
|
@@ -222,7 +282,7 @@ function readDarwinAvailableMemMb(deps) {
|
|
|
222
282
|
});
|
|
223
283
|
const mb = parseVmStatAvailableMb(contents);
|
|
224
284
|
if (mb !== null)
|
|
225
|
-
return mb;
|
|
285
|
+
return { source: "vm_stat", available_mb: mb };
|
|
226
286
|
process.stderr.write("lanes throttle: darwin vm_stat output was unreadable; skipping the memory gate\n");
|
|
227
287
|
return null;
|
|
228
288
|
}
|
|
@@ -236,7 +296,7 @@ function readFreememMb(deps) {
|
|
|
236
296
|
const bytes = deps.freemem();
|
|
237
297
|
if (!Number.isFinite(bytes) || bytes < 0)
|
|
238
298
|
return null;
|
|
239
|
-
return bytes / (1024 * 1024);
|
|
299
|
+
return { source: "freemem", available_mb: bytes / (1024 * 1024) };
|
|
240
300
|
}
|
|
241
301
|
catch (err) {
|
|
242
302
|
process.stderr.write(`lanes throttle: freemem fallback failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -244,17 +304,19 @@ function readFreememMb(deps) {
|
|
|
244
304
|
}
|
|
245
305
|
}
|
|
246
306
|
/**
|
|
247
|
-
*
|
|
248
|
-
* /proc/meminfo,
|
|
249
|
-
*
|
|
250
|
-
*
|
|
307
|
+
* Current memory reading for the admission gate. Prefers kernel MemAvailable.
|
|
308
|
+
* On darwin without /proc/meminfo, reads the same values `memory_pressure`
|
|
309
|
+
* prints (free percentage and pressure level), falls back to vm_stat, and never
|
|
310
|
+
* uses os.freemem(). Elsewhere, falls back to os.freemem() only where
|
|
311
|
+
* MemAvailable is absent. Returning null skips the memory gate instead of
|
|
312
|
+
* holding every lane.
|
|
251
313
|
*/
|
|
252
|
-
export function
|
|
314
|
+
export function readMemorySignal(deps = defaultThrottleDeps()) {
|
|
253
315
|
try {
|
|
254
316
|
const contents = deps.readFileSync(PROC_MEMINFO, "utf8");
|
|
255
317
|
const kb = parseMemAvailableKb(contents);
|
|
256
318
|
if (kb !== null)
|
|
257
|
-
return kb / 1024;
|
|
319
|
+
return { source: "meminfo", available_mb: kb / 1024 };
|
|
258
320
|
// /proc/meminfo exists but has no MemAvailable (unusual). Fall back.
|
|
259
321
|
process.stderr.write("lanes throttle: MemAvailable missing from /proc/meminfo; falling back to os.freemem()\n");
|
|
260
322
|
}
|
|
@@ -264,11 +326,35 @@ export function readAvailableMemMb(deps = defaultThrottleDeps()) {
|
|
|
264
326
|
process.stderr.write(`lanes throttle: failed to read ${PROC_MEMINFO}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
265
327
|
return null;
|
|
266
328
|
}
|
|
267
|
-
if (deps.platform() === "darwin")
|
|
268
|
-
return
|
|
329
|
+
if (deps.platform() === "darwin") {
|
|
330
|
+
return readDarwinMemoryPressure(deps) ?? readDarwinVmStat(deps);
|
|
331
|
+
}
|
|
269
332
|
}
|
|
270
333
|
return readFreememMb(deps);
|
|
271
334
|
}
|
|
335
|
+
/** Available memory in MiB from readMemorySignal; null skips the memory gate. */
|
|
336
|
+
export function readAvailableMemMb(deps = defaultThrottleDeps()) {
|
|
337
|
+
return readMemorySignal(deps)?.available_mb ?? null;
|
|
338
|
+
}
|
|
339
|
+
function formatMb(mb) {
|
|
340
|
+
return `${Math.round(mb)} MB`;
|
|
341
|
+
}
|
|
342
|
+
/** Plain explanation of a memory hold: which signal, its value, and the threshold. */
|
|
343
|
+
export function formatMemoryHoldMessage(signal, thresholdMb) {
|
|
344
|
+
if (signal.source === "memory_pressure") {
|
|
345
|
+
const reading = `memory_pressure reports ${signal.free_percent}% free (about ${formatMb(signal.available_mb)}), pressure level ${signal.pressure_level}`;
|
|
346
|
+
if (signal.pressure_level !== "normal") {
|
|
347
|
+
return `Holding new lanes: ${reading}. Lanes wait while the level is warn or critical (throttle.min_available_mem_mb is ${thresholdMb}).`;
|
|
348
|
+
}
|
|
349
|
+
return `Holding new lanes: ${reading}, below throttle.min_available_mem_mb ${thresholdMb}.`;
|
|
350
|
+
}
|
|
351
|
+
const label = signal.source === "vm_stat"
|
|
352
|
+
? "vm_stat free+inactive+speculative"
|
|
353
|
+
: signal.source === "meminfo"
|
|
354
|
+
? "/proc/meminfo MemAvailable"
|
|
355
|
+
: "os.freemem()";
|
|
356
|
+
return `Holding new lanes: ${label} is ${formatMb(signal.available_mb)}, below throttle.min_available_mem_mb ${thresholdMb}.`;
|
|
357
|
+
}
|
|
272
358
|
export function emptyThrottleState() {
|
|
273
359
|
return {
|
|
274
360
|
consecutiveDeferrals: 0,
|
|
@@ -303,9 +389,17 @@ export function evaluateThrottle(input) {
|
|
|
303
389
|
cpuBlocks = input.busy >= pause;
|
|
304
390
|
}
|
|
305
391
|
}
|
|
392
|
+
// On darwin the signal is memory_pressure's: a warn or critical level holds
|
|
393
|
+
// even above the threshold, and a normal level holds only below it.
|
|
306
394
|
let memBlocks = false;
|
|
395
|
+
let memorySignal;
|
|
307
396
|
if (minMem !== undefined && input.availableMemMb !== null) {
|
|
308
|
-
|
|
397
|
+
memorySignal = input.memory && input.memory.available_mb === input.availableMemMb
|
|
398
|
+
? input.memory
|
|
399
|
+
: { source: "meminfo", available_mb: input.availableMemMb };
|
|
400
|
+
memBlocks = input.availableMemMb < minMem ||
|
|
401
|
+
memorySignal.pressure_level === "warn" ||
|
|
402
|
+
memorySignal.pressure_level === "critical";
|
|
309
403
|
}
|
|
310
404
|
if (!cpuBlocks && !memBlocks) {
|
|
311
405
|
return {
|
|
@@ -323,27 +417,67 @@ export function evaluateThrottle(input) {
|
|
|
323
417
|
admit: false,
|
|
324
418
|
reason: "memory",
|
|
325
419
|
availableMemMb: input.availableMemMb ?? undefined,
|
|
420
|
+
memory_hold: {
|
|
421
|
+
...memorySignal,
|
|
422
|
+
threshold_mb: minMem,
|
|
423
|
+
message: formatMemoryHoldMessage(memorySignal, minMem),
|
|
424
|
+
},
|
|
326
425
|
};
|
|
327
426
|
}
|
|
328
427
|
export function applyThrottleDecision(state, decision, nowMs) {
|
|
428
|
+
const { memory_hold: _previousHold, ...rest } = state;
|
|
329
429
|
if (decision.admit) {
|
|
330
430
|
return {
|
|
331
|
-
...
|
|
431
|
+
...rest,
|
|
332
432
|
consecutiveDeferrals: 0,
|
|
333
433
|
lastAdmissionAt: new Date(nowMs).toISOString(),
|
|
334
434
|
cpuPaused: false,
|
|
335
435
|
};
|
|
336
436
|
}
|
|
337
437
|
return {
|
|
338
|
-
...
|
|
438
|
+
...rest,
|
|
339
439
|
consecutiveDeferrals: state.consecutiveDeferrals + 1,
|
|
340
440
|
lastAdmissionAt: state.lastAdmissionAt,
|
|
341
441
|
cpuPaused: decision.reason === "cpu" ? true : state.cpuPaused,
|
|
442
|
+
...(decision.memory_hold !== undefined ? { memory_hold: decision.memory_hold } : {}),
|
|
342
443
|
};
|
|
343
444
|
}
|
|
344
445
|
export function throttleStatePath(hqRoot) {
|
|
345
446
|
return path.join(hqRoot, "workspace", "lanes-runs", "throttle-state.json");
|
|
346
447
|
}
|
|
448
|
+
const MEMORY_SIGNAL_SOURCES = [
|
|
449
|
+
"memory_pressure",
|
|
450
|
+
"vm_stat",
|
|
451
|
+
"meminfo",
|
|
452
|
+
"freemem",
|
|
453
|
+
];
|
|
454
|
+
/** A stored memory_hold is informational; a malformed one is dropped, not fatal. */
|
|
455
|
+
function parseMemoryHold(value) {
|
|
456
|
+
if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
const hold = value;
|
|
460
|
+
const finiteNonNegative = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
461
|
+
if (!MEMORY_SIGNAL_SOURCES.includes(hold.source) ||
|
|
462
|
+
!finiteNonNegative(hold.available_mb) ||
|
|
463
|
+
!finiteNonNegative(hold.threshold_mb) ||
|
|
464
|
+
typeof hold.message !== "string" || hold.message.trim() === "" ||
|
|
465
|
+
(hold.free_percent !== undefined && !finiteNonNegative(hold.free_percent)) ||
|
|
466
|
+
(hold.pressure_level !== undefined &&
|
|
467
|
+
!["normal", "warn", "critical"].includes(String(hold.pressure_level)))) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
source: hold.source,
|
|
472
|
+
available_mb: hold.available_mb,
|
|
473
|
+
...(hold.free_percent !== undefined ? { free_percent: hold.free_percent } : {}),
|
|
474
|
+
...(hold.pressure_level !== undefined
|
|
475
|
+
? { pressure_level: hold.pressure_level }
|
|
476
|
+
: {}),
|
|
477
|
+
threshold_mb: hold.threshold_mb,
|
|
478
|
+
message: hold.message,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
347
481
|
export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
|
|
348
482
|
if (!deps.existsSync(filePath))
|
|
349
483
|
return "missing";
|
|
@@ -410,10 +544,12 @@ export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
|
|
|
410
544
|
process.stderr.write(`lanes throttle: state ${filePath} has invalid sampled_at\n`);
|
|
411
545
|
return "unreadable";
|
|
412
546
|
}
|
|
547
|
+
const memoryHold = parseMemoryHold(obj.memory_hold);
|
|
413
548
|
return {
|
|
414
549
|
consecutiveDeferrals: obj.consecutiveDeferrals,
|
|
415
550
|
lastAdmissionAt: obj.lastAdmissionAt,
|
|
416
551
|
cpuPaused: obj.cpuPaused,
|
|
552
|
+
...(memoryHold !== undefined ? { memory_hold: memoryHold } : {}),
|
|
417
553
|
...(cpuSnapshot !== undefined ? { cpu_snapshot: cpuSnapshot } : {}),
|
|
418
554
|
...(obj.last_busy !== undefined ? { last_busy: obj.last_busy } : {}),
|
|
419
555
|
...(obj.busy_source !== undefined ? { busy_source: obj.busy_source } : {}),
|
|
@@ -465,11 +601,12 @@ export async function decideAdmission(hqRoot, throttle = {}, deps = defaultThrot
|
|
|
465
601
|
const busy = cpuBusyFraction === undefined
|
|
466
602
|
? observation?.busy ?? null
|
|
467
603
|
: cpuBusyFraction;
|
|
468
|
-
const
|
|
604
|
+
const memory = readMemorySignal(deps);
|
|
469
605
|
const nowMs = deps.now();
|
|
470
606
|
const decision = evaluateThrottle({
|
|
471
607
|
busy,
|
|
472
|
-
availableMemMb,
|
|
608
|
+
availableMemMb: memory?.available_mb ?? null,
|
|
609
|
+
memory,
|
|
473
610
|
cpuCheckEnabled,
|
|
474
611
|
state,
|
|
475
612
|
throttle,
|
|
@@ -181,6 +181,17 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
|
|
|
181
181
|
export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
|
|
182
182
|
export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
183
183
|
export type LaneProvider = (typeof LANE_PROVIDERS)[number];
|
|
184
|
+
/**
|
|
185
|
+
* Who runs a lane's work. A process lane (the default, recorded by absence)
|
|
186
|
+
* is a provider process HQ spawned and watches. A subagent lane is a record
|
|
187
|
+
* only: an in-session Claude subagent does the work and reports through
|
|
188
|
+
* `hq lanes progress`, so admission, watchers, reapers and keep-alive skip it.
|
|
189
|
+
*/
|
|
190
|
+
export declare const LANE_RUNNERS: readonly ["process", "subagent"];
|
|
191
|
+
export type LaneRunner = (typeof LANE_RUNNERS)[number];
|
|
192
|
+
export declare function parseLaneRunner(value: unknown): LaneRunner;
|
|
193
|
+
/** True when an in-session subagent, not an HQ-spawned process, runs this lane. */
|
|
194
|
+
export declare function isSubagentLane(lane: Pick<LaneRecord, "runner">): boolean;
|
|
184
195
|
export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "lead_reserve", "worker_profile_changed", "low_disk", "admit_failed"];
|
|
185
196
|
export type QueuedReason = (typeof QUEUED_REASONS)[number];
|
|
186
197
|
type QueuedDetails = {
|
|
@@ -616,6 +627,8 @@ export interface LaneRecord {
|
|
|
616
627
|
/** How policy files were delivered on the most recent profile spawn. */
|
|
617
628
|
worker_policy_delivery?: "hook" | "prompt";
|
|
618
629
|
provider: LaneProvider;
|
|
630
|
+
/** `subagent` when an in-session subagent runs the work; absent for HQ-spawned process lanes. */
|
|
631
|
+
runner?: "subagent";
|
|
619
632
|
/** Open each Claude turn as a named Remote Control session using account a's login. */
|
|
620
633
|
remote_control?: boolean;
|
|
621
634
|
/** Human-readable Remote Control name, fixed when the lane is created. Older lanes may lack it. */
|
|
@@ -1121,6 +1134,14 @@ export interface LanesConfig {
|
|
|
1121
1134
|
}
|
|
1122
1135
|
export declare function isIsoDateTime(value: string): boolean;
|
|
1123
1136
|
export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
|
|
1124
|
-
export
|
|
1137
|
+
export interface ParseLaneEnvelopeOptions {
|
|
1138
|
+
/**
|
|
1139
|
+
* False for a lane started with --no-story. Such a lane has no board story
|
|
1140
|
+
* for story_status to describe, so a story_complete / story_status mismatch
|
|
1141
|
+
* is repaired from story_complete instead of failing the envelope.
|
|
1142
|
+
*/
|
|
1143
|
+
storyBound?: boolean;
|
|
1144
|
+
}
|
|
1145
|
+
export declare function parseLaneEnvelope(raw: unknown, options?: ParseLaneEnvelopeOptions): LaneEnvelope;
|
|
1125
1146
|
export {};
|
|
1126
1147
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -106,6 +106,23 @@ export const TERMINAL_LANE_STATES = [
|
|
|
106
106
|
"killed",
|
|
107
107
|
];
|
|
108
108
|
export const LANE_PROVIDERS = ["claude", "codex", "grok"];
|
|
109
|
+
/**
|
|
110
|
+
* Who runs a lane's work. A process lane (the default, recorded by absence)
|
|
111
|
+
* is a provider process HQ spawned and watches. A subagent lane is a record
|
|
112
|
+
* only: an in-session Claude subagent does the work and reports through
|
|
113
|
+
* `hq lanes progress`, so admission, watchers, reapers and keep-alive skip it.
|
|
114
|
+
*/
|
|
115
|
+
export const LANE_RUNNERS = ["process", "subagent"];
|
|
116
|
+
export function parseLaneRunner(value) {
|
|
117
|
+
if (typeof value === "string" && LANE_RUNNERS.includes(value)) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
throw new LanesError("unknown_lane_runner", `Unknown lane runner ${JSON.stringify(value)}. Accepted: ${LANE_RUNNERS.join(", ")}`, { accepted: [...LANE_RUNNERS], value });
|
|
121
|
+
}
|
|
122
|
+
/** True when an in-session subagent, not an HQ-spawned process, runs this lane. */
|
|
123
|
+
export function isSubagentLane(lane) {
|
|
124
|
+
return lane.runner === "subagent";
|
|
125
|
+
}
|
|
109
126
|
export const QUEUED_REASONS = [
|
|
110
127
|
"cap",
|
|
111
128
|
"provider_cap",
|
|
@@ -240,7 +257,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
240
257
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
241
258
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
242
259
|
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile", "spawn"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile", "spawn"], keep_alive_next_eligible_at: ["command", "reconcile", "spawn"], keep_alive_exhausted_at: ["command", "reconcile", "spawn"], loop_mode: ["creator"],
|
|
243
|
-
provider: ["creator", "command"], remote_control: ["creator"], remote_control_title: ["creator"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], account_pick: ["creator"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
260
|
+
provider: ["creator", "command"], runner: ["creator"], remote_control: ["creator"], remote_control_title: ["creator"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], account_pick: ["creator"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
244
261
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["command", "watcher", "reconcile"],
|
|
245
262
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
246
263
|
last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
@@ -288,7 +305,8 @@ export function parseMajorSchema(schemaVersion, expectedPrefix, expectedMajor) {
|
|
|
288
305
|
throw new LaneSchemaError(`Unsupported ${expectedPrefix} major version ${major} (found ${schemaVersion})`, { schema_version: schemaVersion, major });
|
|
289
306
|
}
|
|
290
307
|
}
|
|
291
|
-
export function parseLaneEnvelope(raw) {
|
|
308
|
+
export function parseLaneEnvelope(raw, options = {}) {
|
|
309
|
+
const storyBound = options.storyBound !== false;
|
|
292
310
|
if (!isPlainObject(raw)) {
|
|
293
311
|
throw new LaneSchemaError("Lane envelope must be a JSON object");
|
|
294
312
|
}
|
|
@@ -345,7 +363,7 @@ export function parseLaneEnvelope(raw) {
|
|
|
345
363
|
else if (raw.blocked_reason !== undefined) {
|
|
346
364
|
throw new LaneSchemaError('blocked_reason is present exactly when decision is "blocked"');
|
|
347
365
|
}
|
|
348
|
-
|
|
366
|
+
let storyStatus = raw.story_status;
|
|
349
367
|
let storyComplete;
|
|
350
368
|
let storyCompleteReason;
|
|
351
369
|
if (raw.story_complete !== undefined) {
|
|
@@ -358,6 +376,12 @@ export function parseLaneEnvelope(raw) {
|
|
|
358
376
|
raw.story_complete_reason.trim().length === 0) {
|
|
359
377
|
throw new LaneSchemaError("story_complete_reason is required when story_complete is present", { field: "story_complete_reason" });
|
|
360
378
|
}
|
|
379
|
+
if (!storyBound && raw.story_complete === true && storyStatus !== "done") {
|
|
380
|
+
storyStatus = "done";
|
|
381
|
+
}
|
|
382
|
+
else if (!storyBound && raw.story_complete === false && storyStatus === "done") {
|
|
383
|
+
storyStatus = "in_progress";
|
|
384
|
+
}
|
|
361
385
|
if (raw.story_complete === true && storyStatus !== "done") {
|
|
362
386
|
throw new LaneSchemaError("story_complete true requires story_status done", {
|
|
363
387
|
error: "story_complete_status_mismatch",
|