@indigoai-us/hq-cli 5.247.1 → 5.249.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.
@@ -12,7 +12,9 @@ import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { Option } from "commander";
14
14
  import { createLaneAccountsHandlers } from "./lanes-accounts.js";
15
+ import { createLaneMessageHandlers } from "./lanes-message.js";
15
16
  import { createLaneQuestionsHandlers } from "./lanes-questions.js";
17
+ import { createLaneResumeHandlers } from "./lanes-resume.js";
16
18
  export { resolveAnswerChoice } from "./lanes-questions.js";
17
19
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
18
20
  import { registerLaneChecksCommand } from "./lanes-checks.js";
@@ -43,7 +45,7 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
43
45
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
44
46
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
45
47
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
46
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, appendDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
48
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
47
49
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
48
50
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
49
51
  import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
@@ -53,14 +55,11 @@ import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
53
55
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
54
56
  import { LanesError } from "../lib/lanes/errors.js";
55
57
  import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
56
- import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
57
- import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
58
- import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
59
58
  import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
60
59
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
61
60
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
62
61
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
63
- import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, readPidRecord, probePidRecord, readProcStarttime, stopLaneProcesses, } from "../lib/lanes/spawn.js";
62
+ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
64
63
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
65
64
  export { spawnLaneWorker };
66
65
  export { questionBodyAbsPath };
@@ -5389,57 +5388,6 @@ function recordResumeSetup(ctx, lane, input) {
5389
5388
  return next;
5390
5389
  });
5391
5390
  }
5392
- function hasKeepAliveProgress(lane) {
5393
- return (lane.keep_alive_resume_count ?? 0) !== 0 ||
5394
- lane.keep_alive_last_resume_at !== undefined ||
5395
- lane.keep_alive_next_eligible_at !== undefined ||
5396
- lane.keep_alive_exhausted_at !== undefined;
5397
- }
5398
- function resetKeepAliveProgress(lane) {
5399
- if (!hasKeepAliveProgress(lane))
5400
- return lane;
5401
- return {
5402
- ...lane,
5403
- keep_alive_resume_count: 0,
5404
- keep_alive_last_resume_at: undefined,
5405
- keep_alive_next_eligible_at: undefined,
5406
- keep_alive_exhausted_at: undefined,
5407
- };
5408
- }
5409
- /** Resolve an explicit resume override or newly attached profile like create does. */
5410
- function resolveResumeEffort(ctx, lane, input) {
5411
- if (input.cliEffort === undefined && input.profile === undefined)
5412
- return undefined;
5413
- const config = loadLanesConfig(ctx.hqRoot());
5414
- const providerConfig = config.providers[lane.provider];
5415
- const accountId = input.account?.trim() || lane.account;
5416
- const accountConfig = accountId === undefined
5417
- ? undefined
5418
- : providerConfig.accounts[accountId];
5419
- const profileDefaultsApply = input.profile?.profile.meta.provider === lane.provider;
5420
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
5421
- ? lane.model
5422
- : undefined;
5423
- const model = resolveLaneModel({
5424
- cliModel: input.model?.trim() || pinnedModel,
5425
- profileModel: profileDefaultsApply ? input.profile?.profile.meta.model : undefined,
5426
- accountModel: accountConfig?.model,
5427
- providerModel: providerConfig.model,
5428
- });
5429
- return resolveLaneEffort({
5430
- cliEffort: input.cliEffort,
5431
- profileEffort: profileDefaultsApply ? input.profile?.profile.meta.effort : undefined,
5432
- accountEffort: accountConfig?.effort,
5433
- providerEffort: providerConfig.effort,
5434
- model,
5435
- });
5436
- }
5437
- function recordedResumeEffort(lane) {
5438
- return {
5439
- ...(lane.effort !== undefined ? { effort: lane.effort } : {}),
5440
- source: lane.effort_source ?? "none",
5441
- };
5442
- }
5443
5391
  /**
5444
5392
  * US-023: move the lane's provider session to `--account` (same provider)
5445
5393
  * before a resume or a message resume. Every check runs before anything is
@@ -5728,585 +5676,6 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
5728
5676
  ...(preservedModel !== undefined ? { preservedModel } : {}),
5729
5677
  };
5730
5678
  }
5731
- function accountRequestKeepsCurrentLane(lane, rawAccount) {
5732
- const raw = rawAccount.trim();
5733
- const colon = raw.indexOf(":");
5734
- const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
5735
- const requestedAccount = colon > 0 ? raw.slice(colon + 1) : raw;
5736
- return requestedAccount.length > 0 &&
5737
- requestedAccount === lane.account &&
5738
- (requestedProvider === undefined || requestedProvider === lane.provider);
5739
- }
5740
- function assertCurrentResumeAccountAvailable(ctx, lane) {
5741
- const config = loadLanesConfig(ctx.hqRoot());
5742
- const account = lane.account === undefined
5743
- ? undefined
5744
- : config.providers[lane.provider].accounts[lane.account];
5745
- if (account === undefined || !account.enabled) {
5746
- throw new AccountResolutionError(account === undefined ? "unknown_account" : "account_disabled", account === undefined
5747
- ? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`
5748
- : `Account ${JSON.stringify(lane.account)} is disabled${account.disabled_reason ? `: ${account.disabled_reason}` : ""}`, { provider: lane.provider, account: lane.account ?? "", ...(account?.disabled_reason !== undefined ? { reason: account.disabled_reason } : {}) });
5749
- }
5750
- if (accountStatus(lane.provider, lane.account, config, {
5751
- existsSync: ctx.existsSync,
5752
- hostname: ctx.hostname(),
5753
- probe: ctx.probeProvider,
5754
- env: ctx.env,
5755
- }).logged_in !== true) {
5756
- throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: lane.account, lane_id: lane.lane_id });
5757
- }
5758
- }
5759
- async function runMessage(ctx, laneId, opts) {
5760
- const hqRoot = ctx.hqRoot();
5761
- const hireGrantContext = opts.grantContext === undefined
5762
- ? undefined
5763
- : (() => {
5764
- if (!ctx.isSsmOwnerDispatch()) {
5765
- throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
5766
- }
5767
- return parseOutpostHireGrantContext(opts.grantContext, laneId);
5768
- })();
5769
- let accepted = false;
5770
- let claimedMessages = [];
5771
- const markAccepted = () => {
5772
- accepted = true;
5773
- opts.onAccepted?.();
5774
- };
5775
- let text = opts.text;
5776
- if (!text) {
5777
- throw new LanesError("missing_text", "--text is required", {});
5778
- }
5779
- let lane = readLane(hqRoot, laneId);
5780
- if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
5781
- await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
5782
- return;
5783
- }
5784
- assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
5785
- if (lane.state === "running") {
5786
- const workerPidPath = path.join(laneRunDir(hqRoot, laneId), "worker.pid");
5787
- const workerRecordExists = fs.existsSync(workerPidPath);
5788
- const worker = readPidRecord(workerPidPath);
5789
- if (workerRecordExists && worker === null) {
5790
- throw new LanesError("worker_pid_invalid", `Lane ${laneId} has a malformed worker PID record; message delivery cannot be classified safely`, { lane_id: laneId, worker_pid_path: workerPidPath });
5791
- }
5792
- // A fresh spawn may have committed running before publishing worker.pid;
5793
- // absence is not proof of exit. Reclassify only a recorded process that
5794
- // fails the PID plus start-time identity probe.
5795
- const processAlive = worker === null
5796
- ? true
5797
- : probePidRecord(worker, ctx.isAlive ?? isPidAlive, ctx.readStarttime ?? readProcStarttime).alive;
5798
- if (worker !== null && !processAlive) {
5799
- // The watcher may not have reached its next poll yet. Run the same
5800
- // classifier synchronously, then make the queue/resume choice again.
5801
- await runWatcherLoop(hqRoot, laneId, {
5802
- ...ctx.watcherLoopDeps,
5803
- now: () => ctx.now().getTime(),
5804
- isAlive: ctx.isAlive ?? isPidAlive,
5805
- readStarttime: ctx.readStarttime ?? readProcStarttime,
5806
- mesh: null,
5807
- resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
5808
- });
5809
- lane = readLane(hqRoot, laneId);
5810
- // This invocation won the race to classify the dead worker, so the
5811
- // watcher callback did not own recovery. Fold any earlier queued
5812
- // messages into this resume to keep one provider turn and one delivery.
5813
- if (laneDeliveryMechanism(lane) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
5814
- const queuedMessages = drainDropbox(hqRoot, laneId);
5815
- if (queuedMessages.length > 0) {
5816
- claimedMessages = queuedMessages;
5817
- text = `${formatDropboxMessages(queuedMessages)}\n\n${text}`;
5818
- }
5819
- }
5820
- }
5821
- }
5822
- try {
5823
- const eligibility = await messageLaneExclusion(ctx, lane);
5824
- lane = eligibility.lane;
5825
- const exclusion = eligibility.exclusion;
5826
- if (exclusion !== undefined) {
5827
- let messageCount;
5828
- if (dropboxInboxHasMessages(hqRoot, laneId)) {
5829
- messageCount = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" }).length;
5830
- }
5831
- recordMessageUndeliverable(ctx, lane, exclusion, messageCount);
5832
- const refusalMessage = deliveryRefusalReason(lane);
5833
- throw new LanesError("lane_not_accepting_messages", refusalMessage || `Lane ${laneId} cannot receive messages: ${exclusion}`, { lane_id: laneId, state: lane.state, reason: exclusion });
5834
- }
5835
- if (lane.state === "done" && dropboxInboxHasMessages(hqRoot, laneId)) {
5836
- const staleMessages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
5837
- if (staleMessages.length > 0) {
5838
- recordMessageUndeliverable(ctx, lane, "done_before_queued_message", staleMessages.length);
5839
- }
5840
- }
5841
- const cliEffort = parseCliEffort(lane.provider, opts.effort);
5842
- if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
5843
- throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
5844
- }
5845
- // Check before transcript recovery, account probing, or message writes.
5846
- // The final state transition repeats this check under the senior lock.
5847
- await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
5848
- lane = laneWithRecoveredProviderSession(hqRoot, lane);
5849
- const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
5850
- if (keepCurrentAccount)
5851
- assertCurrentResumeAccountAvailable(ctx, lane);
5852
- // A real account move is refused on its own terms first: a running lane and
5853
- // a provider without session resume get the move's named errors, not the
5854
- // generic delivery refusal below. Naming the current account is a keep.
5855
- if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
5856
- throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
5857
- lane_id: laneId,
5858
- state: lane.state,
5859
- });
5860
- }
5861
- if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
5862
- throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
5863
- }
5864
- // One decision point for how a follow-up reaches this lane (delivery.ts).
5865
- // `none` refuses before anything is written: no inbox append, no spawn.
5866
- const mechanism = laneDeliveryMechanism(lane);
5867
- if (mechanism === "none") {
5868
- const message = deliveryRefusalReason(lane);
5869
- throw new LanesError("lane_not_accepting_messages", message, {
5870
- lane_id: laneId,
5871
- state: lane.state,
5872
- provider: lane.provider,
5873
- message,
5874
- });
5875
- }
5876
- if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
5877
- throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
5878
- }
5879
- if (opts.worker !== undefined && mechanism === "dropbox") {
5880
- throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
5881
- }
5882
- if (cliEffort !== undefined && mechanism === "dropbox") {
5883
- throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
5884
- }
5885
- const resumeProfile = opts.worker !== undefined
5886
- ? resolveResumeProfile(ctx, lane, opts.worker)
5887
- : undefined;
5888
- if (opts.model !== undefined && opts.account === undefined) {
5889
- throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
5890
- }
5891
- const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
5892
- ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
5893
- : undefined;
5894
- const resumeEffort = resolveResumeEffort(ctx, lane, {
5895
- cliEffort,
5896
- profile: resumeProfile,
5897
- account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
5898
- model: opts.model,
5899
- });
5900
- const messageId = mintMessageId();
5901
- const at = ctx.now().toISOString();
5902
- const projectId = opts.project?.trim() || lane.project_id;
5903
- const storyId = opts.story?.trim() || lane.story_id;
5904
- if (opts.story !== undefined && !opts.story.trim()) {
5905
- throw new LanesError("missing_story", "--story must be non-empty", {});
5906
- }
5907
- if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
5908
- validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
5909
- }
5910
- const framedMonitorEvent = opts.monitorEvent === undefined
5911
- ? undefined
5912
- : formatMonitorEvent({
5913
- monitorId: opts.monitorEvent.monitor_id,
5914
- description: opts.monitorEvent.description,
5915
- sequence: opts.monitorEvent.sequence,
5916
- text,
5917
- });
5918
- const deliveryText = framedMonitorEvent ?? text;
5919
- const dropboxMessage = {
5920
- at,
5921
- message_id: messageId,
5922
- text,
5923
- project_id: projectId,
5924
- story_id: storyId,
5925
- ...(opts.monitorEvent === undefined ? {} : {
5926
- kind: "monitor_event",
5927
- monitor_id: opts.monitorEvent.monitor_id,
5928
- description: opts.monitorEvent.description,
5929
- sequence: opts.monitorEvent.sequence,
5930
- }),
5931
- };
5932
- let resumed = null;
5933
- let move = null;
5934
- let queuedOnAdmission = false;
5935
- if (opts.account !== undefined && !keepCurrentAccount) {
5936
- // A moved message is always a resume on the target account: a running
5937
- // lane (the dropbox mechanism) is refused inside prepareAccountMove.
5938
- move = await prepareAccountMove(ctx, lane, {
5939
- account: opts.account,
5940
- cliModel: opts.model?.trim() || undefined,
5941
- ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
5942
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5943
- ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
5944
- pending: {
5945
- trigger: "message",
5946
- text: deliveryText,
5947
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5948
- message_id: messageId,
5949
- compact: opts.compact !== false,
5950
- },
5951
- });
5952
- lane = move.lane;
5953
- if (move.queued) {
5954
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5955
- }
5956
- }
5957
- else if (mechanism === "resume") {
5958
- const automatic = automaticAccountSelection;
5959
- if (automatic !== undefined) {
5960
- const selection = automatic.selection;
5961
- if (selection.account !== lane.account) {
5962
- move = await prepareAccountMove(ctx, lane, {
5963
- account: selection.account,
5964
- cliModel: undefined,
5965
- ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
5966
- ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
5967
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5968
- ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
5969
- pending: {
5970
- trigger: "message",
5971
- text: deliveryText,
5972
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5973
- message_id: messageId,
5974
- compact: opts.compact !== false,
5975
- },
5976
- accountSelection: selection,
5977
- });
5978
- lane = move.lane;
5979
- if (move.queued)
5980
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5981
- }
5982
- else {
5983
- lane = persistClaudeAccountSelection(hqRoot, lane, selection);
5984
- }
5985
- }
5986
- }
5987
- if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
5988
- validateResumeProfileModel(ctx, lane, resumeProfile.profile);
5989
- }
5990
- if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
5991
- lane = recordResumeSetup(ctx, lane, {
5992
- ...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
5993
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5994
- ...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
5995
- });
5996
- }
5997
- if (move?.queued) {
5998
- // Parked at the target's cap: reconcile resumes the session there.
5999
- markAccepted();
6000
- }
6001
- else {
6002
- const dispatched = await dispatchLaneDelivery(lane, {
6003
- dropbox: () => {
6004
- // Live (or about-to-be-live) process: the PreToolUse drain delivers it.
6005
- if (!laneAcceptsOperatorMessage(lane)) {
6006
- throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
6007
- }
6008
- appendDropboxMessage(hqRoot, laneId, dropboxMessage);
6009
- return null;
6010
- },
6011
- resume: async () => {
6012
- // Exited worker with a recorded session: queue the message when capacity
6013
- // is occupied, otherwise start its next turn under the shared admission lock.
6014
- assertLaneRunsHereForAdmission(ctx, lane);
6015
- return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
6016
- const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
6017
- if (admission.action === "refuse" && admission.error)
6018
- throw admission.error;
6019
- return withResumeHireAdmission(ctx, lane, () => {
6020
- if (admission.action === "queue") {
6021
- const sessionId = lane.provider_session_id;
6022
- if (!sessionId) {
6023
- throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
6024
- lane_id: laneId,
6025
- provider: lane.provider,
6026
- });
6027
- }
6028
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
6029
- queuedOnAdmission = true;
6030
- return casUpdateLane(hqRoot, laneId, (current) => {
6031
- if (current.version !== lane.version || current.state !== lane.state) {
6032
- throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
6033
- lane_id: laneId,
6034
- prepared_version: lane.version,
6035
- version: current.version,
6036
- state: current.state,
6037
- });
6038
- }
6039
- return {
6040
- ...current,
6041
- state: "queued",
6042
- queued_reason: admission.reason ?? "throttle",
6043
- queued_at: at,
6044
- pending_resume: {
6045
- session_id: sessionId,
6046
- trigger: "message",
6047
- text: deliveryText,
6048
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
6049
- message_id: messageId,
6050
- compact: opts.compact !== false,
6051
- at,
6052
- },
6053
- };
6054
- });
6055
- }
6056
- const started = spawnLaneWorker(hqRoot, lane, {
6057
- env: ctx.env,
6058
- modelDeps: {
6059
- readFileSync: ctx.readFileSync,
6060
- probe: ctx.probeProvider,
6061
- env: ctx.env,
6062
- },
6063
- existsSync: ctx.existsSync,
6064
- spawnProcess: ctx.spawnProcess,
6065
- proveDetached: ctx.proveDetached,
6066
- killProcess: ctx.killProcess,
6067
- now: ctx.now,
6068
- resolveMachineIdentity: ctx.resolveMachineIdentity,
6069
- runCompaction: ctx.runCompaction,
6070
- locateTranscript: ctx.locateTranscript,
6071
- }, {
6072
- trigger: {
6073
- kind: "message",
6074
- text: deliveryText,
6075
- message_id: messageId,
6076
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
6077
- },
6078
- compact: opts.compact !== false,
6079
- effort: resumeEffort ?? recordedResumeEffort(lane),
6080
- });
6081
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
6082
- return started;
6083
- }, hireGrantContext);
6084
- }, ctx.sleep);
6085
- },
6086
- }, mechanism);
6087
- if (dispatched.mechanism === "resume")
6088
- resumed = dispatched.value;
6089
- markAccepted();
6090
- }
6091
- // Every accepted message moves the compaction reference forward.
6092
- let next = casUpdateLane(hqRoot, laneId, (current) => ({
6093
- ...resetKeepAliveProgress(current),
6094
- last_message_at: at,
6095
- }));
6096
- if (projectId !== lane.project_id || storyId !== lane.story_id) {
6097
- next = casUpdateLane(hqRoot, laneId, (current) => {
6098
- const already = current.attribution_history.some((entry) => entry.message_id === messageId);
6099
- if (already)
6100
- return current;
6101
- return {
6102
- ...current,
6103
- project_id: projectId,
6104
- story_id: storyId,
6105
- no_story_reason: storyId ? undefined : current.no_story_reason,
6106
- stories: laneStory(current, projectId, storyId)
6107
- ? laneStories(current)
6108
- : [
6109
- ...laneStories(current),
6110
- {
6111
- project_id: projectId,
6112
- story_id: storyId,
6113
- status: "in_progress",
6114
- added_at: at,
6115
- updated_at: at,
6116
- },
6117
- ],
6118
- attribution_history: appendCappedHistory(current.attribution_history, {
6119
- project_id: projectId,
6120
- story_id: storyId,
6121
- message_id: messageId,
6122
- at,
6123
- }),
6124
- };
6125
- });
6126
- }
6127
- // What this command can claim when it returns: the message is durably
6128
- // written (accepted). Delivery to the model is asynchronous in both
6129
- // mechanisms. `dropbox` delivers on the worker's next tool call, and only
6130
- // if the drain hook is registered in the account home the worker was
6131
- // started from; `resume` has started a new worker with the message as its
6132
- // prompt. Neither is "delivered", so the response never says so.
6133
- const drainHook = drainHookStatusFor(ctx, next);
6134
- const deliveryStatus = move?.queued
6135
- ? "queued_on_target_account"
6136
- : queuedOnAdmission
6137
- ? "queued_on_admission"
6138
- : mechanism === "dropbox"
6139
- ? "awaiting_next_tool_call"
6140
- : "worker_spawned";
6141
- const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
6142
- ? `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.`
6143
- : mechanism === "dropbox" && next.stalled === true
6144
- ? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
6145
- : null;
6146
- if (!opts.silent)
6147
- printJson({
6148
- ok: true,
6149
- action: "message",
6150
- lane_id: laneId,
6151
- message_id: messageId,
6152
- accepted: true,
6153
- delivered: false,
6154
- delivery: mechanism,
6155
- delivery_status: deliveryStatus,
6156
- inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
6157
- drain_hook: drainHook,
6158
- warning,
6159
- resumed_session_id: resumed?.last_resume?.session_id ?? null,
6160
- compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
6161
- ? resumed.last_compaction
6162
- : null,
6163
- account_move: move ? accountMoveSummary(move) : null,
6164
- project_id: next.project_id,
6165
- story_id: next.story_id,
6166
- stories: laneStories(next),
6167
- story_warning: storyWarning(next) ?? null,
6168
- lane: next,
6169
- });
6170
- }
6171
- catch (err) {
6172
- if (claimedMessages.length > 0 && !accepted) {
6173
- const error = err instanceof Error ? err.message : String(err);
6174
- writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
6175
- event: "message_recovery_failed",
6176
- at: ctx.now().toISOString(),
6177
- state: lane.state,
6178
- error,
6179
- message_count: claimedMessages.length,
6180
- }, WATCHER_OUT_MAX_BYTES);
6181
- try {
6182
- requeueDropboxMessages(hqRoot, laneId, claimedMessages);
6183
- }
6184
- catch (requeueErr) {
6185
- const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
6186
- writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
6187
- event: "message_requeue_failed",
6188
- at: ctx.now().toISOString(),
6189
- state: lane.state,
6190
- recovery_error: error,
6191
- requeue_error: requeueMessage,
6192
- message_count: claimedMessages.length,
6193
- }, WATCHER_OUT_MAX_BYTES);
6194
- throw new LanesError("message_requeue_failed", `Could not return ${claimedMessages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: claimedMessages.length, recovery_error: error });
6195
- }
6196
- }
6197
- throw err;
6198
- }
6199
- }
6200
- async function messageLaneExclusion(ctx, lane) {
6201
- if (lane.state === "killed" || lane.state === "cancelled") {
6202
- return { lane, exclusion: lane.state };
6203
- }
6204
- const lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
6205
- company: lane.company,
6206
- env: ctx.env,
6207
- });
6208
- const currentLane = readLane(ctx.hqRoot(), lane.lane_id);
6209
- if (currentLane.state === "killed" || currentLane.state === "cancelled") {
6210
- return { lane: currentLane, exclusion: currentLane.state };
6211
- }
6212
- const lanes = loadAllLanes(ctx.hqRoot());
6213
- const retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
6214
- return {
6215
- lane: currentLane,
6216
- exclusion: keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware),
6217
- };
6218
- }
6219
- function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
6220
- writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
6221
- event: "message_undeliverable",
6222
- at: ctx.now().toISOString(),
6223
- state: lane.state,
6224
- reason,
6225
- ...(messageCount === undefined ? {} : { message_count: messageCount }),
6226
- }, WATCHER_OUT_MAX_BYTES);
6227
- }
6228
- /**
6229
- * Watcher and reconcile call the hidden command below after they win the
6230
- * terminal transition. The inbox is claimed once with the normal atomic
6231
- * dropbox drain, and an accepted message is routed through runMessage so the
6232
- * existing provider, admission, account, worker-profile and senior checks
6233
- * remain authoritative.
6234
- */
6235
- async function recoverQueuedMessages(ctx, laneId) {
6236
- const hqRoot = ctx.hqRoot();
6237
- let lane = readLane(hqRoot, laneId);
6238
- if (!dropboxInboxHasMessages(hqRoot, laneId))
6239
- return { action: "none", message_count: 0 };
6240
- const eligibility = await messageLaneExclusion(ctx, lane);
6241
- lane = eligibility.lane;
6242
- const exclusion = eligibility.exclusion;
6243
- const refusal = exclusion ?? (lane.state === "done"
6244
- ? "done_before_envelope"
6245
- : lane.state === "awaiting_input"
6246
- ? "awaiting_input_requires_question_answer"
6247
- : laneDeliveryMechanism(lane) !== "resume"
6248
- ? `no_resume_path_for_${lane.state}`
6249
- : undefined);
6250
- if (refusal !== undefined) {
6251
- const messages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
6252
- if (messages.length > 0)
6253
- recordMessageUndeliverable(ctx, lane, refusal, messages.length);
6254
- return { action: messages.length > 0 ? "undeliverable" : "none", message_count: messages.length };
6255
- }
6256
- const messages = drainDropbox(hqRoot, laneId);
6257
- if (messages.length === 0)
6258
- return { action: "none", message_count: 0 };
6259
- let accepted = false;
6260
- try {
6261
- await runMessage(ctx, laneId, {
6262
- text: formatDropboxMessages(messages),
6263
- compact: false,
6264
- silent: true,
6265
- onAccepted: () => { accepted = true; },
6266
- });
6267
- return { action: "resumed", message_count: messages.length };
6268
- }
6269
- catch (err) {
6270
- const error = err instanceof Error ? err.message : String(err);
6271
- writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
6272
- event: "message_recovery_failed",
6273
- at: ctx.now().toISOString(),
6274
- state: lane.state,
6275
- error,
6276
- message_count: messages.length,
6277
- }, WATCHER_OUT_MAX_BYTES);
6278
- if (!accepted) {
6279
- try {
6280
- requeueDropboxMessages(hqRoot, laneId, messages);
6281
- }
6282
- catch (requeueErr) {
6283
- const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
6284
- writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
6285
- event: "message_requeue_failed",
6286
- at: ctx.now().toISOString(),
6287
- state: lane.state,
6288
- recovery_error: error,
6289
- requeue_error: requeueMessage,
6290
- message_count: messages.length,
6291
- }, WATCHER_OUT_MAX_BYTES);
6292
- throw new LanesError("message_requeue_failed", `Could not return ${messages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: messages.length, recovery_error: error });
6293
- }
6294
- }
6295
- throw err;
6296
- }
6297
- }
6298
- function accountMoveSummary(move) {
6299
- return {
6300
- from: move.from,
6301
- to: move.to,
6302
- queued: move.queued,
6303
- copied: move.copy.copied,
6304
- sources: move.copy.sources,
6305
- targets: move.copy.targets,
6306
- source_dir: move.copy.source_dir,
6307
- target_dir: move.copy.target_dir,
6308
- };
6309
- }
6310
5679
  function persistClaudeAccountSelection(hqRoot, lane, selection) {
6311
5680
  return casUpdateLane(hqRoot, lane.lane_id, (current) => {
6312
5681
  if (current.version !== lane.version || current.state !== lane.state || current.account !== lane.account) {
@@ -6321,69 +5690,6 @@ function persistClaudeAccountSelection(hqRoot, lane, selection) {
6321
5690
  return { ...current, account_selection: selection };
6322
5691
  });
6323
5692
  }
6324
- async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
6325
- const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
6326
- assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
6327
- if (lane.provider !== "claude") {
6328
- throw new LanesError("claude_account_balance_provider_mismatch", `Lane ${laneId} does not use Claude`, {
6329
- lane_id: laneId,
6330
- provider: lane.provider,
6331
- });
6332
- }
6333
- const expectedFrom = opts.fromAccount?.trim();
6334
- if (!expectedFrom) {
6335
- throw new LanesError("claude_account_balance_source_invalid", "Balance move requires --from-account <id>", {
6336
- lane_id: laneId,
6337
- });
6338
- }
6339
- if (lane.account !== expectedFrom) {
6340
- throw new LanesError("claude_account_balance_source_changed", `Lane ${laneId} no longer uses the planned source account`, {
6341
- lane_id: laneId,
6342
- expected_account: expectedFrom,
6343
- actual_account: lane.account ?? null,
6344
- });
6345
- }
6346
- const expectedState = opts.expectedState?.trim();
6347
- if (!expectedState) {
6348
- throw new LanesError("claude_account_balance_state_invalid", "Balance move requires --expected-state <state>", {
6349
- lane_id: laneId,
6350
- });
6351
- }
6352
- if (lane.state !== expectedState) {
6353
- throw new LanesError("claude_account_balance_state_changed", `Lane ${laneId} changed state after balance was calculated`, {
6354
- lane_id: laneId,
6355
- expected_state: expectedState,
6356
- actual_state: lane.state,
6357
- });
6358
- }
6359
- if (!BALANCE_MOVABLE_STATES.includes(lane.state) || lane.pending_resume !== undefined) {
6360
- throw new LanesError("claude_account_balance_state_not_movable", `Lane ${laneId} is not between turns`, {
6361
- lane_id: laneId,
6362
- state: lane.state,
6363
- pending_resume: lane.pending_resume !== undefined,
6364
- });
6365
- }
6366
- const account = opts.account?.trim();
6367
- if (!account) {
6368
- throw new LanesError("claude_account_balance_account_invalid", "Balance move requires --account <id>", {
6369
- lane_id: laneId,
6370
- });
6371
- }
6372
- const moved = await prepareAccountMove(ctx, lane, {
6373
- account,
6374
- cliModel: undefined,
6375
- accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
6376
- deferTurnStart: true,
6377
- });
6378
- printJson({
6379
- ok: true,
6380
- action: "balance-move-target",
6381
- lane_id: laneId,
6382
- from: moved.from,
6383
- state: moved.lane.state,
6384
- account: moved.lane.account,
6385
- });
6386
- }
6387
5693
  function automaticBalanceMoveHistoryReason(ctx, lane) {
6388
5694
  const hqRoot = ctx.hqRoot();
6389
5695
  const config = loadLanesConfig(hqRoot);
@@ -6432,384 +5738,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6432
5738
  : {}),
6433
5739
  };
6434
5740
  }
6435
- /**
6436
- * `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
6437
- * continues a lane in `interrupted`, `done` or `blocked`, or a failed lane
6438
- * whose only terminal reason is an invalid envelope after its repair turn,
6439
- * on its recorded
6440
- * provider session. `hq lanes message` keeps refusing `interrupted`; this
6441
- * command is the path its refusal names. The launch is the same
6442
- * `spawnLaneWorker` a message resume uses, so the resume-time compaction
6443
- * rule and the watcher's mesh events are the same. The record gains
6444
- * `last_resume` with `trigger: "resume"`, `resumed_from` and the text.
6445
- */
6446
- async function runResume(ctx, laneId, opts) {
6447
- const hqRoot = ctx.hqRoot();
6448
- let lane = readLane(hqRoot, laneId);
6449
- const failedResumeReason = opts.reason?.trim();
6450
- if (opts.reason !== undefined && opts.fromFailed !== true) {
6451
- throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
6452
- }
6453
- if (opts.fromFailed === true && !failedResumeReason) {
6454
- throw new LanesError("failed_resume_reason_required", "--from-failed requires a non-empty --reason", { lane_id: laneId });
6455
- }
6456
- if (opts.reason !== undefined)
6457
- assertNoSecretText("resume reason", opts.reason);
6458
- if (opts.failedResumeContext !== undefined && opts.fromFailed !== true) {
6459
- throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context requires --from-failed", { lane_id: laneId });
6460
- }
6461
- const failedResumeActor = opts.fromFailed === true
6462
- ? authorizeFailedResume(ctx, lane, opts.failedResumeContext)
6463
- : undefined;
6464
- if (opts.fromFailed === true && lane.state !== "failed") {
6465
- throw new LanesError("lane_not_failed", `Lane ${laneId} is ${lane.state}; --from-failed only applies to a failed lane`, { lane_id: laneId, state: lane.state });
6466
- }
6467
- const failedResumeExpectedSenior = opts.fromFailed === true
6468
- ? { ...lane.senior }
6469
- : undefined;
6470
- const recoveredFailure = opts.fromFailed === true
6471
- ? lane.terminal_reason ?? "failure_reason_unavailable"
6472
- : undefined;
6473
- const hireGrantContext = opts.grantContext === undefined
6474
- ? undefined
6475
- : (() => {
6476
- if (!ctx.isSsmOwnerDispatch()) {
6477
- throw new LanesError("hire_grant_dispatch_unauthorized", "Resume grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6478
- }
6479
- return parseOutpostHireGrantContext(opts.grantContext, laneId);
6480
- })();
6481
- if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
6482
- throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6483
- }
6484
- if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
6485
- await withResumeHireAdmission(ctx, lane, () => undefined);
6486
- await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
6487
- return;
6488
- }
6489
- // Fail before provider/session work if a terminal child would consume a
6490
- // slot that its senior has already assigned elsewhere.
6491
- await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
6492
- assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
6493
- const cliEffort = parseCliEffort(lane.provider, opts.effort);
6494
- if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
6495
- throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
6496
- }
6497
- lane = laneWithRecoveredProviderSession(hqRoot, lane);
6498
- if (opts.model !== undefined && opts.account === undefined) {
6499
- throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
6500
- }
6501
- if (opts.account !== undefined && lane.state === "running") {
6502
- throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
6503
- lane_id: laneId,
6504
- state: lane.state,
6505
- });
6506
- }
6507
- if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
6508
- throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
6509
- }
6510
- if (lane.state === "queued") {
6511
- 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}` });
6512
- }
6513
- const refusal = resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
6514
- if (refusal !== null) {
6515
- throw new LanesError(refusal.error, refusal.message, {
6516
- lane_id: laneId,
6517
- state: lane.state,
6518
- provider: lane.provider,
6519
- message: refusal.message,
6520
- });
6521
- }
6522
- assertLaneRunsHereForAdmission(ctx, lane);
6523
- const resumedFrom = lane.state;
6524
- const text = opts.text?.trim() ? opts.text : undefined;
6525
- const at = ctx.now().toISOString();
6526
- const projectId = opts.project?.trim() || lane.project_id;
6527
- const storyId = opts.story?.trim() || lane.story_id;
6528
- if (opts.story !== undefined && !opts.story.trim()) {
6529
- throw new LanesError("missing_story", "--story must be non-empty", {});
6530
- }
6531
- if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
6532
- validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
6533
- }
6534
- const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
6535
- const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
6536
- let requestedMeshCompany;
6537
- let meshThreadId;
6538
- if (opts.meshCompany !== undefined) {
6539
- const meshCompanySlug = opts.meshCompany.trim();
6540
- if (!meshCompanySlug) {
6541
- throw new LanesError("missing_mesh_company", "--mesh-company must be a non-empty company slug", {});
6542
- }
6543
- requestedMeshCompany = await ctx.resolveCompany(meshCompanySlug);
6544
- if (lane.company !== null &&
6545
- (lane.company.uid !== requestedMeshCompany.uid || lane.company.slug !== requestedMeshCompany.slug)) {
6546
- throw new LanesError("mesh_company_mismatch", "--mesh-company must match the lane's company when the lane is company-owned", { company: lane.company.slug, mesh_company: requestedMeshCompany.slug });
6547
- }
6548
- meshThreadId = await ctx.mesh.ensureProjectThread(requestedMeshCompany, projectId);
6549
- }
6550
- const resumeProfile = opts.worker !== undefined
6551
- ? resolveResumeProfile(ctx, lane, opts.worker)
6552
- : undefined;
6553
- const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
6554
- const keepAlive = opts.keepAlive === true
6555
- ? true
6556
- : resumeProfile?.profile.meta.keep_alive;
6557
- const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
6558
- if (keepCurrentAccount)
6559
- assertCurrentResumeAccountAvailable(ctx, lane);
6560
- if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
6561
- validateResumeProfileModel(ctx, lane, resumeProfile.profile);
6562
- }
6563
- const automaticAccountSelection = opts.account === undefined
6564
- ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
6565
- : undefined;
6566
- const resumeEffort = resolveResumeEffort(ctx, lane, {
6567
- cliEffort,
6568
- profile: resumeProfile,
6569
- account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
6570
- model: opts.model,
6571
- });
6572
- let move = null;
6573
- if (opts.account !== undefined && !keepCurrentAccount) {
6574
- move = await prepareAccountMove(ctx, lane, {
6575
- account: opts.account,
6576
- cliModel: opts.model?.trim() || undefined,
6577
- ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6578
- ...(profileRepin !== undefined ? { profileRepin } : {}),
6579
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6580
- pending: {
6581
- trigger: "resume",
6582
- resumed_from: resumedFrom,
6583
- ...(opts.fromFailed === true ? { from_failed: true } : {}),
6584
- ...(failedResumeExpectedSenior !== undefined
6585
- ? { expected_senior: failedResumeExpectedSenior }
6586
- : {}),
6587
- ...(text !== undefined ? { text } : {}),
6588
- ...(messageId !== undefined ? { message_id: messageId } : {}),
6589
- compact: opts.compact !== false,
6590
- },
6591
- });
6592
- lane = move.lane;
6593
- }
6594
- else if (automaticAccountSelection !== undefined) {
6595
- const selection = automaticAccountSelection.selection;
6596
- if (selection.account !== lane.account) {
6597
- move = await prepareAccountMove(ctx, lane, {
6598
- account: selection.account,
6599
- cliModel: undefined,
6600
- ...(automaticAccountSelection.historyReason !== undefined
6601
- ? { accountHistoryReason: automaticAccountSelection.historyReason }
6602
- : {}),
6603
- ...(automaticAccountSelection.preservedModel !== undefined
6604
- ? { preservedModel: automaticAccountSelection.preservedModel }
6605
- : {}),
6606
- ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6607
- ...(profileRepin !== undefined ? { profileRepin } : {}),
6608
- ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6609
- pending: {
6610
- trigger: "resume",
6611
- resumed_from: resumedFrom,
6612
- ...(opts.fromFailed === true ? { from_failed: true } : {}),
6613
- ...(failedResumeExpectedSenior !== undefined
6614
- ? { expected_senior: failedResumeExpectedSenior }
6615
- : {}),
6616
- ...(text !== undefined ? { text } : {}),
6617
- ...(messageId !== undefined ? { message_id: messageId } : {}),
6618
- compact: opts.compact !== false,
6619
- },
6620
- accountSelection: selection,
6621
- });
6622
- lane = move.lane;
6623
- }
6624
- else {
6625
- lane = persistClaudeAccountSelection(hqRoot, lane, selection);
6626
- }
6627
- }
6628
- lane = recordResumeSetup(ctx, lane, {
6629
- ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
6630
- ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
6631
- ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6632
- ...(keepAlive !== undefined ? { keepAlive } : {}),
6633
- ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
6634
- ...(requestedMeshCompany !== undefined
6635
- ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
6636
- : {}),
6637
- });
6638
- let queuedOnAdmission = false;
6639
- let resumed;
6640
- if (move?.queued) {
6641
- resumed = lane;
6642
- }
6643
- else {
6644
- resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
6645
- const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
6646
- if (admission.action === "refuse" && admission.error)
6647
- throw admission.error;
6648
- return withResumeHireAdmission(ctx, lane, () => {
6649
- if (admission.action === "queue") {
6650
- const sessionId = lane.provider_session_id;
6651
- if (!sessionId) {
6652
- throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
6653
- lane_id: laneId,
6654
- provider: lane.provider,
6655
- });
6656
- }
6657
- queuedOnAdmission = true;
6658
- return casUpdateLane(hqRoot, laneId, (current) => {
6659
- if (current.version !== lane.version || current.state !== lane.state) {
6660
- throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
6661
- lane_id: laneId,
6662
- prepared_version: lane.version,
6663
- version: current.version,
6664
- state: current.state,
6665
- });
6666
- }
6667
- return {
6668
- ...current,
6669
- state: "queued",
6670
- queued_reason: admission.reason ?? "throttle",
6671
- queued_at: at,
6672
- pending_resume: {
6673
- session_id: sessionId,
6674
- trigger: "resume",
6675
- resumed_from: resumedFrom,
6676
- ...(opts.fromFailed === true ? { from_failed: true } : {}),
6677
- ...(failedResumeExpectedSenior !== undefined
6678
- ? { expected_senior: failedResumeExpectedSenior }
6679
- : {}),
6680
- ...(text !== undefined ? { text } : {}),
6681
- ...(messageId !== undefined ? { message_id: messageId } : {}),
6682
- compact: opts.compact !== false,
6683
- at,
6684
- },
6685
- };
6686
- });
6687
- }
6688
- return spawnLaneWorker(hqRoot, lane, {
6689
- env: ctx.env,
6690
- modelDeps: {
6691
- readFileSync: ctx.readFileSync,
6692
- probe: ctx.probeProvider,
6693
- env: ctx.env,
6694
- },
6695
- existsSync: ctx.existsSync,
6696
- spawnProcess: ctx.spawnProcess,
6697
- proveDetached: ctx.proveDetached,
6698
- killProcess: ctx.killProcess,
6699
- now: ctx.now,
6700
- resolveMachineIdentity: ctx.resolveMachineIdentity,
6701
- runCompaction: ctx.runCompaction,
6702
- locateTranscript: ctx.locateTranscript,
6703
- }, {
6704
- trigger: {
6705
- kind: "resume",
6706
- from: resumedFrom,
6707
- ...(opts.fromFailed === true ? { fromFailed: true } : {}),
6708
- ...(failedResumeExpectedSenior !== undefined
6709
- ? { expectedSenior: failedResumeExpectedSenior }
6710
- : {}),
6711
- ...(text !== undefined ? { text } : {}),
6712
- ...(messageId !== undefined ? { message_id: messageId } : {}),
6713
- },
6714
- compact: opts.compact !== false,
6715
- effort: resumeEffort ?? recordedResumeEffort(lane),
6716
- });
6717
- }, hireGrantContext);
6718
- }, ctx.sleep);
6719
- }
6720
- if (text !== undefined && messageId !== undefined) {
6721
- // Same audit copy a message resume leaves; nothing drains it. Archive only
6722
- // after a successful spawn so a concurrent resume cannot leave a phantom.
6723
- archiveResumeMessage(hqRoot, laneId, {
6724
- at,
6725
- message_id: messageId,
6726
- text,
6727
- project_id: projectId,
6728
- story_id: storyId,
6729
- resumed_from: resumedFrom,
6730
- });
6731
- }
6732
- let next = resumed;
6733
- if (failedResumeActor !== undefined &&
6734
- failedResumeReason !== undefined &&
6735
- recoveredFailure !== undefined) {
6736
- next = casUpdateLane(hqRoot, laneId, (current) => ({
6737
- ...current,
6738
- failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
6739
- at,
6740
- actor: failedResumeActor,
6741
- reason: failedResumeReason,
6742
- failure: recoveredFailure,
6743
- }),
6744
- }));
6745
- }
6746
- if (text !== undefined) {
6747
- // An operator text moves the compaction reference forward, as a message does.
6748
- next = casUpdateLane(hqRoot, laneId, (current) => ({
6749
- ...resetKeepAliveProgress(current),
6750
- last_message_at: at,
6751
- }));
6752
- }
6753
- if (messageId !== undefined && attributionChanged) {
6754
- next = casUpdateLane(hqRoot, laneId, (current) => {
6755
- const already = current.attribution_history.some((entry) => entry.message_id === messageId);
6756
- if (already)
6757
- return current;
6758
- return {
6759
- ...current,
6760
- project_id: projectId,
6761
- story_id: storyId,
6762
- no_story_reason: storyId ? undefined : current.no_story_reason,
6763
- stories: laneStory(current, projectId, storyId)
6764
- ? laneStories(current)
6765
- : [
6766
- ...laneStories(current),
6767
- {
6768
- project_id: projectId,
6769
- story_id: storyId,
6770
- status: "in_progress",
6771
- added_at: at,
6772
- updated_at: at,
6773
- },
6774
- ],
6775
- attribution_history: appendCappedHistory(current.attribution_history, {
6776
- project_id: projectId,
6777
- story_id: storyId,
6778
- message_id: messageId,
6779
- at,
6780
- }),
6781
- };
6782
- });
6783
- }
6784
- if (text === undefined && hasKeepAliveProgress(next)) {
6785
- next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
6786
- }
6787
- printJson({
6788
- ok: true,
6789
- action: "resume",
6790
- lane_id: laneId,
6791
- resumed_from: resumedFrom,
6792
- message_id: messageId ?? null,
6793
- text: text ?? null,
6794
- delivery: "resume",
6795
- delivery_status: move?.queued
6796
- ? "queued_on_target_account"
6797
- : queuedOnAdmission
6798
- ? "queued_on_admission"
6799
- : "worker_spawned",
6800
- account_move: move ? accountMoveSummary(move) : null,
6801
- resumed_session_id: resumed.last_resume?.session_id ?? null,
6802
- last_resume: next.last_resume ?? null,
6803
- compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
6804
- ? resumed.last_compaction
6805
- : null,
6806
- project_id: next.project_id,
6807
- story_id: next.story_id,
6808
- stories: laneStories(next),
6809
- story_warning: storyWarning(next) ?? null,
6810
- lane: next,
6811
- });
6812
- }
6813
5741
  function storyCommandTarget(lane, storyId, project) {
6814
5742
  const projectId = project?.trim() || lane.project_id;
6815
5743
  const normalizedStoryId = storyId.trim();
@@ -7911,7 +6839,7 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
7911
6839
  * Record is already durable. Delivery is best-effort onto the asking lane's
7912
6840
  * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
7913
6841
  */
7914
- async function runReconcile(ctx, opts) {
6842
+ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7915
6843
  const fromEnv = ctx.env.HQ_ROOT?.trim();
7916
6844
  const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7917
6845
  const summary = await runReconcilePass(hqRoot, {
@@ -7938,9 +6866,7 @@ async function runReconcile(ctx, opts) {
7938
6866
  livenessDeps: livenessDepsFromCtx(ctx),
7939
6867
  presenceConnected: ctx.presenceConnected,
7940
6868
  resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
7941
- recoverQueuedMessages: async (lane) => {
7942
- await recoverQueuedMessages(ctx, lane.lane_id);
7943
- },
6869
+ recoverQueuedMessages,
7944
6870
  mesh: ctx.mesh,
7945
6871
  env: ctx.env,
7946
6872
  });
@@ -8057,6 +6983,51 @@ export function registerLanesCommand(parent, deps = {}) {
8057
6983
  seniorOfSession,
8058
6984
  writeAnswerDeliveryFailed,
8059
6985
  });
6986
+ const messageHandlerDeps = {
6987
+ archiveResumeMessage,
6988
+ assertLaneRunsHereForAdmission,
6989
+ assertLaneRunsHereForOperatorMutation,
6990
+ assessLaneAdmission,
6991
+ casUpdateLane,
6992
+ dispatchOutpostLaneCommand,
6993
+ drainHookStatusFor,
6994
+ laneIsHostedHere,
6995
+ laneWithRecoveredProviderSession,
6996
+ loadAllLanes,
6997
+ mintMessageId,
6998
+ prepareAccountMove,
6999
+ persistClaudeAccountSelection,
7000
+ printJson,
7001
+ recordResumeSetup,
7002
+ resolveActiveQuestionCompany,
7003
+ resolveResumeProfile,
7004
+ selectAutomaticClaudeResumeAccount,
7005
+ validateResumeProfileModel,
7006
+ withResumeHireAdmission,
7007
+ };
7008
+ const messageHandlers = createLaneMessageHandlers(messageHandlerDeps);
7009
+ const resumeHandlers = createLaneResumeHandlers({
7010
+ archiveResumeMessage,
7011
+ assertLaneRunsHereForAdmission,
7012
+ assertLaneRunsHereForOperatorMutation,
7013
+ assessLaneAdmission,
7014
+ automaticBalanceMoveHistoryReason,
7015
+ authorizeFailedResume,
7016
+ authorizeResumeProfileRepin,
7017
+ casUpdateLane,
7018
+ dispatchOutpostLaneCommand,
7019
+ laneIsHostedHere,
7020
+ laneWithRecoveredProviderSession,
7021
+ mintMessageId,
7022
+ prepareAccountMove,
7023
+ persistClaudeAccountSelection,
7024
+ printJson,
7025
+ recordResumeSetup,
7026
+ resolveResumeProfile,
7027
+ selectAutomaticClaudeResumeAccount,
7028
+ validateResumeProfileModel,
7029
+ withResumeHireAdmission,
7030
+ });
8060
7031
  const lanes = parent
8061
7032
  .command("lanes", { hidden: true })
8062
7033
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -8243,7 +7214,7 @@ export function registerLanesCommand(parent, deps = {}) {
8243
7214
  .requiredOption("--from-account <id>", "Expected source Claude account id")
8244
7215
  .requiredOption("--expected-state <state>", "Expected lane state from the balance plan")
8245
7216
  .action(wrap(async (laneId, opts) => {
8246
- await runClaudeBalanceMoveTarget(ctx, laneId, opts);
7217
+ await resumeHandlers.runClaudeBalanceMoveTarget(ctx, laneId, opts);
8247
7218
  }));
8248
7219
  lanes
8249
7220
  .command("admit")
@@ -8344,13 +7315,13 @@ export function registerLanesCommand(parent, deps = {}) {
8344
7315
  .addOption(new Option("--grant-context <base64url>").hideHelp())
8345
7316
  .option("--no-compact", "Skip the resume-time compaction check for this message")
8346
7317
  .action(wrap(async (laneId, opts) => {
8347
- await runMessage(ctx, laneId, opts);
7318
+ await messageHandlers.runMessage(ctx, laneId, opts);
8348
7319
  }));
8349
7320
  lanes
8350
7321
  .command("_recover-queued-messages", { hidden: true })
8351
7322
  .argument("<lane-id>", "Lane id")
8352
7323
  .action(wrap(async (laneId) => {
8353
- printJson(await recoverQueuedMessages(ctx, laneId));
7324
+ printJson(await messageHandlers.recoverQueuedMessages(ctx, laneId));
8354
7325
  }));
8355
7326
  lanes
8356
7327
  .command("_monitor-message", { hidden: true })
@@ -8375,7 +7346,7 @@ export function registerLanesCommand(parent, deps = {}) {
8375
7346
  Number(value.sequence) < 1) {
8376
7347
  throw new LanesError("monitor_lane_payload_invalid", "Monitor lane delivery payload is missing text, monitor metadata, or sequence.", {});
8377
7348
  }
8378
- await runMessage(ctx, laneId, {
7349
+ await messageHandlers.runMessage(ctx, laneId, {
8379
7350
  text: value.text,
8380
7351
  monitorEvent: {
8381
7352
  monitor_id: value.monitor_id,
@@ -8404,7 +7375,7 @@ export function registerLanesCommand(parent, deps = {}) {
8404
7375
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
8405
7376
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
8406
7377
  .action(wrap(async (laneId, opts) => {
8407
- await runResume(ctx, laneId, opts);
7378
+ await resumeHandlers.runResume(ctx, laneId, opts);
8408
7379
  }));
8409
7380
  lanes
8410
7381
  .command("reparent")
@@ -8590,7 +7561,9 @@ export function registerLanesCommand(parent, deps = {}) {
8590
7561
  .description("Run one machine-owned reconcile pass and exit")
8591
7562
  .option("--dry-run", "Report what would change and write nothing")
8592
7563
  .action(wrap(async (opts) => {
8593
- await runReconcile(ctx, opts);
7564
+ await runReconcile(ctx, opts, async (lane) => {
7565
+ await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
7566
+ });
8594
7567
  }));
8595
7568
  reconcile
8596
7569
  .command("install")