@indigoai-us/hq-cli 5.247.0 → 5.248.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.
@@ -0,0 +1,455 @@
1
+ import { BALANCE_MOVABLE_STATES } from "../lib/lanes/claude-balance.js";
2
+ import { LanesError } from "../lib/lanes/errors.js";
3
+ import { appendCappedHistory, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
4
+ import { parseCliEffort } from "../lib/lanes/effort.js";
5
+ import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
6
+ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
7
+ import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
8
+ import { spawnLaneWorker } from "../lib/lanes/spawn.js";
9
+ import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
10
+ export function createLaneResumeHandlers(deps) {
11
+ const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, automaticBalanceMoveHistoryReason, authorizeFailedResume, authorizeResumeProfileRepin, casUpdateLane, dispatchOutpostLaneCommand, laneIsHostedHere, laneWithRecoveredProviderSession, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
12
+ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
13
+ const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
14
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
15
+ if (lane.provider !== "claude") {
16
+ throw new LanesError("claude_account_balance_provider_mismatch", `Lane ${laneId} does not use Claude`, {
17
+ lane_id: laneId,
18
+ provider: lane.provider,
19
+ });
20
+ }
21
+ const expectedFrom = opts.fromAccount?.trim();
22
+ if (!expectedFrom) {
23
+ throw new LanesError("claude_account_balance_source_invalid", "Balance move requires --from-account <id>", {
24
+ lane_id: laneId,
25
+ });
26
+ }
27
+ if (lane.account !== expectedFrom) {
28
+ throw new LanesError("claude_account_balance_source_changed", `Lane ${laneId} no longer uses the planned source account`, {
29
+ lane_id: laneId,
30
+ expected_account: expectedFrom,
31
+ actual_account: lane.account ?? null,
32
+ });
33
+ }
34
+ const expectedState = opts.expectedState?.trim();
35
+ if (!expectedState) {
36
+ throw new LanesError("claude_account_balance_state_invalid", "Balance move requires --expected-state <state>", {
37
+ lane_id: laneId,
38
+ });
39
+ }
40
+ if (lane.state !== expectedState) {
41
+ throw new LanesError("claude_account_balance_state_changed", `Lane ${laneId} changed state after balance was calculated`, {
42
+ lane_id: laneId,
43
+ expected_state: expectedState,
44
+ actual_state: lane.state,
45
+ });
46
+ }
47
+ if (!BALANCE_MOVABLE_STATES.includes(lane.state) || lane.pending_resume !== undefined) {
48
+ throw new LanesError("claude_account_balance_state_not_movable", `Lane ${laneId} is not between turns`, {
49
+ lane_id: laneId,
50
+ state: lane.state,
51
+ pending_resume: lane.pending_resume !== undefined,
52
+ });
53
+ }
54
+ const account = opts.account?.trim();
55
+ if (!account) {
56
+ throw new LanesError("claude_account_balance_account_invalid", "Balance move requires --account <id>", {
57
+ lane_id: laneId,
58
+ });
59
+ }
60
+ const moved = await prepareAccountMove(ctx, lane, {
61
+ account,
62
+ cliModel: undefined,
63
+ accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
64
+ deferTurnStart: true,
65
+ });
66
+ printJson({
67
+ ok: true,
68
+ action: "balance-move-target",
69
+ lane_id: laneId,
70
+ from: moved.from,
71
+ state: moved.lane.state,
72
+ account: moved.lane.account,
73
+ });
74
+ }
75
+ /**
76
+ * `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
77
+ * continues a lane in `interrupted`, `done` or `blocked`, or a failed lane
78
+ * whose only terminal reason is an invalid envelope after its repair turn,
79
+ * on its recorded
80
+ * provider session. `hq lanes message` keeps refusing `interrupted`; this
81
+ * command is the path its refusal names. The launch is the same
82
+ * `spawnLaneWorker` a message resume uses, so the resume-time compaction
83
+ * rule and the watcher's mesh events are the same. The record gains
84
+ * `last_resume` with `trigger: "resume"`, `resumed_from` and the text.
85
+ */
86
+ async function runResume(ctx, laneId, opts) {
87
+ const hqRoot = ctx.hqRoot();
88
+ let lane = readLane(hqRoot, laneId);
89
+ const failedResumeReason = opts.reason?.trim();
90
+ if (opts.reason !== undefined && opts.fromFailed !== true) {
91
+ throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
92
+ }
93
+ if (opts.fromFailed === true && !failedResumeReason) {
94
+ throw new LanesError("failed_resume_reason_required", "--from-failed requires a non-empty --reason", { lane_id: laneId });
95
+ }
96
+ if (opts.reason !== undefined)
97
+ assertNoSecretText("resume reason", opts.reason);
98
+ if (opts.failedResumeContext !== undefined && opts.fromFailed !== true) {
99
+ throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context requires --from-failed", { lane_id: laneId });
100
+ }
101
+ const failedResumeActor = opts.fromFailed === true
102
+ ? authorizeFailedResume(ctx, lane, opts.failedResumeContext)
103
+ : undefined;
104
+ if (opts.fromFailed === true && lane.state !== "failed") {
105
+ throw new LanesError("lane_not_failed", `Lane ${laneId} is ${lane.state}; --from-failed only applies to a failed lane`, { lane_id: laneId, state: lane.state });
106
+ }
107
+ const failedResumeExpectedSenior = opts.fromFailed === true
108
+ ? { ...lane.senior }
109
+ : undefined;
110
+ const recoveredFailure = opts.fromFailed === true
111
+ ? lane.terminal_reason ?? "failure_reason_unavailable"
112
+ : undefined;
113
+ const hireGrantContext = opts.grantContext === undefined
114
+ ? undefined
115
+ : (() => {
116
+ if (!ctx.isSsmOwnerDispatch()) {
117
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Resume grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
118
+ }
119
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
120
+ })();
121
+ if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
122
+ throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
123
+ }
124
+ if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
125
+ await withResumeHireAdmission(ctx, lane, () => undefined);
126
+ await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
127
+ return;
128
+ }
129
+ // Fail before provider/session work if a terminal child would consume a
130
+ // slot that its senior has already assigned elsewhere.
131
+ await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
132
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
133
+ const cliEffort = parseCliEffort(lane.provider, opts.effort);
134
+ if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
135
+ throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
136
+ }
137
+ lane = laneWithRecoveredProviderSession(hqRoot, lane);
138
+ if (opts.model !== undefined && opts.account === undefined) {
139
+ throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
140
+ }
141
+ if (opts.account !== undefined && lane.state === "running") {
142
+ throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
143
+ lane_id: laneId,
144
+ state: lane.state,
145
+ });
146
+ }
147
+ if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
148
+ throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
149
+ }
150
+ if (lane.state === "queued") {
151
+ throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
152
+ }
153
+ const refusal = resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
154
+ if (refusal !== null) {
155
+ throw new LanesError(refusal.error, refusal.message, {
156
+ lane_id: laneId,
157
+ state: lane.state,
158
+ provider: lane.provider,
159
+ message: refusal.message,
160
+ });
161
+ }
162
+ assertLaneRunsHereForAdmission(ctx, lane);
163
+ const resumedFrom = lane.state;
164
+ const text = opts.text?.trim() ? opts.text : undefined;
165
+ const at = ctx.now().toISOString();
166
+ const projectId = opts.project?.trim() || lane.project_id;
167
+ const storyId = opts.story?.trim() || lane.story_id;
168
+ if (opts.story !== undefined && !opts.story.trim()) {
169
+ throw new LanesError("missing_story", "--story must be non-empty", {});
170
+ }
171
+ if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
172
+ validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
173
+ }
174
+ const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
175
+ const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
176
+ let requestedMeshCompany;
177
+ let meshThreadId;
178
+ if (opts.meshCompany !== undefined) {
179
+ const meshCompanySlug = opts.meshCompany.trim();
180
+ if (!meshCompanySlug) {
181
+ throw new LanesError("missing_mesh_company", "--mesh-company must be a non-empty company slug", {});
182
+ }
183
+ requestedMeshCompany = await ctx.resolveCompany(meshCompanySlug);
184
+ if (lane.company !== null &&
185
+ (lane.company.uid !== requestedMeshCompany.uid || lane.company.slug !== requestedMeshCompany.slug)) {
186
+ throw new LanesError("mesh_company_mismatch", "--mesh-company must match the lane's company when the lane is company-owned", { company: lane.company.slug, mesh_company: requestedMeshCompany.slug });
187
+ }
188
+ meshThreadId = await ctx.mesh.ensureProjectThread(requestedMeshCompany, projectId);
189
+ }
190
+ const resumeProfile = opts.worker !== undefined
191
+ ? resolveResumeProfile(ctx, lane, opts.worker)
192
+ : undefined;
193
+ const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
194
+ const keepAlive = opts.keepAlive === true
195
+ ? true
196
+ : resumeProfile?.profile.meta.keep_alive;
197
+ const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
198
+ if (keepCurrentAccount)
199
+ assertCurrentResumeAccountAvailable(ctx, lane);
200
+ if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
201
+ validateResumeProfileModel(ctx, lane, resumeProfile.profile);
202
+ }
203
+ const automaticAccountSelection = opts.account === undefined
204
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
205
+ : undefined;
206
+ const resumeEffort = resolveResumeEffort(ctx, lane, {
207
+ cliEffort,
208
+ profile: resumeProfile,
209
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
210
+ model: opts.model,
211
+ });
212
+ let move = null;
213
+ if (opts.account !== undefined && !keepCurrentAccount) {
214
+ move = await prepareAccountMove(ctx, lane, {
215
+ account: opts.account,
216
+ cliModel: opts.model?.trim() || undefined,
217
+ ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
218
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
219
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
220
+ pending: {
221
+ trigger: "resume",
222
+ resumed_from: resumedFrom,
223
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
224
+ ...(failedResumeExpectedSenior !== undefined
225
+ ? { expected_senior: failedResumeExpectedSenior }
226
+ : {}),
227
+ ...(text !== undefined ? { text } : {}),
228
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
229
+ compact: opts.compact !== false,
230
+ },
231
+ });
232
+ lane = move.lane;
233
+ }
234
+ else if (automaticAccountSelection !== undefined) {
235
+ const selection = automaticAccountSelection.selection;
236
+ if (selection.account !== lane.account) {
237
+ move = await prepareAccountMove(ctx, lane, {
238
+ account: selection.account,
239
+ cliModel: undefined,
240
+ ...(automaticAccountSelection.historyReason !== undefined
241
+ ? { accountHistoryReason: automaticAccountSelection.historyReason }
242
+ : {}),
243
+ ...(automaticAccountSelection.preservedModel !== undefined
244
+ ? { preservedModel: automaticAccountSelection.preservedModel }
245
+ : {}),
246
+ ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
247
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
248
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
249
+ pending: {
250
+ trigger: "resume",
251
+ resumed_from: resumedFrom,
252
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
253
+ ...(failedResumeExpectedSenior !== undefined
254
+ ? { expected_senior: failedResumeExpectedSenior }
255
+ : {}),
256
+ ...(text !== undefined ? { text } : {}),
257
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
258
+ compact: opts.compact !== false,
259
+ },
260
+ accountSelection: selection,
261
+ });
262
+ lane = move.lane;
263
+ }
264
+ else {
265
+ lane = persistClaudeAccountSelection(hqRoot, lane, selection);
266
+ }
267
+ }
268
+ lane = recordResumeSetup(ctx, lane, {
269
+ ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
270
+ ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
271
+ ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
272
+ ...(keepAlive !== undefined ? { keepAlive } : {}),
273
+ ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
274
+ ...(requestedMeshCompany !== undefined
275
+ ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
276
+ : {}),
277
+ });
278
+ let queuedOnAdmission = false;
279
+ let resumed;
280
+ if (move?.queued) {
281
+ resumed = lane;
282
+ }
283
+ else {
284
+ resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
285
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
286
+ if (admission.action === "refuse" && admission.error)
287
+ throw admission.error;
288
+ return withResumeHireAdmission(ctx, lane, () => {
289
+ if (admission.action === "queue") {
290
+ const sessionId = lane.provider_session_id;
291
+ if (!sessionId) {
292
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
293
+ lane_id: laneId,
294
+ provider: lane.provider,
295
+ });
296
+ }
297
+ queuedOnAdmission = true;
298
+ return casUpdateLane(hqRoot, laneId, (current) => {
299
+ if (current.version !== lane.version || current.state !== lane.state) {
300
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
301
+ lane_id: laneId,
302
+ prepared_version: lane.version,
303
+ version: current.version,
304
+ state: current.state,
305
+ });
306
+ }
307
+ return {
308
+ ...current,
309
+ state: "queued",
310
+ queued_reason: admission.reason ?? "throttle",
311
+ queued_at: at,
312
+ pending_resume: {
313
+ session_id: sessionId,
314
+ trigger: "resume",
315
+ resumed_from: resumedFrom,
316
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
317
+ ...(failedResumeExpectedSenior !== undefined
318
+ ? { expected_senior: failedResumeExpectedSenior }
319
+ : {}),
320
+ ...(text !== undefined ? { text } : {}),
321
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
322
+ compact: opts.compact !== false,
323
+ at,
324
+ },
325
+ };
326
+ });
327
+ }
328
+ return spawnLaneWorker(hqRoot, lane, {
329
+ env: ctx.env,
330
+ modelDeps: {
331
+ readFileSync: ctx.readFileSync,
332
+ probe: ctx.probeProvider,
333
+ env: ctx.env,
334
+ },
335
+ existsSync: ctx.existsSync,
336
+ spawnProcess: ctx.spawnProcess,
337
+ proveDetached: ctx.proveDetached,
338
+ killProcess: ctx.killProcess,
339
+ now: ctx.now,
340
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
341
+ runCompaction: ctx.runCompaction,
342
+ locateTranscript: ctx.locateTranscript,
343
+ }, {
344
+ trigger: {
345
+ kind: "resume",
346
+ from: resumedFrom,
347
+ ...(opts.fromFailed === true ? { fromFailed: true } : {}),
348
+ ...(failedResumeExpectedSenior !== undefined
349
+ ? { expectedSenior: failedResumeExpectedSenior }
350
+ : {}),
351
+ ...(text !== undefined ? { text } : {}),
352
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
353
+ },
354
+ compact: opts.compact !== false,
355
+ effort: resumeEffort ?? recordedResumeEffort(lane),
356
+ });
357
+ }, hireGrantContext);
358
+ }, ctx.sleep);
359
+ }
360
+ if (text !== undefined && messageId !== undefined) {
361
+ // Same audit copy a message resume leaves; nothing drains it. Archive only
362
+ // after a successful spawn so a concurrent resume cannot leave a phantom.
363
+ archiveResumeMessage(hqRoot, laneId, {
364
+ at,
365
+ message_id: messageId,
366
+ text,
367
+ project_id: projectId,
368
+ story_id: storyId,
369
+ resumed_from: resumedFrom,
370
+ });
371
+ }
372
+ let next = resumed;
373
+ if (failedResumeActor !== undefined &&
374
+ failedResumeReason !== undefined &&
375
+ recoveredFailure !== undefined) {
376
+ next = casUpdateLane(hqRoot, laneId, (current) => ({
377
+ ...current,
378
+ failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
379
+ at,
380
+ actor: failedResumeActor,
381
+ reason: failedResumeReason,
382
+ failure: recoveredFailure,
383
+ }),
384
+ }));
385
+ }
386
+ if (text !== undefined) {
387
+ // An operator text moves the compaction reference forward, as a message does.
388
+ next = casUpdateLane(hqRoot, laneId, (current) => ({
389
+ ...resetKeepAliveProgress(current),
390
+ last_message_at: at,
391
+ }));
392
+ }
393
+ if (messageId !== undefined && attributionChanged) {
394
+ next = casUpdateLane(hqRoot, laneId, (current) => {
395
+ const already = current.attribution_history.some((entry) => entry.message_id === messageId);
396
+ if (already)
397
+ return current;
398
+ return {
399
+ ...current,
400
+ project_id: projectId,
401
+ story_id: storyId,
402
+ no_story_reason: storyId ? undefined : current.no_story_reason,
403
+ stories: laneStory(current, projectId, storyId)
404
+ ? laneStories(current)
405
+ : [
406
+ ...laneStories(current),
407
+ {
408
+ project_id: projectId,
409
+ story_id: storyId,
410
+ status: "in_progress",
411
+ added_at: at,
412
+ updated_at: at,
413
+ },
414
+ ],
415
+ attribution_history: appendCappedHistory(current.attribution_history, {
416
+ project_id: projectId,
417
+ story_id: storyId,
418
+ message_id: messageId,
419
+ at,
420
+ }),
421
+ };
422
+ });
423
+ }
424
+ if (text === undefined && hasKeepAliveProgress(next)) {
425
+ next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
426
+ }
427
+ printJson({
428
+ ok: true,
429
+ action: "resume",
430
+ lane_id: laneId,
431
+ resumed_from: resumedFrom,
432
+ message_id: messageId ?? null,
433
+ text: text ?? null,
434
+ delivery: "resume",
435
+ delivery_status: move?.queued
436
+ ? "queued_on_target_account"
437
+ : queuedOnAdmission
438
+ ? "queued_on_admission"
439
+ : "worker_spawned",
440
+ account_move: move ? accountMoveSummary(move) : null,
441
+ resumed_session_id: resumed.last_resume?.session_id ?? null,
442
+ last_resume: next.last_resume ?? null,
443
+ compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
444
+ ? resumed.last_compaction
445
+ : null,
446
+ project_id: next.project_id,
447
+ story_id: next.story_id,
448
+ stories: laneStories(next),
449
+ story_warning: storyWarning(next) ?? null,
450
+ lane: next,
451
+ });
452
+ }
453
+ return { runClaudeBalanceMoveTarget, runResume };
454
+ }
455
+ //# sourceMappingURL=lanes-resume.js.map
@@ -1,4 +1,4 @@
1
- import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
1
+ import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
2
2
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
3
3
  import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
4
4
  import type { ProbeCommand } from "../lib/lanes/models.js";
@@ -7,6 +7,158 @@ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
7
7
  import type { RunCompactionFn } from "../lib/lanes/compaction.js";
8
8
  import type { LaneMesh, LaneMeshEvent } from "../lib/lanes/mesh-events.js";
9
9
  import type { BalanceHostGap, BalanceHostSnapshot } from "../lib/lanes/claude-balance.js";
10
+ import { type ResolvedEffort } from "../lib/lanes/effort.js";
11
+ import type { SessionCopyResult } from "../lib/lanes/move-account.js";
12
+ import type { WorkerProfile } from "../lib/lanes/profile.js";
13
+ import type { OutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
14
+ import { LanesError } from "../lib/lanes/errors.js";
15
+ export type { LaneCompanyRef, LaneRecord } from "../lib/lanes/index.js";
16
+ export interface LaneAccountMove {
17
+ lane: LaneRecord;
18
+ from: string | null;
19
+ to: string;
20
+ copy: SessionCopyResult;
21
+ queued: boolean;
22
+ }
23
+ export type ResolvedResumeProfile = ResolvedLaneWorker & {
24
+ profile: NonNullable<ResolvedLaneWorker["profile"]>;
25
+ };
26
+ export interface ResumeProfileRepinAuthorization {
27
+ kind: "senior" | "ssm-owner-dispatch";
28
+ expectedSenior: SeniorRef;
29
+ actor: string;
30
+ reason: "resume --worker";
31
+ newHash: string;
32
+ }
33
+ export interface LaneMessageOptions {
34
+ text?: string;
35
+ project?: string;
36
+ story?: string;
37
+ compact?: boolean;
38
+ account?: string;
39
+ model?: string;
40
+ effort?: string;
41
+ worker?: string;
42
+ grantContext?: string;
43
+ monitorEvent?: {
44
+ monitor_id: string;
45
+ description: string;
46
+ sequence: number;
47
+ };
48
+ /** Internal recovery messages do not emit a second command response. */
49
+ silent?: boolean;
50
+ /** Internal recovery claim is safe to archive once this callback runs. */
51
+ onAccepted?: () => void;
52
+ }
53
+ export interface LaneResumeOptions {
54
+ text?: string;
55
+ fromFailed?: boolean;
56
+ reason?: string;
57
+ failedResumeContext?: string;
58
+ project?: string;
59
+ story?: string;
60
+ compact?: boolean;
61
+ account?: string;
62
+ model?: string;
63
+ effort?: string;
64
+ worker?: string;
65
+ keepAlive?: boolean;
66
+ meshCompany?: string;
67
+ profileRepinContext?: string;
68
+ grantContext?: string;
69
+ }
70
+ export interface LaneBalanceMoveTargetOptions {
71
+ account?: string;
72
+ fromAccount?: string;
73
+ expectedState?: string;
74
+ }
75
+ export interface AdmissionResult {
76
+ action: "start" | "queue" | "refuse";
77
+ reason?: QueuedReason;
78
+ error?: LanesError;
79
+ }
80
+ export interface PrepareAccountMoveInput {
81
+ account: string;
82
+ cliModel: string | undefined;
83
+ preservedModel?: string;
84
+ accountHistoryReason?: AccountHistoryEntry["reason"];
85
+ workerProfile?: ResolvedResumeProfile;
86
+ profileRepin?: ResumeProfileRepinAuthorization;
87
+ effort?: ResolvedEffort;
88
+ grantContext?: OutpostHireGrantContext;
89
+ pending?: Omit<PendingResume, "session_id" | "at"> & {
90
+ trigger: PendingResume["trigger"];
91
+ };
92
+ /** Copy the session and change account while preserving the between-turn state. */
93
+ deferTurnStart?: boolean;
94
+ accountSelection?: LaneAccountSelection;
95
+ }
96
+ export interface RecordResumeSetupInput {
97
+ profile?: ResolvedResumeProfile;
98
+ effort?: ResolvedEffort;
99
+ keepAlive?: boolean;
100
+ cliModel?: string;
101
+ meshCompany?: LaneCompanyRef;
102
+ threadId?: string;
103
+ profileRepin?: ResumeProfileRepinAuthorization;
104
+ }
105
+ export declare function accountRequestKeepsCurrentLane(lane: LaneRecord, rawAccount: string): boolean;
106
+ export declare function assertCurrentResumeAccountAvailable(ctx: Ctx, lane: LaneRecord): void;
107
+ export declare function accountMoveSummary(move: LaneAccountMove): Record<string, unknown>;
108
+ export declare function hasKeepAliveProgress(lane: LaneRecord): boolean;
109
+ export declare function resetKeepAliveProgress(lane: LaneRecord): LaneRecord;
110
+ export declare function resolveResumeEffort(ctx: Ctx, lane: LaneRecord, input: {
111
+ cliEffort?: string;
112
+ profile?: ResolvedResumeProfile;
113
+ account?: string;
114
+ model?: string;
115
+ }): ResolvedEffort | undefined;
116
+ export declare function recordedResumeEffort(lane: LaneRecord): ResolvedEffort;
117
+ export interface LaneMessageHandlerDeps {
118
+ archiveResumeMessage(hqRoot: string, laneId: string, message: {
119
+ message_id: string;
120
+ [key: string]: unknown;
121
+ }): void;
122
+ assertLaneRunsHereForAdmission(ctx: Ctx, lane: LaneRecord): void;
123
+ assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
124
+ assessLaneAdmission(ctx: Ctx, lane: LaneRecord, options: {
125
+ account?: string;
126
+ queueAt: string;
127
+ alreadyQueued?: boolean;
128
+ }): Promise<AdmissionResult>;
129
+ casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
130
+ dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions): Promise<void>;
131
+ drainHookStatusFor(ctx: Ctx, lane: LaneRecord): {
132
+ registered: boolean;
133
+ kind: string;
134
+ path: string;
135
+ } | null;
136
+ laneIsHostedHere(ctx: Ctx, lane: LaneRecord): boolean;
137
+ laneWithRecoveredProviderSession(hqRoot: string, lane: LaneRecord): LaneRecord;
138
+ loadAllLanes(hqRoot: string): LaneRecord[];
139
+ mintMessageId(): string;
140
+ prepareAccountMove(ctx: Ctx, lane: LaneRecord, input: PrepareAccountMoveInput): Promise<LaneAccountMove>;
141
+ persistClaudeAccountSelection(hqRoot: string, lane: LaneRecord, selection: LaneAccountSelection): LaneRecord;
142
+ printJson(value: unknown): void;
143
+ recordResumeSetup(ctx: Ctx, lane: LaneRecord, input: RecordResumeSetupInput): LaneRecord;
144
+ resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
145
+ resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
146
+ selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
147
+ repairUnavailableAccount?: boolean;
148
+ }): Promise<{
149
+ selection: LaneAccountSelection;
150
+ historyReason?: AccountHistoryEntry["reason"];
151
+ preservedModel?: string;
152
+ } | undefined>;
153
+ validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
154
+ withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
155
+ }
156
+ export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
157
+ dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions): Promise<void>;
158
+ authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
159
+ authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
160
+ automaticBalanceMoveHistoryReason(ctx: Ctx, lane: LaneRecord): AccountHistoryEntry["reason"];
161
+ }
10
162
  export interface SpawnLoginResult {
11
163
  status: number;
12
164
  }