@indigoai-us/hq-cli 5.230.1 → 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,16 @@
|
|
|
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
|
+
|
|
5
15
|
## [5.230.1] — 2026-09-27
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -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"
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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 = (
|
|
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
|
-
|
|
3771
|
+
grantContext: contexts.grantContext,
|
|
3772
|
+
profileRepinContext: contexts.profileRepinContext,
|
|
3743
3773
|
compact: options.compact,
|
|
3744
3774
|
}),
|
|
3745
3775
|
});
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
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" &&
|
|
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
|
-
|
|
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" ||
|
|
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")
|
|
@@ -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:
|
|
8
|
-
children:
|
|
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:
|
|
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;
|
package/dist/lib/lanes/grants.js
CHANGED
|
@@ -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
|
|
78
|
-
|
|
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));
|