@indigoai-us/hq-cli 5.298.0 → 5.300.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/command-catalog.generated.d.ts +27 -4
  3. package/dist/command-catalog.generated.js +34 -4
  4. package/dist/commands/cloud.js +12 -7
  5. package/dist/commands/daemon.js +1 -0
  6. package/dist/commands/files.d.ts +1 -0
  7. package/dist/commands/files.js +59 -0
  8. package/dist/commands/lanes-accounts.d.ts +3 -0
  9. package/dist/commands/lanes-accounts.js +25 -1
  10. package/dist/commands/lanes-message.js +68 -35
  11. package/dist/commands/lanes-resume.js +247 -210
  12. package/dist/commands/lanes-shared.d.ts +3 -1
  13. package/dist/commands/lanes.d.ts +2 -0
  14. package/dist/commands/lanes.js +16 -4
  15. package/dist/lib/daemon/legacy.d.ts +5 -2
  16. package/dist/lib/daemon/legacy.js +6 -4
  17. package/dist/lib/daemon/migrate.d.ts +10 -1
  18. package/dist/lib/daemon/migrate.js +22 -7
  19. package/dist/lib/daemon/run.js +19 -7
  20. package/dist/lib/daemon/supervisor.js +9 -1
  21. package/dist/lib/lanes/quota.d.ts +19 -0
  22. package/dist/lib/lanes/quota.js +106 -29
  23. package/dist/lib/lanes/reconcile.js +34 -5
  24. package/dist/lib/lanes/store.d.ts +2 -0
  25. package/dist/lib/lanes/store.js +4 -0
  26. package/dist/lib/lanes/story-switch.d.ts +21 -0
  27. package/dist/lib/lanes/story-switch.js +75 -0
  28. package/dist/lib/lanes/types.d.ts +25 -0
  29. package/dist/lib/lanes/types.js +1 -1
  30. package/dist/lib/lanes/watcher.js +25 -5
  31. package/dist/lib/mesh/live/daemon/install.d.ts +3 -0
  32. package/dist/lib/mesh/live/daemon/install.js +13 -2
  33. package/dist/utils/self-update.js +249 -2
  34. package/dist/utils/staged-install.js +71 -21
  35. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { parseCliEffort } from "../lib/lanes/effort.js";
5
5
  import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
6
6
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
7
7
  import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
8
+ import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
8
9
  import { spawnLaneWorker } from "../lib/lanes/spawn.js";
9
10
  import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
10
11
  export function createLaneResumeHandlers(deps) {
@@ -195,6 +196,8 @@ export function createLaneResumeHandlers(deps) {
195
196
  }
196
197
  const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
197
198
  const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
199
+ const notice = storySwitchNotice(lane.project_id, lane.story_id, projectId, storyId);
200
+ const deliveryText = notice === undefined ? text : `${notice}${text === undefined ? "" : `\n\n${text}`}`;
198
201
  let requestedMeshCompany;
199
202
  let meshThreadId;
200
203
  if (opts.meshCompany !== undefined) {
@@ -253,41 +256,60 @@ export function createLaneResumeHandlers(deps) {
253
256
  account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
254
257
  model: opts.model,
255
258
  });
256
- let move = null;
257
- if (opts.account !== undefined && !keepCurrentAccount) {
258
- move = await prepareAccountMove(ctx, lane, {
259
- account: opts.account,
260
- cliModel: opts.model?.trim() || undefined,
261
- ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
262
- ...(profileRepin !== undefined ? { profileRepin } : {}),
263
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
264
- ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
265
- pending: {
266
- trigger: "resume",
267
- resumed_from: resumedFrom,
268
- ...(opts.fromFailed === true ? { from_failed: true } : {}),
269
- ...(failedResumeExpectedSenior !== undefined
270
- ? { expected_senior: failedResumeExpectedSenior }
271
- : {}),
272
- ...(text !== undefined ? { text } : {}),
273
- ...(messageId !== undefined ? { message_id: messageId } : {}),
274
- compact: opts.compact !== false,
275
- },
259
+ let storySwitchRollback;
260
+ let storySwitchWorkerStarted = false;
261
+ if (messageId !== undefined && attributionChanged) {
262
+ lane = casUpdateLane(hqRoot, laneId, (current) => {
263
+ const already = current.attribution_history.some((entry) => entry.message_id === messageId);
264
+ if (already)
265
+ return current;
266
+ const existingStory = laneStory(current, projectId, storyId);
267
+ storySwitchRollback = {
268
+ message_id: messageId,
269
+ project_id: current.project_id,
270
+ story_id: current.story_id,
271
+ no_story_reason: current.no_story_reason,
272
+ ...(!existingStory ? {
273
+ added_story: { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
274
+ } : {}),
275
+ story_switch_unacknowledged: current.story_switch_unacknowledged,
276
+ };
277
+ return {
278
+ ...current,
279
+ project_id: projectId,
280
+ story_id: storyId,
281
+ no_story_reason: storyId ? undefined : current.no_story_reason,
282
+ stories: laneStory(current, projectId, storyId)
283
+ ? laneStories(current)
284
+ : [
285
+ ...laneStories(current),
286
+ { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
287
+ ],
288
+ attribution_history: appendCappedHistory(current.attribution_history, {
289
+ project_id: projectId,
290
+ story_id: storyId,
291
+ message_id: messageId,
292
+ at,
293
+ }),
294
+ story_switch_pending: {
295
+ previous_project_id: current.project_id,
296
+ previous_story_id: current.story_id,
297
+ project_id: projectId,
298
+ story_id: storyId,
299
+ message_id: messageId,
300
+ at,
301
+ previous_artifact_keys: artifactIdentityKeys(current.last_envelope?.artifacts),
302
+ },
303
+ story_switch_unacknowledged: undefined,
304
+ };
276
305
  });
277
- lane = move.lane;
278
306
  }
279
- else if (automaticAccountSelection !== undefined) {
280
- const selection = automaticAccountSelection.selection;
281
- if (selection.account !== lane.account) {
307
+ try {
308
+ let move = null;
309
+ if (opts.account !== undefined && !keepCurrentAccount) {
282
310
  move = await prepareAccountMove(ctx, lane, {
283
- account: selection.account,
284
- cliModel: undefined,
285
- ...(automaticAccountSelection.historyReason !== undefined
286
- ? { accountHistoryReason: automaticAccountSelection.historyReason }
287
- : {}),
288
- ...(automaticAccountSelection.preservedModel !== undefined
289
- ? { preservedModel: automaticAccountSelection.preservedModel }
290
- : {}),
311
+ account: opts.account,
312
+ cliModel: opts.model?.trim() || undefined,
291
313
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
292
314
  ...(profileRepin !== undefined ? { profileRepin } : {}),
293
315
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
@@ -299,204 +321,219 @@ export function createLaneResumeHandlers(deps) {
299
321
  ...(failedResumeExpectedSenior !== undefined
300
322
  ? { expected_senior: failedResumeExpectedSenior }
301
323
  : {}),
302
- ...(text !== undefined ? { text } : {}),
324
+ ...(deliveryText !== undefined ? { text: deliveryText } : {}),
303
325
  ...(messageId !== undefined ? { message_id: messageId } : {}),
304
326
  compact: opts.compact !== false,
305
327
  },
306
- accountSelection: selection,
307
328
  });
308
329
  lane = move.lane;
309
330
  }
310
- else {
311
- lane = persistClaudeAccountSelection(hqRoot, lane, selection);
312
- }
313
- }
314
- lane = recordResumeSetup(ctx, lane, {
315
- ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
316
- ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
317
- ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
318
- ...(keepAlive !== undefined ? { keepAlive } : {}),
319
- ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
320
- ...(requestedMeshCompany !== undefined
321
- ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
322
- : {}),
323
- });
324
- let queuedOnAdmission = false;
325
- let resumed;
326
- if (move?.queued) {
327
- resumed = lane;
328
- }
329
- else {
330
- resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
331
- const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
332
- if (admission.action === "refuse" && admission.error)
333
- throw admission.error;
334
- return withResumeHireAdmission(ctx, lane, () => {
335
- if (admission.action === "queue") {
336
- const sessionId = lane.provider_session_id;
337
- if (!sessionId) {
338
- throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
339
- lane_id: laneId,
340
- provider: lane.provider,
341
- });
342
- }
343
- queuedOnAdmission = true;
344
- return casUpdateLane(hqRoot, laneId, (current) => {
345
- if (current.version !== lane.version || current.state !== lane.state) {
346
- throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
347
- lane_id: laneId,
348
- prepared_version: lane.version,
349
- version: current.version,
350
- state: current.state,
351
- });
352
- }
353
- return {
354
- ...current,
355
- state: "queued",
356
- queued_reason: admission.reason ?? "throttle",
357
- queued_details: undefined,
358
- queued_at: at,
359
- pending_resume: {
360
- session_id: sessionId,
361
- trigger: "resume",
362
- caller: ctx.env.HQ_LANE_ID?.trim() || "user",
363
- resumed_from: resumedFrom,
364
- ...(opts.fromFailed === true ? { from_failed: true } : {}),
365
- ...(failedResumeExpectedSenior !== undefined
366
- ? { expected_senior: failedResumeExpectedSenior }
367
- : {}),
368
- ...(text !== undefined ? { text } : {}),
369
- ...(messageId !== undefined ? { message_id: messageId } : {}),
370
- compact: opts.compact !== false,
371
- at,
372
- },
373
- };
374
- });
375
- }
376
- return spawnLaneWorker(hqRoot, lane, {
377
- env: ctx.env,
378
- modelDeps: {
379
- readFileSync: ctx.readFileSync,
380
- probe: ctx.probeProvider,
381
- env: ctx.env,
382
- },
383
- existsSync: ctx.existsSync,
384
- spawnProcess: ctx.spawnProcess,
385
- proveDetached: ctx.proveDetached,
386
- killProcess: ctx.killProcess,
387
- now: ctx.now,
388
- resolveMachineIdentity: ctx.resolveMachineIdentity,
389
- runCompaction: ctx.runCompaction,
390
- locateTranscript: ctx.locateTranscript,
391
- }, {
392
- trigger: {
393
- kind: "resume",
394
- from: resumedFrom,
395
- ...(opts.fromFailed === true ? { fromFailed: true } : {}),
331
+ else if (automaticAccountSelection !== undefined) {
332
+ const selection = automaticAccountSelection.selection;
333
+ if (selection.account !== lane.account) {
334
+ move = await prepareAccountMove(ctx, lane, {
335
+ account: selection.account,
336
+ cliModel: undefined,
337
+ ...(automaticAccountSelection.historyReason !== undefined
338
+ ? { accountHistoryReason: automaticAccountSelection.historyReason }
339
+ : {}),
340
+ ...(automaticAccountSelection.preservedModel !== undefined
341
+ ? { preservedModel: automaticAccountSelection.preservedModel }
342
+ : {}),
343
+ ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
344
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
345
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
346
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
347
+ pending: {
348
+ trigger: "resume",
349
+ resumed_from: resumedFrom,
350
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
396
351
  ...(failedResumeExpectedSenior !== undefined
397
- ? { expectedSenior: failedResumeExpectedSenior }
352
+ ? { expected_senior: failedResumeExpectedSenior }
398
353
  : {}),
399
- ...(text !== undefined ? { text } : {}),
354
+ ...(deliveryText !== undefined ? { text: deliveryText } : {}),
400
355
  ...(messageId !== undefined ? { message_id: messageId } : {}),
356
+ compact: opts.compact !== false,
401
357
  },
402
- compact: opts.compact !== false,
403
- effort: resumeEffort ?? recordedResumeEffort(lane),
358
+ accountSelection: selection,
404
359
  });
405
- }, hireGrantContext);
406
- }, ctx.sleep);
407
- }
408
- if (text !== undefined && messageId !== undefined) {
409
- // Same audit copy a message resume leaves; nothing drains it. Archive only
410
- // after a successful spawn so a concurrent resume cannot leave a phantom.
411
- archiveResumeMessage(hqRoot, laneId, {
412
- at,
413
- message_id: messageId,
414
- text,
415
- project_id: projectId,
416
- story_id: storyId,
417
- resumed_from: resumedFrom,
360
+ lane = move.lane;
361
+ }
362
+ else {
363
+ lane = persistClaudeAccountSelection(hqRoot, lane, selection);
364
+ }
365
+ }
366
+ lane = recordResumeSetup(ctx, lane, {
367
+ ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
368
+ ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
369
+ ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
370
+ ...(keepAlive !== undefined ? { keepAlive } : {}),
371
+ ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
372
+ ...(requestedMeshCompany !== undefined
373
+ ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
374
+ : {}),
418
375
  });
419
- }
420
- let next = resumed;
421
- if (failedResumeActor !== undefined &&
422
- failedResumeReason !== undefined &&
423
- recoveredFailure !== undefined) {
424
- next = casUpdateLane(hqRoot, laneId, (current) => ({
425
- ...current,
426
- failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
376
+ let queuedOnAdmission = false;
377
+ let resumed;
378
+ if (move?.queued) {
379
+ resumed = lane;
380
+ }
381
+ else {
382
+ resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
383
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
384
+ if (admission.action === "refuse" && admission.error)
385
+ throw admission.error;
386
+ return withResumeHireAdmission(ctx, lane, () => {
387
+ if (admission.action === "queue") {
388
+ const sessionId = lane.provider_session_id;
389
+ if (!sessionId) {
390
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
391
+ lane_id: laneId,
392
+ provider: lane.provider,
393
+ });
394
+ }
395
+ queuedOnAdmission = true;
396
+ return casUpdateLane(hqRoot, laneId, (current) => {
397
+ if (current.version !== lane.version || current.state !== lane.state) {
398
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
399
+ lane_id: laneId,
400
+ prepared_version: lane.version,
401
+ version: current.version,
402
+ state: current.state,
403
+ });
404
+ }
405
+ return {
406
+ ...current,
407
+ state: "queued",
408
+ queued_reason: admission.reason ?? "throttle",
409
+ queued_details: undefined,
410
+ queued_at: at,
411
+ pending_resume: {
412
+ session_id: sessionId,
413
+ trigger: "resume",
414
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
415
+ resumed_from: resumedFrom,
416
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
417
+ ...(failedResumeExpectedSenior !== undefined
418
+ ? { expected_senior: failedResumeExpectedSenior }
419
+ : {}),
420
+ ...(deliveryText !== undefined ? { text: deliveryText } : {}),
421
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
422
+ compact: opts.compact !== false,
423
+ at,
424
+ },
425
+ };
426
+ });
427
+ }
428
+ const started = spawnLaneWorker(hqRoot, lane, {
429
+ env: ctx.env,
430
+ modelDeps: {
431
+ readFileSync: ctx.readFileSync,
432
+ probe: ctx.probeProvider,
433
+ env: ctx.env,
434
+ },
435
+ existsSync: ctx.existsSync,
436
+ spawnProcess: ctx.spawnProcess,
437
+ proveDetached: ctx.proveDetached,
438
+ killProcess: ctx.killProcess,
439
+ now: ctx.now,
440
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
441
+ runCompaction: ctx.runCompaction,
442
+ locateTranscript: ctx.locateTranscript,
443
+ }, {
444
+ trigger: {
445
+ kind: "resume",
446
+ from: resumedFrom,
447
+ ...(opts.fromFailed === true ? { fromFailed: true } : {}),
448
+ ...(failedResumeExpectedSenior !== undefined
449
+ ? { expectedSenior: failedResumeExpectedSenior }
450
+ : {}),
451
+ ...(deliveryText !== undefined ? { text: deliveryText } : {}),
452
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
453
+ },
454
+ compact: opts.compact !== false,
455
+ effort: resumeEffort ?? recordedResumeEffort(lane),
456
+ });
457
+ storySwitchWorkerStarted = true;
458
+ deps.onLaneWorkerStarted?.(laneId, "resume");
459
+ return started;
460
+ }, hireGrantContext);
461
+ }, ctx.sleep);
462
+ }
463
+ if (deliveryText !== undefined && messageId !== undefined) {
464
+ // Same audit copy a message resume leaves; nothing drains it. Archive only
465
+ // after a successful spawn so a concurrent resume cannot leave a phantom.
466
+ archiveResumeMessage(hqRoot, laneId, {
427
467
  at,
428
- actor: failedResumeActor,
429
- reason: failedResumeReason,
430
- failure: recoveredFailure,
431
- }),
432
- }));
433
- }
434
- if (text !== undefined) {
435
- // An operator text moves the compaction reference forward, as a message does.
436
- next = casUpdateLane(hqRoot, laneId, (current) => ({
437
- ...resetKeepAliveProgress(current),
438
- last_message_at: at,
439
- }));
440
- }
441
- if (messageId !== undefined && attributionChanged) {
442
- next = casUpdateLane(hqRoot, laneId, (current) => {
443
- const already = current.attribution_history.some((entry) => entry.message_id === messageId);
444
- if (already)
445
- return current;
446
- return {
447
- ...current,
468
+ message_id: messageId,
469
+ text: deliveryText,
448
470
  project_id: projectId,
449
471
  story_id: storyId,
450
- no_story_reason: storyId ? undefined : current.no_story_reason,
451
- stories: laneStory(current, projectId, storyId)
452
- ? laneStories(current)
453
- : [
454
- ...laneStories(current),
455
- {
456
- project_id: projectId,
457
- story_id: storyId,
458
- status: "in_progress",
459
- added_at: at,
460
- updated_at: at,
461
- },
462
- ],
463
- attribution_history: appendCappedHistory(current.attribution_history, {
464
- project_id: projectId,
465
- story_id: storyId,
466
- message_id: messageId,
472
+ resumed_from: resumedFrom,
473
+ });
474
+ }
475
+ let next = resumed;
476
+ if (failedResumeActor !== undefined &&
477
+ failedResumeReason !== undefined &&
478
+ recoveredFailure !== undefined) {
479
+ next = casUpdateLane(hqRoot, laneId, (current) => ({
480
+ ...current,
481
+ failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
467
482
  at,
483
+ actor: failedResumeActor,
484
+ reason: failedResumeReason,
485
+ failure: recoveredFailure,
468
486
  }),
469
- };
487
+ }));
488
+ }
489
+ if (deliveryText !== undefined) {
490
+ // An operator text moves the compaction reference forward, as a message does.
491
+ next = casUpdateLane(hqRoot, laneId, (current) => ({
492
+ ...resetKeepAliveProgress(current),
493
+ last_message_at: at,
494
+ }));
495
+ }
496
+ if (text === undefined && hasKeepAliveProgress(next)) {
497
+ next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
498
+ }
499
+ printJson({
500
+ ok: true,
501
+ action: "resume",
502
+ lane_id: laneId,
503
+ resumed_from: resumedFrom,
504
+ message_id: messageId ?? null,
505
+ text: text ?? null,
506
+ delivery: "resume",
507
+ delivery_status: move?.queued
508
+ ? "queued_on_target_account"
509
+ : queuedOnAdmission
510
+ ? "queued_on_admission"
511
+ : "worker_spawned",
512
+ account_move: move ? accountMoveSummary(move) : null,
513
+ resumed_session_id: resumed.last_resume?.session_id ?? null,
514
+ last_resume: next.last_resume ?? null,
515
+ compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
516
+ ? resumed.last_compaction
517
+ : null,
518
+ project_id: next.project_id,
519
+ story_id: next.story_id,
520
+ stories: laneStories(next),
521
+ story_warning: storyWarning(next) ?? null,
522
+ lane: next,
470
523
  });
471
524
  }
472
- if (text === undefined && hasKeepAliveProgress(next)) {
473
- next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
525
+ catch (err) {
526
+ if (storySwitchRollback !== undefined && !storySwitchWorkerStarted) {
527
+ try {
528
+ casUpdateLane(hqRoot, laneId, (current) => rollbackStorySwitchAttribution(current, storySwitchRollback));
529
+ }
530
+ catch (rollbackErr) {
531
+ const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
532
+ process.stderr.write(`Could not roll back story attribution after resume failure: ${rollbackMessage}\n`);
533
+ }
534
+ }
535
+ throw err;
474
536
  }
475
- printJson({
476
- ok: true,
477
- action: "resume",
478
- lane_id: laneId,
479
- resumed_from: resumedFrom,
480
- message_id: messageId ?? null,
481
- text: text ?? null,
482
- delivery: "resume",
483
- delivery_status: move?.queued
484
- ? "queued_on_target_account"
485
- : queuedOnAdmission
486
- ? "queued_on_admission"
487
- : "worker_spawned",
488
- account_move: move ? accountMoveSummary(move) : null,
489
- resumed_session_id: resumed.last_resume?.session_id ?? null,
490
- last_resume: next.last_resume ?? null,
491
- compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
492
- ? resumed.last_compaction
493
- : null,
494
- project_id: next.project_id,
495
- story_id: next.story_id,
496
- stories: laneStories(next),
497
- story_warning: storyWarning(next) ?? null,
498
- lane: next,
499
- });
500
537
  }
501
538
  return { runClaudeBalanceMoveTarget, runResume };
502
539
  }
@@ -144,6 +144,8 @@ export interface OutpostLaneDispatchBehavior {
144
144
  expectedOwnerHost?: LaneRecord["host"];
145
145
  }
146
146
  export interface LaneMessageHandlerDeps {
147
+ /** Test seam invoked after a provider worker has been started. */
148
+ onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
147
149
  archiveResumeMessage(hqRoot: string, laneId: string, message: {
148
150
  message_id: string;
149
151
  [key: string]: unknown;
@@ -184,7 +186,7 @@ export interface LaneMessageHandlerDeps {
184
186
  validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
185
187
  withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
186
188
  }
187
- export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
189
+ export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
188
190
  dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
189
191
  authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
190
192
  authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
@@ -29,6 +29,8 @@ export declare const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
29
29
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
30
30
  export { emitLaneDecisionEvents };
31
31
  export interface LanesCommandDeps {
32
+ /** Test seam for failures after a lane provider worker is started. */
33
+ onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
32
34
  hqRoot?: () => string;
33
35
  /** Start HQ-root walk here instead of process.cwd(). Tests only. */
34
36
  cwd?: () => string;
@@ -7837,6 +7837,7 @@ export function registerLanesCommand(parent, deps = {}) {
7837
7837
  writeAnswerDeliveryFailed,
7838
7838
  });
7839
7839
  const messageHandlerDeps = {
7840
+ onLaneWorkerStarted: deps.onLaneWorkerStarted,
7840
7841
  archiveResumeMessage,
7841
7842
  assertLaneRunsHereForAdmission,
7842
7843
  assertLaneRunsHereForOperatorMutation,
@@ -7862,6 +7863,7 @@ export function registerLanesCommand(parent, deps = {}) {
7862
7863
  };
7863
7864
  const messageHandlers = createLaneMessageHandlers(messageHandlerDeps);
7864
7865
  const resumeHandlers = createLaneResumeHandlers({
7866
+ onLaneWorkerStarted: deps.onLaneWorkerStarted,
7865
7867
  archiveResumeMessage,
7866
7868
  assertLaneRunsHereForAdmission,
7867
7869
  assertLaneRunsHereForOperatorMutation,
@@ -8214,8 +8216,8 @@ export function registerLanesCommand(parent, deps = {}) {
8214
8216
  .description("Send a follow-up message to a lane")
8215
8217
  .argument("<lane-id>", "Lane id")
8216
8218
  .requiredOption("--text <msg>", "Message text")
8217
- .option("--project <slug>", "Re-point project attribution from this message")
8218
- .option("--story <id>", "Re-point story attribution from this message")
8219
+ .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
8220
+ .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
8219
8221
  .option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
8220
8222
  .option("--model <id>", "Model for the moved lane (with --account only)")
8221
8223
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
@@ -8292,8 +8294,8 @@ export function registerLanesCommand(parent, deps = {}) {
8292
8294
  .option("--text <msg>", "Operator message for the resumed turn")
8293
8295
  .option("--from-failed", "Explicitly resume a failed lane as its senior")
8294
8296
  .option("--reason <text>", "Required explanation for resuming from failed")
8295
- .option("--project <slug>", "Re-point project attribution from this resume")
8296
- .option("--story <id>", "Re-point story attribution from this resume")
8297
+ .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
8298
+ .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
8297
8299
  .option("--account <id>", "Move the lane's session to this account of its provider before resuming")
8298
8300
  .option("--model <id>", "Model for the moved lane (with --account only)")
8299
8301
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
@@ -8453,6 +8455,16 @@ export function registerLanesCommand(parent, deps = {}) {
8453
8455
  .action(wrap(async (provider, accountId, opts) => {
8454
8456
  await accountHandlers.runAccountsLogin(ctx, provider, accountId, opts);
8455
8457
  }));
8458
+ accounts
8459
+ .command("clear-quota")
8460
+ .description("Clear quota reset marks for a provider account")
8461
+ .argument("<provider>", "claude | codex | grok")
8462
+ .argument("<account-id>", "Account id")
8463
+ .option("--model <model>", "Clear only this model's reset")
8464
+ .option("--json", "JSON on stdout (always)")
8465
+ .action(wrap(async (provider, accountId, opts) => {
8466
+ await accountHandlers.runAccountsClearQuota(ctx, provider, accountId, opts);
8467
+ }));
8456
8468
  accounts
8457
8469
  .command("enable")
8458
8470
  .description("Enable an account without touching credentials")
@@ -3,8 +3,9 @@
3
3
  * still runs a service, the daemon leaves that service alone instead of
4
4
  * starting a second copy, and `hq daemon migrate` lists it.
5
5
  *
6
- * Detection only reads the filesystem (and asks schtasks on Windows). Nothing
7
- * here stops or removes a unit.
6
+ * Detection reads unit files and checks for an orphaned loaded macOS mesh job
7
+ * whose plist was removed. Windows scheduled tasks are probed through
8
+ * schtasks. Nothing here stops or removes a unit.
8
9
  */
9
10
  import type { ServiceName } from "./catalog.js";
10
11
  export interface LegacyUnit {
@@ -22,6 +23,8 @@ export interface LegacyProbeDeps {
22
23
  home: string;
23
24
  existsSync: (p: string) => boolean;
24
25
  listDir: (dir: string) => string[];
26
+ /** True when launchctl can print the GUI-domain job despite a missing plist. */
27
+ launchdJobLoaded: (label: string) => boolean;
25
28
  windowsTaskExists: (taskName: string) => boolean;
26
29
  }
27
30
  export declare function describeLegacyUnit(unit: LegacyUnit): string;
@@ -3,8 +3,9 @@
3
3
  * still runs a service, the daemon leaves that service alone instead of
4
4
  * starting a second copy, and `hq daemon migrate` lists it.
5
5
  *
6
- * Detection only reads the filesystem (and asks schtasks on Windows). Nothing
7
- * here stops or removes a unit.
6
+ * Detection reads unit files and checks for an orphaned loaded macOS mesh job
7
+ * whose plist was removed. Windows scheduled tasks are probed through
8
+ * schtasks. Nothing here stops or removes a unit.
8
9
  */
9
10
  import * as path from "node:path";
10
11
  const KIT_SERVICES = ["sync", "inbox", "heartbeat"];
@@ -52,8 +53,9 @@ function darwinUnits(deps) {
52
53
  const units = [];
53
54
  const at = (label) => path.join(dir, `${label}.plist`);
54
55
  const add = (unitId, service, label, migratable = true) => units.push({ unitId, service, kind: "launchd", name: label, path: at(label), migratable });
55
- if (deps.existsSync(at("ai.getindigo.hq-mesh-daemon")))
56
- add("mesh", "mesh", "ai.getindigo.hq-mesh-daemon");
56
+ const meshLabel = "ai.getindigo.hq-mesh-daemon";
57
+ if (deps.existsSync(at(meshLabel)) || deps.launchdJobLoaded(meshLabel))
58
+ add("mesh", "mesh", meshLabel);
57
59
  for (const file of safeList(deps, dir).sort()) {
58
60
  const m = /^ai\.getindigo\.hq-bot\.(.+)\.plist$/.exec(file);
59
61
  if (m)