@indigoai-us/hq-cli 5.229.1 → 5.229.2
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/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/lanes.js +6 -0
- package/dist/lib/lanes/keep-alive.d.ts +16 -1
- package/dist/lib/lanes/keep-alive.js +51 -0
- package/dist/lib/lanes/reconcile.js +25 -11
- package/dist/lib/lanes/store.js +7 -0
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watch.js +4 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.229.2] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Standing keep-alive leads now wait after done turns for drop-box messages,
|
|
10
|
+
undelivered answers, or pending resumes; queued answers are included in the
|
|
11
|
+
next turn.
|
|
12
|
+
- An unreadable question record no longer aborts reconciliation of other lanes.
|
|
13
|
+
|
|
5
14
|
## [5.229.1] — 2026-09-27
|
|
6
15
|
|
|
7
16
|
### Fixed
|
|
@@ -7541,6 +7541,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7541
7541
|
}, {
|
|
7542
7542
|
readonly flags: "--off";
|
|
7543
7543
|
readonly description: "Disable keep-alive for this lane";
|
|
7544
|
+
}, {
|
|
7545
|
+
readonly flags: "--standing";
|
|
7546
|
+
readonly description: "Wait for queued work after every done turn; requires --on";
|
|
7544
7547
|
}, {
|
|
7545
7548
|
readonly flags: "--json";
|
|
7546
7549
|
readonly description: "Emit JSON on stdout (default)";
|
|
@@ -9750,6 +9750,10 @@ export const COMMAND_CATALOG = [
|
|
|
9750
9750
|
"flags": "--off",
|
|
9751
9751
|
"description": "Disable keep-alive for this lane"
|
|
9752
9752
|
},
|
|
9753
|
+
{
|
|
9754
|
+
"flags": "--standing",
|
|
9755
|
+
"description": "Wait for queued work after every done turn; requires --on"
|
|
9756
|
+
},
|
|
9753
9757
|
{
|
|
9754
9758
|
"flags": "--json",
|
|
9755
9759
|
"description": "Emit JSON on stdout (default)"
|
package/dist/commands/lanes.js
CHANGED
|
@@ -4975,6 +4975,7 @@ async function runShow(ctx, laneId) {
|
|
|
4975
4975
|
keep_alive_state: {
|
|
4976
4976
|
enabled: keepAliveState.enabled,
|
|
4977
4977
|
source: keepAliveState.source,
|
|
4978
|
+
standing: storedLane.keep_alive_standing === true,
|
|
4978
4979
|
opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
|
|
4979
4980
|
blocked_reason: storedLane.keep_alive_blocked_reason ??
|
|
4980
4981
|
(storedLane.keep_alive === true ? null : "not_opted_in"),
|
|
@@ -5104,6 +5105,9 @@ function printGrantResult(hqRoot, updated, hierarchy) {
|
|
|
5104
5105
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
5105
5106
|
}
|
|
5106
5107
|
async function runKeepAlive(ctx, laneId, opts) {
|
|
5108
|
+
if (opts.standing && !opts.on) {
|
|
5109
|
+
throw new LanesError("keep_alive_standing_requires_on", "Pass --standing together with --on", { lane_id: laneId });
|
|
5110
|
+
}
|
|
5107
5111
|
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
5108
5112
|
throw new LanesError("keep_alive_mode_invalid", "Pass exactly one of --on or --off", { lane_id: laneId });
|
|
5109
5113
|
}
|
|
@@ -5131,6 +5135,7 @@ async function runKeepAlive(ctx, laneId, opts) {
|
|
|
5131
5135
|
return updateLane(hqRoot, laneId, lane.version, "command", (current) => ({
|
|
5132
5136
|
...current,
|
|
5133
5137
|
keep_alive: Boolean(opts.on),
|
|
5138
|
+
keep_alive_standing: Boolean(opts.on && opts.standing),
|
|
5134
5139
|
keep_alive_opted_in_at: optedInAt,
|
|
5135
5140
|
keep_alive_blocked_reason: undefined,
|
|
5136
5141
|
}));
|
|
@@ -9329,6 +9334,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9329
9334
|
.argument("<lane-id>", "Lane id")
|
|
9330
9335
|
.option("--on", "Enable explicit keep-alive for this lane")
|
|
9331
9336
|
.option("--off", "Disable keep-alive for this lane")
|
|
9337
|
+
.option("--standing", "Wait for queued work after every done turn; requires --on")
|
|
9332
9338
|
.option("--json", "Emit JSON on stdout (default)")
|
|
9333
9339
|
.action(wrap(async (laneId, opts) => {
|
|
9334
9340
|
await runKeepAlive(ctx, laneId, opts);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneRecord } from "./types.js";
|
|
1
|
+
import type { LaneRecord, QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
3
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
4
4
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
@@ -7,6 +7,21 @@ export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
|
7
7
|
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.";
|
|
8
8
|
export declare 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.";
|
|
9
9
|
export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
|
|
10
|
+
/** The latest terminal envelope fields used by both watch and reconcile. */
|
|
11
|
+
export interface KeepAliveDoneEnvelope {
|
|
12
|
+
decision?: string;
|
|
13
|
+
story_complete?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Shared done-state policy: ordinary opted-in lanes resume open stories;
|
|
17
|
+
* standing leads wait for queued work, even when each turn marks the story
|
|
18
|
+
* complete. Watch and reconcile both use this decision.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
|
|
21
|
+
/** Work signals already persisted for a lane that ended its turn. */
|
|
22
|
+
export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
|
|
23
|
+
/** Undelivered answers that belong to this lane and company. */
|
|
24
|
+
export declare function undeliveredAnsweredQuestions(lane: Pick<LaneRecord, "lane_id" | "company">, questions: readonly QuestionRecord[]): QuestionRecord[];
|
|
10
25
|
export declare function retiredLaneRegistryPath(hqRoot: string): string;
|
|
11
26
|
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
12
27
|
export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
+
import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
5
|
+
import { listQuestionIds, readQuestion } from "./store.js";
|
|
4
6
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
5
7
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
6
8
|
export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
@@ -8,6 +10,55 @@ export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
|
8
10
|
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
9
11
|
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.";
|
|
10
12
|
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.";
|
|
13
|
+
/**
|
|
14
|
+
* Shared done-state policy: ordinary opted-in lanes resume open stories;
|
|
15
|
+
* standing leads wait for queued work, even when each turn marks the story
|
|
16
|
+
* complete. Watch and reconcile both use this decision.
|
|
17
|
+
*/
|
|
18
|
+
export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
|
|
19
|
+
if (envelope.decision !== "done")
|
|
20
|
+
return false;
|
|
21
|
+
return lane.keep_alive_standing === true
|
|
22
|
+
? hasQueuedWork
|
|
23
|
+
: envelope.story_complete !== true;
|
|
24
|
+
}
|
|
25
|
+
/** Work signals already persisted for a lane that ended its turn. */
|
|
26
|
+
export function hasPendingKeepAliveWork(hqRoot, lane, questionRecords) {
|
|
27
|
+
if (lane.pending_resume !== undefined)
|
|
28
|
+
return true;
|
|
29
|
+
if (dropboxInboxHasMessages(hqRoot, lane.lane_id))
|
|
30
|
+
return true;
|
|
31
|
+
const questions = questionRecords ?? readKeepAliveQuestionRecords(hqRoot);
|
|
32
|
+
return undeliveredAnsweredQuestions(lane, questions).length > 0;
|
|
33
|
+
}
|
|
34
|
+
/** Undelivered answers that belong to this lane and company. */
|
|
35
|
+
export function undeliveredAnsweredQuestions(lane, questions) {
|
|
36
|
+
return questions.filter((question) => {
|
|
37
|
+
const sameCompany = question.company === null
|
|
38
|
+
? lane.company === null
|
|
39
|
+
: lane.company !== null &&
|
|
40
|
+
question.company.uid === lane.company.uid &&
|
|
41
|
+
question.company.slug === lane.company.slug;
|
|
42
|
+
return (question.lane_id === lane.lane_id &&
|
|
43
|
+
question.origin.id === lane.lane_id &&
|
|
44
|
+
sameCompany &&
|
|
45
|
+
question.status === "answered" &&
|
|
46
|
+
question.answer !== undefined &&
|
|
47
|
+
(question.delivered_at === null || question.delivered_at === undefined));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function readKeepAliveQuestionRecords(hqRoot) {
|
|
51
|
+
const questions = [];
|
|
52
|
+
for (const questionId of listQuestionIds(hqRoot)) {
|
|
53
|
+
try {
|
|
54
|
+
questions.push(readQuestion(hqRoot, questionId));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
process.stderr.write(`hq lanes keep-alive: skipped unreadable question ${questionId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return questions;
|
|
61
|
+
}
|
|
11
62
|
export function retiredLaneRegistryPath(hqRoot) {
|
|
12
63
|
return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
|
|
13
64
|
}
|
|
@@ -21,7 +21,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
|
|
|
21
21
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
22
22
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
23
23
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
24
|
-
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, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, } from "./keep-alive.js";
|
|
24
|
+
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, canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
25
25
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
26
26
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
27
27
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -818,7 +818,10 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
818
818
|
continue;
|
|
819
819
|
if (lane.state === "done") {
|
|
820
820
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
821
|
-
if (!envelope || envelope.decision !== "done"
|
|
821
|
+
if (!envelope || envelope.decision !== "done")
|
|
822
|
+
continue;
|
|
823
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
824
|
+
if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork))
|
|
822
825
|
continue;
|
|
823
826
|
}
|
|
824
827
|
const key = keepAliveCompanyKey(lane.company);
|
|
@@ -834,9 +837,9 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
834
837
|
}));
|
|
835
838
|
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
836
839
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
837
|
-
resumeKeepAlive(hqRoot, freshLanes, summary, deps, now, keepAliveFlagByCompany);
|
|
838
|
-
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
839
840
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
841
|
+
resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, deps, now, keepAliveFlagByCompany);
|
|
842
|
+
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
840
843
|
await admitQueued(hqRoot, drainLanes, summary, deps, now);
|
|
841
844
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
|
|
842
845
|
});
|
|
@@ -1532,7 +1535,7 @@ function latestEnvelopeMtimeMs(hqRoot, lane, ref) {
|
|
|
1532
1535
|
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 });
|
|
1533
1536
|
}
|
|
1534
1537
|
}
|
|
1535
|
-
function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
1538
|
+
function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany) {
|
|
1536
1539
|
const nowMs = now.getTime();
|
|
1537
1540
|
const nowIso = now.toISOString();
|
|
1538
1541
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -1566,8 +1569,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1566
1569
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
1567
1570
|
if (!envelope || envelope.decision !== "done")
|
|
1568
1571
|
continue;
|
|
1569
|
-
|
|
1570
|
-
|
|
1572
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1573
|
+
if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork)) {
|
|
1574
|
+
if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
1571
1575
|
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
|
|
1572
1576
|
current.state === "done" &&
|
|
1573
1577
|
current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
@@ -1652,7 +1656,10 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1652
1656
|
let eligibilityAtMs;
|
|
1653
1657
|
if (initialState === "done") {
|
|
1654
1658
|
const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
|
|
1655
|
-
if (!latest || latest.envelope.decision !== "done"
|
|
1659
|
+
if (!latest || latest.envelope.decision !== "done")
|
|
1660
|
+
continue;
|
|
1661
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1662
|
+
if (!canResumeDoneKeepAliveLane(lane, latest.envelope, hasQueuedWork))
|
|
1656
1663
|
continue;
|
|
1657
1664
|
try {
|
|
1658
1665
|
eligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, lane, latest.ref);
|
|
@@ -1726,8 +1733,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1726
1733
|
const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
|
|
1727
1734
|
if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
|
|
1728
1735
|
continue;
|
|
1729
|
-
|
|
1730
|
-
|
|
1736
|
+
const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
|
|
1737
|
+
if (!canResumeDoneKeepAliveLane(current, currentEnvelope.envelope, hasQueuedWork)) {
|
|
1738
|
+
if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
|
|
1731
1739
|
casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
1732
1740
|
latestLane.state === "done" &&
|
|
1733
1741
|
latestLane.keep_alive === true &&
|
|
@@ -1840,6 +1848,12 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1840
1848
|
const resumeText = resumedFrom === "interrupted"
|
|
1841
1849
|
? LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT
|
|
1842
1850
|
: LEAD_LANE_KEEP_ALIVE_RESUME_TEXT;
|
|
1851
|
+
const carriesAnsweredQuestions = resumedFrom === "done" &&
|
|
1852
|
+
current.keep_alive_standing === true &&
|
|
1853
|
+
undeliveredAnsweredQuestions(current, questions).length > 0;
|
|
1854
|
+
if (carriesAnsweredQuestions && !deps.dryRun) {
|
|
1855
|
+
writeAnsweredQuestions(hqRoot, current, questions);
|
|
1856
|
+
}
|
|
1843
1857
|
const queue = (expectedSeniorId) => casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
1844
1858
|
latestLane.state === current.state &&
|
|
1845
1859
|
latestLane.keep_alive === true &&
|
|
@@ -1855,7 +1869,7 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1855
1869
|
terminal_details: undefined,
|
|
1856
1870
|
pending_resume: {
|
|
1857
1871
|
session_id: currentSessionId,
|
|
1858
|
-
trigger: "resume",
|
|
1872
|
+
trigger: carriesAnsweredQuestions ? "answered_questions" : "resume",
|
|
1859
1873
|
resumed_from: resumedFrom,
|
|
1860
1874
|
text: resumeText,
|
|
1861
1875
|
compact: true,
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -409,6 +409,13 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
409
409
|
value: raw.keep_alive,
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
|
+
if (raw.keep_alive_standing !== undefined && typeof raw.keep_alive_standing !== "boolean") {
|
|
413
|
+
throw new LaneSchemaError("keep_alive_standing must be a boolean", {
|
|
414
|
+
path: filePath,
|
|
415
|
+
field: "keep_alive_standing",
|
|
416
|
+
value: raw.keep_alive_standing,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
412
419
|
const keepAliveBlockedReason = raw.keep_alive_blocked_reason;
|
|
413
420
|
const validKeepAliveBlockedReasons = [
|
|
414
421
|
"not_opted_in",
|
|
@@ -364,6 +364,8 @@ export interface LaneRecord {
|
|
|
364
364
|
worker_profile?: string;
|
|
365
365
|
/** Whether this lead lane should resume completed turns while its story is open. */
|
|
366
366
|
keep_alive?: boolean;
|
|
367
|
+
/** Whether a keep-alive lead waits for queued work after every done turn. */
|
|
368
|
+
keep_alive_standing?: boolean;
|
|
367
369
|
/** Time this lane most recently explicitly opted into keep-alive. */
|
|
368
370
|
keep_alive_opted_in_at?: string;
|
|
369
371
|
/** Why reconcile declined to resume this lane most recently. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -224,7 +224,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
224
224
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
225
225
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
226
226
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
227
|
-
keep_alive: ["creator", "command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
227
|
+
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
228
228
|
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
229
229
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
230
230
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
2
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
3
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
4
|
-
import { LEAD_LANE_KEEP_ALIVE_MAX_RESUMES } from "./keep-alive.js";
|
|
4
|
+
import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
|
|
5
5
|
import { isTerminalLaneState, } from "./types.js";
|
|
6
6
|
export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
|
|
7
7
|
export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
|
|
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
|
|
|
32
32
|
}
|
|
33
33
|
return unique;
|
|
34
34
|
}
|
|
35
|
-
function canResumeKeepAliveLane(lane) {
|
|
35
|
+
function canResumeKeepAliveLane(hqRoot, lane) {
|
|
36
36
|
if (lane.keep_alive !== true ||
|
|
37
37
|
lane.keep_alive_opted_in_at === undefined ||
|
|
38
38
|
!Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
|
|
@@ -49,8 +49,7 @@ function canResumeKeepAliveLane(lane) {
|
|
|
49
49
|
return lane.terminal_reason === "exited_with_partial_output";
|
|
50
50
|
}
|
|
51
51
|
if (lane.state === "done") {
|
|
52
|
-
return lane.last_envelope?.decision ===
|
|
53
|
-
lane.story_complete !== true &&
|
|
52
|
+
return canResumeDoneKeepAliveLane(lane, { decision: lane.last_envelope?.decision, story_complete: lane.story_complete }, lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane)) &&
|
|
54
53
|
lane.terminal_reason !== "provider_rejected" &&
|
|
55
54
|
lane.terminal_reason !== "account_not_logged_in";
|
|
56
55
|
}
|
|
@@ -133,7 +132,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
133
132
|
if (deps.keepAliveEnabled !== undefined) {
|
|
134
133
|
for (const record of records) {
|
|
135
134
|
if (isTerminalLaneState(record.state) &&
|
|
136
|
-
canResumeKeepAliveLane(record) &&
|
|
135
|
+
canResumeKeepAliveLane(hqRoot, record) &&
|
|
137
136
|
await deps.keepAliveEnabled(record))
|
|
138
137
|
keepAliveWaiting.add(record.lane_id);
|
|
139
138
|
}
|