@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 +5 -1
- package/dist/commands/lanes-message.js +40 -7
- package/dist/commands/lanes-shared.d.ts +4 -0
- package/dist/commands/lanes-shared.js +5 -0
- package/dist/commands/lanes.js +11 -8
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +37 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [5.274.
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
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 =
|
|
105
|
-
|
|
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
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
|
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"
|
|
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
|
-
|
|
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 ${
|
|
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 ===
|
|
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 &&
|