@indigoai-us/hq-cli 5.149.1 → 5.151.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.
@@ -18,7 +18,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
18
18
  import { openMeshTransport } from "../lib/mesh/client.js";
19
19
  import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
20
20
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
21
- import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, MAX_LANE_DEPTH, 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, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
21
+ 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, isLoggedOut, 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";
22
22
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
23
23
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
24
24
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -840,36 +840,25 @@ function isOverdue(hqRoot, laneId, now) {
840
840
  return false;
841
841
  return now.getTime() > at;
842
842
  }
843
- function compareQueueOrder(a, b) {
844
- if (b.priority !== a.priority)
845
- return b.priority - a.priority;
846
- return a.created_at.localeCompare(b.created_at);
847
- }
848
843
  function admitCreate(input) {
849
- const { hqRoot, provider, pinnedAccount, profileId, profileMaxConcurrent, priority, createdAt, config, } = input;
844
+ const { hqRoot, provider, pinnedAccount, profileId, profileMaxConcurrent, host, priority, createdAt, nowMs, config, } = input;
850
845
  const providerCfg = config.providers[provider];
851
846
  const lanes = loadAllLanes(hqRoot).filter((lane) => lane.lane_id !== input.excludeLaneId);
852
- const runningProvider = lanes.filter((lane) => lane.provider === provider && lane.state === "running");
853
- const queuedSame = lanes.filter((lane) => {
854
- if (lane.provider !== provider || lane.state !== "queued")
855
- return false;
856
- if (pinnedAccount)
857
- return lane.account === pinnedAccount;
858
- return !lane.account;
859
- });
847
+ const localLanes = lanes.filter((lane) => laneRunsHere(lane, host));
848
+ const runningProvider = lanes.filter((lane) => lane.provider === provider &&
849
+ (lane.state === "running" ||
850
+ hasActiveAdmissionReservation(lane, nowMs) ||
851
+ hasActivePendingResume(lane, nowMs)) &&
852
+ laneRunsHere(lane, host));
853
+ const queuedProvider = localLanes.filter((lane) => lane.provider === provider &&
854
+ lane.state === "queued" &&
855
+ !hasActiveAdmissionReservation(lane, nowMs));
860
856
  const runningCount = pinnedAccount
861
857
  ? runningProvider.filter((lane) => lane.account === pinnedAccount).length
862
858
  : runningProvider.length;
863
859
  const providerRunning = runningProvider.length;
864
- const queuedCount = queuedSame.length;
860
+ const queuedCount = queuedProvider.length;
865
861
  const maxQueued = providerCfg.max_queued;
866
- const profileRunning = profileId === undefined
867
- ? 0
868
- : lanes.filter((lane) => lane.worker_profile === profileId && lane.state === "running").length;
869
- const atCap = providerRunning >= providerCfg.max_running ||
870
- (pinnedAccount !== undefined &&
871
- runningCount >= accountMaxRunning(providerCfg, pinnedAccount)) ||
872
- (profileMaxConcurrent !== undefined && profileRunning >= profileMaxConcurrent);
873
862
  const synthetic = {
874
863
  schema_version: "hq-lane.v1",
875
864
  lane_id: "__incoming__",
@@ -883,19 +872,24 @@ function admitCreate(input) {
883
872
  priority,
884
873
  provider,
885
874
  account: pinnedAccount,
886
- host: { kind: "local", id: "local" },
875
+ host: host.outpostId
876
+ ? { kind: "outpost", id: host.outpostId }
877
+ : { kind: "local", id: host.hostname },
878
+ ...(profileId !== undefined ? { worker_profile: profileId } : {}),
887
879
  version: 0,
888
880
  created_at: createdAt,
889
881
  updated_at: createdAt,
890
882
  attribution_history: [],
891
883
  };
892
- const ranked = [...queuedSame, synthetic].sort(compareQueueOrder);
893
- const first = ranked[0];
894
- const isHead = first?.lane_id === "__incoming__";
895
- const wouldStart = !atCap && isHead;
896
- if (wouldStart)
884
+ const capReason = laneAdmissionCapReason(localLanes, synthetic, config, host, profileMaxConcurrent, nowMs);
885
+ const earlierEligible = !capReason && hasEarlierEligibleQueuedLane(localLanes, synthetic, config, host, (lane) => lane.worker_profile === undefined
886
+ ? undefined
887
+ : loadWorkerProfile(hqRoot, lane.worker_profile).meta.max_concurrent, nowMs);
888
+ const queuedReason = capReason ??
889
+ (earlierEligible ? "queue_position" : undefined);
890
+ if (!queuedReason)
897
891
  return { action: "start" };
898
- if (queuedCount >= maxQueued) {
892
+ if (!input.alreadyQueued && queuedCount >= maxQueued) {
899
893
  return {
900
894
  action: "refuse",
901
895
  error: new LanesError("lane_queue_full", `Lane queue is full for ${pinnedAccount ? `account ${pinnedAccount}` : provider}`, {
@@ -906,27 +900,38 @@ function admitCreate(input) {
906
900
  }),
907
901
  };
908
902
  }
909
- if (atCap) {
910
- return { action: "queue", reason: isHead ? "cap" : "queue_position" };
911
- }
912
- return { action: "queue", reason: "queue_position" };
903
+ return { action: "queue", reason: queuedReason };
913
904
  }
914
- async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccount) {
915
- if (current.action !== "start")
905
+ async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccount, options = {}) {
906
+ if (current.action !== "start" &&
907
+ !(current.action === "queue" && current.reason === "queue_position"))
916
908
  return current;
917
909
  const { decision } = await decideAdmission(hqRoot, config.throttle, ctx.throttleDeps);
918
910
  if (decision.admit)
919
911
  return current;
912
+ if (current.action === "queue")
913
+ return { action: "queue", reason: "throttle" };
920
914
  const providerCfg = config.providers[provider];
915
+ const host = options.host ?? reconcileHostFromCtx(ctx);
916
+ const nowMs = options.nowMs ?? ctx.now().getTime();
921
917
  const queuedCount = loadAllLanes(hqRoot).filter((lane) => {
922
- if (lane.provider !== provider || lane.state !== "queued")
918
+ if (lane.lane_id === options.excludeLaneId ||
919
+ lane.provider !== provider ||
920
+ lane.state !== "queued" ||
921
+ hasActiveAdmissionReservation(lane, nowMs) ||
922
+ !laneRunsHere(lane, host))
923
923
  return false;
924
924
  if (pinnedAccount)
925
925
  return lane.account === pinnedAccount;
926
926
  return !lane.account;
927
927
  }).length;
928
- if (queuedCount >= providerCfg.max_queued) {
929
- const running = loadAllLanes(hqRoot).filter((lane) => lane.provider === provider && lane.state === "running").length;
928
+ if (!options.alreadyQueued && queuedCount >= providerCfg.max_queued) {
929
+ const running = loadAllLanes(hqRoot).filter((lane) => lane.lane_id !== options.excludeLaneId &&
930
+ lane.provider === provider &&
931
+ (lane.state === "running" ||
932
+ hasActiveAdmissionReservation(lane, nowMs) ||
933
+ hasActivePendingResume(lane, nowMs)) &&
934
+ laneRunsHere(lane, host)).length;
930
935
  return {
931
936
  action: "refuse",
932
937
  error: new LanesError("lane_queue_full", `Lane queue is full for ${pinnedAccount ? `account ${pinnedAccount}` : provider}`, {
@@ -939,6 +944,45 @@ async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccou
939
944
  }
940
945
  return { action: "queue", reason: "throttle" };
941
946
  }
947
+ async function assessLaneAdmission(ctx, lane, options) {
948
+ const hqRoot = ctx.hqRoot();
949
+ const config = loadLanesConfig(hqRoot);
950
+ const account = options.account ?? lane.account;
951
+ const profile = lane.worker_profile
952
+ ? loadWorkerProfile(hqRoot, lane.worker_profile)
953
+ : undefined;
954
+ const nowMs = ctx.now().getTime();
955
+ let admission = admitCreate({
956
+ hqRoot,
957
+ provider: lane.provider,
958
+ pinnedAccount: account,
959
+ ...(profile
960
+ ? { profileId: profile.id, profileMaxConcurrent: profile.meta.max_concurrent }
961
+ : {}),
962
+ host: reconcileHostFromCtx(ctx),
963
+ priority: lane.priority,
964
+ createdAt: options.queueAt,
965
+ nowMs,
966
+ config,
967
+ excludeLaneId: lane.lane_id,
968
+ alreadyQueued: options.alreadyQueued,
969
+ });
970
+ if (admission.action === "start" ||
971
+ (admission.action === "queue" && admission.reason === "queue_position")) {
972
+ admission = await applyThrottle(hqRoot, config, ctx, admission, lane.provider, account, {
973
+ host: reconcileHostFromCtx(ctx),
974
+ excludeLaneId: lane.lane_id,
975
+ alreadyQueued: options.alreadyQueued,
976
+ nowMs,
977
+ });
978
+ }
979
+ return admission;
980
+ }
981
+ function assertLaneRunsHereForAdmission(ctx, lane) {
982
+ if (laneRunsHere(lane, reconcileHostFromCtx(ctx)))
983
+ return;
984
+ 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 });
985
+ }
942
986
  function assertKnownOutpost(requested, known) {
943
987
  if (!known.includes(requested)) {
944
988
  throw new LanesError("unknown_outpost", `Unknown Outpost ${JSON.stringify(requested)}. Accepted: ${known.join(", ") || "(none)"}`, { accepted: known, value: requested });
@@ -954,8 +998,6 @@ function copyHookRails(fromHome, toHome) {
954
998
  function railsPresent(home) {
955
999
  return fs.existsSync(path.join(home, "hooks"));
956
1000
  }
957
- const ADMISSION_LOCK_STALE_MS = 30_000;
958
- const ADMISSION_LOCK_TIMEOUT_MS = 5_000;
959
1001
  /**
960
1002
  * Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
961
1003
  * being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
@@ -969,83 +1011,6 @@ function yieldEventLoop() {
969
1011
  setImmediate(resolve);
970
1012
  });
971
1013
  }
972
- function admissionLockPath(hqRoot, provider, pinnedAccount) {
973
- const name = pinnedAccount
974
- ? `${provider}-${pinnedAccount}`
975
- : `${provider}-unpinned`;
976
- return path.join(hqRoot, LANES_RUNS_REL, ".admission-locks", `${name}.lock`);
977
- }
978
- async function withAdmissionLock(lockPath, fn, sleep) {
979
- ensureDir(path.dirname(lockPath));
980
- const started = Date.now();
981
- while (true) {
982
- let fd;
983
- try {
984
- fd = fs.openSync(lockPath, "wx");
985
- try {
986
- fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n`, "utf8"));
987
- }
988
- catch (err) {
989
- fs.closeSync(fd);
990
- fd = undefined;
991
- throw err;
992
- }
993
- try {
994
- await yieldEventLoop();
995
- return await fn();
996
- }
997
- finally {
998
- try {
999
- fs.closeSync(fd);
1000
- }
1001
- catch (err) {
1002
- process.stderr.write(`hq lanes: admission lock close failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
1003
- }
1004
- try {
1005
- fs.unlinkSync(lockPath);
1006
- }
1007
- catch (err) {
1008
- const code = err.code;
1009
- if (code !== "ENOENT") {
1010
- process.stderr.write(`hq lanes: admission lock unlink failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
1011
- }
1012
- }
1013
- }
1014
- }
1015
- catch (err) {
1016
- const code = err.code;
1017
- if (code !== "EEXIST")
1018
- throw err;
1019
- if (Date.now() - started > ADMISSION_LOCK_TIMEOUT_MS) {
1020
- throw new LanesError("store_lock_timeout", `Timed out waiting for admission lock ${lockPath}`, { path: lockPath });
1021
- }
1022
- try {
1023
- const raw = fs.readFileSync(lockPath, "utf8");
1024
- const tsLine = raw.split("\n")[1];
1025
- const ts = tsLine ? Number(tsLine) : NaN;
1026
- if (Number.isFinite(ts) && Date.now() - ts > ADMISSION_LOCK_STALE_MS) {
1027
- try {
1028
- fs.unlinkSync(lockPath);
1029
- }
1030
- catch (unlinkErr) {
1031
- const unlinkCode = unlinkErr.code;
1032
- if (unlinkCode !== "ENOENT")
1033
- throw unlinkErr;
1034
- }
1035
- continue;
1036
- }
1037
- }
1038
- catch (statErr) {
1039
- const statCode = statErr.code;
1040
- if (statCode !== "ENOENT") {
1041
- process.stderr.write(`hq lanes: stale admission-lock probe failed for ${lockPath}: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
1042
- }
1043
- }
1044
- await sleep(10);
1045
- await yieldEventLoop();
1046
- }
1047
- }
1048
- }
1049
1014
  function withCasRetry(fn, attempts = 5) {
1050
1015
  let last;
1051
1016
  for (let i = 0; i < attempts; i += 1) {
@@ -1666,6 +1631,13 @@ async function runCreate(ctx, opts) {
1666
1631
  }
1667
1632
  }
1668
1633
  const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
1634
+ const balancedAdmissionAccounts = !outpostId && pinnedAccount === undefined
1635
+ ? [...new Set(allowedAccounts ??
1636
+ Object.entries(config.providers[provider].accounts)
1637
+ .filter(([id, account]) => account.enabled &&
1638
+ (profileAccounts === undefined || profileAccounts.includes(id)))
1639
+ .map(([id]) => id))]
1640
+ : undefined;
1669
1641
  const threadId = meshCompany
1670
1642
  ? await ctx.mesh.ensureProjectThread(meshCompany, project)
1671
1643
  : undefined;
@@ -1704,6 +1676,7 @@ async function runCreate(ctx, opts) {
1704
1676
  validation = "pending";
1705
1677
  }
1706
1678
  else {
1679
+ const admissionNowMs = ctx.now().getTime();
1707
1680
  admission = admitCreate({
1708
1681
  hqRoot,
1709
1682
  provider,
@@ -1712,14 +1685,16 @@ async function runCreate(ctx, opts) {
1712
1685
  ...(profile?.meta.max_concurrent !== undefined
1713
1686
  ? { profileMaxConcurrent: profile.meta.max_concurrent }
1714
1687
  : {}),
1688
+ host: reconcileHostFromCtx(ctx),
1715
1689
  priority,
1716
1690
  createdAt,
1691
+ nowMs: admissionNowMs,
1717
1692
  config,
1718
1693
  });
1719
1694
  if (admission.action === "refuse" && admission.error) {
1720
1695
  throw admission.error;
1721
1696
  }
1722
- admission = await applyThrottle(hqRoot, config, ctx, admission, provider, pinnedAccount);
1697
+ admission = await applyThrottle(hqRoot, config, ctx, admission, provider, pinnedAccount, { nowMs: admissionNowMs });
1723
1698
  if (admission.action === "refuse" && admission.error) {
1724
1699
  throw admission.error;
1725
1700
  }
@@ -1728,6 +1703,7 @@ async function runCreate(ctx, opts) {
1728
1703
  ? { kind: "outpost", id: outpostId }
1729
1704
  : { kind: "local", id: ctx.hostname() };
1730
1705
  const queuedReason = admission.action === "queue" ? admission.reason : undefined;
1706
+ const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
1731
1707
  const lane = createLane(hqRoot, {
1732
1708
  company,
1733
1709
  ...(company === null && meshCompany
@@ -1748,6 +1724,9 @@ async function runCreate(ctx, opts) {
1748
1724
  senior_history: [],
1749
1725
  state: "queued",
1750
1726
  queued_reason: queuedReason,
1727
+ ...(admissionReservedAt !== undefined
1728
+ ? { admission_reserved_at: admissionReservedAt }
1729
+ : {}),
1751
1730
  priority,
1752
1731
  worker: resolvedWorker?.id ?? opts.worker,
1753
1732
  ...(resolvedWorker ? { worker_path: resolvedWorker.path } : {}),
@@ -1761,6 +1740,9 @@ async function runCreate(ctx, opts) {
1761
1740
  : {}),
1762
1741
  provider,
1763
1742
  account: pinnedAccount,
1743
+ ...(balancedAdmissionAccounts !== undefined
1744
+ ? { admission_accounts: balancedAdmissionAccounts }
1745
+ : {}),
1764
1746
  ...(model !== undefined ? { model } : {}),
1765
1747
  model_source: cliModel !== undefined
1766
1748
  ? "cli"
@@ -1787,7 +1769,7 @@ async function runCreate(ctx, opts) {
1787
1769
  persisted = await persist();
1788
1770
  }
1789
1771
  else {
1790
- persisted = await withAdmissionLock(admissionLockPath(hqRoot, provider, accountWasExplicit ? pinnedAccount : undefined), persist, ctx.sleep);
1772
+ persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
1791
1773
  }
1792
1774
  const { lane, admission, validation } = persisted;
1793
1775
  const dest = runDir(hqRoot, lane.lane_id);
@@ -2160,38 +2142,54 @@ async function runPickup(ctx, opts) {
2160
2142
  });
2161
2143
  return;
2162
2144
  }
2163
- const config = loadLanesConfig(hqRoot);
2164
- let admission = admitCreate({
2165
- hqRoot,
2166
- provider: afterAccount.provider,
2167
- pinnedAccount: afterAccount.account,
2168
- priority: afterAccount.priority,
2169
- createdAt: afterAccount.created_at,
2170
- config,
2171
- excludeLaneId: afterAccount.lane_id,
2172
- });
2173
- if (admission.action === "refuse" && admission.error) {
2174
- throw admission.error;
2175
- }
2176
- admission = await applyThrottle(hqRoot, config, ctx, admission, afterAccount.provider, afterAccount.account);
2177
- if (admission.action === "refuse" && admission.error) {
2178
- throw admission.error;
2179
- }
2180
- let started;
2181
- if (admission.action === "queue") {
2182
- started = casUpdateLane(hqRoot, afterAccount.lane_id, (current) => {
2183
- if (isTerminalLaneState(current.state))
2184
- return current;
2185
- return {
2186
- ...current,
2187
- state: "queued",
2188
- queued_reason: admission.reason,
2189
- };
2145
+ let selfTargetCancelled = false;
2146
+ const started = await withLaneAdmissionLock(hqRoot, afterAccount.provider, async () => {
2147
+ const current = readLane(hqRoot, afterAccount.lane_id);
2148
+ const config = loadLanesConfig(hqRoot);
2149
+ const nowMs = ctx.now().getTime();
2150
+ let admission = admitCreate({
2151
+ hqRoot,
2152
+ provider: current.provider,
2153
+ pinnedAccount: current.account,
2154
+ ...(current.worker_profile !== undefined
2155
+ ? {
2156
+ profileId: current.worker_profile,
2157
+ profileMaxConcurrent: loadWorkerProfile(hqRoot, current.worker_profile).meta.max_concurrent,
2158
+ }
2159
+ : {}),
2160
+ host: reconcileHostFromCtx(ctx),
2161
+ priority: current.priority,
2162
+ createdAt: current.queued_at ?? current.created_at,
2163
+ nowMs,
2164
+ config,
2165
+ excludeLaneId: current.lane_id,
2166
+ alreadyQueued: true,
2190
2167
  });
2191
- }
2192
- else {
2168
+ if (admission.action === "refuse" && admission.error)
2169
+ throw admission.error;
2170
+ admission = await applyThrottle(hqRoot, config, ctx, admission, current.provider, current.account, { host: reconcileHostFromCtx(ctx), excludeLaneId: current.lane_id, alreadyQueued: true, nowMs });
2171
+ if (admission.action === "refuse" && admission.error)
2172
+ throw admission.error;
2173
+ if (admission.action === "queue") {
2174
+ return casUpdateLane(hqRoot, current.lane_id, (live) => {
2175
+ if (live.version !== current.version || isTerminalLaneState(live.state)) {
2176
+ throw new LanesError("lane_admission_conflict", `Lane ${current.lane_id} changed while pickup admission was checked`, {
2177
+ lane_id: current.lane_id,
2178
+ prepared_version: current.version,
2179
+ version: live.version,
2180
+ state: live.state,
2181
+ });
2182
+ }
2183
+ return {
2184
+ ...live,
2185
+ state: "queued",
2186
+ queued_reason: admission.reason ?? "throttle",
2187
+ queued_at: live.queued_at ?? ctx.now().toISOString(),
2188
+ };
2189
+ });
2190
+ }
2193
2191
  try {
2194
- started = spawnLaneWorker(hqRoot, afterAccount, {
2192
+ return spawnLaneWorker(hqRoot, current, {
2195
2193
  env: ctx.env,
2196
2194
  modelDeps: {
2197
2195
  readFileSync: ctx.readFileSync,
@@ -2210,19 +2208,22 @@ async function runPickup(ctx, opts) {
2210
2208
  }
2211
2209
  catch (err) {
2212
2210
  if (isLanesError(err) && err.error === "outpost_self_target") {
2213
- const cancelled = readLane(hqRoot, afterAccount.lane_id);
2214
- printJson({
2215
- ok: true,
2216
- action: "pickup",
2217
- lane_id: cancelled.lane_id,
2218
- state: cancelled.state,
2219
- error: "outpost_self_target",
2220
- lane: cancelled,
2221
- });
2222
- return;
2211
+ selfTargetCancelled = true;
2212
+ return readLane(hqRoot, afterAccount.lane_id);
2223
2213
  }
2224
2214
  throw err;
2225
2215
  }
2216
+ }, ctx.sleep);
2217
+ if (selfTargetCancelled) {
2218
+ printJson({
2219
+ ok: true,
2220
+ action: "pickup",
2221
+ lane_id: started.lane_id,
2222
+ state: started.state,
2223
+ error: "outpost_self_target",
2224
+ lane: started,
2225
+ });
2226
+ return;
2226
2227
  }
2227
2228
  printJson({
2228
2229
  ok: true,
@@ -2242,6 +2243,7 @@ async function runList(ctx, opts) {
2242
2243
  company = await ctx.resolveCompany(opts.company);
2243
2244
  }
2244
2245
  const now = ctx.now();
2246
+ const host = reconcileHostFromCtx(ctx);
2245
2247
  const lanes = loadAllLanes(hqRoot)
2246
2248
  .filter((lane) => {
2247
2249
  if (state && lane.state !== state)
@@ -2258,28 +2260,42 @@ async function runList(ctx, opts) {
2258
2260
  return false;
2259
2261
  return true;
2260
2262
  })
2261
- .sort(compareQueueOrder)
2262
- .map((lane) => ({
2263
- ...lane,
2264
- overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
2265
- stalled: Boolean(lane.stalled),
2266
- // Listing is observational. Reconcile owns durable liveness transitions.
2267
- senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
2268
- ? (() => {
2269
- const state = sessionLiveness(hqRoot, lane.senior.id, {
2270
- nowMs: now.getTime(),
2271
- deps: livenessDepsFromCtx(ctx),
2272
- presenceConnected: ctx.presenceConnected,
2273
- });
2274
- return state === null || state === "ended" || state === "lost";
2275
- })()
2276
- : false,
2277
- }));
2263
+ .sort(compareLaneQueueOrder)
2264
+ .map((lane) => {
2265
+ const remoteQueued = lane.state === "queued" &&
2266
+ lane.host.kind === "outpost" &&
2267
+ !laneRunsHere(lane, host);
2268
+ return {
2269
+ ...lane,
2270
+ ...(remoteQueued
2271
+ ? { state: "remote", remote_state: lane.state, queued_reason: undefined }
2272
+ : {}),
2273
+ overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
2274
+ stalled: Boolean(lane.stalled),
2275
+ // Listing is observational. Reconcile owns durable liveness transitions.
2276
+ senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
2277
+ ? (() => {
2278
+ const state = sessionLiveness(hqRoot, lane.senior.id, {
2279
+ nowMs: now.getTime(),
2280
+ deps: livenessDepsFromCtx(ctx),
2281
+ presenceConnected: ctx.presenceConnected,
2282
+ });
2283
+ return state === null || state === "ended" || state === "lost";
2284
+ })()
2285
+ : false,
2286
+ };
2287
+ });
2278
2288
  printJson({ ok: true, action: "list", lanes });
2279
2289
  }
2280
2290
  async function runShow(ctx, laneId) {
2281
2291
  const hqRoot = ctx.hqRoot();
2282
- const lane = readLane(hqRoot, laneId);
2292
+ const storedLane = readLane(hqRoot, laneId);
2293
+ const remoteQueued = storedLane.state === "queued" &&
2294
+ storedLane.host.kind === "outpost" &&
2295
+ !laneRunsHere(storedLane, reconcileHostFromCtx(ctx));
2296
+ const lane = remoteQueued
2297
+ ? { ...storedLane, state: "remote", remote_state: storedLane.state, queued_reason: undefined }
2298
+ : storedLane;
2283
2299
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
2284
2300
  printJson({
2285
2301
  ok: true,
@@ -2290,7 +2306,7 @@ async function runShow(ctx, laneId) {
2290
2306
  stalled_at: lane.stalled_at ?? null,
2291
2307
  stalled_reason: lane.stalled_reason ?? null,
2292
2308
  ask_delivery_failed: askDeliveryFailed,
2293
- delivery: laneDeliveryMechanism(lane),
2309
+ delivery: laneDeliveryMechanism(storedLane),
2294
2310
  provider_session_id: lane.provider_session_id ?? null,
2295
2311
  provider_session_id_absent: lane.provider_session_id_absent ?? null,
2296
2312
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
@@ -2306,10 +2322,136 @@ async function runShow(ctx, laneId) {
2306
2322
  terminal_details: lane.terminal_details ?? null,
2307
2323
  last_message_at: lane.last_message_at ?? null,
2308
2324
  last_compaction: lane.last_compaction
2309
- ? { ...lane.last_compaction, after: compactionAfterUsage(lane) }
2325
+ ? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
2310
2326
  : null,
2311
2327
  });
2312
2328
  }
2329
+ function forceAdmissionAuthorized(ctx, lane) {
2330
+ const sessionId = sessionIdFromEnv(ctx.env);
2331
+ const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
2332
+ if (lane.senior.kind === "lane") {
2333
+ return callerLaneId !== undefined && callerLaneId === lane.senior.id;
2334
+ }
2335
+ if (!sessionId || callerLaneId !== undefined)
2336
+ return false;
2337
+ if (lane.senior.kind === "session")
2338
+ return sessionId === lane.senior.id;
2339
+ if (lane.senior.kind !== "user")
2340
+ return false;
2341
+ const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
2342
+ if (loaded.status !== "ok")
2343
+ return false;
2344
+ const senior = loaded.doc.senior;
2345
+ return senior === "user" ||
2346
+ (senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
2347
+ senior.kind === "user");
2348
+ }
2349
+ async function runAdmit(ctx, laneId, opts) {
2350
+ const hqRoot = ctx.hqRoot();
2351
+ let lane = readLane(hqRoot, laneId);
2352
+ if (lane.state !== "queued") {
2353
+ throw new LanesError("lane_not_queued", `Lane ${laneId} is ${lane.state}; only a queued lane can be admitted`, {
2354
+ lane_id: laneId,
2355
+ state: lane.state,
2356
+ });
2357
+ }
2358
+ const host = reconcileHostFromCtx(ctx);
2359
+ if (!laneRunsHere(lane, host)) {
2360
+ printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
2361
+ return;
2362
+ }
2363
+ if (lane.pickup_validation_pending === true) {
2364
+ printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
2365
+ return;
2366
+ }
2367
+ await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
2368
+ lane = readLane(hqRoot, laneId);
2369
+ if (lane.state !== "queued") {
2370
+ throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
2371
+ lane_id: laneId,
2372
+ state: lane.state,
2373
+ });
2374
+ }
2375
+ let blockedBy;
2376
+ if (opts.force) {
2377
+ if (!forceAdmissionAuthorized(ctx, lane)) {
2378
+ throw new LanesError("force_not_authorized", `Only lane ${lane.senior.kind === "lane" ? lane.senior.id : lane.senior.kind === "session" ? lane.senior.id : "senior/operator"} may force admission of ${laneId}`, { lane_id: laneId, senior: lane.senior });
2379
+ }
2380
+ const config = loadLanesConfig(hqRoot);
2381
+ const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
2382
+ const { decision } = await decideAdmission(hqRoot, config.throttle, ctx.throttleDeps);
2383
+ if (!decision.admit) {
2384
+ blockedBy = "throttle";
2385
+ }
2386
+ else if (hasEarlierEligibleQueuedLane(lanes, lane, config, host, (row) => row.worker_profile === undefined
2387
+ ? undefined
2388
+ : loadWorkerProfile(hqRoot, row.worker_profile).meta.max_concurrent)) {
2389
+ blockedBy = "queue_position";
2390
+ }
2391
+ }
2392
+ else {
2393
+ const admission = await assessLaneAdmission(ctx, lane, {
2394
+ queueAt: lane.queued_at ?? lane.created_at,
2395
+ alreadyQueued: true,
2396
+ });
2397
+ if (admission.action === "refuse" && admission.error) {
2398
+ throw admission.error;
2399
+ }
2400
+ if (admission.action === "queue")
2401
+ blockedBy = admission.reason ?? "throttle";
2402
+ }
2403
+ if (blockedBy !== undefined) {
2404
+ const updated = casUpdateLane(hqRoot, laneId, (current) => {
2405
+ if (current.version !== lane.version || current.state !== "queued") {
2406
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while admission was checked`, {
2407
+ lane_id: laneId,
2408
+ prepared_version: lane.version,
2409
+ version: current.version,
2410
+ state: current.state,
2411
+ });
2412
+ }
2413
+ return { ...current, queued_reason: blockedBy };
2414
+ });
2415
+ printJson({
2416
+ ok: true,
2417
+ action: "admit",
2418
+ lane_id: laneId,
2419
+ admitted: false,
2420
+ blocked_by: blockedBy,
2421
+ lane: updated,
2422
+ });
2423
+ return;
2424
+ }
2425
+ const started = spawnLaneWorker(hqRoot, lane, {
2426
+ env: ctx.env,
2427
+ modelDeps: {
2428
+ readFileSync: ctx.readFileSync,
2429
+ probe: ctx.probeProvider,
2430
+ env: ctx.env,
2431
+ },
2432
+ existsSync: ctx.existsSync,
2433
+ spawnProcess: ctx.spawnProcess,
2434
+ proveDetached: ctx.proveDetached,
2435
+ killProcess: ctx.killProcess,
2436
+ now: ctx.now,
2437
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
2438
+ runCompaction: ctx.runCompaction,
2439
+ locateTranscript: ctx.locateTranscript,
2440
+ });
2441
+ const admitted = opts.force && started.state === "running"
2442
+ ? casUpdateLane(hqRoot, laneId, (current) => ({ ...current, admitted_reason: "operator_force" }))
2443
+ : started;
2444
+ printJson({
2445
+ ok: true,
2446
+ action: "admit",
2447
+ lane_id: laneId,
2448
+ admitted: admitted.state === "running",
2449
+ forced: Boolean(opts.force),
2450
+ blocked_by: admitted.state === "running" ? null : admitted.queued_reason ?? null,
2451
+ lane: admitted,
2452
+ });
2453
+ }, ctx.sleep);
2454
+ }
2313
2455
  /**
2314
2456
  * US-023: move the lane's provider session to `--account` (same provider)
2315
2457
  * before a resume or a message resume. Every check runs before anything is
@@ -2322,6 +2464,7 @@ async function prepareAccountMove(ctx, lane, input) {
2322
2464
  const hqRoot = ctx.hqRoot();
2323
2465
  const now = ctx.now();
2324
2466
  const laneId = lane.lane_id;
2467
+ assertLaneRunsHereForAdmission(ctx, lane);
2325
2468
  if (lane.state === "running") {
2326
2469
  throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
2327
2470
  lane_id: laneId,
@@ -2432,16 +2575,14 @@ async function prepareAccountMove(ctx, lane, input) {
2432
2575
  // pointed at the account whose spawn has not claimed it yet
2433
2576
  // (`pending_resume` set by this write, cleared by the claim), so two moves
2434
2577
  // into one free slot cannot both start.
2435
- return withAdmissionLock(admissionLockPath(hqRoot, lane.provider, target), async () => {
2436
- const lanes = loadAllLanes(hqRoot);
2437
- // A queued lane with a pending resume waits for admission and holds
2438
- // no slot; one in any other state with a pending resume is a move
2439
- // whose spawn is about to claim, and holds the slot it was granted.
2440
- const occupies = (row) => row.lane_id !== laneId &&
2441
- (row.state === "running" || (row.pending_resume !== undefined && row.state !== "queued"));
2442
- const onProvider = lanes.filter((row) => row.provider === lane.provider && occupies(row));
2443
- const atCap = onProvider.length >= providerCfg.max_running ||
2444
- onProvider.filter((row) => row.account === target).length >= accountMaxRunning(providerCfg, target);
2578
+ return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
2579
+ const admission = await assessLaneAdmission(ctx, lane, {
2580
+ account: target,
2581
+ queueAt: at,
2582
+ });
2583
+ if (admission.action === "refuse" && admission.error)
2584
+ throw admission.error;
2585
+ const queued = admission.action === "queue";
2445
2586
  const moved = casUpdateLane(hqRoot, laneId, (current) => {
2446
2587
  // The copy and every check above were made from `lane`; a record
2447
2588
  // that moved on since (another move, a spawn, a cancel) invalidates
@@ -2453,6 +2594,7 @@ async function prepareAccountMove(ctx, lane, input) {
2453
2594
  const next = {
2454
2595
  ...current,
2455
2596
  account: target,
2597
+ admission_accounts: undefined,
2456
2598
  provider_session_id: sessionId,
2457
2599
  provider_session_id_absent: undefined,
2458
2600
  ...(input.cliModel !== undefined
@@ -2466,13 +2608,17 @@ async function prepareAccountMove(ctx, lane, input) {
2466
2608
  }),
2467
2609
  pending_resume: { session_id: sessionId, at, ...input.pending },
2468
2610
  };
2469
- if (atCap) {
2611
+ if (queued) {
2470
2612
  next.state = "queued";
2471
- next.queued_reason = "cap";
2613
+ next.queued_reason = admission.reason ?? "throttle";
2614
+ next.queued_at = at;
2615
+ }
2616
+ else {
2617
+ next.queued_at = undefined;
2472
2618
  }
2473
2619
  return next;
2474
2620
  });
2475
- return { lane: moved, from, to: target, copy, queued: atCap };
2621
+ return { lane: moved, from, to: target, copy, queued };
2476
2622
  }, ctx.sleep);
2477
2623
  }
2478
2624
  async function runMessage(ctx, laneId, opts) {
@@ -2527,6 +2673,7 @@ async function runMessage(ctx, laneId, opts) {
2527
2673
  };
2528
2674
  let resumed = null;
2529
2675
  let move = null;
2676
+ let queuedOnAdmission = false;
2530
2677
  if (opts.account !== undefined) {
2531
2678
  // A moved message is always a resume on the target account: a running
2532
2679
  // lane (the dropbox mechanism) is refused inside prepareAccountMove.
@@ -2551,31 +2698,70 @@ async function runMessage(ctx, laneId, opts) {
2551
2698
  appendDropboxMessage(hqRoot, laneId, dropboxMessage);
2552
2699
  }
2553
2700
  else {
2554
- // Exited Claude worker with a recorded session: the message becomes the
2555
- // next turn of that session. It is archived under delivered/ (never the
2556
- // inbox, which would hand it to the drain hook a second time) and the
2557
- // spawn happens before any attribution write so a failed spawn leaves
2558
- // the record untouched.
2559
- resumed = spawnLaneWorker(hqRoot, lane, {
2560
- env: ctx.env,
2561
- modelDeps: {
2562
- readFileSync: ctx.readFileSync,
2563
- probe: ctx.probeProvider,
2701
+ // Exited worker with a recorded session: queue the message when capacity
2702
+ // is occupied, otherwise start its next turn under the shared admission lock.
2703
+ assertLaneRunsHereForAdmission(ctx, lane);
2704
+ resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
2705
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
2706
+ if (admission.action === "refuse" && admission.error)
2707
+ throw admission.error;
2708
+ if (admission.action === "queue") {
2709
+ const sessionId = lane.provider_session_id;
2710
+ if (!sessionId) {
2711
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
2712
+ lane_id: laneId,
2713
+ provider: lane.provider,
2714
+ });
2715
+ }
2716
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
2717
+ queuedOnAdmission = true;
2718
+ return casUpdateLane(hqRoot, laneId, (current) => {
2719
+ if (current.version !== lane.version || current.state !== lane.state) {
2720
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
2721
+ lane_id: laneId,
2722
+ prepared_version: lane.version,
2723
+ version: current.version,
2724
+ state: current.state,
2725
+ });
2726
+ }
2727
+ return {
2728
+ ...current,
2729
+ state: "queued",
2730
+ queued_reason: admission.reason ?? "throttle",
2731
+ queued_at: at,
2732
+ pending_resume: {
2733
+ session_id: sessionId,
2734
+ trigger: "message",
2735
+ text,
2736
+ message_id: messageId,
2737
+ compact: opts.compact !== false,
2738
+ at,
2739
+ },
2740
+ };
2741
+ });
2742
+ }
2743
+ const started = spawnLaneWorker(hqRoot, lane, {
2564
2744
  env: ctx.env,
2565
- },
2566
- existsSync: ctx.existsSync,
2567
- spawnProcess: ctx.spawnProcess,
2568
- proveDetached: ctx.proveDetached,
2569
- killProcess: ctx.killProcess,
2570
- now: ctx.now,
2571
- resolveMachineIdentity: ctx.resolveMachineIdentity,
2572
- runCompaction: ctx.runCompaction,
2573
- locateTranscript: ctx.locateTranscript,
2574
- }, {
2575
- trigger: { kind: "message", text, message_id: messageId },
2576
- compact: opts.compact !== false,
2577
- });
2578
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
2745
+ modelDeps: {
2746
+ readFileSync: ctx.readFileSync,
2747
+ probe: ctx.probeProvider,
2748
+ env: ctx.env,
2749
+ },
2750
+ existsSync: ctx.existsSync,
2751
+ spawnProcess: ctx.spawnProcess,
2752
+ proveDetached: ctx.proveDetached,
2753
+ killProcess: ctx.killProcess,
2754
+ now: ctx.now,
2755
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
2756
+ runCompaction: ctx.runCompaction,
2757
+ locateTranscript: ctx.locateTranscript,
2758
+ }, {
2759
+ trigger: { kind: "message", text, message_id: messageId },
2760
+ compact: opts.compact !== false,
2761
+ });
2762
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
2763
+ return started;
2764
+ }, ctx.sleep);
2579
2765
  }
2580
2766
  // Every accepted message moves the compaction reference forward.
2581
2767
  let next = casUpdateLane(hqRoot, laneId, (current) => ({
@@ -2621,9 +2807,11 @@ async function runMessage(ctx, laneId, opts) {
2621
2807
  const drainHook = drainHookStatusFor(ctx, next);
2622
2808
  const deliveryStatus = move?.queued
2623
2809
  ? "queued_on_target_account"
2624
- : mechanism === "dropbox"
2625
- ? "awaiting_next_tool_call"
2626
- : "worker_spawned";
2810
+ : queuedOnAdmission
2811
+ ? "queued_on_admission"
2812
+ : mechanism === "dropbox"
2813
+ ? "awaiting_next_tool_call"
2814
+ : "worker_spawned";
2627
2815
  const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
2628
2816
  ? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
2629
2817
  : mechanism === "dropbox" && next.stalled === true
@@ -2718,6 +2906,9 @@ async function runResume(ctx, laneId, opts) {
2718
2906
  if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
2719
2907
  throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
2720
2908
  }
2909
+ if (lane.state === "queued") {
2910
+ throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
2911
+ }
2721
2912
  const refusal = resumeCommandRefusal(lane);
2722
2913
  if (refusal !== null) {
2723
2914
  throw new LanesError(refusal.error, refusal.message, {
@@ -2727,6 +2918,7 @@ async function runResume(ctx, laneId, opts) {
2727
2918
  message: refusal.message,
2728
2919
  });
2729
2920
  }
2921
+ assertLaneRunsHereForAdmission(ctx, lane);
2730
2922
  const resumedFrom = lane.state;
2731
2923
  const text = opts.text?.trim() ? opts.text : undefined;
2732
2924
  const at = ctx.now().toISOString();
@@ -2749,32 +2941,77 @@ async function runResume(ctx, laneId, opts) {
2749
2941
  });
2750
2942
  lane = move.lane;
2751
2943
  }
2752
- const resumed = move?.queued
2753
- ? lane
2754
- : spawnLaneWorker(hqRoot, lane, {
2755
- env: ctx.env,
2756
- modelDeps: {
2757
- readFileSync: ctx.readFileSync,
2758
- probe: ctx.probeProvider,
2944
+ let queuedOnAdmission = false;
2945
+ let resumed;
2946
+ if (move?.queued) {
2947
+ resumed = lane;
2948
+ }
2949
+ else {
2950
+ resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
2951
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
2952
+ if (admission.action === "refuse" && admission.error)
2953
+ throw admission.error;
2954
+ if (admission.action === "queue") {
2955
+ const sessionId = lane.provider_session_id;
2956
+ if (!sessionId) {
2957
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
2958
+ lane_id: laneId,
2959
+ provider: lane.provider,
2960
+ });
2961
+ }
2962
+ queuedOnAdmission = true;
2963
+ return casUpdateLane(hqRoot, laneId, (current) => {
2964
+ if (current.version !== lane.version || current.state !== lane.state) {
2965
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
2966
+ lane_id: laneId,
2967
+ prepared_version: lane.version,
2968
+ version: current.version,
2969
+ state: current.state,
2970
+ });
2971
+ }
2972
+ return {
2973
+ ...current,
2974
+ state: "queued",
2975
+ queued_reason: admission.reason ?? "throttle",
2976
+ queued_at: at,
2977
+ pending_resume: {
2978
+ session_id: sessionId,
2979
+ trigger: "resume",
2980
+ resumed_from: resumedFrom,
2981
+ ...(text !== undefined ? { text } : {}),
2982
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
2983
+ compact: opts.compact !== false,
2984
+ at,
2985
+ },
2986
+ };
2987
+ });
2988
+ }
2989
+ return spawnLaneWorker(hqRoot, lane, {
2759
2990
  env: ctx.env,
2760
- },
2761
- existsSync: ctx.existsSync,
2762
- spawnProcess: ctx.spawnProcess,
2763
- proveDetached: ctx.proveDetached,
2764
- killProcess: ctx.killProcess,
2765
- now: ctx.now,
2766
- resolveMachineIdentity: ctx.resolveMachineIdentity,
2767
- runCompaction: ctx.runCompaction,
2768
- locateTranscript: ctx.locateTranscript,
2769
- }, {
2770
- trigger: {
2771
- kind: "resume",
2772
- from: resumedFrom,
2773
- ...(text !== undefined ? { text } : {}),
2774
- ...(messageId !== undefined ? { message_id: messageId } : {}),
2775
- },
2776
- compact: opts.compact !== false,
2777
- });
2991
+ modelDeps: {
2992
+ readFileSync: ctx.readFileSync,
2993
+ probe: ctx.probeProvider,
2994
+ env: ctx.env,
2995
+ },
2996
+ existsSync: ctx.existsSync,
2997
+ spawnProcess: ctx.spawnProcess,
2998
+ proveDetached: ctx.proveDetached,
2999
+ killProcess: ctx.killProcess,
3000
+ now: ctx.now,
3001
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
3002
+ runCompaction: ctx.runCompaction,
3003
+ locateTranscript: ctx.locateTranscript,
3004
+ }, {
3005
+ trigger: {
3006
+ kind: "resume",
3007
+ from: resumedFrom,
3008
+ ...(text !== undefined ? { text } : {}),
3009
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
3010
+ },
3011
+ compact: opts.compact !== false,
3012
+ });
3013
+ }, ctx.sleep);
3014
+ }
2778
3015
  if (text !== undefined && messageId !== undefined) {
2779
3016
  // Same audit copy a message resume leaves; nothing drains it. Archive only
2780
3017
  // after a successful spawn so a concurrent resume cannot leave a phantom.
@@ -2833,7 +3070,11 @@ async function runResume(ctx, laneId, opts) {
2833
3070
  message_id: messageId ?? null,
2834
3071
  text: text ?? null,
2835
3072
  delivery: "resume",
2836
- delivery_status: move?.queued ? "queued_on_target_account" : "worker_spawned",
3073
+ delivery_status: move?.queued
3074
+ ? "queued_on_target_account"
3075
+ : queuedOnAdmission
3076
+ ? "queued_on_admission"
3077
+ : "worker_spawned",
2837
3078
  account_move: move ? accountMoveSummary(move) : null,
2838
3079
  resumed_session_id: resumed.last_resume?.session_id ?? null,
2839
3080
  last_resume: next.last_resume ?? null,
@@ -3903,6 +4144,14 @@ export function registerLanesCommand(parent, deps = {}) {
3903
4144
  .action(wrap(async (opts) => {
3904
4145
  await runList(ctx, opts);
3905
4146
  }));
4147
+ lanes
4148
+ .command("admit")
4149
+ .description("Try to admit the oldest eligible queued lane")
4150
+ .argument("<lane-id>", "Lane id")
4151
+ .option("--force", "Senior/operator override for provider, account, and profile caps")
4152
+ .action(wrap(async (laneId, opts) => {
4153
+ await runAdmit(ctx, laneId, opts);
4154
+ }));
3906
4155
  lanes
3907
4156
  .command("watch")
3908
4157
  .description("Watch lane state and question changes until all lanes are terminal")