@indigoai-us/hq-cli 5.230.0 → 5.230.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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.230.2] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - Resuming a terminal Outpost lane whose senior lives on the primary now checks
10
+ hire-grant capacity on both hosts using the primary hierarchy, then syncs the
11
+ running owner record back to the primary. An owner CLI that rejects the
12
+ hierarchy context now fails with an upgrade-required error instead of retrying
13
+ without the grant check.
14
+
15
+ ## [5.230.1] — 2026-09-27
16
+
17
+ ### Fixed
18
+
19
+ - A queued account move keeps its pending resume without reserving a running slot.
20
+
5
21
  ## [5.230.0] — 2026-09-27
6
22
 
7
23
  ### Added
@@ -8056,6 +8056,9 @@ export declare const COMMAND_CATALOG: readonly [{
8056
8056
  }, {
8057
8057
  readonly flags: "--worker <profile>";
8058
8058
  readonly description: "Attach a personal worker profile for the resumed turn";
8059
+ }, {
8060
+ readonly flags: "--grant-context <base64url>";
8061
+ readonly description: "";
8059
8062
  }, {
8060
8063
  readonly flags: "--keep-alive";
8061
8064
  readonly description: "Automatically resume done lead lanes while their story remains open";
@@ -10421,6 +10421,10 @@ export const COMMAND_CATALOG = [
10421
10421
  "flags": "--worker <profile>",
10422
10422
  "description": "Attach a personal worker profile for the resumed turn"
10423
10423
  },
10424
+ {
10425
+ "flags": "--grant-context <base64url>",
10426
+ "description": ""
10427
+ },
10424
10428
  {
10425
10429
  "flags": "--keep-alive",
10426
10430
  "description": "Automatically resume done lead lanes while their story remains open"
@@ -1039,15 +1039,36 @@ async function mapWithConcurrency(values, concurrency, map) {
1039
1039
  await Promise.all(workers);
1040
1040
  return results;
1041
1041
  }
1042
- async function withResumeHireAdmission(ctx, lane, fn) {
1042
+ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
1043
1043
  const hqRoot = ctx.hqRoot();
1044
+ if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
1045
+ lane.host.kind !== "outpost" ||
1046
+ !laneIsHostedHere(ctx, lane) ||
1047
+ lane.senior.kind !== "lane" ||
1048
+ !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
1049
+ throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its resume`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
1050
+ }
1044
1051
  if (!isTerminalLaneState(lane.state) ||
1045
1052
  lane.senior.kind !== "lane" ||
1046
1053
  !lane.senior.id) {
1047
1054
  return fn();
1048
1055
  }
1056
+ if (grantContext === undefined &&
1057
+ lane.host.kind === "outpost" &&
1058
+ laneIsHostedHere(ctx, lane) &&
1059
+ !listLaneIds(hqRoot).includes(lane.senior.id)) {
1060
+ throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
1061
+ }
1049
1062
  return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
1050
1063
  const current = readLane(hqRoot, lane.lane_id);
1064
+ if (grantContext !== undefined &&
1065
+ !sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
1066
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
1067
+ lane_id: current.lane_id,
1068
+ expected_senior: grantContext.expected_senior,
1069
+ current_senior: current.senior,
1070
+ });
1071
+ }
1051
1072
  // A different resume already reactivated this lane; it occupies the slot
1052
1073
  // in the shared snapshot and is not a second hire.
1053
1074
  if (!isTerminalLaneState(current.state))
@@ -1056,7 +1077,7 @@ async function withResumeHireAdmission(ctx, lane, fn) {
1056
1077
  if (refusal !== null)
1057
1078
  throw refusal;
1058
1079
  return fn();
1059
- });
1080
+ }, grantContext?.hierarchy);
1060
1081
  }
1061
1082
  const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
1062
1083
  const MAX_PLACEMENT_OUTPOSTS = 16;
@@ -3569,6 +3590,10 @@ function outpostRejectedProfileRepinOption(result) {
3569
3590
  return result.exitCode !== 0 &&
3570
3591
  /unknown option\s+(?:['"])?--profile-repin-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3571
3592
  }
3593
+ function outpostRejectedGrantContextOption(result) {
3594
+ return result.exitCode !== 0 &&
3595
+ /unknown option\s+(?:['"])?--grant-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3596
+ }
3572
3597
  function verifyOutpostProfileContent(hqRoot, lane) {
3573
3598
  if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
3574
3599
  return;
@@ -3713,9 +3738,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3713
3738
  grantHierarchy = hierarchy;
3714
3739
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3715
3740
  }
3741
+ else if (action === "resume" &&
3742
+ lane.senior.kind === "lane") {
3743
+ grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
3744
+ }
3716
3745
  let dispatched;
3746
+ let grantContextRejected = false;
3717
3747
  try {
3718
- const dispatchCommand = (repinContext) => ctx.dispatchOutpostExec({
3748
+ const dispatchCommand = (contexts) => ctx.dispatchOutpostExec({
3719
3749
  outpostId,
3720
3750
  command: buildOutpostLaneCommand({
3721
3751
  action: action === "grant"
@@ -3738,20 +3768,38 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3738
3768
  hire: options.hire,
3739
3769
  senior: options.senior,
3740
3770
  reason: options.reason,
3741
- grantContext,
3742
- ...(repinContext !== undefined ? { profileRepinContext: repinContext } : {}),
3771
+ grantContext: contexts.grantContext,
3772
+ profileRepinContext: contexts.profileRepinContext,
3743
3773
  compact: options.compact,
3744
3774
  }),
3745
3775
  });
3746
- dispatched = await dispatchCommand(profileRepinContext);
3747
- if (profileRepinContext !== undefined &&
3748
- outpostRejectedProfileRepinOption(dispatched)) {
3749
- dispatched = await dispatchCommand();
3776
+ let contexts = { grantContext, profileRepinContext };
3777
+ dispatched = await dispatchCommand(contexts);
3778
+ for (let retry = 0; action === "resume" && retry < 2; retry += 1) {
3779
+ if (contexts.grantContext !== undefined && outpostRejectedGrantContextOption(dispatched)) {
3780
+ grantContextRejected = true;
3781
+ break;
3782
+ }
3783
+ else if (contexts.profileRepinContext !== undefined &&
3784
+ outpostRejectedProfileRepinOption(dispatched)) {
3785
+ contexts = { ...contexts, profileRepinContext: undefined };
3786
+ }
3787
+ else {
3788
+ break;
3789
+ }
3790
+ dispatched = await dispatchCommand(contexts);
3750
3791
  }
3751
3792
  }
3752
3793
  catch (err) {
3753
3794
  throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
3754
3795
  }
3796
+ if (grantContextRejected) {
3797
+ throw new LanesError("outpost_cli_upgrade_required", `Outpost ${outpostId} needs an hq-cli release containing this fix before it can resume lane ${lane.lane_id} under its primary-hosted senior`, {
3798
+ lane_id: lane.lane_id,
3799
+ outpost_id: outpostId,
3800
+ required_option: "--grant-context",
3801
+ });
3802
+ }
3755
3803
  const ownerError = ownerErrorFromOutput(dispatched.stdout);
3756
3804
  if (options.effort !== undefined && outpostRejectedEffortOption(dispatched)) {
3757
3805
  throw new LanesError("outpost_effort_unsupported", `Outpost ${outpostId} rejected --effort; update hq-cli on that Outpost before changing a lane's effort remotely`, {
@@ -3766,7 +3814,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3766
3814
  });
3767
3815
  }
3768
3816
  if (ownerError) {
3769
- if (action === "resume" && ownerError.error === "worker_profile_hash_mismatch") {
3817
+ if (action === "resume" &&
3818
+ (ownerError.error === "worker_profile_hash_mismatch" ||
3819
+ ownerError.error.startsWith("hire_grant_"))) {
3770
3820
  throw new LanesError(ownerError.error, ownerError.message, {
3771
3821
  ...ownerError.details,
3772
3822
  lane_id: lane.lane_id,
@@ -3821,7 +3871,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3821
3871
  Array.isArray(remote.changed_descendant_ids)
3822
3872
  : (remote.delivery_status === "worker_spawned" ||
3823
3873
  remote.delivery_status === "queued_on_target_account") &&
3824
- (options.worker === undefined || remote.lane !== undefined));
3874
+ remote.lane !== undefined);
3825
3875
  if (!resultConfirmed) {
3826
3876
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3827
3877
  lane_id: lane.lane_id,
@@ -3842,7 +3892,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3842
3892
  });
3843
3893
  return;
3844
3894
  }
3845
- if (action === "reparent" || (action === "resume" && options.worker !== undefined)) {
3895
+ if (action === "reparent" || action === "resume") {
3846
3896
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3847
3897
  const descendants = [];
3848
3898
  if (action === "reparent") {
@@ -6549,16 +6599,25 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6549
6599
  async function runResume(ctx, laneId, opts) {
6550
6600
  const hqRoot = ctx.hqRoot();
6551
6601
  let lane = readLane(hqRoot, laneId);
6602
+ const hireGrantContext = opts.grantContext === undefined
6603
+ ? undefined
6604
+ : (() => {
6605
+ if (!ctx.isSsmOwnerDispatch()) {
6606
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Resume grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6607
+ }
6608
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
6609
+ })();
6552
6610
  if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
6553
6611
  throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6554
6612
  }
6555
6613
  if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
6614
+ await withResumeHireAdmission(ctx, lane, () => undefined);
6556
6615
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
6557
6616
  return;
6558
6617
  }
6559
6618
  // Fail before provider/session work if a terminal child would consume a
6560
6619
  // slot that its senior has already assigned elsewhere.
6561
- await withResumeHireAdmission(ctx, lane, () => undefined);
6620
+ await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
6562
6621
  assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
6563
6622
  const cliEffort = parseCliEffort(lane.provider, opts.effort);
6564
6623
  if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
@@ -6768,7 +6827,7 @@ async function runResume(ctx, laneId, opts) {
6768
6827
  compact: opts.compact !== false,
6769
6828
  effort: resumeEffort ?? recordedResumeEffort(lane),
6770
6829
  });
6771
- });
6830
+ }, hireGrantContext);
6772
6831
  }, ctx.sleep);
6773
6832
  }
6774
6833
  if (text !== undefined && messageId !== undefined) {
@@ -9567,6 +9626,7 @@ export function registerLanesCommand(parent, deps = {}) {
9567
9626
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
9568
9627
  .option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
9569
9628
  .addOption(new Option("--profile-repin-context <base64url>").hideHelp())
9629
+ .addOption(new Option("--grant-context <base64url>").hideHelp())
9570
9630
  .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
9571
9631
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
9572
9632
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
@@ -149,6 +149,11 @@ export function hasActivePendingResume(lane, nowMs) {
149
149
  }
150
150
  /** Whether a lane currently owns a running slot for its host/provider caps. */
151
151
  export function laneOccupiesRunningSlot(lane, nowMs = Date.now()) {
152
+ // An account-cap queue is waiting behind an occupied account slot. Its
153
+ // pending resume must remain active for admission ordering, but it must not
154
+ // count as a second owner of the slot that blocked it.
155
+ if (lane.state === "queued" && lane.queued_reason === "account_cap")
156
+ return false;
152
157
  return lane.state === "running" ||
153
158
  hasActiveAdmissionReservation(lane, nowMs) ||
154
159
  hasActivePendingResume(lane, nowMs);
@@ -4,16 +4,16 @@ export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" |
4
4
  export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
5
5
  export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
6
6
  export interface HireGrantAdmissionSnapshot {
7
- parent: LaneRecord;
8
- children: LaneRecord[];
7
+ parent: LaneHireGrantRow;
8
+ children: LaneHireGrantRow[];
9
9
  parentGrant: number;
10
10
  availableChildGrant: number;
11
11
  }
12
12
  /** Shared refusal logic for resuming a terminal lane under an active senior. */
13
13
  export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot): LanesError | null;
14
- export declare function hireGrantCapacityError(parent: LaneRecord, children: readonly LaneRecord[]): LanesError;
14
+ export declare function hireGrantCapacityError(parent: LaneHireGrantRow, children: readonly LaneHireGrantRow[]): LanesError;
15
15
  /** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
16
- export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T): T;
16
+ export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[]): T;
17
17
  export interface HireGrantBackfillRow {
18
18
  lane_id: string;
19
19
  existing_grant: number | null;
@@ -72,10 +72,28 @@ export function hireGrantCapacityError(parent, children) {
72
72
  });
73
73
  }
74
74
  /** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
75
- export function withHireGrantAdmission(hqRoot, parentLaneId, fn) {
75
+ export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy) {
76
76
  return withLaneHireLocks(hqRoot, [parentLaneId], () => {
77
- const parent = readLane(hqRoot, parentLaneId);
78
- const allLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
77
+ const ownerLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
78
+ let allLanes;
79
+ let parent;
80
+ if (suppliedHierarchy === undefined) {
81
+ parent = readLane(hqRoot, parentLaneId);
82
+ allLanes = ownerLanes;
83
+ }
84
+ else {
85
+ const byId = new Map(suppliedHierarchy.map((lane) => [lane.lane_id, lane]));
86
+ // The owner copy is authoritative for every lane it stores; the supplied
87
+ // primary snapshot fills only hierarchy rows that live on another host.
88
+ for (const lane of ownerLanes)
89
+ byId.set(lane.lane_id, lane);
90
+ allLanes = [...byId.values()];
91
+ const suppliedParent = byId.get(parentLaneId);
92
+ if (!suppliedParent) {
93
+ throw new LanesError("hire_grant_context_invalid", `The dispatched hierarchy is missing senior lane ${parentLaneId}`, { senior_lane_id: parentLaneId });
94
+ }
95
+ parent = suppliedParent;
96
+ }
79
97
  const children = aliveDirectChildren(allLanes, parentLaneId);
80
98
  const parentGrant = parent.hire_grant ?? 0;
81
99
  const availableChildGrant = Math.max(0, parentGrant - assignedChildHireGrants(children));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.230.0",
3
+ "version": "5.230.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {