@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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.248.0] — 2026-09-27
6
+
7
+ ### Changed
8
+
9
+ - Internal: moved the message and resume subcommands out of src/commands/lanes.ts; no behaviour change
10
+
11
+ ## [5.247.1] — 2026-09-27
12
+
13
+ ### Fixed
14
+
15
+ - `hq core hq-session set company_slug` accepts case-sensitive ASCII company
16
+ IDs that match company directory names and writes the same value to the
17
+ session scope capability.
18
+ - `hq bot create setup` stops before creating a bot when the reserved setup slug exists but its record cannot be verified.
19
+
5
20
  ## [5.247.0] — 2026-09-27
6
21
  ### Changed
7
22
 
@@ -99,8 +99,8 @@ cmd_set() {
99
99
  # session_scope_mint's own character-set rule; `personal` is a valid slug.
100
100
  if [ "$key" = "company_slug" ] && [ -n "$value" ]; then
101
101
  case "$value" in
102
- *[!a-z0-9_-]*)
103
- echo "hq-session: invalid company_slug: '$value' (allowed characters: a-z 0-9 - _)" >&2
102
+ *[!a-zA-Z0-9_-]*)
103
+ echo "hq-session: invalid company_slug: '$value' (allowed characters: A-Z a-z 0-9 - _)" >&2
104
104
  exit 1
105
105
  ;;
106
106
  esac
@@ -29,7 +29,7 @@ session_scope_mint() {
29
29
  [ -n "$root" ] && [ -n "$sid" ] && [ -n "$slug" ] || return 1
30
30
 
31
31
  case "$slug" in
32
- ''|*[!a-z0-9_-]*)
32
+ ''|*[!a-zA-Z0-9_-]*)
33
33
  echo "session-scope-capability: invalid company_slug: $slug" >&2
34
34
  return 1
35
35
  ;;
@@ -521,11 +521,17 @@ export async function runBotCreate(nameArg, opts) {
521
521
  if (matches.length > 1) {
522
522
  throw Object.assign(new Error(`You own more than one bot called "${name}": ${matches.map((bot) => bot.agentUid).join(", ")}. Name the one you mean by its id.`), { expected: true });
523
523
  }
524
- if (matches.length === 0)
525
- return false;
526
- const remote = matches[0];
527
- if (!isReservedSetupBotRecord(remote, owner))
524
+ const setupSlug = `${RESERVED_SETUP_BOT_NAME}-${owner.slice(-8).toLowerCase()}`;
525
+ const slugMatches = mine.filter((bot) => (bot.slug ?? "").trim().toLowerCase() === setupSlug);
526
+ if (slugMatches.length > 1) {
527
+ throw Object.assign(new Error(`HQ returned more than one agent with the setup slug "${setupSlug}". The CLI cannot safely choose one; ask your HQ admin to inspect the duplicate records.`), { expected: true });
528
+ }
529
+ if (slugMatches.length === 0)
528
530
  return false;
531
+ const remote = slugMatches[0];
532
+ if (!isReservedSetupBotRecord(remote, owner)) {
533
+ throw Object.assign(new Error(`HQ lists an agent with the setup slug "${setupSlug}", but its record does not identify it as your setup bot. The CLI cannot safely adopt it; check it with your HQ admin before retrying.`), { expected: true });
534
+ }
529
535
  const { result, config } = await adoptRemoteBot({
530
536
  bot: remote,
531
537
  name,
@@ -0,0 +1,9 @@
1
+ import { type Ctx, type LaneMessageHandlerDeps, type LaneMessageOptions } from "./lanes-shared.js";
2
+ export declare function createLaneMessageHandlers(deps: LaneMessageHandlerDeps): {
3
+ runMessage: (ctx: Ctx, laneId: string, opts: LaneMessageOptions) => Promise<void>;
4
+ recoverQueuedMessages: (ctx: Ctx, laneId: string) => Promise<{
5
+ action: "none" | "resumed" | "undeliverable";
6
+ message_count: number;
7
+ }>;
8
+ };
9
+ //# sourceMappingURL=lanes-message.d.ts.map
@@ -0,0 +1,558 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { appendCappedHistory, appendDropboxMessage, deliveryRefusalReason, dropboxInboxPath, laneAcceptsOperatorMessage, laneDeliveryMechanism, operatorMessageRefusalReason, providerSessionPortable, providerSupportsSessionResume, readLane, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
4
+ import { LanesError } from "../lib/lanes/errors.js";
5
+ import { parseCliEffort } from "../lib/lanes/effort.js";
6
+ import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
7
+ import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
8
+ import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages } from "../lib/lanes/dropbox.js";
9
+ import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision } from "../lib/lanes/watcher.js";
10
+ import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
11
+ import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
12
+ import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
13
+ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
14
+ import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
15
+ export function createLaneMessageHandlers(deps) {
16
+ const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, casUpdateLane, dispatchOutpostLaneCommand, drainHookStatusFor, laneIsHostedHere, laneWithRecoveredProviderSession, loadAllLanes, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveActiveQuestionCompany, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
17
+ async function messageLaneExclusion(ctx, lane) {
18
+ if (lane.state === "killed" || lane.state === "cancelled") {
19
+ return { lane, exclusion: lane.state };
20
+ }
21
+ const lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
22
+ company: lane.company,
23
+ env: ctx.env,
24
+ });
25
+ const currentLane = readLane(ctx.hqRoot(), lane.lane_id);
26
+ if (currentLane.state === "killed" || currentLane.state === "cancelled") {
27
+ return { lane: currentLane, exclusion: currentLane.state };
28
+ }
29
+ const lanes = loadAllLanes(ctx.hqRoot());
30
+ const retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
31
+ return {
32
+ lane: currentLane,
33
+ exclusion: keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware),
34
+ };
35
+ }
36
+ function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
37
+ writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
38
+ event: "message_undeliverable",
39
+ at: ctx.now().toISOString(),
40
+ state: lane.state,
41
+ reason,
42
+ ...(messageCount === undefined ? {} : { message_count: messageCount }),
43
+ }, WATCHER_OUT_MAX_BYTES);
44
+ }
45
+ async function runMessage(ctx, laneId, opts) {
46
+ const hqRoot = ctx.hqRoot();
47
+ const hireGrantContext = opts.grantContext === undefined
48
+ ? undefined
49
+ : (() => {
50
+ if (!ctx.isSsmOwnerDispatch()) {
51
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
52
+ }
53
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
54
+ })();
55
+ let accepted = false;
56
+ let claimedMessages = [];
57
+ const markAccepted = () => {
58
+ accepted = true;
59
+ opts.onAccepted?.();
60
+ };
61
+ let text = opts.text;
62
+ if (!text) {
63
+ throw new LanesError("missing_text", "--text is required", {});
64
+ }
65
+ let lane = readLane(hqRoot, laneId);
66
+ if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
67
+ await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
68
+ return;
69
+ }
70
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
71
+ if (lane.state === "running") {
72
+ const workerPidPath = path.join(laneRunDir(hqRoot, laneId), "worker.pid");
73
+ const workerRecordExists = fs.existsSync(workerPidPath);
74
+ const worker = readPidRecord(workerPidPath);
75
+ if (workerRecordExists && worker === null) {
76
+ throw new LanesError("worker_pid_invalid", `Lane ${laneId} has a malformed worker PID record; message delivery cannot be classified safely`, { lane_id: laneId, worker_pid_path: workerPidPath });
77
+ }
78
+ // A fresh spawn may have committed running before publishing worker.pid;
79
+ // absence is not proof of exit. Reclassify only a recorded process that
80
+ // fails the PID plus start-time identity probe.
81
+ const processAlive = worker === null
82
+ ? true
83
+ : probePidRecord(worker, ctx.isAlive ?? isPidAlive, ctx.readStarttime ?? readProcStarttime).alive;
84
+ if (worker !== null && !processAlive) {
85
+ // The watcher may not have reached its next poll yet. Run the same
86
+ // classifier synchronously, then make the queue/resume choice again.
87
+ await runWatcherLoop(hqRoot, laneId, {
88
+ ...ctx.watcherLoopDeps,
89
+ now: () => ctx.now().getTime(),
90
+ isAlive: ctx.isAlive ?? isPidAlive,
91
+ readStarttime: ctx.readStarttime ?? readProcStarttime,
92
+ mesh: null,
93
+ resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
94
+ });
95
+ lane = readLane(hqRoot, laneId);
96
+ // This invocation won the race to classify the dead worker, so the
97
+ // watcher callback did not own recovery. Fold any earlier queued
98
+ // messages into this resume to keep one provider turn and one delivery.
99
+ if (laneDeliveryMechanism(lane) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
100
+ const queuedMessages = drainDropbox(hqRoot, laneId);
101
+ if (queuedMessages.length > 0) {
102
+ claimedMessages = queuedMessages;
103
+ text = `${formatDropboxMessages(queuedMessages)}\n\n${text}`;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ try {
109
+ const eligibility = await messageLaneExclusion(ctx, lane);
110
+ lane = eligibility.lane;
111
+ const exclusion = eligibility.exclusion;
112
+ if (exclusion !== undefined) {
113
+ let messageCount;
114
+ if (dropboxInboxHasMessages(hqRoot, laneId)) {
115
+ messageCount = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" }).length;
116
+ }
117
+ recordMessageUndeliverable(ctx, lane, exclusion, messageCount);
118
+ const refusalMessage = deliveryRefusalReason(lane);
119
+ throw new LanesError("lane_not_accepting_messages", refusalMessage || `Lane ${laneId} cannot receive messages: ${exclusion}`, { lane_id: laneId, state: lane.state, reason: exclusion });
120
+ }
121
+ if (lane.state === "done" && dropboxInboxHasMessages(hqRoot, laneId)) {
122
+ const staleMessages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
123
+ if (staleMessages.length > 0) {
124
+ recordMessageUndeliverable(ctx, lane, "done_before_queued_message", staleMessages.length);
125
+ }
126
+ }
127
+ const cliEffort = parseCliEffort(lane.provider, opts.effort);
128
+ if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
129
+ throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
130
+ }
131
+ // Check before transcript recovery, account probing, or message writes.
132
+ // The final state transition repeats this check under the senior lock.
133
+ await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
134
+ lane = laneWithRecoveredProviderSession(hqRoot, lane);
135
+ const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
136
+ if (keepCurrentAccount)
137
+ assertCurrentResumeAccountAvailable(ctx, lane);
138
+ // A real account move is refused on its own terms first: a running lane and
139
+ // a provider without session resume get the move's named errors, not the
140
+ // generic delivery refusal below. Naming the current account is a keep.
141
+ if (opts.account !== undefined && !keepCurrentAccount && 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 && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
148
+ throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
149
+ }
150
+ // One decision point for how a follow-up reaches this lane (delivery.ts).
151
+ // `none` refuses before anything is written: no inbox append, no spawn.
152
+ const mechanism = laneDeliveryMechanism(lane);
153
+ if (mechanism === "none") {
154
+ const message = deliveryRefusalReason(lane);
155
+ throw new LanesError("lane_not_accepting_messages", message, {
156
+ lane_id: laneId,
157
+ state: lane.state,
158
+ provider: lane.provider,
159
+ message,
160
+ });
161
+ }
162
+ if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
163
+ throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
164
+ }
165
+ if (opts.worker !== undefined && mechanism === "dropbox") {
166
+ throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
167
+ }
168
+ if (cliEffort !== undefined && mechanism === "dropbox") {
169
+ throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
170
+ }
171
+ const resumeProfile = opts.worker !== undefined
172
+ ? resolveResumeProfile(ctx, lane, opts.worker)
173
+ : undefined;
174
+ if (opts.model !== undefined && opts.account === undefined) {
175
+ throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
176
+ }
177
+ const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
178
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
179
+ : undefined;
180
+ const resumeEffort = resolveResumeEffort(ctx, lane, {
181
+ cliEffort,
182
+ profile: resumeProfile,
183
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
184
+ model: opts.model,
185
+ });
186
+ const messageId = mintMessageId();
187
+ const at = ctx.now().toISOString();
188
+ const projectId = opts.project?.trim() || lane.project_id;
189
+ const storyId = opts.story?.trim() || lane.story_id;
190
+ if (opts.story !== undefined && !opts.story.trim()) {
191
+ throw new LanesError("missing_story", "--story must be non-empty", {});
192
+ }
193
+ if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
194
+ validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
195
+ }
196
+ const framedMonitorEvent = opts.monitorEvent === undefined
197
+ ? undefined
198
+ : formatMonitorEvent({
199
+ monitorId: opts.monitorEvent.monitor_id,
200
+ description: opts.monitorEvent.description,
201
+ sequence: opts.monitorEvent.sequence,
202
+ text,
203
+ });
204
+ const deliveryText = framedMonitorEvent ?? text;
205
+ const dropboxMessage = {
206
+ at,
207
+ message_id: messageId,
208
+ text,
209
+ project_id: projectId,
210
+ story_id: storyId,
211
+ ...(opts.monitorEvent === undefined ? {} : {
212
+ kind: "monitor_event",
213
+ monitor_id: opts.monitorEvent.monitor_id,
214
+ description: opts.monitorEvent.description,
215
+ sequence: opts.monitorEvent.sequence,
216
+ }),
217
+ };
218
+ let resumed = null;
219
+ let move = null;
220
+ let queuedOnAdmission = false;
221
+ if (opts.account !== undefined && !keepCurrentAccount) {
222
+ // A moved message is always a resume on the target account: a running
223
+ // lane (the dropbox mechanism) is refused inside prepareAccountMove.
224
+ move = await prepareAccountMove(ctx, lane, {
225
+ account: opts.account,
226
+ cliModel: opts.model?.trim() || undefined,
227
+ ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
228
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
229
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
230
+ pending: {
231
+ trigger: "message",
232
+ text: deliveryText,
233
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
234
+ message_id: messageId,
235
+ compact: opts.compact !== false,
236
+ },
237
+ });
238
+ lane = move.lane;
239
+ if (move.queued) {
240
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
241
+ }
242
+ }
243
+ else if (mechanism === "resume") {
244
+ const automatic = automaticAccountSelection;
245
+ if (automatic !== undefined) {
246
+ const selection = automatic.selection;
247
+ if (selection.account !== lane.account) {
248
+ move = await prepareAccountMove(ctx, lane, {
249
+ account: selection.account,
250
+ cliModel: undefined,
251
+ ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
252
+ ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
253
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
254
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
255
+ pending: {
256
+ trigger: "message",
257
+ text: deliveryText,
258
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
259
+ message_id: messageId,
260
+ compact: opts.compact !== false,
261
+ },
262
+ accountSelection: selection,
263
+ });
264
+ lane = move.lane;
265
+ if (move.queued)
266
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
267
+ }
268
+ else {
269
+ lane = persistClaudeAccountSelection(hqRoot, lane, selection);
270
+ }
271
+ }
272
+ }
273
+ if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
274
+ validateResumeProfileModel(ctx, lane, resumeProfile.profile);
275
+ }
276
+ if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
277
+ lane = recordResumeSetup(ctx, lane, {
278
+ ...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
279
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
280
+ ...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
281
+ });
282
+ }
283
+ if (move?.queued) {
284
+ // Parked at the target's cap: reconcile resumes the session there.
285
+ markAccepted();
286
+ }
287
+ else {
288
+ const dispatched = await dispatchLaneDelivery(lane, {
289
+ dropbox: () => {
290
+ // Live (or about-to-be-live) process: the PreToolUse drain delivers it.
291
+ if (!laneAcceptsOperatorMessage(lane)) {
292
+ throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
293
+ }
294
+ appendDropboxMessage(hqRoot, laneId, dropboxMessage);
295
+ return null;
296
+ },
297
+ resume: async () => {
298
+ // Exited worker with a recorded session: queue the message when capacity
299
+ // is occupied, otherwise start its next turn under the shared admission lock.
300
+ assertLaneRunsHereForAdmission(ctx, lane);
301
+ return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
302
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
303
+ if (admission.action === "refuse" && admission.error)
304
+ throw admission.error;
305
+ return withResumeHireAdmission(ctx, lane, () => {
306
+ if (admission.action === "queue") {
307
+ const sessionId = lane.provider_session_id;
308
+ if (!sessionId) {
309
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
310
+ lane_id: laneId,
311
+ provider: lane.provider,
312
+ });
313
+ }
314
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
315
+ queuedOnAdmission = true;
316
+ return casUpdateLane(hqRoot, laneId, (current) => {
317
+ if (current.version !== lane.version || current.state !== lane.state) {
318
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
319
+ lane_id: laneId,
320
+ prepared_version: lane.version,
321
+ version: current.version,
322
+ state: current.state,
323
+ });
324
+ }
325
+ return {
326
+ ...current,
327
+ state: "queued",
328
+ queued_reason: admission.reason ?? "throttle",
329
+ queued_at: at,
330
+ pending_resume: {
331
+ session_id: sessionId,
332
+ trigger: "message",
333
+ text: deliveryText,
334
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
335
+ message_id: messageId,
336
+ compact: opts.compact !== false,
337
+ at,
338
+ },
339
+ };
340
+ });
341
+ }
342
+ const started = spawnLaneWorker(hqRoot, lane, {
343
+ env: ctx.env,
344
+ modelDeps: {
345
+ readFileSync: ctx.readFileSync,
346
+ probe: ctx.probeProvider,
347
+ env: ctx.env,
348
+ },
349
+ existsSync: ctx.existsSync,
350
+ spawnProcess: ctx.spawnProcess,
351
+ proveDetached: ctx.proveDetached,
352
+ killProcess: ctx.killProcess,
353
+ now: ctx.now,
354
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
355
+ runCompaction: ctx.runCompaction,
356
+ locateTranscript: ctx.locateTranscript,
357
+ }, {
358
+ trigger: {
359
+ kind: "message",
360
+ text: deliveryText,
361
+ message_id: messageId,
362
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
363
+ },
364
+ compact: opts.compact !== false,
365
+ effort: resumeEffort ?? recordedResumeEffort(lane),
366
+ });
367
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
368
+ return started;
369
+ }, hireGrantContext);
370
+ }, ctx.sleep);
371
+ },
372
+ }, mechanism);
373
+ if (dispatched.mechanism === "resume")
374
+ resumed = dispatched.value;
375
+ markAccepted();
376
+ }
377
+ // Every accepted message moves the compaction reference forward.
378
+ let next = casUpdateLane(hqRoot, laneId, (current) => ({
379
+ ...resetKeepAliveProgress(current),
380
+ last_message_at: at,
381
+ }));
382
+ if (projectId !== lane.project_id || storyId !== lane.story_id) {
383
+ next = casUpdateLane(hqRoot, laneId, (current) => {
384
+ const already = current.attribution_history.some((entry) => entry.message_id === messageId);
385
+ if (already)
386
+ return current;
387
+ return {
388
+ ...current,
389
+ project_id: projectId,
390
+ story_id: storyId,
391
+ no_story_reason: storyId ? undefined : current.no_story_reason,
392
+ stories: laneStory(current, projectId, storyId)
393
+ ? laneStories(current)
394
+ : [
395
+ ...laneStories(current),
396
+ {
397
+ project_id: projectId,
398
+ story_id: storyId,
399
+ status: "in_progress",
400
+ added_at: at,
401
+ updated_at: at,
402
+ },
403
+ ],
404
+ attribution_history: appendCappedHistory(current.attribution_history, {
405
+ project_id: projectId,
406
+ story_id: storyId,
407
+ message_id: messageId,
408
+ at,
409
+ }),
410
+ };
411
+ });
412
+ }
413
+ // What this command can claim when it returns: the message is durably
414
+ // written (accepted). Delivery to the model is asynchronous in both
415
+ // mechanisms. `dropbox` delivers on the worker's next tool call, and only
416
+ // if the drain hook is registered in the account home the worker was
417
+ // started from; `resume` has started a new worker with the message as its
418
+ // prompt. Neither is "delivered", so the response never says so.
419
+ const drainHook = drainHookStatusFor(ctx, next);
420
+ const deliveryStatus = move?.queued
421
+ ? "queued_on_target_account"
422
+ : queuedOnAdmission
423
+ ? "queued_on_admission"
424
+ : mechanism === "dropbox"
425
+ ? "awaiting_next_tool_call"
426
+ : "worker_spawned";
427
+ const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
428
+ ? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
429
+ : mechanism === "dropbox" && next.stalled === true
430
+ ? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
431
+ : null;
432
+ if (!opts.silent)
433
+ printJson({
434
+ ok: true,
435
+ action: "message",
436
+ lane_id: laneId,
437
+ message_id: messageId,
438
+ accepted: true,
439
+ delivered: false,
440
+ delivery: mechanism,
441
+ delivery_status: deliveryStatus,
442
+ inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
443
+ drain_hook: drainHook,
444
+ warning,
445
+ resumed_session_id: resumed?.last_resume?.session_id ?? null,
446
+ compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
447
+ ? resumed.last_compaction
448
+ : null,
449
+ account_move: move ? accountMoveSummary(move) : null,
450
+ project_id: next.project_id,
451
+ story_id: next.story_id,
452
+ stories: laneStories(next),
453
+ story_warning: storyWarning(next) ?? null,
454
+ lane: next,
455
+ });
456
+ }
457
+ catch (err) {
458
+ if (claimedMessages.length > 0 && !accepted) {
459
+ const error = err instanceof Error ? err.message : String(err);
460
+ writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
461
+ event: "message_recovery_failed",
462
+ at: ctx.now().toISOString(),
463
+ state: lane.state,
464
+ error,
465
+ message_count: claimedMessages.length,
466
+ }, WATCHER_OUT_MAX_BYTES);
467
+ try {
468
+ requeueDropboxMessages(hqRoot, laneId, claimedMessages);
469
+ }
470
+ catch (requeueErr) {
471
+ const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
472
+ writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
473
+ event: "message_requeue_failed",
474
+ at: ctx.now().toISOString(),
475
+ state: lane.state,
476
+ recovery_error: error,
477
+ requeue_error: requeueMessage,
478
+ message_count: claimedMessages.length,
479
+ }, WATCHER_OUT_MAX_BYTES);
480
+ throw new LanesError("message_requeue_failed", `Could not return ${claimedMessages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: claimedMessages.length, recovery_error: error });
481
+ }
482
+ }
483
+ throw err;
484
+ }
485
+ }
486
+ /**
487
+ * Watcher and reconcile call the hidden command below after they win the
488
+ * terminal transition. The inbox is claimed once with the normal atomic
489
+ * dropbox drain, and an accepted message is routed through runMessage so the
490
+ * existing provider, admission, account, worker-profile and senior checks
491
+ * remain authoritative.
492
+ */
493
+ async function recoverQueuedMessages(ctx, laneId) {
494
+ const hqRoot = ctx.hqRoot();
495
+ let lane = readLane(hqRoot, laneId);
496
+ if (!dropboxInboxHasMessages(hqRoot, laneId))
497
+ return { action: "none", message_count: 0 };
498
+ const eligibility = await messageLaneExclusion(ctx, lane);
499
+ lane = eligibility.lane;
500
+ const exclusion = eligibility.exclusion;
501
+ const refusal = exclusion ?? (lane.state === "done"
502
+ ? "done_before_envelope"
503
+ : lane.state === "awaiting_input"
504
+ ? "awaiting_input_requires_question_answer"
505
+ : laneDeliveryMechanism(lane) !== "resume"
506
+ ? `no_resume_path_for_${lane.state}`
507
+ : undefined);
508
+ if (refusal !== undefined) {
509
+ const messages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
510
+ if (messages.length > 0)
511
+ recordMessageUndeliverable(ctx, lane, refusal, messages.length);
512
+ return { action: messages.length > 0 ? "undeliverable" : "none", message_count: messages.length };
513
+ }
514
+ const messages = drainDropbox(hqRoot, laneId);
515
+ if (messages.length === 0)
516
+ return { action: "none", message_count: 0 };
517
+ let accepted = false;
518
+ try {
519
+ await runMessage(ctx, laneId, {
520
+ text: formatDropboxMessages(messages),
521
+ compact: false,
522
+ silent: true,
523
+ onAccepted: () => { accepted = true; },
524
+ });
525
+ return { action: "resumed", message_count: messages.length };
526
+ }
527
+ catch (err) {
528
+ const error = err instanceof Error ? err.message : String(err);
529
+ writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
530
+ event: "message_recovery_failed",
531
+ at: ctx.now().toISOString(),
532
+ state: lane.state,
533
+ error,
534
+ message_count: messages.length,
535
+ }, WATCHER_OUT_MAX_BYTES);
536
+ if (!accepted) {
537
+ try {
538
+ requeueDropboxMessages(hqRoot, laneId, messages);
539
+ }
540
+ catch (requeueErr) {
541
+ const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
542
+ writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
543
+ event: "message_requeue_failed",
544
+ at: ctx.now().toISOString(),
545
+ state: lane.state,
546
+ recovery_error: error,
547
+ requeue_error: requeueMessage,
548
+ message_count: messages.length,
549
+ }, WATCHER_OUT_MAX_BYTES);
550
+ throw new LanesError("message_requeue_failed", `Could not return ${messages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: messages.length, recovery_error: error });
551
+ }
552
+ }
553
+ throw err;
554
+ }
555
+ }
556
+ return { runMessage, recoverQueuedMessages };
557
+ }
558
+ //# sourceMappingURL=lanes-message.js.map
@@ -0,0 +1,6 @@
1
+ import { type Ctx, type LaneBalanceMoveTargetOptions, type LaneResumeHandlerDeps, type LaneResumeOptions } from "./lanes-shared.js";
2
+ export declare function createLaneResumeHandlers(deps: LaneResumeHandlerDeps): {
3
+ runClaudeBalanceMoveTarget: (ctx: Ctx, laneId: string, opts: LaneBalanceMoveTargetOptions) => Promise<void>;
4
+ runResume: (ctx: Ctx, laneId: string, opts: LaneResumeOptions) => Promise<void>;
5
+ };
6
+ //# sourceMappingURL=lanes-resume.d.ts.map