@indigoai-us/hq-cli 5.345.130 → 5.345.132
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 +7 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +2 -1
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +2 -1
- package/assets/scaffold/core/scripts/session-journal.sh +1 -1
- package/dist/commands/capacity.d.ts +1 -2
- package/dist/commands/capacity.js +7 -9
- package/dist/commands/lanes-loop.d.ts +10 -0
- package/dist/commands/lanes-loop.js +33 -2
- package/dist/commands/lanes-resume.js +15 -3
- package/dist/commands/lanes-shared.d.ts +7 -0
- package/dist/commands/lanes.js +12 -6
- package/dist/lib/capacity/freshness.d.ts +17 -0
- package/dist/lib/capacity/freshness.js +16 -0
- package/dist/lib/capacity/route.d.ts +7 -0
- package/dist/lib/capacity/route.js +46 -1
- package/dist/lib/lanes/delivery.d.ts +13 -1
- package/dist/lib/lanes/delivery.js +11 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/loop.d.ts +5 -0
- package/dist/lib/lanes/loop.js +8 -0
- package/dist/lib/lanes/reconcile.js +18 -1
- package/dist/lib/lanes/spawn.js +13 -3
- package/dist/lib/lanes/worker-continuity.d.ts +13 -1
- package/dist/lib/lanes/worker-continuity.js +14 -0
- package/dist/utils/jq-windows-compat.d.ts +1 -0
- package/dist/utils/jq-windows-compat.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.132] — 2026-10-14
|
|
6
|
+
- `hq lanes --account auto` and `hq capacity route` no longer refuse every signed-in account when the capacity collector is off or its readings are stale; those accounts are picked with unknown usage, below accounts with fresh headroom.
|
|
7
|
+
- `hq lanes create` now resumes an earlier worker session only when the project, model, and effort match, and otherwise starts a fresh thread.
|
|
8
|
+
- A loop lane starting its next phase now stays running in `hq lanes status` and `hq lanes list` while the phase resumes, instead of showing done for several seconds.
|
|
9
|
+
- A loop lane waiting for its next phase now shows as running in `hq lanes status` and `hq lanes list`, and becomes done only after it is stopped.
|
|
10
|
+
- Recognize Git Bash on Git for Windows 2.56+, which reports `MSYSTEM=UCRT64` instead of `MINGW64`, so delegate jq calls keep their byte-exact output and journal titles with newlines get a valid file name on Windows.
|
|
11
|
+
|
|
5
12
|
## [5.345.130] — 2026-10-14
|
|
6
13
|
- Add native TypeScript `hq core workflow run` orchestration with the bundled runner as its flag-off fallback.
|
|
7
14
|
|
|
@@ -36,8 +36,9 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
|
|
|
36
36
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
37
37
|
|
|
38
38
|
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
39
|
+
# Git for Windows 2.56+ reports MSYSTEM=UCRT64; match every MSYS2 environment.
|
|
39
40
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
40
|
-
msys*:*|*:MINGW
|
|
41
|
+
msys*:*|*:MINGW*|*:UCRT*|*:CLANG*|*:MSYS)
|
|
41
42
|
# Probe --binary once per script run, in the top-level shell. Most jq
|
|
42
43
|
# calls run inside $(...) subshells, so a cache filled lazily inside jq()
|
|
43
44
|
# would be lost and the probe would repeat on every call.
|
|
@@ -43,8 +43,9 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
|
|
|
43
43
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
44
44
|
|
|
45
45
|
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
46
|
+
# Git for Windows 2.56+ reports MSYSTEM=UCRT64; match every MSYS2 environment.
|
|
46
47
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
47
|
-
msys*:*|*:MINGW
|
|
48
|
+
msys*:*|*:MINGW*|*:UCRT*|*:CLANG*|*:MSYS)
|
|
48
49
|
_hq_jq_binary_supported=""
|
|
49
50
|
jq() {
|
|
50
51
|
local stdout_file stderr_file status binary_supported
|
|
@@ -37,7 +37,7 @@ warn() { echo "session-journal: $*" >&2; }
|
|
|
37
37
|
|
|
38
38
|
slugify() {
|
|
39
39
|
# Win32 rejects newline file-name bytes; otherwise keep the POSIX slug pipeline.
|
|
40
|
-
local slug_input="$1"; if [[ "${OSTYPE:-}" == msys* ]] || [[ "${MSYSTEM:-}"
|
|
40
|
+
local slug_input="$1"; if [[ "${OSTYPE:-}" == msys* ]] || [[ "${MSYSTEM:-}" =~ ^(MINGW|UCRT|CLANG|MSYS) ]]; then slug_input="${slug_input//$'\n'/-}"; fi; printf '%s' "$slug_input" \
|
|
41
41
|
| tr '[:upper:]' '[:lower:]' \
|
|
42
42
|
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
|
|
43
43
|
| cut -c1-40
|
|
@@ -15,8 +15,7 @@ import { type ClaudeRefreshLastEntry } from "../lib/capacity/collectors/claude-o
|
|
|
15
15
|
export declare const CAPACITY_SCHEMA_VERSION: "hq-capacity-now-view.v1";
|
|
16
16
|
export declare const DOCTOR_SCHEMA_VERSION = "hq-capacity-doctor.v1";
|
|
17
17
|
export declare const PARSER_FAILURE_WINDOW_MS: number;
|
|
18
|
-
|
|
19
|
-
export declare const CAPACITY_STALE_MS: number;
|
|
18
|
+
export { CAPACITY_STALE_MS } from "../lib/capacity/freshness.js";
|
|
20
19
|
/** Short versions inside test/fixtures/capacity/cli-versions.json. */
|
|
21
20
|
export declare const PINNED_CLI_VERSIONS: Record<string, string>;
|
|
22
21
|
export interface CapacityJson extends NowView {
|
|
@@ -7,6 +7,7 @@ import { LedgerCorrupt, LedgerMissing, READING_SOURCES, capacityOptionsFromEnv,
|
|
|
7
7
|
import { listCapacityAccounts, registryAccountIds } from "../lib/capacity/accounts.js";
|
|
8
8
|
import { claudeIdentityKeys, readClaudeRefreshLast } from "../lib/capacity/collectors/claude-oauth.js";
|
|
9
9
|
import { projectCapacityView } from "../lib/capacity/project.js";
|
|
10
|
+
import { capacityViewAge } from "../lib/capacity/freshness.js";
|
|
10
11
|
import { capacityWidgetTimeZone, renderCapacityWidget } from "../lib/capacity/widget.js";
|
|
11
12
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
12
13
|
import { loadLanesConfig } from "../lib/lanes/config.js";
|
|
@@ -18,8 +19,7 @@ import { CapacityUsageError, REFUSAL_HELP, ingestRefusal, } from "../lib/capacit
|
|
|
18
19
|
export const CAPACITY_SCHEMA_VERSION = nowViewJsonSchema.$id;
|
|
19
20
|
export const DOCTOR_SCHEMA_VERSION = "hq-capacity-doctor.v1";
|
|
20
21
|
export const PARSER_FAILURE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
21
|
-
|
|
22
|
-
export const CAPACITY_STALE_MS = 5 * 60 * 1000;
|
|
22
|
+
export { CAPACITY_STALE_MS } from "../lib/capacity/freshness.js";
|
|
23
23
|
/** Short versions inside test/fixtures/capacity/cli-versions.json. */
|
|
24
24
|
export const PINNED_CLI_VERSIONS = {
|
|
25
25
|
claude: "2.1.152",
|
|
@@ -54,20 +54,18 @@ export function withSchemaVersion(view) {
|
|
|
54
54
|
}
|
|
55
55
|
/** Stale when generatedAt is missing or older than five minutes. */
|
|
56
56
|
export function capacityFreshness(generatedAt, nowMs) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
57
|
+
const age = capacityViewAge(generatedAt, nowMs);
|
|
58
|
+
if (!age.known) {
|
|
59
59
|
return {
|
|
60
60
|
stale: true,
|
|
61
61
|
ageMs: 0,
|
|
62
62
|
message: "collector not running, numbers may be stale; last update unknown",
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
const ageMs = Math.max(0, nowMs - at);
|
|
66
|
-
const stale = ageMs > CAPACITY_STALE_MS;
|
|
67
65
|
return {
|
|
68
|
-
stale,
|
|
69
|
-
ageMs,
|
|
70
|
-
message: stale
|
|
66
|
+
stale: age.stale,
|
|
67
|
+
ageMs: age.ageMs,
|
|
68
|
+
message: age.stale
|
|
71
69
|
? `collector not running, numbers may be stale; last update ${formatAge(generatedAt, nowMs)}`
|
|
72
70
|
: null,
|
|
73
71
|
};
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Ctx, LaneResumeOptions } from "./lanes-shared.js";
|
|
2
2
|
import type { LaneRecord } from "../lib/lanes/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A loop lane that finished a phase and is waiting for its next envelope is
|
|
5
|
+
* alive, not finished. The watcher still writes `done` when the phase's worker
|
|
6
|
+
* exits (the controller reads that as the phase-complete signal); this moves
|
|
7
|
+
* the lane back to `running`, the same state an admitted loop lane holds
|
|
8
|
+
* before its first phase. Only a stop makes a loop lane `done`. The next
|
|
9
|
+
* phase resumes the lane from `running` (`loopIdle`), so it is not written
|
|
10
|
+
* `done` again while the loop is active.
|
|
11
|
+
*/
|
|
12
|
+
export declare function settleLoopLaneIdle(hqRoot: string, laneId: string): LaneRecord;
|
|
3
13
|
export interface LoopControllerDeps {
|
|
4
14
|
resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
|
|
5
15
|
interruptLane(laneId: string, reason: string, options: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { readLane, retryUpdateLane, updateLane } from "../lib/lanes/store.js";
|
|
5
|
+
import { LanesError, isLaneVersionConflict } from "../lib/lanes/errors.js";
|
|
6
6
|
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
7
7
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
8
8
|
import { raiseLaneQuestion } from "../lib/lanes/questions.js";
|
|
@@ -15,6 +15,34 @@ const LOOP_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
|
15
15
|
function loopRunDir(hqRoot, laneId) {
|
|
16
16
|
return path.join(hqRoot, LOOP_RUNS_REL, laneId);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A loop lane that finished a phase and is waiting for its next envelope is
|
|
20
|
+
* alive, not finished. The watcher still writes `done` when the phase's worker
|
|
21
|
+
* exits (the controller reads that as the phase-complete signal); this moves
|
|
22
|
+
* the lane back to `running`, the same state an admitted loop lane holds
|
|
23
|
+
* before its first phase. Only a stop makes a loop lane `done`. The next
|
|
24
|
+
* phase resumes the lane from `running` (`loopIdle`), so it is not written
|
|
25
|
+
* `done` again while the loop is active.
|
|
26
|
+
*/
|
|
27
|
+
export function settleLoopLaneIdle(hqRoot, laneId) {
|
|
28
|
+
for (let attempt = 0;; attempt += 1) {
|
|
29
|
+
const current = readLane(hqRoot, laneId);
|
|
30
|
+
if (current.loop_mode !== true || current.state !== "done")
|
|
31
|
+
return current;
|
|
32
|
+
try {
|
|
33
|
+
return updateLane(hqRoot, laneId, current.version, "command", (record) => ({
|
|
34
|
+
...record,
|
|
35
|
+
state: "running",
|
|
36
|
+
terminal_reason: undefined,
|
|
37
|
+
terminal_details: undefined,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!isLaneVersionConflict(error) || attempt >= 4)
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
18
46
|
export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
19
47
|
const entry = process.argv[1];
|
|
20
48
|
if (!entry)
|
|
@@ -158,6 +186,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
158
186
|
freshThread: fresh,
|
|
159
187
|
...(sessionId ? { loopSessionId: sessionId } : {}),
|
|
160
188
|
...(repairError !== undefined ? { loopHandoffRepair: true } : {}),
|
|
189
|
+
...(current.state === "running" ? { loopIdle: true } : {}),
|
|
161
190
|
workingDirectory: worktree,
|
|
162
191
|
});
|
|
163
192
|
}
|
|
@@ -187,6 +216,8 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
187
216
|
if (data.schema === "hq-phase-envelope/v1" && !handoff) {
|
|
188
217
|
throw new Error("section 8 phase requires an hq-phase-handoff/v1 object in the lane envelope artifacts");
|
|
189
218
|
}
|
|
219
|
+
if (latest.state === "done")
|
|
220
|
+
settleLoopLaneIdle(hqRoot, laneId);
|
|
190
221
|
return { output: JSON.stringify(handoff ?? envelopeRecord.envelope), ...(latest.provider_session_id ? { sessionId: latest.provider_session_id } : {}) };
|
|
191
222
|
}
|
|
192
223
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BALANCE_MOVABLE_STATES } from "../lib/lanes/claude-balance.js";
|
|
2
2
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
3
|
-
import { appendCappedHistory, isFailedQuotaKeepAliveLane, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
3
|
+
import { appendCappedHistory, isFailedQuotaKeepAliveLane, isLoopIdleResumeLane, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
4
4
|
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
5
5
|
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
6
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
@@ -164,6 +164,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
164
164
|
}
|
|
165
165
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
166
166
|
const loopHandoffRepair = isLoopHandoffRepairResume(lane, opts);
|
|
167
|
+
const loopIdle = isLoopIdleResumeLane(lane, opts.loopIdle);
|
|
167
168
|
if (opts.loopHandoffRepair === true && !loopHandoffRepair) {
|
|
168
169
|
throw new LanesError("loop_handoff_repair_session_mismatch", "Loop handoff repair requires the awaiting-input loop lane's current provider session", { lane_id: laneId, state: lane.state });
|
|
169
170
|
}
|
|
@@ -250,7 +251,11 @@ export function createLaneResumeHandlers(deps) {
|
|
|
250
251
|
}
|
|
251
252
|
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
252
253
|
? null
|
|
253
|
-
: resumeCommandRefusal(lane, {
|
|
254
|
+
: resumeCommandRefusal(lane, {
|
|
255
|
+
allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
256
|
+
allowFresh: opts.freshThread === true,
|
|
257
|
+
allowLoopIdle: loopIdle,
|
|
258
|
+
});
|
|
254
259
|
if (refusal !== null) {
|
|
255
260
|
throw new LanesError(refusal.error, refusal.message, {
|
|
256
261
|
lane_id: laneId,
|
|
@@ -260,7 +265,10 @@ export function createLaneResumeHandlers(deps) {
|
|
|
260
265
|
});
|
|
261
266
|
}
|
|
262
267
|
assertLaneRunsHereForAdmission(ctx, lane);
|
|
263
|
-
|
|
268
|
+
// An idle loop lane resumes from the phase that finished; it stays `running`.
|
|
269
|
+
const resumedFrom = loopIdle
|
|
270
|
+
? "done"
|
|
271
|
+
: lane.state;
|
|
264
272
|
const text = opts.text?.trim() ? opts.text : undefined;
|
|
265
273
|
const at = ctx.now().toISOString();
|
|
266
274
|
const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
|
|
@@ -442,6 +450,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
442
450
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
443
451
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
444
452
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
453
|
+
...(loopIdle ? { loopIdle: true } : {}),
|
|
445
454
|
pending: {
|
|
446
455
|
trigger: "resume",
|
|
447
456
|
resumed_from: resumedFrom,
|
|
@@ -478,6 +487,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
478
487
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
479
488
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
480
489
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
490
|
+
...(loopIdle ? { loopIdle: true } : {}),
|
|
481
491
|
pending: {
|
|
482
492
|
trigger: "resume",
|
|
483
493
|
resumed_from: resumedFrom,
|
|
@@ -509,6 +519,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
509
519
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
510
520
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
511
521
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
522
|
+
...(loopIdle ? { loopIdle: true } : {}),
|
|
512
523
|
pending: {
|
|
513
524
|
trigger: "resume",
|
|
514
525
|
resumed_from: resumedFrom,
|
|
@@ -643,6 +654,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
643
654
|
kind: "resume",
|
|
644
655
|
from: resumedFrom,
|
|
645
656
|
...(opts.freshThread === true ? { fresh: true } : {}),
|
|
657
|
+
...(loopIdle ? { loopIdle: true } : {}),
|
|
646
658
|
...(resumedFrom === "awaiting_input" && (pendingQuestions.length === 0 || loopHandoffRepair) ? { allowAwaitingInput: true } : {}),
|
|
647
659
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
648
660
|
...(failedResumeExpectedSenior !== undefined
|
|
@@ -83,6 +83,11 @@ export interface LaneResumeOptions {
|
|
|
83
83
|
loopSessionId?: string;
|
|
84
84
|
/** Allows only a section 8 handoff repair to continue a loop lane with a pending question. */
|
|
85
85
|
loopHandoffRepair?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Internal loop runner: start the next phase of a loop lane that is idle in
|
|
88
|
+
* `running` between phases. The lane is resumed from `running` directly.
|
|
89
|
+
*/
|
|
90
|
+
loopIdle?: boolean;
|
|
86
91
|
meshCompany?: string;
|
|
87
92
|
profileRepinContext?: string;
|
|
88
93
|
grantContext?: string;
|
|
@@ -121,6 +126,8 @@ export interface PrepareAccountMoveInput {
|
|
|
121
126
|
refreshProfileSettings?: boolean;
|
|
122
127
|
/** Provenance recorded for the account this move selects. */
|
|
123
128
|
accountSource?: "cli" | "balanced" | "profile";
|
|
129
|
+
/** The loop controller's next-phase resume of a loop lane idle in `running`. */
|
|
130
|
+
loopIdle?: boolean;
|
|
124
131
|
}
|
|
125
132
|
export interface RecordResumeSetupInput {
|
|
126
133
|
profile?: ResolvedResumeProfile;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -46,8 +46,8 @@ import { registerDaemonLanesReconcileHandler } from "../lib/lanes/daemon-reconci
|
|
|
46
46
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
47
47
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
48
48
|
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, 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_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
49
|
-
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
50
|
-
import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
49
|
+
import { applyContinuitySettings, findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
50
|
+
import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopLaneIsIdle, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
51
51
|
import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
52
52
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
53
53
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
@@ -77,7 +77,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
77
77
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
78
78
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
79
79
|
import { remoteControlSelectionConfig, remoteControlSessionHome } from "../lib/lanes/remote-control-lane.js";
|
|
80
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
80
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, isLoopIdleResumeLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
81
81
|
import { admitQueuedNow } from "../lib/lanes/reconcile.js";
|
|
82
82
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
83
83
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
@@ -89,8 +89,9 @@ import { writeQuestionBody } from "../lib/lanes/questions.js";
|
|
|
89
89
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
90
90
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
91
91
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
92
|
-
import { LedgerCorrupt, capacityOptionsFromEnv, readNow, rebuildNow, } from "../lib/capacity/ledger.js";
|
|
92
|
+
import { LedgerCorrupt, capacityOptionsFromEnv, readCollectorMark, readNow, rebuildNow, } from "../lib/capacity/ledger.js";
|
|
93
93
|
import { pickAccount } from "../lib/capacity/route.js";
|
|
94
|
+
import { capacityViewAge } from "../lib/capacity/freshness.js";
|
|
94
95
|
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
95
96
|
import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
96
97
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
@@ -3436,6 +3437,8 @@ async function runCreate(ctx, opts) {
|
|
|
3436
3437
|
...(explicitProvider ? { providers: [explicitProvider] } : {}),
|
|
3437
3438
|
model: opts.model ?? null,
|
|
3438
3439
|
now: ctx.now(),
|
|
3440
|
+
collectorEnabled: readCollectorMark(ledgerOptions)?.enabled ?? null,
|
|
3441
|
+
capacityStale: capacityViewAge(view.generatedAt, ctx.now().getTime()).stale,
|
|
3439
3442
|
lanesError: routeLanesError,
|
|
3440
3443
|
});
|
|
3441
3444
|
if (!result.ok || !result.pick) {
|
|
@@ -4102,6 +4105,7 @@ async function runCreate(ctx, opts) {
|
|
|
4102
4105
|
providerEffort: config.providers[provider].effort,
|
|
4103
4106
|
model,
|
|
4104
4107
|
});
|
|
4108
|
+
workerContinuity = applyContinuitySettings(workerContinuity, { project, model, effort: effort.effort });
|
|
4105
4109
|
if (outpostId) {
|
|
4106
4110
|
admission = { action: "queue" };
|
|
4107
4111
|
validation = "pending";
|
|
@@ -7414,7 +7418,9 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
7414
7418
|
const now = ctx.now();
|
|
7415
7419
|
const laneId = lane.lane_id;
|
|
7416
7420
|
assertLaneRunsHereForAdmission(ctx, lane);
|
|
7417
|
-
|
|
7421
|
+
// An idle loop lane is `running` with no worker between phases, so its
|
|
7422
|
+
// session can move before the next phase starts.
|
|
7423
|
+
if (lane.state === "running" && !isLoopIdleResumeLane(lane, input.loopIdle)) {
|
|
7418
7424
|
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
7419
7425
|
lane_id: laneId,
|
|
7420
7426
|
state: lane.state,
|
|
@@ -8914,7 +8920,7 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
|
8914
8920
|
const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
|
|
8915
8921
|
? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
|
|
8916
8922
|
: undefined;
|
|
8917
|
-
if (lane.loop_mode === true && lane.state !== "running") {
|
|
8923
|
+
if (lane.loop_mode === true && (lane.state !== "running" || loopLaneIsIdle(runDir(hqRoot, lane.lane_id), lane.lane_id))) {
|
|
8918
8924
|
if (!options.quiet)
|
|
8919
8925
|
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...queueInterrupt });
|
|
8920
8926
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity view freshness. Kept in lib (not commands/capacity.ts) so resident
|
|
3
|
+
* paths such as `hq lanes` can check staleness without pulling in the capacity
|
|
4
|
+
* command module and its lazy collector imports.
|
|
5
|
+
*/
|
|
6
|
+
/** now.json older than this is stale: the collector is not keeping the view current. */
|
|
7
|
+
export declare const CAPACITY_STALE_MS: number;
|
|
8
|
+
export interface CapacityViewAge {
|
|
9
|
+
stale: boolean;
|
|
10
|
+
/** Milliseconds since generatedAt; 0 when generatedAt is missing or unparseable. */
|
|
11
|
+
ageMs: number;
|
|
12
|
+
/** False when generatedAt is missing or unparseable. */
|
|
13
|
+
known: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Stale when generatedAt is missing or older than CAPACITY_STALE_MS. */
|
|
16
|
+
export declare function capacityViewAge(generatedAt: string, nowMs: number): CapacityViewAge;
|
|
17
|
+
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity view freshness. Kept in lib (not commands/capacity.ts) so resident
|
|
3
|
+
* paths such as `hq lanes` can check staleness without pulling in the capacity
|
|
4
|
+
* command module and its lazy collector imports.
|
|
5
|
+
*/
|
|
6
|
+
/** now.json older than this is stale: the collector is not keeping the view current. */
|
|
7
|
+
export const CAPACITY_STALE_MS = 5 * 60 * 1000;
|
|
8
|
+
/** Stale when generatedAt is missing or older than CAPACITY_STALE_MS. */
|
|
9
|
+
export function capacityViewAge(generatedAt, nowMs) {
|
|
10
|
+
const at = Date.parse(generatedAt);
|
|
11
|
+
if (Number.isNaN(at))
|
|
12
|
+
return { stale: true, ageMs: 0, known: false };
|
|
13
|
+
const ageMs = Math.max(0, nowMs - at);
|
|
14
|
+
return { stale: ageMs > CAPACITY_STALE_MS, ageMs, known: true };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=freshness.js.map
|
|
@@ -23,6 +23,11 @@ export declare const LIMIT_STALE_MS: number;
|
|
|
23
23
|
export declare const FRESH_MS: number;
|
|
24
24
|
/** Score penalty for an account whose binding limit reading is not fresh. */
|
|
25
25
|
export declare const OLD_READING_PENALTY = 10;
|
|
26
|
+
/**
|
|
27
|
+
* Score for an account ranked with unknown usage (collector off or capacity
|
|
28
|
+
* view stale). Sits below any account with fresh known headroom.
|
|
29
|
+
*/
|
|
30
|
+
export declare const UNKNOWN_USAGE_SCORE = -100;
|
|
26
31
|
/** Default --floor applied by the CLI (percent headroom to clear). */
|
|
27
32
|
export declare const DEFAULT_FLOOR = 15;
|
|
28
33
|
/** Default --prefer order applied by the CLI. */
|
|
@@ -78,6 +83,8 @@ export interface RouteRanked {
|
|
|
78
83
|
weekly_resets_at: string | null;
|
|
79
84
|
why: string;
|
|
80
85
|
lane_flags: string;
|
|
86
|
+
/** True when no usable reading exists and the account is ranked as unknown. */
|
|
87
|
+
usage_unknown: boolean;
|
|
81
88
|
}
|
|
82
89
|
/** Live lane on an account the router would avoid; command names a replacement. */
|
|
83
90
|
export interface RouteMove {
|
|
@@ -42,6 +42,11 @@ export const LIMIT_STALE_MS = 24 * 36e5;
|
|
|
42
42
|
export const FRESH_MS = 60 * 6e4;
|
|
43
43
|
/** Score penalty for an account whose binding limit reading is not fresh. */
|
|
44
44
|
export const OLD_READING_PENALTY = 10;
|
|
45
|
+
/**
|
|
46
|
+
* Score for an account ranked with unknown usage (collector off or capacity
|
|
47
|
+
* view stale). Sits below any account with fresh known headroom.
|
|
48
|
+
*/
|
|
49
|
+
export const UNKNOWN_USAGE_SCORE = -100;
|
|
45
50
|
/** Default --floor applied by the CLI (percent headroom to clear). */
|
|
46
51
|
export const DEFAULT_FLOOR = 15;
|
|
47
52
|
/** Default --prefer order applied by the CLI. */
|
|
@@ -197,6 +202,37 @@ export function pickAccount(input) {
|
|
|
197
202
|
const lanes = input.lanes ?? [];
|
|
198
203
|
const ranked = [];
|
|
199
204
|
const skipped = [];
|
|
205
|
+
// With the collector off (or its mark missing) or the view stale, readings
|
|
206
|
+
// never refresh. Missing or old readings then say nothing about the account,
|
|
207
|
+
// so a signed-in, enabled account is ranked as unknown instead of skipped.
|
|
208
|
+
const readingsUnreliable = input.collectorEnabled === false
|
|
209
|
+
|| input.collectorEnabled === null
|
|
210
|
+
|| input.capacityStale === true;
|
|
211
|
+
const rankUnknown = (base, accountId, tokenExpired) => {
|
|
212
|
+
const live = liveLaneCount(lanes, base.provider, accountId);
|
|
213
|
+
const whyParts = ["usage unknown, capacity readings are stale or the collector is off"];
|
|
214
|
+
if (live)
|
|
215
|
+
whyParts.push(`${live} live lane${live === 1 ? "" : "s"} already on it`);
|
|
216
|
+
if (tokenExpired)
|
|
217
|
+
whyParts.push("sign-in renews when the lane starts");
|
|
218
|
+
ranked.push({
|
|
219
|
+
provider: base.provider,
|
|
220
|
+
account: accountId,
|
|
221
|
+
label: base.label,
|
|
222
|
+
organization: base.organization,
|
|
223
|
+
headroom: 0,
|
|
224
|
+
score: UNKNOWN_USAGE_SCORE - live * LANE_PENALTY,
|
|
225
|
+
live_lanes: live,
|
|
226
|
+
reset_soon: false,
|
|
227
|
+
token_expired: tokenExpired,
|
|
228
|
+
reading_age_h: 0,
|
|
229
|
+
binding: { limit: "unknown", used_percent: 0, resets_at: null },
|
|
230
|
+
weekly_resets_at: null,
|
|
231
|
+
why: whyParts.join("; "),
|
|
232
|
+
lane_flags: `--provider ${base.provider} --account ${accountId}`,
|
|
233
|
+
usage_unknown: true,
|
|
234
|
+
});
|
|
235
|
+
};
|
|
200
236
|
for (const provider of input.view.providers) {
|
|
201
237
|
const providerId = provider.provider;
|
|
202
238
|
if (!isLaneProvider(providerId))
|
|
@@ -247,6 +283,10 @@ export function pickAccount(input) {
|
|
|
247
283
|
? Math.max(0, nowMs - observedMs)
|
|
248
284
|
: Infinity;
|
|
249
285
|
if (accountAge > STALE_SKIP_MS) {
|
|
286
|
+
if (readingsUnreliable) {
|
|
287
|
+
rankUnknown(base, laneMatch.id, tokenExpired);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
250
290
|
skipped.push({
|
|
251
291
|
...base,
|
|
252
292
|
reason: "no reading in the last 24 hours",
|
|
@@ -313,6 +353,10 @@ export function pickAccount(input) {
|
|
|
313
353
|
}
|
|
314
354
|
const known = viewRows.filter((row) => typeof row.used === "number");
|
|
315
355
|
if (known.length === 0) {
|
|
356
|
+
if (readingsUnreliable) {
|
|
357
|
+
rankUnknown(base, laneMatch.id, tokenExpired);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
316
360
|
skipped.push({
|
|
317
361
|
...base,
|
|
318
362
|
reason: "no usage numbers for this account in the last day",
|
|
@@ -376,6 +420,7 @@ export function pickAccount(input) {
|
|
|
376
420
|
weekly_resets_at: weekly?.resetsAt ?? null,
|
|
377
421
|
why: whyParts.join("; "),
|
|
378
422
|
lane_flags: `--provider ${providerId} --account ${laneMatch.id}`,
|
|
423
|
+
usage_unknown: false,
|
|
379
424
|
});
|
|
380
425
|
}
|
|
381
426
|
}
|
|
@@ -401,7 +446,7 @@ export function pickAccount(input) {
|
|
|
401
446
|
bad.set(`${row.provider}:${row.account}`, row.reason);
|
|
402
447
|
}
|
|
403
448
|
for (const row of ranked) {
|
|
404
|
-
if (row.headroom < floor) {
|
|
449
|
+
if (!row.usage_unknown && row.headroom < floor) {
|
|
405
450
|
bad.set(`${row.provider}:${row.account}`, `${row.binding.limit} has ${row.headroom}% left`);
|
|
406
451
|
}
|
|
407
452
|
}
|
|
@@ -68,7 +68,12 @@ export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "n
|
|
|
68
68
|
export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
|
|
69
69
|
/** Additional presentation-only result used by `hq lanes show`. */
|
|
70
70
|
export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
|
|
71
|
-
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details"
|
|
71
|
+
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details"> & Partial<Pick<LaneRecord, "loop_mode">>;
|
|
72
|
+
/**
|
|
73
|
+
* A loop lane between phases is `running` with no worker. Only the loop
|
|
74
|
+
* controller's next-phase resume may continue it from that state.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isLoopIdleResumeLane(lane: DeliveryLane, loopIdle: boolean | undefined): boolean;
|
|
72
77
|
export declare function isFailedQuotaKeepAliveLane(lane: DeliveryLane): boolean;
|
|
73
78
|
/**
|
|
74
79
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
@@ -147,6 +152,12 @@ export type LaunchTrigger = {
|
|
|
147
152
|
/** Loop phases may deliberately start a fresh session at a story boundary. */
|
|
148
153
|
fresh?: true;
|
|
149
154
|
allowAwaitingInput?: true;
|
|
155
|
+
/**
|
|
156
|
+
* The loop controller starting the next phase of a loop lane that sits
|
|
157
|
+
* in `running` with no worker between phases. The lane stays `running`
|
|
158
|
+
* through the resume; it is never written `done` to get here.
|
|
159
|
+
*/
|
|
160
|
+
loopIdle?: true;
|
|
150
161
|
fromFailed?: true;
|
|
151
162
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
152
163
|
expectedSenior?: SeniorRef;
|
|
@@ -167,6 +178,7 @@ export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
|
167
178
|
allowFailed?: boolean;
|
|
168
179
|
allowAwaitingInput?: boolean;
|
|
169
180
|
allowFresh?: boolean;
|
|
181
|
+
allowLoopIdle?: boolean;
|
|
170
182
|
}): {
|
|
171
183
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
172
184
|
message: string;
|
|
@@ -87,6 +87,13 @@ export function isResumeCommandState(state) {
|
|
|
87
87
|
return RESUME_COMMAND_STATES.includes(state);
|
|
88
88
|
}
|
|
89
89
|
export const LANE_DELIVERY_MECHANISMS = ["dropbox", "resume", "none"];
|
|
90
|
+
/**
|
|
91
|
+
* A loop lane between phases is `running` with no worker. Only the loop
|
|
92
|
+
* controller's next-phase resume may continue it from that state.
|
|
93
|
+
*/
|
|
94
|
+
export function isLoopIdleResumeLane(lane, loopIdle) {
|
|
95
|
+
return loopIdle === true && lane.loop_mode === true && lane.state === "running";
|
|
96
|
+
}
|
|
90
97
|
/** Only the terminal result of a failed automatic envelope repair is recoverable. */
|
|
91
98
|
function isFailedEnvelopeRepairLane(lane) {
|
|
92
99
|
return (lane.state === "failed" &&
|
|
@@ -356,6 +363,7 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
|
|
|
356
363
|
*/
|
|
357
364
|
export function resumeCommandRefusal(lane, options = {}) {
|
|
358
365
|
if (!isResumeCommandState(lane.state) &&
|
|
366
|
+
!isLoopIdleResumeLane(lane, options.allowLoopIdle) &&
|
|
359
367
|
!(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
|
|
360
368
|
!isFailedEnvelopeRepairLane(lane) &&
|
|
361
369
|
!isFailedQuotaKeepAliveLane(lane) &&
|
|
@@ -414,6 +422,7 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
414
422
|
allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
415
423
|
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
416
424
|
allowFresh: trigger.fresh === true,
|
|
425
|
+
allowLoopIdle: trigger.loopIdle === true,
|
|
417
426
|
});
|
|
418
427
|
if (trigger.fresh === true && refusal === null)
|
|
419
428
|
return { mode: "fresh" };
|
|
@@ -446,6 +455,8 @@ export function claimableStatesFor(trigger, lane) {
|
|
|
446
455
|
const states = trigger.fromFailed === true || envelopeRepairFailed || (lane !== undefined && isFailedQuotaKeepAliveLane(lane))
|
|
447
456
|
? [...RESUME_COMMAND_STATES, "failed"]
|
|
448
457
|
: [...RESUME_COMMAND_STATES];
|
|
458
|
+
if (lane !== undefined && isLoopIdleResumeLane(lane, trigger.loopIdle))
|
|
459
|
+
states.push("running");
|
|
449
460
|
return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
|
|
450
461
|
}
|
|
451
462
|
default: {
|
|
@@ -63,7 +63,7 @@ export type { RegistryPersona } from "./registry-persona.js";
|
|
|
63
63
|
export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
|
|
64
64
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
65
65
|
export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
|
|
66
|
-
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
66
|
+
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, isLoopIdleResumeLane, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
67
67
|
export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
|
|
68
68
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
69
69
|
export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorke
|
|
|
34
34
|
export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
35
35
|
export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
|
|
36
36
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
37
|
-
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
37
|
+
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, isLoopIdleResumeLane, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
38
38
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
39
39
|
export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
|
|
40
40
|
export { EFFORT_SOURCES, PROVIDER_EFFORT_LEVELS, appendEffortFlag, defaultEffortForModel, parseCliEffort, parseConfigEffort, resolveLaneEffort, } from "./effort.js";
|
package/dist/lib/lanes/loop.d.ts
CHANGED
|
@@ -188,6 +188,11 @@ export declare function loopStatus(runDir: string, laneId: string): LoopStatus;
|
|
|
188
188
|
export declare function commitIdleLoopStop(runDir: string, laneId: string, now?: () => Date): boolean;
|
|
189
189
|
export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined): void;
|
|
190
190
|
export declare function clearLoopControllerPid(runDir: string, laneId: string, pid: number): void;
|
|
191
|
+
/**
|
|
192
|
+
* True when a loop lane's controller is alive with no phase active: the lane
|
|
193
|
+
* finished its last phase and is waiting for the next envelope.
|
|
194
|
+
*/
|
|
195
|
+
export declare function loopLaneIsIdle(runDir: string, laneId: string): boolean;
|
|
191
196
|
export declare function loopControllerIsAlive(runDir: string): boolean;
|
|
192
197
|
/** Run queued phases serially. Sessions are isolated by story and provider. */
|
|
193
198
|
export declare function runLoopLane(options: LoopRunnerOptions): Promise<LoopRunSummary>;
|
package/dist/lib/lanes/loop.js
CHANGED
|
@@ -648,6 +648,14 @@ export function clearLoopControllerPid(runDir, laneId, pid) {
|
|
|
648
648
|
if (currentPid === pid)
|
|
649
649
|
persistStatus(runDir, laneId, { pid: undefined });
|
|
650
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* True when a loop lane's controller is alive with no phase active: the lane
|
|
653
|
+
* finished its last phase and is waiting for the next envelope.
|
|
654
|
+
*/
|
|
655
|
+
export function loopLaneIsIdle(runDir, laneId) {
|
|
656
|
+
const status = loopStatus(runDir, laneId);
|
|
657
|
+
return status.state === "waiting" && status.active_envelope_id === null && loopControllerIsAlive(runDir);
|
|
658
|
+
}
|
|
651
659
|
export function loopControllerIsAlive(runDir) {
|
|
652
660
|
let pid;
|
|
653
661
|
try {
|
|
@@ -12,7 +12,7 @@ import { compareLaneQueueOrder, compareLaneQueueOrderWithLeads, leadLaneIds, has
|
|
|
12
12
|
import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
|
|
13
13
|
import { effectiveAccountHome } from "./account-home.js";
|
|
14
14
|
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
15
|
-
import { loopStatus } from "./loop.js";
|
|
15
|
+
import { loopLaneIsIdle, loopStatus } from "./loop.js";
|
|
16
16
|
import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
|
|
17
17
|
import { MAX_LANE_DEPTH } from "./constants.js";
|
|
18
18
|
import { laneDepthCap } from "./depth-cap.js";
|
|
@@ -1641,6 +1641,23 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1641
1641
|
// Watcher owns terminal classification of a process that has just exited.
|
|
1642
1642
|
continue;
|
|
1643
1643
|
}
|
|
1644
|
+
if (lane.loop_mode === true && loopLaneIsIdle(runDir, lane.lane_id)) {
|
|
1645
|
+
// An idle loop lane has no worker between phases. Its controller is
|
|
1646
|
+
// alive and waiting for the next envelope, so the lane is not finished.
|
|
1647
|
+
try {
|
|
1648
|
+
const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running" && current.last_host_report_at !== nowIso, (current) => ({ ...current, last_host_report_at: nowIso }), deps.dryRun);
|
|
1649
|
+
if (result.changed)
|
|
1650
|
+
summary.heartbeats.push(lane.lane_id);
|
|
1651
|
+
}
|
|
1652
|
+
catch (err) {
|
|
1653
|
+
markDirty(summary, {
|
|
1654
|
+
kind: "lane",
|
|
1655
|
+
id: lane.lane_id,
|
|
1656
|
+
why: `heartbeat CAS failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1644
1661
|
const hadPids = workerPid !== null || watcherPid !== null;
|
|
1645
1662
|
if (!hadPids && nowMs - lastHostReportMs(lane) < DEAD_HOST_MS) {
|
|
1646
1663
|
try {
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -23,7 +23,7 @@ import { installDrainHook } from "./hooks-install.js";
|
|
|
23
23
|
import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
|
|
24
24
|
import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
|
|
25
25
|
import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
|
|
26
|
-
import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
26
|
+
import { claimableStatesFor, isLoopIdleResumeLane, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
27
27
|
import { AccountResolutionError, LanesError } from "./errors.js";
|
|
28
28
|
import { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
29
29
|
import { registryWorkerRecord } from "./registry-persona.js";
|
|
@@ -2143,7 +2143,17 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2143
2143
|
? { provider_home_at_spawn: previousSpawn.provider_home_at_spawn }
|
|
2144
2144
|
: {}),
|
|
2145
2145
|
};
|
|
2146
|
-
|
|
2146
|
+
const loopIdleResume = launch.trigger?.kind === "resume" &&
|
|
2147
|
+
isLoopIdleResumeLane(existing, launch.trigger.loopIdle);
|
|
2148
|
+
if (loopIdleResume) {
|
|
2149
|
+
// An idle loop lane has no worker between phases. A live worker pid means
|
|
2150
|
+
// a phase is still running, so this resume would start a second worker.
|
|
2151
|
+
const priorWorker = readPidRecord(path.join(runDir, "worker.pid"));
|
|
2152
|
+
if (priorWorker !== null && isPidRecordAlive(priorWorker)) {
|
|
2153
|
+
throw new LanesError("lane_not_resumable", `Lane ${lane.lane_id} is already running`, { lane_id: lane.lane_id, state: existing.state, worker_pid: priorWorker.pid });
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
else if (existing.state === "running") {
|
|
2147
2157
|
if (launch.trigger?.kind === "resume") {
|
|
2148
2158
|
throw new LanesError("lane_not_resumable", `Lane ${lane.lane_id} is already running`, { lane_id: lane.lane_id, state: existing.state });
|
|
2149
2159
|
}
|
|
@@ -2643,7 +2653,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2643
2653
|
let priorStartRecorded = false;
|
|
2644
2654
|
const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
2645
2655
|
claimed = false;
|
|
2646
|
-
if (current.state === "running" && current.account === resolved.id) {
|
|
2656
|
+
if (current.state === "running" && current.account === resolved.id && !loopIdleResume) {
|
|
2647
2657
|
if (trigger.kind === "resume") {
|
|
2648
2658
|
throw new LanesError("lane_not_resumable", `Lane ${current.lane_id} is already running`, { lane_id: current.lane_id, state: current.state });
|
|
2649
2659
|
}
|
|
@@ -9,10 +9,22 @@ export interface WorkerContinuityLookup {
|
|
|
9
9
|
export interface WorkerContinuityResult {
|
|
10
10
|
previous?: LaneRecord;
|
|
11
11
|
resumable: boolean;
|
|
12
|
-
fallbackReason?: "provider_unsupported" | "session_id_missing" | "state_unsafe" | "host_mismatch";
|
|
12
|
+
fallbackReason?: "provider_unsupported" | "session_id_missing" | "state_unsafe" | "host_mismatch" | "settings_mismatch";
|
|
13
13
|
}
|
|
14
14
|
export declare function sameLaneHost(left: LaneRecord["host"], right: LaneRecord["host"]): boolean;
|
|
15
15
|
/** Select the prior worker lane within exactly one senior/company/provider scope. */
|
|
16
16
|
export declare function findWorkerContinuityLane(lanes: readonly LaneRecord[], lookup: WorkerContinuityLookup): WorkerContinuityResult;
|
|
17
|
+
export interface WorkerContinuitySettings {
|
|
18
|
+
project: LaneRecord["project_id"];
|
|
19
|
+
/** Resolved model for the new lane; undefined when the provider chooses. */
|
|
20
|
+
model?: string;
|
|
21
|
+
/** Resolved effort for the new lane; undefined when none is passed. */
|
|
22
|
+
effort?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Refuse reuse when the earlier session ran under a different project, model,
|
|
26
|
+
* or effort. A prior lane that recorded no model does not match a pinned one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyContinuitySettings(result: WorkerContinuityResult, settings: WorkerContinuitySettings): WorkerContinuityResult;
|
|
17
29
|
export declare function workerContinuityPrompt(previous: LaneRecord, resultSummary?: string, resultRef?: string): string;
|
|
18
30
|
//# sourceMappingURL=worker-continuity.d.ts.map
|
|
@@ -65,6 +65,20 @@ export function findWorkerContinuityLane(lanes, lookup) {
|
|
|
65
65
|
}
|
|
66
66
|
return { previous, resumable: true };
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Refuse reuse when the earlier session ran under a different project, model,
|
|
70
|
+
* or effort. A prior lane that recorded no model does not match a pinned one.
|
|
71
|
+
*/
|
|
72
|
+
export function applyContinuitySettings(result, settings) {
|
|
73
|
+
const previous = result.previous;
|
|
74
|
+
if (!result.resumable || !previous)
|
|
75
|
+
return result;
|
|
76
|
+
if (previous.project_id === settings.project && previous.model === settings.model &&
|
|
77
|
+
previous.effort === settings.effort) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
return { previous, resumable: false, fallbackReason: "settings_mismatch" };
|
|
81
|
+
}
|
|
68
82
|
export function workerContinuityPrompt(previous, resultSummary, resultRef) {
|
|
69
83
|
const summary = resultSummary?.trim();
|
|
70
84
|
const resultPath = resultRef?.trim()
|
|
@@ -9,5 +9,6 @@ export type JqInvoke = (args: string[], input: Buffer | string | undefined, cwd:
|
|
|
9
9
|
stdout?: "capture" | "ignore";
|
|
10
10
|
stderr?: "pipe" | "ignore";
|
|
11
11
|
}) => JqResult;
|
|
12
|
+
export declare function isMsys2Environment(msystem: string | undefined): boolean;
|
|
12
13
|
export declare function createShellCompatibleJqInvoker(invoke: JqInvoke, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, resolveShellType: (cwd: string) => string | undefined): JqInvoke;
|
|
13
14
|
//# sourceMappingURL=jq-windows-compat.d.ts.map
|
|
@@ -21,6 +21,10 @@ function normalizeResult(result) {
|
|
|
21
21
|
stderr: stderrBytes.toString("utf8"),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
// MSYS2 environment names. Git for Windows 2.56+ reports UCRT64 instead of MINGW64.
|
|
25
|
+
export function isMsys2Environment(msystem) {
|
|
26
|
+
return /^(?:MINGW|UCRT|CLANG|MSYS)/i.test(msystem ?? "");
|
|
27
|
+
}
|
|
24
28
|
export function createShellCompatibleJqInvoker(invoke, platform, env, resolveShellType) {
|
|
25
29
|
let binaryOptionSupported;
|
|
26
30
|
let shellTypeResolved = false;
|
|
@@ -32,7 +36,7 @@ export function createShellCompatibleJqInvoker(invoke, platform, env, resolveShe
|
|
|
32
36
|
shellType = resolveShellType(cwd);
|
|
33
37
|
shellTypeResolved = true;
|
|
34
38
|
}
|
|
35
|
-
const msysMode = /^msys/i.test(shellType ?? "") ||
|
|
39
|
+
const msysMode = /^msys/i.test(shellType ?? "") || isMsys2Environment(env.MSYSTEM);
|
|
36
40
|
if (!msysMode)
|
|
37
41
|
return invoke(args, input, cwd, options);
|
|
38
42
|
if (binaryOptionSupported === undefined) {
|