@indigoai-us/hq-cli 5.345.8 → 5.345.10

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 (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/commands/lanes-message.js +100 -75
  3. package/dist/commands/lanes.js +52 -4
  4. package/dist/lib/bot/coding-tool-activity.d.ts +2 -8
  5. package/dist/lib/bot/coding-tool-activity.js +1 -5
  6. package/dist/lib/bot/setup-tool-offer.d.ts +0 -4
  7. package/dist/lib/bot/setup-tool-offer.js +2 -2
  8. package/dist/lib/checkpoint-ontology/tree.d.ts +0 -1
  9. package/dist/lib/checkpoint-ontology/tree.js +1 -1
  10. package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +0 -2
  11. package/dist/lib/core-utils/qmd-reindex-after-sync.js +2 -2
  12. package/dist/lib/daemon/convergence.d.ts +0 -2
  13. package/dist/lib/daemon/convergence.js +2 -2
  14. package/dist/lib/daemon/flag-monitor.d.ts +0 -2
  15. package/dist/lib/daemon/flag-monitor.js +2 -2
  16. package/dist/lib/daemon/heartbeat.d.ts +0 -4
  17. package/dist/lib/daemon/heartbeat.js +3 -3
  18. package/dist/lib/daemon/legacy-sync-env.d.ts +0 -1
  19. package/dist/lib/daemon/legacy-sync-env.js +1 -1
  20. package/dist/lib/daemon/ops/memory.d.ts +0 -2
  21. package/dist/lib/daemon/ops/memory.js +2 -2
  22. package/dist/lib/daemon/run.d.ts +1 -1
  23. package/dist/lib/daemon/run.js +11 -2
  24. package/dist/lib/daemon/supervisor.d.ts +0 -1
  25. package/dist/lib/daemon/supervisor.js +1 -1
  26. package/dist/lib/daemon/sync-runner-bundle.d.ts +2 -1
  27. package/dist/lib/daemon/sync-runner-bundle.js +1 -1
  28. package/dist/lib/daemon/sync-runner.d.ts +2 -1
  29. package/dist/lib/doctor/checks/anywhere.d.ts +3 -4
  30. package/dist/lib/doctor/checks/anywhere.js +2 -2
  31. package/dist/lib/doctor/checks/runtime-probe.d.ts +2 -16
  32. package/dist/lib/doctor/checks/runtime-probe.js +2 -2
  33. package/dist/lib/doctor/fix/json-output.d.ts +0 -1
  34. package/dist/lib/doctor/fix/json-output.js +1 -1
  35. package/dist/lib/doctor/platform.d.ts +5 -4
  36. package/dist/lib/install/claude-targets.d.ts +0 -4
  37. package/dist/lib/install/claude-targets.js +3 -3
  38. package/dist/lib/install/codex-targets.d.ts +0 -9
  39. package/dist/lib/install/codex-targets.js +4 -4
  40. package/dist/lib/lanes/blocker-freshness.d.ts +0 -2
  41. package/dist/lib/lanes/blocker-freshness.js +2 -2
  42. package/dist/lib/lanes/config.d.ts +1 -5
  43. package/dist/lib/lanes/config.js +4 -4
  44. package/dist/lib/lanes/keep-alive.d.ts +0 -4
  45. package/dist/lib/lanes/keep-alive.js +3 -3
  46. package/dist/lib/lanes/kr-freshness.d.ts +2 -4
  47. package/dist/lib/lanes/kr-freshness.js +4 -4
  48. package/dist/lib/lanes/outpost-pruner-install.d.ts +0 -3
  49. package/dist/lib/lanes/outpost-pruner-install.js +3 -3
  50. package/dist/lib/lanes/reviews.d.ts +3 -4
  51. package/dist/lib/lanes/reviews.js +3 -3
  52. package/dist/lib/lanes/store.js +1 -1
  53. package/dist/lib/lanes/types.d.ts +1 -1
  54. package/dist/lib/mesh/live/report-summary.d.ts +0 -10
  55. package/dist/lib/mesh/live/report-summary.js +5 -5
  56. package/dist/lib/work-context/bind-best.d.ts +0 -19
  57. package/dist/lib/work-context/bind-best.js +3 -3
  58. package/dist/lib/work-context/organize.d.ts +3 -11
  59. package/dist/lib/work-context/organize.js +3 -3
  60. package/dist/utils/pack-contributions.d.ts +1 -3
  61. package/dist/utils/pack-contributions.js +0 -5
  62. package/dist/utils/package-use-lease.d.ts +0 -7
  63. package/dist/utils/package-use-lease.js +1 -1
  64. package/dist/utils/resolve-executable.d.ts +0 -1
  65. package/dist/utils/resolve-executable.js +1 -1
  66. package/dist/utils/version-gate.d.ts +2 -2
  67. package/dist/utils/version-gate.js +2 -2
  68. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.10] — 2026-10-05
6
+ - Keep a healthy recorded Claude account for lane messages when every worker-profile-pinned account is unavailable.
7
+
8
+ ## [5.345.9] — 2026-10-05
9
+ - Lane messages to exited sessions now queue durably when provider admission is busy, so delivery succeeds and reconcile can resume them later.
10
+ - Keep the current sync runner until an hq-cloud version matching the configured pin is available.
11
+
5
12
  ## [5.345.8] — 2026-10-05
6
13
  - Prioritize due lead keep-alive retries and honor due next-check times ahead of later backoff deadlines behind an hq-flags gate.
7
14
  - Drain bot turns before replacing a running bot after a daemon plan change.
@@ -754,6 +754,59 @@ export function createLaneMessageHandlers(deps) {
754
754
  ...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
755
755
  });
756
756
  }
757
+ const queueResumeMessage = (queuedReason, archiveBeforeWrite) => {
758
+ const sessionId = lane.provider_session_id;
759
+ if (!sessionId) {
760
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message`, {
761
+ lane_id: laneId,
762
+ provider: lane.provider,
763
+ });
764
+ }
765
+ if (archiveBeforeWrite)
766
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
767
+ const queued = casUpdateLane(hqRoot, laneId, (current) => {
768
+ if (current.version !== lane.version ||
769
+ current.state !== lane.state ||
770
+ current.provider_session_id !== sessionId) {
771
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while its message was queued`, {
772
+ lane_id: laneId,
773
+ prepared_version: lane.version,
774
+ version: current.version,
775
+ state: current.state,
776
+ });
777
+ }
778
+ return {
779
+ ...current,
780
+ state: "queued",
781
+ queued_reason: queuedReason,
782
+ queued_details: undefined,
783
+ queued_at: at,
784
+ admission_reserved_at: undefined,
785
+ pending_resume: {
786
+ session_id: sessionId,
787
+ trigger: messageHistoryTrigger,
788
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
789
+ text: deliveryText,
790
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
791
+ message_id: messageId,
792
+ compact: opts.compact !== false,
793
+ at,
794
+ },
795
+ };
796
+ });
797
+ queuedOnAdmission = true;
798
+ storySwitchDeliveryStarted = true;
799
+ deps.onLaneMessageQueued?.(laneId, "admission");
800
+ if (!archiveBeforeWrite) {
801
+ try {
802
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
803
+ }
804
+ catch (archiveError) {
805
+ process.stderr.write(`Could not write the resume message audit copy for ${laneId}: ${archiveError instanceof Error ? archiveError.message : String(archiveError)}\n`);
806
+ }
807
+ }
808
+ return queued;
809
+ };
757
810
  let questionCancellation = null;
758
811
  if (move?.queued || queuedOnQuotaHold) {
759
812
  // Parked at the target's cap: reconcile resumes the session there.
@@ -777,84 +830,56 @@ export function createLaneMessageHandlers(deps) {
777
830
  // Exited worker with a recorded session: queue the message when capacity
778
831
  // is occupied, otherwise start its next turn under the shared admission lock.
779
832
  assertLaneRunsHereForAdmission(ctx, lane);
780
- return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
781
- const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
782
- if (admission.action === "refuse" && admission.error)
783
- throw admission.error;
784
- return withResumeHireAdmission(ctx, lane, () => {
785
- if (admission.action === "queue") {
786
- const sessionId = lane.provider_session_id;
787
- if (!sessionId) {
788
- throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
789
- lane_id: laneId,
790
- provider: lane.provider,
791
- });
833
+ try {
834
+ return await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
835
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
836
+ if (admission.action === "refuse" && admission.error)
837
+ throw admission.error;
838
+ return withResumeHireAdmission(ctx, lane, () => {
839
+ if (admission.action === "queue") {
840
+ return queueResumeMessage(admission.reason ?? "throttle", true);
792
841
  }
793
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
794
- queuedOnAdmission = true;
795
- const queued = casUpdateLane(hqRoot, laneId, (current) => {
796
- if (current.version !== lane.version || current.state !== lane.state) {
797
- throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
798
- lane_id: laneId,
799
- prepared_version: lane.version,
800
- version: current.version,
801
- state: current.state,
802
- });
803
- }
804
- return {
805
- ...current,
806
- state: "queued",
807
- queued_reason: admission.reason ?? "throttle",
808
- queued_details: undefined,
809
- queued_at: at,
810
- pending_resume: {
811
- session_id: sessionId,
812
- trigger: messageHistoryTrigger,
813
- caller: ctx.env.HQ_LANE_ID?.trim() || "user",
814
- text: deliveryText,
815
- ...(isMonitorEvent ? { monitor_event: true } : {}),
816
- message_id: messageId,
817
- compact: opts.compact !== false,
818
- at,
819
- },
820
- };
842
+ const started = spawnLaneWorker(hqRoot, lane, {
843
+ env: ctx.env,
844
+ modelDeps: {
845
+ readFileSync: ctx.readFileSync,
846
+ probe: ctx.probeProvider,
847
+ env: ctx.env,
848
+ },
849
+ existsSync: ctx.existsSync,
850
+ spawnProcess: ctx.spawnProcess,
851
+ proveDetached: ctx.proveDetached,
852
+ killProcess: ctx.killProcess,
853
+ now: ctx.now,
854
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
855
+ runCompaction: ctx.runCompaction,
856
+ locateTranscript: ctx.locateTranscript,
857
+ }, {
858
+ trigger: {
859
+ kind: "message",
860
+ text: deliveryText,
861
+ message_id: messageId,
862
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
863
+ ...(isSystemNotice ? { system_notice: true } : {}),
864
+ },
865
+ compact: opts.compact !== false,
866
+ effort: resumeEffort ?? recordedResumeEffort(lane),
821
867
  });
822
868
  storySwitchDeliveryStarted = true;
823
- deps.onLaneMessageQueued?.(laneId, "admission");
824
- return queued;
825
- }
826
- const started = spawnLaneWorker(hqRoot, lane, {
827
- env: ctx.env,
828
- modelDeps: {
829
- readFileSync: ctx.readFileSync,
830
- probe: ctx.probeProvider,
831
- env: ctx.env,
832
- },
833
- existsSync: ctx.existsSync,
834
- spawnProcess: ctx.spawnProcess,
835
- proveDetached: ctx.proveDetached,
836
- killProcess: ctx.killProcess,
837
- now: ctx.now,
838
- resolveMachineIdentity: ctx.resolveMachineIdentity,
839
- runCompaction: ctx.runCompaction,
840
- locateTranscript: ctx.locateTranscript,
841
- }, {
842
- trigger: {
843
- kind: "message",
844
- text: deliveryText,
845
- message_id: messageId,
846
- ...(isMonitorEvent ? { monitor_event: true } : {}),
847
- ...(isSystemNotice ? { system_notice: true } : {}),
848
- },
849
- compact: opts.compact !== false,
850
- effort: resumeEffort ?? recordedResumeEffort(lane),
851
- });
852
- storySwitchDeliveryStarted = true;
853
- deps.onLaneWorkerStarted?.(laneId, "message");
854
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
855
- return started;
856
- }, hireGrantContext);
857
- }, ctx.sleep);
869
+ deps.onLaneWorkerStarted?.(laneId, "message");
870
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
871
+ return started;
872
+ }, hireGrantContext);
873
+ }, ctx.sleep);
874
+ }
875
+ catch (error) {
876
+ const expectedLockPath = path.join(hqRoot, "workspace", "lanes-runs", ".admission-locks", `${lane.provider}-provider.lock`);
877
+ if (!(error instanceof LanesError) ||
878
+ error.error !== "store_lock_timeout" ||
879
+ error.details.path !== expectedLockPath)
880
+ throw error;
881
+ return queueResumeMessage(undefined, false);
882
+ }
858
883
  },
859
884
  }, mechanism);
860
885
  if (dispatched.mechanism === "resume")
@@ -6791,18 +6791,62 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6791
6791
  (model === undefined || currentConfig.model === undefined || currentConfig.model === model ||
6792
6792
  lane.account === recordedModelAccount))
6793
6793
  return undefined;
6794
- const selected = selectClaudeAccount({
6794
+ const selectionInput = {
6795
6795
  hosts: balanceHosts.snapshots,
6796
6796
  targetHost: lane.host,
6797
6797
  config,
6798
6798
  quotaState: loadQuotaState(hqRoot),
6799
6799
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
6800
6800
  now: ctx.now(),
6801
- ...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
6802
6801
  ...(model !== undefined ? { model } : {}),
6803
6802
  ...(recordedModelAccount !== undefined ? { recordedModelAccount } : {}),
6804
6803
  unavailableHosts: balanceHosts.unavailableHosts,
6805
- });
6804
+ };
6805
+ let selected;
6806
+ let profilePinUnavailable = false;
6807
+ try {
6808
+ selected = selectClaudeAccount({
6809
+ ...selectionInput,
6810
+ ...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
6811
+ });
6812
+ }
6813
+ catch (err) {
6814
+ const accountReasons = err instanceof LanesError &&
6815
+ err.error === "claude_account_balance_no_eligible_account" &&
6816
+ Array.isArray(err.details.account_reasons)
6817
+ ? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
6818
+ : [];
6819
+ const unavailablePinReasons = new Set(["disabled", "home_missing", "not_logged_in", "model_mismatch", "quota_held"]);
6820
+ const everyProfileAccountUnavailable = profilePin !== undefined && profilePin.length > 0 &&
6821
+ profilePin.every((account) => accountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
6822
+ if (!everyProfileAccountUnavailable || lane.account === undefined ||
6823
+ (lane.admission_accounts !== undefined && !lane.admission_accounts.includes(lane.account)))
6824
+ throw err;
6825
+ const recordedSelection = selectClaudeAccount({
6826
+ ...selectionInput,
6827
+ allowedAccounts: [lane.account],
6828
+ });
6829
+ selected = {
6830
+ ...recordedSelection,
6831
+ considered: recordedSelection.considered.map((entry) => {
6832
+ const pinnedReason = profilePin.includes(entry.account)
6833
+ ? accountReasons.find((reason) => reason.account === entry.account)?.reason
6834
+ : undefined;
6835
+ return typeof pinnedReason === "string"
6836
+ ? { ...entry, eligible: false, ineligible_reason: pinnedReason }
6837
+ : entry;
6838
+ }),
6839
+ skipped_accounts: [
6840
+ ...recordedSelection.skipped_accounts.filter((entry) => !profilePin.includes(entry.account)),
6841
+ ...profilePin.map((account) => ({
6842
+ account,
6843
+ host: lane.host,
6844
+ reason: String(accountReasons.find((entry) => entry.account === account)?.reason),
6845
+ })),
6846
+ ],
6847
+ };
6848
+ profilePinUnavailable = true;
6849
+ }
6806
6850
  const historyReason = lane.account === undefined
6807
6851
  ? "load_balance"
6808
6852
  : currentConfig?.enabled !== true
@@ -6815,7 +6859,11 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6815
6859
  ? "usage_limit"
6816
6860
  : "load_balance";
6817
6861
  return {
6818
- selection: { ...selected, selected_at: ctx.now().toISOString() },
6862
+ selection: {
6863
+ ...selected,
6864
+ ...(profilePinUnavailable ? { reason: "profile_pin_unavailable" } : {}),
6865
+ selected_at: ctx.now().toISOString(),
6866
+ },
6819
6867
  historyReason,
6820
6868
  ...(preservedModel !== undefined ? { preservedModel } : {}),
6821
6869
  };
@@ -45,7 +45,7 @@ export interface ActivityThreshold {
45
45
  * at least `minDistinctDays` calendar days, within the last `windowDays` days.
46
46
  */
47
47
  export declare const SIGNIFICANT_CODING_TOOL_ACTIVITY: Readonly<ActivityThreshold>;
48
- export interface ToolActivity {
48
+ interface ToolActivity {
49
49
  /** Is the desktop app installed? */
50
50
  installed: boolean;
51
51
  /** App sessions used within the window (0 when the app is not installed). */
@@ -79,15 +79,8 @@ export interface DetectCodingToolActivityInput {
79
79
  extraHqRoots?: string[];
80
80
  threshold?: ActivityThreshold;
81
81
  }
82
- /** The desktop apps, by bundle id, and the bundle names they ship under. */
83
- export declare const DESKTOP_APPS: Readonly<Record<CodingTool, {
84
- bundleId: string;
85
- bundles: readonly string[];
86
- }>>;
87
82
  /** `originator` the Codex app writes into its sessions' first line. */
88
83
  export declare const CODEX_APP_ORIGINATOR = "Codex Desktop";
89
- /** Claude Code's project folder name for a working directory. */
90
- export declare function claudeProjectDirName(cwd: string): string;
91
84
  /**
92
85
  * Is `bundleId` this Info.plist's CFBundleIdentifier? An XML plist is read by
93
86
  * key. A binary plist has no readable key/value pairing, so it matches when the
@@ -139,4 +132,5 @@ export declare function pickCodingTool(claude: {
139
132
  }, threshold?: ActivityThreshold): CodingTool | null;
140
133
  /** Look for the Claude and Codex desktop apps and their recent sessions, and pick one to offer, if any. */
141
134
  export declare function detectCodingToolActivity(input: DetectCodingToolActivityInput): CodingToolActivity;
135
+ export {};
142
136
  //# sourceMappingURL=coding-tool-activity.d.ts.map
@@ -48,7 +48,7 @@ export const SIGNIFICANT_CODING_TOOL_ACTIVITY = Object.freeze({
48
48
  minDistinctDays: 3,
49
49
  });
50
50
  /** The desktop apps, by bundle id, and the bundle names they ship under. */
51
- export const DESKTOP_APPS = Object.freeze({
51
+ const DESKTOP_APPS = Object.freeze({
52
52
  claude: { bundleId: "com.anthropic.claudefordesktop", bundles: ["Claude.app"] },
53
53
  codex: { bundleId: "com.openai.codex", bundles: ["ChatGPT.app", "Codex.app"] },
54
54
  });
@@ -65,10 +65,6 @@ const DAY_MS = 24 * 60 * 60 * 1000;
65
65
  * many days older than the window, so a resumed session is still counted.
66
66
  */
67
67
  const CODEX_FOLDER_MARGIN_DAYS = 31;
68
- /** Claude Code's project folder name for a working directory. */
69
- export function claudeProjectDirName(cwd) {
70
- return path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
71
- }
72
68
  function codexHome(home, env) {
73
69
  const override = env.CODEX_HOME?.trim();
74
70
  return override ? override : path.join(home, ".codex");
@@ -26,10 +26,6 @@ import type { CodingTool } from "./coding-tool-activity.js";
26
26
  export declare const KEEP_GOING_HERE_REPLY = "Keep going here";
27
27
  /** The hq-block kind the desktop app draws as the two-button card. */
28
28
  export declare const CONTINUE_IN_TOOL_BLOCK_KIND = "continueInTool";
29
- /** The desktop apps' product names. */
30
- export declare const CODING_TOOL_NAMES: Readonly<Record<CodingTool, string>>;
31
- /** The offer's one line. */
32
- export declare function setupToolOfferLine(tool: CodingTool): string;
33
29
  /**
34
30
  * The opening of the app's welcome, kept when the offer replaces it: its
35
31
  * first sentence (who the bot is and what setup covers). The rest of the
@@ -26,12 +26,12 @@ export const KEEP_GOING_HERE_REPLY = "Keep going here";
26
26
  /** The hq-block kind the desktop app draws as the two-button card. */
27
27
  export const CONTINUE_IN_TOOL_BLOCK_KIND = "continueInTool";
28
28
  /** The desktop apps' product names. */
29
- export const CODING_TOOL_NAMES = Object.freeze({
29
+ const CODING_TOOL_NAMES = Object.freeze({
30
30
  claude: "Claude",
31
31
  codex: "Codex",
32
32
  });
33
33
  /** The offer's one line. */
34
- export function setupToolOfferLine(tool) {
34
+ function setupToolOfferLine(tool) {
35
35
  return `Looks like you use the ${CODING_TOOL_NAMES[tool]} app. Want to continue setup there?`;
36
36
  }
37
37
  /**
@@ -25,7 +25,6 @@ export interface EntityIndex {
25
25
  /** Files under `ontology/entities/` that fail validation. Never written. */
26
26
  invalid: Set<string>;
27
27
  }
28
- export declare function emptyIndex(): EntityIndex;
29
28
  /** Add one parsed entity to the index (also used for entities created mid-run). */
30
29
  export declare function indexEntity(index: EntityIndex, entity: IndexedEntity): void;
31
30
  /** Record that the entity at `rel` answers to `alias`. */
@@ -18,7 +18,7 @@ function add(map, key, value) {
18
18
  }
19
19
  set.add(value);
20
20
  }
21
- export function emptyIndex() {
21
+ function emptyIndex() {
22
22
  return {
23
23
  byRel: new Map(),
24
24
  canonical: new Map(),
@@ -1,8 +1,6 @@
1
1
  import { type EmbedLockDependencies } from "../search-index/embed-lock.js";
2
2
  import { type LoadGateDependencies } from "../search-index/load-gate.js";
3
3
  import { type UtilityIo } from "./common.js";
4
- export declare const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
5
- export declare const QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK = false;
6
4
  export type QmdReindexOptions = UtilityIo & {
7
5
  cwd?: string;
8
6
  /** Test seams; default to the real search-index implementations. */
@@ -20,8 +20,8 @@ import { resolveProcessFlagGate } from "../flag-registry.js";
20
20
  import { qmdReindexSingleFlightEnabled } from "../search-index/reindex-single-flight-flag.js";
21
21
  import { isQmdNativeBindingError, QMD_NATIVE_BINDING_REMEDY, } from "../../utils/qmd-native-binding-error.js";
22
22
  import { ioFor, line } from "./common.js";
23
- export const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
24
- export const QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK = false;
23
+ const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
24
+ const QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK = false;
25
25
  function qmdStartupFailureWarningsEnabled() {
26
26
  return resolveProcessFlagGate(QMD_REINDEX_STARTUP_WARNING_FLAG, { fallback: QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK }, () => QMD_REINDEX_STARTUP_WARNING_FLAG_FALLBACK);
27
27
  }
@@ -7,8 +7,6 @@
7
7
  * on disk. Policy hq-self-updater-must-verify-convergence-after-install: the
8
8
  * caller reports a mismatch as a failure; it is never swallowed.
9
9
  */
10
- export declare const CONVERGENCE_TIMEOUT_MS = 10000;
11
- export declare const CONVERGENCE_POLL_MS = 500;
12
10
  export interface ConvergenceResult {
13
11
  converged: boolean;
14
12
  expected: string;
@@ -9,8 +9,8 @@
9
9
  */
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { hqdRequest } from "./protocol.js";
12
- export const CONVERGENCE_TIMEOUT_MS = 10_000;
13
- export const CONVERGENCE_POLL_MS = 500;
12
+ const CONVERGENCE_TIMEOUT_MS = 10_000;
13
+ const CONVERGENCE_POLL_MS = 500;
14
14
  /** Ping hqd over its socket and return the version it reports. */
15
15
  export async function pingHqdVersion(socketPath, timeoutMs = 2_000) {
16
16
  const result = (await hqdRequest("ping", {}, { socketPath, timeoutMs }));
@@ -1,5 +1,3 @@
1
- export declare const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30000;
2
- export declare const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60000;
3
1
  export interface BoundedDaemonFlagMonitorOptions {
4
2
  read: () => Promise<boolean>;
5
3
  now?: () => number;
@@ -1,6 +1,6 @@
1
1
  import { performance } from "node:perf_hooks";
2
- export const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30_000;
3
- export const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60_000;
2
+ const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30_000;
3
+ const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60_000;
4
4
  /**
5
5
  * Refreshes a long-running daemon's flag independently of incoming requests.
6
6
  * A failed read keeps the last successful decision only until its 60s age limit.
@@ -5,9 +5,7 @@
5
5
  * from a stale one offline. A device with no beat for 5 minutes is
6
6
  * "daemon stale".
7
7
  */
8
- export declare const HEARTBEAT_INTERVAL_MS = 60000;
9
8
  export declare const HEARTBEAT_STALE_MS: number;
10
- export declare const HEARTBEAT_FILE = "hqd-heartbeat.json";
11
9
  export declare const DAEMON_STALE_LABEL = "daemon stale";
12
10
  export interface HeartbeatState {
13
11
  at: string;
@@ -16,8 +14,6 @@ export interface HeartbeatState {
16
14
  device: string;
17
15
  }
18
16
  export declare function heartbeatFile(home?: string, env?: NodeJS.ProcessEnv): string;
19
- /** Mesh session id for this device's daemon; only characters the spool accepts. */
20
- export declare function heartbeatSessionId(hostname?: string): string;
21
17
  export declare function readHeartbeat(file: string): HeartbeatState | null;
22
18
  /** Presence label for a device given its last beat: "daemon stale" after 5 minutes of silence. */
23
19
  export declare function daemonPresenceLabel(state: HeartbeatState | null, now?: Date): "daemon live" | typeof DAEMON_STALE_LABEL;
@@ -8,16 +8,16 @@
8
8
  import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
- export const HEARTBEAT_INTERVAL_MS = 60_000;
11
+ const HEARTBEAT_INTERVAL_MS = 60_000;
12
12
  export const HEARTBEAT_STALE_MS = 5 * 60_000;
13
- export const HEARTBEAT_FILE = "hqd-heartbeat.json";
13
+ const HEARTBEAT_FILE = "hqd-heartbeat.json";
14
14
  export const DAEMON_STALE_LABEL = "daemon stale";
15
15
  export function heartbeatFile(home = os.homedir(), env = process.env) {
16
16
  const dir = env.HQ_REGISTRY_DIR?.trim() ? path.resolve(env.HQ_REGISTRY_DIR.trim()) : path.join(home, ".hq");
17
17
  return path.join(dir, HEARTBEAT_FILE);
18
18
  }
19
19
  /** Mesh session id for this device's daemon; only characters the spool accepts. */
20
- export function heartbeatSessionId(hostname = os.hostname()) {
20
+ function heartbeatSessionId(hostname = os.hostname()) {
21
21
  return `hqd-${hostname.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 48) || "device"}`;
22
22
  }
23
23
  export function readHeartbeat(file) {
@@ -4,7 +4,6 @@
4
4
  * sync limits. This module is deliberately tolerant: a bad or missing drop-in
5
5
  * must never stop daemon planning.
6
6
  */
7
- export declare const LEGACY_SYNC_DROPIN_DIR = "/etc/systemd/system/outpost-sync.service.d";
8
7
  /** Read simple KEY=VALUE and quoted "KEY=VALUE" Environment directives. */
9
8
  export declare function readLegacySyncEnv(platform: NodeJS.Platform, directory?: string): Record<string, string>;
10
9
  //# sourceMappingURL=legacy-sync-env.d.ts.map
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- export const LEGACY_SYNC_DROPIN_DIR = "/etc/systemd/system/outpost-sync.service.d";
9
+ const LEGACY_SYNC_DROPIN_DIR = "/etc/systemd/system/outpost-sync.service.d";
10
10
  function assignments(text, into) {
11
11
  let inService = false;
12
12
  for (const rawLine of text.split(/\r?\n/)) {
@@ -2,12 +2,10 @@
2
2
  * memory.get / memory.put ops (hq-anywhere US-009): session-scoped notes
3
3
  * persisted at workspace/sessions/<sid>/hqd-memory.json under HQ_ROOT.
4
4
  */
5
- export declare const MEMORY_FILE = "hqd-memory.json";
6
5
  export declare class MemoryOpError extends Error {
7
6
  readonly code: string;
8
7
  constructor(code: string, message: string);
9
8
  }
10
- export declare function memoryPath(hqRoot: string, sessionId: string): string;
11
9
  export declare function memoryPut(hqRoot: string, sessionId: string, key: string, value: unknown, now?: Date): {
12
10
  key: string;
13
11
  updatedAt: string;
@@ -5,7 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  const SESSION_RE = /^[A-Za-z0-9._-]+$/;
8
- export const MEMORY_FILE = "hqd-memory.json";
8
+ const MEMORY_FILE = "hqd-memory.json";
9
9
  const MAX_VALUE_BYTES = 256 * 1024;
10
10
  export class MemoryOpError extends Error {
11
11
  code;
@@ -15,7 +15,7 @@ export class MemoryOpError extends Error {
15
15
  this.name = "MemoryOpError";
16
16
  }
17
17
  }
18
- export function memoryPath(hqRoot, sessionId) {
18
+ function memoryPath(hqRoot, sessionId) {
19
19
  if (!SESSION_RE.test(sessionId) || sessionId === "." || sessionId === "..") {
20
20
  throw new MemoryOpError("bad_args", "sessionId may only contain letters, digits, '.', '_' and '-'");
21
21
  }
@@ -62,7 +62,7 @@ export type SyncRunnerSource = {
62
62
  export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null, staged?: {
63
63
  version: string | null;
64
64
  pinnedVersion?: string | null;
65
- }): SyncRunnerSource;
65
+ }, currentSyncRunnerSource?: SyncRunnerSource | null): SyncRunnerSource;
66
66
  /** Whether every executable in the first service plan remains usable after the CLI package is replaced. */
67
67
  export declare function daemonPlanCanReleasePackageUseLease(plans: readonly UnitPlan[], replaceablePackageRoots: readonly string[], stagingFailed: boolean, hqBootstrapReady?: boolean): boolean;
68
68
  export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
@@ -76,13 +76,20 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
76
76
  const LEGACY_PROBE_TTL_MS = 60_000;
77
77
  /** A flag read that has not answered by then counts as off. */
78
78
  const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
79
- export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }) {
79
+ export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }, currentSyncRunnerSource = null) {
80
80
  // A managed copy lives in the daemon directory, so it survives a CLI
81
81
  // replacement. Before lease release pickHqCloud returns it only when it is
82
82
  // pinned or newer than the bundled copy; after release it is the only
83
83
  // eligible package, so it must not displace a newer staged bundle unless it
84
84
  // is the pinned version. The staged bundle is built from the bundled copy and
85
85
  // otherwise only stands in for that copy (hq-cli#1547).
86
+ const pin = staged.pinnedVersion;
87
+ const hasRunnerAtOrAbovePin = pin != null &&
88
+ ((staged.version !== null && !hqCloudVersionOlder(staged.version, pin)) ||
89
+ (hqCloud !== null && !hqCloudVersionOlder(hqCloud.version, pin)));
90
+ if (pin && !hasRunnerAtOrAbovePin && currentSyncRunnerSource?.runnerPath) {
91
+ return currentSyncRunnerSource;
92
+ }
86
93
  const managedYieldsToStaged = stagedRunnerPath !== null &&
87
94
  hqCloud?.source === "managed" &&
88
95
  hqCloud.version !== staged.pinnedVersion &&
@@ -373,6 +380,7 @@ export async function runDaemon(opts) {
373
380
  let hqChildBootstrapStageFailed = false;
374
381
  let legacySyncRunnerPathChecked = false;
375
382
  let legacySyncRunnerPath = null;
383
+ let lastPlannedSyncRunnerSource = null;
376
384
  let packageUseLeaseReleased = false;
377
385
  let hqCloudVersionBeforeRelease = null;
378
386
  let startupPlanOpen = true;
@@ -570,7 +578,8 @@ export async function runDaemon(opts) {
570
578
  legacySyncRunnerPath = resolveSyncRunnerPath();
571
579
  legacySyncRunnerPathChecked = true;
572
580
  }
573
- const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath, { version: staged?.metadata.version ?? null, pinnedVersion: config.hqCloudVersion ?? null });
581
+ const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath, { version: staged?.metadata.version ?? null, pinnedVersion: config.hqCloudVersion ?? null }, lastPlannedSyncRunnerSource);
582
+ lastPlannedSyncRunnerSource = syncSource.runnerPath ? syncSource : null;
574
583
  if (!syncSource.runnerPath && !warnedMissingSyncRunner) {
575
584
  warnedMissingSyncRunner = true;
576
585
  log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
@@ -26,7 +26,6 @@ import type { MachineType } from "./machine-type.js";
26
26
  import type { ControlRequest } from "./requests.js";
27
27
  import { syncPassResult } from "./sync-runner.js";
28
28
  export declare const STOP_ESCALATION_MS = 10000;
29
- export declare const CLI_UPDATE_SYNC_IDLE_MAX_MS: number;
30
29
  export declare const CLI_UPDATE_SYNC_IDLE_KILL_GRACE_MS = 60000;
31
30
  /** After a handoff, how long a replaced unit must stay up before the next one is replaced. */
32
31
  export declare const HANDOFF_STAGGER_MS = 15000;
@@ -27,7 +27,7 @@ const BACKOFF_BASE_MS = 1_000;
27
27
  const BACKOFF_MAX_MS = 60_000;
28
28
  const HEALTHY_RUN_MS = 60_000;
29
29
  export const STOP_ESCALATION_MS = 10_000;
30
- export const CLI_UPDATE_SYNC_IDLE_MAX_MS = 10 * 60_000;
30
+ const CLI_UPDATE_SYNC_IDLE_MAX_MS = 10 * 60_000;
31
31
  export const CLI_UPDATE_SYNC_IDLE_KILL_GRACE_MS = 60_000;
32
32
  const MESH_OWNER_RECHECK_MS = 15_000;
33
33
  const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  export declare const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
4
4
  export declare const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
5
5
  export declare const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
6
- export declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
6
+ declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
7
7
  /** Root directory where validated sync-runner bundles are selected. */
8
8
  export declare function syncRunnerBundleStageRoot(daemonRoot: string): string;
9
9
  /** package.json contract that the hq-cli build step emits beside the bundled bin directory. */
@@ -41,4 +41,5 @@ export declare function stagedSyncRunnerVersion(runnerPath: string): string;
41
41
  * Only a completed, fsynced directory is returned; temp siblings are ignored.
42
42
  */
43
43
  export declare function stageSyncRunnerBundle(options: StageSyncRunnerBundleOptions): StagedSyncRunnerBundle;
44
+ export {};
44
45
  //# sourceMappingURL=sync-runner-bundle.d.ts.map