@indigoai-us/hq-cli 5.234.0 → 5.234.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.234.2] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - The flag-registry worker now uses source embedded in the CLI, so package
10
+ updates can no longer cause startup failures by removing its script.
11
+
12
+ ## [5.234.1] — 2026-09-27
13
+
14
+ ### Fixed
15
+
16
+ - A primary resume now confirms and syncs an Outpost lane queued during
17
+ owner-side admission. (#926)
18
+ - Outpost messages and account moves for children of primary-hosted lanes now
19
+ carry the bounded hire-grant context required by the owner. (#926)
20
+ - Outpost message resumes, moves, and admission queues now sync the returned
21
+ owner lane record back to the primary. (#926)
22
+
5
23
  ## [5.234.0] — 2026-09-27
6
24
 
7
25
  ### Added
@@ -8005,6 +8005,9 @@ export declare const COMMAND_CATALOG: readonly [{
8005
8005
  }, {
8006
8006
  readonly flags: "--worker <profile>";
8007
8007
  readonly description: "Attach a personal worker profile when this message resumes the lane";
8008
+ }, {
8009
+ readonly flags: "--grant-context <base64url>";
8010
+ readonly description: "";
8008
8011
  }, {
8009
8012
  readonly flags: "--no-compact";
8010
8013
  readonly description: "Skip the resume-time compaction check for this message";
@@ -10353,6 +10353,10 @@ export const COMMAND_CATALOG = [
10353
10353
  "flags": "--worker <profile>",
10354
10354
  "description": "Attach a personal worker profile when this message resumes the lane"
10355
10355
  },
10356
+ {
10357
+ "flags": "--grant-context <base64url>",
10358
+ "description": ""
10359
+ },
10356
10360
  {
10357
10361
  "flags": "--no-compact",
10358
10362
  "description": "Skip the resume-time compaction check for this message"
@@ -3841,7 +3841,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3841
3841
  grantHierarchy = hierarchy;
3842
3842
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3843
3843
  }
3844
- else if (action === "resume" &&
3844
+ else if ((action === "resume" || action === "message") &&
3845
3845
  lane.senior.kind === "lane") {
3846
3846
  grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
3847
3847
  }
@@ -3987,9 +3987,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3987
3987
  : action === "reparent"
3988
3988
  ? remote.lane !== null && typeof remote.lane === "object" &&
3989
3989
  Array.isArray(remote.changed_descendant_ids)
3990
- : (remote.delivery_status === "worker_spawned" ||
3990
+ : (((remote.delivery_status === "worker_spawned" ||
3991
3991
  remote.delivery_status === "queued_on_target_account") &&
3992
- remote.lane !== undefined);
3992
+ remote.lane !== undefined) ||
3993
+ (remote.delivery_status === "queued_on_admission" &&
3994
+ remote.lane !== null &&
3995
+ typeof remote.lane === "object" &&
3996
+ !Array.isArray(remote.lane) &&
3997
+ remote.lane.state === "queued")));
3993
3998
  if (!resultConfirmed) {
3994
3999
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3995
4000
  lane_id: lane.lane_id,
@@ -4010,6 +4015,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
4010
4015
  });
4011
4016
  return;
4012
4017
  }
4018
+ if (action === "message" &&
4019
+ (remote.delivery_status === "worker_spawned" ||
4020
+ remote.delivery_status === "queued_on_target_account" ||
4021
+ remote.delivery_status === "queued_on_admission")) {
4022
+ const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
4023
+ printJson({
4024
+ ...remote,
4025
+ lane_id: ownerLane.lane_id,
4026
+ lane: ownerLane,
4027
+ outpost_id: outpostId,
4028
+ outpost_command_id: dispatched.commandId,
4029
+ outpost_transport: "outposts_exec",
4030
+ });
4031
+ return;
4032
+ }
4013
4033
  if (action === "reparent" || action === "resume") {
4014
4034
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
4015
4035
  const descendants = [];
@@ -5872,7 +5892,7 @@ async function prepareAccountMove(ctx, lane, input) {
5872
5892
  next.queued_at = undefined;
5873
5893
  }
5874
5894
  return next;
5875
- }));
5895
+ }), input.grantContext);
5876
5896
  return { lane: moved, from, to: target, copy, queued };
5877
5897
  }, ctx.sleep);
5878
5898
  }
@@ -5990,6 +6010,14 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
5990
6010
  }
5991
6011
  async function runMessage(ctx, laneId, opts) {
5992
6012
  const hqRoot = ctx.hqRoot();
6013
+ const hireGrantContext = opts.grantContext === undefined
6014
+ ? undefined
6015
+ : (() => {
6016
+ if (!ctx.isSsmOwnerDispatch()) {
6017
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6018
+ }
6019
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
6020
+ })();
5993
6021
  let accepted = false;
5994
6022
  let claimedMessages = [];
5995
6023
  const markAccepted = () => {
@@ -6066,7 +6094,7 @@ async function runMessage(ctx, laneId, opts) {
6066
6094
  }
6067
6095
  // Check before transcript recovery, account probing, or message writes.
6068
6096
  // The final state transition repeats this check under the senior lock.
6069
- await withResumeHireAdmission(ctx, lane, () => undefined);
6097
+ await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
6070
6098
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
6071
6099
  const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
6072
6100
  if (keepCurrentAccount)
@@ -6162,6 +6190,7 @@ async function runMessage(ctx, laneId, opts) {
6162
6190
  cliModel: opts.model?.trim() || undefined,
6163
6191
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6164
6192
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6193
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
6165
6194
  pending: {
6166
6195
  trigger: "message",
6167
6196
  text: deliveryText,
@@ -6186,6 +6215,7 @@ async function runMessage(ctx, laneId, opts) {
6186
6215
  ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
6187
6216
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6188
6217
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6218
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
6189
6219
  pending: {
6190
6220
  trigger: "message",
6191
6221
  text: deliveryText,
@@ -6300,7 +6330,7 @@ async function runMessage(ctx, laneId, opts) {
6300
6330
  });
6301
6331
  archiveResumeMessage(hqRoot, laneId, dropboxMessage);
6302
6332
  return started;
6303
- });
6333
+ }, hireGrantContext);
6304
6334
  }, ctx.sleep);
6305
6335
  },
6306
6336
  }, mechanism);
@@ -9752,6 +9782,7 @@ export function registerLanesCommand(parent, deps = {}) {
9752
9782
  .option("--model <id>", "Model for the moved lane (with --account only)")
9753
9783
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
9754
9784
  .option("--worker <profile>", "Attach a personal worker profile when this message resumes the lane")
9785
+ .addOption(new Option("--grant-context <base64url>").hideHelp())
9755
9786
  .option("--no-compact", "Skip the resume-time compaction check for this message")
9756
9787
  .action(wrap(async (laneId, opts) => {
9757
9788
  await runMessage(ctx, laneId, opts);
@@ -12,9 +12,8 @@ import { readMachineCompanyBinding } from "./work-context/company.js";
12
12
  import { readSessionState } from "./work-context/state.js";
13
13
  import { workContextRoot } from "./work-context/paths.js";
14
14
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
15
- import { loadCachedTokens, resolveDefaultHqRoot } from "../utils/cognito-session.js";
15
+ import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
16
16
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
17
- import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryWorkerModeCacheFailureContext, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
18
17
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
19
18
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
20
19
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
@@ -101,86 +100,6 @@ function processSnapshotReader(companyIdentifiers) {
101
100
  },
102
101
  };
103
102
  }
104
- function personSubjectForWorkerMode(idToken) {
105
- if (typeof idToken !== "string")
106
- return null;
107
- const payload = idToken.split(".")[1];
108
- if (!payload)
109
- return null;
110
- try {
111
- const subject = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).sub;
112
- return typeof subject === "string" && subject.trim() ? subject : null;
113
- }
114
- catch {
115
- Sentry.captureMessage("Could not scope cached flag-registry worker mode because the cached ID token payload is invalid", { level: "warning", tags: { component: "flag-registry-worker-mode-cache" } });
116
- return null;
117
- }
118
- }
119
- /** Scope a persisted rollout value to the same identity that fetched it. */
120
- function workerModeCacheScopes(company) {
121
- const tokens = loadCachedTokens();
122
- const personSubject = tokens ? personSubjectForWorkerMode(tokens.idToken) : null;
123
- if (tokens && !personSubject)
124
- return null;
125
- return [
126
- ...(company.companyUid ? [`company:${company.companyUid}`] : []),
127
- ...(company.companySlug ? [`company-slug:${company.companySlug}`] : []),
128
- ...(personSubject ? [`person:${personSubject}`] : []),
129
- ];
130
- }
131
- function selfContainedWorkerEnabled(company) {
132
- const localOverride = readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env);
133
- if (localOverride !== null)
134
- return localOverride;
135
- const scopes = workerModeCacheScopes(company);
136
- if (!scopes)
137
- return false;
138
- const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
139
- try {
140
- return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
141
- }
142
- catch (error) {
143
- const failure = flagRegistryWorkerModeCacheFailureContext(error, "read", filePath);
144
- Sentry.captureException(failure.exception, {
145
- tags: {
146
- component: "flag-registry-worker-mode-cache",
147
- worker: "flag-registry",
148
- ...failure.tags,
149
- },
150
- });
151
- return false;
152
- }
153
- }
154
- function cacheWorkerModeFromSnapshot(snapshot, company) {
155
- // A developer's local hq-flags override is process-local and must not become
156
- // the value selected by a future process.
157
- if (readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env) !== null)
158
- return;
159
- const scopes = workerModeCacheScopes(company);
160
- if (!scopes)
161
- return;
162
- const decision = resolveFlag({
163
- flagKey: FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG,
164
- env: {},
165
- snapshot,
166
- companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
167
- lookup: { fallback: false },
168
- });
169
- try {
170
- writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
171
- }
172
- catch (error) {
173
- const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
174
- const failure = flagRegistryWorkerModeCacheFailureContext(error, "write", filePath);
175
- Sentry.captureException(failure.exception, {
176
- tags: {
177
- component: "flag-registry-worker-mode-cache",
178
- worker: "flag-registry",
179
- ...failure.tags,
180
- },
181
- });
182
- }
183
- }
184
103
  /**
185
104
  * Begin one registry refresh without awaiting it or retaining the CLI process.
186
105
  *
@@ -199,42 +118,38 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
199
118
  const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
200
119
  processReader = processSnapshotReader(companyIdentifiers);
201
120
  try {
202
- let workerSource = new URL("./flag-registry-worker.js", import.meta.url);
203
- let workerOptions = { workerData: {
121
+ const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
122
+ if (embeddedSource.length === 0) {
123
+ (dependencies.captureWorkerFailure ?? ((error, tags) => {
124
+ Sentry.captureException(error, { tags });
125
+ }))(new Error("Self-contained flag-registry worker source is unavailable"), {
126
+ component: "flag-registry-worker-startup",
127
+ worker: "flag-registry",
128
+ flag_worker_failure: "embedded_source_unavailable",
129
+ });
130
+ return processReader;
131
+ }
132
+ // This source is embedded at build time, so it remains available while an
133
+ // updater replaces the installed package tree.
134
+ const workerOptions = {
135
+ workerData: {
204
136
  endpoint,
205
137
  ...(companyUid ? { companyUid } : {}),
206
138
  ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
207
- } };
208
- if (selfContainedWorkerEnabled(company)) {
209
- // The worker source is generated and embedded in this parent module at
210
- // build time. It is already resident before company, identity, and cache
211
- // reads can yield to an updater replacing this package tree.
212
- const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
213
- if (embeddedSource.length === 0) {
214
- (dependencies.captureWorkerFailure ?? ((error, tags) => {
215
- Sentry.captureException(error, { tags });
216
- }))(new Error("Self-contained flag-registry worker source is unavailable"), {
217
- component: "flag-registry-worker-startup",
218
- worker: "flag-registry",
219
- flag_worker_failure: "embedded_source_unavailable",
220
- });
221
- return processReader;
222
- }
223
- workerSource = embeddedSource;
224
- workerOptions = { ...workerOptions, eval: true };
225
- }
139
+ },
140
+ eval: true,
141
+ };
226
142
  // The SDK's child-process integration captures every Worker error unless
227
143
  // its process-wide captureWorkerErrors option is disabled. Scope only this
228
144
  // worker so the reporting boundary can classify its startup errors without
229
145
  // changing capture for unrelated workers.
230
146
  const worker = Sentry.withScope((scope) => {
231
147
  scope.setTag("worker", "flag-registry");
232
- return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(workerSource, workerOptions);
148
+ return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(embeddedSource, workerOptions);
233
149
  });
234
150
  worker.on("message", (value) => {
235
151
  if (isFlagSnapshot(value)) {
236
152
  heldSnapshot = value;
237
- cacheWorkerModeFromSnapshot(value, company);
238
153
  }
239
154
  });
240
155
  // A failed background refresh must stay invisible to an offline CLI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.234.0",
3
+ "version": "5.234.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,20 +0,0 @@
1
- export declare const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
2
- interface CachedWorkerMode {
3
- version: number;
4
- value: boolean;
5
- }
6
- export type FlagRegistryWorkerModeCacheOperation = "read" | "write";
7
- export type FlagRegistryWorkerModeCacheStep = "mkdir" | "write_temp" | "link" | "unlink" | "prune" | "readdir" | "read";
8
- /** Build bounded filesystem diagnostics without exposing the state directory. */
9
- export declare function flagRegistryWorkerModeCacheFailureContext(error: unknown, operation: FlagRegistryWorkerModeCacheOperation, filePath: string, env?: NodeJS.ProcessEnv): {
10
- exception: unknown;
11
- tags: Record<string, string>;
12
- };
13
- /** Keep the last server snapshot decision for this one startup-mode flag. */
14
- export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
15
- /** A missing, invalid, or inaccessible cache never enables the rollout. */
16
- export declare function readCachedFlagRegistryWorkerMode(filePath: string): CachedWorkerMode | null;
17
- export declare function writeCachedFlagRegistryWorkerMode(filePath: string, snapshot: CachedWorkerMode): void;
18
- export declare function flagRegistryStateDir(env?: NodeJS.ProcessEnv): string;
19
- export {};
20
- //# sourceMappingURL=flag-registry-worker-mode-cache.d.ts.map
@@ -1,254 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import * as fs from "node:fs";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
7
- class FlagRegistryWorkerModeCacheStepError extends Error {
8
- cacheStep;
9
- originalError;
10
- constructor(cacheStep, originalError) {
11
- super(`Flag registry worker-mode cache failed at ${cacheStep}`);
12
- this.cacheStep = cacheStep;
13
- this.originalError = originalError;
14
- this.name = "FlagRegistryWorkerModeCacheStepError";
15
- }
16
- }
17
- const KNOWN_ERRNOS = new Set([
18
- "EACCES",
19
- "EBUSY",
20
- "EEXIST",
21
- "EIO",
22
- "EISDIR",
23
- "EMFILE",
24
- "ENFILE",
25
- "ENOENT",
26
- "ENOSPC",
27
- "ENOTDIR",
28
- "EPERM",
29
- "EROFS",
30
- ]);
31
- function withCacheFsStep(cacheStep, operation) {
32
- try {
33
- return operation();
34
- }
35
- catch (error) {
36
- if (error instanceof FlagRegistryWorkerModeCacheStepError)
37
- throw error;
38
- throw new FlagRegistryWorkerModeCacheStepError(cacheStep, error);
39
- }
40
- }
41
- function originalCacheError(error) {
42
- return error instanceof FlagRegistryWorkerModeCacheStepError
43
- ? error.originalError
44
- : error;
45
- }
46
- function cacheErrorCode(error) {
47
- const original = originalCacheError(error);
48
- return typeof original === "object" && original !== null && "code" in original
49
- ? original.code
50
- : undefined;
51
- }
52
- function directoryHasUchg(directory) {
53
- if (process.platform !== "darwin")
54
- return "unknown";
55
- try {
56
- const rawFlags = execFileSync("/usr/bin/stat", ["-f", "%f", directory], { timeout: 1000 }).toString("utf8").trim();
57
- if (!/^\d+$/.test(rawFlags))
58
- return "unknown";
59
- const flags = Number(rawFlags);
60
- if (!Number.isSafeInteger(flags))
61
- return "unknown";
62
- return String((flags & 0x00000002) !== 0 || (flags & 0x00020000) !== 0);
63
- }
64
- catch {
65
- // This bounded diagnostic is best-effort; preserve unknown and the original cache error.
66
- return "unknown";
67
- }
68
- }
69
- /** Build bounded filesystem diagnostics without exposing the state directory. */
70
- export function flagRegistryWorkerModeCacheFailureContext(error, operation, filePath, env = process.env) {
71
- const cacheError = error instanceof FlagRegistryWorkerModeCacheStepError
72
- ? error
73
- : undefined;
74
- const directory = path.dirname(filePath);
75
- let dirExists = "unknown";
76
- let stats;
77
- try {
78
- stats = fs.statSync(directory);
79
- dirExists = "true";
80
- }
81
- catch (metadataError) {
82
- if (cacheErrorCode(metadataError) === "ENOENT")
83
- dirExists = "false";
84
- // Keep inaccessible metadata unknown; diagnostics must not hide the cache error.
85
- }
86
- const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
87
- const ownerMatches = typeof uid === "number" && typeof stats?.uid === "number"
88
- ? String(stats.uid === uid)
89
- : "unknown";
90
- const dirMode = typeof stats?.mode === "number"
91
- ? (stats.mode & 0o777).toString(8).padStart(3, "0")
92
- : "unknown";
93
- const dirHasUchg = directoryHasUchg(directory);
94
- const rawErrno = cacheErrorCode(error);
95
- const errno = typeof rawErrno === "string" && KNOWN_ERRNOS.has(rawErrno)
96
- ? rawErrno
97
- : "UNKNOWN";
98
- const stateDirSource = env.HQ_STATE_DIR?.trim() ? "env" : "home";
99
- return {
100
- exception: originalCacheError(error),
101
- tags: {
102
- cache_op: operation,
103
- cache_step: cacheError?.cacheStep ?? (operation === "read" ? "read" : "write_temp"),
104
- errno,
105
- state_dir_source: stateDirSource,
106
- dir_exists: dirExists,
107
- dir_owner_matches_uid: ownerMatches,
108
- dir_mode: dirMode,
109
- dir_has_uchg: dirHasUchg,
110
- cache_path_kind: "<stateDir>/flags/<basename>",
111
- },
112
- };
113
- }
114
- /** Keep the last server snapshot decision for this one startup-mode flag. */
115
- export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
116
- const basename = scopeIdentifiers.length === 0
117
- ? "worker-mode-default.json"
118
- : `worker-mode-${createHash("sha256")
119
- .update([...scopeIdentifiers].sort().join("\0"))
120
- .digest("hex")
121
- .slice(0, 24)}.json`;
122
- return path.join(stateDir, "flags", basename);
123
- }
124
- /** A missing, invalid, or inaccessible cache never enables the rollout. */
125
- export function readCachedFlagRegistryWorkerMode(filePath) {
126
- let latest = readCacheRecordIfPresent(filePath);
127
- const directory = path.dirname(filePath);
128
- if (!withCacheFsStep("read", () => fs.existsSync(directory)))
129
- return latest;
130
- const basename = path.basename(filePath);
131
- const versionedPrefix = `${basename}.v`;
132
- for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
133
- if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
134
- continue;
135
- const versionedPath = path.join(directory, entry);
136
- const record = readCacheRecordIfPresent(versionedPath);
137
- if (record)
138
- latest = newerCacheRecord(latest, record);
139
- }
140
- return latest;
141
- }
142
- export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
143
- withCacheFsStep("mkdir", () => fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }));
144
- const existing = readCachedFlagRegistryWorkerMode(filePath);
145
- if (existing && snapshot.version < existing.version)
146
- return;
147
- // Each registry version gets its own immutable destination. Concurrent
148
- // processes can publish v7 and v8 in either order without an older value
149
- // replacing the newer record.
150
- const versionedPath = `${filePath}.v${snapshot.version}.json`;
151
- const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
152
- try {
153
- withCacheFsStep("write_temp", () => fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
154
- encoding: "utf8",
155
- mode: 0o600,
156
- flag: "wx",
157
- }));
158
- try {
159
- withCacheFsStep("link", () => fs.linkSync(tempPath, versionedPath));
160
- }
161
- catch (error) {
162
- if (!isAlreadyExistsError(error))
163
- throw error;
164
- const existingVersion = readCacheRecord(versionedPath);
165
- if (existingVersion.value !== snapshot.value) {
166
- throw new FlagRegistryWorkerModeCacheStepError("link", new Error("Conflicting flag-registry worker cache values share a version"));
167
- }
168
- }
169
- }
170
- finally {
171
- if (withCacheFsStep("unlink", () => fs.existsSync(tempPath))) {
172
- withCacheFsStep("unlink", () => fs.unlinkSync(tempPath));
173
- }
174
- }
175
- pruneOlderCacheRecords(filePath);
176
- }
177
- export function flagRegistryStateDir(env = process.env) {
178
- const override = env.HQ_STATE_DIR?.trim();
179
- return override ? override : path.join(os.homedir(), ".hq");
180
- }
181
- function isCachedWorkerMode(value) {
182
- return typeof value === "object" && value !== null &&
183
- "version" in value && typeof value.version === "number" &&
184
- Number.isSafeInteger(value.version) && value.version >= 0 &&
185
- "value" in value && typeof value.value === "boolean";
186
- }
187
- function readCacheRecord(filePath) {
188
- const contents = withCacheFsStep("read", () => fs.readFileSync(filePath, "utf8"));
189
- const value = JSON.parse(contents);
190
- if (!isCachedWorkerMode(value)) {
191
- throw new Error("Cached flag-registry worker mode has an invalid shape");
192
- }
193
- return value;
194
- }
195
- function newerCacheRecord(current, candidate) {
196
- if (!current || candidate.version > current.version)
197
- return candidate;
198
- if (candidate.version === current.version && candidate.value !== current.value) {
199
- throw new Error("Conflicting flag-registry worker cache values share a version");
200
- }
201
- return current;
202
- }
203
- function pruneOlderCacheRecords(filePath) {
204
- const latest = readCachedFlagRegistryWorkerMode(filePath);
205
- if (!latest)
206
- return;
207
- const directory = path.dirname(filePath);
208
- const basename = path.basename(filePath);
209
- const versionedPrefix = `${basename}.v`;
210
- for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
211
- if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
212
- continue;
213
- const recordPath = path.join(directory, entry);
214
- const record = readCacheRecordIfPresent(recordPath);
215
- if (record && record.version < latest.version) {
216
- try {
217
- withCacheFsStep("prune", () => fs.unlinkSync(recordPath));
218
- }
219
- catch (error) {
220
- if (!isNotFoundError(error))
221
- throw error;
222
- }
223
- }
224
- }
225
- if (withCacheFsStep("prune", () => fs.existsSync(filePath))) {
226
- const legacyRecord = readCacheRecordIfPresent(filePath);
227
- if (legacyRecord && legacyRecord.version < latest.version) {
228
- try {
229
- withCacheFsStep("prune", () => fs.unlinkSync(filePath));
230
- }
231
- catch (error) {
232
- if (!isNotFoundError(error))
233
- throw error;
234
- }
235
- }
236
- }
237
- }
238
- function readCacheRecordIfPresent(filePath) {
239
- try {
240
- return readCacheRecord(filePath);
241
- }
242
- catch (error) {
243
- if (isNotFoundError(error))
244
- return null;
245
- throw error;
246
- }
247
- }
248
- function isAlreadyExistsError(error) {
249
- return cacheErrorCode(error) === "EEXIST";
250
- }
251
- function isNotFoundError(error) {
252
- return cacheErrorCode(error) === "ENOENT";
253
- }
254
- //# sourceMappingURL=flag-registry-worker-mode-cache.js.map