@indigoai-us/hq-cli 5.204.0 → 5.206.0
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 +25 -0
- package/dist/command-catalog.generated.d.ts +6 -0
- package/dist/command-catalog.generated.js +8 -0
- package/dist/commands/lanes.js +53 -25
- package/dist/lib/lanes/admission.js +6 -0
- package/dist/lib/lanes/hire-grants.d.ts +4 -0
- package/dist/lib/lanes/hire-grants.js +62 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +8 -0
- package/dist/lib/lanes/keep-alive.js +14 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/dist/lib/lanes/profile.d.ts +1 -0
- package/dist/lib/lanes/profile.js +8 -0
- package/dist/lib/lanes/provider-rejection.d.ts +1 -1
- package/dist/lib/lanes/provider-rejection.js +15 -5
- package/dist/lib/lanes/reconcile.d.ts +3 -0
- package/dist/lib/lanes/reconcile.js +273 -4
- package/dist/lib/lanes/store.js +25 -0
- package/dist/lib/lanes/types.d.ts +10 -0
- package/dist/lib/lanes/types.js +1 -0
- package/dist/startup-registration.js +2 -0
- package/dist/utils/install-tree-torn.d.ts +22 -7
- package/dist/utils/install-tree-torn.js +108 -20
- package/dist/utils/sentry-fingerprint.js +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.206.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Startup recovery no longer waits the full install deadline when an unchanged retired hq-cli package remains beside the install. Node 24 ESM loader file-read `ENOENT` errors also enter the existing recovery path and receive a report tag.
|
|
10
|
+
|
|
11
|
+
## [5.205.0] — 2026-09-26
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `hq lanes create` and `hq lanes resume` accept `--keep-alive`. When the
|
|
16
|
+
default-off `lanes.lead-keep-alive` flag is enabled, an opted-in lead lane
|
|
17
|
+
resumes after a `done` turn that leaves its story open. The first resume
|
|
18
|
+
waits at least five minutes; later attempts back off to an hour and stop
|
|
19
|
+
after 24 consecutive attempts.
|
|
20
|
+
`hq lanes show --json` reports the counter and next eligible time.
|
|
21
|
+
- Worker profiles accept boolean `keep_alive`. Add this key only after the
|
|
22
|
+
hq-cli release with this change is installed; older CLIs reject unknown
|
|
23
|
+
profile metadata.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Claude result text that says `Not logged in` or `Please run /login` now marks
|
|
28
|
+
a lane failed with `account_not_logged_in` when no envelope was written.
|
|
29
|
+
|
|
5
30
|
## [5.204.0] — 2026-09-26
|
|
6
31
|
|
|
7
32
|
### Added
|
|
@@ -7345,6 +7345,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7345
7345
|
}, {
|
|
7346
7346
|
readonly flags: "--hire-grant <n>";
|
|
7347
7347
|
readonly description: "Maximum alive direct child lanes assigned to this lane; set by its senior";
|
|
7348
|
+
}, {
|
|
7349
|
+
readonly flags: "--keep-alive";
|
|
7350
|
+
readonly description: "Automatically resume done lead lanes while their story remains open";
|
|
7348
7351
|
}, {
|
|
7349
7352
|
readonly flags: "--timeout <secs>";
|
|
7350
7353
|
readonly description: "Deadline in seconds (warns, does not kill)";
|
|
@@ -7878,6 +7881,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7878
7881
|
}, {
|
|
7879
7882
|
readonly flags: "--worker <profile>";
|
|
7880
7883
|
readonly description: "Attach a personal worker profile for the resumed turn";
|
|
7884
|
+
}, {
|
|
7885
|
+
readonly flags: "--keep-alive";
|
|
7886
|
+
readonly description: "Automatically resume done lead lanes while their story remains open";
|
|
7881
7887
|
}, {
|
|
7882
7888
|
readonly flags: "--mesh-company <slug>";
|
|
7883
7889
|
readonly description: "Attach Work Mesh attribution after verifying company membership";
|
|
@@ -9494,6 +9494,10 @@ export const COMMAND_CATALOG = [
|
|
|
9494
9494
|
"flags": "--hire-grant <n>",
|
|
9495
9495
|
"description": "Maximum alive direct child lanes assigned to this lane; set by its senior"
|
|
9496
9496
|
},
|
|
9497
|
+
{
|
|
9498
|
+
"flags": "--keep-alive",
|
|
9499
|
+
"description": "Automatically resume done lead lanes while their story remains open"
|
|
9500
|
+
},
|
|
9497
9501
|
{
|
|
9498
9502
|
"flags": "--timeout <secs>",
|
|
9499
9503
|
"description": "Deadline in seconds (warns, does not kill)"
|
|
@@ -10189,6 +10193,10 @@ export const COMMAND_CATALOG = [
|
|
|
10189
10193
|
"flags": "--worker <profile>",
|
|
10190
10194
|
"description": "Attach a personal worker profile for the resumed turn"
|
|
10191
10195
|
},
|
|
10196
|
+
{
|
|
10197
|
+
"flags": "--keep-alive",
|
|
10198
|
+
"description": "Automatically resume done lead lanes while their story remains open"
|
|
10199
|
+
},
|
|
10192
10200
|
{
|
|
10193
10201
|
"flags": "--mesh-company <slug>",
|
|
10194
10202
|
"description": "Attach Work Mesh attribution after verifying company membership"
|
package/dist/commands/lanes.js
CHANGED
|
@@ -34,7 +34,7 @@ import { openMeshTransport } from "../lib/mesh/client.js";
|
|
|
34
34
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
35
35
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
36
36
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
37
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
37
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
38
38
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
39
39
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
40
40
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -864,33 +864,15 @@ async function withResumeHireAdmission(ctx, lane, fn) {
|
|
|
864
864
|
}
|
|
865
865
|
if (!await ctx.laneHireGrantsEnabled(lane.company))
|
|
866
866
|
return fn();
|
|
867
|
-
return withHireGrantAdmission(hqRoot, lane.senior.id, (
|
|
867
|
+
return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
|
|
868
868
|
const current = readLane(hqRoot, lane.lane_id);
|
|
869
869
|
// A different resume already reactivated this lane; it occupies the slot
|
|
870
870
|
// in the shared snapshot and is not a second hire.
|
|
871
871
|
if (!isTerminalLaneState(current.state))
|
|
872
872
|
return fn();
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (children.length >= (parent.hire_grant ?? 0)) {
|
|
877
|
-
throw hireGrantCapacityError(parent, children);
|
|
878
|
-
}
|
|
879
|
-
const requestedGrant = current.hire_grant ?? 0;
|
|
880
|
-
if (requestedGrant > availableChildGrant) {
|
|
881
|
-
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
882
|
-
senior_lane_id: parent.lane_id,
|
|
883
|
-
parent_grant: parentGrant,
|
|
884
|
-
alive_children: children.map((child) => child.lane_id),
|
|
885
|
-
assigned_child_grants: assignedChildHireGrants(children),
|
|
886
|
-
requested_hire_grant: requestedGrant,
|
|
887
|
-
available_hire_grant: availableChildGrant,
|
|
888
|
-
ask_guidance: {
|
|
889
|
-
envelope: "decision=ask; include the desired total grant and the reason",
|
|
890
|
-
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
891
|
-
},
|
|
892
|
-
});
|
|
893
|
-
}
|
|
873
|
+
const refusal = terminalChildResumeHireGrantError(current, snapshot);
|
|
874
|
+
if (refusal !== null)
|
|
875
|
+
throw refusal;
|
|
894
876
|
return fn();
|
|
895
877
|
});
|
|
896
878
|
}
|
|
@@ -2469,6 +2451,11 @@ async function runCreate(ctx, opts) {
|
|
|
2469
2451
|
const profileEffort = profileDefaultsApply ? profile?.meta.effort : undefined;
|
|
2470
2452
|
const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
|
|
2471
2453
|
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
2454
|
+
const keepAlive = opts.keepAlive === true ||
|
|
2455
|
+
(profileDefaultsApply && profile?.meta.keep_alive === true);
|
|
2456
|
+
if (keepAlive && !providerSupportsSessionResume(provider)) {
|
|
2457
|
+
throw new LanesError("keep_alive_provider_unsupported", `Provider ${provider} cannot resume recorded sessions, so keep-alive is unavailable`, { provider });
|
|
2458
|
+
}
|
|
2472
2459
|
const briefAbs = resolveCallerPath(ctx.callerCwd(), briefFile);
|
|
2473
2460
|
if (!ctx.existsSync(briefAbs)) {
|
|
2474
2461
|
throw new LanesError("brief_not_found", `Brief file not found: ${briefAbs}`, {
|
|
@@ -2927,6 +2914,7 @@ async function runCreate(ctx, opts) {
|
|
|
2927
2914
|
worker_profile_ignored: ignoredWorkerProfileFields(provider),
|
|
2928
2915
|
}
|
|
2929
2916
|
: {}),
|
|
2917
|
+
keep_alive: keepAlive,
|
|
2930
2918
|
provider,
|
|
2931
2919
|
account: pinnedAccount,
|
|
2932
2920
|
...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
|
|
@@ -3382,6 +3370,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3382
3370
|
model: options.model,
|
|
3383
3371
|
effort: options.effort,
|
|
3384
3372
|
worker: options.worker,
|
|
3373
|
+
keepAlive: options.keepAlive,
|
|
3385
3374
|
hire: options.hire,
|
|
3386
3375
|
grantContext,
|
|
3387
3376
|
compact: options.compact,
|
|
@@ -4508,6 +4497,13 @@ async function runShow(ctx, laneId) {
|
|
|
4508
4497
|
effort_source: lane.effort_source ?? null,
|
|
4509
4498
|
terminal_reason: lane.terminal_reason ?? null,
|
|
4510
4499
|
terminal_details: lane.terminal_details ?? null,
|
|
4500
|
+
keep_alive_state: {
|
|
4501
|
+
enabled: lane.keep_alive === true,
|
|
4502
|
+
consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
|
|
4503
|
+
last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
|
|
4504
|
+
next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
|
|
4505
|
+
exhausted_at: lane.keep_alive_exhausted_at ?? null,
|
|
4506
|
+
},
|
|
4511
4507
|
last_message_at: lane.last_message_at ?? null,
|
|
4512
4508
|
last_compaction: lane.last_compaction
|
|
4513
4509
|
? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
|
|
@@ -4922,6 +4918,9 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4922
4918
|
next.worker_profile_hash = profile.contentHash;
|
|
4923
4919
|
next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
|
|
4924
4920
|
next.worker_policy_delivery = undefined;
|
|
4921
|
+
if (profile.meta.keep_alive !== undefined) {
|
|
4922
|
+
next.keep_alive = profile.meta.keep_alive;
|
|
4923
|
+
}
|
|
4925
4924
|
if (next.model_source !== "cli") {
|
|
4926
4925
|
if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
|
|
4927
4926
|
next.model = profile.meta.model;
|
|
@@ -4937,6 +4936,8 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4937
4936
|
next.effort = input.effort.effort;
|
|
4938
4937
|
next.effort_source = input.effort.source;
|
|
4939
4938
|
}
|
|
4939
|
+
if (input.keepAlive !== undefined)
|
|
4940
|
+
next.keep_alive = input.keepAlive;
|
|
4940
4941
|
}
|
|
4941
4942
|
function recordResumeSetup(ctx, lane, input) {
|
|
4942
4943
|
if (input.cliModel !== undefined) {
|
|
@@ -4958,6 +4959,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4958
4959
|
}
|
|
4959
4960
|
if (input.profile === undefined &&
|
|
4960
4961
|
input.effort === undefined &&
|
|
4962
|
+
input.keepAlive === undefined &&
|
|
4961
4963
|
input.cliModel === undefined &&
|
|
4962
4964
|
input.meshCompany === undefined)
|
|
4963
4965
|
return lane;
|
|
@@ -4980,6 +4982,23 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4980
4982
|
return next;
|
|
4981
4983
|
});
|
|
4982
4984
|
}
|
|
4985
|
+
function hasKeepAliveProgress(lane) {
|
|
4986
|
+
return (lane.keep_alive_resume_count ?? 0) !== 0 ||
|
|
4987
|
+
lane.keep_alive_last_resume_at !== undefined ||
|
|
4988
|
+
lane.keep_alive_next_eligible_at !== undefined ||
|
|
4989
|
+
lane.keep_alive_exhausted_at !== undefined;
|
|
4990
|
+
}
|
|
4991
|
+
function resetKeepAliveProgress(lane) {
|
|
4992
|
+
if (!hasKeepAliveProgress(lane))
|
|
4993
|
+
return lane;
|
|
4994
|
+
return {
|
|
4995
|
+
...lane,
|
|
4996
|
+
keep_alive_resume_count: 0,
|
|
4997
|
+
keep_alive_last_resume_at: undefined,
|
|
4998
|
+
keep_alive_next_eligible_at: undefined,
|
|
4999
|
+
keep_alive_exhausted_at: undefined,
|
|
5000
|
+
};
|
|
5001
|
+
}
|
|
4983
5002
|
/** Resolve an explicit resume override or newly attached profile like create does. */
|
|
4984
5003
|
function resolveResumeEffort(ctx, lane, input) {
|
|
4985
5004
|
if (input.cliEffort === undefined && input.profile === undefined)
|
|
@@ -5575,7 +5594,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5575
5594
|
}
|
|
5576
5595
|
// Every accepted message moves the compaction reference forward.
|
|
5577
5596
|
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
5578
|
-
...current,
|
|
5597
|
+
...resetKeepAliveProgress(current),
|
|
5579
5598
|
last_message_at: at,
|
|
5580
5599
|
}));
|
|
5581
5600
|
if (projectId !== lane.project_id || storyId !== lane.story_id) {
|
|
@@ -5913,6 +5932,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5913
5932
|
const resumeProfile = opts.worker !== undefined
|
|
5914
5933
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5915
5934
|
: undefined;
|
|
5935
|
+
const keepAlive = opts.keepAlive === true
|
|
5936
|
+
? true
|
|
5937
|
+
: resumeProfile?.profile.meta.keep_alive;
|
|
5916
5938
|
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5917
5939
|
if (keepCurrentAccount)
|
|
5918
5940
|
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
@@ -5970,6 +5992,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5970
5992
|
lane = recordResumeSetup(ctx, lane, {
|
|
5971
5993
|
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5972
5994
|
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5995
|
+
...(keepAlive !== undefined ? { keepAlive } : {}),
|
|
5973
5996
|
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5974
5997
|
...(requestedMeshCompany !== undefined
|
|
5975
5998
|
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
@@ -6065,7 +6088,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6065
6088
|
if (text !== undefined) {
|
|
6066
6089
|
// An operator text moves the compaction reference forward, as a message does.
|
|
6067
6090
|
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
6068
|
-
...current,
|
|
6091
|
+
...resetKeepAliveProgress(current),
|
|
6069
6092
|
last_message_at: at,
|
|
6070
6093
|
}));
|
|
6071
6094
|
}
|
|
@@ -6100,6 +6123,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6100
6123
|
};
|
|
6101
6124
|
});
|
|
6102
6125
|
}
|
|
6126
|
+
if (text === undefined && hasKeepAliveProgress(next)) {
|
|
6127
|
+
next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
|
|
6128
|
+
}
|
|
6103
6129
|
printJson({
|
|
6104
6130
|
ok: true,
|
|
6105
6131
|
action: "resume",
|
|
@@ -8309,6 +8335,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8309
8335
|
.option("--effort <level>", "Effort level; claude: low|medium|high|xhigh|max, codex: minimal|low|medium|high|xhigh|max|ultra, grok: low|medium|high|xhigh")
|
|
8310
8336
|
.option("--senior <ref>", "session:<id> | lane:<id> | user")
|
|
8311
8337
|
.option("--hire-grant <n>", "Maximum alive direct child lanes assigned to this lane; set by its senior")
|
|
8338
|
+
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
8312
8339
|
.option("--timeout <secs>", "Deadline in seconds (warns, does not kill)")
|
|
8313
8340
|
.option("--priority <n>", "Queue priority; higher runs first", "1")
|
|
8314
8341
|
.option("--outpost <id>", "Run on this Outpost instead of the local host")
|
|
@@ -8546,6 +8573,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8546
8573
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
8547
8574
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
8548
8575
|
.option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
|
|
8576
|
+
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
8549
8577
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
8550
8578
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
8551
8579
|
.action(wrap(async (laneId, opts) => {
|
|
@@ -137,6 +137,12 @@ export function hasActivePendingResume(lane, nowMs) {
|
|
|
137
137
|
return false;
|
|
138
138
|
if (lane.state === "queued" && pending.trigger === "answered_questions")
|
|
139
139
|
return false;
|
|
140
|
+
if (lane.state === "queued" &&
|
|
141
|
+
lane.keep_alive === true &&
|
|
142
|
+
pending.trigger === "resume" &&
|
|
143
|
+
pending.resumed_from === "done" &&
|
|
144
|
+
lane.keep_alive_last_resume_at === pending.at)
|
|
145
|
+
return false;
|
|
140
146
|
const reservedAt = Date.parse(pending.at);
|
|
141
147
|
const ageMs = nowMs - reservedAt;
|
|
142
148
|
return Number.isFinite(reservedAt) && ageMs >= 0 && ageMs < ADMISSION_RESERVATION_GRACE_MS;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LaneRecord } from "./types.js";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
2
3
|
export declare const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
3
4
|
export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
|
|
4
5
|
export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
|
|
@@ -9,6 +10,9 @@ export interface HireGrantAdmissionSnapshot {
|
|
|
9
10
|
parentGrant: number;
|
|
10
11
|
availableChildGrant: number;
|
|
11
12
|
}
|
|
13
|
+
/** Shared refusal logic for resuming a terminal lane under an active senior. */
|
|
14
|
+
export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot): LanesError | null;
|
|
15
|
+
export declare function hireGrantCapacityError(parent: LaneRecord, children: readonly LaneRecord[]): LanesError;
|
|
12
16
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
13
17
|
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T): T;
|
|
14
18
|
export interface HireGrantBackfillRow {
|
|
@@ -10,6 +10,68 @@ export function aliveDirectChildren(lanes, parentLaneId) {
|
|
|
10
10
|
export function assignedChildHireGrants(children, exceptLaneId) {
|
|
11
11
|
return children.reduce((sum, child) => child.lane_id === exceptLaneId ? sum : sum + (child.hire_grant ?? 0), 0);
|
|
12
12
|
}
|
|
13
|
+
/** Shared refusal logic for resuming a terminal lane under an active senior. */
|
|
14
|
+
export function terminalChildResumeHireGrantError(current, snapshot) {
|
|
15
|
+
if (!isTerminalLaneState(current.state))
|
|
16
|
+
return null;
|
|
17
|
+
const { parent, children, parentGrant, availableChildGrant } = snapshot;
|
|
18
|
+
if (isTerminalLaneState(parent.state)) {
|
|
19
|
+
return new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot resume child lanes`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
20
|
+
}
|
|
21
|
+
if (children.length >= parentGrant)
|
|
22
|
+
return hireGrantCapacityError(parent, children);
|
|
23
|
+
const requestedGrant = current.hire_grant ?? 0;
|
|
24
|
+
if (requestedGrant > availableChildGrant) {
|
|
25
|
+
return new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
26
|
+
senior_lane_id: parent.lane_id,
|
|
27
|
+
parent_grant: parentGrant,
|
|
28
|
+
alive_children: children.map((child) => child.lane_id),
|
|
29
|
+
assigned_child_grants: assignedChildHireGrants(children),
|
|
30
|
+
requested_hire_grant: requestedGrant,
|
|
31
|
+
available_hire_grant: availableChildGrant,
|
|
32
|
+
ask_guidance: {
|
|
33
|
+
envelope: "decision=ask; include the desired total grant and the reason",
|
|
34
|
+
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function hireGrantCapacityError(parent, children) {
|
|
41
|
+
const grant = parent.hire_grant ?? 0;
|
|
42
|
+
const grantor = (parent.senior.kind === "lane" || parent.senior.kind === "session") &&
|
|
43
|
+
parent.senior.id
|
|
44
|
+
? { kind: parent.senior.kind, id: parent.senior.id }
|
|
45
|
+
: null;
|
|
46
|
+
const grantorReason = grantor
|
|
47
|
+
? undefined
|
|
48
|
+
: parent.senior.kind === "user"
|
|
49
|
+
? "senior_is_user"
|
|
50
|
+
: "parent_senior_missing_id";
|
|
51
|
+
const capacity = children.length === grant
|
|
52
|
+
? `is at its hire grant (${children.length} of ${grant} alive children)`
|
|
53
|
+
: `has exhausted its hire grant (${children.length} alive children; grant ${grant})`;
|
|
54
|
+
const grantCommand = `hq lanes grant ${parent.lane_id} --hire <n>`;
|
|
55
|
+
const authorityGuidance = grantor
|
|
56
|
+
? `Its senior ${grantor.kind} ${grantor.id} can raise it with: ${grantCommand}`
|
|
57
|
+
: parent.senior.kind === "user"
|
|
58
|
+
? `Its senior is the user and can raise it with: ${grantCommand}`
|
|
59
|
+
: `No senior is recorded, so an operator must resolve ownership before running: ${grantCommand}`;
|
|
60
|
+
const message = `Lane ${parent.lane_id} ${capacity}. ${authorityGuidance}`;
|
|
61
|
+
return new LanesError("hire_grant_exhausted", message, {
|
|
62
|
+
senior_lane_id: parent.lane_id,
|
|
63
|
+
parent_grant: grant,
|
|
64
|
+
alive_children: children.map((child) => child.lane_id),
|
|
65
|
+
alive_child_count: children.length,
|
|
66
|
+
grantor,
|
|
67
|
+
...(grantorReason ? { grantor_reason: grantorReason } : {}),
|
|
68
|
+
message,
|
|
69
|
+
ask_guidance: {
|
|
70
|
+
envelope: "decision=ask; include the desired total grant and the reason",
|
|
71
|
+
senior_command: grantCommand,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
13
75
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
14
76
|
export function withHireGrantAdmission(hqRoot, parentLaneId, fn) {
|
|
15
77
|
return withLaneHireLocks(hqRoot, [parentLaneId], () => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission } from "./hire-grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
7
7
|
export type { HireGrantBackfillRow } from "./hire-grants.js";
|
|
8
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
9
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission } from "./hire-grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
7
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
8
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
9
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
2
|
+
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
3
|
+
export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
4
|
+
export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
|
|
5
|
+
export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
6
|
+
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
7
|
+
export declare function leadLaneKeepAliveBackoffMs(attempt: number): number;
|
|
8
|
+
//# sourceMappingURL=keep-alive.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
2
|
+
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
3
|
+
export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
4
|
+
export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
5
|
+
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
6
|
+
export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
7
|
+
export function leadLaneKeepAliveBackoffMs(attempt) {
|
|
8
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
9
|
+
throw new RangeError("keep-alive attempt must be a positive safe integer");
|
|
10
|
+
}
|
|
11
|
+
const exponential = LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS * 2 ** (attempt - 1);
|
|
12
|
+
return Math.min(exponential, LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=keep-alive.js.map
|
|
@@ -154,6 +154,8 @@ export function buildOutpostLaneCommand(input) {
|
|
|
154
154
|
if (value !== undefined)
|
|
155
155
|
args.push(flag, value);
|
|
156
156
|
}
|
|
157
|
+
if (input.action === "resume" && input.keepAlive === true)
|
|
158
|
+
args.push("--keep-alive");
|
|
157
159
|
if (input.compact === false)
|
|
158
160
|
args.push("--no-compact");
|
|
159
161
|
return withRemoteHqDir(joinCommandParts(args));
|
|
@@ -28,6 +28,7 @@ const PROFILE_META_KEYS = new Set([
|
|
|
28
28
|
"max_concurrent",
|
|
29
29
|
"source",
|
|
30
30
|
"accounts",
|
|
31
|
+
"keep_alive",
|
|
31
32
|
]);
|
|
32
33
|
function isPlainObject(value) {
|
|
33
34
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -164,6 +165,12 @@ function parseMeta(id, dir, raw) {
|
|
|
164
165
|
value: raw.fast_mode,
|
|
165
166
|
});
|
|
166
167
|
}
|
|
168
|
+
if (raw.keep_alive !== undefined && typeof raw.keep_alive !== "boolean") {
|
|
169
|
+
throw profileError(id, dir, "meta.yaml keep_alive must be a boolean when present", {
|
|
170
|
+
field: "keep_alive",
|
|
171
|
+
value: raw.keep_alive,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
167
174
|
let version;
|
|
168
175
|
if (raw.version !== undefined) {
|
|
169
176
|
if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
|
|
@@ -211,6 +218,7 @@ function parseMeta(id, dir, raw) {
|
|
|
211
218
|
...(account !== undefined ? { account } : {}),
|
|
212
219
|
...(effort !== undefined ? { effort } : {}),
|
|
213
220
|
...(raw.fast_mode !== undefined ? { fast_mode: raw.fast_mode } : {}),
|
|
221
|
+
...(raw.keep_alive !== undefined ? { keep_alive: raw.keep_alive } : {}),
|
|
214
222
|
...(maxConcurrent !== undefined ? { max_concurrent: maxConcurrent } : {}),
|
|
215
223
|
...(source !== undefined ? { source } : {}),
|
|
216
224
|
...(accounts !== undefined ? { accounts } : {}),
|
|
@@ -35,7 +35,7 @@ export interface ProviderRejection {
|
|
|
35
35
|
/** Derived from the status: a transient class (408, 425, 5xx) is retryable; a 4xx is not. */
|
|
36
36
|
retryable: boolean;
|
|
37
37
|
/** Which event on the stream carried the failure. */
|
|
38
|
-
event: "codex.turn.failed" | "claude.result.is_error" | "grok.error";
|
|
38
|
+
event: "codex.turn.failed" | "claude.result.is_error" | "claude.result.login_failure" | "grok.error";
|
|
39
39
|
}
|
|
40
40
|
export declare function isProviderLoginFailure(failure: ProviderRejection | null): boolean;
|
|
41
41
|
export declare function capMessage(message: string): string;
|
|
@@ -28,7 +28,7 @@ export const QUOTA_STATUS = 429;
|
|
|
28
28
|
function isProviderLoginMessage(provider, message) {
|
|
29
29
|
switch (provider) {
|
|
30
30
|
case "claude":
|
|
31
|
-
return
|
|
31
|
+
return /(?:\bnot logged in\b|\bplease run\s+\/login\b)/i.test(message);
|
|
32
32
|
case "codex":
|
|
33
33
|
return /\bnot logged in\b/i.test(message) ||
|
|
34
34
|
/\bmissing bearer or basic authentication in header\b/i.test(message);
|
|
@@ -40,6 +40,11 @@ function isProviderLoginMessage(provider, message) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** Successful Claude results can contain worker prose, so only accept its short CLI login banner. */
|
|
44
|
+
function isClaudeSuccessLoginMessage(message) {
|
|
45
|
+
const trimmed = message.trim();
|
|
46
|
+
return trimmed.length <= 80 && /^not logged in\b.{0,40}please run \/login\.?$/i.test(trimmed);
|
|
47
|
+
}
|
|
43
48
|
export function isProviderLoginFailure(failure) {
|
|
44
49
|
return failure !== null && isProviderLoginMessage(failure.provider, failure.message);
|
|
45
50
|
}
|
|
@@ -163,11 +168,14 @@ export function parseClaudeResultRejection(text) {
|
|
|
163
168
|
const found = scanJsonLinesBackward(text, (rec) => {
|
|
164
169
|
if (rec.type !== "result")
|
|
165
170
|
return null;
|
|
166
|
-
|
|
171
|
+
const message = typeof rec.result === "string" ? rec.result : "";
|
|
172
|
+
const loginFailure = rec.is_error === true
|
|
173
|
+
? isProviderLoginMessage("claude", message)
|
|
174
|
+
: isClaudeSuccessLoginMessage(message);
|
|
175
|
+
if (rec.is_error !== true && !loginFailure)
|
|
167
176
|
return NOT_REJECTED;
|
|
168
177
|
const status = statusOf(rec.api_error_status);
|
|
169
|
-
|
|
170
|
-
if (status === null && !isProviderLoginMessage("claude", message))
|
|
178
|
+
if (status === null && !loginFailure)
|
|
171
179
|
return NOT_REJECTED;
|
|
172
180
|
return {
|
|
173
181
|
reason: PROVIDER_REJECTED_REASON,
|
|
@@ -176,7 +184,9 @@ export function parseClaudeResultRejection(text) {
|
|
|
176
184
|
status,
|
|
177
185
|
error_type: typeof rec.subtype === "string" ? rec.subtype : null,
|
|
178
186
|
retryable: retryableFromStatus(status),
|
|
179
|
-
event:
|
|
187
|
+
event: loginFailure && rec.is_error !== true
|
|
188
|
+
? "claude.result.login_failure"
|
|
189
|
+
: "claude.result.is_error",
|
|
180
190
|
};
|
|
181
191
|
});
|
|
182
192
|
return found === NOT_REJECTED ? null : found;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { type LivenessDeps } from "./liveness.js";
|
|
9
9
|
import { type EnvelopeRepairSpawnInput, type SpawnLaneDeps } from "./spawn.js";
|
|
10
10
|
import { type ThrottleDeps } from "./throttle.js";
|
|
11
|
+
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
11
12
|
import type { LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
12
13
|
import { type LaneMesh } from "./mesh-events.js";
|
|
13
14
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
@@ -71,6 +72,8 @@ export interface ReconcileDeps {
|
|
|
71
72
|
readSessionSenior?: (sessionId: string) => SeniorRef;
|
|
72
73
|
presenceConnected?: boolean;
|
|
73
74
|
env?: NodeJS.ProcessEnv;
|
|
75
|
+
/** Flag reader seam used to assert rollout reads happen before admission locks. */
|
|
76
|
+
readRegistryFlag?: typeof readLanesRegistryFlag;
|
|
74
77
|
/** Detached mesh seam for dead-watcher recovery. */
|
|
75
78
|
mesh?: LaneMesh | null;
|
|
76
79
|
/** Bound on one terminal mesh emit during dead-host recovery. Default 10 s. */
|