@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.274.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 +65 -2
- package/README.md +32 -1
- package/dist/command-catalog.generated.d.ts +83 -536
- package/dist/command-catalog.generated.js +129 -714
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/cloud.js +40 -0
- package/dist/commands/core-checkpoint.js +3 -1
- package/dist/commands/core-command-catalog.d.ts +32 -0
- package/dist/commands/core-command-catalog.js +58 -0
- package/dist/commands/core-ontology.js +9 -4
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -75
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/deploy-comments.d.ts +3 -0
- package/dist/commands/deploy-comments.js +159 -0
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-move.d.ts +12 -0
- package/dist/commands/lanes-move.js +342 -0
- package/dist/commands/lanes-resume.js +3 -1
- package/dist/commands/lanes-shared.d.ts +39 -3
- package/dist/commands/lanes-shared.js +59 -0
- package/dist/commands/lanes.d.ts +3 -5
- package/dist/commands/lanes.js +206 -478
- package/dist/commands/report.js +2 -14
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/fast-core.js +1 -1
- package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
- package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
- package/dist/lib/checkpoint-ontology/apply.js +33 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
- package/dist/lib/checkpoint-ontology/plan.js +15 -0
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/deploy-api.d.ts +6 -0
- package/dist/lib/deploy-api.js +21 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/fix/apply.d.ts +17 -2
- package/dist/lib/doctor/fix/apply.js +111 -57
- package/dist/lib/doctor/fix/json-output.d.ts +9 -1
- package/dist/lib/doctor/fix/json-output.js +11 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/first-sync-next-step.d.ts +37 -0
- package/dist/lib/first-sync-next-step.js +189 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/dist/main.js +4 -1
- package/dist/utils/cli-telemetry.d.ts +7 -1
- package/dist/utils/cli-telemetry.js +84 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
- package/dist/utils/scaffold-fast-import-guard.js +28 -0
- package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
- package/dist/utils/scaffold-packlist-guard.js +23 -0
- package/dist/utils/scaffold-sibling-references.d.ts +21 -0
- package/dist/utils/scaffold-sibling-references.js +156 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { chooseHostForLane } from "../lib/lanes/host-capacity.js";
|
|
3
|
+
import { laneRunsHere, loadLanesConfig, loadWorkerProfile, readLane, updateLane, withLaneAdmissionLock, } from "../lib/lanes/index.js";
|
|
4
|
+
import { LanesError, isLanesError } from "../lib/lanes/errors.js";
|
|
5
|
+
import { buildOutpostMoveTargetCommand, dispatchErrorMessage, } from "../lib/lanes/outpost-dispatch.js";
|
|
6
|
+
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
7
|
+
import { assertPlacementEnabled, parsePlacementPolicy, readLanePlacementEnabled, } from "./lanes-shared.js";
|
|
8
|
+
export function createLaneMoveHandlers(deps) {
|
|
9
|
+
const { assertKnownOutpost, assertLaneRunsHereForOperatorMutation, dispatchCreatedOutpostLane, loadHostCapacitySnapshots, ownerErrorFromOutput, parseRemoteJson, printJson, readJsonFile, reconcileHostFromCtx, runDir, timeoutPath, } = deps;
|
|
10
|
+
function laneCanMoveBeforeStart(lane) {
|
|
11
|
+
return lane.state === "queued" &&
|
|
12
|
+
lane.pickup_validation_pending !== true &&
|
|
13
|
+
lane.admission_reserved_at === undefined &&
|
|
14
|
+
lane.provider_session_id === undefined &&
|
|
15
|
+
lane.provider_session_id_absent === undefined &&
|
|
16
|
+
lane.provider_home_at_spawn === undefined &&
|
|
17
|
+
lane.last_resume === undefined &&
|
|
18
|
+
lane.last_run_usage === undefined &&
|
|
19
|
+
lane.last_host_report_at === undefined &&
|
|
20
|
+
lane.pending_resume === undefined;
|
|
21
|
+
}
|
|
22
|
+
function readLaneTimeout(hqRoot, laneId) {
|
|
23
|
+
const raw = readJsonFile(timeoutPath(hqRoot, laneId));
|
|
24
|
+
if (raw === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (typeof raw !== "object" || Array.isArray(raw) ||
|
|
27
|
+
typeof raw.timeout_secs !== "number" ||
|
|
28
|
+
typeof raw.deadline_at !== "string" ||
|
|
29
|
+
!Number.isSafeInteger(raw.timeout_secs) ||
|
|
30
|
+
raw.timeout_secs <= 0 ||
|
|
31
|
+
!Number.isFinite(Date.parse(raw.deadline_at))) {
|
|
32
|
+
throw new LanesError("lane_timeout_invalid", `Lane ${laneId} has a malformed timeout record`, {
|
|
33
|
+
lane_id: laneId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const record = raw;
|
|
37
|
+
return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
|
|
38
|
+
}
|
|
39
|
+
async function runMove(ctx, laneId, opts) {
|
|
40
|
+
const hqRoot = ctx.hqRoot();
|
|
41
|
+
const policy = parsePlacementPolicy(opts.placement ?? "");
|
|
42
|
+
const source = readLane(hqRoot, laneId);
|
|
43
|
+
const placementCompany = resolveLaneCompany(hqRoot, source).company ?? undefined;
|
|
44
|
+
let placementGate;
|
|
45
|
+
const placementEnabled = () => {
|
|
46
|
+
placementGate ??= readLanePlacementEnabled(ctx, placementCompany);
|
|
47
|
+
return placementGate;
|
|
48
|
+
};
|
|
49
|
+
await assertPlacementEnabled(ctx, placementCompany, placementEnabled);
|
|
50
|
+
if (!laneCanMoveBeforeStart(source)) {
|
|
51
|
+
throw new LanesError(source.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${source.state}`, { lane_id: laneId, state: source.state });
|
|
52
|
+
}
|
|
53
|
+
const sourceHost = source.host;
|
|
54
|
+
const config = loadLanesConfig(hqRoot);
|
|
55
|
+
const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
|
|
56
|
+
let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
|
|
57
|
+
if (policy.kind === "outpost") {
|
|
58
|
+
targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "outpost" && snapshot.host.id === policy.id);
|
|
59
|
+
}
|
|
60
|
+
else if (policy.kind === "local") {
|
|
61
|
+
targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "local");
|
|
62
|
+
}
|
|
63
|
+
const profileAccounts = source.worker_profile === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: loadWorkerProfile(hqRoot, source.worker_profile).meta.accounts;
|
|
66
|
+
const allowedAccounts = source.admission_accounts ?? profileAccounts;
|
|
67
|
+
const choice = chooseHostForLane(targetSnapshots, {
|
|
68
|
+
provider: source.provider,
|
|
69
|
+
...(source.account !== undefined ? { account: source.account } : {}),
|
|
70
|
+
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
71
|
+
});
|
|
72
|
+
let moved;
|
|
73
|
+
if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
|
|
74
|
+
moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
|
|
75
|
+
}
|
|
76
|
+
else if (source.host.kind === "outpost") {
|
|
77
|
+
moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
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" });
|
|
81
|
+
}
|
|
82
|
+
printJson({
|
|
83
|
+
ok: true,
|
|
84
|
+
action: "move",
|
|
85
|
+
lane_id: laneId,
|
|
86
|
+
state: moved.state,
|
|
87
|
+
host: moved.host,
|
|
88
|
+
account: moved.account ?? null,
|
|
89
|
+
lane: moved,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
|
|
93
|
+
const hqRoot = ctx.hqRoot();
|
|
94
|
+
const before = readLane(hqRoot, laneId);
|
|
95
|
+
assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
|
|
96
|
+
await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
|
|
97
|
+
if (before.host.kind === target.kind && before.host.id === target.id) {
|
|
98
|
+
throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
|
|
99
|
+
lane_id: laneId,
|
|
100
|
+
host: target,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!laneCanMoveBeforeStart(before)) {
|
|
104
|
+
throw new LanesError(before.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${before.state}`, { lane_id: laneId, state: before.state });
|
|
105
|
+
}
|
|
106
|
+
if (target.kind === "outpost") {
|
|
107
|
+
let known;
|
|
108
|
+
try {
|
|
109
|
+
known = await ctx.listOutpostIds();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new LanesError("outpost_list_unavailable", `Failed to list Outpost ids: ${dispatchErrorMessage(err)}`, {});
|
|
113
|
+
}
|
|
114
|
+
assertKnownOutpost(target.id, known);
|
|
115
|
+
}
|
|
116
|
+
const accountConfig = loadLanesConfig(hqRoot).providers[before.provider].accounts[account];
|
|
117
|
+
if (accountConfig === undefined || !accountConfig.enabled) {
|
|
118
|
+
throw new LanesError("lane_move_account_invalid", `Account ${account} is unavailable for ${before.provider}`, {
|
|
119
|
+
lane_id: laneId,
|
|
120
|
+
provider: before.provider,
|
|
121
|
+
account,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const profileAccounts = before.worker_profile === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: loadWorkerProfile(hqRoot, before.worker_profile).meta.accounts;
|
|
127
|
+
const allowedAccounts = before.admission_accounts ?? profileAccounts;
|
|
128
|
+
if (allowedAccounts !== undefined &&
|
|
129
|
+
!allowedAccounts.includes(account)) {
|
|
130
|
+
throw new LanesError("lane_move_account_invalid", `Account ${account} is not allowed by lane ${laneId}`, {
|
|
131
|
+
lane_id: laneId,
|
|
132
|
+
account,
|
|
133
|
+
allowed_accounts: allowedAccounts,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const briefText = target.kind === "outpost"
|
|
137
|
+
? (() => {
|
|
138
|
+
const briefPath = path.join(runDir(hqRoot, laneId), "brief.md");
|
|
139
|
+
if (!ctx.existsSync(briefPath)) {
|
|
140
|
+
throw new LanesError("lane_move_brief_missing", `Lane ${laneId} has no stored brief to dispatch`, {
|
|
141
|
+
lane_id: laneId,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return ctx.readFileSync(briefPath, "utf8");
|
|
145
|
+
})()
|
|
146
|
+
: undefined;
|
|
147
|
+
const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
|
|
148
|
+
const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
149
|
+
const current = readLane(hqRoot, laneId);
|
|
150
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
|
|
151
|
+
if (!laneCanMoveBeforeStart(current)) {
|
|
152
|
+
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 });
|
|
153
|
+
}
|
|
154
|
+
return updateLane(hqRoot, laneId, current.version, "command", (live) => ({
|
|
155
|
+
...live,
|
|
156
|
+
host: target,
|
|
157
|
+
account,
|
|
158
|
+
queued_reason: undefined,
|
|
159
|
+
admission_reserved_at: undefined,
|
|
160
|
+
}));
|
|
161
|
+
}, ctx.sleep);
|
|
162
|
+
if (target.kind !== "outpost")
|
|
163
|
+
return moved;
|
|
164
|
+
try {
|
|
165
|
+
return await dispatchCreatedOutpostLane(ctx, {
|
|
166
|
+
hqRoot,
|
|
167
|
+
lane: moved,
|
|
168
|
+
outpostId: target.id,
|
|
169
|
+
briefText: briefText ?? "",
|
|
170
|
+
timeout,
|
|
171
|
+
preserveQueuedOnFailure: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const dispatchDetails = isLanesError(err) ? err.details : {};
|
|
176
|
+
const commandId = typeof dispatchDetails.command_id === "string" &&
|
|
177
|
+
dispatchDetails.command_id.trim() !== ""
|
|
178
|
+
? dispatchDetails.command_id
|
|
179
|
+
: undefined;
|
|
180
|
+
const definitelyNotSubmitted = commandId === undefined &&
|
|
181
|
+
dispatchDetails.outcome_unknown !== true &&
|
|
182
|
+
dispatchDetails.done !== false;
|
|
183
|
+
const knownNoPersistence = (isLanesError(err) && err.error === "outpost_self_target") ||
|
|
184
|
+
(dispatchDetails.done === true && dispatchDetails.pickup_persisted === false);
|
|
185
|
+
if (!definitelyNotSubmitted && !knownNoPersistence) {
|
|
186
|
+
const checkCommand = `hq lanes show ${laneId}`;
|
|
187
|
+
const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; outcome is unknown and the lane remains assigned to Outpost ${target.id}. ` +
|
|
188
|
+
`Check the target copy by running \`${checkCommand}\` on Outpost ${target.id}. ` +
|
|
189
|
+
dispatchErrorMessage(err);
|
|
190
|
+
throw new LanesError("lane_move_dispatch_failed", message, {
|
|
191
|
+
lane_id: laneId,
|
|
192
|
+
message,
|
|
193
|
+
source_host: before.host,
|
|
194
|
+
target_host: target,
|
|
195
|
+
source_restored: false,
|
|
196
|
+
outcome: "unknown",
|
|
197
|
+
...(commandId !== undefined ? { command_id: commandId } : {}),
|
|
198
|
+
check_command: checkCommand,
|
|
199
|
+
...(dispatchDetails.owner_error !== undefined
|
|
200
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
201
|
+
: {}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
let restored = false;
|
|
205
|
+
let rollbackError;
|
|
206
|
+
try {
|
|
207
|
+
restored = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
208
|
+
const current = readLane(hqRoot, laneId);
|
|
209
|
+
if (current.version !== moved.version ||
|
|
210
|
+
current.state !== "queued" ||
|
|
211
|
+
current.host.kind !== target.kind ||
|
|
212
|
+
current.host.id !== target.id ||
|
|
213
|
+
current.account !== account) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
updateLane(hqRoot, laneId, current.version, "command", (live) => ({
|
|
217
|
+
...live,
|
|
218
|
+
host: before.host,
|
|
219
|
+
account: before.account,
|
|
220
|
+
queued_reason: before.queued_reason,
|
|
221
|
+
admission_reserved_at: before.admission_reserved_at,
|
|
222
|
+
}));
|
|
223
|
+
return true;
|
|
224
|
+
}, ctx.sleep);
|
|
225
|
+
}
|
|
226
|
+
catch (restoreErr) {
|
|
227
|
+
rollbackError = restoreErr;
|
|
228
|
+
}
|
|
229
|
+
const dispatchMessage = dispatchErrorMessage(err);
|
|
230
|
+
const outcome = restored
|
|
231
|
+
? "the lane stayed queued on its source"
|
|
232
|
+
: rollbackError === undefined
|
|
233
|
+
? "the lane changed after the move and was not overwritten"
|
|
234
|
+
: `source restoration failed: ${dispatchErrorMessage(rollbackError)}`;
|
|
235
|
+
const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; ${outcome}: ${dispatchMessage}`;
|
|
236
|
+
throw new LanesError("lane_move_dispatch_failed", message, {
|
|
237
|
+
lane_id: laneId,
|
|
238
|
+
message,
|
|
239
|
+
source_host: before.host,
|
|
240
|
+
target_host: target,
|
|
241
|
+
source_restored: restored,
|
|
242
|
+
...(dispatchDetails.owner_error !== undefined
|
|
243
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
244
|
+
: {}),
|
|
245
|
+
...(rollbackError !== undefined
|
|
246
|
+
? { rollback_error: dispatchErrorMessage(rollbackError) }
|
|
247
|
+
: {}),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function dispatchRemoteLaneMove(ctx, source, target, account) {
|
|
252
|
+
if (source.host.kind !== "outpost") {
|
|
253
|
+
throw new LanesError("lane_move_wrong_host", `Lane ${source.lane_id} is not Outpost-hosted`, {
|
|
254
|
+
lane_id: source.lane_id,
|
|
255
|
+
host: source.host,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
let dispatched;
|
|
259
|
+
try {
|
|
260
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
261
|
+
outpostId: source.host.id,
|
|
262
|
+
command: buildOutpostMoveTargetCommand({
|
|
263
|
+
laneId: source.lane_id,
|
|
264
|
+
host: target,
|
|
265
|
+
account,
|
|
266
|
+
}),
|
|
267
|
+
timeoutSeconds: 120,
|
|
268
|
+
waitMs: 120_000,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
throw new LanesError("lane_move_dispatch_failed", `Could not ask Outpost ${source.host.id} to move lane ${source.lane_id}: ${dispatchErrorMessage(err)}`, { lane_id: source.lane_id, source_outpost: source.host.id });
|
|
273
|
+
}
|
|
274
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
275
|
+
if (ownerError) {
|
|
276
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
|
|
277
|
+
lane_id: source.lane_id,
|
|
278
|
+
source_outpost: source.host.id,
|
|
279
|
+
command_id: dispatched.commandId,
|
|
280
|
+
status: dispatched.status,
|
|
281
|
+
done: dispatched.done ?? false,
|
|
282
|
+
exit_code: dispatched.exitCode,
|
|
283
|
+
owner_error: ownerError,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
287
|
+
throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
|
|
288
|
+
lane_id: source.lane_id,
|
|
289
|
+
source_outpost: source.host.id,
|
|
290
|
+
command_id: dispatched.commandId,
|
|
291
|
+
status: dispatched.status,
|
|
292
|
+
done: dispatched.done ?? false,
|
|
293
|
+
exit_code: dispatched.exitCode,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane move");
|
|
297
|
+
const remoteHost = remote?.host;
|
|
298
|
+
if (remote?.ok !== true ||
|
|
299
|
+
remote.action !== "move-target" ||
|
|
300
|
+
remote.lane_id !== source.lane_id ||
|
|
301
|
+
remote.state !== "queued" ||
|
|
302
|
+
remote.account !== account ||
|
|
303
|
+
remoteHost === null || typeof remoteHost !== "object" || Array.isArray(remoteHost) ||
|
|
304
|
+
remoteHost.kind !== target.kind ||
|
|
305
|
+
remoteHost.id !== target.id) {
|
|
306
|
+
throw new LanesError("lane_move_unconfirmed", `Outpost ${source.host.id} returned an invalid lane move result`, {
|
|
307
|
+
lane_id: source.lane_id,
|
|
308
|
+
source_outpost: source.host.id,
|
|
309
|
+
target,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...source,
|
|
314
|
+
state: "queued",
|
|
315
|
+
host: target,
|
|
316
|
+
account,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function runMoveTarget(ctx, laneId, opts) {
|
|
320
|
+
const kind = opts.hostKind?.trim();
|
|
321
|
+
const id = opts.hostId?.trim();
|
|
322
|
+
const account = opts.account?.trim();
|
|
323
|
+
if ((kind !== "local" && kind !== "outpost") || !id || !account) {
|
|
324
|
+
throw new LanesError("lane_move_target_invalid", "Move target requires a valid host kind, host id, and account", {
|
|
325
|
+
host_kind: kind ?? null,
|
|
326
|
+
host_id: id ?? null,
|
|
327
|
+
account: account ?? null,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const moved = await commitLaneMove(ctx, laneId, { kind, id }, account);
|
|
331
|
+
printJson({
|
|
332
|
+
ok: true,
|
|
333
|
+
action: "move-target",
|
|
334
|
+
lane_id: moved.lane_id,
|
|
335
|
+
state: moved.state,
|
|
336
|
+
host: moved.host,
|
|
337
|
+
account: moved.account ?? null,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return { runMove, runMoveTarget };
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=lanes-move.js.map
|
|
@@ -124,7 +124,9 @@ export function createLaneResumeHandlers(deps) {
|
|
|
124
124
|
throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
125
125
|
}
|
|
126
126
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
127
|
-
|
|
127
|
+
if (!resolution.remoteOnly) {
|
|
128
|
+
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
129
|
+
}
|
|
128
130
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts, resolution.remoteOnly
|
|
129
131
|
? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
|
|
130
132
|
: undefined);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
1
|
+
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { ProbeCommand } from "../lib/lanes/models.js";
|
|
@@ -41,6 +41,8 @@ export interface LaneMessageOptions {
|
|
|
41
41
|
effort?: string;
|
|
42
42
|
worker?: string;
|
|
43
43
|
grantContext?: string;
|
|
44
|
+
/** Primary-verified caller identity for a superseded Outpost message. */
|
|
45
|
+
supersededMessageCallerId?: string;
|
|
44
46
|
monitorEvent?: {
|
|
45
47
|
monitor_id: string;
|
|
46
48
|
description: string;
|
|
@@ -237,10 +239,15 @@ export interface Ctx {
|
|
|
237
239
|
storyLedgerEnabled: () => boolean;
|
|
238
240
|
isSsmOwnerDispatch: () => boolean;
|
|
239
241
|
withGrantHireLocks: typeof withLaneHireLocks;
|
|
240
|
-
|
|
242
|
+
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
241
243
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
242
|
-
trackerActor: () => string | null;
|
|
243
244
|
}
|
|
245
|
+
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
246
|
+
export declare function defaultLanePlacementEnabled(company: LaneCompanyRef | undefined): Promise<boolean>;
|
|
247
|
+
export declare function readLanePlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined): Promise<boolean>;
|
|
248
|
+
export declare function processLanePlacementCompany(ctx: Ctx): LaneCompanyRef | undefined;
|
|
249
|
+
export declare function parsePlacementPolicy(raw: string): LanePlacementPolicy;
|
|
250
|
+
export declare function assertPlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined, readPlacementEnabled?: () => Promise<boolean>): Promise<void>;
|
|
244
251
|
export interface LaneWaitCommandOptions {
|
|
245
252
|
any?: string[];
|
|
246
253
|
for?: string[];
|
|
@@ -267,6 +274,35 @@ export interface LaneWatchHandlerDeps {
|
|
|
267
274
|
printJson(value: unknown): void;
|
|
268
275
|
readOutpostCapacity(ctx: Ctx, outpostId: string): Promise<HostCapacitySnapshot>;
|
|
269
276
|
}
|
|
277
|
+
export interface LaneMoveHandlerDeps {
|
|
278
|
+
assertKnownOutpost(requested: string, known: string[]): void;
|
|
279
|
+
assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
|
|
280
|
+
dispatchCreatedOutpostLane(ctx: Ctx, input: {
|
|
281
|
+
hqRoot: string;
|
|
282
|
+
lane: LaneRecord;
|
|
283
|
+
outpostId: string;
|
|
284
|
+
briefText: string;
|
|
285
|
+
timeout?: {
|
|
286
|
+
timeout_secs: number;
|
|
287
|
+
deadline_at: string;
|
|
288
|
+
};
|
|
289
|
+
preserveQueuedOnFailure?: boolean;
|
|
290
|
+
}): Promise<LaneRecord>;
|
|
291
|
+
loadHostCapacitySnapshots(ctx: Ctx, hqRoot: string, config: LanesConfig): Promise<{
|
|
292
|
+
snapshots: HostCapacitySnapshot[];
|
|
293
|
+
}>;
|
|
294
|
+
ownerErrorFromOutput(stdout: string): {
|
|
295
|
+
error: string;
|
|
296
|
+
message: string;
|
|
297
|
+
details: Record<string, unknown>;
|
|
298
|
+
} | undefined;
|
|
299
|
+
parseRemoteJson(stdout: string, surface: string): Record<string, unknown> | null;
|
|
300
|
+
printJson(value: unknown): void;
|
|
301
|
+
readJsonFile(filePath: string): unknown | null;
|
|
302
|
+
reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
|
|
303
|
+
runDir(hqRoot: string, laneId: string): string;
|
|
304
|
+
timeoutPath(hqRoot: string, laneId: string): string;
|
|
305
|
+
}
|
|
270
306
|
export interface PrimaryOwnedOutpostQuestion {
|
|
271
307
|
question: QuestionRecord;
|
|
272
308
|
lane: LaneRecord;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
|
|
2
|
+
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
1
3
|
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
2
4
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
3
5
|
import { AccountResolutionError, accountStatus, loadLanesConfig } from "../lib/lanes/index.js";
|
|
@@ -92,4 +94,61 @@ export function recordedResumeEffort(lane) {
|
|
|
92
94
|
source: lane.effort_source ?? "none",
|
|
93
95
|
};
|
|
94
96
|
}
|
|
97
|
+
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
98
|
+
function lanePlacementFlagErrorCode(error) {
|
|
99
|
+
if (error && typeof error === "object") {
|
|
100
|
+
for (const key of ["code", "error", "name"]) {
|
|
101
|
+
const value = error[key];
|
|
102
|
+
if (typeof value === "string" && value.trim())
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return "UnknownError";
|
|
107
|
+
}
|
|
108
|
+
function reportLanePlacementFlagReadFailure(error) {
|
|
109
|
+
process.stderr.write(`${JSON.stringify({
|
|
110
|
+
event: "lanes.outpost_load_balancing.flag_read_failed",
|
|
111
|
+
flag: LANE_OUTPOST_LOAD_BALANCING_FLAG,
|
|
112
|
+
error: lanePlacementFlagErrorCode(error),
|
|
113
|
+
fallback: false,
|
|
114
|
+
})}\n`);
|
|
115
|
+
}
|
|
116
|
+
export function defaultLanePlacementEnabled(company) {
|
|
117
|
+
return readLanesRegistryFlag(LANE_OUTPOST_LOAD_BALANCING_FLAG, {
|
|
118
|
+
company,
|
|
119
|
+
reportFailure: reportLanePlacementFlagReadFailure,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export async function readLanePlacementEnabled(ctx, company) {
|
|
123
|
+
try {
|
|
124
|
+
return await ctx.lanePlacementEnabled(company);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
reportLanePlacementFlagReadFailure(error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function processLanePlacementCompany(ctx) {
|
|
132
|
+
const context = resolveProcessFlagCompanyContext(ctx.env);
|
|
133
|
+
if (!context.companySlug)
|
|
134
|
+
return undefined;
|
|
135
|
+
return { slug: context.companySlug, uid: context.companyUid ?? "" };
|
|
136
|
+
}
|
|
137
|
+
export function parsePlacementPolicy(raw) {
|
|
138
|
+
const value = raw.trim();
|
|
139
|
+
if (value === "local")
|
|
140
|
+
return { kind: "local" };
|
|
141
|
+
if (value === "auto")
|
|
142
|
+
return { kind: "auto" };
|
|
143
|
+
const match = /^outpost:(.+)$/.exec(value);
|
|
144
|
+
const id = match?.[1]?.trim();
|
|
145
|
+
if (id)
|
|
146
|
+
return { kind: "outpost", id };
|
|
147
|
+
throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
|
|
148
|
+
}
|
|
149
|
+
export async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
|
|
150
|
+
if (await readPlacementEnabled())
|
|
151
|
+
return;
|
|
152
|
+
throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
|
|
153
|
+
}
|
|
95
154
|
//# sourceMappingURL=lanes-shared.js.map
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { type LanesMesh, type SpawnLoginResult } from "./lanes-shared.js";
|
|
10
10
|
export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
|
|
11
|
+
export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
11
12
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
12
13
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
13
14
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
@@ -90,14 +91,11 @@ export interface LanesCommandDeps {
|
|
|
90
91
|
isSsmOwnerDispatch?: () => boolean;
|
|
91
92
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
92
93
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
93
|
-
/** Test seam for the hq-flags gated tracker commands. */
|
|
94
|
-
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
95
94
|
/** Test seam for the review-before-merge registry flag. */
|
|
96
95
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
97
|
-
/** Test seam for
|
|
98
|
-
|
|
96
|
+
/** Test seam for the repository-merge-hold registry flag. */
|
|
97
|
+
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
99
98
|
}
|
|
100
|
-
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
101
99
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
102
100
|
export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
|
|
103
101
|
/**
|