@indigoai-us/hq-cli 5.278.0 → 5.279.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 +9 -0
- package/dist/commands/lanes.js +97 -7
- package/dist/lib/lanes/reconcile.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +148 -3
- package/dist/lib/lanes/types.d.ts +3 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.279.0] — 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Claude keep-alive resumes use the same account selection as manual resumes,
|
|
10
|
+
including profile pins and the optional recent-use tiebreak. If no eligible
|
|
11
|
+
account can be selected, the lane resumes on its current account and records
|
|
12
|
+
the failure.
|
|
13
|
+
|
|
5
14
|
## [5.278.0] — 2026-09-29
|
|
6
15
|
|
|
7
16
|
### Changed
|
package/dist/commands/lanes.js
CHANGED
|
@@ -5534,12 +5534,12 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5534
5534
|
const at = now.toISOString();
|
|
5535
5535
|
const from = lane.account ?? null;
|
|
5536
5536
|
// The capacity decision and the record write run under the target
|
|
5537
|
-
// account's admission lock,
|
|
5538
|
-
// slot is taken by a running lane or
|
|
5539
|
-
// pointed at
|
|
5540
|
-
// (`pending_resume` set by this write
|
|
5541
|
-
// into one free slot cannot both start.
|
|
5542
|
-
|
|
5537
|
+
// account's admission lock, or while reconcile holds all provider
|
|
5538
|
+
// admission locks. A slot is taken by a running lane or a lane another move
|
|
5539
|
+
// has pointed at its target account but whose spawn has not claimed it yet
|
|
5540
|
+
// (`pending_resume` is set by this write and cleared by the claim), so two
|
|
5541
|
+
// moves into one free slot cannot both start.
|
|
5542
|
+
const writeMove = async () => {
|
|
5543
5543
|
const admission = input.deferTurnStart
|
|
5544
5544
|
? undefined
|
|
5545
5545
|
: await assessLaneAdmission(ctx, lane, {
|
|
@@ -5602,7 +5602,10 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5602
5602
|
return next;
|
|
5603
5603
|
}), input.grantContext);
|
|
5604
5604
|
return { lane: moved, from, to: target, copy, queued };
|
|
5605
|
-
}
|
|
5605
|
+
};
|
|
5606
|
+
return input.admissionLocksHeld
|
|
5607
|
+
? writeMove()
|
|
5608
|
+
: withLaneAdmissionLock(hqRoot, lane.provider, writeMove, ctx.sleep);
|
|
5606
5609
|
}
|
|
5607
5610
|
async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, options = {}) {
|
|
5608
5611
|
if (lane.provider !== "claude")
|
|
@@ -6993,6 +6996,93 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
6993
6996
|
recoverQueuedMessages,
|
|
6994
6997
|
mesh: ctx.mesh,
|
|
6995
6998
|
env: ctx.env,
|
|
6999
|
+
prepareKeepAliveClaudeResume: async (lane) => {
|
|
7000
|
+
const recoveredLane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
7001
|
+
const pendingModel = recoveredLane.pending_settings?.model;
|
|
7002
|
+
const selectionLane = pendingModel === undefined
|
|
7003
|
+
? recoveredLane
|
|
7004
|
+
: { ...recoveredLane, model: pendingModel, model_source: undefined };
|
|
7005
|
+
let automatic;
|
|
7006
|
+
try {
|
|
7007
|
+
automatic = await selectAutomaticClaudeResumeAccount(ctx, selectionLane, undefined, {
|
|
7008
|
+
repairUnavailableAccount: true,
|
|
7009
|
+
});
|
|
7010
|
+
}
|
|
7011
|
+
catch (err) {
|
|
7012
|
+
return {
|
|
7013
|
+
lane: recoveredLane,
|
|
7014
|
+
selectionFailure: err instanceof LanesError
|
|
7015
|
+
? `${err.error}: ${err.message}`
|
|
7016
|
+
: err instanceof Error
|
|
7017
|
+
? err.message
|
|
7018
|
+
: String(err),
|
|
7019
|
+
};
|
|
7020
|
+
}
|
|
7021
|
+
if (automatic === undefined)
|
|
7022
|
+
return { lane: recoveredLane };
|
|
7023
|
+
const { selection } = automatic;
|
|
7024
|
+
if (opts.dryRun) {
|
|
7025
|
+
return {
|
|
7026
|
+
lane: {
|
|
7027
|
+
...selectionLane,
|
|
7028
|
+
account: selection.account,
|
|
7029
|
+
account_selection: selection,
|
|
7030
|
+
...(pendingModel !== undefined
|
|
7031
|
+
? { model: pendingModel, model_source: "cli" }
|
|
7032
|
+
: automatic.preservedModel !== undefined
|
|
7033
|
+
? { model: automatic.preservedModel, model_source: "preserved" }
|
|
7034
|
+
: {}),
|
|
7035
|
+
},
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
7038
|
+
let latest;
|
|
7039
|
+
try {
|
|
7040
|
+
latest = readLane(hqRoot, lane.lane_id);
|
|
7041
|
+
}
|
|
7042
|
+
catch (err) {
|
|
7043
|
+
throw new LanesError("keep_alive_account_selection_lane_unreadable", `Cannot re-read lane ${lane.lane_id} before applying Claude account selection: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id });
|
|
7044
|
+
}
|
|
7045
|
+
if (latest.version !== lane.version ||
|
|
7046
|
+
latest.state !== lane.state ||
|
|
7047
|
+
latest.keep_alive !== true ||
|
|
7048
|
+
latest.keep_alive_opted_in_at !== lane.keep_alive_opted_in_at ||
|
|
7049
|
+
latest.provider_session_id !== lane.provider_session_id ||
|
|
7050
|
+
latest.last_resume?.session_id !== lane.last_resume?.session_id)
|
|
7051
|
+
return { lane: latest };
|
|
7052
|
+
if (selection.account === lane.account) {
|
|
7053
|
+
return { lane: persistClaudeAccountSelection(hqRoot, recoveredLane, selection) };
|
|
7054
|
+
}
|
|
7055
|
+
try {
|
|
7056
|
+
const moved = await prepareAccountMove(ctx, recoveredLane, {
|
|
7057
|
+
account: selection.account,
|
|
7058
|
+
cliModel: pendingModel,
|
|
7059
|
+
accountHistoryReason: "keep_alive_resume",
|
|
7060
|
+
...(pendingModel === undefined && automatic.preservedModel !== undefined
|
|
7061
|
+
? { preservedModel: automatic.preservedModel }
|
|
7062
|
+
: {}),
|
|
7063
|
+
deferTurnStart: true,
|
|
7064
|
+
admissionLocksHeld: true,
|
|
7065
|
+
accountSelection: selection,
|
|
7066
|
+
});
|
|
7067
|
+
return { lane: moved.lane };
|
|
7068
|
+
}
|
|
7069
|
+
catch (err) {
|
|
7070
|
+
let latest;
|
|
7071
|
+
try {
|
|
7072
|
+
latest = readLane(hqRoot, lane.lane_id);
|
|
7073
|
+
}
|
|
7074
|
+
catch (readErr) {
|
|
7075
|
+
return {
|
|
7076
|
+
lane,
|
|
7077
|
+
selectionFailure: `account move failed: ${err instanceof Error ? err.message : String(err)}; lane re-read failed: ${readErr instanceof Error ? readErr.message : String(readErr)}`,
|
|
7078
|
+
};
|
|
7079
|
+
}
|
|
7080
|
+
return {
|
|
7081
|
+
lane: latest,
|
|
7082
|
+
selectionFailure: `account move failed: ${err instanceof LanesError ? `${err.error}: ${err.message}` : err instanceof Error ? err.message : String(err)}`,
|
|
7083
|
+
};
|
|
7084
|
+
}
|
|
7085
|
+
},
|
|
6996
7086
|
});
|
|
6997
7087
|
printJson(summary);
|
|
6998
7088
|
if (!summary.ok) {
|
|
@@ -92,6 +92,11 @@ export interface ReconcileDeps {
|
|
|
92
92
|
meshEmitTimeoutMs?: number;
|
|
93
93
|
/** Fresh keep-alive eligibility read; injected to exercise opt-in changes before queueing. */
|
|
94
94
|
readKeepAliveRecheckLane?: (hqRoot: string, laneId: string) => LaneRecord;
|
|
95
|
+
/** Reuse the command's Claude resume selection and session-move path for keep-alive. */
|
|
96
|
+
prepareKeepAliveClaudeResume?: (lane: LaneRecord) => Promise<{
|
|
97
|
+
lane: LaneRecord;
|
|
98
|
+
selectionFailure?: string;
|
|
99
|
+
}>;
|
|
95
100
|
/** Process-table snapshot seam for the opt-in runaway-search guard. */
|
|
96
101
|
runawayToolProcessLister?: () => readonly RunawayProcessInfo[];
|
|
97
102
|
/** Test seam: prevent real process signals while exercising the guard. */
|
|
@@ -1001,7 +1001,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1001
1001
|
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
1002
1002
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
1003
1003
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
1004
|
-
resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, startDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany);
|
|
1004
|
+
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, startDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany);
|
|
1005
1005
|
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
1006
1006
|
await admitQueued(hqRoot, drainLanes, summary, startDeps, now);
|
|
1007
1007
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, startDeps);
|
|
@@ -1723,7 +1723,7 @@ function latestEnvelopeMtimeMs(hqRoot, lane, ref) {
|
|
|
1723
1723
|
throw new LanesError("keep_alive_envelope_timestamp_unavailable", `Cannot stat latest envelope for lane ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id, path: filePath });
|
|
1724
1724
|
}
|
|
1725
1725
|
}
|
|
1726
|
-
function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany) {
|
|
1726
|
+
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany) {
|
|
1727
1727
|
const nowMs = now.getTime();
|
|
1728
1728
|
const nowIso = now.toISOString();
|
|
1729
1729
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -2108,6 +2108,44 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
2108
2108
|
});
|
|
2109
2109
|
continue;
|
|
2110
2110
|
}
|
|
2111
|
+
const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
|
|
2112
|
+
if (seniorId !== undefined) {
|
|
2113
|
+
let grantPreflight;
|
|
2114
|
+
try {
|
|
2115
|
+
grantPreflight = withHireGrantAdmission(hqRoot, seniorId, (snapshot) => {
|
|
2116
|
+
const latestLane = readLane(hqRoot, current.lane_id);
|
|
2117
|
+
if (latestLane.version !== current.version ||
|
|
2118
|
+
latestLane.state !== current.state ||
|
|
2119
|
+
latestLane.keep_alive !== true ||
|
|
2120
|
+
latestLane.keep_alive_opted_in_at !== current.keep_alive_opted_in_at ||
|
|
2121
|
+
latestLane.senior.kind !== "lane" ||
|
|
2122
|
+
latestLane.senior.id !== seniorId)
|
|
2123
|
+
return { kind: "stale" };
|
|
2124
|
+
const refusal = terminalChildResumeHireGrantError(latestLane, snapshot);
|
|
2125
|
+
return refusal === null
|
|
2126
|
+
? { kind: "eligible" }
|
|
2127
|
+
: { kind: "refused", error: refusal.error, message: refusal.message };
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
catch (err) {
|
|
2131
|
+
markDirty(summary, {
|
|
2132
|
+
kind: "lane",
|
|
2133
|
+
id: current.lane_id,
|
|
2134
|
+
why: `keep-alive hire admission preflight failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2135
|
+
});
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
if (grantPreflight.kind === "stale")
|
|
2139
|
+
continue;
|
|
2140
|
+
if (grantPreflight.kind === "refused") {
|
|
2141
|
+
summary.skipped.push({
|
|
2142
|
+
kind: "lane",
|
|
2143
|
+
id: current.lane_id,
|
|
2144
|
+
why: `keep_alive_hire_admission: ${grantPreflight.error}: ${grantPreflight.message}`,
|
|
2145
|
+
});
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2111
2149
|
const resumedFrom = current.state === "done" ? "done" : "interrupted";
|
|
2112
2150
|
const resumeText = resumedFrom === "interrupted"
|
|
2113
2151
|
? LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT
|
|
@@ -2118,6 +2156,114 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
2118
2156
|
if (carriesAnsweredQuestions && !deps.dryRun) {
|
|
2119
2157
|
writeAnsweredQuestions(hqRoot, current, questions);
|
|
2120
2158
|
}
|
|
2159
|
+
let accountSelectionFailure;
|
|
2160
|
+
if (current.provider === "claude" && deps.prepareKeepAliveClaudeResume !== undefined) {
|
|
2161
|
+
const optInBeforePreparation = current.keep_alive_opted_in_at;
|
|
2162
|
+
let preparedLane;
|
|
2163
|
+
try {
|
|
2164
|
+
const prepared = await deps.prepareKeepAliveClaudeResume(current);
|
|
2165
|
+
preparedLane = prepared.lane;
|
|
2166
|
+
accountSelectionFailure = prepared.selectionFailure;
|
|
2167
|
+
}
|
|
2168
|
+
catch (err) {
|
|
2169
|
+
// Account balancing is best-effort for keep-alive. A failure to read
|
|
2170
|
+
// balance inputs must not prevent the existing session from resuming.
|
|
2171
|
+
try {
|
|
2172
|
+
preparedLane = readLane(hqRoot, current.lane_id);
|
|
2173
|
+
}
|
|
2174
|
+
catch (readErr) {
|
|
2175
|
+
markDirty(summary, {
|
|
2176
|
+
kind: "lane",
|
|
2177
|
+
id: current.lane_id,
|
|
2178
|
+
why: `keep-alive account selection failed and lane could not be re-read: ${readErr instanceof Error ? readErr.message : String(readErr)}`,
|
|
2179
|
+
});
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
accountSelectionFailure = err instanceof Error ? err.message : String(err);
|
|
2183
|
+
}
|
|
2184
|
+
const preparedSessionId = preparedLane.provider_session_id ?? preparedLane.last_resume?.session_id;
|
|
2185
|
+
if (preparedLane.state !== initialState ||
|
|
2186
|
+
preparedLane.keep_alive !== true ||
|
|
2187
|
+
!laneRunsHere(preparedLane, deps.host) ||
|
|
2188
|
+
preparedSessionId !== currentSessionId)
|
|
2189
|
+
continue;
|
|
2190
|
+
let latest;
|
|
2191
|
+
try {
|
|
2192
|
+
latest = deps.dryRun
|
|
2193
|
+
? (deps.readKeepAliveRecheckLane?.(hqRoot, current.lane_id) ?? readLane(hqRoot, current.lane_id))
|
|
2194
|
+
: readLane(hqRoot, current.lane_id);
|
|
2195
|
+
}
|
|
2196
|
+
catch (err) {
|
|
2197
|
+
markDirty(summary, {
|
|
2198
|
+
kind: "lane",
|
|
2199
|
+
id: current.lane_id,
|
|
2200
|
+
why: `keep-alive eligibility recheck failed after account selection: ${err instanceof Error ? err.message : String(err)}`,
|
|
2201
|
+
});
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
if (latest.state !== initialState || !laneRunsHere(latest, deps.host))
|
|
2205
|
+
continue;
|
|
2206
|
+
if (latest.keep_alive !== true) {
|
|
2207
|
+
recordKeepAliveSkip(hqRoot, latest, "not_opted_in", summary, deps);
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
if (latest.keep_alive_opted_in_at !== optInBeforePreparation) {
|
|
2211
|
+
summary.skipped.push({
|
|
2212
|
+
kind: "lane",
|
|
2213
|
+
id: latest.lane_id,
|
|
2214
|
+
why: "keep_alive_eligibility_changed_during_account_selection",
|
|
2215
|
+
});
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const latestSessionId = latest.provider_session_id ?? latest.last_resume?.session_id;
|
|
2219
|
+
if (latestSessionId !== currentSessionId)
|
|
2220
|
+
continue;
|
|
2221
|
+
let latestEligibilityAtMs;
|
|
2222
|
+
if (initialState === "done") {
|
|
2223
|
+
const envelope = readLatestEnvelopeRecord(hqRoot, latest.lane_id);
|
|
2224
|
+
if (!envelope || envelope.envelope.decision !== "done")
|
|
2225
|
+
continue;
|
|
2226
|
+
if (doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, latest, questions)) !== "eligible")
|
|
2227
|
+
continue;
|
|
2228
|
+
try {
|
|
2229
|
+
latestEligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, latest, envelope.ref);
|
|
2230
|
+
}
|
|
2231
|
+
catch (err) {
|
|
2232
|
+
markDirty(summary, {
|
|
2233
|
+
kind: "lane",
|
|
2234
|
+
id: latest.lane_id,
|
|
2235
|
+
why: `keep-alive envelope timestamp unavailable after account selection: ${err instanceof Error ? err.message : String(err)}`,
|
|
2236
|
+
});
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
else {
|
|
2241
|
+
latestEligibilityAtMs = Date.parse(latest.interrupted_at ?? "");
|
|
2242
|
+
}
|
|
2243
|
+
const latestOptedInAtMs = Date.parse(latest.keep_alive_opted_in_at ?? "");
|
|
2244
|
+
if (!Number.isFinite(latestEligibilityAtMs) || !Number.isFinite(latestOptedInAtMs))
|
|
2245
|
+
continue;
|
|
2246
|
+
if (latestEligibilityAtMs <= latestOptedInAtMs) {
|
|
2247
|
+
recordKeepAliveSkip(hqRoot, latest, initialState === "done" ? "envelope_before_opt_in" : "interruption_before_opt_in", summary, deps);
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
current = deps.dryRun ? preparedLane : latest;
|
|
2251
|
+
}
|
|
2252
|
+
if (accountSelectionFailure !== undefined) {
|
|
2253
|
+
summary.skipped.push({
|
|
2254
|
+
kind: "lane",
|
|
2255
|
+
id: current.lane_id,
|
|
2256
|
+
why: `keep_alive_account_selection_failed: ${accountSelectionFailure}`,
|
|
2257
|
+
});
|
|
2258
|
+
if (!deps.dryRun) {
|
|
2259
|
+
recordKeepAliveEvent(hqRoot, current.lane_id, summary, {
|
|
2260
|
+
event: "keep_alive_account_selection_failed",
|
|
2261
|
+
at: nowIso,
|
|
2262
|
+
account: current.account ?? null,
|
|
2263
|
+
reason: accountSelectionFailure,
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2121
2267
|
const queue = (expectedSeniorId) => casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
2122
2268
|
latestLane.state === current.state &&
|
|
2123
2269
|
latestLane.keep_alive === true &&
|
|
@@ -2145,7 +2291,6 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
2145
2291
|
keep_alive_exhausted_at: undefined,
|
|
2146
2292
|
keep_alive_blocked_reason: undefined,
|
|
2147
2293
|
}), deps.dryRun);
|
|
2148
|
-
const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
|
|
2149
2294
|
let queued;
|
|
2150
2295
|
if (seniorId) {
|
|
2151
2296
|
try {
|
|
@@ -500,9 +500,8 @@ export interface LaneRecord {
|
|
|
500
500
|
/** Eligible accounts for a queue entry created without an explicit account. */
|
|
501
501
|
admission_accounts?: string[];
|
|
502
502
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* like the other histories.
|
|
503
|
+
* Moves of the lane's session to another account of the same provider,
|
|
504
|
+
* newest last and capped like the other histories.
|
|
506
505
|
*/
|
|
507
506
|
account_history?: AccountHistoryEntry[];
|
|
508
507
|
/** Bounded audit history for explicitly authorized resumes from failed. */
|
|
@@ -665,7 +664,7 @@ export interface AccountHistoryEntry {
|
|
|
665
664
|
from: string | null;
|
|
666
665
|
to: string;
|
|
667
666
|
at: string;
|
|
668
|
-
reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "operator_move";
|
|
667
|
+
reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "keep_alive_resume" | "operator_move";
|
|
669
668
|
}
|
|
670
669
|
export interface FailedResumeHistoryEntry {
|
|
671
670
|
at: string;
|