@indigoai-us/hq-cli 5.229.1 → 5.230.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 +20 -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/checkpoint-ontology/apply.js +3 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +11 -0
- package/dist/lib/checkpoint-ontology/plan.js +11 -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,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.230.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Behind the off-by-default `checkpoint.ontology.no-link-aliases` flag, a
|
|
10
|
+
checkpoint ontology candidate that links to an existing entity no longer adds
|
|
11
|
+
aliases to that entity's file. The plan records `link_aliases: false` and
|
|
12
|
+
apply reports the link without writing. New entities keep their aliases.
|
|
13
|
+
Most aliases written through links were other names the text mentioned next
|
|
14
|
+
to the entity, such as `Publisher` on `indigo`.
|
|
15
|
+
|
|
16
|
+
## [5.229.2] — 2026-09-27
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Standing keep-alive leads now wait after done turns for drop-box messages,
|
|
21
|
+
undelivered answers, or pending resumes; queued answers are included in the
|
|
22
|
+
next turn.
|
|
23
|
+
- An unreadable question record no longer aborts reconciliation of other lanes.
|
|
24
|
+
|
|
5
25
|
## [5.229.1] — 2026-09-27
|
|
6
26
|
|
|
7
27
|
### 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);
|
|
@@ -286,6 +286,9 @@ export function applyOntologyRun(options) {
|
|
|
286
286
|
report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: [resolved.target, ...resolved.duplicates] });
|
|
287
287
|
continue;
|
|
288
288
|
}
|
|
289
|
+
// checkpoint.ontology.no-link-aliases: the link is reported and the entity file is left as it is.
|
|
290
|
+
if (plan.link_aliases === false)
|
|
291
|
+
continue;
|
|
289
292
|
const pending = updates.get(resolved.target);
|
|
290
293
|
if (!pending && updates.size >= CAPS.updates) {
|
|
291
294
|
note(report.deferred, "cap_updates");
|
|
@@ -39,6 +39,15 @@ export declare const MULTI_WRITER_FLAG = "checkpoint.ontology.multi-writer";
|
|
|
39
39
|
* runs were model errors, 5 of them duplicates of an existing entity.
|
|
40
40
|
*/
|
|
41
41
|
export declare const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
|
|
42
|
+
/**
|
|
43
|
+
* A candidate that links to an existing entity adds no aliases to it; the plan
|
|
44
|
+
* carries `link_aliases: false` and apply honours it. CO-42: most aliases
|
|
45
|
+
* written through links on the CO-41 runs were other things the text named
|
|
46
|
+
* next to the entity (`Publisher` on indigo, `Stripe Connect` on Stripe), and
|
|
47
|
+
* entity-hints multiplied them (22 aliases on a flag-off run, 45 and 125 on two
|
|
48
|
+
* hinted runs).
|
|
49
|
+
*/
|
|
50
|
+
export declare const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
|
|
42
51
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
43
52
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
44
53
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -105,6 +114,8 @@ export interface OntologyPlan {
|
|
|
105
114
|
};
|
|
106
115
|
/** Present only when `checkpoint.ontology.multi-writer` was on for this plan. */
|
|
107
116
|
multi_writer?: MultiWriterStart;
|
|
117
|
+
/** Present, as false, only when `checkpoint.ontology.no-link-aliases` was on for this plan. */
|
|
118
|
+
link_aliases?: false;
|
|
108
119
|
inputs: PlanInput[];
|
|
109
120
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
110
121
|
session_text: string | null;
|
|
@@ -43,6 +43,15 @@ export const MULTI_WRITER_FLAG = "checkpoint.ontology.multi-writer";
|
|
|
43
43
|
* runs were model errors, 5 of them duplicates of an existing entity.
|
|
44
44
|
*/
|
|
45
45
|
export const ENTITY_HINTS_FLAG = "checkpoint.ontology.entity-hints";
|
|
46
|
+
/**
|
|
47
|
+
* A candidate that links to an existing entity adds no aliases to it; the plan
|
|
48
|
+
* carries `link_aliases: false` and apply honours it. CO-42: most aliases
|
|
49
|
+
* written through links on the CO-41 runs were other things the text named
|
|
50
|
+
* next to the entity (`Publisher` on indigo, `Stripe Connect` on Stripe), and
|
|
51
|
+
* entity-hints multiplied them (22 aliases on a flag-off run, 45 and 125 on two
|
|
52
|
+
* hinted runs).
|
|
53
|
+
*/
|
|
54
|
+
export const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
|
|
46
55
|
export const PLAN_FILE = "ontology-plan.json";
|
|
47
56
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
48
57
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -219,6 +228,7 @@ export async function planOntologyRun(options) {
|
|
|
219
228
|
recordBackendPreference(hqRoot, company, await options.readFlag(GROK_BACKEND_FLAG, company));
|
|
220
229
|
const multiWriterOn = await options.readFlag(MULTI_WRITER_FLAG, company);
|
|
221
230
|
const hintsOn = await options.readFlag(ENTITY_HINTS_FLAG, company);
|
|
231
|
+
const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
|
|
222
232
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
223
233
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
224
234
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -331,6 +341,7 @@ export async function planOntologyRun(options) {
|
|
|
331
341
|
}
|
|
332
342
|
: {}),
|
|
333
343
|
...(multiWriter ? { multi_writer: multiWriter } : {}),
|
|
344
|
+
...(noLinkAliases ? { link_aliases: false } : {}),
|
|
334
345
|
inputs,
|
|
335
346
|
session_text: sessionText,
|
|
336
347
|
pending_after_run: remaining,
|
|
@@ -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
|
}
|