@indigoai-us/hq-cli 5.345.132 → 5.345.134
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 +10 -2
- package/dist/commands/access.d.ts +2 -0
- package/dist/commands/access.js +58 -9
- package/dist/commands/lanes-loop.d.ts +7 -3
- package/dist/commands/lanes-loop.js +16 -6
- package/dist/commands/lanes-resume.js +3 -15
- package/dist/commands/lanes-shared.d.ts +0 -7
- package/dist/commands/lanes.js +2 -4
- package/dist/lib/lanes/delivery.d.ts +1 -13
- package/dist/lib/lanes/delivery.js +0 -11
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/spawn.js +3 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.134] — 2026-10-15
|
|
6
|
+
- `hq access --as <member>` now honors the most specific ACL row covering a path, so an empty folder lock reports no access instead of the write access of a broader grant. `hq access` also reports "no access" rather than "never created" when a caller cannot list a path that an ACL lock denies them.
|
|
7
|
+
|
|
8
|
+
## [5.345.133] — 2026-10-15
|
|
9
|
+
- Pipeline loop lanes no longer hang after their first phase. 5.345.132 kept the lane running while its next phase resumed, which left the previous phase's watcher holding the lane, so the next phase's result was never recorded. That change is reverted; a loop lane may again show as done for a few seconds while its next phase starts.
|
|
10
|
+
|
|
5
11
|
## [5.345.132] — 2026-10-14
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## [5.345.131] — 2026-10-14
|
|
6
16
|
- `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
17
|
- `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
18
|
- 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
19
|
|
|
12
20
|
## [5.345.130] — 2026-10-14
|
|
13
21
|
- Add native TypeScript `hq core workflow run` orchestration with the bundled runner as its flag-off fallback.
|
|
@@ -48,6 +48,8 @@ export interface AccessAclTree {
|
|
|
48
48
|
updatedAt: string;
|
|
49
49
|
} | null;
|
|
50
50
|
effectivePermission: string | null;
|
|
51
|
+
/** Most-specific ACL row covering this prefix, including empty lock rows. */
|
|
52
|
+
coveringAclPath?: string | null;
|
|
51
53
|
}
|
|
52
54
|
export interface AccessMember {
|
|
53
55
|
personUid: string;
|
package/dist/commands/access.js
CHANGED
|
@@ -91,6 +91,27 @@ function fileGrantCoversPath(grantPath, targetPath) {
|
|
|
91
91
|
return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
|
|
92
92
|
}
|
|
93
93
|
const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
|
|
94
|
+
function aclPatternSpecificity(pattern) {
|
|
95
|
+
if (pattern === "*" || pattern === "")
|
|
96
|
+
return [0, 0];
|
|
97
|
+
const glob = pattern.endsWith("/*");
|
|
98
|
+
const body = glob ? pattern.slice(0, -2) : pattern;
|
|
99
|
+
const segs = body.split("/").filter(Boolean).length;
|
|
100
|
+
return [segs, glob ? 0 : 1];
|
|
101
|
+
}
|
|
102
|
+
function pickMostSpecificCoveringRow(rows, targetPath, company) {
|
|
103
|
+
const covering = rows.filter((row) => fileGrantCoversPath(toBucketRelative(row.path, company), targetPath));
|
|
104
|
+
if (covering.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
covering.sort((a, b) => {
|
|
107
|
+
const [aSegs, aExact] = aclPatternSpecificity(toBucketRelative(a.path, company));
|
|
108
|
+
const [bSegs, bExact] = aclPatternSpecificity(toBucketRelative(b.path, company));
|
|
109
|
+
if (aSegs !== bSegs)
|
|
110
|
+
return bSegs - aSegs;
|
|
111
|
+
return bExact - aExact;
|
|
112
|
+
});
|
|
113
|
+
return covering[0] ?? null;
|
|
114
|
+
}
|
|
94
115
|
function httpStatusOf(err) {
|
|
95
116
|
if (!err || typeof err !== "object")
|
|
96
117
|
return undefined;
|
|
@@ -146,10 +167,13 @@ function treeIsEmpty(tree) {
|
|
|
146
167
|
function aclDenied(tree) {
|
|
147
168
|
if (!tree)
|
|
148
169
|
return false;
|
|
170
|
+
const perm = tree.effectivePermission;
|
|
171
|
+
const denied = perm === null || perm === "none" || perm === "deny";
|
|
172
|
+
if (tree.coveringAclPath && denied)
|
|
173
|
+
return true;
|
|
149
174
|
if (treeIsEmpty(tree))
|
|
150
175
|
return false;
|
|
151
|
-
|
|
152
|
-
return perm === null || perm === "none" || perm === "deny";
|
|
176
|
+
return denied;
|
|
153
177
|
}
|
|
154
178
|
/**
|
|
155
179
|
* Build the result and record one cause-mix telemetry line. Every exit path
|
|
@@ -289,6 +313,18 @@ export async function runAccess(input) {
|
|
|
289
313
|
}
|
|
290
314
|
else if (fuzzy.length === 0) {
|
|
291
315
|
steps.push("list: 0 hits");
|
|
316
|
+
const tree = await deps.fetchAclTree(relative);
|
|
317
|
+
if (aclDenied(tree)) {
|
|
318
|
+
steps.push("acl: deny");
|
|
319
|
+
const result = finish("no-access", {
|
|
320
|
+
path: anchored(relative) || input.target,
|
|
321
|
+
company: slug,
|
|
322
|
+
exists: true,
|
|
323
|
+
steps,
|
|
324
|
+
}, finishCtx);
|
|
325
|
+
emit(input, result, log);
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
292
328
|
const result = finish("never-existed", {
|
|
293
329
|
path: anchored(relative) || input.target,
|
|
294
330
|
company: slug,
|
|
@@ -632,7 +668,7 @@ async function promptPick(candidates) {
|
|
|
632
668
|
function treeFromBody(body) {
|
|
633
669
|
if (!body)
|
|
634
670
|
return null;
|
|
635
|
-
if (treeIsEmpty(body))
|
|
671
|
+
if (treeIsEmpty(body) && !body.coveringAclPath)
|
|
636
672
|
return null;
|
|
637
673
|
return body;
|
|
638
674
|
}
|
|
@@ -730,12 +766,25 @@ export async function runAccessForPerson(rawPath, personUid, opts) {
|
|
|
730
766
|
}
|
|
731
767
|
const roleBypass = projection.files.roleBypass === true;
|
|
732
768
|
const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
769
|
+
const locks = Array.isArray(projection.files.locks) ? projection.files.locks : [];
|
|
770
|
+
const coveringRows = [];
|
|
771
|
+
for (const grant of grants) {
|
|
772
|
+
if (typeof grant.path === "string" &&
|
|
773
|
+
typeof grant.permission === "string" &&
|
|
774
|
+
grant.permission in FILE_PERMISSION_RANK) {
|
|
775
|
+
coveringRows.push({
|
|
776
|
+
path: grant.path,
|
|
777
|
+
permission: grant.permission,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const lock of locks) {
|
|
782
|
+
if (typeof lock.path === "string") {
|
|
783
|
+
coveringRows.push({ path: lock.path, permission: null });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const winner = pickMostSpecificCoveringRow(coveringRows, relativePath, company);
|
|
787
|
+
const permission = roleBypass ? "admin" : (winner?.permission ?? null);
|
|
739
788
|
return {
|
|
740
789
|
personUid: uid,
|
|
741
790
|
company,
|
|
@@ -5,11 +5,15 @@ import type { LaneRecord } from "../lib/lanes/types.js";
|
|
|
5
5
|
* alive, not finished. The watcher still writes `done` when the phase's worker
|
|
6
6
|
* exits (the controller reads that as the phase-complete signal); this moves
|
|
7
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`.
|
|
9
|
-
* phase resumes the lane from `running` (`loopIdle`), so it is not written
|
|
10
|
-
* `done` again while the loop is active.
|
|
8
|
+
* before its first phase. Only a stop makes a loop lane `done`.
|
|
11
9
|
*/
|
|
12
10
|
export declare function settleLoopLaneIdle(hqRoot: string, laneId: string): LaneRecord;
|
|
11
|
+
/**
|
|
12
|
+
* The next phase resumes the provider session, which `hq lanes resume` only
|
|
13
|
+
* accepts from a resumable state. An idle loop lane sits in `running` with no
|
|
14
|
+
* worker, so it is handed to resume as the `done` lane the phase left.
|
|
15
|
+
*/
|
|
16
|
+
export declare function prepareIdleLoopLaneForResume(hqRoot: string, laneId: string): LaneRecord;
|
|
13
17
|
export interface LoopControllerDeps {
|
|
14
18
|
resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
|
|
15
19
|
interruptLane(laneId: string, reason: string, options: {
|
|
@@ -20,19 +20,28 @@ function loopRunDir(hqRoot, laneId) {
|
|
|
20
20
|
* alive, not finished. The watcher still writes `done` when the phase's worker
|
|
21
21
|
* exits (the controller reads that as the phase-complete signal); this moves
|
|
22
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`.
|
|
24
|
-
* phase resumes the lane from `running` (`loopIdle`), so it is not written
|
|
25
|
-
* `done` again while the loop is active.
|
|
23
|
+
* before its first phase. Only a stop makes a loop lane `done`.
|
|
26
24
|
*/
|
|
27
25
|
export function settleLoopLaneIdle(hqRoot, laneId) {
|
|
26
|
+
return casLoopLaneState(hqRoot, laneId, "done", "running");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The next phase resumes the provider session, which `hq lanes resume` only
|
|
30
|
+
* accepts from a resumable state. An idle loop lane sits in `running` with no
|
|
31
|
+
* worker, so it is handed to resume as the `done` lane the phase left.
|
|
32
|
+
*/
|
|
33
|
+
export function prepareIdleLoopLaneForResume(hqRoot, laneId) {
|
|
34
|
+
return casLoopLaneState(hqRoot, laneId, "running", "done");
|
|
35
|
+
}
|
|
36
|
+
function casLoopLaneState(hqRoot, laneId, from, to) {
|
|
28
37
|
for (let attempt = 0;; attempt += 1) {
|
|
29
38
|
const current = readLane(hqRoot, laneId);
|
|
30
|
-
if (current.loop_mode !== true || current.state !==
|
|
39
|
+
if (current.loop_mode !== true || current.state !== from)
|
|
31
40
|
return current;
|
|
32
41
|
try {
|
|
33
42
|
return updateLane(hqRoot, laneId, current.version, "command", (record) => ({
|
|
34
43
|
...record,
|
|
35
|
-
state:
|
|
44
|
+
state: to,
|
|
36
45
|
terminal_reason: undefined,
|
|
37
46
|
terminal_details: undefined,
|
|
38
47
|
}));
|
|
@@ -179,6 +188,8 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
179
188
|
}));
|
|
180
189
|
}
|
|
181
190
|
else {
|
|
191
|
+
if (current.state === "running")
|
|
192
|
+
current = prepareIdleLoopLaneForResume(hqRoot, laneId);
|
|
182
193
|
await deps.resumeLane(laneId, {
|
|
183
194
|
text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
|
|
184
195
|
project: current.project_id,
|
|
@@ -186,7 +197,6 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
186
197
|
freshThread: fresh,
|
|
187
198
|
...(sessionId ? { loopSessionId: sessionId } : {}),
|
|
188
199
|
...(repairError !== undefined ? { loopHandoffRepair: true } : {}),
|
|
189
|
-
...(current.state === "running" ? { loopIdle: true } : {}),
|
|
190
200
|
workingDirectory: worktree,
|
|
191
201
|
});
|
|
192
202
|
}
|
|
@@ -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,
|
|
3
|
+
import { appendCappedHistory, isFailedQuotaKeepAliveLane, 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,7 +164,6 @@ 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);
|
|
168
167
|
if (opts.loopHandoffRepair === true && !loopHandoffRepair) {
|
|
169
168
|
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 });
|
|
170
169
|
}
|
|
@@ -251,11 +250,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
251
250
|
}
|
|
252
251
|
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
253
252
|
? null
|
|
254
|
-
: resumeCommandRefusal(lane, {
|
|
255
|
-
allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
256
|
-
allowFresh: opts.freshThread === true,
|
|
257
|
-
allowLoopIdle: loopIdle,
|
|
258
|
-
});
|
|
253
|
+
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane), allowFresh: opts.freshThread === true });
|
|
259
254
|
if (refusal !== null) {
|
|
260
255
|
throw new LanesError(refusal.error, refusal.message, {
|
|
261
256
|
lane_id: laneId,
|
|
@@ -265,10 +260,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
265
260
|
});
|
|
266
261
|
}
|
|
267
262
|
assertLaneRunsHereForAdmission(ctx, lane);
|
|
268
|
-
|
|
269
|
-
const resumedFrom = loopIdle
|
|
270
|
-
? "done"
|
|
271
|
-
: lane.state;
|
|
263
|
+
const resumedFrom = lane.state;
|
|
272
264
|
const text = opts.text?.trim() ? opts.text : undefined;
|
|
273
265
|
const at = ctx.now().toISOString();
|
|
274
266
|
const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
|
|
@@ -450,7 +442,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
450
442
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
451
443
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
452
444
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
453
|
-
...(loopIdle ? { loopIdle: true } : {}),
|
|
454
445
|
pending: {
|
|
455
446
|
trigger: "resume",
|
|
456
447
|
resumed_from: resumedFrom,
|
|
@@ -487,7 +478,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
487
478
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
488
479
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
489
480
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
490
|
-
...(loopIdle ? { loopIdle: true } : {}),
|
|
491
481
|
pending: {
|
|
492
482
|
trigger: "resume",
|
|
493
483
|
resumed_from: resumedFrom,
|
|
@@ -519,7 +509,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
519
509
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
520
510
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
521
511
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
522
|
-
...(loopIdle ? { loopIdle: true } : {}),
|
|
523
512
|
pending: {
|
|
524
513
|
trigger: "resume",
|
|
525
514
|
resumed_from: resumedFrom,
|
|
@@ -654,7 +643,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
654
643
|
kind: "resume",
|
|
655
644
|
from: resumedFrom,
|
|
656
645
|
...(opts.freshThread === true ? { fresh: true } : {}),
|
|
657
|
-
...(loopIdle ? { loopIdle: true } : {}),
|
|
658
646
|
...(resumedFrom === "awaiting_input" && (pendingQuestions.length === 0 || loopHandoffRepair) ? { allowAwaitingInput: true } : {}),
|
|
659
647
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
660
648
|
...(failedResumeExpectedSenior !== undefined
|
|
@@ -83,11 +83,6 @@ 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;
|
|
91
86
|
meshCompany?: string;
|
|
92
87
|
profileRepinContext?: string;
|
|
93
88
|
grantContext?: string;
|
|
@@ -126,8 +121,6 @@ export interface PrepareAccountMoveInput {
|
|
|
126
121
|
refreshProfileSettings?: boolean;
|
|
127
122
|
/** Provenance recorded for the account this move selects. */
|
|
128
123
|
accountSource?: "cli" | "balanced" | "profile";
|
|
129
|
-
/** The loop controller's next-phase resume of a loop lane idle in `running`. */
|
|
130
|
-
loopIdle?: boolean;
|
|
131
124
|
}
|
|
132
125
|
export interface RecordResumeSetupInput {
|
|
133
126
|
profile?: ResolvedResumeProfile;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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,
|
|
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";
|
|
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";
|
|
@@ -7418,9 +7418,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
7418
7418
|
const now = ctx.now();
|
|
7419
7419
|
const laneId = lane.lane_id;
|
|
7420
7420
|
assertLaneRunsHereForAdmission(ctx, lane);
|
|
7421
|
-
|
|
7422
|
-
// session can move before the next phase starts.
|
|
7423
|
-
if (lane.state === "running" && !isLoopIdleResumeLane(lane, input.loopIdle)) {
|
|
7421
|
+
if (lane.state === "running") {
|
|
7424
7422
|
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
7425
7423
|
lane_id: laneId,
|
|
7426
7424
|
state: lane.state,
|
|
@@ -68,12 +68,7 @@ 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"
|
|
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;
|
|
71
|
+
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details">;
|
|
77
72
|
export declare function isFailedQuotaKeepAliveLane(lane: DeliveryLane): boolean;
|
|
78
73
|
/**
|
|
79
74
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
@@ -152,12 +147,6 @@ export type LaunchTrigger = {
|
|
|
152
147
|
/** Loop phases may deliberately start a fresh session at a story boundary. */
|
|
153
148
|
fresh?: true;
|
|
154
149
|
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;
|
|
161
150
|
fromFailed?: true;
|
|
162
151
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
163
152
|
expectedSenior?: SeniorRef;
|
|
@@ -178,7 +167,6 @@ export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
|
178
167
|
allowFailed?: boolean;
|
|
179
168
|
allowAwaitingInput?: boolean;
|
|
180
169
|
allowFresh?: boolean;
|
|
181
|
-
allowLoopIdle?: boolean;
|
|
182
170
|
}): {
|
|
183
171
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
184
172
|
message: string;
|
|
@@ -87,13 +87,6 @@ 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
|
-
}
|
|
97
90
|
/** Only the terminal result of a failed automatic envelope repair is recoverable. */
|
|
98
91
|
function isFailedEnvelopeRepairLane(lane) {
|
|
99
92
|
return (lane.state === "failed" &&
|
|
@@ -363,7 +356,6 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
|
|
|
363
356
|
*/
|
|
364
357
|
export function resumeCommandRefusal(lane, options = {}) {
|
|
365
358
|
if (!isResumeCommandState(lane.state) &&
|
|
366
|
-
!isLoopIdleResumeLane(lane, options.allowLoopIdle) &&
|
|
367
359
|
!(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
|
|
368
360
|
!isFailedEnvelopeRepairLane(lane) &&
|
|
369
361
|
!isFailedQuotaKeepAliveLane(lane) &&
|
|
@@ -422,7 +414,6 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
422
414
|
allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
423
415
|
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
424
416
|
allowFresh: trigger.fresh === true,
|
|
425
|
-
allowLoopIdle: trigger.loopIdle === true,
|
|
426
417
|
});
|
|
427
418
|
if (trigger.fresh === true && refusal === null)
|
|
428
419
|
return { mode: "fresh" };
|
|
@@ -455,8 +446,6 @@ export function claimableStatesFor(trigger, lane) {
|
|
|
455
446
|
const states = trigger.fromFailed === true || envelopeRepairFailed || (lane !== undefined && isFailedQuotaKeepAliveLane(lane))
|
|
456
447
|
? [...RESUME_COMMAND_STATES, "failed"]
|
|
457
448
|
: [...RESUME_COMMAND_STATES];
|
|
458
|
-
if (lane !== undefined && isLoopIdleResumeLane(lane, trigger.loopIdle))
|
|
459
|
-
states.push("running");
|
|
460
449
|
return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
|
|
461
450
|
}
|
|
462
451
|
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,
|
|
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";
|
|
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,
|
|
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";
|
|
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/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,
|
|
26
|
+
import { claimableStatesFor, 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,17 +2143,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2143
2143
|
? { provider_home_at_spawn: previousSpawn.provider_home_at_spawn }
|
|
2144
2144
|
: {}),
|
|
2145
2145
|
};
|
|
2146
|
-
|
|
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") {
|
|
2146
|
+
if (existing.state === "running") {
|
|
2157
2147
|
if (launch.trigger?.kind === "resume") {
|
|
2158
2148
|
throw new LanesError("lane_not_resumable", `Lane ${lane.lane_id} is already running`, { lane_id: lane.lane_id, state: existing.state });
|
|
2159
2149
|
}
|
|
@@ -2653,7 +2643,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2653
2643
|
let priorStartRecorded = false;
|
|
2654
2644
|
const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
2655
2645
|
claimed = false;
|
|
2656
|
-
if (current.state === "running" && current.account === resolved.id
|
|
2646
|
+
if (current.state === "running" && current.account === resolved.id) {
|
|
2657
2647
|
if (trigger.kind === "resume") {
|
|
2658
2648
|
throw new LanesError("lane_not_resumable", `Lane ${current.lane_id} is already running`, { lane_id: current.lane_id, state: current.state });
|
|
2659
2649
|
}
|