@indigoai-us/hq-cli 5.247.0 → 5.248.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/assets/scaffold/core/scripts/hq-session.sh +2 -2
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +1 -1
- package/dist/commands/bot.js +10 -4
- package/dist/commands/lanes-message.d.ts +9 -0
- package/dist/commands/lanes-message.js +558 -0
- package/dist/commands/lanes-resume.d.ts +6 -0
- package/dist/commands/lanes-resume.js +455 -0
- package/dist/commands/lanes-shared.d.ts +153 -1
- package/dist/commands/lanes-shared.js +94 -1
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +59 -1086
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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")
|