@indigoai-us/hq-cli 5.294.0 → 5.295.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/command-catalog.generated.d.ts +12 -9
  3. package/dist/command-catalog.generated.js +15 -12
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +76 -1
  6. package/dist/commands/doctor.js +0 -74
  7. package/dist/commands/lanes-message.js +23 -5
  8. package/dist/commands/lanes-resume.js +0 -3
  9. package/dist/commands/lanes-shared.d.ts +1 -2
  10. package/dist/commands/lanes.d.ts +1 -9
  11. package/dist/commands/lanes.js +177 -172
  12. package/dist/fast-scaffold.d.ts +0 -5
  13. package/dist/fast-scaffold.js +0 -10
  14. package/dist/index.js +1 -10
  15. package/dist/lib/core-utils/sentry-report.d.ts +0 -19
  16. package/dist/lib/core-utils/sentry-report.js +8 -61
  17. package/dist/lib/doctor/checks/claude-wiring.js +9 -0
  18. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  19. package/dist/lib/lanes/flag-keys.js +2 -0
  20. package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
  21. package/dist/lib/lanes/manager-status-gate.js +2 -43
  22. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
  23. package/dist/lib/lanes/outpost-dispatch.js +7 -1
  24. package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
  25. package/dist/lib/lanes/outpost-hire-cache.js +0 -1
  26. package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
  27. package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
  28. package/dist/lib/lanes/reconcile.d.ts +2 -3
  29. package/dist/lib/lanes/reconcile.js +44 -38
  30. package/dist/lib/lanes/spawn.d.ts +0 -2
  31. package/dist/lib/lanes/spawn.js +18 -5
  32. package/dist/lib/lanes/store.js +22 -0
  33. package/dist/lib/lanes/types.d.ts +18 -1
  34. package/dist/lib/lanes/types.js +1 -1
  35. package/dist/lib/lanes/watcher.d.ts +4 -3
  36. package/dist/lib/lanes/watcher.js +102 -34
  37. package/dist/lib/mesh/live/daemon/run.js +18 -1
  38. package/dist/utils/cognito-session.d.ts +6 -0
  39. package/dist/utils/cognito-session.js +9 -0
  40. package/dist/utils/self-update.js +31 -0
  41. package/dist/utils/version-gate.js +22 -3
  42. package/package.json +1 -1
  43. package/dist/fast-lanes-list-route.d.ts +0 -8
  44. package/dist/fast-lanes-list-route.js +0 -32
  45. package/dist/fast-lanes-list.d.ts +0 -3
  46. package/dist/fast-lanes-list.js +0 -224
  47. package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
  48. package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
  49. package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
  50. package/dist/lib/doctor/client-health-sentry.js +0 -286
  51. package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
  52. package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
  53. package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
  54. package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
@@ -354,12 +354,13 @@ function archivedResumeMessageText(hqRoot, laneId, messageId) {
354
354
  }
355
355
  function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
356
356
  const last = lane.last_resume;
357
+ const keepAlive = last?.keep_alive === true ? { keep_alive: true } : {};
357
358
  if (last === undefined || last.session_id !== sessionId) {
358
- return { session_id: sessionId, trigger: "resume", resumed_from: "interrupted", at };
359
+ return { session_id: sessionId, trigger: "resume", resumed_from: "interrupted", at, ...keepAlive };
359
360
  }
360
361
  const compact = last.compact === undefined ? {} : { compact: last.compact };
361
362
  if (last.trigger === "answered_questions") {
362
- return { session_id: sessionId, trigger: "answered_questions", at, ...compact };
363
+ return { session_id: sessionId, trigger: "answered_questions", at, ...compact, ...keepAlive };
363
364
  }
364
365
  if (last.trigger === "message") {
365
366
  if (last.message_id === undefined) {
@@ -376,6 +377,7 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
376
377
  message_id: last.message_id,
377
378
  at,
378
379
  ...compact,
380
+ ...keepAlive,
379
381
  };
380
382
  }
381
383
  return {
@@ -386,6 +388,7 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
386
388
  ...(last.message_id !== undefined ? { message_id: last.message_id } : {}),
387
389
  at,
388
390
  ...compact,
391
+ ...keepAlive,
389
392
  };
390
393
  }
391
394
  /** Change account only after a provider/runtime account failure, then re-admit. */
@@ -1112,6 +1115,8 @@ export function envelopeRepairPrompt(input) {
1112
1115
  `Missing expected envelope file: ${input.envelopePath}`,
1113
1116
  "The previous envelope is stale and must not be reused or copied into this run's result.",
1114
1117
  "Printing an envelope in the conversation or writing it to last-message.txt is not enough; write the JSON object directly to the expected file.",
1118
+ "If the task is unfinished, write decision: done, story_complete: false, and set story_status and story_complete_reason to describe the work that remains. The repair turn itself is never a reason for blocked.",
1119
+ "This repair turn only writes the missing envelope; do not resume the task.",
1115
1120
  "Schema:",
1116
1121
  JSON.stringify(LANE_ENVELOPE_JSON_SCHEMA, null, 2),
1117
1122
  `Write the corrected JSON object to ${input.envelopePath}, overwriting it. ` +
@@ -2000,9 +2005,16 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2000
2005
  // Claim running before spawning. Two callers that both observe queued must
2001
2006
  // not both start processes; the CAS loser returns without spawn.
2002
2007
  const nowIso = (deps.now ?? (() => new Date()))().toISOString();
2003
- const statusGateHeartbeat = deps.managerStatusGateEnabled === true
2004
- ? managerStatusGateHeartbeat(hqRoot, existing, laneTurnId, nowIso)
2005
- : undefined;
2008
+ let statusGateHeartbeat;
2009
+ try {
2010
+ statusGateHeartbeat = managerStatusGateHeartbeat(hqRoot, existing, laneTurnId, nowIso);
2011
+ }
2012
+ catch (err) {
2013
+ const code = err instanceof LanesError ? err.error : "manager_status_gate_unavailable";
2014
+ const message = err instanceof Error ? err.message : String(err);
2015
+ process.stderr.write(`hq lanes: manager status gate disabled for ${lane.lane_id}: ${code}: ${message}\n`);
2016
+ statusGateHeartbeat = undefined;
2017
+ }
2006
2018
  let claimed = false;
2007
2019
  const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
2008
2020
  claimed = false;
@@ -2102,6 +2114,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2102
2114
  at: nowIso,
2103
2115
  account: resolved.id,
2104
2116
  trigger: pendingResume.trigger,
2117
+ ...(pendingResume.keep_alive === true ? { keep_alive: true } : {}),
2105
2118
  ...(pendingResume.message_id !== undefined ? { message_id: pendingResume.message_id } : {}),
2106
2119
  ...(pendingResume.monitor_event === true ? { monitor_event: true } : {}),
2107
2120
  ...(pendingResume.resumed_from !== undefined ? { resumed_from: pendingResume.resumed_from } : {}),
@@ -751,6 +751,28 @@ export function parseLaneRecord(raw, filePath) {
751
751
  path: filePath,
752
752
  });
753
753
  }
754
+ if (raw.keep_alive_history !== undefined) {
755
+ if (!Array.isArray(raw.keep_alive_history) || raw.keep_alive_history.length > HISTORY_CAP) {
756
+ throw new LaneSchemaError(`keep_alive_history must be an array capped at ${HISTORY_CAP} entries`, {
757
+ path: filePath,
758
+ field: "keep_alive_history",
759
+ });
760
+ }
761
+ for (const [index, value] of raw.keep_alive_history.entries()) {
762
+ const field = `keep_alive_history[${index}]`;
763
+ if (!isPlainObject(value) ||
764
+ Object.keys(value).some((key) => !["at", "resumed_from", "account"].includes(key)) ||
765
+ (value.resumed_from !== "done" && value.resumed_from !== "interrupted" && value.resumed_from !== "blocked") ||
766
+ (value.account !== null && typeof value.account !== "string")) {
767
+ throw new LaneSchemaError(`${field} must be a keep-alive resume history entry`, {
768
+ path: filePath,
769
+ field,
770
+ value,
771
+ });
772
+ }
773
+ requireTimestamp(value.at, `${field}.at`, filePath);
774
+ }
775
+ }
754
776
  if (raw.stop_history !== undefined) {
755
777
  if (!Array.isArray(raw.stop_history)) {
756
778
  throw new LaneSchemaError("stop_history must be an array when present", {
@@ -407,6 +407,11 @@ export interface KeepAliveBlockedHistoryEntry {
407
407
  resume_skipped_reason?: "open_question" | "blocked_grace";
408
408
  resume_queued?: boolean;
409
409
  }
410
+ export interface KeepAliveResumeHistoryEntry {
411
+ at: string;
412
+ resumed_from: "done" | "interrupted" | "blocked";
413
+ account: string | null;
414
+ }
410
415
  export interface KrFreshnessHistoryEntry {
411
416
  event: "kr_stalled";
412
417
  turn_id: string;
@@ -418,7 +423,13 @@ export interface KrFreshnessHistoryEntry {
418
423
  resume_queued?: boolean;
419
424
  resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
420
425
  }
421
- export type LaneGateHistoryEntry = ManagerStatusGateHistoryEntry | BlockerFreshnessHistoryEntry | KrFreshnessHistoryEntry | KeepAliveBlockedHistoryEntry;
426
+ export interface EnvelopeRepairContinueHistoryEntry {
427
+ event: "envelope_repair_continued";
428
+ at: string;
429
+ envelope_ref: string;
430
+ session_id: string;
431
+ }
432
+ export type LaneGateHistoryEntry = ManagerStatusGateHistoryEntry | BlockerFreshnessHistoryEntry | KrFreshnessHistoryEntry | EnvelopeRepairContinueHistoryEntry | KeepAliveBlockedHistoryEntry;
422
433
  export interface LaneStory {
423
434
  project_id: string;
424
435
  story_id: string;
@@ -491,6 +502,8 @@ export interface LaneRecord {
491
502
  keep_alive_blocked_reason?: "not_opted_in" | "missing_opt_in_time" | "envelope_before_opt_in" | "interruption_before_opt_in" | "missing_interruption_time" | "retired" | "superseded" | "retired_list_invalid" | "terminal_state";
492
503
  /** Consecutive automatic keep-alive resumes since the last human reset. */
493
504
  keep_alive_resume_count?: number;
505
+ /** Bounded audit trail of automatic keep-alive resumes; human resets never clear it. */
506
+ keep_alive_history?: KeepAliveResumeHistoryEntry[];
494
507
  /** Blocked keep-alive resumes in the current blocked episode. */
495
508
  keep_alive_blocked_resume_streak?: number;
496
509
  /** Time the most recent automatic keep-alive resume was queued. */
@@ -700,6 +713,8 @@ export interface PendingResume {
700
713
  /** Fresh answered-question launches need no previous session id. */
701
714
  session_id?: string;
702
715
  trigger: "answered_questions" | "message" | "resume";
716
+ /** True when this queued resume was initiated automatically by keep-alive. */
717
+ keep_alive?: true;
703
718
  resumed_from?: "interrupted" | "done" | "blocked" | "failed";
704
719
  from_failed?: true;
705
720
  /** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
@@ -727,6 +742,8 @@ export interface LaneResumeRecord {
727
742
  /** Account the resumed run was bound to. */
728
743
  account?: string;
729
744
  trigger: "answered_questions" | "message" | "resume";
745
+ /** True when this was an automatic keep-alive resume. */
746
+ keep_alive?: true;
730
747
  message_id?: string;
731
748
  monitor_event?: true;
732
749
  /**
@@ -225,7 +225,7 @@ export const LANE_FIELD_WRITERS = {
225
225
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
226
226
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
227
227
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
228
- 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_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
+ 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_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
229
229
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
230
230
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command"],
231
231
  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"],
@@ -41,6 +41,7 @@ import { type EnvelopeRepairSpawnInput, type SpawnLaneDeps } from "./spawn.js";
41
41
  /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
42
42
  export declare const SILENCE_MS: number;
43
43
  export declare const WATCHER_POLL_MS = 5000;
44
+ export declare const ENVELOPE_REPAIR_CONTINUE_TEXT = "continue the task; the previous turn ended without an envelope.";
44
45
  /** Synced last_host_report_at cadence. Reconcile dead-host TTL is 20 minutes. */
45
46
  export declare const WATCHER_HEARTBEAT_MS: number;
46
47
  /**
@@ -49,7 +50,7 @@ export declare const WATCHER_HEARTBEAT_MS: number;
49
50
  * once the file would exceed this, the oldest complete lines are dropped.
50
51
  */
51
52
  export declare const WATCHER_OUT_MAX_BYTES: number;
52
- export declare const WATCHER_DECISION_EVENTS: readonly ["liveness_transition", "silence_deadline", "envelope_observed", "envelope_invalid", "envelope_repair", "classification", "terminal_write", "message_recovery", "message_recovery_failed", "message_undeliverable", "mesh_event", "mesh_event_failed"];
53
+ export declare const WATCHER_DECISION_EVENTS: readonly ["liveness_transition", "silence_deadline", "envelope_observed", "envelope_invalid", "envelope_repair", "envelope_repair_continue", "classification", "terminal_write", "message_recovery", "message_recovery_failed", "message_undeliverable", "mesh_event", "mesh_event_failed"];
53
54
  /** Terminal reason when the envelope failed to parse after the repair turn (spec §4). */
54
55
  export { ENVELOPE_INVALID_REASON };
55
56
  export type WatcherDecisionEvent = (typeof WATCHER_DECISION_EVENTS)[number];
@@ -232,11 +233,11 @@ export interface WatcherLoopDeps {
232
233
  /** Deliver a lane-senior escalation through the normal lane-message router. */
233
234
  deliverLaneSeniorMessage?: (seniorLaneId: string, text: string) => Promise<void>;
234
235
  /** Test seam; production reads the company-scoped hq-flags row. */
235
- readManagerStatusGateFlag?: typeof readLanesRegistryFlag;
236
- /** Test seam; production reads the company-scoped hq-flags row. */
237
236
  readBlockerFreshnessFlag?: typeof readLanesRegistryFlag;
238
237
  /** Test seam; production reads the company-scoped hq-flags row. */
239
238
  readKrFreshnessFlag?: typeof readLanesRegistryFlag;
239
+ /** Test seam; production reads the company-scoped hq-flags row. */
240
+ readEnvelopeRepairContinueFlag?: typeof readLanesRegistryFlag;
240
241
  }
241
242
  /**
242
243
  * Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
@@ -37,7 +37,7 @@ import { LanesError } from "./errors.js";
37
37
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
38
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
39
39
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
40
- import { appendCappedHistory, readLane, retryUpdateLane, withLaneHireLocks } from "./store.js";
40
+ import { appendCappedHistory, listQuestionIds, readLane, readQuestion, retryUpdateLane, withLaneHireLocks } from "./store.js";
41
41
  import { enforceRegistryApprovalGate } from "./approval-gate.js";
42
42
  import { readWorkerRunResult } from "./delivery.js";
43
43
  import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
@@ -51,13 +51,15 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
51
51
  import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
52
52
  import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "./blocker-freshness.js";
53
53
  import { krFreshnessFingerprint, resolveKrFreshnessForLane, } from "./kr-freshness.js";
54
- import { MANAGER_STATUS_GATE_FLAG, managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, managerStatusGateEnabledForLane, } from "./manager-status-gate.js";
54
+ import { managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, } from "./manager-status-gate.js";
55
55
  import { queueLaneTerminalResume, queueManagerStatusGateResume } from "./keep-alive.js";
56
+ import { LANE_ENVELOPE_REPAIR_CONTINUE_FLAG } from "./flag-keys.js";
56
57
  import { monitorSessionDir } from "../monitor.js";
57
58
  import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, writePidRecord, retryLaneOnAlternateAccount, } from "./spawn.js";
58
59
  /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
59
60
  export const SILENCE_MS = 20 * 60 * 1000;
60
61
  export const WATCHER_POLL_MS = 5_000;
62
+ export const ENVELOPE_REPAIR_CONTINUE_TEXT = "continue the task; the previous turn ended without an envelope.";
61
63
  /** Synced last_host_report_at cadence. Reconcile dead-host TTL is 20 minutes. */
62
64
  export const WATCHER_HEARTBEAT_MS = 60 * 1000;
63
65
  /**
@@ -72,6 +74,7 @@ export const WATCHER_DECISION_EVENTS = [
72
74
  "envelope_observed",
73
75
  "envelope_invalid",
74
76
  "envelope_repair",
77
+ "envelope_repair_continue",
75
78
  "classification",
76
79
  "terminal_write",
77
80
  "message_recovery",
@@ -659,26 +662,9 @@ export function missingExpectedEnvelopeFile(hqRoot, laneId, envelopePath) {
659
662
  };
660
663
  }
661
664
  async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, logDecision) {
662
- const readFlag = deps.readManagerStatusGateFlag ?? readLanesRegistryFlag;
663
- let enabled = false;
664
- try {
665
- const resolution = resolveLaneCompany(hqRoot, lane);
666
- const company = resolution.company ?? undefined;
667
- enabled = await readFlag(MANAGER_STATUS_GATE_FLAG, {
668
- company,
669
- ...(company ? {
670
- companyUidSource: laneCompanyUidSource(resolution.source),
671
- } : {}),
672
- });
673
- }
674
- catch (err) {
675
- process.stderr.write(`hq lanes watcher: manager status gate flag read failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
676
- logDecision({ event: "status_gate_flag_failed", at, error: err instanceof Error ? err.name : "UnknownError" });
677
- }
678
- if (!enabled)
679
- return lane;
680
665
  let target;
681
666
  try {
667
+ resolveLaneCompany(hqRoot, lane);
682
668
  target = resolveManagerStatusTarget(hqRoot, lane);
683
669
  }
684
670
  catch (err) {
@@ -725,8 +711,8 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
725
711
  turn_id: gate.turn_id,
726
712
  path: gate.status_path,
727
713
  consecutive_misses: updated.status_gate?.consecutive_misses ?? 0,
728
- resume_queued: miss.resume_queued === true,
729
- resume_skipped_reason: miss.resume_skipped_reason,
714
+ resume_queued: "resume_queued" in miss && miss.resume_queued === true,
715
+ resume_skipped_reason: "resume_skipped_reason" in miss ? miss.resume_skipped_reason : undefined,
730
716
  state: updated.state,
731
717
  });
732
718
  }
@@ -1058,6 +1044,92 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
1058
1044
  });
1059
1045
  };
1060
1046
  }
1047
+ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, envelopeRef, at, deps, logDecision) {
1048
+ const repair = lane.envelope_repair;
1049
+ const missingRepair = repair?.attempted === true &&
1050
+ repair.succeeded === true &&
1051
+ repair.envelope_ref === envelopeRef &&
1052
+ repair.first_error.startsWith("Missing expected envelope file:");
1053
+ if (!missingRepair || envelope === null || envelopeRef === undefined) {
1054
+ if (envelope?.decision === "blocked")
1055
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "not_a_successful_missing_repair", missing_repair: missingRepair, envelope_ref: envelopeRef, repair_ref: repair?.envelope_ref, first_error: repair?.first_error, succeeded: repair?.succeeded });
1056
+ return lane;
1057
+ }
1058
+ const incompleteDone = envelope.decision === "done" && envelope.story_complete === false;
1059
+ const repairInstructionBlock = envelope.decision === "blocked" &&
1060
+ (envelope.questions?.length ?? 0) === 0 &&
1061
+ /(?:not to resume|do not resume|don't resume)(?: the)? task/i.test(envelope.blocked_reason ?? "");
1062
+ if (!incompleteDone && !repairInstructionBlock) {
1063
+ if (envelope.decision === "blocked")
1064
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "blocked_reason_not_repair_instruction", blocked_reason: envelope.blocked_reason });
1065
+ return lane;
1066
+ }
1067
+ if (lane.last_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT) {
1068
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "continue_already_used" });
1069
+ return lane;
1070
+ }
1071
+ let pendingQuestion = false;
1072
+ try {
1073
+ for (const id of listQuestionIds(hqRoot)) {
1074
+ const question = readQuestion(hqRoot, id);
1075
+ if (question.lane_id === lane.lane_id && question.status === "pending") {
1076
+ pendingQuestion = true;
1077
+ break;
1078
+ }
1079
+ }
1080
+ }
1081
+ catch (error) {
1082
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "question_check_failed", error: error instanceof Error ? error.message : String(error) });
1083
+ return lane;
1084
+ }
1085
+ if (pendingQuestion) {
1086
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "pending_question" });
1087
+ return lane;
1088
+ }
1089
+ const readFlag = deps.readEnvelopeRepairContinueFlag ?? readLanesRegistryFlag;
1090
+ let enabled = false;
1091
+ try {
1092
+ const resolution = resolveLaneCompany(hqRoot, lane);
1093
+ const company = resolution.company ?? undefined;
1094
+ enabled = await readFlag(LANE_ENVELOPE_REPAIR_CONTINUE_FLAG, {
1095
+ company,
1096
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
1097
+ });
1098
+ }
1099
+ catch (error) {
1100
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "flag_read_failed", error: error instanceof Error ? error.message : String(error) });
1101
+ return lane;
1102
+ }
1103
+ if (!enabled)
1104
+ return lane;
1105
+ const queued = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1106
+ if (current.version !== lane.version ||
1107
+ (current.state !== "done" && current.state !== "blocked") ||
1108
+ current.last_envelope?.ref !== envelopeRef ||
1109
+ current.envelope_repair?.attempted !== true ||
1110
+ current.envelope_repair.succeeded !== true ||
1111
+ current.envelope_repair.envelope_ref !== envelopeRef ||
1112
+ current.envelope_repair.first_error !== repair.first_error ||
1113
+ current.last_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT)
1114
+ return current;
1115
+ const result = queueLaneTerminalResume(current, at, ENVELOPE_REPAIR_CONTINUE_TEXT, { allowBlockedResume: true });
1116
+ if (!result.resumeQueued || result.lane.pending_resume?.session_id === undefined)
1117
+ return current;
1118
+ return {
1119
+ ...result.lane,
1120
+ status_gate_history: appendCappedHistory(current.status_gate_history ?? [], {
1121
+ event: "envelope_repair_continued",
1122
+ at,
1123
+ envelope_ref: envelopeRef,
1124
+ session_id: result.lane.pending_resume.session_id,
1125
+ }),
1126
+ };
1127
+ });
1128
+ if (queued.state === "queued" && queued.pending_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT) {
1129
+ logDecision({ event: "envelope_repair_continue", at, outcome: "queued", envelope_ref: envelopeRef, session_id: queued.pending_resume.session_id });
1130
+ }
1131
+ return queued;
1132
+ }
1061
1133
  /** Detached and synchronous classifiers share one process-identity claim. */
1062
1134
  export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
1063
1135
  const release = claimWatcherLoop(hqRoot, laneId, deps);
@@ -1144,17 +1216,10 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1144
1216
  }
1145
1217
  const progressIntervalMs = deps.progressEventMinIntervalMs ?? readProgressIntervalMs(hqRoot, readLaneSoft(hqRoot, laneId));
1146
1218
  const spawnRepair = deps.spawnRepair ?? spawnEnvelopeRepair;
1147
- const retryAccount = deps.retryAccount ?? (async (root, lane, reason) => {
1148
- const managerStatusGateEnabled = await managerStatusGateEnabledForLane(root, lane, {
1149
- env: deps.retrySpawnDeps?.env,
1150
- readFlag: deps.readManagerStatusGateFlag,
1151
- });
1152
- return retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
1153
- ...deps.retrySpawnDeps,
1154
- managerStatusGateEnabled,
1155
- admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
1156
- });
1157
- });
1219
+ const retryAccount = deps.retryAccount ?? ((root, lane, reason) => retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
1220
+ ...deps.retrySpawnDeps,
1221
+ admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
1222
+ }));
1158
1223
  let lastSignalMs = now();
1159
1224
  let lastSeenMtime = 0;
1160
1225
  let lastAlive = null;
@@ -1542,7 +1607,10 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1542
1607
  ((entry.event === "status_missed" && entry.resume_queued === true) ||
1543
1608
  (entry.event === "blocker_stale" && entry.resume_queued === true) ||
1544
1609
  (entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
1545
- if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued) {
1610
+ if (terminalWrite.changed && !managerStatusFollowupQueued) {
1611
+ updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, deps, logDecision);
1612
+ }
1613
+ if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued && updated.state !== "queued") {
1546
1614
  try {
1547
1615
  if (dropboxInboxHasMessages(hqRoot, laneId)) {
1548
1616
  await deps.recoverQueuedMessages(updated);
@@ -701,13 +701,30 @@ export async function runMeshDaemon(deps = {}) {
701
701
  if (!fs.existsSync(spoolPath)) {
702
702
  fs.writeFileSync(spoolPath, "", { mode: 0o600 });
703
703
  }
704
- watcher = fs.watch(path.dirname(spoolPath), { persistent: true }, (_event, filename) => {
704
+ const spoolWatcher = fs.watch(path.dirname(spoolPath), { persistent: true }, (_event, filename) => {
705
705
  if (stopped)
706
706
  return;
707
707
  if (shouldFlushOnSpoolDirEvent(filename)) {
708
708
  scheduleDebouncedFlush();
709
709
  }
710
710
  });
711
+ watcher = spoolWatcher;
712
+ spoolWatcher.on("error", (err) => {
713
+ // fs.watch can fail asynchronously after returning a watcher. Without
714
+ // an error listener Node treats EMFILE as an uncaught exception; the
715
+ // periodic flush below already provides a polling fallback.
716
+ if (watcher === spoolWatcher)
717
+ watcher = null;
718
+ try {
719
+ spoolWatcher.close();
720
+ }
721
+ catch (closeError) {
722
+ const closeMessage = closeError instanceof Error ? closeError.message : String(closeError);
723
+ log(dir, `fs.watch close failed after watcher error: ${closeMessage.slice(0, 120)}`);
724
+ }
725
+ const msg = err instanceof Error ? err.message : String(err);
726
+ log(dir, `fs.watch error; continuing interval flush: ${msg.slice(0, 120)}`);
727
+ });
711
728
  }
712
729
  catch (err) {
713
730
  const msg = err instanceof Error ? err.message : String(err);
@@ -237,6 +237,12 @@ export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?:
237
237
  home?: string;
238
238
  env?: NodeJS.ProcessEnv;
239
239
  }): Promise<T>;
240
+ /**
241
+ * Ensure the current machine identity has a valid session and return only the
242
+ * access/ID token pair needed by supported machine-session handoff scripts.
243
+ * The machine state directory is isolated from the person's session cache.
244
+ */
245
+ export declare function ensureMachineSessionTokens(): Promise<Pick<CognitoTokens, "accessToken" | "idToken" | "expiresAt">>;
240
246
  /**
241
247
  * True when a person-token refresh failed with a definitive Cognito auth
242
248
  * rejection (refresh token invalid/revoked). Transient network/5xx/timeouts
@@ -840,6 +840,15 @@ function withTokenStateDirSection(fn) {
840
840
  async function ensureMachineTokens() {
841
841
  return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
842
842
  }
843
+ /**
844
+ * Ensure the current machine identity has a valid session and return only the
845
+ * access/ID token pair needed by supported machine-session handoff scripts.
846
+ * The machine state directory is isolated from the person's session cache.
847
+ */
848
+ export async function ensureMachineSessionTokens() {
849
+ const tokens = await ensureMachineTokensMaybeFallback({ tokenSource: "machine" });
850
+ return { accessToken: tokens.accessToken, idToken: tokens.idToken, expiresAt: tokens.expiresAt };
851
+ }
843
852
  /**
844
853
  * Mint machine tokens (daemon state dir cache). Logs once when the fresh-box
845
854
  * default-creds fallback is what selected the machine path.
@@ -62,6 +62,8 @@ import * as fs from "node:fs";
62
62
  import * as path from "node:path";
63
63
  import semver from "semver";
64
64
  import chalk from "chalk";
65
+ import { Sentry } from "../sentry.js";
66
+ import { isCliTelemetryDisabled } from "./cli-telemetry.js";
65
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
66
68
  import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, isPrefixWritable, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
67
69
  import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
@@ -499,6 +501,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
499
501
  console.error(chalk.dim(` Or repair the install: hq rescue`));
500
502
  }
501
503
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
504
+ await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result.code, env, flavor.noun === "rescue");
502
505
  return { action: "update-failed", latest };
503
506
  }
504
507
  if (installSnapshot)
@@ -521,6 +524,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
521
524
  if (converged === false) {
522
525
  (deps.markIneffective ?? markLatestIneffective)(latest);
523
526
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
527
+ await captureSelfUpdateOutcome("update-ineffective", current, latestValid, install.manager, undefined, env, flavor.noun === "rescue");
524
528
  return { action: "update-ineffective", latest };
525
529
  }
526
530
  // A resident mesh daemon keeps the old JS loaded after a global install.
@@ -550,6 +554,33 @@ async function updateAndReexec(argv, flavor, known, deps) {
550
554
  console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
551
555
  return { action: "reexec", reexecStatus: status, latest };
552
556
  }
557
+ const SELF_UPDATE_ERROR_CLASSES = new Set(["MODULE_NOT_FOUND", "EACCES", "EEXIST"]);
558
+ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, code, env, flushBeforeExit) {
559
+ if (isCliTelemetryDisabled(env))
560
+ return;
561
+ const error = new Error(action === "update-failed" ? "Self-update failed" : "Self-update ineffective");
562
+ // This event needs no stack: frames would expose the machine's install path.
563
+ error.stack = undefined;
564
+ Sentry.captureException(error, {
565
+ fingerprint: ["self-update", action],
566
+ tags: {
567
+ from_version: fromVersion,
568
+ target_version: targetVersion,
569
+ install_flavor: installFlavor,
570
+ error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code) ? code : "other",
571
+ },
572
+ });
573
+ if (flushBeforeExit) {
574
+ try {
575
+ if (!(await Sentry.flush(500))) {
576
+ console.error(chalk.dim("hq: Sentry flush timed out after self-update."));
577
+ }
578
+ }
579
+ catch {
580
+ console.error(chalk.dim("hq: Sentry flush failed after self-update."));
581
+ }
582
+ }
583
+ }
553
584
  async function readStagedSelfUpdateFlag(deps, env) {
554
585
  try {
555
586
  if (deps.stagedSelfUpdate)
@@ -706,13 +706,32 @@ export function detachUpdateSupervisor() {
706
706
  try {
707
707
  rmSync(tempDir, { recursive: true, force: true });
708
708
  }
709
- catch {
710
- // Best-effort cleanup of a temp dir.
709
+ catch (error) {
710
+ const failureClass = error &&
711
+ typeof error === "object" &&
712
+ "code" in error &&
713
+ typeof error.code === "string" &&
714
+ /^[A-Z0-9_]{1,32}$/.test(error.code)
715
+ ? error.code
716
+ : error instanceof Error
717
+ ? "Error"
718
+ : "UnknownError";
719
+ console.error(chalk.yellow(`Could not remove update supervisor temp dir (${failureClass}).`));
711
720
  }
712
721
  },
713
722
  };
714
723
  }
715
- catch {
724
+ catch (error) {
725
+ const failureClass = error &&
726
+ typeof error === "object" &&
727
+ "code" in error &&
728
+ typeof error.code === "string" &&
729
+ /^[A-Z0-9_]{1,32}$/.test(error.code)
730
+ ? error.code
731
+ : error instanceof Error
732
+ ? "Error"
733
+ : "UnknownError";
734
+ console.error(chalk.yellow(`Could not copy update supervisor (${failureClass}).`));
716
735
  if (dir)
717
736
  rmSync(dir, { recursive: true, force: true });
718
737
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.294.0",
3
+ "version": "5.295.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,8 +0,0 @@
1
- export interface FastLanesListRouteDeps {
2
- flagEnabled: (flagKey: string, env: NodeJS.ProcessEnv) => Promise<boolean>;
3
- runFastList: () => Promise<void>;
4
- runPlacementList: () => Promise<void>;
5
- }
6
- /** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
7
- export declare function runFastLanesListRequest(argv: readonly string[], env?: NodeJS.ProcessEnv, deps?: FastLanesListRouteDeps): Promise<boolean>;
8
- //# sourceMappingURL=fast-lanes-list-route.d.ts.map
@@ -1,32 +0,0 @@
1
- import { isFastCachedLanesListRequest } from "./fast-scaffold.js";
2
- import { fastFlagEnabled } from "./native-fast-route.js";
3
- import { registerOutpostLaneSnapshotRefreshChild } from "./lib/lanes/outpost-snapshot-refresh.js";
4
- import { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG } from "./lib/lanes/outpost-hire-cache.js";
5
- import { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lib/lanes/flag-keys.js";
6
- /** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
7
- export async function runFastLanesListRequest(argv, env = process.env, deps = {
8
- flagEnabled: fastFlagEnabled,
9
- runFastList: async () => {
10
- const { runFastCachedLanesList } = await import("./fast-lanes-list.js");
11
- await runFastCachedLanesList();
12
- },
13
- runPlacementList: async () => {
14
- const { runFastCachedLanesListFromEntrypoint } = await import("./commands/lanes.js");
15
- await runFastCachedLanesListFromEntrypoint();
16
- },
17
- }) {
18
- registerOutpostLaneSnapshotRefreshChild(env);
19
- if (!isFastCachedLanesListRequest(argv))
20
- return false;
21
- if (env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1")
22
- return false;
23
- if (!await deps.flagEnabled(LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, env))
24
- return false;
25
- const lanePlacementEnabled = await deps.flagEnabled(LANE_OUTPOST_LOAD_BALANCING_FLAG, env);
26
- if (lanePlacementEnabled)
27
- await deps.runPlacementList();
28
- else
29
- await deps.runFastList();
30
- return true;
31
- }
32
- //# sourceMappingURL=fast-lanes-list-route.js.map
@@ -1,3 +0,0 @@
1
- /** Lightweight implementation for the exact cache-first `lanes list --json` entrypoint. */
2
- export declare function runFastCachedLanesList(): Promise<void>;
3
- //# sourceMappingURL=fast-lanes-list.d.ts.map