@indigoai-us/hq-cli 5.274.0 → 5.274.1

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,7 +2,7 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [5.274.0] — 2026-09-28
5
+ ## [5.274.1] — 2026-09-28
6
6
 
7
7
  ### Fixed
8
8
 
@@ -37,6 +37,10 @@
37
37
  - Internal: moved the move, move-target and placement subcommands out of
38
38
  `src/commands/lanes.ts`; no behaviour change.
39
39
 
40
+ ### Fixed
41
+
42
+ - With lineage-aware supersession enabled, same-profile workers under the same session senior no longer supersede each other. The recorded senior can message its worker if it is later superseded.
43
+
40
44
  ## [5.273.0] — 2026-09-28
41
45
 
42
46
  ### Fixed
@@ -13,7 +13,7 @@ import { keepAliveExclusion, keepAliveSupersededBy, LANE_SUPERSESSION_LINEAGE_AW
13
13
  import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
14
14
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
15
15
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
16
- import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
16
+ import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
17
17
  export function createLaneMessageHandlers(deps) {
18
18
  const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, casUpdateLane, dispatchOutpostLaneCommand, drainHookStatusFor, laneIsHostedHere, laneWithRecoveredProviderSession, loadAllLanes, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveActiveQuestionCompany, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
19
19
  function currentSuccessorOf(lane, lanes, lineageAware, companyForLane) {
@@ -30,7 +30,19 @@ export function createLaneMessageHandlers(deps) {
30
30
  }
31
31
  return current.lane_id === lane.lane_id ? undefined : current;
32
32
  }
33
- function callerMayMessageSupersededLane(ctx, lane, lanes, lineageAware, companyForLane, callerLaneIdOverride) {
33
+ function callerMayMessageSupersededLane(ctx, lane, lanes, lineageAware, companyForLane, callerLaneIdOverride, callerSessionIdOverride) {
34
+ if (lane.senior.kind === "session") {
35
+ const sessionSeniorId = lane.senior.id;
36
+ if (!sessionSeniorId)
37
+ return false;
38
+ if (callerLaneIdOverride !== undefined || ctx.env.HQ_LANE_ID?.trim())
39
+ return false;
40
+ if (callerSessionIdOverride === null)
41
+ return false;
42
+ return callerSessionIdOverride === undefined
43
+ ? callerMatchesSessionSenior(ctx, sessionSeniorId)
44
+ : callerSessionIdOverride === sessionSeniorId;
45
+ }
34
46
  if (lane.senior.kind !== "lane")
35
47
  return false;
36
48
  const callerLaneId = callerLaneIdOverride ?? ctx.env.HQ_LANE_ID?.trim();
@@ -86,11 +98,15 @@ export function createLaneMessageHandlers(deps) {
86
98
  let lanes = localLanes;
87
99
  const retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
88
100
  const dispatchedCallerId = grantContext?.superseded_message_caller_id;
89
- const dispatchedContextAuthorized = grantContext !== undefined &&
90
- dispatchedCallerId !== undefined &&
101
+ const dispatchedSessionId = grantContext?.superseded_message_session_id;
102
+ const dispatchedContextBound = grantContext !== undefined &&
91
103
  ctx.isSsmOwnerDispatch() &&
92
104
  sameHireGrantSenior(grantContext.expected_senior, currentLane.senior);
93
- if (dispatchedContextAuthorized && grantContext) {
105
+ const dispatchedSessionContext = dispatchedContextBound && currentLane.senior.kind === "session";
106
+ const dispatchedContextAuthorized = dispatchedContextBound && (dispatchedCallerId !== undefined ||
107
+ (dispatchedSessionId !== undefined && dispatchedSessionId === currentLane.senior.id));
108
+ if (dispatchedContextBound && grantContext &&
109
+ (dispatchedCallerId !== undefined || dispatchedSessionContext)) {
94
110
  const merged = new Map(localLanes.map((record) => [record.lane_id, record]));
95
111
  for (const snapshot of grantContext.hierarchy) {
96
112
  const contextLane = keepAliveLaneFromSnapshot(snapshot);
@@ -101,14 +117,23 @@ export function createLaneMessageHandlers(deps) {
101
117
  lanes = [...merged.values()];
102
118
  }
103
119
  const exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
104
- const callerLaneId = dispatchedContextAuthorized ? dispatchedCallerId : undefined;
105
- if (exclusion === "superseded" && callerMayMessageSupersededLane(ctx, currentLane, lanes, lineageAware, companyForLane, callerLaneId)) {
120
+ const callerLaneId = dispatchedContextBound ? dispatchedCallerId : undefined;
121
+ const callerSessionId = dispatchedContextBound
122
+ ? (dispatchedSessionId === undefined ? null : dispatchedSessionId)
123
+ : undefined;
124
+ if (exclusion === "superseded" && callerMayMessageSupersededLane(ctx, currentLane, lanes, lineageAware, companyForLane, callerLaneId, callerSessionId)) {
106
125
  return {
107
126
  lane: currentLane,
108
127
  exclusion: undefined,
109
128
  ...(!dispatchedContextAuthorized && ctx.env.HQ_LANE_ID?.trim()
110
129
  ? { supersededMessageCallerId: ctx.env.HQ_LANE_ID.trim() }
111
130
  : {}),
131
+ ...(!dispatchedContextBound && currentLane.senior.kind === "session" &&
132
+ typeof currentLane.senior.id === "string" &&
133
+ currentLane.senior.id.length > 0 &&
134
+ callerMatchesSessionSenior(ctx, currentLane.senior.id)
135
+ ? { supersededMessageSessionId: currentLane.senior.id }
136
+ : {}),
112
137
  };
113
138
  }
114
139
  return { lane: currentLane, exclusion };
@@ -149,9 +174,17 @@ export function createLaneMessageHandlers(deps) {
149
174
  const supersededMessageCallerId = callerLaneId === undefined || resolution.remoteOnly
150
175
  ? undefined
151
176
  : (await messageLaneExclusion(ctx, lane)).supersededMessageCallerId;
177
+ const sessionSeniorId = lane.senior.kind === "session" ? lane.senior.id : undefined;
178
+ const supersededMessageSessionId = !resolution.remoteOnly &&
179
+ typeof sessionSeniorId === "string" &&
180
+ sessionSeniorId.length > 0 &&
181
+ callerMatchesSessionSenior(ctx, sessionSeniorId)
182
+ ? sessionSeniorId
183
+ : undefined;
152
184
  await dispatchOutpostLaneCommand(ctx, lane, "message", {
153
185
  ...opts,
154
186
  ...(supersededMessageCallerId === undefined ? {} : { supersededMessageCallerId }),
187
+ ...(supersededMessageSessionId === undefined ? {} : { supersededMessageSessionId }),
155
188
  }, resolution.remoteOnly
156
189
  ? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
157
190
  : undefined);
@@ -43,6 +43,8 @@ export interface LaneMessageOptions {
43
43
  grantContext?: string;
44
44
  /** Primary-verified caller identity for a superseded Outpost message. */
45
45
  supersededMessageCallerId?: string;
46
+ /** Primary-verified session senior for a superseded Outpost message. */
47
+ supersededMessageSessionId?: string;
46
48
  monitorEvent?: {
47
49
  monitor_id: string;
48
50
  description: string;
@@ -248,6 +250,8 @@ export declare function readLanePlacementEnabled(ctx: Ctx, company: LaneCompanyR
248
250
  export declare function processLanePlacementCompany(ctx: Ctx): LaneCompanyRef | undefined;
249
251
  export declare function parsePlacementPolicy(raw: string): LanePlacementPolicy;
250
252
  export declare function assertPlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined, readPlacementEnabled?: () => Promise<boolean>): Promise<void>;
253
+ /** Match a direct session-senior caller while refusing lane-worker identities. */
254
+ export declare function callerMatchesSessionSenior(ctx: Pick<Ctx, "env">, seniorSessionId: string): boolean;
251
255
  export interface LaneWaitCommandOptions {
252
256
  any?: string[];
253
257
  for?: string[];
@@ -4,6 +4,7 @@ import { resolveLaneEffort } from "../lib/lanes/effort.js";
4
4
  import { LanesError } from "../lib/lanes/errors.js";
5
5
  import { AccountResolutionError, accountStatus, loadLanesConfig } from "../lib/lanes/index.js";
6
6
  import { resolveLaneModel } from "../lib/lanes/spawn.js";
7
+ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
7
8
  export function accountRequestKeepsCurrentLane(lane, rawAccount) {
8
9
  const raw = rawAccount.trim();
9
10
  const colon = raw.indexOf(":");
@@ -151,4 +152,8 @@ export async function assertPlacementEnabled(ctx, company, readPlacementEnabled
151
152
  return;
152
153
  throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
153
154
  }
155
+ /** Match a direct session-senior caller while refusing lane-worker identities. */
156
+ export function callerMatchesSessionSenior(ctx, seniorSessionId) {
157
+ return !ctx.env.HQ_LANE_ID?.trim() && sessionIdFromEnv(ctx.env) === seniorSessionId;
158
+ }
154
159
  //# sourceMappingURL=lanes-shared.js.map
@@ -17,7 +17,7 @@ import { createLaneMoveHandlers } from "./lanes-move.js";
17
17
  import { createLaneQuestionsHandlers } from "./lanes-questions.js";
18
18
  import { createLaneResumeHandlers } from "./lanes-resume.js";
19
19
  import { createLaneWatchHandlers } from "./lanes-watch.js";
20
- import { assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readLanePlacementEnabled, } from "./lanes-shared.js";
20
+ import { callerMatchesSessionSenior, assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readLanePlacementEnabled, } from "./lanes-shared.js";
21
21
  export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
22
22
  export { resolveAnswerChoice } from "./lanes-questions.js";
23
23
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
@@ -826,9 +826,9 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
826
826
  const hqRoot = ctx.hqRoot();
827
827
  if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
828
828
  !laneGrantTargetHostedHere(ctx, lane) ||
829
- lane.senior.kind !== "lane" ||
829
+ (lane.senior.kind !== "lane" && lane.senior.kind !== "session") ||
830
830
  !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
831
- 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 });
831
+ throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its Outpost dispatch`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
832
832
  }
833
833
  if (!isTerminalLaneState(lane.state) ||
834
834
  lane.senior.kind !== "lane" ||
@@ -3486,10 +3486,10 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3486
3486
  grantHierarchy = hierarchy;
3487
3487
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3488
3488
  }
3489
- else if ((action === "resume" || action === "message") &&
3490
- lane.senior.kind === "lane") {
3489
+ else if ((action === "resume" && lane.senior.kind === "lane") ||
3490
+ (action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
3491
3491
  const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3492
- grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined);
3492
+ grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
3493
3493
  }
3494
3494
  let dispatched;
3495
3495
  let grantContextRejected = false;
@@ -4468,9 +4468,12 @@ function callerSeniorAuthorization(ctx, seniorRef) {
4468
4468
  if (!sessionId)
4469
4469
  return { authorized: false, reason: "no session id" };
4470
4470
  if (seniorRef.kind === "session") {
4471
- return sessionId === seniorRef.id
4471
+ const seniorSessionId = seniorRef.id;
4472
+ if (!seniorSessionId)
4473
+ return { authorized: false, reason: "session senior has no id" };
4474
+ return callerMatchesSessionSenior(ctx, seniorSessionId)
4472
4475
  ? { authorized: true }
4473
- : { authorized: false, reason: `session id ${sessionId} is not senior session ${seniorRef.id}` };
4476
+ : { authorized: false, reason: `session id ${sessionId} is not senior session ${seniorSessionId}` };
4474
4477
  }
4475
4478
  if (seniorRef.kind !== "user")
4476
4479
  return { authorized: false, reason: "senior is not a user" };
@@ -16,9 +16,10 @@ export interface OutpostHireGrantContext {
16
16
  expected_senior: SeniorRef;
17
17
  hierarchy: HireGrantLaneSnapshot[];
18
18
  superseded_message_caller_id?: string;
19
+ superseded_message_session_id?: string;
19
20
  }
20
21
  /** Encode only the owner-relevant hierarchy from the primary's complete store. */
21
- export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], supersededMessageCallerId?: string): string;
22
+ export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], supersededMessageCallerId?: string, supersededMessageSessionId?: string): string;
22
23
  /** Decode and validate an owner grant context, binding it to the target lane. */
23
24
  export declare function parseOutpostHireGrantContext(encoded: string, targetLaneId: string): OutpostHireGrantContext;
24
25
  export declare function sameHireGrantSenior(a: SeniorRef, b: SeniorRef): boolean;
@@ -30,11 +30,18 @@ function snapshot(lane) {
30
30
  };
31
31
  }
32
32
  /** Encode only the owner-relevant hierarchy from the primary's complete store. */
33
- export function createOutpostHireGrantContext(target, lanes, supersededMessageCallerId) {
33
+ export function createOutpostHireGrantContext(target, lanes, supersededMessageCallerId, supersededMessageSessionId) {
34
34
  const current = lanes.find((lane) => lane.lane_id === target.lane_id);
35
35
  if (!current || !sameSenior(current.senior, target.senior)) {
36
36
  throw new LanesError("hire_grant_hierarchy_unavailable", `Cannot build an authoritative hire-grant hierarchy for lane ${target.lane_id}`, { lane_id: target.lane_id });
37
37
  }
38
+ if (supersededMessageCallerId !== undefined && supersededMessageSessionId !== undefined) {
39
+ throw contextError("Outpost grant context cannot contain both lane and session message callers");
40
+ }
41
+ if (supersededMessageSessionId !== undefined &&
42
+ (current.senior.kind !== "session" || current.senior.id !== supersededMessageSessionId)) {
43
+ throw new LanesError("hire_grant_message_caller_invalid", `Cannot include an unverified session message caller in lane ${target.lane_id}'s Outpost context`, { lane_id: target.lane_id });
44
+ }
38
45
  const relevant = new Map();
39
46
  relevant.set(current.lane_id, current);
40
47
  if (current.senior.kind === "lane") {
@@ -65,6 +72,18 @@ export function createOutpostHireGrantContext(target, lanes, supersededMessageCa
65
72
  }
66
73
  }
67
74
  }
75
+ if (current.senior.kind === "session") {
76
+ const companyUid = current.company?.uid;
77
+ const projectId = current.project_id;
78
+ if (companyUid) {
79
+ for (const candidate of lanes) {
80
+ if (candidate.company?.uid === companyUid && ((current.worker_profile !== undefined && candidate.worker_profile === current.worker_profile) ||
81
+ (projectId !== undefined && candidate.project_id === projectId && (candidate.hire_grant ?? 0) > 0))) {
82
+ relevant.set(candidate.lane_id, candidate);
83
+ }
84
+ }
85
+ }
86
+ }
68
87
  const hierarchy = [...relevant.values()]
69
88
  .map(snapshot)
70
89
  .sort((a, b) => a.lane_id.localeCompare(b.lane_id));
@@ -79,6 +98,9 @@ export function createOutpostHireGrantContext(target, lanes, supersededMessageCa
79
98
  ...(supersededMessageCallerId === undefined
80
99
  ? {}
81
100
  : { superseded_message_caller_id: supersededMessageCallerId }),
101
+ ...(supersededMessageSessionId === undefined
102
+ ? {}
103
+ : { superseded_message_session_id: supersededMessageSessionId }),
82
104
  };
83
105
  const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
84
106
  if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
@@ -183,7 +205,7 @@ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
183
205
  throw contextError("Outpost grant context must be an object");
184
206
  }
185
207
  const record = raw;
186
- if (Object.keys(record).some((key) => !["schema_version", "enforcement_enabled", "expected_senior", "hierarchy", "superseded_message_caller_id"].includes(key))) {
208
+ if (Object.keys(record).some((key) => !["schema_version", "enforcement_enabled", "expected_senior", "hierarchy", "superseded_message_caller_id", "superseded_message_session_id"].includes(key))) {
187
209
  throw contextError("Outpost grant context contains unknown fields");
188
210
  }
189
211
  if (record.schema_version !== HIRE_GRANT_CONTEXT_SCHEMA ||
@@ -194,10 +216,18 @@ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
194
216
  }
195
217
  const expectedSenior = parseSenior(record.expected_senior);
196
218
  const messageCallerId = record.superseded_message_caller_id;
219
+ const messageSessionId = record.superseded_message_session_id;
197
220
  if (messageCallerId !== undefined &&
198
221
  (typeof messageCallerId !== "string" || !messageCallerId.trim() || messageCallerId.trim() !== messageCallerId)) {
199
222
  throw contextError("Outpost grant context contains an invalid superseded-message caller");
200
223
  }
224
+ if (messageSessionId !== undefined &&
225
+ (typeof messageSessionId !== "string" || !messageSessionId.trim() || messageSessionId.trim() !== messageSessionId)) {
226
+ throw contextError("Outpost grant context contains an invalid superseded-message session");
227
+ }
228
+ if (messageCallerId !== undefined && messageSessionId !== undefined) {
229
+ throw contextError("Outpost grant context cannot contain both lane and session message callers");
230
+ }
201
231
  const hierarchy = record.hierarchy.map(parseLaneSnapshot);
202
232
  if (!expectedSenior || hierarchy.some((lane) => lane === null)) {
203
233
  throw contextError("Outpost grant context contains an invalid senior or lane snapshot");
@@ -211,6 +241,10 @@ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
211
241
  if (!target || !sameSenior(target.senior, expectedSenior)) {
212
242
  throw contextError("Outpost grant context does not match the target's expected senior");
213
243
  }
244
+ if (messageSessionId !== undefined &&
245
+ (expectedSenior.kind !== "session" || expectedSenior.id !== messageSessionId)) {
246
+ throw contextError("Outpost grant context session caller does not match the target's session senior");
247
+ }
214
248
  if (expectedSenior.kind === "lane" &&
215
249
  (!expectedSenior.id || !byId.has(expectedSenior.id))) {
216
250
  throw contextError("Outpost grant context is missing the target's senior lane");
@@ -232,6 +266,7 @@ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
232
266
  expected_senior: expectedSenior,
233
267
  hierarchy: snapshots,
234
268
  ...(messageCallerId === undefined ? {} : { superseded_message_caller_id: messageCallerId }),
269
+ ...(messageSessionId === undefined ? {} : { superseded_message_session_id: messageSessionId }),
235
270
  };
236
271
  }
237
272
  export function sameHireGrantSenior(a, b) {
@@ -234,8 +234,8 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
234
234
  // Any descendant is part of this lead's work tree, not a replacement lead.
235
235
  if (laneAncestorsFrom(candidate.senior, lanesById, sessionOwners).has(lane.lane_id))
236
236
  return false;
237
- const sameLaneSenior = lane.senior.kind === "lane" &&
238
- candidate.senior.kind === "lane" &&
237
+ const sameLaneSenior = (lane.senior.kind === "lane" || lane.senior.kind === "session") &&
238
+ candidate.senior.kind === lane.senior.kind &&
239
239
  candidate.senior.id === lane.senior.id;
240
240
  const sameProfile = lane.worker_profile !== undefined &&
241
241
  candidate.worker_profile === lane.worker_profile &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.274.0",
3
+ "version": "5.274.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {