@indigoai-us/hq-cli 5.166.0 → 5.167.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 +11 -0
- package/dist/commands/lanes.js +267 -28
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +66 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.167.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes stop` sends Outpost-hosted stops to the owning host and updates the
|
|
10
|
+
local record after confirmation. If the Outpost already finished the lane,
|
|
11
|
+
the CLI syncs its terminal state.
|
|
12
|
+
- Story updates, reparenting, admission, question creation, and active answer
|
|
13
|
+
delivery return `lane_remote` when another host owns the lane. Message, resume,
|
|
14
|
+
and move keep their Outpost dispatch paths.
|
|
15
|
+
|
|
5
16
|
## [5.166.0] — 2026-09-24
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/dist/commands/lanes.js
CHANGED
|
@@ -23,7 +23,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
23
23
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
24
24
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
25
25
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
26
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
26
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
27
27
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
28
28
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
29
29
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -1119,7 +1119,7 @@ async function assessLaneAdmission(ctx, lane, options) {
|
|
|
1119
1119
|
return admission;
|
|
1120
1120
|
}
|
|
1121
1121
|
function assertLaneRunsHereForAdmission(ctx, lane) {
|
|
1122
|
-
if (
|
|
1122
|
+
if (laneRunsHereForOperatorMutation(ctx, lane))
|
|
1123
1123
|
return;
|
|
1124
1124
|
throw new LanesError("lane_remote", `Lane ${lane.lane_id} is hosted remotely; run the admission command on its Outpost`, { lane_id: lane.lane_id, host: lane.host });
|
|
1125
1125
|
}
|
|
@@ -2382,6 +2382,16 @@ function laneIsHostedHere(ctx, lane) {
|
|
|
2382
2382
|
}
|
|
2383
2383
|
return identity.kind === "id" && identity.id === lane.host.id;
|
|
2384
2384
|
}
|
|
2385
|
+
function laneRunsHereForOperatorMutation(ctx, lane) {
|
|
2386
|
+
if (lane.host.kind === "outpost")
|
|
2387
|
+
return laneIsHostedHere(ctx, lane);
|
|
2388
|
+
return laneRunsHere(lane, reconcileHostFromCtx(ctx));
|
|
2389
|
+
}
|
|
2390
|
+
function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
|
|
2391
|
+
if (laneRunsHereForOperatorMutation(ctx, lane))
|
|
2392
|
+
return;
|
|
2393
|
+
throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, host: lane.host, action });
|
|
2394
|
+
}
|
|
2385
2395
|
async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
2386
2396
|
if (lane.host.kind !== "outpost") {
|
|
2387
2397
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
@@ -2896,8 +2906,7 @@ async function runMove(ctx, laneId, opts) {
|
|
|
2896
2906
|
moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
|
|
2897
2907
|
}
|
|
2898
2908
|
else {
|
|
2899
|
-
|
|
2900
|
-
throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${source.host.kind}:${source.host.id})`, { lane_id: laneId, lane_host: source.host, current_host: currentHost });
|
|
2909
|
+
throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
|
|
2901
2910
|
}
|
|
2902
2911
|
printJson({
|
|
2903
2912
|
ok: true,
|
|
@@ -2912,12 +2921,8 @@ async function runMove(ctx, laneId, opts) {
|
|
|
2912
2921
|
async function commitLaneMove(ctx, laneId, target, account) {
|
|
2913
2922
|
assertPlacementEnabled(ctx);
|
|
2914
2923
|
const hqRoot = ctx.hqRoot();
|
|
2915
|
-
const sourceHost = laneHostFromCtx(ctx);
|
|
2916
|
-
const admissionHost = reconcileHostFromCtx(ctx);
|
|
2917
2924
|
const before = readLane(hqRoot, laneId);
|
|
2918
|
-
|
|
2919
|
-
throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${before.host.kind}:${before.host.id})`, { lane_id: laneId, lane_host: before.host, current_host: sourceHost });
|
|
2920
|
-
}
|
|
2925
|
+
assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
|
|
2921
2926
|
if (before.host.kind === target.kind && before.host.id === target.id) {
|
|
2922
2927
|
throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
|
|
2923
2928
|
lane_id: laneId,
|
|
@@ -2971,12 +2976,7 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
2971
2976
|
const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
|
|
2972
2977
|
const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
2973
2978
|
const current = readLane(hqRoot, laneId);
|
|
2974
|
-
|
|
2975
|
-
throw new LanesError("lane_move_conflict", `Lane ${laneId} changed hosts before the move`, {
|
|
2976
|
-
lane_id: laneId,
|
|
2977
|
-
host: current.host,
|
|
2978
|
-
});
|
|
2979
|
-
}
|
|
2979
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
|
|
2980
2980
|
if (!laneCanMoveBeforeStart(current)) {
|
|
2981
2981
|
throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
|
|
2982
2982
|
}
|
|
@@ -3216,17 +3216,15 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
3216
3216
|
state: lane.state,
|
|
3217
3217
|
});
|
|
3218
3218
|
}
|
|
3219
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
|
|
3219
3220
|
const host = reconcileHostFromCtx(ctx);
|
|
3220
|
-
if (!laneRunsHere(lane, host)) {
|
|
3221
|
-
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
3221
|
if (lane.pickup_validation_pending === true) {
|
|
3225
3222
|
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
|
|
3226
3223
|
return;
|
|
3227
3224
|
}
|
|
3228
3225
|
await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
3229
3226
|
lane = readLane(hqRoot, laneId);
|
|
3227
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
|
|
3230
3228
|
if (lane.state !== "queued") {
|
|
3231
3229
|
throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
|
|
3232
3230
|
lane_id: laneId,
|
|
@@ -3572,6 +3570,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
3572
3570
|
await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
|
|
3573
3571
|
return;
|
|
3574
3572
|
}
|
|
3573
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
3575
3574
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3576
3575
|
// An account move is refused on its own terms first: a running lane and a
|
|
3577
3576
|
// provider without session resume get the move's named errors, not the
|
|
@@ -3847,6 +3846,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
3847
3846
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
3848
3847
|
return;
|
|
3849
3848
|
}
|
|
3849
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
|
|
3850
3850
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3851
3851
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
3852
3852
|
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
@@ -4223,6 +4223,7 @@ async function runStoryList(ctx, laneId) {
|
|
|
4223
4223
|
async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
4224
4224
|
const hqRoot = ctx.hqRoot();
|
|
4225
4225
|
let lane = readLane(hqRoot, laneId);
|
|
4226
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4226
4227
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
4227
4228
|
if (!projectId)
|
|
4228
4229
|
throw new LanesError("missing_project", "--project is required", {});
|
|
@@ -4232,7 +4233,10 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4232
4233
|
const at = ctx.now().toISOString();
|
|
4233
4234
|
lane = await ensureStoryThread(ctx, lane, projectId);
|
|
4234
4235
|
const before = lane;
|
|
4235
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4236
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4237
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4238
|
+
return addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
4239
|
+
});
|
|
4236
4240
|
await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
4237
4241
|
const finalLane = readLane(hqRoot, laneId);
|
|
4238
4242
|
printJson({
|
|
@@ -4249,6 +4253,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4249
4253
|
async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
4250
4254
|
const hqRoot = ctx.hqRoot();
|
|
4251
4255
|
let lane = readLane(hqRoot, laneId);
|
|
4256
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4252
4257
|
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
4253
4258
|
if (target.story.status === "done") {
|
|
4254
4259
|
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
|
|
@@ -4260,7 +4265,10 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
|
4260
4265
|
const at = ctx.now().toISOString();
|
|
4261
4266
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4262
4267
|
const before = lane;
|
|
4263
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4268
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4269
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4270
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
|
|
4271
|
+
});
|
|
4264
4272
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
4265
4273
|
const finalLane = readLane(hqRoot, laneId);
|
|
4266
4274
|
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4268,6 +4276,7 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
|
4268
4276
|
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
4269
4277
|
const hqRoot = ctx.hqRoot();
|
|
4270
4278
|
let lane = readLane(hqRoot, laneId);
|
|
4279
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4271
4280
|
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
4272
4281
|
if (target.story.status === "done") {
|
|
4273
4282
|
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
|
|
@@ -4282,7 +4291,10 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
|
4282
4291
|
const at = ctx.now().toISOString();
|
|
4283
4292
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4284
4293
|
const before = lane;
|
|
4285
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4294
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4295
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4296
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
|
|
4297
|
+
});
|
|
4286
4298
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
4287
4299
|
const finalLane = readLane(hqRoot, laneId);
|
|
4288
4300
|
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4290,6 +4302,7 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
|
4290
4302
|
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
4291
4303
|
const hqRoot = ctx.hqRoot();
|
|
4292
4304
|
let lane = readLane(hqRoot, laneId);
|
|
4305
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4293
4306
|
if (!opts.verified) {
|
|
4294
4307
|
throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
|
|
4295
4308
|
}
|
|
@@ -4311,10 +4324,13 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
|
4311
4324
|
const at = ctx.now().toISOString();
|
|
4312
4325
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4313
4326
|
const before = lane;
|
|
4314
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4327
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4328
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4329
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
|
|
4330
|
+
verified_at: at,
|
|
4331
|
+
verified_by: current.senior,
|
|
4332
|
+
});
|
|
4333
|
+
});
|
|
4318
4334
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
4319
4335
|
const finalLane = readLane(hqRoot, laneId);
|
|
4320
4336
|
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4354,6 +4370,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4354
4370
|
const nextSenior = parseSeniorRef(opts.senior);
|
|
4355
4371
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
4356
4372
|
const lane = readLane(hqRoot, laneId);
|
|
4373
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
|
|
4357
4374
|
if (isTerminalLaneState(lane.state)) {
|
|
4358
4375
|
throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
4359
4376
|
}
|
|
@@ -4378,6 +4395,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4378
4395
|
continue;
|
|
4379
4396
|
if (!walkHitsLane(hqRoot, other.senior, laneId, ctx))
|
|
4380
4397
|
continue;
|
|
4398
|
+
assertLaneRunsHereForOperatorMutation(ctx, other, "reparent descendant of");
|
|
4381
4399
|
const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
|
|
4382
4400
|
const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
|
|
4383
4401
|
if (descendantDepth > MAX_LANE_DEPTH) {
|
|
@@ -4395,6 +4413,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4395
4413
|
}
|
|
4396
4414
|
const at = ctx.now().toISOString();
|
|
4397
4415
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4416
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
|
|
4398
4417
|
const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
|
|
4399
4418
|
if (already && current.depth === newDepth)
|
|
4400
4419
|
return current;
|
|
@@ -4413,6 +4432,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4413
4432
|
});
|
|
4414
4433
|
for (const descendant of descendants) {
|
|
4415
4434
|
casUpdateLane(hqRoot, descendant.laneId, (current) => {
|
|
4435
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "reparent descendant of");
|
|
4416
4436
|
if (current.depth === descendant.depth)
|
|
4417
4437
|
return current;
|
|
4418
4438
|
return { ...current, depth: descendant.depth };
|
|
@@ -4487,14 +4507,226 @@ async function runLogs(ctx, laneId, opts) {
|
|
|
4487
4507
|
export function stopCommittedState(state) {
|
|
4488
4508
|
return state === "queued" ? "cancelled" : "killed";
|
|
4489
4509
|
}
|
|
4510
|
+
function parseConfirmedOutpostStopLane(raw, laneId, outpostId, commandId) {
|
|
4511
|
+
let remoteLane;
|
|
4512
|
+
try {
|
|
4513
|
+
remoteLane = parseLaneRecord(raw);
|
|
4514
|
+
}
|
|
4515
|
+
catch (err) {
|
|
4516
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane snapshot for stop ${laneId}`, {
|
|
4517
|
+
lane_id: laneId,
|
|
4518
|
+
outpost_id: outpostId,
|
|
4519
|
+
command_id: commandId,
|
|
4520
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
if (remoteLane.lane_id !== laneId ||
|
|
4524
|
+
remoteLane.host.kind !== "outpost" ||
|
|
4525
|
+
remoteLane.host.id !== outpostId ||
|
|
4526
|
+
!isTerminalLaneState(remoteLane.state)) {
|
|
4527
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return its terminal lane ${laneId}`, {
|
|
4528
|
+
lane_id: laneId,
|
|
4529
|
+
outpost_id: outpostId,
|
|
4530
|
+
command_id: commandId,
|
|
4531
|
+
owner_lane_id: remoteLane.lane_id,
|
|
4532
|
+
owner_host: remoteLane.host,
|
|
4533
|
+
owner_state: remoteLane.state,
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
return remoteLane;
|
|
4537
|
+
}
|
|
4538
|
+
function parseRemoteCancelledQuestionIds(value, laneId, outpostId, commandId) {
|
|
4539
|
+
if (!Array.isArray(value) ||
|
|
4540
|
+
value.some((id) => typeof id !== "string" || id.trim() === "") ||
|
|
4541
|
+
new Set(value).size !== value.length) {
|
|
4542
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned invalid cancelled question ids for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: commandId });
|
|
4543
|
+
}
|
|
4544
|
+
return value;
|
|
4545
|
+
}
|
|
4546
|
+
async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
4547
|
+
let dispatched;
|
|
4548
|
+
try {
|
|
4549
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
4550
|
+
outpostId,
|
|
4551
|
+
command: buildOutpostLaneCommand({ action: "show", laneId }),
|
|
4552
|
+
timeoutSeconds: 120,
|
|
4553
|
+
waitMs: 120_000,
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
catch (err) {
|
|
4557
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Could not read terminal lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, {
|
|
4558
|
+
lane_id: laneId,
|
|
4559
|
+
outpost_id: outpostId,
|
|
4560
|
+
command_id: stopCommandId,
|
|
4561
|
+
sync_command_id: null,
|
|
4562
|
+
});
|
|
4563
|
+
}
|
|
4564
|
+
const commandId = dispatched.commandId?.trim();
|
|
4565
|
+
if (dispatched.done !== true ||
|
|
4566
|
+
!commandId ||
|
|
4567
|
+
dispatched.status !== "Success" ||
|
|
4568
|
+
dispatched.exitCode !== 0) {
|
|
4569
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
|
|
4570
|
+
lane_id: laneId,
|
|
4571
|
+
outpost_id: outpostId,
|
|
4572
|
+
command_id: stopCommandId,
|
|
4573
|
+
sync_command_id: commandId || null,
|
|
4574
|
+
status: dispatched.status,
|
|
4575
|
+
done: dispatched.done ?? false,
|
|
4576
|
+
exit_code: dispatched.exitCode,
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
let remote;
|
|
4580
|
+
try {
|
|
4581
|
+
remote = parseRemoteJson(dispatched.stdout, "Outpost lane show");
|
|
4582
|
+
}
|
|
4583
|
+
catch (err) {
|
|
4584
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane read after stop ${laneId}`, {
|
|
4585
|
+
lane_id: laneId,
|
|
4586
|
+
outpost_id: outpostId,
|
|
4587
|
+
command_id: stopCommandId,
|
|
4588
|
+
sync_command_id: commandId,
|
|
4589
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
if (remote?.ok !== true || remote.action !== "show") {
|
|
4593
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
|
|
4594
|
+
lane_id: laneId,
|
|
4595
|
+
outpost_id: outpostId,
|
|
4596
|
+
command_id: stopCommandId,
|
|
4597
|
+
sync_command_id: commandId,
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
return {
|
|
4601
|
+
lane: parseConfirmedOutpostStopLane(remote.lane, laneId, outpostId, commandId),
|
|
4602
|
+
commandId,
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
async function dispatchOutpostLaneStop(ctx, lane) {
|
|
4606
|
+
if (lane.host.kind !== "outpost") {
|
|
4607
|
+
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
4608
|
+
}
|
|
4609
|
+
const outpostId = lane.host.id;
|
|
4610
|
+
let dispatched;
|
|
4611
|
+
try {
|
|
4612
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
4613
|
+
outpostId,
|
|
4614
|
+
command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id }),
|
|
4615
|
+
timeoutSeconds: 120,
|
|
4616
|
+
waitMs: 120_000,
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
catch (err) {
|
|
4620
|
+
throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
4621
|
+
}
|
|
4622
|
+
const commandId = dispatched.commandId?.trim();
|
|
4623
|
+
if (dispatched.done !== true || !commandId) {
|
|
4624
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
|
|
4625
|
+
lane_id: lane.lane_id,
|
|
4626
|
+
outpost_id: outpostId,
|
|
4627
|
+
command_id: commandId || null,
|
|
4628
|
+
status: dispatched.status,
|
|
4629
|
+
done: dispatched.done ?? false,
|
|
4630
|
+
exit_code: dispatched.exitCode,
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane stop");
|
|
4634
|
+
let state;
|
|
4635
|
+
let alreadyTerminal = false;
|
|
4636
|
+
let ownerLane;
|
|
4637
|
+
let cancelledQuestions;
|
|
4638
|
+
let syncCommandId;
|
|
4639
|
+
if (remote?.error === "already_terminal") {
|
|
4640
|
+
if (remote.lane_id !== lane.lane_id ||
|
|
4641
|
+
!isTerminalLaneState(remote.state) ||
|
|
4642
|
+
(dispatched.status !== "Success" && dispatched.status !== "Failed") ||
|
|
4643
|
+
dispatched.exitCode === null ||
|
|
4644
|
+
dispatched.exitCode === 0) {
|
|
4645
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid already-terminal result for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: commandId });
|
|
4646
|
+
}
|
|
4647
|
+
state = remote.state;
|
|
4648
|
+
alreadyTerminal = true;
|
|
4649
|
+
const owner = await readOutpostTerminalLane(ctx, lane.lane_id, outpostId, commandId);
|
|
4650
|
+
if (owner.lane.state !== state) {
|
|
4651
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned a different terminal state for lane ${lane.lane_id}`, {
|
|
4652
|
+
lane_id: lane.lane_id,
|
|
4653
|
+
outpost_id: outpostId,
|
|
4654
|
+
command_id: commandId,
|
|
4655
|
+
sync_command_id: owner.commandId,
|
|
4656
|
+
stop_state: state,
|
|
4657
|
+
owner_state: owner.lane.state,
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
ownerLane = owner.lane;
|
|
4661
|
+
cancelledQuestions = [];
|
|
4662
|
+
syncCommandId = owner.commandId;
|
|
4663
|
+
}
|
|
4664
|
+
else if (remote?.error !== undefined) {
|
|
4665
|
+
throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
|
|
4666
|
+
? remote.message
|
|
4667
|
+
: `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
|
|
4668
|
+
lane_id: lane.lane_id,
|
|
4669
|
+
outpost_id: outpostId,
|
|
4670
|
+
command_id: commandId,
|
|
4671
|
+
remote_error: remote.error,
|
|
4672
|
+
});
|
|
4673
|
+
}
|
|
4674
|
+
else {
|
|
4675
|
+
const remoteLane = remote?.lane;
|
|
4676
|
+
const remoteLaneRecord = remoteLane !== null &&
|
|
4677
|
+
typeof remoteLane === "object" &&
|
|
4678
|
+
!Array.isArray(remoteLane)
|
|
4679
|
+
? remoteLane
|
|
4680
|
+
: null;
|
|
4681
|
+
if (dispatched.status !== "Success" ||
|
|
4682
|
+
dispatched.exitCode !== 0 ||
|
|
4683
|
+
remote?.ok !== true ||
|
|
4684
|
+
remote.action !== "stop" ||
|
|
4685
|
+
remoteLaneRecord?.lane_id !== lane.lane_id ||
|
|
4686
|
+
(remoteLaneRecord.state !== "cancelled" && remoteLaneRecord.state !== "killed")) {
|
|
4687
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the stop for lane ${lane.lane_id}`, {
|
|
4688
|
+
lane_id: lane.lane_id,
|
|
4689
|
+
outpost_id: outpostId,
|
|
4690
|
+
command_id: commandId,
|
|
4691
|
+
status: dispatched.status,
|
|
4692
|
+
exit_code: dispatched.exitCode,
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
ownerLane = parseConfirmedOutpostStopLane(remoteLaneRecord, lane.lane_id, outpostId, commandId);
|
|
4696
|
+
state = ownerLane.state;
|
|
4697
|
+
cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
|
|
4698
|
+
}
|
|
4699
|
+
const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
|
|
4700
|
+
printJson({
|
|
4701
|
+
ok: true,
|
|
4702
|
+
action: "stop",
|
|
4703
|
+
lane: updated,
|
|
4704
|
+
cancelled_questions: cancelledQuestions,
|
|
4705
|
+
outpost_id: outpostId,
|
|
4706
|
+
outpost_command_id: commandId,
|
|
4707
|
+
...(syncCommandId ? { outpost_sync_command_id: syncCommandId } : {}),
|
|
4708
|
+
outpost_result: {
|
|
4709
|
+
state,
|
|
4710
|
+
already_terminal: alreadyTerminal,
|
|
4711
|
+
status: dispatched.status,
|
|
4712
|
+
exit_code: dispatched.exitCode,
|
|
4713
|
+
},
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4490
4716
|
async function runStop(ctx, laneId) {
|
|
4491
4717
|
const hqRoot = ctx.hqRoot();
|
|
4492
4718
|
const lane = readLane(hqRoot, laneId);
|
|
4719
|
+
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
4720
|
+
await dispatchOutpostLaneStop(ctx, lane);
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
|
|
4493
4724
|
if (isTerminalLaneState(lane.state)) {
|
|
4494
4725
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
4495
4726
|
}
|
|
4496
4727
|
const at = ctx.now().toISOString();
|
|
4497
4728
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4729
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "stop");
|
|
4498
4730
|
if (isTerminalLaneState(current.state)) {
|
|
4499
4731
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${current.state}`, { lane_id: laneId, state: current.state });
|
|
4500
4732
|
}
|
|
@@ -4531,6 +4763,7 @@ async function runAsk(ctx, opts) {
|
|
|
4531
4763
|
throw new LanesError("missing_question", "--question is required", {});
|
|
4532
4764
|
}
|
|
4533
4765
|
const lane = readLane(hqRoot, laneId);
|
|
4766
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
|
|
4534
4767
|
const optionList = Array.isArray(opts.option)
|
|
4535
4768
|
? opts.option
|
|
4536
4769
|
: opts.option
|
|
@@ -4659,6 +4892,10 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
4659
4892
|
const question = readQuestion(hqRoot, questionId);
|
|
4660
4893
|
throwIfQuestionNotPending(question);
|
|
4661
4894
|
await yieldEventLoop();
|
|
4895
|
+
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
4896
|
+
if (laneAcceptsDropboxDelivery(askingLane)) {
|
|
4897
|
+
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
|
|
4898
|
+
}
|
|
4662
4899
|
const body = requireQuestionBody(hqRoot, question);
|
|
4663
4900
|
// A trimmed-empty --freeform is absent: it must not win over --choice and
|
|
4664
4901
|
// must not become a freeform answer of "".
|
|
@@ -4700,7 +4937,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
4700
4937
|
},
|
|
4701
4938
|
};
|
|
4702
4939
|
});
|
|
4703
|
-
const delivery = deliverAnswerToAskingLane(
|
|
4940
|
+
const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
|
|
4704
4941
|
printJson({
|
|
4705
4942
|
ok: true,
|
|
4706
4943
|
action: "questions.answer",
|
|
@@ -4729,13 +4966,15 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
|
|
|
4729
4966
|
* Record is already durable. Delivery is best-effort onto the asking lane's
|
|
4730
4967
|
* drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
|
|
4731
4968
|
*/
|
|
4732
|
-
function deliverAnswerToAskingLane(
|
|
4969
|
+
function deliverAnswerToAskingLane(ctx, question, choice, at) {
|
|
4970
|
+
const hqRoot = ctx.hqRoot();
|
|
4733
4971
|
const laneId = laneIdForAnswerDelivery(question);
|
|
4734
4972
|
try {
|
|
4735
4973
|
const lane = readLane(hqRoot, laneId);
|
|
4736
4974
|
if (!laneAcceptsDropboxDelivery(lane)) {
|
|
4737
4975
|
return { status: "recorded_only" };
|
|
4738
4976
|
}
|
|
4977
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
|
|
4739
4978
|
appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
|
|
4740
4979
|
return { status: "dropped" };
|
|
4741
4980
|
}
|
|
@@ -16,7 +16,7 @@ export { resolveLanesHqRoot } from "./hq-root.js";
|
|
|
16
16
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
|
17
17
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
18
18
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
19
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
19
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
|
|
20
20
|
export type { StoreDeps } from "./store.js";
|
|
21
21
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
22
22
|
export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory,
|
|
|
10
10
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
11
11
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
12
12
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
13
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
13
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
|
|
14
14
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
15
15
|
export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
|
|
16
16
|
export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
|
|
@@ -54,6 +54,12 @@ export declare function tryReadQuestion(hqRoot: string, questionId: string, deps
|
|
|
54
54
|
export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
|
|
55
55
|
export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
|
|
56
56
|
export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
|
|
57
|
+
/**
|
|
58
|
+
* Replace the primary copy with a terminal snapshot returned by the Outpost
|
|
59
|
+
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
60
|
+
* primary version is a conflict and must not be overwritten.
|
|
61
|
+
*/
|
|
62
|
+
export declare function syncLaneRecordFromOwner(hqRoot: string, laneId: string, outpostId: string, rawOwnerLane: unknown, deps?: StoreDeps): LaneRecord;
|
|
57
63
|
/** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
|
|
58
64
|
export declare function retryUpdateLane(hqRoot: string, laneId: string, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps, attempts?: number): LaneRecord;
|
|
59
65
|
/** Compare-and-set with conflict retry for question records (spec §6.2). */
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { generateUlid } from "../mesh/live/ulid.js";
|
|
14
14
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
15
15
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
16
|
-
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
16
|
+
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
17
17
|
import { laneStories } from "./stories.js";
|
|
18
18
|
export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
19
19
|
export const LANE_RECORDS_DIR = "lanes";
|
|
@@ -810,6 +810,71 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
810
810
|
return updated;
|
|
811
811
|
}, deps);
|
|
812
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Replace the primary copy with a terminal snapshot returned by the Outpost
|
|
815
|
+
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
816
|
+
* primary version is a conflict and must not be overwritten.
|
|
817
|
+
*/
|
|
818
|
+
export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane, deps = defaultStoreDeps()) {
|
|
819
|
+
let ownerLane;
|
|
820
|
+
try {
|
|
821
|
+
ownerLane = parseLaneRecord(rawOwnerLane);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${laneId}`, {
|
|
825
|
+
lane_id: laneId,
|
|
826
|
+
outpost_id: outpostId,
|
|
827
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (ownerLane.lane_id !== laneId ||
|
|
831
|
+
ownerLane.host.kind !== "outpost" ||
|
|
832
|
+
ownerLane.host.id !== outpostId ||
|
|
833
|
+
!isTerminalLaneState(ownerLane.state)) {
|
|
834
|
+
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} did not return its terminal record for lane ${laneId}`, {
|
|
835
|
+
lane_id: laneId,
|
|
836
|
+
outpost_id: outpostId,
|
|
837
|
+
owner_lane_id: ownerLane.lane_id,
|
|
838
|
+
owner_host: ownerLane.host,
|
|
839
|
+
owner_state: ownerLane.state,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
const filePath = laneRecordPath(hqRoot, laneId);
|
|
843
|
+
return withRecordLock(filePath, deps, () => {
|
|
844
|
+
const current = readMergedRecord(filePath, parseLaneRecord, mergeLaneRecords, deps);
|
|
845
|
+
if (current === null) {
|
|
846
|
+
throw new LaneSchemaError(`Record disappeared under lock: ${filePath}`, {
|
|
847
|
+
path: filePath,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
if (current.lane_id !== laneId ||
|
|
851
|
+
current.host.kind !== "outpost" ||
|
|
852
|
+
current.host.id !== outpostId) {
|
|
853
|
+
throw new LanesError("outpost_lane_sync_conflict", `Primary lane ${laneId} is no longer owned by Outpost ${outpostId}`, {
|
|
854
|
+
lane_id: laneId,
|
|
855
|
+
outpost_id: outpostId,
|
|
856
|
+
current_lane_id: current.lane_id,
|
|
857
|
+
current_host: current.host,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (ownerLane.version < current.version) {
|
|
861
|
+
throw new LanesError("outpost_lane_sync_conflict", `Outpost ${outpostId} returned lane ${laneId} at version ${ownerLane.version}, behind primary version ${current.version}`, {
|
|
862
|
+
lane_id: laneId,
|
|
863
|
+
outpost_id: outpostId,
|
|
864
|
+
owner_version: ownerLane.version,
|
|
865
|
+
primary_version: current.version,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const ownerSnapshot = ownerLane.outpost_command_id === undefined &&
|
|
869
|
+
current.outpost_command_id !== undefined
|
|
870
|
+
? { ...ownerLane, outpost_command_id: current.outpost_command_id }
|
|
871
|
+
: ownerLane;
|
|
872
|
+
const synced = mergeLaneRecords(ownerSnapshot, [current]);
|
|
873
|
+
parseLaneRecord(synced, filePath);
|
|
874
|
+
atomicWriteJson(filePath, synced, deps);
|
|
875
|
+
return synced;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
813
878
|
/** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
|
|
814
879
|
export function retryUpdateLane(hqRoot, laneId, writer, updater, deps = defaultStoreDeps(), attempts = 5) {
|
|
815
880
|
let last;
|