@indigoai-us/hq-cli 5.345.2 → 5.345.4
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 +9 -0
- package/dist/commands/lanes-message.js +0 -3
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes.js +1 -4
- package/dist/lib/daemon/hq-cloud.d.ts +5 -0
- package/dist/lib/daemon/hq-cloud.js +10 -0
- package/dist/lib/daemon/run.d.ts +4 -1
- package/dist/lib/daemon/run.js +18 -7
- package/dist/lib/install/claude-targets.js +1 -1
- package/dist/lib/lanes/disk-guard.d.ts +3 -4
- package/dist/lib/lanes/disk-guard.js +7 -6
- package/dist/lib/lanes/reconcile.js +22 -28
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +21 -29
- package/dist/lib/lanes/watcher.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.4] — 2026-10-05
|
|
6
|
+
- Run a pinned or newer managed hq-cloud sync runner ahead of the staged sync bundle, so daemon sync no longer stays on the hq-cloud version bundled with the CLI after an update or pin (hq-cli#1547).
|
|
7
|
+
- Avoid dispatching the global hook when the working directory is inside an HQ root.
|
|
8
|
+
|
|
9
|
+
## [5.345.3] — 2026-10-05
|
|
10
|
+
- Retry a blocker freshness lane-inbox fallback after a transient append failure.
|
|
11
|
+
- Scope bundled-script stdin checks to changes in their workflow, build inputs, CLI sources, tests, or shell assets.
|
|
12
|
+
- Always apply the lane low-disk preflight before starting or resuming workers.
|
|
13
|
+
|
|
5
14
|
## [5.345.2] — 2026-10-05
|
|
6
15
|
- Build and version-check the hq-cloud sync-runner bundle, then validate and atomically stage it with the daemon child bootstrap before releasing the CLI package-use lease.
|
|
7
16
|
- Defer MCP tool and prompt handler imports until a request needs them so `hq mcp serve` can answer initialize without waiting on the full handler modules.
|
|
@@ -13,7 +13,6 @@ import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice
|
|
|
13
13
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
14
14
|
import { keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
15
15
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
16
|
-
import { lowDiskGuardEnabledForLane } from "../lib/lanes/disk-guard.js";
|
|
17
16
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
18
17
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
19
18
|
export function parseMonitorLaneMessagePayload(input) {
|
|
@@ -782,7 +781,6 @@ export function createLaneMessageHandlers(deps) {
|
|
|
782
781
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
783
782
|
if (admission.action === "refuse" && admission.error)
|
|
784
783
|
throw admission.error;
|
|
785
|
-
const guardEnabled = admission.action === "queue" ? false : await lowDiskGuardEnabledForLane(hqRoot, lane);
|
|
786
784
|
return withResumeHireAdmission(ctx, lane, () => {
|
|
787
785
|
if (admission.action === "queue") {
|
|
788
786
|
const sessionId = lane.provider_session_id;
|
|
@@ -826,7 +824,6 @@ export function createLaneMessageHandlers(deps) {
|
|
|
826
824
|
return queued;
|
|
827
825
|
}
|
|
828
826
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
829
|
-
lowDiskGuardEnabled: guardEnabled,
|
|
830
827
|
env: ctx.env,
|
|
831
828
|
modelDeps: {
|
|
832
829
|
readFileSync: ctx.readFileSync,
|
|
@@ -7,7 +7,6 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
|
7
7
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
8
8
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
9
9
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
|
-
import { lowDiskGuardEnabledForLane } from "../lib/lanes/disk-guard.js";
|
|
11
10
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
12
11
|
export function createLaneResumeHandlers(deps) {
|
|
13
12
|
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, automaticBalanceMoveHistoryReason, authorizeFailedResume, authorizeResumeProfileRepin, casUpdateLane, dispatchOutpostLaneCommand, laneIsHostedHere, laneWithRecoveredProviderSession, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
@@ -448,7 +447,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
448
447
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
449
448
|
if (admission.action === "refuse" && admission.error)
|
|
450
449
|
throw admission.error;
|
|
451
|
-
const guardEnabled = admission.action === "queue" ? false : await lowDiskGuardEnabledForLane(hqRoot, lane);
|
|
452
450
|
return withResumeHireAdmission(ctx, lane, () => {
|
|
453
451
|
if (admission.action === "queue") {
|
|
454
452
|
const sessionId = lane.provider_session_id;
|
|
@@ -492,7 +490,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
492
490
|
});
|
|
493
491
|
}
|
|
494
492
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
495
|
-
lowDiskGuardEnabled: guardEnabled,
|
|
496
493
|
env: ctx.env,
|
|
497
494
|
modelDeps: {
|
|
498
495
|
readFileSync: ctx.readFileSync,
|
package/dist/commands/lanes.js
CHANGED
|
@@ -37,7 +37,7 @@ import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
|
37
37
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
38
38
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
39
39
|
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
40
|
-
import { configuredCodexRolloutHomes,
|
|
40
|
+
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
41
41
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
42
42
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
43
43
|
import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
|
|
@@ -3735,7 +3735,6 @@ async function runCreate(ctx, opts) {
|
|
|
3735
3735
|
let started = lane;
|
|
3736
3736
|
if (admission.action === "start") {
|
|
3737
3737
|
started = spawnLaneWorker(hqRoot, lane, {
|
|
3738
|
-
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(hqRoot, lane),
|
|
3739
3738
|
env: ctx.env,
|
|
3740
3739
|
modelDeps: {
|
|
3741
3740
|
readFileSync: ctx.readFileSync,
|
|
@@ -4877,7 +4876,6 @@ async function runPickup(ctx, opts) {
|
|
|
4877
4876
|
}
|
|
4878
4877
|
try {
|
|
4879
4878
|
return spawnLaneWorker(hqRoot, current, {
|
|
4880
|
-
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(hqRoot, current),
|
|
4881
4879
|
env: ctx.env,
|
|
4882
4880
|
modelDeps: {
|
|
4883
4881
|
readFileSync: ctx.readFileSync,
|
|
@@ -6252,7 +6250,6 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
6252
6250
|
return;
|
|
6253
6251
|
}
|
|
6254
6252
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
6255
|
-
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(hqRoot, lane),
|
|
6256
6253
|
env: ctx.env,
|
|
6257
6254
|
modelDeps: {
|
|
6258
6255
|
readFileSync: ctx.readFileSync,
|
|
@@ -23,6 +23,11 @@ export type ManagedHqCloud = PickedHqCloud & {
|
|
|
23
23
|
export declare function managedHqCloudDir(daemonRoot: string): string;
|
|
24
24
|
/** The package folder inside the updater's install prefix. */
|
|
25
25
|
export declare function managedHqCloudRoot(daemonRoot: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* True only when both versions are valid semver and `a` is older than `b`.
|
|
28
|
+
* Prereleases count, so 6.18.33-alpha.1 is older than 6.18.33.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hqCloudVersionOlder(a: string, b: string): boolean;
|
|
26
31
|
/**
|
|
27
32
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
28
33
|
* that fails it (an interrupted or damaged install) is passed over.
|
|
@@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { lt as semverLt, valid as validSemver } from "semver";
|
|
13
14
|
import { readDaemonConfig } from "./config.js";
|
|
14
15
|
import { buildSpawnPlan } from "../../utils/windows-spawn.js";
|
|
15
16
|
export const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
|
|
@@ -31,6 +32,15 @@ function newer(a, b) {
|
|
|
31
32
|
}
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* True only when both versions are valid semver and `a` is older than `b`.
|
|
37
|
+
* Prereleases count, so 6.18.33-alpha.1 is older than 6.18.33.
|
|
38
|
+
*/
|
|
39
|
+
export function hqCloudVersionOlder(a, b) {
|
|
40
|
+
const va = validSemver(a.trim());
|
|
41
|
+
const vb = validSemver(b.trim());
|
|
42
|
+
return !!va && !!vb && semverLt(va, vb);
|
|
43
|
+
}
|
|
34
44
|
/**
|
|
35
45
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
36
46
|
* that fails it (an interrupted or damaged install) is passed over.
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -59,7 +59,10 @@ export type SyncRunnerSource = {
|
|
|
59
59
|
kind: "unavailable";
|
|
60
60
|
runnerPath: null;
|
|
61
61
|
};
|
|
62
|
-
export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null
|
|
62
|
+
export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null, staged?: {
|
|
63
|
+
version: string | null;
|
|
64
|
+
pinnedVersion?: string | null;
|
|
65
|
+
}): SyncRunnerSource;
|
|
63
66
|
/** Whether every executable in the first service plan remains usable after the CLI package is replaced. */
|
|
64
67
|
export declare function daemonPlanCanReleasePackageUseLease(plans: readonly UnitPlan[], replaceablePackageRoots: readonly string[], stagingFailed: boolean, hqBootstrapReady?: boolean): boolean;
|
|
65
68
|
export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -37,7 +37,7 @@ import { startHqdServer } from "./server.js";
|
|
|
37
37
|
import { heartbeatFile, startHeartbeat } from "./heartbeat.js";
|
|
38
38
|
import { createProtocolTail, ensureProtocolFifo, openProtocolWriter } from "./protocol-tail.js";
|
|
39
39
|
import { takeControlRequests } from "./requests.js";
|
|
40
|
-
import { currentHqCloud, currentHqCloudAfterLeaseRelease, hqCloudRunners } from "./hq-cloud.js";
|
|
40
|
+
import { currentHqCloud, currentHqCloudAfterLeaseRelease, hqCloudRunners, hqCloudVersionOlder } from "./hq-cloud.js";
|
|
41
41
|
import { stageSyncRunnerBundle } from "./sync-runner-bundle.js";
|
|
42
42
|
import { stageHqChildBootstrap } from "./hq-child-bootstrap-stage.js";
|
|
43
43
|
import { runnerSessionName } from "./remote-control.js";
|
|
@@ -76,7 +76,21 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
|
76
76
|
const LEGACY_PROBE_TTL_MS = 60_000;
|
|
77
77
|
/** A flag read that has not answered by then counts as off. */
|
|
78
78
|
const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
|
|
79
|
-
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null) {
|
|
79
|
+
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }) {
|
|
80
|
+
// A managed copy lives in the daemon directory, so it survives a CLI
|
|
81
|
+
// replacement. Before lease release pickHqCloud returns it only when it is
|
|
82
|
+
// pinned or newer than the bundled copy; after release it is the only
|
|
83
|
+
// eligible package, so it must not displace a newer staged bundle unless it
|
|
84
|
+
// is the pinned version. The staged bundle is built from the bundled copy and
|
|
85
|
+
// otherwise only stands in for that copy (hq-cli#1547).
|
|
86
|
+
const managedYieldsToStaged = stagedRunnerPath !== null &&
|
|
87
|
+
hqCloud?.source === "managed" &&
|
|
88
|
+
hqCloud.version !== staged.pinnedVersion &&
|
|
89
|
+
staged.version !== null &&
|
|
90
|
+
hqCloudVersionOlder(hqCloud.version, staged.version);
|
|
91
|
+
if (hqCloud?.source === "managed" && !managedYieldsToStaged) {
|
|
92
|
+
return { kind: "managed-full-package", runnerPath: path.join(hqCloud.root, "dist", "bin", "sync-runner.js") };
|
|
93
|
+
}
|
|
80
94
|
if (stagedRunnerPath)
|
|
81
95
|
return { kind: "staged-sync", runnerPath: stagedRunnerPath };
|
|
82
96
|
if (!hqCloud) {
|
|
@@ -86,10 +100,7 @@ export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundled
|
|
|
86
100
|
}
|
|
87
101
|
// The caller has already selected a usable full package; preserve the
|
|
88
102
|
// conventional path here, including in tests with virtual package roots.
|
|
89
|
-
|
|
90
|
-
return hqCloud.source === "managed"
|
|
91
|
-
? { kind: "managed-full-package", runnerPath }
|
|
92
|
-
: { kind: "bundled-full-package", runnerPath };
|
|
103
|
+
return { kind: "bundled-full-package", runnerPath: path.join(hqCloud.root, "dist", "bin", "sync-runner.js") };
|
|
93
104
|
}
|
|
94
105
|
function isPathWithin(root, candidate) {
|
|
95
106
|
if (!root || !path.isAbsolute(candidate))
|
|
@@ -559,7 +570,7 @@ export async function runDaemon(opts) {
|
|
|
559
570
|
legacySyncRunnerPath = resolveSyncRunnerPath();
|
|
560
571
|
legacySyncRunnerPathChecked = true;
|
|
561
572
|
}
|
|
562
|
-
const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath);
|
|
573
|
+
const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath, { version: staged?.metadata.version ?? null, pinnedVersion: config.hqCloudVersion ?? null });
|
|
563
574
|
if (!syncSource.runnerPath && !warnedMissingSyncRunner) {
|
|
564
575
|
warnedMissingSyncRunner = true;
|
|
565
576
|
log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
|
|
@@ -49,7 +49,7 @@ export function hqSkillsSource(hqRoot) {
|
|
|
49
49
|
}
|
|
50
50
|
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
51
51
|
export function hookCommand(hqRoot, event, target = 'master') {
|
|
52
|
-
const skipInsideRoot = `if [ -d "\${CLAUDE_PROJECT_DIR:-}" ] && [ "\${CLAUDE_PROJECT_DIR}" -ef ${shellQuote(hqRoot)} ]; then exit 0; fi; `;
|
|
52
|
+
const skipInsideRoot = `if [ -d "\${CLAUDE_PROJECT_DIR:-}" ] && [ "\${CLAUDE_PROJECT_DIR}" -ef ${shellQuote(hqRoot)} ]; then exit 0; fi; cwd=$(pwd -P 2>/dev/null || pwd); while [ -n "$cwd" ]; do if [ -f "$cwd/core/core.yaml" ] && [ -f "$cwd/companies/manifest.yaml" ]; then exit 0; fi; [ "$cwd" = / ] && break; cwd=\${cwd%/*}; [ -n "$cwd" ] || cwd=/; done; `;
|
|
53
53
|
if (target === 'shim')
|
|
54
54
|
return `${skipInsideRoot}exec /bin/sh ${shellQuote(shimPath(hqRoot))} ${shellQuote(event)} --runtime claude`;
|
|
55
55
|
return `${skipInsideRoot}BASH_ENV=/dev/null exec bash ${shellQuote(masterHookPath(hqRoot))} ${shellQuote(event)}`;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import type { LaneRecord, LanesConfig } from "./types.js";
|
|
4
|
-
export declare const LOW_DISK_GUARD_FLAG = "lanes.low-disk-guard";
|
|
5
|
-
export declare function lowDiskGuardEnabledForLane(hqRoot: string, lane: LaneRecord, readFlag?: (key: string, options: ReadLanesFlagOptions) => Promise<boolean>): Promise<boolean>;
|
|
2
|
+
import type { LaneProvider, LanesConfig } from "./types.js";
|
|
6
3
|
interface DiskStats {
|
|
7
4
|
bsize: number;
|
|
8
5
|
blocks: number;
|
|
9
6
|
bavail: number;
|
|
10
7
|
}
|
|
11
8
|
export type StatFs = (target: string) => DiskStats;
|
|
9
|
+
/** Resolve the enabled provider home a lane can use for its disk preflight. */
|
|
10
|
+
export declare function laneDiskAccount(config: LanesConfig, provider: LaneProvider, preferredAccountId?: string): import("./types.js").AccountConfig | undefined;
|
|
12
11
|
export interface DiskSpaceMeasurement {
|
|
13
12
|
path: string;
|
|
14
13
|
free_bytes: number;
|
|
@@ -3,17 +3,18 @@ import * as path from "node:path";
|
|
|
3
3
|
import { createGzip } from "node:zlib";
|
|
4
4
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
7
|
-
import { resolveLaneCompany } from "./lane-company.js";
|
|
8
6
|
import { effectiveAccountHome } from "./account-home.js";
|
|
9
7
|
import { lanesConfigPath } from "./config.js";
|
|
10
|
-
export const LOW_DISK_GUARD_FLAG = "lanes.low-disk-guard";
|
|
11
8
|
const DEFAULT_LOW_DISK_FREE_BYTES = 5 * 1024 ** 3;
|
|
12
9
|
const DEFAULT_LOW_DISK_FRACTION = 0.02;
|
|
13
10
|
const EXCLUDED_SESSION_FILES = new Set(["auth.json", ".credentials.json", "thread_history.sqlite"]);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/** Resolve the enabled provider home a lane can use for its disk preflight. */
|
|
12
|
+
export function laneDiskAccount(config, provider, preferredAccountId) {
|
|
13
|
+
const accounts = config.providers[provider].accounts;
|
|
14
|
+
const preferred = preferredAccountId ? accounts[preferredAccountId] : undefined;
|
|
15
|
+
if (preferred?.enabled)
|
|
16
|
+
return preferred;
|
|
17
|
+
return Object.values(accounts).find((account) => account.enabled);
|
|
17
18
|
}
|
|
18
19
|
export function measureDiskSpace(target, statfs = (p) => fs.statfsSync(p), minFreeBytes = DEFAULT_LOW_DISK_FREE_BYTES) {
|
|
19
20
|
const stats = statfs(target);
|
|
@@ -23,7 +23,7 @@ import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionReco
|
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
25
25
|
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LEAD_ADMISSION_PRIORITY_FLAG } from "./flag-keys.js";
|
|
26
|
-
import { checkLaneDiskSpace, configuredCodexRolloutHomes,
|
|
26
|
+
import { checkLaneDiskSpace, configuredCodexRolloutHomes, laneDiskAccount, retainCodexRollouts } from "./disk-guard.js";
|
|
27
27
|
import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
28
28
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
29
29
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
@@ -1452,31 +1452,27 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1452
1452
|
repairCandidate &&
|
|
1453
1453
|
repairCandidate.envelope === null &&
|
|
1454
1454
|
(!repairCandidate.missing || envelopeRepairMode(classified.provider) === "resume")) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}));
|
|
1477
|
-
summary.skipped.push({ kind: "lane", id: classified.lane_id, why: `low_disk: hq=${free.hq.free_bytes} provider=${free.provider.free_bytes}` });
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1455
|
+
const repairConfig = loadLanesConfig(hqRoot);
|
|
1456
|
+
const repairAccount = laneDiskAccount(repairConfig, classified.provider, classified.account);
|
|
1457
|
+
if (!repairAccount) {
|
|
1458
|
+
throw new LanesError("low_disk_account_unavailable", `Cannot determine provider home for envelope repair of lane ${classified.lane_id}`, { lane_id: classified.lane_id });
|
|
1459
|
+
}
|
|
1460
|
+
const providerHome = effectiveAccountHome(repairAccount, deps.host.hostname).home;
|
|
1461
|
+
const free = checkLaneDiskSpace(hqRoot, providerHome, deps.spawnLaneDeps?.statfs, repairConfig.low_disk_min_free_bytes);
|
|
1462
|
+
if (free.low) {
|
|
1463
|
+
const measuredAt = now.toISOString();
|
|
1464
|
+
updateLane(hqRoot, classified.lane_id, classified.version, "reconcile", (current) => ({
|
|
1465
|
+
...current,
|
|
1466
|
+
state: "queued",
|
|
1467
|
+
queued_reason: "low_disk",
|
|
1468
|
+
queued_details: { reason: "low_disk", hq_free_bytes: free.hq.free_bytes, provider_free_bytes: free.provider.free_bytes, measured_at: measuredAt },
|
|
1469
|
+
queued_at: current.queued_at ?? measuredAt,
|
|
1470
|
+
admission_reserved_at: undefined,
|
|
1471
|
+
terminal_reason: undefined,
|
|
1472
|
+
terminal_details: undefined,
|
|
1473
|
+
}));
|
|
1474
|
+
summary.skipped.push({ kind: "lane", id: classified.lane_id, why: `low_disk: hq=${free.hq.free_bytes} provider=${free.provider.free_bytes}` });
|
|
1475
|
+
continue;
|
|
1480
1476
|
}
|
|
1481
1477
|
const spawnRepair = deps.spawnRepair ??
|
|
1482
1478
|
((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, spawnDepsForHost(deps)));
|
|
@@ -1848,7 +1844,6 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1848
1844
|
const spawnFn = deps.spawn ??
|
|
1849
1845
|
(async (root, lane) => spawnLaneWorker(root, lane, {
|
|
1850
1846
|
...spawnDepsForHost(deps),
|
|
1851
|
-
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(root, lane, deps.readRegistryFlag),
|
|
1852
1847
|
}));
|
|
1853
1848
|
const running = lanes.filter((lane) => (lane.state === "running" ||
|
|
1854
1849
|
hasFreshAdmissionReservation(lane, now.getTime()) ||
|
|
@@ -3292,7 +3287,6 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
3292
3287
|
}
|
|
3293
3288
|
const spawnFn = deps.spawn ?? (async (root, lane) => spawnLaneWorker(root, lane, {
|
|
3294
3289
|
...spawnDepsForHost(deps),
|
|
3295
|
-
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(root, lane, deps.readRegistryFlag),
|
|
3296
3290
|
}));
|
|
3297
3291
|
const nowMs = deps.now().getTime();
|
|
3298
3292
|
const fallbackAnswerAt = new Date(nowMs).toISOString();
|
|
@@ -75,8 +75,6 @@ export interface SpawnLaneDeps {
|
|
|
75
75
|
hostname?: string;
|
|
76
76
|
/** The caller's host scope for admission checks; reconcile supplies its resolved host. */
|
|
77
77
|
admissionHost?: AdmissionHost;
|
|
78
|
-
/** Resolved per-company by the async command/reconcile boundary. */
|
|
79
|
-
lowDiskGuardEnabled?: boolean;
|
|
80
78
|
/** Injected statfs boundary; production uses node:fs.statfsSync. */
|
|
81
79
|
statfs?: StatFs;
|
|
82
80
|
/** Provider model catalog dependencies, shared with create in tests. */
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -36,7 +36,7 @@ import { MESSAGE_REPLAY_STATE_NAME } from "./message-replay.js";
|
|
|
36
36
|
import { markAnsweredQuestionsDelivered } from "./questions.js";
|
|
37
37
|
import { hasEarlierEligibleQueuedLane, laneAdmissionCapReason, withLaneAdmissionLock, } from "./admission.js";
|
|
38
38
|
import { resolveAdmissionHost } from "./admission-host.js";
|
|
39
|
-
import { checkLaneDiskSpace,
|
|
39
|
+
import { checkLaneDiskSpace, laneDiskAccount } from "./disk-guard.js";
|
|
40
40
|
import { LANE_ENVELOPE_JSON_SCHEMA, ENVELOPE_INVALID_REASON, parseLaneProvider, } from "./types.js";
|
|
41
41
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
42
42
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
@@ -606,7 +606,6 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
606
606
|
return moved;
|
|
607
607
|
return spawnLaneWorker(hqRoot, moved, {
|
|
608
608
|
...deps,
|
|
609
|
-
lowDiskGuardEnabled: deps.lowDiskGuardEnabled ?? await lowDiskGuardEnabledForLane(hqRoot, moved),
|
|
610
609
|
});
|
|
611
610
|
});
|
|
612
611
|
}
|
|
@@ -1840,33 +1839,26 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1840
1839
|
probe: modelDeps.probe ?? defaultProbe,
|
|
1841
1840
|
env,
|
|
1842
1841
|
};
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
},
|
|
1864
|
-
queued_at: current.queued_at ?? measuredAt,
|
|
1865
|
-
admission_reserved_at: undefined,
|
|
1866
|
-
terminal_reason: undefined,
|
|
1867
|
-
terminal_details: undefined,
|
|
1868
|
-
}));
|
|
1869
|
-
}
|
|
1842
|
+
const diskAccount = laneDiskAccount(config, existing.provider, existing.account);
|
|
1843
|
+
if (diskAccount !== undefined) {
|
|
1844
|
+
const free = checkLaneDiskSpace(hqRoot, effectiveAccountHome(diskAccount, hostname).home, deps.statfs, config.low_disk_min_free_bytes);
|
|
1845
|
+
if (free.low) {
|
|
1846
|
+
const measuredAt = (deps.now ?? (() => new Date()))().toISOString();
|
|
1847
|
+
return retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => ({
|
|
1848
|
+
...current,
|
|
1849
|
+
state: "queued",
|
|
1850
|
+
queued_reason: "low_disk",
|
|
1851
|
+
queued_details: {
|
|
1852
|
+
reason: "low_disk",
|
|
1853
|
+
hq_free_bytes: free.hq.free_bytes,
|
|
1854
|
+
provider_free_bytes: free.provider.free_bytes,
|
|
1855
|
+
measured_at: measuredAt,
|
|
1856
|
+
},
|
|
1857
|
+
queued_at: current.queued_at ?? measuredAt,
|
|
1858
|
+
admission_reserved_at: undefined,
|
|
1859
|
+
terminal_reason: undefined,
|
|
1860
|
+
terminal_details: undefined,
|
|
1861
|
+
}));
|
|
1870
1862
|
}
|
|
1871
1863
|
}
|
|
1872
1864
|
let selection;
|
|
@@ -863,6 +863,12 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
863
863
|
retryUndeliverable = true;
|
|
864
864
|
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
865
865
|
}
|
|
866
|
+
else if (failedFallback !== undefined) {
|
|
867
|
+
// If the senior inbox is still unavailable, retry the lane inbox fallback.
|
|
868
|
+
deliverFallback = true;
|
|
869
|
+
undeliverableReason = failedFallback.reason ?? prior?.undeliverable_reason ?? "The session senior inbox is unavailable";
|
|
870
|
+
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
871
|
+
}
|
|
866
872
|
else if (prior !== undefined && blockerFreshnessFingerprint(prior.stale_keys.map((key, index) => ({ key, label: prior.stale_labels[index] ?? "" }))) !== fingerprint) {
|
|
867
873
|
escalatedFingerprint = undefined;
|
|
868
874
|
}
|