@indigoai-us/hq-cli 5.260.0 → 5.261.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 +8 -0
- package/dist/commands/lanes.js +5 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -5
- package/dist/lib/lanes/keep-alive.js +11 -10
- package/dist/lib/lanes/reconcile.js +27 -9
- package/dist/lib/lanes/types.d.ts +1 -1
- package/dist/lib/lanes/watcher.js +2 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.261.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Standing keep-alive leads now resume stories that remain open. Completed
|
|
10
|
+
stories resume only on standing leads with queued work, and reconcile reports
|
|
11
|
+
why other completed lanes stay idle.
|
|
12
|
+
|
|
5
13
|
## [5.260.0] — 2026-09-28
|
|
6
14
|
|
|
7
15
|
### Added
|
package/dist/commands/lanes.js
CHANGED
|
@@ -50,7 +50,7 @@ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
|
50
50
|
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, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
51
51
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
52
52
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
53
|
-
import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
53
|
+
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
54
54
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
55
55
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
56
56
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -4524,6 +4524,9 @@ async function runShow(ctx, laneId) {
|
|
|
4524
4524
|
: storedLane;
|
|
4525
4525
|
const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
|
|
4526
4526
|
const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
|
|
4527
|
+
const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
|
|
4528
|
+
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, storedLane))
|
|
4529
|
+
: null;
|
|
4527
4530
|
const managerStatusGate = await resolveManagerStatusGateForLane(hqRoot, storedLane, { env: ctx.env });
|
|
4528
4531
|
const managerStatusTarget = managerStatusGate.target;
|
|
4529
4532
|
const blockerFreshness = await resolveBlockerFreshnessForLane(hqRoot, storedLane, {
|
|
@@ -4560,6 +4563,7 @@ async function runShow(ctx, laneId) {
|
|
|
4560
4563
|
enabled: keepAliveState.enabled,
|
|
4561
4564
|
source: keepAliveState.source,
|
|
4562
4565
|
standing: storedLane.keep_alive_standing === true,
|
|
4566
|
+
done_state_verdict: doneStateVerdict,
|
|
4563
4567
|
opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
|
|
4564
4568
|
blocked_reason: storedLane.keep_alive_blocked_reason ??
|
|
4565
4569
|
(storedLane.keep_alive === true ? null : "not_opted_in"),
|
|
@@ -27,11 +27,9 @@ export interface KeepAliveDoneEnvelope {
|
|
|
27
27
|
decision?: string;
|
|
28
28
|
story_complete?: boolean;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* complete. Watch and reconcile both use this decision.
|
|
34
|
-
*/
|
|
30
|
+
export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete";
|
|
31
|
+
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
32
|
+
export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): DoneKeepAliveVerdict | null;
|
|
35
33
|
export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
|
|
36
34
|
/** Work signals already persisted for a lane that ended its turn. */
|
|
37
35
|
export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
|
|
@@ -89,17 +89,18 @@ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText)
|
|
|
89
89
|
}
|
|
90
90
|
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.";
|
|
91
91
|
export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. 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.";
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
* standing leads wait for queued work, even when each turn marks the story
|
|
95
|
-
* complete. Watch and reconcile both use this decision.
|
|
96
|
-
*/
|
|
97
|
-
export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
|
|
92
|
+
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
93
|
+
export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork) {
|
|
98
94
|
if (envelope.decision !== "done")
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
return null;
|
|
96
|
+
if (envelope.story_complete !== true)
|
|
97
|
+
return "eligible";
|
|
98
|
+
if (lane.keep_alive_standing !== true)
|
|
99
|
+
return "story_complete";
|
|
100
|
+
return hasQueuedWork ? "eligible" : "standing_idle";
|
|
101
|
+
}
|
|
102
|
+
export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
|
|
103
|
+
return doneKeepAliveVerdict(lane, envelope, hasQueuedWork) === "eligible";
|
|
103
104
|
}
|
|
104
105
|
/** Work signals already persisted for a lane that ended its turn. */
|
|
105
106
|
export function hasPendingKeepAliveWork(hqRoot, lane, questionRecords) {
|
|
@@ -22,7 +22,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
|
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
25
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG,
|
|
25
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
26
26
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
27
27
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
28
28
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -848,7 +848,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
848
848
|
if (!envelope || envelope.decision !== "done")
|
|
849
849
|
continue;
|
|
850
850
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
851
|
-
if (
|
|
851
|
+
if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
|
|
852
852
|
continue;
|
|
853
853
|
}
|
|
854
854
|
const key = keepAliveCompanyKey(lane.company);
|
|
@@ -1137,10 +1137,8 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1137
1137
|
if (terminalEnvelope && latest) {
|
|
1138
1138
|
const envelope = terminalEnvelope;
|
|
1139
1139
|
next.story_status = envelope.story_status;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
next.story_complete_reason = envelope.story_complete_reason;
|
|
1143
|
-
}
|
|
1140
|
+
next.story_complete = envelope.story_complete;
|
|
1141
|
+
next.story_complete_reason = envelope.story_complete_reason;
|
|
1144
1142
|
const storyStatus = envelope.decision === "blocked"
|
|
1145
1143
|
? "blocked"
|
|
1146
1144
|
: envelope.decision === "done"
|
|
@@ -1550,6 +1548,13 @@ function recordKeepAliveSkip(hqRoot, lane, reason, summary, deps, detail) {
|
|
|
1550
1548
|
});
|
|
1551
1549
|
}
|
|
1552
1550
|
}
|
|
1551
|
+
function recordDoneKeepAlivePolicySkip(summary, lane, verdict) {
|
|
1552
|
+
summary.skipped.push({
|
|
1553
|
+
kind: "lane",
|
|
1554
|
+
id: lane.lane_id,
|
|
1555
|
+
why: `keep_alive_${verdict}`,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1553
1558
|
function readLatestInterruptedClassificationAt(hqRoot, lane) {
|
|
1554
1559
|
const logPath = watcherOutLogPath(hqRoot, lane.lane_id);
|
|
1555
1560
|
let body;
|
|
@@ -1624,7 +1629,10 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1624
1629
|
if (!envelope || envelope.decision !== "done")
|
|
1625
1630
|
continue;
|
|
1626
1631
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1627
|
-
|
|
1632
|
+
const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork);
|
|
1633
|
+
if (verdict !== "eligible") {
|
|
1634
|
+
if (verdict !== null)
|
|
1635
|
+
recordDoneKeepAlivePolicySkip(summary, lane, verdict);
|
|
1628
1636
|
if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
1629
1637
|
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
|
|
1630
1638
|
current.state === "done" &&
|
|
@@ -1713,8 +1721,15 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1713
1721
|
if (!latest || latest.envelope.decision !== "done")
|
|
1714
1722
|
continue;
|
|
1715
1723
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1716
|
-
|
|
1724
|
+
const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork);
|
|
1725
|
+
if (verdict !== "eligible") {
|
|
1726
|
+
if (verdict !== null)
|
|
1727
|
+
recordDoneKeepAlivePolicySkip(summary, lane, verdict);
|
|
1728
|
+
if (latest.envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
1729
|
+
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
1730
|
+
}
|
|
1717
1731
|
continue;
|
|
1732
|
+
}
|
|
1718
1733
|
try {
|
|
1719
1734
|
eligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, lane, latest.ref);
|
|
1720
1735
|
}
|
|
@@ -1788,7 +1803,10 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1788
1803
|
if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
|
|
1789
1804
|
continue;
|
|
1790
1805
|
const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
|
|
1791
|
-
|
|
1806
|
+
const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork);
|
|
1807
|
+
if (verdict !== "eligible") {
|
|
1808
|
+
if (verdict !== null)
|
|
1809
|
+
recordDoneKeepAlivePolicySkip(summary, current, verdict);
|
|
1792
1810
|
if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
|
|
1793
1811
|
casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
1794
1812
|
latestLane.state === "done" &&
|
|
@@ -434,7 +434,7 @@ export interface LaneRecord {
|
|
|
434
434
|
worker_profile?: string;
|
|
435
435
|
/** Whether this lead lane should resume completed turns while its story is open. */
|
|
436
436
|
keep_alive?: boolean;
|
|
437
|
-
/** Whether
|
|
437
|
+
/** Whether completed keep-alive stories wait for queued work after a done turn. */
|
|
438
438
|
keep_alive_standing?: boolean;
|
|
439
439
|
/** Time this lane most recently explicitly opted into keep-alive. */
|
|
440
440
|
keep_alive_opted_in_at?: string;
|
|
@@ -405,10 +405,8 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
|
|
|
405
405
|
...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
|
|
406
406
|
};
|
|
407
407
|
next.story_status = envelope.story_status;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
next.story_complete_reason = envelope.story_complete_reason;
|
|
411
|
-
}
|
|
408
|
+
next.story_complete = envelope.story_complete;
|
|
409
|
+
next.story_complete_reason = envelope.story_complete_reason;
|
|
412
410
|
const currentProject = current.project_id;
|
|
413
411
|
const currentStory = current.story_id;
|
|
414
412
|
const storyStatus = envelope.decision === "blocked"
|