@indigoai-us/hq-cli 5.345.56 → 5.345.57
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 +6 -0
- package/dist/command-catalog.generated.d.ts +84 -0
- package/dist/command-catalog.generated.js +104 -0
- package/dist/commands/lanes-loop.d.ts +12 -0
- package/dist/commands/lanes-loop.js +214 -0
- package/dist/commands/lanes-questions.js +37 -11
- package/dist/commands/lanes-resume.js +19 -1
- package/dist/commands/lanes-shared.d.ts +12 -1
- package/dist/commands/lanes.js +262 -22
- package/dist/lib/lanes/delivery.d.ts +3 -0
- package/dist/lib/lanes/delivery.js +5 -0
- package/dist/lib/lanes/dropbox.d.ts +3 -0
- package/dist/lib/lanes/dropbox.js +103 -1
- package/dist/lib/lanes/flag-keys.d.ts +4 -0
- package/dist/lib/lanes/flag-keys.js +4 -0
- package/dist/lib/lanes/loop.d.ts +158 -0
- package/dist/lib/lanes/loop.js +743 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +21 -7
- package/dist/lib/lanes/reconcile.d.ts +19 -0
- package/dist/lib/lanes/reconcile.js +173 -6
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +10 -8
- package/dist/lib/lanes/store.js +49 -2
- package/dist/lib/lanes/types.d.ts +18 -0
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -0
- package/package.json +1 -1
|
@@ -77,6 +77,10 @@ export interface LaneResumeOptions {
|
|
|
77
77
|
effort?: string;
|
|
78
78
|
worker?: string;
|
|
79
79
|
keepAlive?: boolean;
|
|
80
|
+
freshThread?: boolean;
|
|
81
|
+
workingDirectory?: string;
|
|
82
|
+
/** Internal loop runner session; selects this story's saved provider session. */
|
|
83
|
+
loopSessionId?: string;
|
|
80
84
|
meshCompany?: string;
|
|
81
85
|
profileRepinContext?: string;
|
|
82
86
|
grantContext?: string;
|
|
@@ -458,7 +462,14 @@ export interface LaneQuestionsHandlerDeps {
|
|
|
458
462
|
questionCompanyForLane(question: QuestionRecord, lane: Pick<LaneRecord, "company" | "mesh_company" | "project_id" | "lane_id">, hqRoot?: string): LaneCompanyRef | null;
|
|
459
463
|
questionCompanyRefMatches(recordCompany: LaneCompanyRef | null | undefined, filter: LaneCompanyRef): boolean;
|
|
460
464
|
questionListCompanyArg(company: LaneCompanyRef | null): string | undefined;
|
|
461
|
-
readOutpostQuestionRecords(ctx: Ctx, outpostId: string, company: LaneCompanyRef | null
|
|
465
|
+
readOutpostQuestionRecords(ctx: Ctx, outpostId: string, company: LaneCompanyRef | null, questionId?: string, visibilityFilter?: {
|
|
466
|
+
sessionId?: string;
|
|
467
|
+
for?: string;
|
|
468
|
+
from?: string;
|
|
469
|
+
project?: string;
|
|
470
|
+
story?: string;
|
|
471
|
+
status?: string;
|
|
472
|
+
}): Promise<OutpostQuestionRow[]>;
|
|
462
473
|
readQuestionBody(hqRoot: string, question: QuestionRecord): QuestionBody | null;
|
|
463
474
|
printJson(value: unknown): void;
|
|
464
475
|
readSessionMeta(hqRoot: string, sessionId: string, ctx: Ctx): Record<string, unknown> | null;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -41,11 +41,15 @@ import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFla
|
|
|
41
41
|
import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
|
|
42
42
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
43
43
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
44
|
-
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
44
|
+
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
45
45
|
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
46
|
+
import { enqueueLoopEnvelope, interruptLoopQueue, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
47
|
+
import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
46
48
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
47
49
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
48
50
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
51
|
+
import { appendDropboxMessageOnce, appendDropboxMessageOnceAtPath } from "../lib/lanes/dropbox.js";
|
|
52
|
+
import { monitorSessionDir } from "../lib/monitor.js";
|
|
49
53
|
import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
|
|
50
54
|
import { OutpostSnapshotInvocationMemo } from "../lib/lanes/outpost-snapshot-memo.js";
|
|
51
55
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
@@ -3326,6 +3330,16 @@ async function runCreate(ctx, opts) {
|
|
|
3326
3330
|
}
|
|
3327
3331
|
meshCompany = resolvedMeshCompany;
|
|
3328
3332
|
}
|
|
3333
|
+
if (opts.loop === true && !(await readLanesRegistryFlagFailOpen(LANE_CONDUCT_LOOP_FLAG, {
|
|
3334
|
+
company: company ?? meshCompany,
|
|
3335
|
+
defaultValue: true,
|
|
3336
|
+
env: ctx.env,
|
|
3337
|
+
}))) {
|
|
3338
|
+
throw new LanesError("loop_lanes_disabled", `Loop lanes are disabled by the ${LANE_CONDUCT_LOOP_FLAG} kill switch`, {});
|
|
3339
|
+
}
|
|
3340
|
+
if (opts.loop === true && (opts.outpost?.trim() || requestedPlacement?.kind === "outpost" || defaultPlacement?.kind === "outpost")) {
|
|
3341
|
+
throw new LanesError("loop_lane_outpost_unsupported", "Loop lanes currently require the local host", { outpost_id: opts.outpost?.trim() ?? (requestedPlacement?.kind === "outpost" ? requestedPlacement.id : defaultPlacement?.kind === "outpost" ? defaultPlacement.id : undefined) });
|
|
3342
|
+
}
|
|
3329
3343
|
let placementCompanyResolved = false;
|
|
3330
3344
|
let placementCompany;
|
|
3331
3345
|
let placementGate;
|
|
@@ -3388,6 +3402,9 @@ async function runCreate(ctx, opts) {
|
|
|
3388
3402
|
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
3389
3403
|
const keepAlive = opts.keepAlive === true ||
|
|
3390
3404
|
(profileDefaultsApply && profile?.meta.keep_alive === true);
|
|
3405
|
+
if (opts.loop === true && keepAlive) {
|
|
3406
|
+
throw new LanesError("loop_keep_alive_conflict", "Loop lanes cannot use keep-alive", { worker: resolvedWorker?.id ?? opts.worker ?? null });
|
|
3407
|
+
}
|
|
3391
3408
|
if (keepAlive && !providerSupportsSessionResume(provider)) {
|
|
3392
3409
|
throw new LanesError("keep_alive_provider_unsupported", `Provider ${provider} cannot resume recorded sessions, so keep-alive is unavailable`, { provider });
|
|
3393
3410
|
}
|
|
@@ -3755,6 +3772,9 @@ async function runCreate(ctx, opts) {
|
|
|
3755
3772
|
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3756
3773
|
? { kind: "outpost", id: outpostId }
|
|
3757
3774
|
: { kind: "local", id: ctx.hostname() });
|
|
3775
|
+
if (opts.loop === true && host.kind === "outpost") {
|
|
3776
|
+
throw new LanesError("loop_lane_outpost_unsupported", "Loop lanes currently require the local host", { outpost_id: host.id });
|
|
3777
|
+
}
|
|
3758
3778
|
// Repeat the busy check while holding provider admission serialization so
|
|
3759
3779
|
// two concurrent creates cannot both claim one worker's provider thread.
|
|
3760
3780
|
if (continuityEnabled) {
|
|
@@ -3914,6 +3934,7 @@ async function runCreate(ctx, opts) {
|
|
|
3914
3934
|
}
|
|
3915
3935
|
: {}),
|
|
3916
3936
|
keep_alive: keepAlive,
|
|
3937
|
+
...(opts.loop === true ? { loop_mode: true } : {}),
|
|
3917
3938
|
...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
|
|
3918
3939
|
provider,
|
|
3919
3940
|
...(workerContinuity.resumable && workerContinuity.previous
|
|
@@ -4113,7 +4134,21 @@ async function runCreate(ctx, opts) {
|
|
|
4113
4134
|
});
|
|
4114
4135
|
}
|
|
4115
4136
|
let started = lane;
|
|
4116
|
-
|
|
4137
|
+
let loopControllerPid;
|
|
4138
|
+
if (opts.loop === true) {
|
|
4139
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => admission.action === "start"
|
|
4140
|
+
? {
|
|
4141
|
+
...current,
|
|
4142
|
+
state: "running",
|
|
4143
|
+
queued_reason: undefined,
|
|
4144
|
+
queued_details: undefined,
|
|
4145
|
+
}
|
|
4146
|
+
: { ...current });
|
|
4147
|
+
started = lane;
|
|
4148
|
+
if (admission.action === "start")
|
|
4149
|
+
loopControllerPid = startLaneLoopController(ctx, lane.lane_id, true);
|
|
4150
|
+
}
|
|
4151
|
+
else if (admission.action === "start") {
|
|
4117
4152
|
started = spawnLaneWorker(hqRoot, lane, {
|
|
4118
4153
|
env: ctx.env,
|
|
4119
4154
|
modelDeps: {
|
|
@@ -4169,6 +4204,7 @@ async function runCreate(ctx, opts) {
|
|
|
4169
4204
|
company: started.company,
|
|
4170
4205
|
project_id: started.project_id,
|
|
4171
4206
|
story_id: started.story_id,
|
|
4207
|
+
...(started.loop_mode ? { loop: loopStatus(runDir(hqRoot, started.lane_id), started.lane_id), loop_pid: loopControllerPid ?? null } : {}),
|
|
4172
4208
|
stories: laneStories(started),
|
|
4173
4209
|
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
4174
4210
|
thread_id: started.thread_id ?? null,
|
|
@@ -5511,10 +5547,11 @@ async function runList(ctx, opts) {
|
|
|
5511
5547
|
action: "list",
|
|
5512
5548
|
lanes: selectedLanes.map((lane) => {
|
|
5513
5549
|
const fields = statusFieldsByLane.get(lane.lane_id) ?? {};
|
|
5550
|
+
const loop = lane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, lane.lane_id), lane.lane_id) } : {};
|
|
5514
5551
|
if (!isTerminalLaneState(lane.state))
|
|
5515
|
-
return { ...lane, ...fields };
|
|
5552
|
+
return { ...lane, ...fields, ...loop };
|
|
5516
5553
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
5517
|
-
return { ...lane, ...fields, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
|
|
5554
|
+
return { ...lane, ...fields, ...loop, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
|
|
5518
5555
|
}),
|
|
5519
5556
|
});
|
|
5520
5557
|
return;
|
|
@@ -5620,6 +5657,7 @@ async function runList(ctx, opts) {
|
|
|
5620
5657
|
...lane,
|
|
5621
5658
|
...statusFieldsByLane.get(lane.lane_id),
|
|
5622
5659
|
...hireByLane.get(lane.lane_id),
|
|
5660
|
+
...(lane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, lane.lane_id), lane.lane_id) } : {}),
|
|
5623
5661
|
...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
|
|
5624
5662
|
stories: laneStories(lane),
|
|
5625
5663
|
story_ids: laneStories(lane).map((story) => story.story_id),
|
|
@@ -5991,6 +6029,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
|
|
|
5991
6029
|
const laneForShow = {
|
|
5992
6030
|
...lane,
|
|
5993
6031
|
...await laneStatusFieldsForCommand(ctx, hqRoot, storedLane, ctx.now()),
|
|
6032
|
+
...(storedLane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, laneId), laneId) } : {}),
|
|
5994
6033
|
};
|
|
5995
6034
|
delete laneForShow.resume_history;
|
|
5996
6035
|
printJson({
|
|
@@ -7785,6 +7824,7 @@ async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount =
|
|
|
7785
7824
|
}
|
|
7786
7825
|
assertLaneNotRunningForReparent(lane);
|
|
7787
7826
|
const pending = pendingQuestionsForLane(hqRoot, laneId);
|
|
7827
|
+
const pendingAudiences = new Map(pending.map((question) => [question.question_id, resolveAudience(hqRoot, question, ctx)]));
|
|
7788
7828
|
const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
|
|
7789
7829
|
if (newDepth > oldDepth && pending.length > 0) {
|
|
7790
7830
|
throw new LanesError("demotion_with_pending_questions", "Cannot increase a lane's depth while it has pending questions", { lane_id: laneId, from_depth: oldDepth, to_depth: newDepth });
|
|
@@ -7849,17 +7889,24 @@ async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount =
|
|
|
7849
7889
|
}
|
|
7850
7890
|
for (const question of pending) {
|
|
7851
7891
|
casUpdateQuestion(hqRoot, question.question_id, (current) => {
|
|
7892
|
+
const previousAudience = pendingAudiences.get(current.question_id);
|
|
7893
|
+
const nextAudience = resolveAudience(hqRoot, current, ctx);
|
|
7894
|
+
const audienceChanged = previousAudience !== undefined &&
|
|
7895
|
+
seniorKey(previousAudience) !== seniorKey(nextAudience);
|
|
7852
7896
|
const already = current.routing_history.some((entry) => entry.reason === "reparented" && entry.at === at);
|
|
7853
|
-
if (already)
|
|
7897
|
+
if (already && !audienceChanged)
|
|
7854
7898
|
return current;
|
|
7855
7899
|
return {
|
|
7856
7900
|
...current,
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7901
|
+
...(audienceChanged ? { seen_at: null, seen_session_id: null } : {}),
|
|
7902
|
+
routing_history: already
|
|
7903
|
+
? current.routing_history
|
|
7904
|
+
: appendCappedHistory(current.routing_history, {
|
|
7905
|
+
hop: current.hop,
|
|
7906
|
+
audience: nextAudience,
|
|
7907
|
+
at,
|
|
7908
|
+
reason: "reparented",
|
|
7909
|
+
}),
|
|
7863
7910
|
};
|
|
7864
7911
|
});
|
|
7865
7912
|
}
|
|
@@ -8470,7 +8517,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8470
8517
|
const hqRoot = ctx.hqRoot();
|
|
8471
8518
|
const target = await resolveCommandLaneTarget(ctx, laneId);
|
|
8472
8519
|
const lane = target.lane;
|
|
8473
|
-
if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
8520
|
+
if (!quiet && lane.loop_mode !== true && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
8474
8521
|
process.stderr.write(stopTerminalWarning(lane.lane_id));
|
|
8475
8522
|
}
|
|
8476
8523
|
if (target.remoteOnly) {
|
|
@@ -8482,6 +8529,23 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8482
8529
|
return;
|
|
8483
8530
|
}
|
|
8484
8531
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
|
|
8532
|
+
if (lane.loop_mode === true) {
|
|
8533
|
+
const runDirPath = runDir(hqRoot, lane.lane_id);
|
|
8534
|
+
const loop = loopStatus(runDirPath, lane.lane_id);
|
|
8535
|
+
if (loop.state === "stopped" || loop.state === "failed") {
|
|
8536
|
+
throw new LanesError("already_terminal", `Loop lane ${lane.lane_id} is already ${lane.state}`, { lane_id: lane.lane_id, state: lane.state });
|
|
8537
|
+
}
|
|
8538
|
+
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
8539
|
+
if (!controllerAlive && lane.state === "running") {
|
|
8540
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true });
|
|
8541
|
+
}
|
|
8542
|
+
const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
|
|
8543
|
+
if (!controllerAlive && lane.state !== "queued")
|
|
8544
|
+
startLaneLoopController(ctx, lane.lane_id);
|
|
8545
|
+
if (!quiet)
|
|
8546
|
+
printJson({ ok: true, action: "stop", lane_id: lane.lane_id, terminal: false, queued_stop: queued });
|
|
8547
|
+
return;
|
|
8548
|
+
}
|
|
8485
8549
|
if (isTerminalLaneState(lane.state)) {
|
|
8486
8550
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
8487
8551
|
}
|
|
@@ -8537,7 +8601,7 @@ function stopTerminalWarning(laneId) {
|
|
|
8537
8601
|
* session so `hq lanes resume` continues it. Keep-alive resumes only
|
|
8538
8602
|
* `exited_with_partial_output` interruptions, so it leaves this lane alone.
|
|
8539
8603
|
*/
|
|
8540
|
-
async function runInterrupt(ctx, laneId, reason) {
|
|
8604
|
+
async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
8541
8605
|
const operatorReason = reason?.trim();
|
|
8542
8606
|
if (reason !== undefined && !operatorReason) {
|
|
8543
8607
|
throw new LanesError("lane_interrupt_reason_invalid", "Interrupt reason must not be empty", {});
|
|
@@ -8550,6 +8614,14 @@ async function runInterrupt(ctx, laneId, reason) {
|
|
|
8550
8614
|
throw new LanesError("interrupt_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes interrupt on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
|
|
8551
8615
|
}
|
|
8552
8616
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "interrupt");
|
|
8617
|
+
const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
|
|
8618
|
+
? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
|
|
8619
|
+
: undefined;
|
|
8620
|
+
if (lane.loop_mode === true && lane.state !== "running") {
|
|
8621
|
+
if (!options.quiet)
|
|
8622
|
+
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...queueInterrupt });
|
|
8623
|
+
return;
|
|
8624
|
+
}
|
|
8553
8625
|
const assertInterruptible = (current) => {
|
|
8554
8626
|
if (current.state !== "running") {
|
|
8555
8627
|
throw new LanesError("lane_not_running", current.state === "queued"
|
|
@@ -8592,13 +8664,15 @@ async function runInterrupt(ctx, laneId, reason) {
|
|
|
8592
8664
|
const resumable = resumeCommandRefusal(laneWithRecoveredProviderSession(hqRoot, updated)) === null;
|
|
8593
8665
|
if (!resumable)
|
|
8594
8666
|
process.stderr.write(interruptNotResumableWarning(laneId));
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8667
|
+
if (!options.quiet)
|
|
8668
|
+
printJson({
|
|
8669
|
+
ok: true,
|
|
8670
|
+
action: "interrupt",
|
|
8671
|
+
lane: updated,
|
|
8672
|
+
terminal: false,
|
|
8673
|
+
resumable,
|
|
8674
|
+
...(queueInterrupt === undefined ? {} : queueInterrupt),
|
|
8675
|
+
});
|
|
8602
8676
|
}
|
|
8603
8677
|
/**
|
|
8604
8678
|
* The watcher skips lanes that are already terminal, so it never records the
|
|
@@ -8762,13 +8836,13 @@ function questionListCompanyArg(company) {
|
|
|
8762
8836
|
}
|
|
8763
8837
|
return value;
|
|
8764
8838
|
}
|
|
8765
|
-
async function readOutpostQuestionRecords(ctx, outpostId, company) {
|
|
8839
|
+
async function readOutpostQuestionRecords(ctx, outpostId, company, questionId, visibilityFilter) {
|
|
8766
8840
|
const companyArg = questionListCompanyArg(company);
|
|
8767
8841
|
let result;
|
|
8768
8842
|
try {
|
|
8769
8843
|
result = await ctx.dispatchOutpostExec({
|
|
8770
8844
|
outpostId,
|
|
8771
|
-
command: buildOutpostQuestionListCommand({ company: companyArg }),
|
|
8845
|
+
command: buildOutpostQuestionListCommand({ company: companyArg, ...visibilityFilter }),
|
|
8772
8846
|
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
8773
8847
|
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
8774
8848
|
});
|
|
@@ -8801,6 +8875,8 @@ async function readOutpostQuestionRecords(ctx, outpostId, company) {
|
|
|
8801
8875
|
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
|
|
8802
8876
|
}
|
|
8803
8877
|
const rowObject = row;
|
|
8878
|
+
if (questionId !== undefined && rowObject.question_id !== questionId)
|
|
8879
|
+
continue;
|
|
8804
8880
|
let question;
|
|
8805
8881
|
try {
|
|
8806
8882
|
question = parseQuestionRecord(row);
|
|
@@ -8930,6 +9006,31 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
8930
9006
|
runCompaction: ctx.runCompaction,
|
|
8931
9007
|
locateTranscript: ctx.locateTranscript,
|
|
8932
9008
|
},
|
|
9009
|
+
spawn: (root, candidate) => {
|
|
9010
|
+
if (candidate.loop_mode !== true || candidate.state !== "queued") {
|
|
9011
|
+
return spawnLaneWorker(root, candidate, {
|
|
9012
|
+
env: ctx.env,
|
|
9013
|
+
modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
|
|
9014
|
+
existsSync: ctx.existsSync,
|
|
9015
|
+
spawnProcess: ctx.spawnProcess,
|
|
9016
|
+
proveDetached: ctx.proveDetached,
|
|
9017
|
+
killProcess: ctx.killProcess,
|
|
9018
|
+
now: ctx.now,
|
|
9019
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
9020
|
+
runCompaction: ctx.runCompaction,
|
|
9021
|
+
locateTranscript: ctx.locateTranscript,
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
9024
|
+
const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
|
|
9025
|
+
...current,
|
|
9026
|
+
state: "running",
|
|
9027
|
+
queued_reason: undefined,
|
|
9028
|
+
queued_details: undefined,
|
|
9029
|
+
admission_reserved_at: undefined,
|
|
9030
|
+
}));
|
|
9031
|
+
startLaneLoopController(ctx, candidate.lane_id, true);
|
|
9032
|
+
return admitted;
|
|
9033
|
+
},
|
|
8933
9034
|
throttleDeps: ctx.throttleDeps,
|
|
8934
9035
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8935
9036
|
presenceConnected: ctx.presenceConnected,
|
|
@@ -8937,6 +9038,63 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
8937
9038
|
recoverQueuedMessages,
|
|
8938
9039
|
mesh: ctx.mesh,
|
|
8939
9040
|
env: ctx.env,
|
|
9041
|
+
deliverBlockedEscalation: async (delivery) => {
|
|
9042
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
9043
|
+
const targetOutpostId = delivery.recipient_host?.kind === "outpost"
|
|
9044
|
+
? delivery.recipient_host.id
|
|
9045
|
+
: "primary";
|
|
9046
|
+
const targetIsCurrent = targetOutpostId === (currentHost.outpostId ?? "primary");
|
|
9047
|
+
if (targetIsCurrent)
|
|
9048
|
+
throw new LanesError("blocked_escalation_local_delivery_misrouted", "Local blocked escalations must use the durable local inbox path");
|
|
9049
|
+
if (!delivery.recipient.id)
|
|
9050
|
+
throw new LanesError("blocked_escalation_recipient_invalid", "Escalation recipient has no id");
|
|
9051
|
+
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
9052
|
+
const dispatched = await ctx.dispatchOutpostExec({
|
|
9053
|
+
outpostId: targetOutpostId,
|
|
9054
|
+
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
9055
|
+
waitMs,
|
|
9056
|
+
command: buildOutpostLaneCommand({
|
|
9057
|
+
action: "blocked-escalation-target",
|
|
9058
|
+
laneId: delivery.recipient.id,
|
|
9059
|
+
recipientKind: delivery.recipient.kind === "session" ? "session" : "lane",
|
|
9060
|
+
text: delivery.text,
|
|
9061
|
+
messageId: delivery.message_id,
|
|
9062
|
+
}),
|
|
9063
|
+
});
|
|
9064
|
+
if (dispatched.exitCode !== 0)
|
|
9065
|
+
throw new LanesError("blocked_escalation_remote_delivery_failed", dispatchErrorMessage(dispatched), { outpost_id: targetOutpostId });
|
|
9066
|
+
},
|
|
9067
|
+
resolveEscalationLane: async (laneId) => {
|
|
9068
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
9069
|
+
if (currentHost.outpostId === null)
|
|
9070
|
+
return null;
|
|
9071
|
+
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
9072
|
+
const dispatched = await ctx.dispatchOutpostExec({
|
|
9073
|
+
outpostId: "primary",
|
|
9074
|
+
command: buildOutpostLaneCommand({ action: "show", laneId }),
|
|
9075
|
+
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
9076
|
+
waitMs,
|
|
9077
|
+
});
|
|
9078
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
9079
|
+
throw new LanesError("blocked_escalation_ancestor_unavailable", `Could not read ancestor lane ${laneId} from the primary hierarchy`, {
|
|
9080
|
+
lane_id: laneId,
|
|
9081
|
+
command_id: dispatched.commandId,
|
|
9082
|
+
exit_code: dispatched.exitCode,
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
const remote = parseRemoteJson(dispatched.stdout, "Blocked escalation ancestor");
|
|
9086
|
+
if (remote?.ok !== true || remote.action !== "show" || remote.lane === undefined) {
|
|
9087
|
+
throw new LanesError("blocked_escalation_ancestor_invalid", `Primary did not return ancestor lane ${laneId}`, { lane_id: laneId });
|
|
9088
|
+
}
|
|
9089
|
+
const ancestor = parseLaneRecord(normalizeLaneRecordView(remote.lane));
|
|
9090
|
+
if (ancestor.lane_id !== laneId) {
|
|
9091
|
+
throw new LanesError("blocked_escalation_ancestor_invalid", `Primary returned a different lane for ancestor lookup ${laneId}`, {
|
|
9092
|
+
requested_lane_id: laneId,
|
|
9093
|
+
returned_lane_id: ancestor.lane_id,
|
|
9094
|
+
});
|
|
9095
|
+
}
|
|
9096
|
+
return ancestor;
|
|
9097
|
+
},
|
|
8940
9098
|
...(opts.terminalKeepAliveCandidate !== undefined
|
|
8941
9099
|
? { terminalKeepAliveCandidate: opts.terminalKeepAliveCandidate }
|
|
8942
9100
|
: {}),
|
|
@@ -9413,6 +9571,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9413
9571
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
9414
9572
|
.option("--worker <name>", "HQ worker or reusable profile id")
|
|
9415
9573
|
.option("--fresh-thread", "Skip worker continuity and start a cold provider session")
|
|
9574
|
+
.option("--loop", "Keep one worker lane registered to process queued phase envelopes")
|
|
9416
9575
|
.option("--provider <name>", "claude | codex | grok")
|
|
9417
9576
|
.option("--account <id>", "Provider account id")
|
|
9418
9577
|
.option("--model <id>", "Model id for the provider CLI")
|
|
@@ -9435,6 +9594,62 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9435
9594
|
.action(wrap(async (laneId, opts) => {
|
|
9436
9595
|
await runApproveOwnerTarget(ctx, laneId, opts);
|
|
9437
9596
|
}));
|
|
9597
|
+
lanes
|
|
9598
|
+
.command("_loop-worker", { hidden: true })
|
|
9599
|
+
.argument("<lane-id>", "Loop lane id")
|
|
9600
|
+
.action(wrap(async (laneId) => {
|
|
9601
|
+
await runLaneLoopController(ctx, laneId, {
|
|
9602
|
+
resumeLane: (id, options) => resumeHandlers.runResume(ctx, id, options),
|
|
9603
|
+
interruptLane: (id, reason, options) => runInterrupt(ctx, id, reason, options),
|
|
9604
|
+
emitTerminal: (lane) => emitLaneTerminalStateSoft(ctx, lane),
|
|
9605
|
+
});
|
|
9606
|
+
}));
|
|
9607
|
+
lanes
|
|
9608
|
+
.command("enqueue")
|
|
9609
|
+
.description("Validate and queue one phase envelope for a loop lane")
|
|
9610
|
+
.argument("<lane-id>", "Loop lane id")
|
|
9611
|
+
.requiredOption("--envelope <file>", "Generic phase or hq-phase-envelope/v1 JSON file")
|
|
9612
|
+
.action(wrap(async (laneId, opts) => {
|
|
9613
|
+
const hqRoot = ctx.hqRoot();
|
|
9614
|
+
const lane = readLane(hqRoot, laneId);
|
|
9615
|
+
if (lane.loop_mode !== true)
|
|
9616
|
+
throw new LanesError("lane_not_loop_mode", `Lane ${laneId} is not a loop lane`, { lane_id: laneId });
|
|
9617
|
+
const authorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
9618
|
+
if (!authorization.authorized)
|
|
9619
|
+
throw new LanesError("lane_enqueue_unauthorized", `Only the lane's senior may enqueue phases`, { lane_id: laneId, reason: authorization.reason });
|
|
9620
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
9621
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_LOOP_FLAG, { company: lane.company ?? lane.mesh_company, defaultValue: true, env: ctx.env });
|
|
9622
|
+
if (!enabled)
|
|
9623
|
+
throw new LanesError("loop_lanes_disabled", `Loop lanes are disabled by the ${LANE_CONDUCT_LOOP_FLAG} kill switch`, { lane_id: laneId });
|
|
9624
|
+
const source = resolveCallerPath(ctx.callerCwd(), opts.envelope ?? "");
|
|
9625
|
+
let envelope;
|
|
9626
|
+
try {
|
|
9627
|
+
envelope = validateLoopEnvelope(JSON.parse(fs.readFileSync(source, "utf8")));
|
|
9628
|
+
}
|
|
9629
|
+
catch (error) {
|
|
9630
|
+
throw new LanesError("loop_envelope_invalid", `Could not read a valid JSON envelope from ${source}: ${error instanceof Error ? error.message : String(error)}`, { lane_id: laneId, path: source });
|
|
9631
|
+
}
|
|
9632
|
+
const envelopeData = envelope;
|
|
9633
|
+
if (envelopeData.engine !== undefined && envelopeData.engine !== lane.provider) {
|
|
9634
|
+
throw new LanesError("loop_envelope_engine_mismatch", `Envelope engine ${String(envelopeData.engine)} does not match lane provider ${lane.provider}`, { lane_id: lane.lane_id, provider: lane.provider });
|
|
9635
|
+
}
|
|
9636
|
+
if (envelopeData.schema === "hq-phase-envelope/v1" && envelopeData.worker_id !== lane.worker) {
|
|
9637
|
+
throw new LanesError("loop_envelope_worker_mismatch", `Envelope worker ${String(envelopeData.worker_id)} does not match lane worker ${lane.worker ?? "<none>"}`, { lane_id: lane.lane_id, worker: lane.worker ?? null });
|
|
9638
|
+
}
|
|
9639
|
+
const runDirPath = runDir(hqRoot, lane.lane_id);
|
|
9640
|
+
const loop = loopStatus(runDirPath, lane.lane_id);
|
|
9641
|
+
if (loop.state === "stopped" || loop.state === "failed") {
|
|
9642
|
+
throw new LanesError("loop_not_running", `Loop lane ${lane.lane_id} is ${loop.state}`, { lane_id: lane.lane_id, loop_state: loop.state });
|
|
9643
|
+
}
|
|
9644
|
+
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
9645
|
+
const result = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope, now: ctx.now });
|
|
9646
|
+
if (!controllerAlive && lane.state === "running") {
|
|
9647
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true });
|
|
9648
|
+
}
|
|
9649
|
+
const shouldRestart = !controllerAlive && lane.state !== "queued";
|
|
9650
|
+
const controllerPid = shouldRestart ? startLaneLoopController(ctx, lane.lane_id) : undefined;
|
|
9651
|
+
printJson({ ok: true, action: "enqueue", ...result, lane: lane.lane_id, ...(controllerPid ? { controller_pid: controllerPid } : {}) });
|
|
9652
|
+
}));
|
|
9438
9653
|
lanes
|
|
9439
9654
|
.command("reparent-target", { hidden: true })
|
|
9440
9655
|
.argument("<lane-id>", "Lane id")
|
|
@@ -9701,6 +9916,31 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9701
9916
|
.action(wrap(async (laneId) => {
|
|
9702
9917
|
printJson(await messageHandlers.recoverQueuedMessages(ctx, laneId));
|
|
9703
9918
|
}));
|
|
9919
|
+
lanes
|
|
9920
|
+
.command("_enqueue-blocked-escalation <recipient-kind> <recipient-id>", { hidden: true })
|
|
9921
|
+
.option("--text <text>", "Escalation notice")
|
|
9922
|
+
.option("--message-id <id>", "Idempotency key")
|
|
9923
|
+
.action(wrap(async (recipientKind, recipientId, opts) => {
|
|
9924
|
+
if (!ctx.isSsmOwnerDispatch())
|
|
9925
|
+
throw new LanesError("blocked_escalation_dispatch_forbidden", "Blocked escalation inbox writes require owner dispatch");
|
|
9926
|
+
if (recipientKind !== "lane" && recipientKind !== "session")
|
|
9927
|
+
throw new LanesError("blocked_escalation_recipient_invalid", `Unknown recipient kind ${recipientKind}`);
|
|
9928
|
+
if (!opts.text?.trim() || !opts.messageId?.trim())
|
|
9929
|
+
throw new LanesError("blocked_escalation_payload_invalid", "Escalation inbox write requires text and message id");
|
|
9930
|
+
const message = { at: ctx.now().toISOString(), message_id: opts.messageId, kind: "blocked_escalation", text: opts.text };
|
|
9931
|
+
if (recipientKind === "lane") {
|
|
9932
|
+
appendDropboxMessageOnce(ctx.hqRoot(), recipientId, message);
|
|
9933
|
+
}
|
|
9934
|
+
else {
|
|
9935
|
+
const dirs = ["claude", "codex", "grok"]
|
|
9936
|
+
.map((provider) => monitorSessionDir(ctx.hqRoot(), provider, recipientId))
|
|
9937
|
+
.filter((dir) => ctx.existsSync(dir));
|
|
9938
|
+
if (dirs.length !== 1)
|
|
9939
|
+
throw new LanesError("blocked_escalation_recipient_unavailable", `Expected one durable inbox for session ${recipientId}; found ${dirs.length}`);
|
|
9940
|
+
appendDropboxMessageOnceAtPath(path.join(dirs[0], "dropbox"), message);
|
|
9941
|
+
}
|
|
9942
|
+
printJson({ ok: true, recipient_kind: recipientKind, recipient_id: recipientId, message_id: opts.messageId });
|
|
9943
|
+
}));
|
|
9704
9944
|
lanes
|
|
9705
9945
|
.command("_spawn-envelope-repair", { hidden: true })
|
|
9706
9946
|
.action(wrap(async () => {
|
|
@@ -144,6 +144,8 @@ export type LaunchTrigger = {
|
|
|
144
144
|
kind: "resume";
|
|
145
145
|
/** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
|
|
146
146
|
from: ResumeCommandState | "failed" | "awaiting_input";
|
|
147
|
+
/** Loop phases may deliberately start a fresh session at a story boundary. */
|
|
148
|
+
fresh?: true;
|
|
147
149
|
allowAwaitingInput?: true;
|
|
148
150
|
fromFailed?: true;
|
|
149
151
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
@@ -164,6 +166,7 @@ export type LaunchPlan = {
|
|
|
164
166
|
export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
165
167
|
allowFailed?: boolean;
|
|
166
168
|
allowAwaitingInput?: boolean;
|
|
169
|
+
allowFresh?: boolean;
|
|
167
170
|
}): {
|
|
168
171
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
169
172
|
message: string;
|
|
@@ -371,6 +371,8 @@ export function resumeCommandRefusal(lane, options = {}) {
|
|
|
371
371
|
: "a lane in this state is not resumed."}`,
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
+
if (options.allowFresh === true)
|
|
375
|
+
return null;
|
|
374
376
|
if (!providerSupportsSessionResume(lane.provider)) {
|
|
375
377
|
return {
|
|
376
378
|
error: "resume_unsupported",
|
|
@@ -411,7 +413,10 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
411
413
|
const refusal = resumeCommandRefusal(lane, {
|
|
412
414
|
allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
413
415
|
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
416
|
+
allowFresh: trigger.fresh === true,
|
|
414
417
|
});
|
|
418
|
+
if (trigger.fresh === true && refusal === null)
|
|
419
|
+
return { mode: "fresh" };
|
|
415
420
|
if (refusal !== null || !hasRecordedSessionId(lane)) {
|
|
416
421
|
throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
|
|
417
422
|
}
|
|
@@ -90,8 +90,11 @@ export interface ToolActivityRecord {
|
|
|
90
90
|
export declare function recordToolActivity(hqRoot: string, laneId: string, at?: string): ToolActivityRecord;
|
|
91
91
|
export declare function readToolActivity(hqRoot: string, laneId: string): ToolActivityRecord | null;
|
|
92
92
|
export declare function appendDropboxMessage(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
93
|
+
export declare function appendDropboxMessageOnce(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
93
94
|
/** Append using the same on-disk layout for lane and monitor-session inboxes. */
|
|
94
95
|
export declare function appendDropboxMessageAtPath(box: string, message: DropboxMessage): DropboxMessage;
|
|
96
|
+
/** Append an idempotent message once, including retries after its inbox was archived. */
|
|
97
|
+
export declare function appendDropboxMessageOnceAtPath(box: string, message: DropboxMessage): DropboxMessage;
|
|
95
98
|
/**
|
|
96
99
|
* Drain this lane's inbox exactly once. Concurrent callers: one rename wins;
|
|
97
100
|
* the loser sees ENOENT and returns whatever it separately claimed (usually
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* A read-then-truncate is not this mechanism and loses or duplicates under
|
|
8
8
|
* overlap.
|
|
9
9
|
*/
|
|
10
|
-
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { LanesError } from "./errors.js";
|
|
@@ -179,6 +179,9 @@ export function readToolActivity(hqRoot, laneId) {
|
|
|
179
179
|
export function appendDropboxMessage(hqRoot, laneId, message) {
|
|
180
180
|
return appendDropboxMessageAtPath(dropboxDir(hqRoot, laneId), message);
|
|
181
181
|
}
|
|
182
|
+
export function appendDropboxMessageOnce(hqRoot, laneId, message) {
|
|
183
|
+
return appendDropboxMessageOnceAtPath(dropboxDir(hqRoot, laneId), message);
|
|
184
|
+
}
|
|
182
185
|
/** Append using the same on-disk layout for lane and monitor-session inboxes. */
|
|
183
186
|
export function appendDropboxMessageAtPath(box, message) {
|
|
184
187
|
const inbox = path.join(box, INBOX_NAME);
|
|
@@ -186,6 +189,105 @@ export function appendDropboxMessageAtPath(box, message) {
|
|
|
186
189
|
fs.appendFileSync(inbox, `${JSON.stringify(message)}\n`, "utf8");
|
|
187
190
|
return message;
|
|
188
191
|
}
|
|
192
|
+
/** Append an idempotent message once, including retries after its inbox was archived. */
|
|
193
|
+
export function appendDropboxMessageOnceAtPath(box, message) {
|
|
194
|
+
const messageId = message.message_id?.trim();
|
|
195
|
+
if (!messageId) {
|
|
196
|
+
throw new LanesError("dropbox_message_id_required", "Idempotent dropbox delivery requires a message id");
|
|
197
|
+
}
|
|
198
|
+
fs.mkdirSync(box, { recursive: true });
|
|
199
|
+
const digest = createHash("sha256").update(messageId).digest("hex");
|
|
200
|
+
const lockDir = path.join(box, `.message-${digest}.lock`);
|
|
201
|
+
const lockTimeoutAt = Date.now() + 5_000;
|
|
202
|
+
while (true) {
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(lockDir);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code !== "EEXIST")
|
|
209
|
+
throw error;
|
|
210
|
+
try {
|
|
211
|
+
if (Date.now() - fs.statSync(lockDir).mtimeMs > 30_000) {
|
|
212
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (statError) {
|
|
217
|
+
if (statError.code === "ENOENT")
|
|
218
|
+
continue;
|
|
219
|
+
throw statError;
|
|
220
|
+
}
|
|
221
|
+
if (Date.now() >= lockTimeoutAt) {
|
|
222
|
+
throw new LanesError("dropbox_message_claim_busy", `Timed out claiming message ${messageId}`, { message_id: messageId });
|
|
223
|
+
}
|
|
224
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const inbox = path.join(box, INBOX_NAME);
|
|
229
|
+
const files = [inbox, ...messageHistoryFiles(box)];
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
let content;
|
|
232
|
+
try {
|
|
233
|
+
content = fs.readFileSync(file, "utf8");
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error.code === "ENOENT")
|
|
237
|
+
continue;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
for (const line of content.split(/\r?\n/)) {
|
|
241
|
+
if (!line.trim())
|
|
242
|
+
continue;
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(line);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new LanesError("dropbox_message_history_invalid", `Cannot verify message ${messageId} against ${file}`, {
|
|
249
|
+
message_id: messageId,
|
|
250
|
+
path: file,
|
|
251
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) &&
|
|
255
|
+
parsed.message_id === messageId)
|
|
256
|
+
return message;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
fs.appendFileSync(inbox, `${JSON.stringify(message)}\n`, "utf8");
|
|
260
|
+
return message;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function messageHistoryFiles(box) {
|
|
267
|
+
const files = [];
|
|
268
|
+
for (const directory of [DRAINING_DIR, INFLIGHT_DIR, DELIVERED_DIR]) {
|
|
269
|
+
const root = path.join(box, directory);
|
|
270
|
+
const visit = (current) => {
|
|
271
|
+
let entries;
|
|
272
|
+
try {
|
|
273
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error.code === "ENOENT")
|
|
277
|
+
return;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
if (entry.isDirectory())
|
|
282
|
+
visit(path.join(current, entry.name));
|
|
283
|
+
else if (entry.isFile())
|
|
284
|
+
files.push(path.join(current, entry.name));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
visit(root);
|
|
288
|
+
}
|
|
289
|
+
return files;
|
|
290
|
+
}
|
|
189
291
|
function listNames(dir, deps) {
|
|
190
292
|
try {
|
|
191
293
|
return deps.readdirSync(dir).filter((name) => !name.startsWith("."));
|