@indigoai-us/hq-cli 5.260.0 → 5.262.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/commands/lanes.js +5 -1
- package/dist/lib/checkpoint-ontology/apply.js +15 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +13 -0
- package/dist/lib/checkpoint-ontology/plan.js +13 -0
- package/dist/lib/doctor/checks/runtime-health.d.ts +2 -0
- package/dist/lib/doctor/checks/runtime-health.js +7 -4
- 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,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.262.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- The `checkpoint.ontology.concept-project-link` flag (off by default) lets
|
|
10
|
+
checkpoint ontology link a knowledge concept whose alias is ambiguous to the
|
|
11
|
+
one project with the same canonical name, such as HQ. It writes nothing.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `hq doctor --live-runtimes` uses a Windows `.cmd` shim only when the sibling file exists.
|
|
16
|
+
|
|
17
|
+
## [5.261.0] — 2026-09-28
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Standing keep-alive leads now resume stories that remain open. Completed
|
|
22
|
+
stories resume only on standing leads with queued work, and reconcile reports
|
|
23
|
+
why other completed lanes stay idle.
|
|
24
|
+
|
|
5
25
|
## [5.260.0] — 2026-09-28
|
|
6
26
|
|
|
7
27
|
### 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"),
|
|
@@ -370,6 +370,21 @@ export function applyOntologyRun(options) {
|
|
|
370
370
|
}
|
|
371
371
|
continue;
|
|
372
372
|
}
|
|
373
|
+
if (resolved.tier === "ambiguous_alias") {
|
|
374
|
+
// checkpoint.ontology.concept-project-link: a knowledge concept whose name is the
|
|
375
|
+
// canonical name of exactly one project links to that project. Nothing is written.
|
|
376
|
+
// Like the tier 1 and 2 links above, it runs before the stopword guard,
|
|
377
|
+
// which only stops new entities.
|
|
378
|
+
const projects = plan.concept_project_link === true && input.kind === "knowledge" && candidate.type === "concept"
|
|
379
|
+
? [...(index.canonical.get(entityKey("project", normalizeEntityName(canonical, "project"))) ?? [])]
|
|
380
|
+
: [];
|
|
381
|
+
if (projects.length === 1) {
|
|
382
|
+
const linked = index.byRel.get(projects[0]);
|
|
383
|
+
report.matched += 1;
|
|
384
|
+
report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: projects[0] });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
373
388
|
// A generic word never becomes a new entity, whatever else it collides with.
|
|
374
389
|
if (isRejectedName(normalizeEntityName(canonical, candidate.type))) {
|
|
375
390
|
note(report.rejected, "stopword");
|
|
@@ -78,6 +78,17 @@ export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-pe
|
|
|
78
78
|
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
79
79
|
*/
|
|
80
80
|
export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
81
|
+
/**
|
|
82
|
+
* A knowledge concept candidate whose name is an alias of more than one
|
|
83
|
+
* entity, and which apply would defer as `ambiguous_alias`, links instead to
|
|
84
|
+
* the one project whose canonical name it is (`HQ` typed as a concept beside
|
|
85
|
+
* `project/HQ` and several concepts aliased `HQ`). The plan carries
|
|
86
|
+
* `concept_project_link: true`. CO-51: over six runs of the shipping config
|
|
87
|
+
* the model typed HQ as a concept in some runs and not others, and the
|
|
88
|
+
* deferral lost 61 gold links; a replay of this rule raised recall from 0.639
|
|
89
|
+
* to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
|
|
90
|
+
*/
|
|
91
|
+
export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
|
|
81
92
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
82
93
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
83
94
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -150,6 +161,8 @@ export interface OntologyPlan {
|
|
|
150
161
|
first_name_people?: true;
|
|
151
162
|
/** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
|
|
152
163
|
skip_ambiguous_concept_links?: true;
|
|
164
|
+
/** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
|
|
165
|
+
concept_project_link?: true;
|
|
153
166
|
inputs: PlanInput[];
|
|
154
167
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
155
168
|
session_text: string | null;
|
|
@@ -82,6 +82,17 @@ export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
|
82
82
|
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
83
83
|
*/
|
|
84
84
|
export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
85
|
+
/**
|
|
86
|
+
* A knowledge concept candidate whose name is an alias of more than one
|
|
87
|
+
* entity, and which apply would defer as `ambiguous_alias`, links instead to
|
|
88
|
+
* the one project whose canonical name it is (`HQ` typed as a concept beside
|
|
89
|
+
* `project/HQ` and several concepts aliased `HQ`). The plan carries
|
|
90
|
+
* `concept_project_link: true`. CO-51: over six runs of the shipping config
|
|
91
|
+
* the model typed HQ as a concept in some runs and not others, and the
|
|
92
|
+
* deferral lost 61 gold links; a replay of this rule raised recall from 0.639
|
|
93
|
+
* to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
|
|
94
|
+
*/
|
|
95
|
+
export const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
|
|
85
96
|
export const PLAN_FILE = "ontology-plan.json";
|
|
86
97
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
87
98
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -265,6 +276,7 @@ export async function planOntologyRun(options) {
|
|
|
265
276
|
const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
|
|
266
277
|
const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
|
|
267
278
|
const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
|
|
279
|
+
const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
|
|
268
280
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
269
281
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
270
282
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -380,6 +392,7 @@ export async function planOntologyRun(options) {
|
|
|
380
392
|
...(noLinkAliases ? { link_aliases: false } : {}),
|
|
381
393
|
...(firstNamePeople ? { first_name_people: true } : {}),
|
|
382
394
|
...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
|
|
395
|
+
...(conceptProjectLink ? { concept_project_link: true } : {}),
|
|
383
396
|
inputs,
|
|
384
397
|
session_text: sessionText,
|
|
385
398
|
pending_after_run: remaining,
|
|
@@ -76,6 +76,8 @@ export interface ProbeOutcome {
|
|
|
76
76
|
export interface RuntimeHealthDeps {
|
|
77
77
|
/** Resolve an executable against PATH; null when not found. */
|
|
78
78
|
resolveBinary?: (binary: string) => string | null;
|
|
79
|
+
/** Check whether a Windows command shim exists next to its resolved binary. */
|
|
80
|
+
fileExists?: (file: string) => boolean;
|
|
79
81
|
/** Execute one probe. The default spawns from the OS temp dir. */
|
|
80
82
|
execProbe?: (file: string, args: string[], timeoutMs: number) => Promise<ProbeOutcome>;
|
|
81
83
|
/** Per-probe timeout. Default {@link DEFAULT_PROBE_TIMEOUT_MS}. */
|
|
@@ -103,7 +103,7 @@ async function runRuntimeChecks(context, deps) {
|
|
|
103
103
|
const resolved = {
|
|
104
104
|
resolveBinary: deps.resolveBinary ?? resolveOnPath,
|
|
105
105
|
execProbe: deps.execProbe ??
|
|
106
|
-
((file, args, timeoutMs) => defaultExecProbe(file, args, timeoutMs, deps.platform ?? process.platform)),
|
|
106
|
+
((file, args, timeoutMs) => defaultExecProbe(file, args, timeoutMs, deps.platform ?? process.platform, deps.fileExists ?? fs.existsSync)),
|
|
107
107
|
timeoutMs: deps.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
108
108
|
live: context.liveRuntimes === true,
|
|
109
109
|
};
|
|
@@ -270,10 +270,13 @@ export function resolveOnPath(binary) {
|
|
|
270
270
|
* CLI that reads piped stdin in non-TTY mode — blocks forever on a pipe that
|
|
271
271
|
* never reaches EOF, which turns every probe into a timeout.
|
|
272
272
|
*/
|
|
273
|
-
function defaultExecProbe(file, args, timeoutMs, platform) {
|
|
273
|
+
function defaultExecProbe(file, args, timeoutMs, platform, fileExists) {
|
|
274
274
|
return new Promise((resolve) => {
|
|
275
|
-
const
|
|
276
|
-
|
|
275
|
+
const cmdSibling = `${file}.cmd`;
|
|
276
|
+
const command = platform === "win32" &&
|
|
277
|
+
!/\.(?:cmd|bat|exe|com)$/i.test(file) &&
|
|
278
|
+
fileExists(cmdSibling)
|
|
279
|
+
? cmdSibling
|
|
277
280
|
: file;
|
|
278
281
|
const plan = planCommandSpawn(command, args, platform);
|
|
279
282
|
const child = spawn(plan.cmd, plan.args, {
|
|
@@ -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"
|