@indigoai-us/hq-cli 5.219.0 → 5.220.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.220.0] — 2026-09-26
6
+
7
+ ### Fixed
8
+
9
+ - Lane questions now resolve their company from the lane's project before using
10
+ the active HQ company. Project ownership is used only when unique; otherwise
11
+ active-company selection applies without choosing a project owner. Legacy
12
+ questions can be answered with `--company <slug>` when lane or project
13
+ ownership cannot be resolved.
14
+
15
+ - CLI updates no longer interrupt background flag refreshes when the
16
+ self-contained worker rollout is enabled.
17
+
5
18
  ## [5.219.0] — 2026-09-26
6
19
 
7
20
  ### Added
@@ -42,7 +42,7 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
42
42
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
43
43
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
44
44
  import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
45
- import { questionBodyAbsPath, resolveQuestionCompany, } from "../lib/lanes/questions.js";
45
+ import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
46
46
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
47
47
  import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
48
48
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
@@ -1581,39 +1581,39 @@ function sameQuestionCompany(a, b) {
1581
1581
  const bSlug = b.slug.trim();
1582
1582
  return aSlug !== "" && aSlug === bSlug;
1583
1583
  }
1584
- function effectiveLaneQuestionCompany(lane) {
1585
- return lane.company ?? lane.mesh_company ?? null;
1584
+ function effectiveLaneQuestionCompany(lane, hqRoot) {
1585
+ return lane.company ?? lane.mesh_company ?? (hqRoot ? resolveProjectQuestionCompany(hqRoot, lane) : null);
1586
1586
  }
1587
- function questionCompanyForLane(question, lane) {
1588
- return question.company ?? effectiveLaneQuestionCompany(lane);
1587
+ function questionCompanyForLane(question, lane, hqRoot) {
1588
+ return question.company ?? effectiveLaneQuestionCompany(lane, hqRoot);
1589
1589
  }
1590
- async function resolveActiveQuestionCompany(ctx) {
1590
+ async function resolveActiveQuestionCompany(ctx, message = "Cannot access lane questions because the active HQ company could not be resolved") {
1591
1591
  try {
1592
1592
  return await ctx.resolveCompany();
1593
1593
  }
1594
1594
  catch (err) {
1595
1595
  if (!(err instanceof CompanySelectionError))
1596
1596
  throw err;
1597
- throw new LanesError("question_company_unavailable", "Cannot access lane questions because the active HQ company could not be resolved", {});
1597
+ throw new LanesError("question_company_unavailable", message, {});
1598
1598
  }
1599
1599
  }
1600
- function assertQuestionCompanyMatchesLane(question, lane, code) {
1601
- const laneCompany = effectiveLaneQuestionCompany(lane);
1600
+ function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
1601
+ const laneCompany = effectiveLaneQuestionCompany(lane, hqRoot);
1602
1602
  if (question.company != null && laneCompany !== null && !sameQuestionCompany(question.company, laneCompany)) {
1603
1603
  throw new LanesError(code, `Question ${question.question_id} does not match lane ${lane.lane_id}'s company`, { question_id: question.question_id, lane_id: lane.lane_id });
1604
1604
  }
1605
1605
  }
1606
- function assertQuestionCompanyAccess(question, lane, requestedCompany) {
1607
- assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch");
1608
- const questionCompany = questionCompanyForLane(question, lane);
1606
+ function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
1607
+ assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
1608
+ const questionCompany = questionCompanyForLane(question, lane, hqRoot);
1609
1609
  if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
1610
1610
  throw new LanesError("question_company_mismatch", `Question ${question.question_id} does not belong to the requested or active company`, { question_id: question.question_id, lane_id: lane.lane_id });
1611
1611
  }
1612
1612
  }
1613
- function assertQuestionCompanyResolvedForAnswer(question, lane) {
1614
- if (questionCompanyForLane(question, lane) !== null)
1613
+ function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
1614
+ if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
1615
1615
  return;
1616
- throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane`, { question_id: question.question_id, lane_id: lane.lane_id });
1616
+ throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane; re-run with --company <slug>`, { question_id: question.question_id, lane_id: lane.lane_id });
1617
1617
  }
1618
1618
  function questionCompanyRefMatches(recordCompany, filter) {
1619
1619
  return recordCompany !== null &&
@@ -3579,7 +3579,7 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3579
3579
  });
3580
3580
  }
3581
3581
  const ownerAnswer = ownerQuestion.answer;
3582
- const ownerLaneCompany = effectiveLaneQuestionCompany(lane);
3582
+ const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
3583
3583
  if (!isQuestionAnswer(ownerAnswer) ||
3584
3584
  ownerQuestion.lane_id !== lane.lane_id ||
3585
3585
  (ownerQuestion.company !== null && ownerLaneCompany !== null &&
@@ -3596,8 +3596,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3596
3596
  throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3597
3597
  }
3598
3598
  if (current.company !== null &&
3599
- effectiveLaneQuestionCompany(lane) !== null &&
3600
- !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane))) {
3599
+ effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
3600
+ !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
3601
3601
  throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3602
3602
  }
3603
3603
  if (current.company !== null &&
@@ -3631,8 +3631,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3631
3631
  if (!isLanesError(err) || err.error !== "question_not_found")
3632
3632
  throw err;
3633
3633
  const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
3634
- const primaryCompany = effectiveLaneQuestionCompany(primaryLane);
3635
- const ownerCompany = effectiveLaneQuestionCompany(lane);
3634
+ const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
3635
+ const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
3636
3636
  if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
3637
3637
  throw new LanesError("outpost_question_sync_conflict", `Primary lane ${lane.lane_id} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3638
3638
  }
@@ -7092,10 +7092,7 @@ async function runAsk(ctx, opts) {
7092
7092
  }
7093
7093
  const lane = readLane(hqRoot, laneId);
7094
7094
  assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
7095
- const activeCompany = lane.company || lane.mesh_company
7096
- ? undefined
7097
- : await resolveActiveQuestionCompany(ctx);
7098
- const questionCompany = resolveQuestionCompany(lane, activeCompany);
7095
+ const questionCompany = await resolveQuestionCompanyWithFallback(hqRoot, lane, () => resolveActiveQuestionCompany(ctx, "Cannot raise a lane question because the active HQ company could not be resolved"));
7099
7096
  const optionList = Array.isArray(opts.option)
7100
7097
  ? opts.option
7101
7098
  : opts.option
@@ -7177,10 +7174,10 @@ async function runAsk(ctx, opts) {
7177
7174
  });
7178
7175
  }
7179
7176
  const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
7180
- function groupQuestionLanesByCompany(lanes, fallbackCompany) {
7177
+ function groupQuestionLanesByCompany(hqRoot, lanes, fallbackCompany) {
7181
7178
  const groups = [];
7182
7179
  for (const lane of lanes) {
7183
- const company = effectiveLaneQuestionCompany(lane) ?? fallbackCompany ?? null;
7180
+ const company = effectiveLaneQuestionCompany(lane, hqRoot) ?? fallbackCompany ?? null;
7184
7181
  const existing = groups.find((group) => sameQuestionCompany(group.company, company));
7185
7182
  if (existing)
7186
7183
  existing.lanes.push(lane);
@@ -7329,10 +7326,10 @@ async function readOutpostQuestionDetail(ctx, owner) {
7329
7326
  throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
7330
7327
  }
7331
7328
  if (question.question_id !== owner.question.question_id ||
7332
- question.lane_id !== owner.lane.lane_id ||
7333
- !sameQuestionCompany(question.company, effectiveLaneQuestionCompany(owner.lane))) {
7329
+ question.lane_id !== owner.lane.lane_id) {
7334
7330
  throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} returned question ${owner.question.question_id} for a different lane or company`, { outpost_id: outpostId, question_id: owner.question.question_id });
7335
7331
  }
7332
+ assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
7336
7333
  const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
7337
7334
  const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
7338
7335
  return { question, audience, body };
@@ -7340,8 +7337,8 @@ async function readOutpostQuestionDetail(ctx, owner) {
7340
7337
  async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7341
7338
  const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
7342
7339
  (!companyFilter ||
7343
- effectiveLaneQuestionCompany(lane) === null ||
7344
- questionCompanyRefMatches(effectiveLaneQuestionCompany(lane), companyFilter)));
7340
+ effectiveLaneQuestionCompany(lane, hqRoot) === null ||
7341
+ questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
7345
7342
  const lanesByOutpost = new Map();
7346
7343
  for (const lane of hostedLanes) {
7347
7344
  if (lane.host.kind !== "outpost")
@@ -7354,7 +7351,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7354
7351
  const errors = [];
7355
7352
  const eligibleGroups = [];
7356
7353
  for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
7357
- for (const group of groupQuestionLanesByCompany(lanes, companyFilter)) {
7354
+ for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
7358
7355
  if (group.company === null) {
7359
7356
  if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
7360
7357
  errors.push({
@@ -7413,9 +7410,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7413
7410
  const lane = localLanesById.get(question.lane_id);
7414
7411
  if (!lane)
7415
7412
  continue;
7416
- if (!sameQuestionCompany(question.company, effectiveLaneQuestionCompany(lane))) {
7417
- throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} question ${question.question_id} does not match its primary lane company`, { outpost_id: outpostId, question_id: question.question_id });
7418
- }
7413
+ assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
7419
7414
  if (seenQuestionIds.has(question.question_id)) {
7420
7415
  throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
7421
7416
  }
@@ -7479,7 +7474,7 @@ async function runQuestionsList(ctx, opts) {
7479
7474
  const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
7480
7475
  const company = opts.company
7481
7476
  ? await ctx.resolveCompany(opts.company)
7482
- : await resolveActiveQuestionCompany(ctx);
7477
+ : await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
7483
7478
  const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
7484
7479
  const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
7485
7480
  const localQuestions = loadAllQuestions(hqRoot)
@@ -7494,8 +7489,8 @@ async function runQuestionsList(ctx, opts) {
7494
7489
  return false;
7495
7490
  const lane = localLanes.get(q.lane_id);
7496
7491
  if (lane)
7497
- assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch");
7498
- const questionCompany = lane ? questionCompanyForLane(q, lane) : q.company;
7492
+ assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
7493
+ const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
7499
7494
  if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
7500
7495
  if (targeted) {
7501
7496
  throw new LanesError("question_company_mismatch", `Question ${q.question_id} does not belong to the requested or active company`, { question_id: q.question_id, lane_id: q.lane_id });
@@ -7513,7 +7508,7 @@ async function runQuestionsList(ctx, opts) {
7513
7508
  ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
7514
7509
  : { questions: [], errors: [] };
7515
7510
  for (const owner of outpostLoad.questions) {
7516
- assertQuestionCompanyAccess(owner.question, owner.lane, company);
7511
+ assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
7517
7512
  syncPrimaryQuestionDelivery(hqRoot, owner);
7518
7513
  }
7519
7514
  const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
@@ -7542,7 +7537,7 @@ async function runQuestionsList(ctx, opts) {
7542
7537
  return false;
7543
7538
  if (opts.story && q.story_id !== opts.story)
7544
7539
  return false;
7545
- assertQuestionCompanyAccess(q, lane, company);
7540
+ assertQuestionCompanyAccess(q, lane, company, hqRoot);
7546
7541
  if (forFilter) {
7547
7542
  if (!audienceMatches(audience, forFilter))
7548
7543
  return false;
@@ -7618,9 +7613,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7618
7613
  assertNoSecretText("freeform answer", opts.freeform);
7619
7614
  if (opts.notes !== undefined)
7620
7615
  assertNoSecretText("answer notes", opts.notes);
7621
- const company = opts.company
7622
- ? await ctx.resolveCompany(opts.company)
7623
- : await resolveActiveQuestionCompany(ctx);
7616
+ const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
7624
7617
  let question;
7625
7618
  try {
7626
7619
  question = readQuestion(hqRoot, questionId);
@@ -7628,6 +7621,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7628
7621
  catch (err) {
7629
7622
  if (!isLanesError(err) || err.error !== "question_not_found")
7630
7623
  throw err;
7624
+ const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
7631
7625
  const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
7632
7626
  if (lookup.errors.length > 0) {
7633
7627
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
@@ -7640,7 +7634,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7640
7634
  const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
7641
7635
  const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
7642
7636
  if (otherCompanyMatch) {
7643
- assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company);
7637
+ assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
7644
7638
  }
7645
7639
  if (allCompanyLookup.errors.length > 0) {
7646
7640
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
@@ -7657,24 +7651,31 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7657
7651
  if (!owner) {
7658
7652
  throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
7659
7653
  }
7660
- assertQuestionCompanyAccess(owner.question, owner.lane, company);
7661
- assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane);
7654
+ assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
7655
+ const ownerCompany = await resolveQuestionCompanyWithFallback(hqRoot, owner.lane, async () => explicitCompany ?? company);
7656
+ assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane, hqRoot, ownerCompany);
7662
7657
  throwIfQuestionNotPending(owner.question);
7663
7658
  await yieldEventLoop();
7664
7659
  await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
7665
7660
  ...opts,
7666
- company: questionListCompanyArg(effectiveLaneQuestionCompany(owner.lane)),
7661
+ company: questionListCompanyArg(ownerCompany),
7667
7662
  });
7668
7663
  return;
7669
7664
  }
7670
7665
  await yieldEventLoop();
7671
7666
  const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
7672
- assertQuestionCompanyAccess(question, askingLane, company);
7673
- assertQuestionCompanyResolvedForAnswer(question, askingLane);
7667
+ const resolvedCompany = await resolveQuestionCompanyWithFallback(hqRoot, askingLane, async () => explicitCompany ?? question.company ?? await resolveActiveQuestionCompany(ctx, `Cannot answer legacy question ${questionId}: its company cannot be resolved from the question or lane; re-run with --company <slug>`));
7668
+ const requestedCompany = explicitCompany ?? resolvedCompany;
7669
+ assertQuestionCompanyAccess(question, askingLane, requestedCompany, hqRoot);
7670
+ assertQuestionCompanyResolvedForAnswer(question, askingLane, hqRoot, resolvedCompany);
7671
+ const questionCompany = question.company ?? effectiveLaneQuestionCompany(askingLane, hqRoot) ?? resolvedCompany;
7672
+ const questionForBody = question.company === null
7673
+ ? { ...question, company: questionCompany }
7674
+ : question;
7674
7675
  if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
7675
7676
  await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
7676
7677
  ...opts,
7677
- company: questionListCompanyArg(effectiveLaneQuestionCompany(askingLane)),
7678
+ company: questionListCompanyArg(questionCompany),
7678
7679
  });
7679
7680
  return;
7680
7681
  }
@@ -7685,7 +7686,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7685
7686
  if (laneAcceptsDropboxDelivery(askingLane)) {
7686
7687
  assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
7687
7688
  }
7688
- const body = requireQuestionBody(hqRoot, question);
7689
+ const body = requireQuestionBody(hqRoot, questionForBody);
7689
7690
  // A trimmed-empty --freeform is absent: it must not win over --choice and
7690
7691
  // must not become a freeform answer of "".
7691
7692
  const freeform = opts.freeform?.trim() || undefined;
@@ -7702,10 +7703,15 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7702
7703
  : "user";
7703
7704
  const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
7704
7705
  throwIfQuestionNotPending(current);
7706
+ assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
7707
+ const recordedCompany = current.company ?? questionCompany;
7708
+ const currentForBody = current.company === null
7709
+ ? { ...current, company: recordedCompany }
7710
+ : current;
7705
7711
  let notesRef;
7706
7712
  let nextBody = current.body;
7707
7713
  if (opts.notes) {
7708
- const stored = writeQuestionBody(hqRoot, current, {
7714
+ const stored = writeQuestionBody(hqRoot, currentForBody, {
7709
7715
  ...body,
7710
7716
  notes: opts.notes,
7711
7717
  });
@@ -7714,6 +7720,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7714
7720
  }
7715
7721
  return {
7716
7722
  ...current,
7723
+ company: recordedCompany,
7717
7724
  body: nextBody,
7718
7725
  status: "answered",
7719
7726
  answer: {
@@ -0,0 +1,13 @@
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
+ /** Keep the last server snapshot decision for this one startup-mode flag. */
7
+ export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
8
+ /** A missing, invalid, or inaccessible cache never enables the rollout. */
9
+ export declare function readCachedFlagRegistryWorkerMode(filePath: string): CachedWorkerMode | null;
10
+ export declare function writeCachedFlagRegistryWorkerMode(filePath: string, snapshot: CachedWorkerMode): void;
11
+ export declare function flagRegistryStateDir(env?: NodeJS.ProcessEnv): string;
12
+ export {};
13
+ //# sourceMappingURL=flag-registry-worker-mode-cache.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
6
+ /** Keep the last server snapshot decision for this one startup-mode flag. */
7
+ export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
8
+ const basename = scopeIdentifiers.length === 0
9
+ ? "worker-mode-default.json"
10
+ : `worker-mode-${createHash("sha256")
11
+ .update([...scopeIdentifiers].sort().join("\0"))
12
+ .digest("hex")
13
+ .slice(0, 24)}.json`;
14
+ return path.join(stateDir, "flags", basename);
15
+ }
16
+ /** A missing, invalid, or inaccessible cache never enables the rollout. */
17
+ export function readCachedFlagRegistryWorkerMode(filePath) {
18
+ let latest = readCacheRecordIfPresent(filePath);
19
+ const directory = path.dirname(filePath);
20
+ if (!fs.existsSync(directory))
21
+ return latest;
22
+ const basename = path.basename(filePath);
23
+ const versionedPrefix = `${basename}.v`;
24
+ for (const entry of fs.readdirSync(directory)) {
25
+ if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
26
+ continue;
27
+ const versionedPath = path.join(directory, entry);
28
+ const record = readCacheRecordIfPresent(versionedPath);
29
+ if (record)
30
+ latest = newerCacheRecord(latest, record);
31
+ }
32
+ return latest;
33
+ }
34
+ export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
35
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
36
+ const existing = readCachedFlagRegistryWorkerMode(filePath);
37
+ if (existing && snapshot.version < existing.version)
38
+ return;
39
+ // Each registry version gets its own immutable destination. Concurrent
40
+ // processes can publish v7 and v8 in either order without an older value
41
+ // replacing the newer record.
42
+ const versionedPath = `${filePath}.v${snapshot.version}.json`;
43
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
44
+ try {
45
+ fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
46
+ encoding: "utf8",
47
+ mode: 0o600,
48
+ flag: "wx",
49
+ });
50
+ try {
51
+ fs.linkSync(tempPath, versionedPath);
52
+ }
53
+ catch (error) {
54
+ if (!isAlreadyExistsError(error))
55
+ throw error;
56
+ const existingVersion = readCacheRecord(versionedPath);
57
+ if (existingVersion.value !== snapshot.value) {
58
+ throw new Error("Conflicting flag-registry worker cache values share a version");
59
+ }
60
+ }
61
+ }
62
+ finally {
63
+ if (fs.existsSync(tempPath))
64
+ fs.unlinkSync(tempPath);
65
+ }
66
+ pruneOlderCacheRecords(filePath);
67
+ }
68
+ export function flagRegistryStateDir(env = process.env) {
69
+ const override = env.HQ_STATE_DIR?.trim();
70
+ return override ? override : path.join(os.homedir(), ".hq");
71
+ }
72
+ function isCachedWorkerMode(value) {
73
+ return typeof value === "object" && value !== null &&
74
+ "version" in value && typeof value.version === "number" &&
75
+ Number.isSafeInteger(value.version) && value.version >= 0 &&
76
+ "value" in value && typeof value.value === "boolean";
77
+ }
78
+ function readCacheRecord(filePath) {
79
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
80
+ if (!isCachedWorkerMode(value)) {
81
+ throw new Error("Cached flag-registry worker mode has an invalid shape");
82
+ }
83
+ return value;
84
+ }
85
+ function newerCacheRecord(current, candidate) {
86
+ if (!current || candidate.version > current.version)
87
+ return candidate;
88
+ if (candidate.version === current.version && candidate.value !== current.value) {
89
+ throw new Error("Conflicting flag-registry worker cache values share a version");
90
+ }
91
+ return current;
92
+ }
93
+ function pruneOlderCacheRecords(filePath) {
94
+ const latest = readCachedFlagRegistryWorkerMode(filePath);
95
+ if (!latest)
96
+ return;
97
+ const directory = path.dirname(filePath);
98
+ const basename = path.basename(filePath);
99
+ const versionedPrefix = `${basename}.v`;
100
+ for (const entry of fs.readdirSync(directory)) {
101
+ if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
102
+ continue;
103
+ const recordPath = path.join(directory, entry);
104
+ const record = readCacheRecordIfPresent(recordPath);
105
+ if (record && record.version < latest.version) {
106
+ try {
107
+ fs.unlinkSync(recordPath);
108
+ }
109
+ catch (error) {
110
+ if (!isNotFoundError(error))
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+ if (fs.existsSync(filePath)) {
116
+ const legacyRecord = readCacheRecordIfPresent(filePath);
117
+ if (legacyRecord && legacyRecord.version < latest.version) {
118
+ try {
119
+ fs.unlinkSync(filePath);
120
+ }
121
+ catch (error) {
122
+ if (!isNotFoundError(error))
123
+ throw error;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function readCacheRecordIfPresent(filePath) {
129
+ try {
130
+ return readCacheRecord(filePath);
131
+ }
132
+ catch (error) {
133
+ if (isNotFoundError(error))
134
+ return null;
135
+ throw error;
136
+ }
137
+ }
138
+ function isAlreadyExistsError(error) {
139
+ return typeof error === "object" && error !== null &&
140
+ "code" in error && error.code === "EEXIST";
141
+ }
142
+ function isNotFoundError(error) {
143
+ return typeof error === "object" && error !== null &&
144
+ "code" in error && error.code === "ENOENT";
145
+ }
146
+ //# sourceMappingURL=flag-registry-worker-mode-cache.js.map