@indigoai-us/hq-cli 5.210.0 → 5.211.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.211.0] — 2026-09-26
6
+
7
+ ### Fixed
8
+
9
+ - Reconcile now applies `keep_alive` from a recorded worker profile to older
10
+ lanes whose lane field is unset. An explicit lane value wins, unreadable
11
+ profiles fail closed and are reported, and `lanes show --json` identifies
12
+ the source of its effective value.
13
+
5
14
  ## [5.210.0] — 2026-09-26
6
15
 
7
16
  ### Fixed
@@ -38,6 +38,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
38
38
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
39
39
  import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
40
40
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
41
+ import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
41
42
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
42
43
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
43
44
  import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
@@ -4489,6 +4490,15 @@ async function runShow(ctx, laneId) {
4489
4490
  ? { ...storedLane, state: "remote", remote_state: storedLane.state, queued_reason: undefined }
4490
4491
  : storedLane;
4491
4492
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
4493
+ let keepAliveState;
4494
+ try {
4495
+ keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
4496
+ }
4497
+ catch (err) {
4498
+ const message = err instanceof Error ? err.message : String(err);
4499
+ process.stderr.write(`hq lanes show: keep-alive profile could not be loaded for ${laneId}: ${message}\n`);
4500
+ keepAliveState = { enabled: false, source: "none" };
4501
+ }
4492
4502
  printJson({
4493
4503
  ok: true,
4494
4504
  action: "show",
@@ -4514,7 +4524,8 @@ async function runShow(ctx, laneId) {
4514
4524
  terminal_reason: lane.terminal_reason ?? null,
4515
4525
  terminal_details: lane.terminal_details ?? null,
4516
4526
  keep_alive_state: {
4517
- enabled: lane.keep_alive === true,
4527
+ enabled: keepAliveState.enabled,
4528
+ source: keepAliveState.source,
4518
4529
  consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
4519
4530
  last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
4520
4531
  next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
@@ -137,8 +137,8 @@ export function hasActivePendingResume(lane, nowMs) {
137
137
  return false;
138
138
  if (lane.state === "queued" && pending.trigger === "answered_questions")
139
139
  return false;
140
+ // This timestamp tags automatic keep-alive resumes, including profile-derived ones.
140
141
  if (lane.state === "queued" &&
141
- lane.keep_alive === true &&
142
142
  pending.trigger === "resume" &&
143
143
  pending.resumed_from === "done" &&
144
144
  lane.keep_alive_last_resume_at === pending.at)
@@ -6,7 +6,7 @@
6
6
  * and package trees; this module describes the provider launch contract that
7
7
  * must be re-read for every spawn.
8
8
  */
9
- import { type LaneProvider } from "./types.js";
9
+ import { type LaneRecord, type LaneProvider } from "./types.js";
10
10
  export interface WorkerProfileMeta {
11
11
  version?: string;
12
12
  provider: LaneProvider;
@@ -37,6 +37,12 @@ export declare function personalWorkerProfileDir(hqRoot: string, id: string): st
37
37
  /** A meta.yaml file is the explicit marker for a personal worker profile. */
38
38
  export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
39
39
  export declare function loadWorkerProfile(hqRoot: string, id: string): WorkerProfile;
40
+ export interface EffectiveLaneKeepAlive {
41
+ enabled: boolean;
42
+ source: "lane" | "profile" | "none";
43
+ }
44
+ /** Resolve the keep-alive opt-in recorded on a lane or its attached profile. */
45
+ export declare function resolveLaneKeepAlive(hqRoot: string, lane: Pick<LaneRecord, "keep_alive" | "worker_profile">): EffectiveLaneKeepAlive;
40
46
  export declare function workerProfilePrompt(profile: WorkerProfile, delivery?: "hook" | "prompt"): string;
41
47
  export declare function ignoredWorkerProfileFields(provider: LaneProvider): string[];
42
48
  /**
@@ -370,6 +370,19 @@ export function loadWorkerProfile(hqRoot, id) {
370
370
  contentHash: hashProfileFiles(dir, hashFiles),
371
371
  };
372
372
  }
373
+ /** Resolve the keep-alive opt-in recorded on a lane or its attached profile. */
374
+ export function resolveLaneKeepAlive(hqRoot, lane) {
375
+ if (lane.keep_alive !== undefined) {
376
+ return { enabled: lane.keep_alive, source: "lane" };
377
+ }
378
+ if (lane.worker_profile === undefined) {
379
+ return { enabled: false, source: "none" };
380
+ }
381
+ const profileKeepAlive = loadWorkerProfile(hqRoot, lane.worker_profile).meta.keep_alive;
382
+ return profileKeepAlive === undefined
383
+ ? { enabled: false, source: "none" }
384
+ : { enabled: profileKeepAlive, source: "profile" };
385
+ }
373
386
  export function workerProfilePrompt(profile, delivery = "prompt") {
374
387
  const parts = [profile.systemPrompt.trimEnd()];
375
388
  if (delivery === "hook")
@@ -15,7 +15,7 @@ import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
15
15
  import { MAX_LANE_DEPTH } from "./constants.js";
16
16
  import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
17
17
  import { LANE_HIRE_GRANTS_FLAG, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
18
- import { loadWorkerProfile } from "./profile.js";
18
+ import { loadWorkerProfile, resolveLaneKeepAlive } from "./profile.js";
19
19
  import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
20
20
  import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
21
21
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
@@ -801,7 +801,8 @@ export async function runPassBody(hqRoot, deps) {
801
801
  const keepAliveHireGrantFlagCompanies = new Map();
802
802
  for (const lane of lanes) {
803
803
  if (lane.state !== "done" ||
804
- lane.keep_alive !== true ||
804
+ (lane.keep_alive !== true &&
805
+ !(lane.keep_alive === undefined && lane.worker_profile !== undefined)) ||
805
806
  !laneRunsHere(lane, deps.host) ||
806
807
  lane.terminal_reason === PROVIDER_REJECTED_REASON ||
807
808
  lane.terminal_reason === "account_not_logged_in" ||
@@ -1417,19 +1418,27 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1417
1418
  const nowIso = now.toISOString();
1418
1419
  const config = loadLanesConfig(hqRoot);
1419
1420
  for (const lane of lanes) {
1421
+ const hasRecordedProfileFallback = lane.keep_alive === undefined && lane.worker_profile !== undefined;
1420
1422
  if (lane.state !== "done" ||
1421
- lane.keep_alive !== true ||
1423
+ lane.keep_alive === false ||
1424
+ (lane.keep_alive !== true && !hasRecordedProfileFallback) ||
1422
1425
  !laneRunsHere(lane, deps.host))
1423
1426
  continue;
1424
1427
  const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
1425
1428
  const envelope = latest?.envelope;
1426
1429
  if (!envelope || envelope.decision !== "done")
1427
1430
  continue;
1431
+ const keepAliveConfigurationUnchanged = (candidate) => lane.keep_alive === true
1432
+ ? candidate.keep_alive === true
1433
+ : lane.keep_alive === undefined &&
1434
+ lane.worker_profile !== undefined &&
1435
+ candidate.keep_alive === undefined &&
1436
+ candidate.worker_profile === lane.worker_profile;
1428
1437
  if (envelope.story_complete === true) {
1429
1438
  if (hasKeepAliveProgress(lane)) {
1430
1439
  casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
1431
1440
  current.state === "done" &&
1432
- current.keep_alive === true, (current) => clearedKeepAliveProgress(current), deps.dryRun);
1441
+ keepAliveConfigurationUnchanged(current), (current) => clearedKeepAliveProgress(current), deps.dryRun);
1433
1442
  }
1434
1443
  continue;
1435
1444
  }
@@ -1440,10 +1449,31 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1440
1449
  continue;
1441
1450
  if (flagByCompany.get(keepAliveCompanyKey(lane.company)) !== true)
1442
1451
  continue;
1452
+ let keepAlive;
1453
+ try {
1454
+ keepAlive = resolveLaneKeepAlive(hqRoot, lane);
1455
+ }
1456
+ catch (err) {
1457
+ const message = err instanceof LanesError
1458
+ ? `${err.error}: ${err.message}`
1459
+ : err instanceof Error ? err.message : String(err);
1460
+ markDirty(summary, {
1461
+ kind: "lane",
1462
+ id: lane.lane_id,
1463
+ why: `keep-alive worker profile could not be loaded: ${message}`,
1464
+ });
1465
+ continue;
1466
+ }
1467
+ if (!keepAlive.enabled)
1468
+ continue;
1469
+ const keepAliveStillEnabled = (candidate) => keepAlive.source === "lane"
1470
+ ? candidate.keep_alive === true
1471
+ : candidate.keep_alive === undefined &&
1472
+ candidate.worker_profile === lane.worker_profile;
1443
1473
  const current = readLane(hqRoot, lane.lane_id);
1444
1474
  if (current.version !== lane.version ||
1445
1475
  current.state !== "done" ||
1446
- current.keep_alive !== true ||
1476
+ !keepAliveStillEnabled(current) ||
1447
1477
  !laneRunsHere(current, deps.host))
1448
1478
  continue;
1449
1479
  const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id)?.envelope;
@@ -1453,7 +1483,7 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1453
1483
  if (hasKeepAliveProgress(current)) {
1454
1484
  casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1455
1485
  latestLane.state === "done" &&
1456
- latestLane.keep_alive === true, (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
1486
+ keepAliveStillEnabled(latestLane), (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
1457
1487
  }
1458
1488
  continue;
1459
1489
  }
@@ -1470,7 +1500,7 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1470
1500
  continue;
1471
1501
  const exhausted = casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1472
1502
  latestLane.state === "done" &&
1473
- latestLane.keep_alive === true &&
1503
+ keepAliveStillEnabled(latestLane) &&
1474
1504
  (latestLane.keep_alive_resume_count ?? 0) >= LEAD_LANE_KEEP_ALIVE_MAX_RESUMES &&
1475
1505
  latestLane.keep_alive_exhausted_at === undefined, (latestLane) => ({ ...latestLane, keep_alive_exhausted_at: nowIso }), deps.dryRun);
1476
1506
  if (exhausted.changed && !deps.dryRun) {
@@ -1495,7 +1525,7 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1495
1525
  if (resetByHuman && hasKeepAliveProgress(current)) {
1496
1526
  casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1497
1527
  latestLane.state === "done" &&
1498
- latestLane.keep_alive === true, (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
1528
+ keepAliveStillEnabled(latestLane), (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
1499
1529
  }
1500
1530
  continue;
1501
1531
  }
@@ -1515,7 +1545,7 @@ function resumeDoneKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, h
1515
1545
  }
1516
1546
  const queue = (expectedSeniorId) => casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1517
1547
  latestLane.state === "done" &&
1518
- latestLane.keep_alive === true &&
1548
+ keepAliveStillEnabled(latestLane) &&
1519
1549
  (expectedSeniorId === undefined ||
1520
1550
  (latestLane.senior.kind === "lane" && latestLane.senior.id === expectedSeniorId)), (latestLane) => ({
1521
1551
  ...latestLane,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.210.0",
3
+ "version": "5.211.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {