@indigoai-us/hq-cli 5.229.0 → 5.229.2

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.229.2] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - Standing keep-alive leads now wait after done turns for drop-box messages,
10
+ undelivered answers, or pending resumes; queued answers are included in the
11
+ next turn.
12
+ - An unreadable question record no longer aborts reconciliation of other lanes.
13
+
14
+ ## [5.229.1] — 2026-09-27
15
+
16
+ ### Fixed
17
+
18
+ - An authorized `hq lanes resume --worker` on an Outpost re-pins a changed
19
+ worker profile before spawn, preserves the provider session, records the
20
+ hash change in lane history, and syncs the resulting lane record to primary.
21
+
5
22
  ## [5.229.0] — 2026-09-27
6
23
 
7
24
  ### Added
@@ -13,6 +30,8 @@
13
30
  new entity, and to skip glossary or translation table rows, paths and names
14
31
  built from tags or slugs.
15
32
 
33
+ ## [5.228.4] — 2026-09-27
34
+
16
35
  ### Fixed
17
36
 
18
37
  - Account history now distinguishes automatic moves caused by a quota hold, an
@@ -7541,6 +7541,9 @@ export declare const COMMAND_CATALOG: readonly [{
7541
7541
  }, {
7542
7542
  readonly flags: "--off";
7543
7543
  readonly description: "Disable keep-alive for this lane";
7544
+ }, {
7545
+ readonly flags: "--standing";
7546
+ readonly description: "Wait for queued work after every done turn; requires --on";
7544
7547
  }, {
7545
7548
  readonly flags: "--json";
7546
7549
  readonly description: "Emit JSON on stdout (default)";
@@ -9750,6 +9750,10 @@ export const COMMAND_CATALOG = [
9750
9750
  "flags": "--off",
9751
9751
  "description": "Disable keep-alive for this lane"
9752
9752
  },
9753
+ {
9754
+ "flags": "--standing",
9755
+ "description": "Wait for queued work after every done turn; requires --on"
9756
+ },
9753
9757
  {
9754
9758
  "flags": "--json",
9755
9759
  "description": "Emit JSON on stdout (default)"
@@ -25,6 +25,7 @@ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
25
25
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
26
26
  import { defaultTrackerActor, LANES_TRACKER_FLAG, LANES_TRACKER_MESH_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
27
27
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
28
+ import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
28
29
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
29
30
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
30
31
  import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
@@ -3564,6 +3565,10 @@ function parseRemoteJson(stdout, surface) {
3564
3565
  }
3565
3566
  return null;
3566
3567
  }
3568
+ function outpostRejectedProfileRepinOption(result) {
3569
+ return result.exitCode !== 0 &&
3570
+ /unknown option\s+(?:['"])?--profile-repin-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3571
+ }
3567
3572
  function verifyOutpostProfileContent(hqRoot, lane) {
3568
3573
  if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
3569
3574
  return;
@@ -3579,6 +3584,89 @@ function verifyOutpostProfileContent(hqRoot, lane) {
3579
3584
  });
3580
3585
  }
3581
3586
  }
3587
+ function resumeProfileRepinActor(ctx) {
3588
+ const laneId = ctx.env.HQ_LANE_ID?.trim();
3589
+ if (laneId)
3590
+ return `lane:${laneId}`;
3591
+ const sessionId = sessionIdFromEnv(ctx.env);
3592
+ return sessionId ? `session:${sessionId}` : "ssm-owner-dispatch";
3593
+ }
3594
+ function workerProfileHashMismatch(lane, profile) {
3595
+ return new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`, {
3596
+ lane_id: lane.lane_id,
3597
+ worker: profile.id,
3598
+ outpost: lane.host.kind === "outpost" ? lane.host.id : null,
3599
+ expected_hash: lane.worker_profile_hash ?? null,
3600
+ actual_hash: profile.profile.contentHash,
3601
+ });
3602
+ }
3603
+ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3604
+ const ssmOwnerDispatch = ctx.isSsmOwnerDispatch();
3605
+ if (encodedContext !== undefined && !ssmOwnerDispatch) {
3606
+ throw profile === undefined
3607
+ ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id })
3608
+ : workerProfileHashMismatch(lane, profile);
3609
+ }
3610
+ let context;
3611
+ if (encodedContext !== undefined) {
3612
+ try {
3613
+ context = parseOutpostProfileRepinContext(encodedContext, lane.lane_id);
3614
+ }
3615
+ catch {
3616
+ throw profile === undefined
3617
+ ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
3618
+ : workerProfileHashMismatch(lane, profile);
3619
+ }
3620
+ if (lane.host.kind !== "outpost" ||
3621
+ !sameHireGrantSenior(lane.senior, context.expected_senior)) {
3622
+ throw profile === undefined
3623
+ ? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
3624
+ : workerProfileHashMismatch(lane, profile);
3625
+ }
3626
+ }
3627
+ if (profile === undefined ||
3628
+ lane.host.kind !== "outpost" ||
3629
+ lane.worker_profile_hash === profile.profile.contentHash) {
3630
+ return undefined;
3631
+ }
3632
+ if (ssmOwnerDispatch) {
3633
+ if (context === undefined)
3634
+ throw workerProfileHashMismatch(lane, profile);
3635
+ return {
3636
+ kind: "ssm-owner-dispatch",
3637
+ expectedSenior: context.expected_senior,
3638
+ actor: context.actor,
3639
+ reason: context.reason,
3640
+ newHash: profile.profile.contentHash,
3641
+ };
3642
+ }
3643
+ if (context !== undefined || !callerMatchesSenior(ctx, lane.senior)) {
3644
+ throw workerProfileHashMismatch(lane, profile);
3645
+ }
3646
+ return {
3647
+ kind: "senior",
3648
+ expectedSenior: lane.senior,
3649
+ actor: resumeProfileRepinActor(ctx),
3650
+ reason: "resume --worker",
3651
+ newHash: profile.profile.contentHash,
3652
+ };
3653
+ }
3654
+ function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
3655
+ const authorized = current.host.kind === "outpost" &&
3656
+ sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
3657
+ (authorization.kind === "ssm-owner-dispatch"
3658
+ ? ctx.isSsmOwnerDispatch()
3659
+ : callerMatchesSenior(ctx, current.senior));
3660
+ if (!authorized) {
3661
+ throw new LanesError("worker_profile_hash_mismatch", `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`, {
3662
+ lane_id: current.lane_id,
3663
+ expected_senior: authorization.expectedSenior,
3664
+ current_senior: current.senior,
3665
+ expected_hash: current.worker_profile_hash ?? null,
3666
+ actual_hash: authorization.newHash,
3667
+ });
3668
+ }
3669
+ }
3582
3670
  function laneIsHostedHere(ctx, lane) {
3583
3671
  if (lane.host.kind !== "outpost")
3584
3672
  return true;
@@ -3603,6 +3691,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3603
3691
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
3604
3692
  }
3605
3693
  const outpostId = lane.host.id;
3694
+ const profileRepinContext = action === "resume" &&
3695
+ options.worker !== undefined &&
3696
+ callerMatchesSenior(ctx, lane.senior)
3697
+ ? createOutpostProfileRepinContext({
3698
+ laneId: lane.lane_id,
3699
+ expectedSenior: lane.senior,
3700
+ actor: resumeProfileRepinActor(ctx),
3701
+ })
3702
+ : undefined;
3606
3703
  let grantContext;
3607
3704
  let grantHierarchy;
3608
3705
  if (action === "grant") {
@@ -3618,7 +3715,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3618
3715
  }
3619
3716
  let dispatched;
3620
3717
  try {
3621
- dispatched = await ctx.dispatchOutpostExec({
3718
+ const dispatchCommand = (repinContext) => ctx.dispatchOutpostExec({
3622
3719
  outpostId,
3623
3720
  command: buildOutpostLaneCommand({
3624
3721
  action: action === "grant"
@@ -3642,9 +3739,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3642
3739
  senior: options.senior,
3643
3740
  reason: options.reason,
3644
3741
  grantContext,
3742
+ ...(repinContext !== undefined ? { profileRepinContext: repinContext } : {}),
3645
3743
  compact: options.compact,
3646
3744
  }),
3647
3745
  });
3746
+ dispatched = await dispatchCommand(profileRepinContext);
3747
+ if (profileRepinContext !== undefined &&
3748
+ outpostRejectedProfileRepinOption(dispatched)) {
3749
+ dispatched = await dispatchCommand();
3750
+ }
3648
3751
  }
3649
3752
  catch (err) {
3650
3753
  throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
@@ -3663,6 +3766,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3663
3766
  });
3664
3767
  }
3665
3768
  if (ownerError) {
3769
+ if (action === "resume" && ownerError.error === "worker_profile_hash_mismatch") {
3770
+ throw new LanesError(ownerError.error, ownerError.message, {
3771
+ ...ownerError.details,
3772
+ lane_id: lane.lane_id,
3773
+ outpost_id: outpostId,
3774
+ command_id: dispatched.commandId,
3775
+ });
3776
+ }
3666
3777
  throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command: ${ownerError.message}`, {
3667
3778
  lane_id: lane.lane_id,
3668
3779
  outpost_id: outpostId,
@@ -3708,8 +3819,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3708
3819
  : action === "reparent"
3709
3820
  ? remote.lane !== null && typeof remote.lane === "object" &&
3710
3821
  Array.isArray(remote.changed_descendant_ids)
3711
- : remote.delivery_status === "worker_spawned" ||
3712
- remote.delivery_status === "queued_on_target_account");
3822
+ : (remote.delivery_status === "worker_spawned" ||
3823
+ remote.delivery_status === "queued_on_target_account") &&
3824
+ (options.worker === undefined || remote.lane !== undefined));
3713
3825
  if (!resultConfirmed) {
3714
3826
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3715
3827
  lane_id: lane.lane_id,
@@ -3730,19 +3842,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3730
3842
  });
3731
3843
  return;
3732
3844
  }
3733
- if (action === "reparent") {
3845
+ if (action === "reparent" || (action === "resume" && options.worker !== undefined)) {
3734
3846
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3735
3847
  const descendants = [];
3736
- for (const descendantId of changedDescendantIds) {
3737
- const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
3738
- descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
3848
+ if (action === "reparent") {
3849
+ for (const descendantId of changedDescendantIds) {
3850
+ const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
3851
+ descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
3852
+ }
3739
3853
  }
3740
3854
  printJson({
3741
3855
  ...remote,
3742
3856
  lane_id: ownerLane.lane_id,
3743
3857
  lane: ownerLane,
3744
- changed_descendant_ids: changedDescendantIds,
3745
- descendants,
3858
+ ...(action === "reparent"
3859
+ ? { changed_descendant_ids: changedDescendantIds, descendants }
3860
+ : {}),
3746
3861
  outpost_id: outpostId,
3747
3862
  outpost_command_id: dispatched.commandId,
3748
3863
  outpost_transport: "outposts_exec",
@@ -4860,6 +4975,7 @@ async function runShow(ctx, laneId) {
4860
4975
  keep_alive_state: {
4861
4976
  enabled: keepAliveState.enabled,
4862
4977
  source: keepAliveState.source,
4978
+ standing: storedLane.keep_alive_standing === true,
4863
4979
  opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
4864
4980
  blocked_reason: storedLane.keep_alive_blocked_reason ??
4865
4981
  (storedLane.keep_alive === true ? null : "not_opted_in"),
@@ -4989,6 +5105,9 @@ function printGrantResult(hqRoot, updated, hierarchy) {
4989
5105
  printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
4990
5106
  }
4991
5107
  async function runKeepAlive(ctx, laneId, opts) {
5108
+ if (opts.standing && !opts.on) {
5109
+ throw new LanesError("keep_alive_standing_requires_on", "Pass --standing together with --on", { lane_id: laneId });
5110
+ }
4992
5111
  if (Boolean(opts.on) === Boolean(opts.off)) {
4993
5112
  throw new LanesError("keep_alive_mode_invalid", "Pass exactly one of --on or --off", { lane_id: laneId });
4994
5113
  }
@@ -5016,6 +5135,7 @@ async function runKeepAlive(ctx, laneId, opts) {
5016
5135
  return updateLane(hqRoot, laneId, lane.version, "command", (current) => ({
5017
5136
  ...current,
5018
5137
  keep_alive: Boolean(opts.on),
5138
+ keep_alive_standing: Boolean(opts.on && opts.standing),
5019
5139
  keep_alive_opted_in_at: optedInAt,
5020
5140
  keep_alive_blocked_reason: undefined,
5021
5141
  }));
@@ -5248,6 +5368,18 @@ function applyResumeProfileAndEffort(current, next, input) {
5248
5368
  next.worker_pack = input.profile.pack;
5249
5369
  next.worker_profile = profile.id;
5250
5370
  next.worker_profile_hash = profile.contentHash;
5371
+ if (input.profileRepin !== undefined &&
5372
+ current.host.kind === "outpost" &&
5373
+ current.worker_profile_hash !== profile.contentHash) {
5374
+ const entry = {
5375
+ old_hash: current.worker_profile_hash ?? null,
5376
+ new_hash: profile.contentHash,
5377
+ actor: input.profileRepin.actor,
5378
+ at: input.at ?? new Date().toISOString(),
5379
+ reason: input.profileRepin.reason,
5380
+ };
5381
+ next.worker_profile_repin_history = appendCappedHistory(current.worker_profile_repin_history ?? [], entry);
5382
+ }
5251
5383
  next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
5252
5384
  next.worker_policy_delivery = undefined;
5253
5385
  if (profile.meta.keep_alive !== undefined) {
@@ -5308,6 +5440,9 @@ function recordResumeSetup(ctx, lane, input) {
5308
5440
  if (current.version !== lane.version) {
5309
5441
  throw new LanesError("lane_resume_setup_conflict", `Lane ${lane.lane_id} changed while resume settings were prepared; run the resume again`, { lane_id: lane.lane_id, prepared_version: lane.version, version: current.version });
5310
5442
  }
5443
+ if (input.profileRepin !== undefined) {
5444
+ assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
5445
+ }
5311
5446
  const next = { ...current };
5312
5447
  applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
5313
5448
  if (input.cliModel !== undefined) {
@@ -5525,6 +5660,9 @@ async function prepareAccountMove(ctx, lane, input) {
5525
5660
  if (current.version !== lane.version) {
5526
5661
  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 });
5527
5662
  }
5663
+ if (input.profileRepin !== undefined) {
5664
+ assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
5665
+ }
5528
5666
  const next = {
5529
5667
  ...current,
5530
5668
  account: target,
@@ -5549,6 +5687,7 @@ async function prepareAccountMove(ctx, lane, input) {
5549
5687
  };
5550
5688
  applyResumeProfileAndEffort(current, next, {
5551
5689
  ...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
5690
+ ...(input.profileRepin !== undefined ? { profileRepin: input.profileRepin } : {}),
5552
5691
  ...(input.effort !== undefined ? { effort: input.effort } : {}),
5553
5692
  at,
5554
5693
  });
@@ -6410,6 +6549,9 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6410
6549
  async function runResume(ctx, laneId, opts) {
6411
6550
  const hqRoot = ctx.hqRoot();
6412
6551
  let lane = readLane(hqRoot, laneId);
6552
+ if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
6553
+ throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6554
+ }
6413
6555
  if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
6414
6556
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
6415
6557
  return;
@@ -6478,6 +6620,7 @@ async function runResume(ctx, laneId, opts) {
6478
6620
  const resumeProfile = opts.worker !== undefined
6479
6621
  ? resolveResumeProfile(ctx, lane, opts.worker)
6480
6622
  : undefined;
6623
+ const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
6481
6624
  const keepAlive = opts.keepAlive === true
6482
6625
  ? true
6483
6626
  : resumeProfile?.profile.meta.keep_alive;
@@ -6502,6 +6645,7 @@ async function runResume(ctx, laneId, opts) {
6502
6645
  account: opts.account,
6503
6646
  cliModel: opts.model?.trim() || undefined,
6504
6647
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6648
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
6505
6649
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6506
6650
  pending: {
6507
6651
  trigger: "resume",
@@ -6526,6 +6670,7 @@ async function runResume(ctx, laneId, opts) {
6526
6670
  ? { preservedModel: automaticAccountSelection.preservedModel }
6527
6671
  : {}),
6528
6672
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6673
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
6529
6674
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6530
6675
  pending: {
6531
6676
  trigger: "resume",
@@ -6544,6 +6689,7 @@ async function runResume(ctx, laneId, opts) {
6544
6689
  }
6545
6690
  lane = recordResumeSetup(ctx, lane, {
6546
6691
  ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
6692
+ ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
6547
6693
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6548
6694
  ...(keepAlive !== undefined ? { keepAlive } : {}),
6549
6695
  ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
@@ -9188,6 +9334,7 @@ export function registerLanesCommand(parent, deps = {}) {
9188
9334
  .argument("<lane-id>", "Lane id")
9189
9335
  .option("--on", "Enable explicit keep-alive for this lane")
9190
9336
  .option("--off", "Disable keep-alive for this lane")
9337
+ .option("--standing", "Wait for queued work after every done turn; requires --on")
9191
9338
  .option("--json", "Emit JSON on stdout (default)")
9192
9339
  .action(wrap(async (laneId, opts) => {
9193
9340
  await runKeepAlive(ctx, laneId, opts);
@@ -9419,6 +9566,7 @@ export function registerLanesCommand(parent, deps = {}) {
9419
9566
  .option("--model <id>", "Model for the moved lane (with --account only)")
9420
9567
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
9421
9568
  .option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
9569
+ .addOption(new Option("--profile-repin-context <base64url>").hideHelp())
9422
9570
  .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
9423
9571
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
9424
9572
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
10
10
  export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
11
11
  export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
12
12
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
13
- export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
13
+ export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
14
14
  export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
15
15
  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";
16
16
  export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
@@ -1,4 +1,4 @@
1
- import type { LaneRecord } from "./types.js";
1
+ import type { LaneRecord, QuestionRecord } from "./types.js";
2
2
  export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
3
3
  export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
4
4
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
@@ -7,6 +7,21 @@ export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
7
7
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
8
8
  export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
9
9
  export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
10
+ /** The latest terminal envelope fields used by both watch and reconcile. */
11
+ export interface KeepAliveDoneEnvelope {
12
+ decision?: string;
13
+ story_complete?: boolean;
14
+ }
15
+ /**
16
+ * Shared done-state policy: ordinary opted-in lanes resume open stories;
17
+ * standing leads wait for queued work, even when each turn marks the story
18
+ * complete. Watch and reconcile both use this decision.
19
+ */
20
+ export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
21
+ /** Work signals already persisted for a lane that ended its turn. */
22
+ export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
23
+ /** Undelivered answers that belong to this lane and company. */
24
+ export declare function undeliveredAnsweredQuestions(lane: Pick<LaneRecord, "lane_id" | "company">, questions: readonly QuestionRecord[]): QuestionRecord[];
10
25
  export declare function retiredLaneRegistryPath(hqRoot: string): string;
11
26
  /** Missing registry means no retired lanes; every present invalid file fails closed. */
12
27
  export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
+ import { dropboxInboxHasMessages } from "./dropbox.js";
5
+ import { listQuestionIds, readQuestion } from "./store.js";
4
6
  export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
5
7
  export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
6
8
  export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
@@ -8,6 +10,55 @@ export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
8
10
  export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
9
11
  export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
10
12
  export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
13
+ /**
14
+ * Shared done-state policy: ordinary opted-in lanes resume open stories;
15
+ * standing leads wait for queued work, even when each turn marks the story
16
+ * complete. Watch and reconcile both use this decision.
17
+ */
18
+ export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
19
+ if (envelope.decision !== "done")
20
+ return false;
21
+ return lane.keep_alive_standing === true
22
+ ? hasQueuedWork
23
+ : envelope.story_complete !== true;
24
+ }
25
+ /** Work signals already persisted for a lane that ended its turn. */
26
+ export function hasPendingKeepAliveWork(hqRoot, lane, questionRecords) {
27
+ if (lane.pending_resume !== undefined)
28
+ return true;
29
+ if (dropboxInboxHasMessages(hqRoot, lane.lane_id))
30
+ return true;
31
+ const questions = questionRecords ?? readKeepAliveQuestionRecords(hqRoot);
32
+ return undeliveredAnsweredQuestions(lane, questions).length > 0;
33
+ }
34
+ /** Undelivered answers that belong to this lane and company. */
35
+ export function undeliveredAnsweredQuestions(lane, questions) {
36
+ return questions.filter((question) => {
37
+ const sameCompany = question.company === null
38
+ ? lane.company === null
39
+ : lane.company !== null &&
40
+ question.company.uid === lane.company.uid &&
41
+ question.company.slug === lane.company.slug;
42
+ return (question.lane_id === lane.lane_id &&
43
+ question.origin.id === lane.lane_id &&
44
+ sameCompany &&
45
+ question.status === "answered" &&
46
+ question.answer !== undefined &&
47
+ (question.delivered_at === null || question.delivered_at === undefined));
48
+ });
49
+ }
50
+ function readKeepAliveQuestionRecords(hqRoot) {
51
+ const questions = [];
52
+ for (const questionId of listQuestionIds(hqRoot)) {
53
+ try {
54
+ questions.push(readQuestion(hqRoot, questionId));
55
+ }
56
+ catch (err) {
57
+ process.stderr.write(`hq lanes keep-alive: skipped unreadable question ${questionId}: ${err instanceof Error ? err.message : String(err)}\n`);
58
+ }
59
+ }
60
+ return questions;
61
+ }
11
62
  export function retiredLaneRegistryPath(hqRoot) {
12
63
  return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
13
64
  }
@@ -40,6 +40,7 @@ export interface OutpostLaneCommand {
40
40
  keepAlive?: boolean;
41
41
  hire?: string;
42
42
  grantContext?: string;
43
+ profileRepinContext?: string;
43
44
  senior?: string;
44
45
  reason?: string;
45
46
  compact?: boolean;
@@ -150,6 +150,7 @@ export function buildOutpostLaneCommand(input) {
150
150
  ["--worker", input.worker],
151
151
  ["--hire", input.hire],
152
152
  ["--grant-context", input.grantContext],
153
+ ["--profile-repin-context", input.profileRepinContext],
153
154
  ["--senior", input.senior],
154
155
  ["--reason", input.reason],
155
156
  ]) {
@@ -0,0 +1,17 @@
1
+ import { type SeniorRef } from "./types.js";
2
+ declare const PROFILE_REPIN_CONTEXT_SCHEMA: "hq-lane-profile-repin-context.v1";
3
+ export interface OutpostProfileRepinContext {
4
+ schema_version: typeof PROFILE_REPIN_CONTEXT_SCHEMA;
5
+ lane_id: string;
6
+ expected_senior: SeniorRef;
7
+ actor: string;
8
+ reason: "resume --worker";
9
+ }
10
+ export declare function createOutpostProfileRepinContext(input: {
11
+ laneId: string;
12
+ expectedSenior: SeniorRef;
13
+ actor: string;
14
+ }): string;
15
+ export declare function parseOutpostProfileRepinContext(encoded: string, expectedLaneId: string): OutpostProfileRepinContext;
16
+ export {};
17
+ //# sourceMappingURL=outpost-profile-repin.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { LanesError } from "./errors.js";
2
+ import { isSeniorKind } from "./types.js";
3
+ const PROFILE_REPIN_CONTEXT_SCHEMA = "hq-lane-profile-repin-context.v1";
4
+ const MAX_CONTEXT_BYTES = 4 * 1024;
5
+ function contextError(message) {
6
+ return new LanesError("worker_profile_repin_context_invalid", message, {});
7
+ }
8
+ function parseSenior(value) {
9
+ if (value === null || typeof value !== "object" || Array.isArray(value))
10
+ return null;
11
+ const record = value;
12
+ if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
13
+ return null;
14
+ if (!isSeniorKind(record.kind))
15
+ return null;
16
+ if (record.id !== undefined &&
17
+ (typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
18
+ return null;
19
+ if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
20
+ return null;
21
+ if (record.kind === "user" && record.id !== undefined)
22
+ return null;
23
+ return record.id === undefined
24
+ ? { kind: record.kind }
25
+ : { kind: record.kind, id: record.id };
26
+ }
27
+ export function createOutpostProfileRepinContext(input) {
28
+ const context = {
29
+ schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
30
+ lane_id: input.laneId,
31
+ expected_senior: input.expectedSenior.id === undefined
32
+ ? { kind: input.expectedSenior.kind }
33
+ : { kind: input.expectedSenior.kind, id: input.expectedSenior.id },
34
+ actor: input.actor,
35
+ reason: "resume --worker",
36
+ };
37
+ const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
38
+ if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
39
+ throw contextError(`Profile re-pin context for lane ${input.laneId} is too large`);
40
+ }
41
+ return encoded;
42
+ }
43
+ export function parseOutpostProfileRepinContext(encoded, expectedLaneId) {
44
+ if (encoded.length === 0 ||
45
+ encoded.length > MAX_CONTEXT_BYTES ||
46
+ !/^[A-Za-z0-9_-]+$/.test(encoded)) {
47
+ throw contextError("Profile re-pin context must be bounded base64url text");
48
+ }
49
+ let raw;
50
+ try {
51
+ const decoded = Buffer.from(encoded, "base64url");
52
+ if (decoded.toString("base64url") !== encoded) {
53
+ throw new Error("non-canonical base64url");
54
+ }
55
+ raw = JSON.parse(decoded.toString("utf8"));
56
+ }
57
+ catch {
58
+ throw contextError("Profile re-pin context is not valid base64url JSON");
59
+ }
60
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
61
+ throw contextError("Profile re-pin context must be a JSON object");
62
+ }
63
+ const record = raw;
64
+ if (Object.keys(record).some((key) => !["schema_version", "lane_id", "expected_senior", "actor", "reason"].includes(key))) {
65
+ throw contextError("Profile re-pin context contains unknown fields");
66
+ }
67
+ const expectedSenior = parseSenior(record.expected_senior);
68
+ if (record.schema_version !== PROFILE_REPIN_CONTEXT_SCHEMA ||
69
+ record.lane_id !== expectedLaneId ||
70
+ expectedSenior === null ||
71
+ typeof record.actor !== "string" ||
72
+ !record.actor.trim() ||
73
+ record.reason !== "resume --worker") {
74
+ throw contextError(`Profile re-pin context is invalid for lane ${expectedLaneId}`);
75
+ }
76
+ return {
77
+ schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
78
+ lane_id: expectedLaneId,
79
+ expected_senior: expectedSenior,
80
+ actor: record.actor,
81
+ reason: "resume --worker",
82
+ };
83
+ }
84
+ //# sourceMappingURL=outpost-profile-repin.js.map
@@ -21,7 +21,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
21
21
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
22
22
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
23
23
  import { readLanesRegistryFlag } from "./registry-flag.js";
24
- import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, } from "./keep-alive.js";
24
+ import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
25
25
  import { providerSupportsSessionResume } from "./delivery.js";
26
26
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
27
27
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
@@ -818,7 +818,10 @@ export async function runPassBody(hqRoot, deps) {
818
818
  continue;
819
819
  if (lane.state === "done") {
820
820
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
821
- if (!envelope || envelope.decision !== "done" || envelope.story_complete === true)
821
+ if (!envelope || envelope.decision !== "done")
822
+ continue;
823
+ const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
824
+ if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork))
822
825
  continue;
823
826
  }
824
827
  const key = keepAliveCompanyKey(lane.company);
@@ -834,9 +837,9 @@ export async function runPassBody(hqRoot, deps) {
834
837
  }));
835
838
  await withAllLaneAdmissionLocks(hqRoot, async () => {
836
839
  const freshLanes = loadLaneRecords(hqRoot, summary);
837
- resumeKeepAlive(hqRoot, freshLanes, summary, deps, now, keepAliveFlagByCompany);
838
- const drainLanes = loadLaneRecords(hqRoot, summary);
839
840
  const freshQuestions = loadQuestionRecords(hqRoot, summary);
841
+ resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, deps, now, keepAliveFlagByCompany);
842
+ const drainLanes = loadLaneRecords(hqRoot, summary);
840
843
  await admitQueued(hqRoot, drainLanes, summary, deps, now);
841
844
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
842
845
  });
@@ -1532,7 +1535,7 @@ function latestEnvelopeMtimeMs(hqRoot, lane, ref) {
1532
1535
  throw new LanesError("keep_alive_envelope_timestamp_unavailable", `Cannot stat latest envelope for lane ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id, path: filePath });
1533
1536
  }
1534
1537
  }
1535
- function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1538
+ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany) {
1536
1539
  const nowMs = now.getTime();
1537
1540
  const nowIso = now.toISOString();
1538
1541
  const config = loadLanesConfig(hqRoot);
@@ -1566,8 +1569,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1566
1569
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
1567
1570
  if (!envelope || envelope.decision !== "done")
1568
1571
  continue;
1569
- if (envelope.story_complete === true) {
1570
- if (hasKeepAliveProgress(lane)) {
1572
+ const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
1573
+ if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork)) {
1574
+ if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
1571
1575
  casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
1572
1576
  current.state === "done" &&
1573
1577
  current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
@@ -1652,7 +1656,10 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1652
1656
  let eligibilityAtMs;
1653
1657
  if (initialState === "done") {
1654
1658
  const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
1655
- if (!latest || latest.envelope.decision !== "done" || latest.envelope.story_complete === true)
1659
+ if (!latest || latest.envelope.decision !== "done")
1660
+ continue;
1661
+ const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
1662
+ if (!canResumeDoneKeepAliveLane(lane, latest.envelope, hasQueuedWork))
1656
1663
  continue;
1657
1664
  try {
1658
1665
  eligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, lane, latest.ref);
@@ -1726,8 +1733,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1726
1733
  const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
1727
1734
  if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
1728
1735
  continue;
1729
- if (currentEnvelope.envelope.story_complete === true) {
1730
- if (hasKeepAliveProgress(current)) {
1736
+ const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
1737
+ if (!canResumeDoneKeepAliveLane(current, currentEnvelope.envelope, hasQueuedWork)) {
1738
+ if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
1731
1739
  casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1732
1740
  latestLane.state === "done" &&
1733
1741
  latestLane.keep_alive === true &&
@@ -1840,6 +1848,12 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1840
1848
  const resumeText = resumedFrom === "interrupted"
1841
1849
  ? LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT
1842
1850
  : LEAD_LANE_KEEP_ALIVE_RESUME_TEXT;
1851
+ const carriesAnsweredQuestions = resumedFrom === "done" &&
1852
+ current.keep_alive_standing === true &&
1853
+ undeliveredAnsweredQuestions(current, questions).length > 0;
1854
+ if (carriesAnsweredQuestions && !deps.dryRun) {
1855
+ writeAnsweredQuestions(hqRoot, current, questions);
1856
+ }
1843
1857
  const queue = (expectedSeniorId) => casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
1844
1858
  latestLane.state === current.state &&
1845
1859
  latestLane.keep_alive === true &&
@@ -1855,7 +1869,7 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
1855
1869
  terminal_details: undefined,
1856
1870
  pending_resume: {
1857
1871
  session_id: currentSessionId,
1858
- trigger: "resume",
1872
+ trigger: carriesAnsweredQuestions ? "answered_questions" : "resume",
1859
1873
  resumed_from: resumedFrom,
1860
1874
  text: resumeText,
1861
1875
  compact: true,
@@ -409,6 +409,13 @@ export function parseLaneRecord(raw, filePath) {
409
409
  value: raw.keep_alive,
410
410
  });
411
411
  }
412
+ if (raw.keep_alive_standing !== undefined && typeof raw.keep_alive_standing !== "boolean") {
413
+ throw new LaneSchemaError("keep_alive_standing must be a boolean", {
414
+ path: filePath,
415
+ field: "keep_alive_standing",
416
+ value: raw.keep_alive_standing,
417
+ });
418
+ }
412
419
  const keepAliveBlockedReason = raw.keep_alive_blocked_reason;
413
420
  const validKeepAliveBlockedReasons = [
414
421
  "not_opted_in",
@@ -562,6 +569,29 @@ export function parseLaneRecord(raw, filePath) {
562
569
  path: filePath,
563
570
  });
564
571
  }
572
+ if (raw.worker_profile_repin_history !== undefined) {
573
+ if (!Array.isArray(raw.worker_profile_repin_history)) {
574
+ throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
575
+ path: filePath,
576
+ field: "worker_profile_repin_history",
577
+ });
578
+ }
579
+ for (const [index, entry] of raw.worker_profile_repin_history.entries()) {
580
+ const field = `worker_profile_repin_history[${index}]`;
581
+ if (!isPlainObject(entry) || Object.keys(entry).some((key) => !["old_hash", "new_hash", "actor", "at", "reason"].includes(key))) {
582
+ throw new LaneSchemaError(`${field} must be an audit entry`, { path: filePath, field });
583
+ }
584
+ if (entry.old_hash !== null && (typeof entry.old_hash !== "string" || !entry.old_hash.trim())) {
585
+ throw new LaneSchemaError(`${field}.old_hash must be a non-empty string or null`, { path: filePath, field });
586
+ }
587
+ for (const key of ["new_hash", "actor", "reason"]) {
588
+ if (typeof entry[key] !== "string" || !entry[key].trim()) {
589
+ throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
590
+ }
591
+ }
592
+ requireTimestamp(entry.at, `${field}.at`, filePath);
593
+ }
594
+ }
565
595
  if (raw.account_selection !== undefined) {
566
596
  parseLaneAccountSelection(raw.account_selection, filePath);
567
597
  }
@@ -853,6 +883,11 @@ export function mergeLaneRecords(live, sidecars) {
853
883
  account_history: unionHistory(all.map((record) => record.account_history ?? []), entryIdentity, base.account_history ?? []),
854
884
  }
855
885
  : {}),
886
+ ...(all.some((record) => record.worker_profile_repin_history !== undefined)
887
+ ? {
888
+ worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
889
+ }
890
+ : {}),
856
891
  stories: mergeLaneStories(all, base),
857
892
  };
858
893
  }
@@ -283,6 +283,14 @@ export interface SeniorHistoryEntry {
283
283
  by: string;
284
284
  reason: string;
285
285
  }
286
+ /** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
287
+ export interface WorkerProfileRepinHistoryEntry {
288
+ old_hash: string | null;
289
+ new_hash: string;
290
+ actor: string;
291
+ at: string;
292
+ reason: string;
293
+ }
286
294
  export interface AttributionHistoryEntry {
287
295
  project_id: string;
288
296
  story_id: string;
@@ -356,6 +364,8 @@ export interface LaneRecord {
356
364
  worker_profile?: string;
357
365
  /** Whether this lead lane should resume completed turns while its story is open. */
358
366
  keep_alive?: boolean;
367
+ /** Whether a keep-alive lead waits for queued work after every done turn. */
368
+ keep_alive_standing?: boolean;
359
369
  /** Time this lane most recently explicitly opted into keep-alive. */
360
370
  keep_alive_opted_in_at?: string;
361
371
  /** Why reconcile declined to resume this lane most recently. */
@@ -372,6 +382,8 @@ export interface LaneRecord {
372
382
  hire_grant?: number;
373
383
  /** Content hash of the profile last applied to this lane. */
374
384
  worker_profile_hash?: string;
385
+ /** Bounded audit history of authorized Outpost profile hash re-pins. */
386
+ worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
375
387
  /** Profile fields intentionally unsupported by the selected provider. */
376
388
  worker_profile_ignored?: string[];
377
389
  /** How policy files were delivered on the most recent profile spawn. */
@@ -223,8 +223,8 @@ export const LANE_FIELD_WRITERS = {
223
223
  state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
224
224
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
225
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
- priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
- keep_alive: ["creator", "command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
226
+ priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
+ keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
229
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
230
230
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
@@ -1,7 +1,7 @@
1
1
  import { listQuestionIds, readLane, readQuestion, } from "./store.js";
2
2
  import { LaneNotFoundError, LanesError } from "./errors.js";
3
3
  import { providerSupportsSessionResume } from "./delivery.js";
4
- import { LEAD_LANE_KEEP_ALIVE_MAX_RESUMES } from "./keep-alive.js";
4
+ import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
5
5
  import { isTerminalLaneState, } from "./types.js";
6
6
  export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
7
7
  export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
32
32
  }
33
33
  return unique;
34
34
  }
35
- function canResumeKeepAliveLane(lane) {
35
+ function canResumeKeepAliveLane(hqRoot, lane) {
36
36
  if (lane.keep_alive !== true ||
37
37
  lane.keep_alive_opted_in_at === undefined ||
38
38
  !Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
@@ -49,8 +49,7 @@ function canResumeKeepAliveLane(lane) {
49
49
  return lane.terminal_reason === "exited_with_partial_output";
50
50
  }
51
51
  if (lane.state === "done") {
52
- return lane.last_envelope?.decision === "done" &&
53
- lane.story_complete !== true &&
52
+ return canResumeDoneKeepAliveLane(lane, { decision: lane.last_envelope?.decision, story_complete: lane.story_complete }, lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane)) &&
54
53
  lane.terminal_reason !== "provider_rejected" &&
55
54
  lane.terminal_reason !== "account_not_logged_in";
56
55
  }
@@ -133,7 +132,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
133
132
  if (deps.keepAliveEnabled !== undefined) {
134
133
  for (const record of records) {
135
134
  if (isTerminalLaneState(record.state) &&
136
- canResumeKeepAliveLane(record) &&
135
+ canResumeKeepAliveLane(hqRoot, record) &&
137
136
  await deps.keepAliveEnabled(record))
138
137
  keepAliveWaiting.add(record.lane_id);
139
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.229.0",
3
+ "version": "5.229.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {