@indigoai-us/hq-cli 5.131.0 → 5.133.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 (66) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/command-catalog.generated.d.ts +12 -0
  3. package/dist/command-catalog.generated.js +16 -0
  4. package/dist/commands/lanes.d.ts +15 -4
  5. package/dist/commands/lanes.js +478 -48
  6. package/dist/commands/mesh.js +24 -2
  7. package/dist/commands/update.js +1 -1
  8. package/dist/lib/bot/paths.d.ts +7 -5
  9. package/dist/lib/bot/paths.js +8 -1
  10. package/dist/lib/bot/prompt.d.ts +0 -2
  11. package/dist/lib/bot/prompt.js +5 -11
  12. package/dist/lib/bot/room-policy.d.ts +1 -24
  13. package/dist/lib/bot/room-policy.js +2 -35
  14. package/dist/lib/bot/run.d.ts +7 -8
  15. package/dist/lib/bot/run.js +21 -33
  16. package/dist/lib/bot/session.d.ts +1 -1
  17. package/dist/lib/bot/session.js +1 -1
  18. package/dist/lib/lanes/accounts.d.ts +6 -0
  19. package/dist/lib/lanes/accounts.js +2 -1
  20. package/dist/lib/lanes/config.d.ts +4 -0
  21. package/dist/lib/lanes/config.js +9 -0
  22. package/dist/lib/lanes/constants.d.ts +3 -0
  23. package/dist/lib/lanes/constants.js +3 -0
  24. package/dist/lib/lanes/index.d.ts +11 -3
  25. package/dist/lib/lanes/index.js +6 -1
  26. package/dist/lib/lanes/mesh-events.js +4 -0
  27. package/dist/lib/lanes/models.d.ts +78 -0
  28. package/dist/lib/lanes/models.js +205 -0
  29. package/dist/lib/lanes/move-account.d.ts +56 -0
  30. package/dist/lib/lanes/move-account.js +193 -0
  31. package/dist/lib/lanes/provider-rejection.d.ts +86 -0
  32. package/dist/lib/lanes/provider-rejection.js +258 -0
  33. package/dist/lib/lanes/quota.d.ts +23 -3
  34. package/dist/lib/lanes/quota.js +88 -14
  35. package/dist/lib/lanes/reconcile-install.d.ts +24 -3
  36. package/dist/lib/lanes/reconcile-install.js +48 -12
  37. package/dist/lib/lanes/reconcile.d.ts +5 -2
  38. package/dist/lib/lanes/reconcile.js +135 -13
  39. package/dist/lib/lanes/spawn.js +26 -5
  40. package/dist/lib/lanes/store.js +10 -0
  41. package/dist/lib/lanes/types.d.ts +51 -2
  42. package/dist/lib/lanes/types.js +5 -3
  43. package/dist/lib/lanes/watcher.d.ts +27 -0
  44. package/dist/lib/lanes/watcher.js +68 -5
  45. package/dist/lib/mesh/live/backfill-held.d.ts +4 -3
  46. package/dist/lib/mesh/live/backfill-held.js +37 -14
  47. package/dist/lib/mesh/live/daemon/doctor.d.ts +6 -0
  48. package/dist/lib/mesh/live/daemon/doctor.js +16 -2
  49. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  50. package/dist/lib/mesh/live/daemon/install.js +100 -7
  51. package/dist/lib/mesh/live/daemon/legacy-listen.d.ts +43 -0
  52. package/dist/lib/mesh/live/daemon/legacy-listen.js +89 -0
  53. package/dist/lib/mesh/live/daemon/live-cache.d.ts +26 -0
  54. package/dist/lib/mesh/live/daemon/live-cache.js +116 -0
  55. package/dist/lib/mesh/live/daemon/presence.d.ts +7 -3
  56. package/dist/lib/mesh/live/daemon/presence.js +10 -9
  57. package/dist/lib/mesh/live/daemon/run.d.ts +18 -0
  58. package/dist/lib/mesh/live/daemon/run.js +65 -6
  59. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +31 -0
  60. package/dist/lib/mesh/live/daemon/work-subscribe.js +124 -0
  61. package/dist/lib/mesh/live/emit-evidence.d.ts +19 -0
  62. package/dist/lib/mesh/live/emit-evidence.js +20 -2
  63. package/dist/lib/work-context/company.d.ts +16 -0
  64. package/dist/lib/work-context/company.js +46 -14
  65. package/dist/utils/self-update.js +4 -1
  66. package/package.json +2 -2
@@ -17,17 +17,19 @@ import { vaultApiFetch } from "../utils/vault-api.js";
17
17
  import { openMeshTransport } from "../lib/mesh/client.js";
18
18
  import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
19
19
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
20
- import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, isLoggedOut, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
20
+ import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, MAX_LANE_DEPTH, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, isLoggedOut, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
21
21
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
22
22
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
23
23
  import { providerCompactAfterMs } from "../lib/lanes/config.js";
24
24
  import { parseCliEffort, resolveLaneEffort } from "../lib/lanes/effort.js";
25
25
  import { LanesError } from "../lib/lanes/errors.js";
26
- import { accountQuotaView, loadQuotaState } from "../lib/lanes/quota.js";
26
+ import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
27
+ import { SESSION_MOVE_REASON, copySessionFiles, planSessionCopy } from "../lib/lanes/move-account.js";
28
+ import { defaultProbe, validateLaneModel, } from "../lib/lanes/models.js";
27
29
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, stopLaneProcesses, } from "../lib/lanes/spawn.js";
28
30
  export { spawnLaneWorker };
29
31
  export { questionBodyAbsPath };
30
- export const MAX_LANE_DEPTH = 2;
32
+ export { MAX_LANE_DEPTH };
31
33
  export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
32
34
  export const OUTPOST_DIR = "/etc/outpost";
33
35
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
@@ -201,6 +203,7 @@ function resolveDeps(deps = {}) {
201
203
  };
202
204
  }),
203
205
  resolveMachineIdentity: deps.resolveMachineIdentity ?? lookupMachineIdentity,
206
+ probeProvider: deps.probeProvider ?? defaultProbe,
204
207
  runCompaction: deps.runCompaction,
205
208
  locateTranscript: deps.locateTranscript,
206
209
  stageOutpostPayload: deps.stageOutpostPayload ?? defaultStageOutpostPayload,
@@ -1047,6 +1050,7 @@ export const PICKUP_TERMINAL_REASONS = [
1047
1050
  "account_not_logged_in",
1048
1051
  "unknown_account",
1049
1052
  "account_disabled",
1053
+ "model_unsupported",
1050
1054
  ];
1051
1055
  export const DISPATCH_TERMINAL_REASON = "outpost_dispatch_failed";
1052
1056
  function parsePickupTerminalReason(value) {
@@ -1069,28 +1073,39 @@ function markDispatchFailure(hqRoot, laneId, details) {
1069
1073
  };
1070
1074
  });
1071
1075
  }
1072
- function markPickupFailure(hqRoot, laneId, reasonRaw, details) {
1076
+ /**
1077
+ * The CAS updater a pickup failure applies. A lane that is already terminal
1078
+ * for any reason (a cancel that landed after the caller's last read, an
1079
+ * earlier pickup failure) is returned unchanged: the guard lives inside the
1080
+ * same compare-and-swap that writes the failure, so no read-then-write gap
1081
+ * can overwrite another writer's terminal decision.
1082
+ */
1083
+ export function pickupFailureUpdater(reasonRaw, details) {
1073
1084
  const reason = parsePickupTerminalReason(reasonRaw);
1074
- return casUpdateLane(hqRoot, laneId, (current) => {
1075
- const existing = current
1076
- .terminal_reason;
1077
- if (isTerminalLaneState(current.state) && existing === reason) {
1085
+ return (current) => {
1086
+ // A running record is never overwritten either: a worker that was
1087
+ // admitted meanwhile keeps its lane, and the watcher classifies it.
1088
+ if (isTerminalLaneState(current.state) || current.state === "running")
1078
1089
  return current;
1079
- }
1080
1090
  return {
1081
1091
  ...current,
1082
1092
  state: "failed",
1083
1093
  queued_reason: undefined,
1094
+ pickup_validation_pending: undefined,
1095
+ pickup_validation_pending_since: undefined,
1084
1096
  terminal_reason: reason,
1085
1097
  terminal_details: details,
1086
1098
  };
1087
- });
1099
+ };
1100
+ }
1101
+ function markPickupFailure(hqRoot, laneId, reasonRaw, details) {
1102
+ return casUpdateLane(hqRoot, laneId, pickupFailureUpdater(reasonRaw, details));
1088
1103
  }
1089
1104
  /**
1090
1105
  * Outpost-side account bind (spec §5.2). Writes a named terminal reason,
1091
1106
  * never a plain `failed`.
1092
1107
  */
1093
- export function applyOutpostAccountPickup(hqRoot, laneId) {
1108
+ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
1094
1109
  const lane = readLane(hqRoot, laneId);
1095
1110
  const config = loadLanesConfig(hqRoot);
1096
1111
  try {
@@ -1104,16 +1119,60 @@ export function applyOutpostAccountPickup(hqRoot, laneId) {
1104
1119
  account: resolved.id,
1105
1120
  });
1106
1121
  }
1107
- if (lane.account === resolved.id)
1108
- return lane;
1122
+ // The executing host is the first place the model can be checked
1123
+ // against this account's provider list: an outpost create without
1124
+ // --account has no local account home to validate against.
1125
+ // The probe can block for up to MODEL_PROBE_TIMEOUT_MS. A cancel or any
1126
+ // other terminal decision taken meanwhile is final: after every probe
1127
+ // result the lane is re-read and a terminal lane is returned as it is
1128
+ // (so runPickup never proceeds into admission for it). The failure
1129
+ // write itself refuses a terminal record inside its CAS
1130
+ // (pickupFailureUpdater), so a cancel landing between this re-read and
1131
+ // that write is kept too.
1132
+ let current = lane;
1133
+ if (lane.model !== undefined) {
1134
+ let unsupported = null;
1135
+ try {
1136
+ validateLaneModel(lane.provider, lane.model, resolved.home, modelDeps);
1137
+ }
1138
+ catch (err) {
1139
+ if (isLanesError(err) && err.error === "model_unsupported") {
1140
+ unsupported = err;
1141
+ }
1142
+ else {
1143
+ throw err;
1144
+ }
1145
+ }
1146
+ current = readLane(hqRoot, laneId);
1147
+ if (isTerminalLaneState(current.state) || current.state === "running")
1148
+ return current;
1149
+ if (unsupported) {
1150
+ return markPickupFailure(hqRoot, laneId, "model_unsupported", {
1151
+ ...unsupported.details,
1152
+ account: resolved.id,
1153
+ });
1154
+ }
1155
+ }
1156
+ if (current.account === resolved.id && current.pickup_validation_pending === undefined) {
1157
+ return current;
1158
+ }
1109
1159
  // The bind is the executing host's pre-spawn account resolution, the same
1110
1160
  // write spawnLaneWorker makes for a local lane, so it carries the spawn
1111
1161
  // writer. `account` stays closed to `command`: an operator command never
1112
- // re-pins the account of a lane it does not execute.
1162
+ // re-pins the account of a lane it does not execute. The bind also
1163
+ // clears the pickup marker: from here the reconcile pass may admit it.
1113
1164
  return retryUpdateLane(hqRoot, laneId, "spawn", (current) => {
1114
- if (current.account === resolved.id)
1165
+ if (isTerminalLaneState(current.state) || current.state === "running")
1166
+ return current;
1167
+ if (current.account === resolved.id && current.pickup_validation_pending === undefined) {
1115
1168
  return current;
1116
- return { ...current, account: resolved.id };
1169
+ }
1170
+ return {
1171
+ ...current,
1172
+ account: resolved.id,
1173
+ pickup_validation_pending: undefined,
1174
+ pickup_validation_pending_since: undefined,
1175
+ };
1117
1176
  });
1118
1177
  }
1119
1178
  catch (err) {
@@ -1255,6 +1314,106 @@ async function runCreate(ctx, opts) {
1255
1314
  }
1256
1315
  const now = ctx.now();
1257
1316
  const createdAt = now.toISOString();
1317
+ // `--model` is validated against the provider's own model list (models.ts)
1318
+ // before anything is written or posted: ahead of the mesh thread, so a
1319
+ // refusal leaves no orphan thread, and ahead of the admission lock, so a
1320
+ // slow `grok models` / `claude --help` probe never holds the lock other
1321
+ // creates wait on. A pinned create checks the pinned account's home. An
1322
+ // unpinned create is not bound to an account here (persist re-resolves the
1323
+ // balanced account under the lock, so overlapping creates still spread),
1324
+ // so it checks the balanced candidate first and then every other enabled,
1325
+ // logged-in account the locked selection could land on (codex catalogs are per home
1326
+ // and can differ). `validatedAccounts` records which accepted the model
1327
+ // or had no readable list, and the balanced pick under the lock is
1328
+ // restricted to that set (resolveAccount's allowedAccounts): an account
1329
+ // whose list refused the model is never selected, so nothing has to be
1330
+ // refused after the mesh thread exists, and no probe or catalog read runs
1331
+ // while the lock is held. Every outpost create,
1332
+ // pinned or not, is validated at pickup on the executing host instead:
1333
+ // its provider list and CLI are the ones that run the lane.
1334
+ const modelDeps = {
1335
+ readFileSync: ctx.readFileSync,
1336
+ probe: ctx.probeProvider,
1337
+ env: ctx.env,
1338
+ };
1339
+ const validatedAccounts = new Set();
1340
+ if (cliModel !== undefined && !outpostId) {
1341
+ const accounts = config.providers[provider].accounts;
1342
+ const candidate = pinnedAccount ??
1343
+ resolveAccount(provider, undefined, config, {
1344
+ hqRoot,
1345
+ existsSync: ctx.existsSync,
1346
+ cliModel,
1347
+ }).id;
1348
+ const toValidate = pinnedAccount !== undefined
1349
+ ? [candidate]
1350
+ : [
1351
+ candidate,
1352
+ ...Object.keys(accounts)
1353
+ .filter((id) => id !== candidate &&
1354
+ accounts[id]?.enabled &&
1355
+ accountStatus(provider, id, config, { existsSync: ctx.existsSync }).logged_in === true)
1356
+ .sort(),
1357
+ ];
1358
+ const checkedAccounts = [];
1359
+ const unsupported = [];
1360
+ const quotaBlocked = [];
1361
+ const quota = loadQuotaState(hqRoot);
1362
+ for (const id of toValidate) {
1363
+ const home = accounts[id]?.home;
1364
+ if (home === undefined)
1365
+ continue;
1366
+ checkedAccounts.push(id);
1367
+ try {
1368
+ validateLaneModel(provider, cliModel, home, modelDeps);
1369
+ }
1370
+ catch (err) {
1371
+ if (!(isLanesError(err) && err.error === "model_unsupported"))
1372
+ throw err;
1373
+ unsupported.push({ account: id, error: err });
1374
+ continue;
1375
+ }
1376
+ const block = quotaBlockFor(quota, provider, id, cliModel, now);
1377
+ if (block.blocked) {
1378
+ quotaBlocked.push({ account: id, block });
1379
+ continue;
1380
+ }
1381
+ validatedAccounts.add(id);
1382
+ }
1383
+ if (validatedAccounts.size === 0 && quotaBlocked.length > 0) {
1384
+ const blocked = quotaBlocked[0];
1385
+ const model = blocked.block.model ?? cliModel;
1386
+ const checkedSuffix = ` Checked accounts: ${checkedAccounts.join(", ")}`;
1387
+ if (blocked.block.reason === "account_model_unavailable") {
1388
+ throw new AccountResolutionError("account_model_unavailable", `Account ${JSON.stringify(blocked.account)} cannot run model ${model ?? "(no model)"}: ${blocked.block.marker ?? "needs credits"}${checkedSuffix}`, {
1389
+ provider,
1390
+ account: blocked.account,
1391
+ model: model ?? null,
1392
+ marker: blocked.block.marker ?? null,
1393
+ checked_accounts: checkedAccounts,
1394
+ });
1395
+ }
1396
+ throw new AccountResolutionError("account_quota_exhausted", `Account ${JSON.stringify(blocked.account)} is out of quota${blocked.block.quota_reset_at ? ` until ${blocked.block.quota_reset_at}` : ""} for model ${model ?? "(no model)"}${checkedSuffix}`, {
1397
+ provider,
1398
+ account: blocked.account,
1399
+ model: model ?? null,
1400
+ quota_reset_at: blocked.block.quota_reset_at ?? null,
1401
+ checked_accounts: checkedAccounts,
1402
+ });
1403
+ }
1404
+ if (validatedAccounts.size === 0 && unsupported.length > 0) {
1405
+ const first = unsupported[0].error;
1406
+ throw new LanesError(first.error, `${first.message} Checked accounts: ${checkedAccounts.join(", ")}`, {
1407
+ ...first.details,
1408
+ checked_accounts: checkedAccounts,
1409
+ account_errors: unsupported.map(({ account, error }) => ({
1410
+ account,
1411
+ accepted: error.details.accepted,
1412
+ source: error.details.source,
1413
+ })),
1414
+ });
1415
+ }
1416
+ }
1258
1417
  const threadId = company
1259
1418
  ? await ctx.mesh.ensureProjectThread(company, project)
1260
1419
  : undefined;
@@ -1266,6 +1425,7 @@ async function runCreate(ctx, opts) {
1266
1425
  hqRoot,
1267
1426
  existsSync: ctx.existsSync,
1268
1427
  cliModel,
1428
+ ...(validatedAccounts.size > 0 ? { allowedAccounts: [...validatedAccounts] } : {}),
1269
1429
  }).id;
1270
1430
  }
1271
1431
  const accountCfg = pinnedAccount !== undefined
@@ -1525,13 +1685,30 @@ function parseRemotePickupJson(stdout) {
1525
1685
  }
1526
1686
  return null;
1527
1687
  }
1528
- function ingestPickupPayload(hqRoot, payload) {
1688
+ function ingestPickupPayload(hqRoot, payload, now) {
1529
1689
  const laneId = payload.lane.lane_id;
1530
1690
  if (!laneId) {
1531
1691
  throw new LanesError("outpost_payload_invalid", "Pickup payload lane.lane_id is required", {});
1532
1692
  }
1533
1693
  const existing = tryReadLane(hqRoot, laneId);
1534
- const lane = existing ?? createLane(hqRoot, payload.lane);
1694
+ // The lane is stored ineligible for admission until the account bind
1695
+ // clears the marker (see LaneRecord.pickup_validation_pending).
1696
+ const lane = existing === null
1697
+ ? createLane(hqRoot, {
1698
+ ...payload.lane,
1699
+ updated_at: now.toISOString(),
1700
+ pickup_validation_pending: true,
1701
+ pickup_validation_pending_since: now.toISOString(),
1702
+ })
1703
+ : existing.state === "queued"
1704
+ ? retryUpdateLane(hqRoot, laneId, "command", (current) => current.state === "queued"
1705
+ ? {
1706
+ ...current,
1707
+ pickup_validation_pending: true,
1708
+ pickup_validation_pending_since: now.toISOString(),
1709
+ }
1710
+ : current)
1711
+ : existing;
1535
1712
  const dest = runDir(hqRoot, lane.lane_id);
1536
1713
  ensureDir(path.join(dest, "logs"));
1537
1714
  ensureDir(path.join(dest, "envelopes"));
@@ -1574,7 +1751,7 @@ async function runPickup(ctx, opts) {
1574
1751
  throw new LanesError("outpost_payload_invalid", `Pickup payload is not JSON: ${err instanceof Error ? err.message : String(err)}`, {});
1575
1752
  }
1576
1753
  const payload = parseOutpostPickupPayload(parsedJson);
1577
- const ingested = ingestPickupPayload(hqRoot, payload);
1754
+ const ingested = ingestPickupPayload(hqRoot, payload, ctx.now());
1578
1755
  if (ingested.host.kind === "outpost") {
1579
1756
  try {
1580
1757
  refuseOutpostSelfTarget(hqRoot, ingested, {
@@ -1599,7 +1776,11 @@ async function runPickup(ctx, opts) {
1599
1776
  throw err;
1600
1777
  }
1601
1778
  }
1602
- const afterAccount = applyOutpostAccountPickup(hqRoot, ingested.lane_id);
1779
+ const afterAccount = applyOutpostAccountPickup(hqRoot, ingested.lane_id, {
1780
+ readFileSync: ctx.readFileSync,
1781
+ probe: ctx.probeProvider,
1782
+ env: ctx.env,
1783
+ });
1603
1784
  if (isTerminalLaneState(afterAccount.state)) {
1604
1785
  printJson({
1605
1786
  ok: true,
@@ -1741,23 +1922,182 @@ async function runShow(ctx, laneId) {
1741
1922
  provider_session_id_absent: lane.provider_session_id_absent ?? null,
1742
1923
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
1743
1924
  last_resume: lane.last_resume ?? null,
1925
+ account: lane.account ?? null,
1926
+ account_history: lane.account_history ?? [],
1927
+ pending_resume: lane.pending_resume ?? null,
1744
1928
  last_run_usage: lane.last_run_usage ?? null,
1745
1929
  model: lane.model ?? null,
1746
1930
  effort: lane.effort ?? null,
1747
1931
  effort_source: lane.effort_source ?? null,
1932
+ terminal_reason: lane.terminal_reason ?? null,
1933
+ terminal_details: lane.terminal_details ?? null,
1748
1934
  last_message_at: lane.last_message_at ?? null,
1749
1935
  last_compaction: lane.last_compaction
1750
1936
  ? { ...lane.last_compaction, after: compactionAfterUsage(lane) }
1751
1937
  : null,
1752
1938
  });
1753
1939
  }
1940
+ /**
1941
+ * US-023: move the lane's provider session to `--account` (same provider)
1942
+ * before a resume or a message resume. Every check runs before anything is
1943
+ * copied or written; the copy runs before the record changes, so a failed
1944
+ * copy leaves the lane on its source account. The turn is started by the
1945
+ * caller, or parked as `pending_resume` when the target is at its cap so the
1946
+ * reconcile pass resumes it there.
1947
+ */
1948
+ async function prepareAccountMove(ctx, lane, input) {
1949
+ const hqRoot = ctx.hqRoot();
1950
+ const now = ctx.now();
1951
+ const laneId = lane.lane_id;
1952
+ if (lane.state === "running") {
1953
+ throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
1954
+ lane_id: laneId,
1955
+ state: lane.state,
1956
+ });
1957
+ }
1958
+ // `--account <id>` or `--account <provider>:<id>`: a provider prefix that
1959
+ // is not the lane's is a cross-provider move, which no session survives.
1960
+ const raw = input.account.trim();
1961
+ const colon = raw.indexOf(":");
1962
+ const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
1963
+ const target = colon > 0 ? raw.slice(colon + 1) : raw;
1964
+ if (requestedProvider !== undefined && requestedProvider !== lane.provider) {
1965
+ throw new LanesError("account_provider_mismatch", `Lane ${laneId} runs on ${lane.provider}; account ${JSON.stringify(raw)} names provider ${requestedProvider}. Sessions do not move between providers`, { lane_id: laneId, provider: lane.provider, requested_provider: requestedProvider, account: target });
1966
+ }
1967
+ if (!target) {
1968
+ throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(input.account)} for ${lane.provider}`, {
1969
+ provider: lane.provider,
1970
+ account: input.account,
1971
+ enabled: [],
1972
+ });
1973
+ }
1974
+ if (target === lane.account) {
1975
+ throw new LanesError("account_unchanged", `Lane ${laneId} already runs on account ${target}`, {
1976
+ lane_id: laneId,
1977
+ account: target,
1978
+ });
1979
+ }
1980
+ if (!providerSupportsSessionResume(lane.provider)) {
1981
+ throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
1982
+ }
1983
+ if (!hasRecordedSessionId(lane)) {
1984
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no recorded provider session id, so there is no session to move`, { lane_id: laneId, provider: lane.provider });
1985
+ }
1986
+ const sessionId = lane.provider_session_id;
1987
+ const config = loadLanesConfig(hqRoot);
1988
+ const providerCfg = config.providers[lane.provider];
1989
+ const targetCfg = providerCfg.accounts[target];
1990
+ if (targetCfg === undefined) {
1991
+ throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
1992
+ }
1993
+ if (!targetCfg.enabled) {
1994
+ throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
1995
+ }
1996
+ if (isLoggedOut(accountStatus(lane.provider, target, config, { existsSync: ctx.existsSync }))) {
1997
+ throw new LanesError("account_not_logged_in", `Account ${target} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: target, lane_id: laneId });
1998
+ }
1999
+ // The model the moved run would use, resolved the way spawn resolves it:
2000
+ // the operator's --model, else the lane's own, else the target account's
2001
+ // default, else the provider's. The quota and validation checks use this
2002
+ // effective model so a per-model hold on the target's default is seen
2003
+ // here, before anything is copied. A target that pins a different model
2004
+ // refuses the lane's model unless the operator names one for the move.
2005
+ const model = resolveLaneModel({
2006
+ cliModel: input.cliModel ?? lane.model,
2007
+ accountModel: targetCfg.model,
2008
+ providerModel: providerCfg.model,
2009
+ });
2010
+ if (input.cliModel === undefined && lane.model !== undefined && targetCfg.model !== undefined && targetCfg.model !== lane.model) {
2011
+ throw new LanesError("model_unsupported", `Account ${target} pins model ${targetCfg.model}; lane ${laneId} runs ${lane.model}. Pass --model ${targetCfg.model} to move it onto that model`, { provider: lane.provider, account: target, model: lane.model, account_model: targetCfg.model, lane_id: laneId });
2012
+ }
2013
+ const block = quotaBlockFor(loadQuotaState(hqRoot), lane.provider, target, model, now);
2014
+ if (block.blocked) {
2015
+ throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
2016
+ }
2017
+ if (model !== undefined) {
2018
+ validateLaneModel(lane.provider, model, targetCfg.home, {
2019
+ readFileSync: ctx.readFileSync,
2020
+ probe: ctx.probeProvider,
2021
+ env: ctx.env,
2022
+ });
2023
+ }
2024
+ const sourceCfg = lane.account !== undefined ? providerCfg.accounts[lane.account] : undefined;
2025
+ if (sourceCfg === undefined) {
2026
+ throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
2027
+ }
2028
+ // Checks done. Copy the session files, then write the record.
2029
+ const copy = copySessionFiles(planSessionCopy(lane.provider, {
2030
+ sourceHome: sourceCfg.home,
2031
+ targetHome: targetCfg.home,
2032
+ cwd: hqRoot,
2033
+ sessionId,
2034
+ }));
2035
+ const at = now.toISOString();
2036
+ const from = lane.account ?? null;
2037
+ // The capacity decision and the record write run under the target
2038
+ // account's admission lock, the same serialization lane creation uses. A
2039
+ // slot is taken by a running lane or by a lane another move has already
2040
+ // pointed at the account whose spawn has not claimed it yet
2041
+ // (`pending_resume` set by this write, cleared by the claim), so two moves
2042
+ // into one free slot cannot both start.
2043
+ return withAdmissionLock(admissionLockPath(hqRoot, lane.provider, target), async () => {
2044
+ const lanes = loadAllLanes(hqRoot);
2045
+ // A queued lane with a pending resume waits for admission and holds
2046
+ // no slot; one in any other state with a pending resume is a move
2047
+ // whose spawn is about to claim, and holds the slot it was granted.
2048
+ const occupies = (row) => row.lane_id !== laneId &&
2049
+ (row.state === "running" || (row.pending_resume !== undefined && row.state !== "queued"));
2050
+ const onProvider = lanes.filter((row) => row.provider === lane.provider && occupies(row));
2051
+ const atCap = onProvider.length >= providerCfg.max_running ||
2052
+ onProvider.filter((row) => row.account === target).length >= accountMaxRunning(providerCfg, target);
2053
+ const moved = casUpdateLane(hqRoot, laneId, (current) => {
2054
+ // The copy and every check above were made from `lane`; a record
2055
+ // that moved on since (another move, a spawn, a cancel) invalidates
2056
+ // them, so the write is refused rather than applied to a state it
2057
+ // was not prepared for.
2058
+ if (current.version !== lane.version) {
2059
+ throw new LanesError("account_move_conflict", `Lane ${laneId} changed while the move was prepared (version ${lane.version} → ${current.version}, state ${current.state}, account ${current.account ?? "(none)"}); run the move again`, { lane_id: laneId, prepared_version: lane.version, version: current.version, state: current.state, account: current.account ?? null });
2060
+ }
2061
+ const next = {
2062
+ ...current,
2063
+ account: target,
2064
+ ...(input.cliModel !== undefined ? { model: input.cliModel } : {}),
2065
+ account_history: appendCappedHistory(current.account_history ?? [], {
2066
+ from,
2067
+ to: target,
2068
+ at,
2069
+ reason: SESSION_MOVE_REASON,
2070
+ }),
2071
+ pending_resume: { session_id: sessionId, at, ...input.pending },
2072
+ };
2073
+ if (atCap) {
2074
+ next.state = "queued";
2075
+ next.queued_reason = "cap";
2076
+ }
2077
+ return next;
2078
+ });
2079
+ return { lane: moved, from, to: target, copy, queued: atCap };
2080
+ }, ctx.sleep);
2081
+ }
1754
2082
  async function runMessage(ctx, laneId, opts) {
1755
2083
  const hqRoot = ctx.hqRoot();
1756
2084
  const text = opts.text;
1757
2085
  if (!text) {
1758
2086
  throw new LanesError("missing_text", "--text is required", {});
1759
2087
  }
1760
- const lane = readLane(hqRoot, laneId);
2088
+ let lane = readLane(hqRoot, laneId);
2089
+ // An account move is refused on its own terms first: a running lane and a
2090
+ // provider without session resume get the move's named errors, not the
2091
+ // generic delivery refusal below.
2092
+ if (opts.account !== undefined && lane.state === "running") {
2093
+ throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
2094
+ lane_id: laneId,
2095
+ state: lane.state,
2096
+ });
2097
+ }
2098
+ if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
2099
+ throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
2100
+ }
1761
2101
  // One decision point for how a follow-up reaches this lane (delivery.ts).
1762
2102
  // `none` refuses before anything is written: no inbox append, no spawn.
1763
2103
  const mechanism = laneDeliveryMechanism(lane);
@@ -1770,6 +2110,9 @@ async function runMessage(ctx, laneId, opts) {
1770
2110
  message,
1771
2111
  });
1772
2112
  }
2113
+ if (opts.model !== undefined && opts.account === undefined) {
2114
+ throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
2115
+ }
1773
2116
  const messageId = mintMessageId();
1774
2117
  const at = ctx.now().toISOString();
1775
2118
  const projectId = opts.project?.trim() || lane.project_id;
@@ -1782,7 +2125,24 @@ async function runMessage(ctx, laneId, opts) {
1782
2125
  story_id: storyId,
1783
2126
  };
1784
2127
  let resumed = null;
1785
- if (mechanism === "dropbox") {
2128
+ let move = null;
2129
+ if (opts.account !== undefined) {
2130
+ // A moved message is always a resume on the target account: a running
2131
+ // lane (the dropbox mechanism) is refused inside prepareAccountMove.
2132
+ move = await prepareAccountMove(ctx, lane, {
2133
+ account: opts.account,
2134
+ cliModel: opts.model?.trim() || undefined,
2135
+ pending: { trigger: "message", text, message_id: messageId, compact: opts.compact !== false },
2136
+ });
2137
+ lane = move.lane;
2138
+ if (move.queued) {
2139
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
2140
+ }
2141
+ }
2142
+ if (move?.queued) {
2143
+ // Parked at the target's cap: reconcile resumes the session there.
2144
+ }
2145
+ else if (mechanism === "dropbox") {
1786
2146
  // Live (or about-to-be-live) process: the PreToolUse drain delivers it.
1787
2147
  if (!laneAcceptsOperatorMessage(lane)) {
1788
2148
  throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
@@ -1841,7 +2201,11 @@ async function runMessage(ctx, laneId, opts) {
1841
2201
  // started from; `resume` has started a new worker with the message as its
1842
2202
  // prompt. Neither is "delivered", so the response never says so.
1843
2203
  const drainHook = drainHookStatusFor(ctx, next);
1844
- const deliveryStatus = mechanism === "dropbox" ? "awaiting_next_tool_call" : "worker_spawned";
2204
+ const deliveryStatus = move?.queued
2205
+ ? "queued_on_target_account"
2206
+ : mechanism === "dropbox"
2207
+ ? "awaiting_next_tool_call"
2208
+ : "worker_spawned";
1845
2209
  const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
1846
2210
  ? `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.`
1847
2211
  : mechanism === "dropbox" && next.stalled === true
@@ -1863,11 +2227,24 @@ async function runMessage(ctx, laneId, opts) {
1863
2227
  compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
1864
2228
  ? resumed.last_compaction
1865
2229
  : null,
2230
+ account_move: move ? accountMoveSummary(move) : null,
1866
2231
  project_id: next.project_id,
1867
2232
  story_id: next.story_id,
1868
2233
  lane: next,
1869
2234
  });
1870
2235
  }
2236
+ function accountMoveSummary(move) {
2237
+ return {
2238
+ from: move.from,
2239
+ to: move.to,
2240
+ queued: move.queued,
2241
+ copied: move.copy.copied,
2242
+ sources: move.copy.sources,
2243
+ targets: move.copy.targets,
2244
+ source_dir: move.copy.source_dir,
2245
+ target_dir: move.copy.target_dir,
2246
+ };
2247
+ }
1871
2248
  /**
1872
2249
  * `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
1873
2250
  * continues a lane in `interrupted`, `done` or `blocked` on its recorded
@@ -1879,7 +2256,19 @@ async function runMessage(ctx, laneId, opts) {
1879
2256
  */
1880
2257
  async function runResume(ctx, laneId, opts) {
1881
2258
  const hqRoot = ctx.hqRoot();
1882
- const lane = readLane(hqRoot, laneId);
2259
+ let lane = readLane(hqRoot, laneId);
2260
+ if (opts.model !== undefined && opts.account === undefined) {
2261
+ throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
2262
+ }
2263
+ if (opts.account !== undefined && lane.state === "running") {
2264
+ throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
2265
+ lane_id: laneId,
2266
+ state: lane.state,
2267
+ });
2268
+ }
2269
+ if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
2270
+ throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
2271
+ }
1883
2272
  const refusal = resumeCommandRefusal(lane);
1884
2273
  if (refusal !== null) {
1885
2274
  throw new LanesError(refusal.error, refusal.message, {
@@ -1896,25 +2285,42 @@ async function runResume(ctx, laneId, opts) {
1896
2285
  const storyId = opts.story?.trim() || lane.story_id;
1897
2286
  const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
1898
2287
  const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
1899
- const resumed = spawnLaneWorker(hqRoot, lane, {
1900
- env: ctx.env,
1901
- existsSync: ctx.existsSync,
1902
- spawnProcess: ctx.spawnProcess,
1903
- proveDetached: ctx.proveDetached,
1904
- killProcess: ctx.killProcess,
1905
- now: ctx.now,
1906
- resolveMachineIdentity: ctx.resolveMachineIdentity,
1907
- runCompaction: ctx.runCompaction,
1908
- locateTranscript: ctx.locateTranscript,
1909
- }, {
1910
- trigger: {
1911
- kind: "resume",
1912
- from: resumedFrom,
1913
- ...(text !== undefined ? { text } : {}),
1914
- ...(messageId !== undefined ? { message_id: messageId } : {}),
1915
- },
1916
- compact: opts.compact !== false,
1917
- });
2288
+ let move = null;
2289
+ if (opts.account !== undefined) {
2290
+ move = await prepareAccountMove(ctx, lane, {
2291
+ account: opts.account,
2292
+ cliModel: opts.model?.trim() || undefined,
2293
+ pending: {
2294
+ trigger: "resume",
2295
+ resumed_from: resumedFrom,
2296
+ ...(text !== undefined ? { text } : {}),
2297
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
2298
+ compact: opts.compact !== false,
2299
+ },
2300
+ });
2301
+ lane = move.lane;
2302
+ }
2303
+ const resumed = move?.queued
2304
+ ? lane
2305
+ : spawnLaneWorker(hqRoot, lane, {
2306
+ env: ctx.env,
2307
+ existsSync: ctx.existsSync,
2308
+ spawnProcess: ctx.spawnProcess,
2309
+ proveDetached: ctx.proveDetached,
2310
+ killProcess: ctx.killProcess,
2311
+ now: ctx.now,
2312
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
2313
+ runCompaction: ctx.runCompaction,
2314
+ locateTranscript: ctx.locateTranscript,
2315
+ }, {
2316
+ trigger: {
2317
+ kind: "resume",
2318
+ from: resumedFrom,
2319
+ ...(text !== undefined ? { text } : {}),
2320
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
2321
+ },
2322
+ compact: opts.compact !== false,
2323
+ });
1918
2324
  if (text !== undefined && messageId !== undefined) {
1919
2325
  // Same audit copy a message resume leaves; nothing drains it. Archive only
1920
2326
  // after a successful spawn so a concurrent resume cannot leave a phantom.
@@ -1961,7 +2367,8 @@ async function runResume(ctx, laneId, opts) {
1961
2367
  message_id: messageId ?? null,
1962
2368
  text: text ?? null,
1963
2369
  delivery: "resume",
1964
- delivery_status: "worker_spawned",
2370
+ delivery_status: move?.queued ? "queued_on_target_account" : "worker_spawned",
2371
+ account_move: move ? accountMoveSummary(move) : null,
1965
2372
  resumed_session_id: resumed.last_resume?.session_id ?? null,
1966
2373
  last_resume: next.last_resume ?? null,
1967
2374
  compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
@@ -2025,15 +2432,26 @@ async function runReparent(ctx, laneId, opts) {
2025
2432
  throw new LanesError("demotion_with_pending_questions", "Cannot increase a lane's depth while it has pending questions", { lane_id: laneId, from_depth: oldDepth, to_depth: newDepth });
2026
2433
  }
2027
2434
  const override = { laneId, senior: nextSenior, depth: newDepth };
2435
+ const descendants = [];
2028
2436
  for (const other of loadAllLanes(hqRoot)) {
2029
2437
  if (other.lane_id === laneId)
2030
2438
  continue;
2031
2439
  if (!walkHitsLane(hqRoot, other.senior, laneId, ctx))
2032
2440
  continue;
2033
- const d = derivedLaneDepth(hqRoot, other.senior, ctx, override);
2034
- if (d > MAX_LANE_DEPTH) {
2035
- throw new LanesError("max_lane_depth", `Re-parent would put descendant ${other.lane_id} at depth ${d}`, { lane_id: other.lane_id, depth: d, max: MAX_LANE_DEPTH });
2441
+ const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
2442
+ const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
2443
+ if (descendantDepth > MAX_LANE_DEPTH) {
2444
+ throw new LanesError("max_lane_depth", `Re-parent would put descendant ${other.lane_id} at depth ${descendantDepth}`, { lane_id: other.lane_id, depth: descendantDepth, max: MAX_LANE_DEPTH });
2445
+ }
2446
+ if (descendantDepth > oldDescendantDepth &&
2447
+ pendingQuestionsForLane(hqRoot, other.lane_id).length > 0) {
2448
+ throw new LanesError("demotion_with_pending_questions", "Cannot increase a lane's depth while it has pending questions", {
2449
+ lane_id: other.lane_id,
2450
+ from_depth: oldDescendantDepth,
2451
+ to_depth: descendantDepth,
2452
+ });
2036
2453
  }
2454
+ descendants.push({ laneId: other.lane_id, depth: descendantDepth });
2037
2455
  }
2038
2456
  const at = ctx.now().toISOString();
2039
2457
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
@@ -2053,6 +2471,13 @@ async function runReparent(ctx, laneId, opts) {
2053
2471
  }),
2054
2472
  };
2055
2473
  });
2474
+ for (const descendant of descendants) {
2475
+ casUpdateLane(hqRoot, descendant.laneId, (current) => {
2476
+ if (current.depth === descendant.depth)
2477
+ return current;
2478
+ return { ...current, depth: descendant.depth };
2479
+ });
2480
+ }
2056
2481
  for (const question of pending) {
2057
2482
  casUpdateQuestion(hqRoot, question.question_id, (current) => {
2058
2483
  const already = current.routing_history.some((entry) => entry.reason === "reparented" && entry.at === at);
@@ -2671,6 +3096,7 @@ async function runReconcile(ctx, opts) {
2671
3096
  throttleDeps: ctx.throttleDeps,
2672
3097
  livenessDeps: livenessDepsFromCtx(ctx),
2673
3098
  presenceConnected: ctx.presenceConnected,
3099
+ mesh: ctx.mesh,
2674
3100
  env: ctx.env,
2675
3101
  });
2676
3102
  printJson(summary);
@@ -2692,7 +3118,7 @@ async function runReconcileInstall(ctx, opts) {
2692
3118
  hqRoot: ctx.hqRoot(),
2693
3119
  enable: Boolean(opts.enable),
2694
3120
  runCommand: opts.enable ? ctx.runSystemctl : undefined,
2695
- pathEnv: providerPath.path,
3121
+ providerDirs: providerPath.resolved_dirs,
2696
3122
  });
2697
3123
  printJson({
2698
3124
  ...result,
@@ -2781,6 +3207,8 @@ export function registerLanesCommand(parent, deps = {}) {
2781
3207
  .requiredOption("--text <msg>", "Message text")
2782
3208
  .option("--project <slug>", "Re-point project attribution from this message")
2783
3209
  .option("--story <id>", "Re-point story attribution from this message")
3210
+ .option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
3211
+ .option("--model <id>", "Model for the moved lane (with --account only)")
2784
3212
  .option("--no-compact", "Skip the resume-time compaction check for this message")
2785
3213
  .action(wrap(async (laneId, opts) => {
2786
3214
  await runMessage(ctx, laneId, opts);
@@ -2792,6 +3220,8 @@ export function registerLanesCommand(parent, deps = {}) {
2792
3220
  .option("--text <msg>", "Operator message for the resumed turn")
2793
3221
  .option("--project <slug>", "Re-point project attribution from this resume")
2794
3222
  .option("--story <id>", "Re-point story attribution from this resume")
3223
+ .option("--account <id>", "Move the lane's session to this account of its provider before resuming")
3224
+ .option("--model <id>", "Model for the moved lane (with --account only)")
2795
3225
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
2796
3226
  .action(wrap(async (laneId, opts) => {
2797
3227
  await runResume(ctx, laneId, opts);