@indigoai-us/hq-cli 5.311.1 → 5.311.3

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +31 -5
  3. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  4. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +27 -2
  5. package/dist/commands/core-delegate-w1b.js +54 -7
  6. package/dist/commands/core-delegate.d.ts +1 -0
  7. package/dist/commands/core-delegate.js +1 -1
  8. package/dist/commands/files-browse.d.ts +3 -2
  9. package/dist/commands/files-browse.js +2 -7
  10. package/dist/commands/lanes-message.js +18 -7
  11. package/dist/commands/lanes-shared.d.ts +2 -0
  12. package/dist/commands/lanes-watch.js +1 -5
  13. package/dist/commands/lanes.js +10 -13
  14. package/dist/fast-scaffold.d.ts +4 -0
  15. package/dist/fast-scaffold.js +5 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/lib/flag-registry.d.ts +2 -0
  19. package/dist/lib/flag-registry.js +19 -12
  20. package/dist/lib/lanes/autopilot.js +19 -2
  21. package/dist/lib/lanes/delivery.d.ts +1 -0
  22. package/dist/lib/lanes/keep-alive.d.ts +1 -3
  23. package/dist/lib/lanes/keep-alive.js +2 -4
  24. package/dist/lib/lanes/reconcile.js +14 -36
  25. package/dist/lib/lanes/spawn.d.ts +2 -0
  26. package/dist/lib/lanes/spawn.js +35 -12
  27. package/dist/lib/lanes/types.d.ts +2 -2
  28. package/dist/lib/lanes/watch.d.ts +0 -1
  29. package/dist/lib/lanes/watch.js +4 -9
  30. package/dist/main.js +1 -1
  31. package/dist/node-network-compat-core.d.ts +12 -0
  32. package/dist/node-network-compat-core.js +41 -0
  33. package/dist/node-network-compat-fast.d.ts +7 -0
  34. package/dist/node-network-compat-fast.js +23 -0
  35. package/dist/node-network-compat.d.ts +2 -18
  36. package/dist/node-network-compat.js +4 -39
  37. package/dist/node-preflight.d.ts +7 -1
  38. package/dist/node-preflight.js +5 -5
  39. package/dist/utils/files-browse-acl.d.ts +5 -0
  40. package/dist/utils/files-browse-acl.js +11 -0
  41. package/dist/utils/hq-core-yaml.d.ts +10 -0
  42. package/dist/utils/hq-core-yaml.js +40 -0
  43. package/dist/utils/pack-contributions.d.ts +2 -9
  44. package/dist/utils/pack-contributions.js +1 -39
  45. package/dist/utils/scaffold-fast-import-guard.js +5 -1
  46. package/dist/utils/version-report.js +1 -1
  47. package/package.json +1 -1
@@ -6,12 +6,16 @@
6
6
  * keep a short-lived, offline CLI alive until the client's network deadline.
7
7
  */
8
8
  import { Worker } from "node:worker_threads";
9
- import * as Sentry from "@sentry/node";
10
9
  import { createFlagClient, isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
11
10
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
12
11
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
13
12
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
14
13
  import { writeFlagSnapshotFile } from "./flag-snapshot-file.js";
14
+ function captureFlagRegistryException(error, tags, sentry) {
15
+ if (!sentry)
16
+ return;
17
+ sentry.captureException(error, { tags });
18
+ }
15
19
  export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
16
20
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
17
21
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
@@ -58,6 +62,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
58
62
  return processReader;
59
63
  const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
60
64
  const company = resolveProcessFlagCompanyContext();
65
+ const sentry = dependencies.sentry;
61
66
  const companyUid = company.companyUid;
62
67
  const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
63
68
  processReader = processSnapshotReader(companyIdentifiers);
@@ -65,7 +70,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
65
70
  const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
66
71
  if (embeddedSource.length === 0) {
67
72
  (dependencies.captureWorkerFailure ?? ((error, tags) => {
68
- Sentry.captureException(error, { tags });
73
+ captureFlagRegistryException(error, tags, sentry);
69
74
  }))(new Error("Self-contained flag-registry worker source is unavailable"), {
70
75
  component: "flag-registry-worker-startup",
71
76
  worker: "flag-registry",
@@ -87,10 +92,14 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
87
92
  // its process-wide captureWorkerErrors option is disabled. Scope only this
88
93
  // worker so the reporting boundary can classify its startup errors without
89
94
  // changing capture for unrelated workers.
90
- const worker = Sentry.withScope((scope) => {
91
- scope.setTag("worker", "flag-registry");
92
- return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(embeddedSource, workerOptions);
93
- });
95
+ const createWorker = dependencies.createWorker ??
96
+ ((filename, options) => new Worker(filename, options));
97
+ const worker = sentry
98
+ ? sentry.withScope((scope) => {
99
+ scope.setTag("worker", "flag-registry");
100
+ return createWorker(embeddedSource, workerOptions);
101
+ })
102
+ : createWorker(embeddedSource, workerOptions);
94
103
  worker.on("message", (value) => {
95
104
  if (isFlagSnapshot(value)) {
96
105
  heldSnapshot = value;
@@ -99,12 +108,10 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
99
108
  writeFlagSnapshotFile(companyUid, value.flags);
100
109
  }
101
110
  catch (error) {
102
- Sentry.captureException(error, {
103
- tags: {
104
- component: "flag-snapshot-cache-write",
105
- company_uid_present: "true",
106
- },
107
- });
111
+ captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
112
+ component: "flag-snapshot-cache-write",
113
+ company_uid_present: "true",
114
+ }, sentry);
108
115
  }
109
116
  }
110
117
  }
@@ -154,7 +154,7 @@ function projectStories(hqRoot, relativePath, companySlug, projectId) {
154
154
  status,
155
155
  ...(story.passes === true ? { passes: true } : {}),
156
156
  ...(briefPath ? { briefPath } : {}),
157
- ...(Array.isArray(story.lanes) ? { laneIds: story.lanes.map((lane) => typeof lane === "string" ? lane : lane.lane_id) } : typeof story.lane === "string" && story.lane.trim() ? { laneIds: [story.lane] } : story.lane === "" ? { laneIds: [] } : {}),
157
+ ...storyLaneIds(story),
158
158
  ...(typeof story.updated_at === "string" ? { updatedAt: story.updated_at } : {}),
159
159
  ...(typeof story.external_blocker === "string" ? { externalBlocker: story.external_blocker } : {}),
160
160
  };
@@ -207,6 +207,18 @@ function parseOkrReports(status) {
207
207
  return { at: value.at, kpis: parseLines(value.kpis, "KPI"), okrs: parseLines(value.okrs, "OKR") };
208
208
  }).sort((a, b) => a.at.localeCompare(b.at));
209
209
  }
210
+ // A story's current `lane` counts alongside its `lanes` history, so `lanes: []` with `lane: X` is assigned to X.
211
+ function storyLaneIds(story) {
212
+ const history = Array.isArray(story.lanes)
213
+ ? story.lanes.map((lane) => typeof lane === "string" ? lane : lane.lane_id)
214
+ : undefined;
215
+ const current = typeof story.lane === "string" && story.lane.trim() ? story.lane : undefined;
216
+ if (history)
217
+ return { laneIds: current && !history.includes(current) ? [...history, current] : history };
218
+ if (current)
219
+ return { laneIds: [current] };
220
+ return story.lane === "" ? { laneIds: [] } : {};
221
+ }
210
222
  function statusBlockers(status) {
211
223
  const raw = status.blockers;
212
224
  if (raw === undefined)
@@ -256,7 +268,9 @@ export function planLeadAutopilotWakes(input) {
256
268
  const finishedMs = Date.parse(story.updatedAt);
257
269
  if (!Number.isFinite(finishedMs) || now.getTime() - finishedMs < LEAD_AUTOPILOT_REVIEW_DELAY_MS)
258
270
  return [];
259
- return story.laneIds.filter((laneId) => !knownChildIds.has(laneId)).map((laneId) => ({ laneId, story, finishedAt: story.updatedAt }));
271
+ // The lead's own lane is never a finished worker of itself.
272
+ return story.laneIds.filter((laneId) => laneId !== lead.lane_id && !knownChildIds.has(laneId))
273
+ .map((laneId) => ({ laneId, story, finishedAt: story.updatedAt }));
260
274
  });
261
275
  const overdueReviews = [...finishedReviews, ...missingReviews].sort((a, b) => a.laneId.localeCompare(b.laneId));
262
276
  if (overdueReviews.length > 0) {
@@ -351,6 +365,9 @@ export function planLeadAutopilotWakes(input) {
351
365
  if (story.status !== "in_progress")
352
366
  return [];
353
367
  const laneIds = story.laneIds ?? [];
368
+ // A story assigned to the lead itself is worked by the lead, which is the lane being woken.
369
+ if (laneIds.includes(lead.lane_id))
370
+ return [];
354
371
  const live = laneIds.filter((laneId) => children.some((child) => child.lane_id === laneId && LIVE_STATES.has(child.state)));
355
372
  if (live.length > 0)
356
373
  return [];
@@ -137,6 +137,7 @@ export type LaunchTrigger = {
137
137
  text: string;
138
138
  message_id: string;
139
139
  monitor_event?: true;
140
+ system_notice?: true;
140
141
  } | {
141
142
  /** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
142
143
  kind: "resume";
@@ -1,8 +1,6 @@
1
1
  import { type LaneRecord, type ManagerStatusGateHistoryEntry, type 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
- export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
5
- export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
6
4
  export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS: number;
7
5
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
8
6
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
@@ -30,6 +28,7 @@ export declare function queueLaneTerminalResume(current: LaneRecord, at: string,
30
28
  allowResume?: boolean;
31
29
  mergeExisting?: boolean;
32
30
  allowBlockedResume?: boolean;
31
+ trigger?: "resume" | "watcher";
33
32
  }): LaneTerminalResumeResult;
34
33
  /** Persist a manager status miss and queue its one bounded follow-up turn. */
35
34
  export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
@@ -47,7 +46,6 @@ export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complet
47
46
  export declare function keepAliveNextCheckAtMs(value: unknown, nowMs: number, anchorMs?: number): number | undefined;
48
47
  /** Shared done-state policy used by watch, reconcile, and the show command. */
49
48
  export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean, options?: {
50
- nextCheckAtEnabled?: boolean;
51
49
  hasPendingWork?: boolean;
52
50
  nowMs?: number;
53
51
  nextCheckAnchorMs?: number;
@@ -6,8 +6,6 @@ import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } f
6
6
  import { isTerminalLaneState, isIsoDateTime, } from "./types.js";
7
7
  export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
8
8
  export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
9
- export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
10
- export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
11
9
  export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
12
10
  export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
13
11
  export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
@@ -63,7 +61,7 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
63
61
  terminal_details: undefined,
64
62
  pending_resume: {
65
63
  session_id: sessionId,
66
- trigger: "resume",
64
+ trigger: options.trigger ?? "watcher",
67
65
  resumed_from: current.state,
68
66
  text: resumeText,
69
67
  compact: true,
@@ -126,7 +124,7 @@ export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork, options = {}
126
124
  verdict = "story_complete";
127
125
  else
128
126
  verdict = hasQueuedWork ? "eligible" : "standing_idle";
129
- if (verdict === "eligible" && options.nextCheckAtEnabled === true && options.hasPendingWork !== true) {
127
+ if (verdict === "eligible" && options.hasPendingWork !== true) {
130
128
  const nowMs = options.nowMs ?? Date.now();
131
129
  const anchorMs = options.nextCheckAnchorMs ?? nowMs;
132
130
  const nextCheckAtMs = keepAliveNextCheckAtMs(envelope.next_check_at, nowMs, anchorMs);
@@ -25,7 +25,7 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
25
25
  import { resolveLaneCompany } from "./lane-company.js";
26
26
  import { appendDropboxMessage } from "./dropbox.js";
27
27
  import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
28
- import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, 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";
28
+ import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, 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";
29
29
  import { providerSupportsSessionResume } from "./delivery.js";
30
30
  import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
31
31
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
@@ -1087,7 +1087,6 @@ export async function runPassBody(hqRoot, deps) {
1087
1087
  }
1088
1088
  }));
1089
1089
  const keepAliveFlagCompanies = new Map();
1090
- const nextCheckAtFlagCompanyKeys = new Set();
1091
1090
  for (const lane of lanes) {
1092
1091
  if (!(lane.state === "done" || lane.state === "interrupted" || lane.state === "blocked" || lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") ||
1093
1092
  (lane.keep_alive !== true && lane.worker_profile === undefined) ||
@@ -1098,26 +1097,25 @@ export async function runPassBody(hqRoot, deps) {
1098
1097
  !providerSupportsSessionResume(lane.provider) ||
1099
1098
  !(lane.provider_session_id ?? lane.last_resume?.session_id))))
1100
1099
  continue;
1101
- let hasNextCheckAt = false;
1102
1100
  if (lane.state === "done") {
1103
1101
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
1104
1102
  if (!envelope || envelope.decision !== "done")
1105
1103
  continue;
1106
1104
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
1107
- if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
1105
+ if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
1106
+ hasPendingWork: hasPendingKeepAliveWork(hqRoot, lane, questions),
1107
+ nowMs: now.getTime(),
1108
+ nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
1109
+ }) !== "eligible")
1108
1110
  continue;
1109
- hasNextCheckAt = envelope.next_check_at !== undefined;
1110
1111
  }
1111
1112
  const company = resolveLaneCompany(hqRoot, lane).company;
1112
1113
  if (company === null)
1113
1114
  continue;
1114
1115
  const key = keepAliveCompanyKey(company);
1115
1116
  keepAliveFlagCompanies.set(key, company);
1116
- if (hasNextCheckAt)
1117
- nextCheckAtFlagCompanyKeys.add(key);
1118
1117
  }
1119
1118
  const keepAliveFlagByCompany = new Map();
1120
- const nextCheckAtFlagByCompany = new Map();
1121
1119
  const blockedResumeFlagByCompany = new Map();
1122
1120
  const immediateFirstResumeFlagByCompany = new Map();
1123
1121
  const supersessionLineageAwareByCompany = new Map();
@@ -1130,11 +1128,8 @@ export async function runPassBody(hqRoot, deps) {
1130
1128
  lane.keep_alive === true &&
1131
1129
  laneRunsHere(lane, startDeps.host) &&
1132
1130
  keepAliveCompanyKey(resolveLaneCompany(hqRoot, lane).company) === key);
1133
- const [keepAliveEnabled, nextCheckAtEnabled, lineageAware, blockedResumeEnabled, immediateFirstResumeEnabled] = await Promise.all([
1131
+ const [keepAliveEnabled, lineageAware, blockedResumeEnabled, immediateFirstResumeEnabled] = await Promise.all([
1134
1132
  readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
1135
- nextCheckAtFlagCompanyKeys.has(key)
1136
- ? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, options)
1137
- : Promise.resolve(false),
1138
1133
  readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
1139
1134
  hasBlockedKeepAliveLane
1140
1135
  ? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
@@ -1142,7 +1137,6 @@ export async function runPassBody(hqRoot, deps) {
1142
1137
  readRegistryFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
1143
1138
  ]);
1144
1139
  keepAliveFlagByCompany.set(key, keepAliveEnabled);
1145
- nextCheckAtFlagByCompany.set(key, nextCheckAtEnabled);
1146
1140
  blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
1147
1141
  immediateFirstResumeFlagByCompany.set(key, immediateFirstResumeEnabled);
1148
1142
  supersessionLineageAwareByCompany.set(key, lineageAware);
@@ -1165,7 +1159,7 @@ export async function runPassBody(hqRoot, deps) {
1165
1159
  const freshLanes = loadLaneRecords(hqRoot, summary);
1166
1160
  const freshQuestions = loadQuestionRecords(hqRoot, summary);
1167
1161
  await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
1168
- await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany, immediateFirstResumeFlagByCompany);
1162
+ await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany);
1169
1163
  const drainLanes = loadLaneRecords(hqRoot, summary);
1170
1164
  await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
1171
1165
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
@@ -2188,7 +2182,7 @@ async function resumeBlockedKeepAlive(hqRoot, lane, questions, summary, deps, no
2188
2182
  current = afterPreparation;
2189
2183
  }
2190
2184
  const expectedVersion = current.version;
2191
- const result = queueLaneTerminalResume(current, now.toISOString(), LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, { allowBlockedResume: true });
2185
+ const result = queueLaneTerminalResume(current, now.toISOString(), LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, { allowBlockedResume: true, trigger: "resume" });
2192
2186
  if (!result.resumeQueued) {
2193
2187
  if (result.resumeSkippedReason !== undefined) {
2194
2188
  summary.skipped.push({ kind: "lane", id: lane.lane_id, why: "keep_alive_" + result.resumeSkippedReason });
@@ -2244,7 +2238,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
2244
2238
  }),
2245
2239
  }), false);
2246
2240
  }
2247
- async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany, immediateFirstResumeFlagByCompany) {
2241
+ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany) {
2248
2242
  const nowMs = now.getTime();
2249
2243
  const nowIso = now.toISOString();
2250
2244
  const config = loadLanesConfig(hqRoot);
@@ -2291,13 +2285,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2291
2285
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
2292
2286
  if (!envelope || envelope.decision !== "done")
2293
2287
  continue;
2294
- const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
2295
- const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
2296
- ? hasPendingKeepAliveWork(hqRoot, lane, questions)
2297
- : false;
2288
+ const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane, questions);
2298
2289
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
2299
2290
  const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
2300
- nextCheckAtEnabled,
2301
2291
  hasPendingWork,
2302
2292
  nowMs,
2303
2293
  nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
@@ -2415,13 +2405,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2415
2405
  const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
2416
2406
  if (!latest || latest.envelope.decision !== "done")
2417
2407
  continue;
2418
- const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
2419
- const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
2420
- ? hasPendingKeepAliveWork(hqRoot, lane, questions)
2421
- : false;
2408
+ const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane, questions);
2422
2409
  const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
2423
2410
  const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork, {
2424
- nextCheckAtEnabled,
2425
2411
  hasPendingWork,
2426
2412
  nowMs,
2427
2413
  nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
@@ -2507,13 +2493,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2507
2493
  const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
2508
2494
  if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
2509
2495
  continue;
2510
- const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(current))) === true;
2511
- const hasPendingWork = current.keep_alive_standing === true || nextCheckAtEnabled
2512
- ? hasPendingKeepAliveWork(hqRoot, current, questions)
2513
- : false;
2496
+ const hasPendingWork = hasPendingKeepAliveWork(hqRoot, current, questions);
2514
2497
  const hasQueuedWork = current.keep_alive_standing === true && hasPendingWork;
2515
2498
  const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork, {
2516
- nextCheckAtEnabled,
2517
2499
  hasPendingWork,
2518
2500
  nowMs,
2519
2501
  nextCheckAnchorMs: Date.parse(current.last_envelope?.at ?? current.updated_at),
@@ -2794,12 +2776,8 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2794
2776
  const envelope = readLatestEnvelopeRecord(hqRoot, latest.lane_id);
2795
2777
  if (!envelope || envelope.envelope.decision !== "done")
2796
2778
  continue;
2797
- const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(latest))) === true;
2798
- const hasPendingWork = latest.keep_alive_standing === true || nextCheckAtEnabled
2799
- ? hasPendingKeepAliveWork(hqRoot, latest, questions)
2800
- : false;
2779
+ const hasPendingWork = hasPendingKeepAliveWork(hqRoot, latest, questions);
2801
2780
  const finalDoneVerdict = doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingWork, {
2802
- nextCheckAtEnabled,
2803
2781
  hasPendingWork,
2804
2782
  nowMs,
2805
2783
  nextCheckAnchorMs: Date.parse(latest.last_envelope?.at ?? latest.updated_at),
@@ -198,6 +198,8 @@ export declare function resumePrompt(input: {
198
198
  answers?: AnsweredQuestionLine[];
199
199
  /** `hq lanes resume`: the state the operator resumed the lane from. */
200
200
  resumedFrom?: string;
201
+ /** Automatic continuation initiated by the lane watcher. */
202
+ resumeSource?: "watcher";
201
203
  /** Exact validator errors recorded after the prior envelope repair failed. */
202
204
  envelopeRepairErrors?: {
203
205
  first_error: string;
@@ -372,7 +372,18 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
372
372
  if (last.trigger === "answered_questions") {
373
373
  return { session_id: sessionId, trigger: "answered_questions", at, ...compact, ...keepAlive };
374
374
  }
375
- if (last.trigger === "message") {
375
+ if (last.trigger === "watcher") {
376
+ return {
377
+ session_id: sessionId,
378
+ trigger: "watcher",
379
+ resumed_from: last.resumed_from ?? "interrupted",
380
+ ...(last.text !== undefined ? { text: last.text } : {}),
381
+ at,
382
+ ...compact,
383
+ ...keepAlive,
384
+ };
385
+ }
386
+ if (last.trigger === "message" || last.trigger === "monitor_event" || last.trigger === "system_notice") {
376
387
  if (last.message_id === undefined) {
377
388
  throw new LanesError("account_retry_context_missing", `The message id needed to resume lane ${lane.lane_id} is missing`, { lane_id: lane.lane_id, trigger: last.trigger });
378
389
  }
@@ -382,9 +393,10 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
382
393
  }
383
394
  return {
384
395
  session_id: sessionId,
385
- trigger: "message",
396
+ trigger: last.trigger === "message" && last.monitor_event === true ? "monitor_event" : last.trigger,
386
397
  text,
387
398
  message_id: last.message_id,
399
+ ...(last.monitor_event === true || last.trigger === "monitor_event" ? { monitor_event: true } : {}),
388
400
  at,
389
401
  ...compact,
390
402
  ...keepAlive,
@@ -1071,7 +1083,7 @@ export function readAnsweredQuestionLines(answersPath) {
1071
1083
  export function resumePrompt(input) {
1072
1084
  const parts = [];
1073
1085
  if (input.resumedFrom !== undefined) {
1074
- parts.push(`The operator resumed this lane from state ${input.resumedFrom}. ` +
1086
+ parts.push(`${input.resumeSource === "watcher" ? "The lane watcher" : "The operator"} resumed this lane from state ${input.resumedFrom}. ` +
1075
1087
  (input.resumedFrom === "interrupted"
1076
1088
  ? "The previous run ended without writing its envelope; continue the work from where it stopped."
1077
1089
  : "Continue the work from where the previous run stopped."));
@@ -1743,7 +1755,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1743
1755
  const profileChanged = profile !== undefined && existing.worker_profile_hash !== profile.contentHash;
1744
1756
  const config = loadLanesConfig(hqRoot);
1745
1757
  const repairRecordedAccount = plan.mode === "resume" &&
1746
- (trigger.kind === "resume" || pendingResume?.trigger === "resume");
1758
+ (trigger.kind === "resume" || pendingResume?.trigger === "resume" || pendingResume?.trigger === "watcher");
1747
1759
  const recordedModel = repairRecordedAccount && existing.model_source === "account"
1748
1760
  ? existing.model
1749
1761
  : undefined;
@@ -1919,6 +1931,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1919
1931
  ? pendingResume.text
1920
1932
  : undefined,
1921
1933
  resumedFrom: trigger.kind === "resume" ? trigger.from : pendingResume?.resumed_from,
1934
+ resumeSource: pendingResume?.trigger === "watcher" ? "watcher" : undefined,
1922
1935
  answersPath: answeredLines !== undefined ? answersPath : undefined,
1923
1936
  answers: answeredLines,
1924
1937
  envelopeRepairErrors: lane.terminal_reason === ENVELOPE_INVALID_REASON &&
@@ -2019,14 +2032,24 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2019
2032
  const nowIso = (deps.now ?? (() => new Date()))().toISOString();
2020
2033
  const resumeHistoryTrigger = pendingResume?.keep_alive === true
2021
2034
  ? "keep_alive"
2022
- : pendingResume?.trigger === "message" || trigger.kind === "message"
2023
- ? "message"
2024
- : pendingResume?.trigger === "resume" || trigger.kind === "resume"
2025
- ? "resume_cli"
2026
- : "reconcile";
2035
+ : pendingResume?.trigger === "monitor_event" || pendingResume?.monitor_event === true ||
2036
+ (trigger.kind === "message" && trigger.monitor_event === true)
2037
+ ? "monitor_event"
2038
+ : pendingResume?.trigger === "system_notice" ||
2039
+ (trigger.kind === "message" && trigger.system_notice === true)
2040
+ ? "system_notice"
2041
+ : pendingResume?.trigger === "watcher"
2042
+ ? "watcher"
2043
+ : pendingResume?.trigger === "message" || trigger.kind === "message"
2044
+ ? "message"
2045
+ : pendingResume?.trigger === "resume" || trigger.kind === "resume"
2046
+ ? "resume_cli"
2047
+ : "reconcile";
2027
2048
  const resumeHistoryCaller = pendingResume?.caller ||
2028
- (typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
2029
- (trigger.kind === "answered_questions" || trigger.kind === "resume" || trigger.kind === "message"
2049
+ (pendingResume?.trigger !== "watcher" && typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
2050
+ (pendingResume?.trigger === "message" || pendingResume?.trigger === "monitor_event" ||
2051
+ pendingResume?.trigger === "system_notice" || trigger.kind === "answered_questions" ||
2052
+ trigger.kind === "resume" || trigger.kind === "message"
2030
2053
  ? "user"
2031
2054
  : undefined);
2032
2055
  let statusGateHeartbeat;
@@ -2159,7 +2182,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2159
2182
  session_id: resume.session_id,
2160
2183
  at: nowIso,
2161
2184
  account: resolved.id,
2162
- trigger: trigger.kind,
2185
+ trigger: trigger.kind === "message" && trigger.system_notice === true ? "system_notice" : trigger.kind,
2163
2186
  ...(trigger.kind === "message"
2164
2187
  ? {
2165
2188
  message_id: trigger.message_id,
@@ -766,7 +766,7 @@ export interface FailedResumeHistoryEntry {
766
766
  export interface PendingResume {
767
767
  /** Fresh answered-question launches need no previous session id. */
768
768
  session_id?: string;
769
- trigger: "answered_questions" | "message" | "resume";
769
+ trigger: "answered_questions" | "message" | "monitor_event" | "system_notice" | "resume" | "watcher";
770
770
  /** True when this queued resume was initiated automatically by keep-alive. */
771
771
  keep_alive?: true;
772
772
  resumed_from?: "interrupted" | "done" | "blocked" | "failed" | "awaiting_input";
@@ -797,7 +797,7 @@ export interface LaneResumeRecord {
797
797
  at: string;
798
798
  /** Account the resumed run was bound to. */
799
799
  account?: string;
800
- trigger: "answered_questions" | "message" | "resume";
800
+ trigger: "answered_questions" | "message" | "monitor_event" | "system_notice" | "resume" | "watcher";
801
801
  /** True when this was an automatic keep-alive resume. */
802
802
  keep_alive?: true;
803
803
  message_id?: string;
@@ -23,7 +23,6 @@ export interface LaneWatchDeps {
23
23
  readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
24
24
  readAccountStatuses?: (lanes: readonly LaneRecord[]) => Promise<readonly LaneWatchAccountStatus[]>;
25
25
  keepAliveEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
26
- keepAliveNextCheckAtEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
27
26
  sleep?: (ms: number) => Promise<void>;
28
27
  intervalMs?: number;
29
28
  maxOutputLines?: number;
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
32
32
  }
33
33
  return unique;
34
34
  }
35
- function canResumeKeepAliveLane(hqRoot, lane, nextCheckAtEnabled = false, nowMs = Date.now()) {
35
+ function canResumeKeepAliveLane(hqRoot, lane, nowMs = Date.now()) {
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,10 +49,8 @@ function canResumeKeepAliveLane(hqRoot, lane, nextCheckAtEnabled = false, nowMs
49
49
  return lane.terminal_reason === "exited_with_partial_output";
50
50
  }
51
51
  if (lane.state === "done") {
52
- const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
53
- ? hasPendingKeepAliveWork(hqRoot, lane)
54
- : false;
55
- const nextCheckAtMs = nextCheckAtEnabled && !hasPendingWork
52
+ const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane);
53
+ const nextCheckAtMs = !hasPendingWork
56
54
  ? keepAliveNextCheckAtMs(lane.last_envelope?.next_check_at, nowMs, Date.parse(lane.last_envelope?.at ?? lane.updated_at))
57
55
  : undefined;
58
56
  if (nextCheckAtMs !== undefined && nowMs < nextCheckAtMs)
@@ -143,10 +141,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
143
141
  continue;
144
142
  if (!await deps.keepAliveEnabled(record))
145
143
  continue;
146
- const nextCheckAtEnabled = deps.keepAliveNextCheckAtEnabled === undefined
147
- ? false
148
- : await deps.keepAliveNextCheckAtEnabled(record);
149
- if (canResumeKeepAliveLane(hqRoot, record, nextCheckAtEnabled))
144
+ if (canResumeKeepAliveLane(hqRoot, record))
150
145
  keepAliveWaiting.add(record.lane_id);
151
146
  }
152
147
  }
package/dist/main.js CHANGED
@@ -201,7 +201,7 @@ export async function runCli() {
201
201
  const isAuthLogin = process.argv[2] === "auth" && process.argv[3] === "login";
202
202
  if (!isAuthLogin) {
203
203
  runGlobalNetworkLifecycle(process.argv, () => {
204
- void kickFlagRegistryReadiness();
204
+ void kickFlagRegistryReadiness({ sentry: Sentry });
205
205
  });
206
206
  }
207
207
  // Set when a self-update re-exec'd this command on a newer CLI: the child
@@ -0,0 +1,12 @@
1
+ /** Node network defaults shared by the full graph and the selective fast path. */
2
+ export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
3
+ export interface NodeNetworkCompatibilityOptions {
4
+ nodeOptions?: string;
5
+ execArgv?: readonly string[];
6
+ /** Inject the net timeout setter (tests). Pass null to simulate a runtime without it. */
7
+ setAttemptTimeout?: ((milliseconds: number) => void) | null;
8
+ }
9
+ export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
10
+ /** Apply at most once when both entrypoint routing layers import this module. */
11
+ export declare function configureNodeNetworkCompatibilityOnce(): boolean;
12
+ //# sourceMappingURL=node-network-compat-core.d.ts.map
@@ -0,0 +1,41 @@
1
+ /** Node network defaults shared by the full graph and the selective fast path. */
2
+ import * as nodeNet from "node:net";
3
+ export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
4
+ const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
5
+ function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
6
+ const optionPattern = new RegExp("(?:^|\\s)" + ATTEMPT_TIMEOUT_OPTION + "(?:=|\\s|$)");
7
+ return (optionPattern.test(nodeOptions) ||
8
+ execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
9
+ argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
10
+ }
11
+ function resolveSetAttemptTimeout(override) {
12
+ if (override === null)
13
+ return undefined;
14
+ if (override)
15
+ return override;
16
+ const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
17
+ return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
18
+ }
19
+ export function configureNodeNetworkCompatibility(options = {}) {
20
+ const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
21
+ const execArgv = options.execArgv ?? process.execArgv;
22
+ if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
23
+ return false;
24
+ }
25
+ const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
26
+ if (!setAttemptTimeout) {
27
+ // Runtime lacks the API; the Node preflight should already have exited.
28
+ return false;
29
+ }
30
+ setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
31
+ return true;
32
+ }
33
+ let configurationAttempted = false;
34
+ /** Apply at most once when both entrypoint routing layers import this module. */
35
+ export function configureNodeNetworkCompatibilityOnce() {
36
+ if (configurationAttempted)
37
+ return false;
38
+ configurationAttempted = true;
39
+ return configureNodeNetworkCompatibility();
40
+ }
41
+ //# sourceMappingURL=node-network-compat-core.js.map
@@ -0,0 +1,7 @@
1
+ /** Selective Node network setup for the entrypoint's fast routes. */
2
+ /**
3
+ * Full-graph invocations keep the historical unconditional network patch.
4
+ * Fast routes opt in only when their direct implementation uses Node networking.
5
+ */
6
+ export declare function shouldConfigureNodeNetworkCompatibility(argv: readonly string[]): boolean;
7
+ //# sourceMappingURL=node-network-compat-fast.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Selective Node network setup for the entrypoint's fast routes. */
2
+ import { parseFastCore, parseSecretsEnvFastRequest } from "./fast-scaffold.js";
3
+ import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
4
+ /**
5
+ * Full-graph invocations keep the historical unconditional network patch.
6
+ * Fast routes opt in only when their direct implementation uses Node networking.
7
+ */
8
+ export function shouldConfigureNodeNetworkCompatibility(argv) {
9
+ if (isVersionRequest(argv))
10
+ return wantsLabelledVersionReport(argv);
11
+ const fastCore = parseFastCore(argv);
12
+ if (fastCore)
13
+ return fastCore.entry.requiresNodeNetworkCompatibility === true;
14
+ // The lightweight secrets route still makes vault API requests.
15
+ if (parseSecretsEnvFastRequest(argv))
16
+ return true;
17
+ return true;
18
+ }
19
+ if (shouldConfigureNodeNetworkCompatibility(process.argv)) {
20
+ const { configureNodeNetworkCompatibilityOnce } = await import("./node-network-compat-core.js");
21
+ configureNodeNetworkCompatibilityOnce();
22
+ }
23
+ //# sourceMappingURL=node-network-compat-fast.js.map
@@ -1,19 +1,3 @@
1
- /**
2
- * Node network defaults for HQ CLI entry points.
3
- *
4
- * The compatibility timeout remains the default for HQ CLI entry points.
5
- *
6
- * Keep this module safe to import immediately after the Node version preflight.
7
- */
8
- export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
9
- export interface NodeNetworkCompatibilityOptions {
10
- nodeOptions?: string;
11
- execArgv?: readonly string[];
12
- /**
13
- * Inject the net timeout setter (tests). Pass null to simulate a runtime
14
- * that lacks setDefaultAutoSelectFamilyAttemptTimeout.
15
- */
16
- setAttemptTimeout?: ((milliseconds: number) => void) | null;
17
- }
18
- export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
1
+ /** Unconditional compatibility setup for the full CLI command graph. */
2
+ export { configureNodeNetworkCompatibility, HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS, type NodeNetworkCompatibilityOptions, } from "./node-network-compat-core.js";
19
3
  //# sourceMappingURL=node-network-compat.d.ts.map