@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
|
+
import { observeLocalHostIdentity } from "./local-host-identity.js";
|
|
3
4
|
export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
4
5
|
export const OUTPOST_DIR = "/etc/outpost";
|
|
5
6
|
export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
|
|
@@ -25,9 +26,15 @@ export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFi
|
|
|
25
26
|
}
|
|
26
27
|
export function resolveAdmissionHost(deps = {}) {
|
|
27
28
|
const identity = deps.readOutpostIdentity?.() ?? readOutpostIdentity(deps.existsSync, deps.readFileSync);
|
|
29
|
+
const hostname = (deps.hostname ?? os.hostname)();
|
|
30
|
+
const observed = observeLocalHostIdentity({
|
|
31
|
+
homedir: deps.homedir?.() ?? os.homedir(),
|
|
32
|
+
hostname,
|
|
33
|
+
});
|
|
28
34
|
return {
|
|
29
|
-
hostname
|
|
35
|
+
hostname,
|
|
30
36
|
outpostId: identity.kind === "id" ? identity.id : null,
|
|
37
|
+
...(observed ? { machineId: observed.id, aliases: observed.aliases } : {}),
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
/** `primary` names the controller registry unless the caller is a different Outpost. */
|
|
@@ -3,6 +3,10 @@ import { type LaneRecord, type LanesConfig } from "./types.js";
|
|
|
3
3
|
export interface AdmissionHost {
|
|
4
4
|
hostname: string;
|
|
5
5
|
outpostId: string | null;
|
|
6
|
+
/** Stable id from ~/.hq/lane-host.json, when this process has read one. */
|
|
7
|
+
machineId?: string;
|
|
8
|
+
/** Hostnames this machine has already used. */
|
|
9
|
+
aliases?: readonly string[];
|
|
6
10
|
}
|
|
7
11
|
export type AdmissionCapReason = "provider_cap" | "account_cap" | "profile_cap";
|
|
8
12
|
export declare const ADMISSION_LOCK_STALE_MS = 30000;
|
|
@@ -2,8 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { accountMaxRunning } from "./config.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
+
import { localHostIdsReferToSameMachine } from "./local-host-identity.js";
|
|
5
6
|
import { quotaBlockFor } from "./quota.js";
|
|
6
|
-
import { isTerminalLaneState } from "./types.js";
|
|
7
|
+
import { isSubagentLane, isTerminalLaneState } from "./types.js";
|
|
7
8
|
export const ADMISSION_LOCK_STALE_MS = 30_000;
|
|
8
9
|
const ADMISSION_LOCK_TIMEOUT_MS = 5_000;
|
|
9
10
|
const ADMISSION_RESERVATION_GRACE_MS = 30_000;
|
|
@@ -117,7 +118,7 @@ export function laneRunsOnHost(lane, host) {
|
|
|
117
118
|
if (host.outpostId && lane.host.kind === "outpost") {
|
|
118
119
|
return lane.host.id === host.outpostId;
|
|
119
120
|
}
|
|
120
|
-
return lane.host.kind === "local" && lane.host.id
|
|
121
|
+
return lane.host.kind === "local" && localHostIdsReferToSameMachine(lane.host.id, host);
|
|
121
122
|
}
|
|
122
123
|
export function compareLaneQueueOrder(a, b) {
|
|
123
124
|
if (b.priority !== a.priority)
|
|
@@ -188,6 +189,9 @@ export function laneOccupiesRunningSlot(lane, nowMs = Date.now()) {
|
|
|
188
189
|
// count as a second owner of the slot that blocked it.
|
|
189
190
|
if (lane.state === "queued" && lane.queued_reason === "account_cap")
|
|
190
191
|
return false;
|
|
192
|
+
// A subagent lane runs inside its senior session, not in an HQ provider slot.
|
|
193
|
+
if (isSubagentLane(lane))
|
|
194
|
+
return false;
|
|
191
195
|
return lane.state === "running" ||
|
|
192
196
|
hasActiveAdmissionReservation(lane, nowMs) ||
|
|
193
197
|
hasActivePendingResume(lane, nowMs);
|
|
@@ -17,6 +17,11 @@ export interface EnvelopeRepairDispatchDeps {
|
|
|
17
17
|
env?: NodeJS.ProcessEnv;
|
|
18
18
|
spawnSync?: (command: string, args: string[], options: EnvelopeRepairDispatchOptions) => EnvelopeRepairDispatchResult;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* True for the detached lane runner scripts. They are not the hq CLI: running
|
|
22
|
+
* one with `lanes ...` argv starts that runner for a lane named "lanes".
|
|
23
|
+
*/
|
|
24
|
+
export declare function isLaneRunnerEntry(entry: string): boolean;
|
|
20
25
|
/**
|
|
21
26
|
* Dispatch repair into the currently installed CLI. A watcher can stay alive
|
|
22
27
|
* across upgrades, so repair must not call its in-memory spawn implementation.
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { spawnSync as nodeSpawnSync } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
+
const LANE_RUNNER_ENTRY_NAMES = new Set(["watcher-main", "worker-process", "remote-control-lane"]);
|
|
5
|
+
/**
|
|
6
|
+
* True for the detached lane runner scripts. They are not the hq CLI: running
|
|
7
|
+
* one with `lanes ...` argv starts that runner for a lane named "lanes".
|
|
8
|
+
*/
|
|
9
|
+
export function isLaneRunnerEntry(entry) {
|
|
10
|
+
return LANE_RUNNER_ENTRY_NAMES.has(path.basename(entry).replace(/\.[cm]?[jt]s$/, ""));
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* Dispatch repair into the currently installed CLI. A watcher can stay alive
|
|
6
14
|
* across upgrades, so repair must not call its in-memory spawn implementation.
|
|
@@ -28,7 +36,10 @@ export function spawnEnvelopeRepairViaCurrentCli(hqRoot, lane, input, deps = {})
|
|
|
28
36
|
});
|
|
29
37
|
const env = deps.env ?? process.env;
|
|
30
38
|
const cliEntry = env.HQ_LANE_CLI_ENTRY?.trim();
|
|
31
|
-
if (!cliEntry ||
|
|
39
|
+
if (!cliEntry ||
|
|
40
|
+
!path.isAbsolute(cliEntry) ||
|
|
41
|
+
path.resolve(cliEntry) !== cliEntry ||
|
|
42
|
+
isLaneRunnerEntry(cliEntry)) {
|
|
32
43
|
throw new LanesError("repair_cli_entry_invalid", "Lane watcher has no valid absolute hq CLI entrypoint for envelope repair", { lane_id: lane.lane_id });
|
|
33
44
|
}
|
|
34
45
|
const result = run(process.execPath, [cliEntry, "lanes", "_spawn-envelope-repair"], {
|
|
@@ -36,8 +36,8 @@ export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQu
|
|
|
36
36
|
export type { EnvelopeRepairDecision, LatestEnvelopeFile } from "./watcher.js";
|
|
37
37
|
export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
|
|
38
38
|
export type { CreateLaneMeshOptions, LaneMesh, LaneMeshEvent, WireEventKind, WireThreadEvent, } from "./mesh-events.js";
|
|
39
|
-
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
40
|
-
export type { CpuTimes, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
|
|
39
|
+
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, MEMORY_PRESSURE_SYSCTLS, SYSCTL_BIN, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, formatMemoryHoldMessage, isCpuCheckEnabled, parseMemAvailableKb, parseMemoryPressureSysctl, parseVmStatAvailableMb, readAvailableMemMb, readMemorySignal, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
40
|
+
export type { CpuTimes, MemoryHold, MemoryPressureLevel, MemorySignal, MemorySignalSource, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
|
|
41
41
|
export { effectiveAccountHome, } from "./account-home.js";
|
|
42
42
|
export type { EffectiveAccountHome, } from "./account-home.js";
|
|
43
43
|
export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { PID_LIVENESS_DECISIONS, PROVIDER_COMMAND, PROVIDER_MODEL_FLAG, SPAWN_TE
|
|
|
20
20
|
export { SILENCE_MS, WATCHER_DECISION_EVENTS, WATCHER_OUT_MAX_BYTES, WATCHER_POLL_MS, WATCHER_SIGNAL_KINDS, appendCappedLog, appendWatcherSignal, applyTerminalToLane, classifyLaneExit, clearLaneStalled, isPidAlive, laneIdentityFromRecord, markLaneStalled, notifySlotFreed, parseWatcherSignalKind, readLatestEnvelope, runWatcherLoop, stalledReason, tickWatcher, watcherEventsPath, watcherLastSignalPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
21
21
|
export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQuestionIds, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
|
|
22
22
|
export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
|
|
23
|
-
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
23
|
+
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, MEMORY_PRESSURE_SYSCTLS, SYSCTL_BIN, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, formatMemoryHoldMessage, isCpuCheckEnabled, parseMemAvailableKb, parseMemoryPressureSysctl, parseVmStatAvailableMb, readAvailableMemMb, readMemorySignal, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
export { effectiveAccountHome, } from "./account-home.js";
|
|
25
25
|
export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
|
|
26
26
|
export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
4
|
import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
5
5
|
import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
|
|
6
|
-
import { isTerminalLaneState, isIsoDateTime, } from "./types.js";
|
|
6
|
+
import { isSubagentLane, isTerminalLaneState, isIsoDateTime, } from "./types.js";
|
|
7
7
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
8
8
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
9
9
|
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -24,6 +24,9 @@ export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
|
24
24
|
const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
25
25
|
/** Shared resume transition for terminal gate follow-ups. */
|
|
26
26
|
export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
27
|
+
// HQ holds no provider session for a subagent lane, so it can never resume one.
|
|
28
|
+
if (isSubagentLane(current))
|
|
29
|
+
return { lane: current, resumeQueued: false, resumeSkippedReason: "no_provider_session" };
|
|
27
30
|
if (options.mergeExisting && current.pending_resume !== undefined) {
|
|
28
31
|
return {
|
|
29
32
|
lane: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const LANE_HOST_SCHEMA = "hq-lane-host.v1";
|
|
2
|
+
export declare const LANE_HOST_FILENAME = "lane-host.json";
|
|
3
|
+
export interface LocalHostIdentity {
|
|
4
|
+
schema_version: typeof LANE_HOST_SCHEMA;
|
|
5
|
+
id: string;
|
|
6
|
+
aliases: string[];
|
|
7
|
+
/** Hardware UUID, `/etc/machine-id`, or Windows MachineGuid. Absent on older files. */
|
|
8
|
+
hardware_id?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LocalHostMatch {
|
|
11
|
+
hostname: string;
|
|
12
|
+
machineId?: string;
|
|
13
|
+
aliases?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function laneHostIdentityPath(homedir: string): string;
|
|
16
|
+
/** True when `recordedId` is this machine's stable id, current hostname, or a known earlier hostname. */
|
|
17
|
+
export declare function localHostIdsReferToSameMachine(recordedId: string, host: LocalHostMatch): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Read the stable id, creating it once. A lost race re-reads the winner
|
|
20
|
+
* instead of minting a second id that would strand the lanes just created.
|
|
21
|
+
* A stored hardware id that does not match this machine mints a new id so a
|
|
22
|
+
* cloned home directory does not keep the source machine's id.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveLocalHostIdentity(input: {
|
|
25
|
+
homedir: string;
|
|
26
|
+
hostname: string;
|
|
27
|
+
createId?: () => string;
|
|
28
|
+
readHardwareId?: () => string | null;
|
|
29
|
+
}): LocalHostIdentity;
|
|
30
|
+
/**
|
|
31
|
+
* Read an existing identity. Does not create the file, record a hostname,
|
|
32
|
+
* or replace an id. A hardware mismatch returns null so a cloned id is not
|
|
33
|
+
* treated as this machine.
|
|
34
|
+
*/
|
|
35
|
+
export declare function observeLocalHostIdentity(input: {
|
|
36
|
+
homedir: string;
|
|
37
|
+
hostname: string;
|
|
38
|
+
readHardwareId?: () => string | null;
|
|
39
|
+
}): LocalHostIdentity | null;
|
|
40
|
+
//# sourceMappingURL=local-host-identity.d.ts.map
|
|
@@ -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 {
|