@indigoai-us/hq-cli 5.166.0 → 5.167.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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.167.0] — 2026-09-24
6
+
7
+ ### Fixed
8
+
9
+ - `hq lanes stop` sends Outpost-hosted stops to the owning host and updates the
10
+ local record after confirmation. If the Outpost already finished the lane,
11
+ the CLI syncs its terminal state.
12
+ - Story updates, reparenting, admission, question creation, and active answer
13
+ delivery return `lane_remote` when another host owns the lane. Message, resume,
14
+ and move keep their Outpost dispatch paths.
15
+
5
16
  ## [5.166.0] — 2026-09-24
6
17
 
7
18
  ### Changed
@@ -23,7 +23,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
23
23
  import { openMeshTransport } from "../lib/mesh/client.js";
24
24
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
25
25
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
26
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
26
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
27
27
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
28
28
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
29
29
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -1119,7 +1119,7 @@ async function assessLaneAdmission(ctx, lane, options) {
1119
1119
  return admission;
1120
1120
  }
1121
1121
  function assertLaneRunsHereForAdmission(ctx, lane) {
1122
- if (laneRunsHere(lane, reconcileHostFromCtx(ctx)))
1122
+ if (laneRunsHereForOperatorMutation(ctx, lane))
1123
1123
  return;
1124
1124
  throw new LanesError("lane_remote", `Lane ${lane.lane_id} is hosted remotely; run the admission command on its Outpost`, { lane_id: lane.lane_id, host: lane.host });
1125
1125
  }
@@ -2382,6 +2382,16 @@ function laneIsHostedHere(ctx, lane) {
2382
2382
  }
2383
2383
  return identity.kind === "id" && identity.id === lane.host.id;
2384
2384
  }
2385
+ function laneRunsHereForOperatorMutation(ctx, lane) {
2386
+ if (lane.host.kind === "outpost")
2387
+ return laneIsHostedHere(ctx, lane);
2388
+ return laneRunsHere(lane, reconcileHostFromCtx(ctx));
2389
+ }
2390
+ function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
2391
+ if (laneRunsHereForOperatorMutation(ctx, lane))
2392
+ return;
2393
+ throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, host: lane.host, action });
2394
+ }
2385
2395
  async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
2386
2396
  if (lane.host.kind !== "outpost") {
2387
2397
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
@@ -2896,8 +2906,7 @@ async function runMove(ctx, laneId, opts) {
2896
2906
  moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
2897
2907
  }
2898
2908
  else {
2899
- const currentHost = laneHostFromCtx(ctx);
2900
- throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${source.host.kind}:${source.host.id})`, { lane_id: laneId, lane_host: source.host, current_host: currentHost });
2909
+ throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
2901
2910
  }
2902
2911
  printJson({
2903
2912
  ok: true,
@@ -2912,12 +2921,8 @@ async function runMove(ctx, laneId, opts) {
2912
2921
  async function commitLaneMove(ctx, laneId, target, account) {
2913
2922
  assertPlacementEnabled(ctx);
2914
2923
  const hqRoot = ctx.hqRoot();
2915
- const sourceHost = laneHostFromCtx(ctx);
2916
- const admissionHost = reconcileHostFromCtx(ctx);
2917
2924
  const before = readLane(hqRoot, laneId);
2918
- if (!laneRunsHere(before, admissionHost)) {
2919
- throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${before.host.kind}:${before.host.id})`, { lane_id: laneId, lane_host: before.host, current_host: sourceHost });
2920
- }
2925
+ assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
2921
2926
  if (before.host.kind === target.kind && before.host.id === target.id) {
2922
2927
  throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
2923
2928
  lane_id: laneId,
@@ -2971,12 +2976,7 @@ async function commitLaneMove(ctx, laneId, target, account) {
2971
2976
  const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
2972
2977
  const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
2973
2978
  const current = readLane(hqRoot, laneId);
2974
- if (!laneRunsHere(current, admissionHost)) {
2975
- throw new LanesError("lane_move_conflict", `Lane ${laneId} changed hosts before the move`, {
2976
- lane_id: laneId,
2977
- host: current.host,
2978
- });
2979
- }
2979
+ assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
2980
2980
  if (!laneCanMoveBeforeStart(current)) {
2981
2981
  throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
2982
2982
  }
@@ -3216,17 +3216,15 @@ async function runAdmit(ctx, laneId, opts) {
3216
3216
  state: lane.state,
3217
3217
  });
3218
3218
  }
3219
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
3219
3220
  const host = reconcileHostFromCtx(ctx);
3220
- if (!laneRunsHere(lane, host)) {
3221
- printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
3222
- return;
3223
- }
3224
3221
  if (lane.pickup_validation_pending === true) {
3225
3222
  printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
3226
3223
  return;
3227
3224
  }
3228
3225
  await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
3229
3226
  lane = readLane(hqRoot, laneId);
3227
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
3230
3228
  if (lane.state !== "queued") {
3231
3229
  throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
3232
3230
  lane_id: laneId,
@@ -3572,6 +3570,7 @@ async function runMessage(ctx, laneId, opts) {
3572
3570
  await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
3573
3571
  return;
3574
3572
  }
3573
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
3575
3574
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
3576
3575
  // An account move is refused on its own terms first: a running lane and a
3577
3576
  // provider without session resume get the move's named errors, not the
@@ -3847,6 +3846,7 @@ async function runResume(ctx, laneId, opts) {
3847
3846
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
3848
3847
  return;
3849
3848
  }
3849
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
3850
3850
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
3851
3851
  if (opts.model !== undefined && opts.account === undefined) {
3852
3852
  throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
@@ -4223,6 +4223,7 @@ async function runStoryList(ctx, laneId) {
4223
4223
  async function runStoryAdd(ctx, laneId, storyId, opts) {
4224
4224
  const hqRoot = ctx.hqRoot();
4225
4225
  let lane = readLane(hqRoot, laneId);
4226
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4226
4227
  const projectId = opts.project?.trim() || lane.project_id;
4227
4228
  if (!projectId)
4228
4229
  throw new LanesError("missing_project", "--project is required", {});
@@ -4232,7 +4233,10 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
4232
4233
  const at = ctx.now().toISOString();
4233
4234
  lane = await ensureStoryThread(ctx, lane, projectId);
4234
4235
  const before = lane;
4235
- const next = casUpdateLane(hqRoot, laneId, (current) => addCarriedStory(current, projectId, normalizedStoryId, at));
4236
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4237
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4238
+ return addCarriedStory(current, projectId, normalizedStoryId, at);
4239
+ });
4236
4240
  await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
4237
4241
  const finalLane = readLane(hqRoot, laneId);
4238
4242
  printJson({
@@ -4249,6 +4253,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
4249
4253
  async function runStoryStart(ctx, laneId, storyId, opts) {
4250
4254
  const hqRoot = ctx.hqRoot();
4251
4255
  let lane = readLane(hqRoot, laneId);
4256
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4252
4257
  const target = storyCommandTarget(lane, storyId, opts.project);
4253
4258
  if (target.story.status === "done") {
4254
4259
  throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
@@ -4260,7 +4265,10 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
4260
4265
  const at = ctx.now().toISOString();
4261
4266
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4262
4267
  const before = lane;
4263
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at));
4268
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4269
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4270
+ return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
4271
+ });
4264
4272
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
4265
4273
  const finalLane = readLane(hqRoot, laneId);
4266
4274
  printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4268,6 +4276,7 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
4268
4276
  async function runStoryBlock(ctx, laneId, storyId, opts) {
4269
4277
  const hqRoot = ctx.hqRoot();
4270
4278
  let lane = readLane(hqRoot, laneId);
4279
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4271
4280
  const target = storyCommandTarget(lane, storyId, opts.project);
4272
4281
  if (target.story.status === "done") {
4273
4282
  throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
@@ -4282,7 +4291,10 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
4282
4291
  const at = ctx.now().toISOString();
4283
4292
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4284
4293
  const before = lane;
4285
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason }));
4294
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4295
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4296
+ return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
4297
+ });
4286
4298
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
4287
4299
  const finalLane = readLane(hqRoot, laneId);
4288
4300
  printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4290,6 +4302,7 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
4290
4302
  async function runStoryDone(ctx, laneId, storyId, opts) {
4291
4303
  const hqRoot = ctx.hqRoot();
4292
4304
  let lane = readLane(hqRoot, laneId);
4305
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4293
4306
  if (!opts.verified) {
4294
4307
  throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
4295
4308
  }
@@ -4311,10 +4324,13 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
4311
4324
  const at = ctx.now().toISOString();
4312
4325
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4313
4326
  const before = lane;
4314
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
4315
- verified_at: at,
4316
- verified_by: current.senior,
4317
- }));
4327
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4328
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4329
+ return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
4330
+ verified_at: at,
4331
+ verified_by: current.senior,
4332
+ });
4333
+ });
4318
4334
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
4319
4335
  const finalLane = readLane(hqRoot, laneId);
4320
4336
  printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4354,6 +4370,7 @@ async function runReparent(ctx, laneId, opts) {
4354
4370
  const nextSenior = parseSeniorRef(opts.senior);
4355
4371
  assertSeniorExists(hqRoot, nextSenior, ctx);
4356
4372
  const lane = readLane(hqRoot, laneId);
4373
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
4357
4374
  if (isTerminalLaneState(lane.state)) {
4358
4375
  throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
4359
4376
  }
@@ -4378,6 +4395,7 @@ async function runReparent(ctx, laneId, opts) {
4378
4395
  continue;
4379
4396
  if (!walkHitsLane(hqRoot, other.senior, laneId, ctx))
4380
4397
  continue;
4398
+ assertLaneRunsHereForOperatorMutation(ctx, other, "reparent descendant of");
4381
4399
  const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
4382
4400
  const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
4383
4401
  if (descendantDepth > MAX_LANE_DEPTH) {
@@ -4395,6 +4413,7 @@ async function runReparent(ctx, laneId, opts) {
4395
4413
  }
4396
4414
  const at = ctx.now().toISOString();
4397
4415
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
4416
+ assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
4398
4417
  const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
4399
4418
  if (already && current.depth === newDepth)
4400
4419
  return current;
@@ -4413,6 +4432,7 @@ async function runReparent(ctx, laneId, opts) {
4413
4432
  });
4414
4433
  for (const descendant of descendants) {
4415
4434
  casUpdateLane(hqRoot, descendant.laneId, (current) => {
4435
+ assertLaneRunsHereForOperatorMutation(ctx, current, "reparent descendant of");
4416
4436
  if (current.depth === descendant.depth)
4417
4437
  return current;
4418
4438
  return { ...current, depth: descendant.depth };
@@ -4487,14 +4507,226 @@ async function runLogs(ctx, laneId, opts) {
4487
4507
  export function stopCommittedState(state) {
4488
4508
  return state === "queued" ? "cancelled" : "killed";
4489
4509
  }
4510
+ function parseConfirmedOutpostStopLane(raw, laneId, outpostId, commandId) {
4511
+ let remoteLane;
4512
+ try {
4513
+ remoteLane = parseLaneRecord(raw);
4514
+ }
4515
+ catch (err) {
4516
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane snapshot for stop ${laneId}`, {
4517
+ lane_id: laneId,
4518
+ outpost_id: outpostId,
4519
+ command_id: commandId,
4520
+ cause: err instanceof Error ? err.message : String(err),
4521
+ });
4522
+ }
4523
+ if (remoteLane.lane_id !== laneId ||
4524
+ remoteLane.host.kind !== "outpost" ||
4525
+ remoteLane.host.id !== outpostId ||
4526
+ !isTerminalLaneState(remoteLane.state)) {
4527
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return its terminal lane ${laneId}`, {
4528
+ lane_id: laneId,
4529
+ outpost_id: outpostId,
4530
+ command_id: commandId,
4531
+ owner_lane_id: remoteLane.lane_id,
4532
+ owner_host: remoteLane.host,
4533
+ owner_state: remoteLane.state,
4534
+ });
4535
+ }
4536
+ return remoteLane;
4537
+ }
4538
+ function parseRemoteCancelledQuestionIds(value, laneId, outpostId, commandId) {
4539
+ if (!Array.isArray(value) ||
4540
+ value.some((id) => typeof id !== "string" || id.trim() === "") ||
4541
+ new Set(value).size !== value.length) {
4542
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned invalid cancelled question ids for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: commandId });
4543
+ }
4544
+ return value;
4545
+ }
4546
+ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
4547
+ let dispatched;
4548
+ try {
4549
+ dispatched = await ctx.dispatchOutpostExec({
4550
+ outpostId,
4551
+ command: buildOutpostLaneCommand({ action: "show", laneId }),
4552
+ timeoutSeconds: 120,
4553
+ waitMs: 120_000,
4554
+ });
4555
+ }
4556
+ catch (err) {
4557
+ throw new LanesError("outpost_delivery_unconfirmed", `Could not read terminal lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, {
4558
+ lane_id: laneId,
4559
+ outpost_id: outpostId,
4560
+ command_id: stopCommandId,
4561
+ sync_command_id: null,
4562
+ });
4563
+ }
4564
+ const commandId = dispatched.commandId?.trim();
4565
+ if (dispatched.done !== true ||
4566
+ !commandId ||
4567
+ dispatched.status !== "Success" ||
4568
+ dispatched.exitCode !== 0) {
4569
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
4570
+ lane_id: laneId,
4571
+ outpost_id: outpostId,
4572
+ command_id: stopCommandId,
4573
+ sync_command_id: commandId || null,
4574
+ status: dispatched.status,
4575
+ done: dispatched.done ?? false,
4576
+ exit_code: dispatched.exitCode,
4577
+ });
4578
+ }
4579
+ let remote;
4580
+ try {
4581
+ remote = parseRemoteJson(dispatched.stdout, "Outpost lane show");
4582
+ }
4583
+ catch (err) {
4584
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane read after stop ${laneId}`, {
4585
+ lane_id: laneId,
4586
+ outpost_id: outpostId,
4587
+ command_id: stopCommandId,
4588
+ sync_command_id: commandId,
4589
+ cause: err instanceof Error ? err.message : String(err),
4590
+ });
4591
+ }
4592
+ if (remote?.ok !== true || remote.action !== "show") {
4593
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
4594
+ lane_id: laneId,
4595
+ outpost_id: outpostId,
4596
+ command_id: stopCommandId,
4597
+ sync_command_id: commandId,
4598
+ });
4599
+ }
4600
+ return {
4601
+ lane: parseConfirmedOutpostStopLane(remote.lane, laneId, outpostId, commandId),
4602
+ commandId,
4603
+ };
4604
+ }
4605
+ async function dispatchOutpostLaneStop(ctx, lane) {
4606
+ if (lane.host.kind !== "outpost") {
4607
+ throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
4608
+ }
4609
+ const outpostId = lane.host.id;
4610
+ let dispatched;
4611
+ try {
4612
+ dispatched = await ctx.dispatchOutpostExec({
4613
+ outpostId,
4614
+ command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id }),
4615
+ timeoutSeconds: 120,
4616
+ waitMs: 120_000,
4617
+ });
4618
+ }
4619
+ catch (err) {
4620
+ throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
4621
+ }
4622
+ const commandId = dispatched.commandId?.trim();
4623
+ if (dispatched.done !== true || !commandId) {
4624
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
4625
+ lane_id: lane.lane_id,
4626
+ outpost_id: outpostId,
4627
+ command_id: commandId || null,
4628
+ status: dispatched.status,
4629
+ done: dispatched.done ?? false,
4630
+ exit_code: dispatched.exitCode,
4631
+ });
4632
+ }
4633
+ const remote = parseRemoteJson(dispatched.stdout, "Outpost lane stop");
4634
+ let state;
4635
+ let alreadyTerminal = false;
4636
+ let ownerLane;
4637
+ let cancelledQuestions;
4638
+ let syncCommandId;
4639
+ if (remote?.error === "already_terminal") {
4640
+ if (remote.lane_id !== lane.lane_id ||
4641
+ !isTerminalLaneState(remote.state) ||
4642
+ (dispatched.status !== "Success" && dispatched.status !== "Failed") ||
4643
+ dispatched.exitCode === null ||
4644
+ dispatched.exitCode === 0) {
4645
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid already-terminal result for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: commandId });
4646
+ }
4647
+ state = remote.state;
4648
+ alreadyTerminal = true;
4649
+ const owner = await readOutpostTerminalLane(ctx, lane.lane_id, outpostId, commandId);
4650
+ if (owner.lane.state !== state) {
4651
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned a different terminal state for lane ${lane.lane_id}`, {
4652
+ lane_id: lane.lane_id,
4653
+ outpost_id: outpostId,
4654
+ command_id: commandId,
4655
+ sync_command_id: owner.commandId,
4656
+ stop_state: state,
4657
+ owner_state: owner.lane.state,
4658
+ });
4659
+ }
4660
+ ownerLane = owner.lane;
4661
+ cancelledQuestions = [];
4662
+ syncCommandId = owner.commandId;
4663
+ }
4664
+ else if (remote?.error !== undefined) {
4665
+ throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
4666
+ ? remote.message
4667
+ : `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
4668
+ lane_id: lane.lane_id,
4669
+ outpost_id: outpostId,
4670
+ command_id: commandId,
4671
+ remote_error: remote.error,
4672
+ });
4673
+ }
4674
+ else {
4675
+ const remoteLane = remote?.lane;
4676
+ const remoteLaneRecord = remoteLane !== null &&
4677
+ typeof remoteLane === "object" &&
4678
+ !Array.isArray(remoteLane)
4679
+ ? remoteLane
4680
+ : null;
4681
+ if (dispatched.status !== "Success" ||
4682
+ dispatched.exitCode !== 0 ||
4683
+ remote?.ok !== true ||
4684
+ remote.action !== "stop" ||
4685
+ remoteLaneRecord?.lane_id !== lane.lane_id ||
4686
+ (remoteLaneRecord.state !== "cancelled" && remoteLaneRecord.state !== "killed")) {
4687
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the stop for lane ${lane.lane_id}`, {
4688
+ lane_id: lane.lane_id,
4689
+ outpost_id: outpostId,
4690
+ command_id: commandId,
4691
+ status: dispatched.status,
4692
+ exit_code: dispatched.exitCode,
4693
+ });
4694
+ }
4695
+ ownerLane = parseConfirmedOutpostStopLane(remoteLaneRecord, lane.lane_id, outpostId, commandId);
4696
+ state = ownerLane.state;
4697
+ cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
4698
+ }
4699
+ const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
4700
+ printJson({
4701
+ ok: true,
4702
+ action: "stop",
4703
+ lane: updated,
4704
+ cancelled_questions: cancelledQuestions,
4705
+ outpost_id: outpostId,
4706
+ outpost_command_id: commandId,
4707
+ ...(syncCommandId ? { outpost_sync_command_id: syncCommandId } : {}),
4708
+ outpost_result: {
4709
+ state,
4710
+ already_terminal: alreadyTerminal,
4711
+ status: dispatched.status,
4712
+ exit_code: dispatched.exitCode,
4713
+ },
4714
+ });
4715
+ }
4490
4716
  async function runStop(ctx, laneId) {
4491
4717
  const hqRoot = ctx.hqRoot();
4492
4718
  const lane = readLane(hqRoot, laneId);
4719
+ if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
4720
+ await dispatchOutpostLaneStop(ctx, lane);
4721
+ return;
4722
+ }
4723
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
4493
4724
  if (isTerminalLaneState(lane.state)) {
4494
4725
  throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
4495
4726
  }
4496
4727
  const at = ctx.now().toISOString();
4497
4728
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
4729
+ assertLaneRunsHereForOperatorMutation(ctx, current, "stop");
4498
4730
  if (isTerminalLaneState(current.state)) {
4499
4731
  throw new LanesError("already_terminal", `Lane ${laneId} is already ${current.state}`, { lane_id: laneId, state: current.state });
4500
4732
  }
@@ -4531,6 +4763,7 @@ async function runAsk(ctx, opts) {
4531
4763
  throw new LanesError("missing_question", "--question is required", {});
4532
4764
  }
4533
4765
  const lane = readLane(hqRoot, laneId);
4766
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
4534
4767
  const optionList = Array.isArray(opts.option)
4535
4768
  ? opts.option
4536
4769
  : opts.option
@@ -4659,6 +4892,10 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
4659
4892
  const question = readQuestion(hqRoot, questionId);
4660
4893
  throwIfQuestionNotPending(question);
4661
4894
  await yieldEventLoop();
4895
+ const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
4896
+ if (laneAcceptsDropboxDelivery(askingLane)) {
4897
+ assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
4898
+ }
4662
4899
  const body = requireQuestionBody(hqRoot, question);
4663
4900
  // A trimmed-empty --freeform is absent: it must not win over --choice and
4664
4901
  // must not become a freeform answer of "".
@@ -4700,7 +4937,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
4700
4937
  },
4701
4938
  };
4702
4939
  });
4703
- const delivery = deliverAnswerToAskingLane(hqRoot, updated, choice, at);
4940
+ const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
4704
4941
  printJson({
4705
4942
  ok: true,
4706
4943
  action: "questions.answer",
@@ -4729,13 +4966,15 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
4729
4966
  * Record is already durable. Delivery is best-effort onto the asking lane's
4730
4967
  * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
4731
4968
  */
4732
- function deliverAnswerToAskingLane(hqRoot, question, choice, at) {
4969
+ function deliverAnswerToAskingLane(ctx, question, choice, at) {
4970
+ const hqRoot = ctx.hqRoot();
4733
4971
  const laneId = laneIdForAnswerDelivery(question);
4734
4972
  try {
4735
4973
  const lane = readLane(hqRoot, laneId);
4736
4974
  if (!laneAcceptsDropboxDelivery(lane)) {
4737
4975
  return { status: "recorded_only" };
4738
4976
  }
4977
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
4739
4978
  appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
4740
4979
  return { status: "dropped" };
4741
4980
  }
@@ -16,7 +16,7 @@ export { resolveLanesHqRoot } from "./hq-root.js";
16
16
  export type { LanesHqRootOpts } from "./hq-root.js";
17
17
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
18
18
  export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
19
- export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
19
+ export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
20
20
  export type { StoreDeps } from "./store.js";
21
21
  export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
22
22
  export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
@@ -10,7 +10,7 @@ export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory,
10
10
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
11
11
  export { resolveLanesHqRoot } from "./hq-root.js";
12
12
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
13
- export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
13
+ export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
14
14
  export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
15
15
  export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
16
16
  export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume";
30
+ action: "message" | "resume" | "stop" | "show";
31
31
  laneId: string;
32
32
  text?: string;
33
33
  project?: string;
@@ -54,6 +54,12 @@ export declare function tryReadQuestion(hqRoot: string, questionId: string, deps
54
54
  export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
55
55
  export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
56
56
  export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
57
+ /**
58
+ * Replace the primary copy with a terminal snapshot returned by the Outpost
59
+ * that owns the lane. The owner is authoritative on equal versions; a newer
60
+ * primary version is a conflict and must not be overwritten.
61
+ */
62
+ export declare function syncLaneRecordFromOwner(hqRoot: string, laneId: string, outpostId: string, rawOwnerLane: unknown, deps?: StoreDeps): LaneRecord;
57
63
  /** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
58
64
  export declare function retryUpdateLane(hqRoot: string, laneId: string, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps, attempts?: number): LaneRecord;
59
65
  /** Compare-and-set with conflict retry for question records (spec §6.2). */
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { generateUlid } from "../mesh/live/ulid.js";
14
14
  import { resolveLanesHqRoot } from "./hq-root.js";
15
15
  import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
16
- import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
16
+ import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
17
17
  import { laneStories } from "./stories.js";
18
18
  export const LANES_STORE_REL = path.join("workspace", "lanes");
19
19
  export const LANE_RECORDS_DIR = "lanes";
@@ -810,6 +810,71 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
810
810
  return updated;
811
811
  }, deps);
812
812
  }
813
+ /**
814
+ * Replace the primary copy with a terminal snapshot returned by the Outpost
815
+ * that owns the lane. The owner is authoritative on equal versions; a newer
816
+ * primary version is a conflict and must not be overwritten.
817
+ */
818
+ export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane, deps = defaultStoreDeps()) {
819
+ let ownerLane;
820
+ try {
821
+ ownerLane = parseLaneRecord(rawOwnerLane);
822
+ }
823
+ catch (err) {
824
+ throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${laneId}`, {
825
+ lane_id: laneId,
826
+ outpost_id: outpostId,
827
+ cause: err instanceof Error ? err.message : String(err),
828
+ });
829
+ }
830
+ if (ownerLane.lane_id !== laneId ||
831
+ ownerLane.host.kind !== "outpost" ||
832
+ ownerLane.host.id !== outpostId ||
833
+ !isTerminalLaneState(ownerLane.state)) {
834
+ throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} did not return its terminal record for lane ${laneId}`, {
835
+ lane_id: laneId,
836
+ outpost_id: outpostId,
837
+ owner_lane_id: ownerLane.lane_id,
838
+ owner_host: ownerLane.host,
839
+ owner_state: ownerLane.state,
840
+ });
841
+ }
842
+ const filePath = laneRecordPath(hqRoot, laneId);
843
+ return withRecordLock(filePath, deps, () => {
844
+ const current = readMergedRecord(filePath, parseLaneRecord, mergeLaneRecords, deps);
845
+ if (current === null) {
846
+ throw new LaneSchemaError(`Record disappeared under lock: ${filePath}`, {
847
+ path: filePath,
848
+ });
849
+ }
850
+ if (current.lane_id !== laneId ||
851
+ current.host.kind !== "outpost" ||
852
+ current.host.id !== outpostId) {
853
+ throw new LanesError("outpost_lane_sync_conflict", `Primary lane ${laneId} is no longer owned by Outpost ${outpostId}`, {
854
+ lane_id: laneId,
855
+ outpost_id: outpostId,
856
+ current_lane_id: current.lane_id,
857
+ current_host: current.host,
858
+ });
859
+ }
860
+ if (ownerLane.version < current.version) {
861
+ throw new LanesError("outpost_lane_sync_conflict", `Outpost ${outpostId} returned lane ${laneId} at version ${ownerLane.version}, behind primary version ${current.version}`, {
862
+ lane_id: laneId,
863
+ outpost_id: outpostId,
864
+ owner_version: ownerLane.version,
865
+ primary_version: current.version,
866
+ });
867
+ }
868
+ const ownerSnapshot = ownerLane.outpost_command_id === undefined &&
869
+ current.outpost_command_id !== undefined
870
+ ? { ...ownerLane, outpost_command_id: current.outpost_command_id }
871
+ : ownerLane;
872
+ const synced = mergeLaneRecords(ownerSnapshot, [current]);
873
+ parseLaneRecord(synced, filePath);
874
+ atomicWriteJson(filePath, synced, deps);
875
+ return synced;
876
+ });
877
+ }
813
878
  /** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
814
879
  export function retryUpdateLane(hqRoot, laneId, writer, updater, deps = defaultStoreDeps(), attempts = 5) {
815
880
  let last;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.166.0",
3
+ "version": "5.167.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {