@indigoai-us/hq-cli 5.224.0 → 5.226.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 +46 -1
- package/dist/command-catalog.generated.d.ts +94 -0
- package/dist/command-catalog.generated.js +120 -0
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +6 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +17 -2
- package/dist/commands/cloud.d.ts +10 -0
- package/dist/commands/cloud.js +52 -47
- package/dist/commands/lanes.d.ts +5 -0
- package/dist/commands/lanes.js +498 -312
- package/dist/commands/staff/reset-test-account.d.ts +110 -0
- package/dist/commands/staff/reset-test-account.js +268 -0
- package/dist/commands/staff/test-account.d.ts +51 -0
- package/dist/commands/staff/test-account.js +196 -0
- package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +24 -0
- package/dist/lib/bot/inbox-poll-backoff-flag.js +132 -0
- package/dist/lib/bot/run.d.ts +3 -1
- package/dist/lib/bot/run.js +89 -11
- package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
- package/dist/lib/doctor/client-health-flag-token.js +66 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
- package/dist/lib/lanes/dropbox.d.ts +9 -0
- package/dist/lib/lanes/dropbox.js +70 -1
- package/dist/lib/lanes/reconcile.d.ts +2 -0
- package/dist/lib/lanes/reconcile.js +12 -0
- package/dist/lib/lanes/watcher-main.js +16 -0
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +77 -3
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +198 -3
- package/dist/utils/upload-author.d.ts +15 -0
- package/dist/utils/upload-author.js +39 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -51,12 +51,13 @@ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/c
|
|
|
51
51
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
52
52
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
53
53
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
54
|
-
import { formatMonitorEvent } from "../lib/lanes/dropbox.js";
|
|
54
|
+
import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
|
|
55
|
+
import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
|
|
55
56
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
56
57
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
57
58
|
import { ensureSessionHome } from "../lib/lanes/session-relink.js";
|
|
58
59
|
import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
|
|
59
|
-
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
60
|
+
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, readPidRecord, probePidRecord, readProcStarttime, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
60
61
|
import { handOffInstallHere } from "../lib/daemon/handoff.js";
|
|
61
62
|
export { spawnLaneWorker };
|
|
62
63
|
export { questionBodyAbsPath };
|
|
@@ -283,6 +284,8 @@ function resolveDeps(deps = {}) {
|
|
|
283
284
|
proveDetached: deps.proveDetached,
|
|
284
285
|
killProcess: deps.killProcess,
|
|
285
286
|
isAlive: deps.isAlive,
|
|
287
|
+
readStarttime: deps.readStarttime,
|
|
288
|
+
watcherLoopDeps: deps.watcherLoopDeps,
|
|
286
289
|
stopSleepMs: deps.stopSleepMs,
|
|
287
290
|
livenessDeps: deps.livenessDeps,
|
|
288
291
|
presenceConnected: deps.presenceConnected,
|
|
@@ -5552,7 +5555,13 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5552
5555
|
}
|
|
5553
5556
|
async function runMessage(ctx, laneId, opts) {
|
|
5554
5557
|
const hqRoot = ctx.hqRoot();
|
|
5555
|
-
|
|
5558
|
+
let accepted = false;
|
|
5559
|
+
let claimedMessages = [];
|
|
5560
|
+
const markAccepted = () => {
|
|
5561
|
+
accepted = true;
|
|
5562
|
+
opts.onAccepted?.();
|
|
5563
|
+
};
|
|
5564
|
+
let text = opts.text;
|
|
5556
5565
|
if (!text) {
|
|
5557
5566
|
throw new LanesError("missing_text", "--text is required", {});
|
|
5558
5567
|
}
|
|
@@ -5562,330 +5571,498 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5562
5571
|
return;
|
|
5563
5572
|
}
|
|
5564
5573
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5602
|
-
}
|
|
5603
|
-
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5604
|
-
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 });
|
|
5605
|
-
}
|
|
5606
|
-
if (cliEffort !== undefined && mechanism === "dropbox") {
|
|
5607
|
-
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 });
|
|
5608
|
-
}
|
|
5609
|
-
const resumeProfile = opts.worker !== undefined
|
|
5610
|
-
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5611
|
-
: undefined;
|
|
5612
|
-
if (opts.model !== undefined && opts.account === undefined) {
|
|
5613
|
-
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5614
|
-
}
|
|
5615
|
-
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5616
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5617
|
-
: undefined;
|
|
5618
|
-
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5619
|
-
cliEffort,
|
|
5620
|
-
profile: resumeProfile,
|
|
5621
|
-
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
|
|
5622
|
-
model: opts.model,
|
|
5623
|
-
});
|
|
5624
|
-
const messageId = mintMessageId();
|
|
5625
|
-
const at = ctx.now().toISOString();
|
|
5626
|
-
const projectId = opts.project?.trim() || lane.project_id;
|
|
5627
|
-
const storyId = opts.story?.trim() || lane.story_id;
|
|
5628
|
-
if (opts.story !== undefined && !opts.story.trim()) {
|
|
5629
|
-
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
5630
|
-
}
|
|
5631
|
-
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5632
|
-
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5574
|
+
if (lane.state === "running") {
|
|
5575
|
+
const workerPidPath = path.join(laneRunDir(hqRoot, laneId), "worker.pid");
|
|
5576
|
+
const workerRecordExists = fs.existsSync(workerPidPath);
|
|
5577
|
+
const worker = readPidRecord(workerPidPath);
|
|
5578
|
+
if (workerRecordExists && worker === null) {
|
|
5579
|
+
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 });
|
|
5580
|
+
}
|
|
5581
|
+
// A fresh spawn may have committed running before publishing worker.pid;
|
|
5582
|
+
// absence is not proof of exit. Reclassify only a recorded process that
|
|
5583
|
+
// fails the PID plus start-time identity probe.
|
|
5584
|
+
const processAlive = worker === null
|
|
5585
|
+
? true
|
|
5586
|
+
: probePidRecord(worker, ctx.isAlive ?? isPidAlive, ctx.readStarttime ?? readProcStarttime).alive;
|
|
5587
|
+
if (worker !== null && !processAlive) {
|
|
5588
|
+
// The watcher may not have reached its next poll yet. Run the same
|
|
5589
|
+
// classifier synchronously, then make the queue/resume choice again.
|
|
5590
|
+
await runWatcherLoop(hqRoot, laneId, {
|
|
5591
|
+
...ctx.watcherLoopDeps,
|
|
5592
|
+
now: () => ctx.now().getTime(),
|
|
5593
|
+
isAlive: ctx.isAlive ?? isPidAlive,
|
|
5594
|
+
readStarttime: ctx.readStarttime ?? readProcStarttime,
|
|
5595
|
+
mesh: null,
|
|
5596
|
+
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
5597
|
+
});
|
|
5598
|
+
lane = readLane(hqRoot, laneId);
|
|
5599
|
+
// This invocation won the race to classify the dead worker, so the
|
|
5600
|
+
// watcher callback did not own recovery. Fold any earlier queued
|
|
5601
|
+
// messages into this resume to keep one provider turn and one delivery.
|
|
5602
|
+
if (laneDeliveryMechanism(lane) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5603
|
+
const queuedMessages = drainDropbox(hqRoot, laneId);
|
|
5604
|
+
if (queuedMessages.length > 0) {
|
|
5605
|
+
claimedMessages = queuedMessages;
|
|
5606
|
+
text = `${formatDropboxMessages(queuedMessages)}\n\n${text}`;
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5633
5610
|
}
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
text,
|
|
5641
|
-
});
|
|
5642
|
-
const deliveryText = framedMonitorEvent ?? text;
|
|
5643
|
-
const dropboxMessage = {
|
|
5644
|
-
at,
|
|
5645
|
-
message_id: messageId,
|
|
5646
|
-
text,
|
|
5647
|
-
project_id: projectId,
|
|
5648
|
-
story_id: storyId,
|
|
5649
|
-
...(opts.monitorEvent === undefined ? {} : {
|
|
5650
|
-
kind: "monitor_event",
|
|
5651
|
-
monitor_id: opts.monitorEvent.monitor_id,
|
|
5652
|
-
description: opts.monitorEvent.description,
|
|
5653
|
-
sequence: opts.monitorEvent.sequence,
|
|
5654
|
-
}),
|
|
5655
|
-
};
|
|
5656
|
-
let resumed = null;
|
|
5657
|
-
let move = null;
|
|
5658
|
-
let queuedOnAdmission = false;
|
|
5659
|
-
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5660
|
-
// A moved message is always a resume on the target account: a running
|
|
5661
|
-
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5662
|
-
move = await prepareAccountMove(ctx, lane, {
|
|
5663
|
-
account: opts.account,
|
|
5664
|
-
cliModel: opts.model?.trim() || undefined,
|
|
5665
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5666
|
-
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5667
|
-
pending: {
|
|
5668
|
-
trigger: "message",
|
|
5669
|
-
text: deliveryText,
|
|
5670
|
-
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5671
|
-
message_id: messageId,
|
|
5672
|
-
compact: opts.compact !== false,
|
|
5673
|
-
},
|
|
5674
|
-
});
|
|
5675
|
-
lane = move.lane;
|
|
5676
|
-
if (move.queued) {
|
|
5677
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5678
|
-
}
|
|
5679
|
-
}
|
|
5680
|
-
else if (mechanism === "resume") {
|
|
5681
|
-
const automatic = automaticAccountSelection;
|
|
5682
|
-
if (automatic !== undefined) {
|
|
5683
|
-
const selection = automatic.selection;
|
|
5684
|
-
if (selection.account !== lane.account) {
|
|
5685
|
-
move = await prepareAccountMove(ctx, lane, {
|
|
5686
|
-
account: selection.account,
|
|
5687
|
-
cliModel: undefined,
|
|
5688
|
-
...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
|
|
5689
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5690
|
-
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5691
|
-
pending: {
|
|
5692
|
-
trigger: "message",
|
|
5693
|
-
text: deliveryText,
|
|
5694
|
-
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5695
|
-
message_id: messageId,
|
|
5696
|
-
compact: opts.compact !== false,
|
|
5697
|
-
},
|
|
5698
|
-
accountSelection: selection,
|
|
5699
|
-
});
|
|
5700
|
-
lane = move.lane;
|
|
5701
|
-
if (move.queued)
|
|
5702
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5611
|
+
try {
|
|
5612
|
+
const exclusion = messageLaneExclusion(ctx, lane);
|
|
5613
|
+
if (exclusion !== undefined) {
|
|
5614
|
+
let messageCount;
|
|
5615
|
+
if (dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5616
|
+
messageCount = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" }).length;
|
|
5703
5617
|
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5618
|
+
recordMessageUndeliverable(ctx, lane, exclusion, messageCount);
|
|
5619
|
+
const refusalMessage = deliveryRefusalReason(lane);
|
|
5620
|
+
throw new LanesError("lane_not_accepting_messages", refusalMessage || `Lane ${laneId} cannot receive messages: ${exclusion}`, { lane_id: laneId, state: lane.state, reason: exclusion });
|
|
5621
|
+
}
|
|
5622
|
+
if (lane.state === "done" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5623
|
+
const staleMessages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
|
|
5624
|
+
if (staleMessages.length > 0) {
|
|
5625
|
+
recordMessageUndeliverable(ctx, lane, "done_before_queued_message", staleMessages.length);
|
|
5706
5626
|
}
|
|
5707
5627
|
}
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5628
|
+
const cliEffort = parseCliEffort(lane.provider, opts.effort);
|
|
5629
|
+
if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
5630
|
+
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
5631
|
+
}
|
|
5632
|
+
// Check before transcript recovery, account probing, or message writes.
|
|
5633
|
+
// The final state transition repeats this check under the senior lock.
|
|
5634
|
+
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
5635
|
+
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
5636
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5637
|
+
if (keepCurrentAccount)
|
|
5638
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5639
|
+
// A real account move is refused on its own terms first: a running lane and
|
|
5640
|
+
// a provider without session resume get the move's named errors, not the
|
|
5641
|
+
// generic delivery refusal below. Naming the current account is a keep.
|
|
5642
|
+
if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
|
|
5643
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
5644
|
+
lane_id: laneId,
|
|
5645
|
+
state: lane.state,
|
|
5646
|
+
});
|
|
5647
|
+
}
|
|
5648
|
+
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
5649
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
5650
|
+
}
|
|
5651
|
+
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
5652
|
+
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
5653
|
+
const mechanism = laneDeliveryMechanism(lane);
|
|
5654
|
+
if (mechanism === "none") {
|
|
5655
|
+
const message = deliveryRefusalReason(lane);
|
|
5656
|
+
throw new LanesError("lane_not_accepting_messages", message, {
|
|
5657
|
+
lane_id: laneId,
|
|
5658
|
+
state: lane.state,
|
|
5659
|
+
provider: lane.provider,
|
|
5660
|
+
message,
|
|
5661
|
+
});
|
|
5662
|
+
}
|
|
5663
|
+
if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
|
|
5664
|
+
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5665
|
+
}
|
|
5666
|
+
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5667
|
+
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 });
|
|
5668
|
+
}
|
|
5669
|
+
if (cliEffort !== undefined && mechanism === "dropbox") {
|
|
5670
|
+
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 });
|
|
5671
|
+
}
|
|
5672
|
+
const resumeProfile = opts.worker !== undefined
|
|
5673
|
+
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5674
|
+
: undefined;
|
|
5675
|
+
if (opts.model !== undefined && opts.account === undefined) {
|
|
5676
|
+
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5677
|
+
}
|
|
5678
|
+
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5679
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5680
|
+
: undefined;
|
|
5681
|
+
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5682
|
+
cliEffort,
|
|
5683
|
+
profile: resumeProfile,
|
|
5684
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
|
|
5685
|
+
model: opts.model,
|
|
5717
5686
|
});
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5687
|
+
const messageId = mintMessageId();
|
|
5688
|
+
const at = ctx.now().toISOString();
|
|
5689
|
+
const projectId = opts.project?.trim() || lane.project_id;
|
|
5690
|
+
const storyId = opts.story?.trim() || lane.story_id;
|
|
5691
|
+
if (opts.story !== undefined && !opts.story.trim()) {
|
|
5692
|
+
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
5693
|
+
}
|
|
5694
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5695
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5696
|
+
}
|
|
5697
|
+
const framedMonitorEvent = opts.monitorEvent === undefined
|
|
5698
|
+
? undefined
|
|
5699
|
+
: formatMonitorEvent({
|
|
5700
|
+
monitorId: opts.monitorEvent.monitor_id,
|
|
5701
|
+
description: opts.monitorEvent.description,
|
|
5702
|
+
sequence: opts.monitorEvent.sequence,
|
|
5703
|
+
text,
|
|
5704
|
+
});
|
|
5705
|
+
const deliveryText = framedMonitorEvent ?? text;
|
|
5706
|
+
const dropboxMessage = {
|
|
5707
|
+
at,
|
|
5708
|
+
message_id: messageId,
|
|
5709
|
+
text,
|
|
5710
|
+
project_id: projectId,
|
|
5711
|
+
story_id: storyId,
|
|
5712
|
+
...(opts.monitorEvent === undefined ? {} : {
|
|
5713
|
+
kind: "monitor_event",
|
|
5714
|
+
monitor_id: opts.monitorEvent.monitor_id,
|
|
5715
|
+
description: opts.monitorEvent.description,
|
|
5716
|
+
sequence: opts.monitorEvent.sequence,
|
|
5717
|
+
}),
|
|
5718
|
+
};
|
|
5719
|
+
let resumed = null;
|
|
5720
|
+
let move = null;
|
|
5721
|
+
let queuedOnAdmission = false;
|
|
5722
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5723
|
+
// A moved message is always a resume on the target account: a running
|
|
5724
|
+
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5725
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
5726
|
+
account: opts.account,
|
|
5727
|
+
cliModel: opts.model?.trim() || undefined,
|
|
5728
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5729
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5730
|
+
pending: {
|
|
5731
|
+
trigger: "message",
|
|
5732
|
+
text: deliveryText,
|
|
5733
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5734
|
+
message_id: messageId,
|
|
5735
|
+
compact: opts.compact !== false,
|
|
5736
|
+
},
|
|
5737
|
+
});
|
|
5738
|
+
lane = move.lane;
|
|
5739
|
+
if (move.queued) {
|
|
5740
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
else if (mechanism === "resume") {
|
|
5744
|
+
const automatic = automaticAccountSelection;
|
|
5745
|
+
if (automatic !== undefined) {
|
|
5746
|
+
const selection = automatic.selection;
|
|
5747
|
+
if (selection.account !== lane.account) {
|
|
5748
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
5749
|
+
account: selection.account,
|
|
5750
|
+
cliModel: undefined,
|
|
5751
|
+
...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
|
|
5752
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5753
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5754
|
+
pending: {
|
|
5755
|
+
trigger: "message",
|
|
5756
|
+
text: deliveryText,
|
|
5757
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5758
|
+
message_id: messageId,
|
|
5759
|
+
compact: opts.compact !== false,
|
|
5760
|
+
},
|
|
5761
|
+
accountSelection: selection,
|
|
5762
|
+
});
|
|
5763
|
+
lane = move.lane;
|
|
5764
|
+
if (move.queued)
|
|
5765
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5728
5766
|
}
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5767
|
+
else {
|
|
5768
|
+
lane = persistClaudeAccountSelection(hqRoot, lane, selection);
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5773
|
+
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5774
|
+
}
|
|
5775
|
+
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
|
|
5776
|
+
lane = recordResumeSetup(ctx, lane, {
|
|
5777
|
+
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5778
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5779
|
+
...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5780
|
+
});
|
|
5781
|
+
}
|
|
5782
|
+
if (move?.queued) {
|
|
5783
|
+
// Parked at the target's cap: reconcile resumes the session there.
|
|
5784
|
+
markAccepted();
|
|
5785
|
+
}
|
|
5786
|
+
else {
|
|
5787
|
+
const dispatched = await dispatchLaneDelivery(lane, {
|
|
5788
|
+
dropbox: () => {
|
|
5789
|
+
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
5790
|
+
if (!laneAcceptsOperatorMessage(lane)) {
|
|
5791
|
+
throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
|
|
5792
|
+
}
|
|
5793
|
+
appendDropboxMessage(hqRoot, laneId, dropboxMessage);
|
|
5794
|
+
return null;
|
|
5795
|
+
},
|
|
5796
|
+
resume: async () => {
|
|
5797
|
+
// Exited worker with a recorded session: queue the message when capacity
|
|
5798
|
+
// is occupied, otherwise start its next turn under the shared admission lock.
|
|
5799
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
5800
|
+
return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
5801
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
5802
|
+
if (admission.action === "refuse" && admission.error)
|
|
5803
|
+
throw admission.error;
|
|
5804
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
5805
|
+
if (admission.action === "queue") {
|
|
5806
|
+
const sessionId = lane.provider_session_id;
|
|
5807
|
+
if (!sessionId) {
|
|
5808
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
|
|
5754
5809
|
lane_id: laneId,
|
|
5755
|
-
|
|
5756
|
-
version: current.version,
|
|
5757
|
-
state: current.state,
|
|
5810
|
+
provider: lane.provider,
|
|
5758
5811
|
});
|
|
5759
5812
|
}
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5813
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5814
|
+
queuedOnAdmission = true;
|
|
5815
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
5816
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
5817
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
5818
|
+
lane_id: laneId,
|
|
5819
|
+
prepared_version: lane.version,
|
|
5820
|
+
version: current.version,
|
|
5821
|
+
state: current.state,
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
return {
|
|
5825
|
+
...current,
|
|
5826
|
+
state: "queued",
|
|
5827
|
+
queued_reason: admission.reason ?? "throttle",
|
|
5828
|
+
queued_at: at,
|
|
5829
|
+
pending_resume: {
|
|
5830
|
+
session_id: sessionId,
|
|
5831
|
+
trigger: "message",
|
|
5832
|
+
text: deliveryText,
|
|
5833
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5834
|
+
message_id: messageId,
|
|
5835
|
+
compact: opts.compact !== false,
|
|
5836
|
+
at,
|
|
5837
|
+
},
|
|
5838
|
+
};
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
5782
5842
|
env: ctx.env,
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5843
|
+
modelDeps: {
|
|
5844
|
+
readFileSync: ctx.readFileSync,
|
|
5845
|
+
probe: ctx.probeProvider,
|
|
5846
|
+
env: ctx.env,
|
|
5847
|
+
},
|
|
5848
|
+
existsSync: ctx.existsSync,
|
|
5849
|
+
spawnProcess: ctx.spawnProcess,
|
|
5850
|
+
proveDetached: ctx.proveDetached,
|
|
5851
|
+
killProcess: ctx.killProcess,
|
|
5852
|
+
now: ctx.now,
|
|
5853
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
5854
|
+
runCompaction: ctx.runCompaction,
|
|
5855
|
+
locateTranscript: ctx.locateTranscript,
|
|
5856
|
+
}, {
|
|
5857
|
+
trigger: {
|
|
5858
|
+
kind: "message",
|
|
5859
|
+
text: deliveryText,
|
|
5860
|
+
message_id: messageId,
|
|
5861
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5862
|
+
},
|
|
5863
|
+
compact: opts.compact !== false,
|
|
5864
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
5865
|
+
});
|
|
5866
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5867
|
+
return started;
|
|
5801
5868
|
});
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
return {
|
|
5822
|
-
...current,
|
|
5823
|
-
project_id: projectId,
|
|
5824
|
-
story_id: storyId,
|
|
5825
|
-
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
5826
|
-
stories: laneStory(current, projectId, storyId)
|
|
5827
|
-
? laneStories(current)
|
|
5828
|
-
: [
|
|
5829
|
-
...laneStories(current),
|
|
5830
|
-
{
|
|
5831
|
-
project_id: projectId,
|
|
5832
|
-
story_id: storyId,
|
|
5833
|
-
status: "in_progress",
|
|
5834
|
-
added_at: at,
|
|
5835
|
-
updated_at: at,
|
|
5836
|
-
},
|
|
5837
|
-
],
|
|
5838
|
-
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
5869
|
+
}, ctx.sleep);
|
|
5870
|
+
},
|
|
5871
|
+
}, mechanism);
|
|
5872
|
+
if (dispatched.mechanism === "resume")
|
|
5873
|
+
resumed = dispatched.value;
|
|
5874
|
+
markAccepted();
|
|
5875
|
+
}
|
|
5876
|
+
// Every accepted message moves the compaction reference forward.
|
|
5877
|
+
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
5878
|
+
...resetKeepAliveProgress(current),
|
|
5879
|
+
last_message_at: at,
|
|
5880
|
+
}));
|
|
5881
|
+
if (projectId !== lane.project_id || storyId !== lane.story_id) {
|
|
5882
|
+
next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
5883
|
+
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
5884
|
+
if (already)
|
|
5885
|
+
return current;
|
|
5886
|
+
return {
|
|
5887
|
+
...current,
|
|
5839
5888
|
project_id: projectId,
|
|
5840
5889
|
story_id: storyId,
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5890
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
5891
|
+
stories: laneStory(current, projectId, storyId)
|
|
5892
|
+
? laneStories(current)
|
|
5893
|
+
: [
|
|
5894
|
+
...laneStories(current),
|
|
5895
|
+
{
|
|
5896
|
+
project_id: projectId,
|
|
5897
|
+
story_id: storyId,
|
|
5898
|
+
status: "in_progress",
|
|
5899
|
+
added_at: at,
|
|
5900
|
+
updated_at: at,
|
|
5901
|
+
},
|
|
5902
|
+
],
|
|
5903
|
+
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
5904
|
+
project_id: projectId,
|
|
5905
|
+
story_id: storyId,
|
|
5906
|
+
message_id: messageId,
|
|
5907
|
+
at,
|
|
5908
|
+
}),
|
|
5909
|
+
};
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
// What this command can claim when it returns: the message is durably
|
|
5913
|
+
// written (accepted). Delivery to the model is asynchronous in both
|
|
5914
|
+
// mechanisms. `dropbox` delivers on the worker's next tool call, and only
|
|
5915
|
+
// if the drain hook is registered in the account home the worker was
|
|
5916
|
+
// started from; `resume` has started a new worker with the message as its
|
|
5917
|
+
// prompt. Neither is "delivered", so the response never says so.
|
|
5918
|
+
const drainHook = drainHookStatusFor(ctx, next);
|
|
5919
|
+
const deliveryStatus = move?.queued
|
|
5920
|
+
? "queued_on_target_account"
|
|
5921
|
+
: queuedOnAdmission
|
|
5922
|
+
? "queued_on_admission"
|
|
5923
|
+
: mechanism === "dropbox"
|
|
5924
|
+
? "awaiting_next_tool_call"
|
|
5925
|
+
: "worker_spawned";
|
|
5926
|
+
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
5927
|
+
? `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.`
|
|
5928
|
+
: mechanism === "dropbox" && next.stalled === true
|
|
5929
|
+
? `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.`
|
|
5930
|
+
: null;
|
|
5931
|
+
if (!opts.silent)
|
|
5932
|
+
printJson({
|
|
5933
|
+
ok: true,
|
|
5934
|
+
action: "message",
|
|
5935
|
+
lane_id: laneId,
|
|
5936
|
+
message_id: messageId,
|
|
5937
|
+
accepted: true,
|
|
5938
|
+
delivered: false,
|
|
5939
|
+
delivery: mechanism,
|
|
5940
|
+
delivery_status: deliveryStatus,
|
|
5941
|
+
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
5942
|
+
drain_hook: drainHook,
|
|
5943
|
+
warning,
|
|
5944
|
+
resumed_session_id: resumed?.last_resume?.session_id ?? null,
|
|
5945
|
+
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
5946
|
+
? resumed.last_compaction
|
|
5947
|
+
: null,
|
|
5948
|
+
account_move: move ? accountMoveSummary(move) : null,
|
|
5949
|
+
project_id: next.project_id,
|
|
5950
|
+
story_id: next.story_id,
|
|
5951
|
+
stories: laneStories(next),
|
|
5952
|
+
story_warning: storyWarning(next) ?? null,
|
|
5953
|
+
lane: next,
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5956
|
+
catch (err) {
|
|
5957
|
+
if (claimedMessages.length > 0 && !accepted) {
|
|
5958
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
5959
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
5960
|
+
event: "message_recovery_failed",
|
|
5961
|
+
at: ctx.now().toISOString(),
|
|
5962
|
+
state: lane.state,
|
|
5963
|
+
error,
|
|
5964
|
+
message_count: claimedMessages.length,
|
|
5965
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
5966
|
+
try {
|
|
5967
|
+
requeueDropboxMessages(hqRoot, laneId, claimedMessages);
|
|
5968
|
+
}
|
|
5969
|
+
catch (requeueErr) {
|
|
5970
|
+
const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
|
|
5971
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
5972
|
+
event: "message_requeue_failed",
|
|
5973
|
+
at: ctx.now().toISOString(),
|
|
5974
|
+
state: lane.state,
|
|
5975
|
+
recovery_error: error,
|
|
5976
|
+
requeue_error: requeueMessage,
|
|
5977
|
+
message_count: claimedMessages.length,
|
|
5978
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
5979
|
+
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 });
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
throw err;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
function messageLaneExclusion(ctx, lane) {
|
|
5986
|
+
if (lane.state === "killed" || lane.state === "cancelled")
|
|
5987
|
+
return lane.state;
|
|
5988
|
+
return keepAliveExclusion(lane, loadAllLanes(ctx.hqRoot()), loadRetiredLaneIds(ctx.hqRoot()));
|
|
5989
|
+
}
|
|
5990
|
+
function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
|
|
5991
|
+
writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
|
|
5992
|
+
event: "message_undeliverable",
|
|
5993
|
+
at: ctx.now().toISOString(),
|
|
5994
|
+
state: lane.state,
|
|
5995
|
+
reason,
|
|
5996
|
+
...(messageCount === undefined ? {} : { message_count: messageCount }),
|
|
5997
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
5998
|
+
}
|
|
5999
|
+
/**
|
|
6000
|
+
* Watcher and reconcile call the hidden command below after they win the
|
|
6001
|
+
* terminal transition. The inbox is claimed once with the normal atomic
|
|
6002
|
+
* dropbox drain, and an accepted message is routed through runMessage so the
|
|
6003
|
+
* existing provider, admission, account, worker-profile and senior checks
|
|
6004
|
+
* remain authoritative.
|
|
6005
|
+
*/
|
|
6006
|
+
async function recoverQueuedMessages(ctx, laneId) {
|
|
6007
|
+
const hqRoot = ctx.hqRoot();
|
|
6008
|
+
const lane = readLane(hqRoot, laneId);
|
|
6009
|
+
if (!dropboxInboxHasMessages(hqRoot, laneId))
|
|
6010
|
+
return { action: "none", message_count: 0 };
|
|
6011
|
+
const exclusion = messageLaneExclusion(ctx, lane);
|
|
6012
|
+
const refusal = exclusion ?? (lane.state === "done"
|
|
6013
|
+
? "done_before_envelope"
|
|
6014
|
+
: lane.state === "awaiting_input"
|
|
6015
|
+
? "awaiting_input_requires_question_answer"
|
|
6016
|
+
: laneDeliveryMechanism(lane) !== "resume"
|
|
6017
|
+
? `no_resume_path_for_${lane.state}`
|
|
6018
|
+
: undefined);
|
|
6019
|
+
if (refusal !== undefined) {
|
|
6020
|
+
const messages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
|
|
6021
|
+
if (messages.length > 0)
|
|
6022
|
+
recordMessageUndeliverable(ctx, lane, refusal, messages.length);
|
|
6023
|
+
return { action: messages.length > 0 ? "undeliverable" : "none", message_count: messages.length };
|
|
6024
|
+
}
|
|
6025
|
+
const messages = drainDropbox(hqRoot, laneId);
|
|
6026
|
+
if (messages.length === 0)
|
|
6027
|
+
return { action: "none", message_count: 0 };
|
|
6028
|
+
let accepted = false;
|
|
6029
|
+
try {
|
|
6030
|
+
await runMessage(ctx, laneId, {
|
|
6031
|
+
text: formatDropboxMessages(messages),
|
|
6032
|
+
compact: false,
|
|
6033
|
+
silent: true,
|
|
6034
|
+
onAccepted: () => { accepted = true; },
|
|
5845
6035
|
});
|
|
6036
|
+
return { action: "resumed", message_count: messages.length };
|
|
6037
|
+
}
|
|
6038
|
+
catch (err) {
|
|
6039
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
6040
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6041
|
+
event: "message_recovery_failed",
|
|
6042
|
+
at: ctx.now().toISOString(),
|
|
6043
|
+
state: lane.state,
|
|
6044
|
+
error,
|
|
6045
|
+
message_count: messages.length,
|
|
6046
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6047
|
+
if (!accepted) {
|
|
6048
|
+
try {
|
|
6049
|
+
requeueDropboxMessages(hqRoot, laneId, messages);
|
|
6050
|
+
}
|
|
6051
|
+
catch (requeueErr) {
|
|
6052
|
+
const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
|
|
6053
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6054
|
+
event: "message_requeue_failed",
|
|
6055
|
+
at: ctx.now().toISOString(),
|
|
6056
|
+
state: lane.state,
|
|
6057
|
+
recovery_error: error,
|
|
6058
|
+
requeue_error: requeueMessage,
|
|
6059
|
+
message_count: messages.length,
|
|
6060
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6061
|
+
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 });
|
|
6062
|
+
}
|
|
6063
|
+
}
|
|
6064
|
+
throw err;
|
|
5846
6065
|
}
|
|
5847
|
-
// What this command can claim when it returns: the message is durably
|
|
5848
|
-
// written (accepted). Delivery to the model is asynchronous in both
|
|
5849
|
-
// mechanisms. `dropbox` delivers on the worker's next tool call, and only
|
|
5850
|
-
// if the drain hook is registered in the account home the worker was
|
|
5851
|
-
// started from; `resume` has started a new worker with the message as its
|
|
5852
|
-
// prompt. Neither is "delivered", so the response never says so.
|
|
5853
|
-
const drainHook = drainHookStatusFor(ctx, next);
|
|
5854
|
-
const deliveryStatus = move?.queued
|
|
5855
|
-
? "queued_on_target_account"
|
|
5856
|
-
: queuedOnAdmission
|
|
5857
|
-
? "queued_on_admission"
|
|
5858
|
-
: mechanism === "dropbox"
|
|
5859
|
-
? "awaiting_next_tool_call"
|
|
5860
|
-
: "worker_spawned";
|
|
5861
|
-
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
5862
|
-
? `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.`
|
|
5863
|
-
: mechanism === "dropbox" && next.stalled === true
|
|
5864
|
-
? `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.`
|
|
5865
|
-
: null;
|
|
5866
|
-
printJson({
|
|
5867
|
-
ok: true,
|
|
5868
|
-
action: "message",
|
|
5869
|
-
lane_id: laneId,
|
|
5870
|
-
message_id: messageId,
|
|
5871
|
-
accepted: true,
|
|
5872
|
-
delivered: false,
|
|
5873
|
-
delivery: mechanism,
|
|
5874
|
-
delivery_status: deliveryStatus,
|
|
5875
|
-
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
5876
|
-
drain_hook: drainHook,
|
|
5877
|
-
warning,
|
|
5878
|
-
resumed_session_id: resumed?.last_resume?.session_id ?? null,
|
|
5879
|
-
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
5880
|
-
? resumed.last_compaction
|
|
5881
|
-
: null,
|
|
5882
|
-
account_move: move ? accountMoveSummary(move) : null,
|
|
5883
|
-
project_id: next.project_id,
|
|
5884
|
-
story_id: next.story_id,
|
|
5885
|
-
stories: laneStories(next),
|
|
5886
|
-
story_warning: storyWarning(next) ?? null,
|
|
5887
|
-
lane: next,
|
|
5888
|
-
});
|
|
5889
6066
|
}
|
|
5890
6067
|
function accountMoveSummary(move) {
|
|
5891
6068
|
return {
|
|
@@ -8532,6 +8709,9 @@ async function runReconcile(ctx, opts) {
|
|
|
8532
8709
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8533
8710
|
presenceConnected: ctx.presenceConnected,
|
|
8534
8711
|
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
8712
|
+
recoverQueuedMessages: async (lane) => {
|
|
8713
|
+
await recoverQueuedMessages(ctx, lane.lane_id);
|
|
8714
|
+
},
|
|
8535
8715
|
mesh: ctx.mesh,
|
|
8536
8716
|
env: ctx.env,
|
|
8537
8717
|
});
|
|
@@ -8863,6 +9043,12 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8863
9043
|
.action(wrap(async (laneId, opts) => {
|
|
8864
9044
|
await runMessage(ctx, laneId, opts);
|
|
8865
9045
|
}));
|
|
9046
|
+
lanes
|
|
9047
|
+
.command("_recover-queued-messages", { hidden: true })
|
|
9048
|
+
.argument("<lane-id>", "Lane id")
|
|
9049
|
+
.action(wrap(async (laneId) => {
|
|
9050
|
+
printJson(await recoverQueuedMessages(ctx, laneId));
|
|
9051
|
+
}));
|
|
8866
9052
|
lanes
|
|
8867
9053
|
.command("_monitor-message", { hidden: true })
|
|
8868
9054
|
.argument("<lane-id>", "Lane id")
|