@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 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)"
@@ -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" || envelope.story_complete === true)
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
- if (envelope.story_complete === true) {
1570
- if (hasKeepAliveProgress(lane)) {
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" || latest.envelope.story_complete === true)
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
- if (currentEnvelope.envelope.story_complete === true) {
1730
- if (hasKeepAliveProgress(current)) {
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,
@@ -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. */
@@ -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"],
@@ -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 === "done" &&
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.229.1",
3
+ "version": "5.229.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {