@indigoai-us/hq-cli 5.176.0 → 5.178.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 +21 -0
- package/dist/command-catalog.generated.d.ts +40 -0
- package/dist/command-catalog.generated.js +52 -0
- package/dist/commands/lanes.d.ts +4 -1
- package/dist/commands/lanes.js +605 -121
- package/dist/lib/bot/daemon.js +20 -17
- package/dist/lib/daemon/install.d.ts +3 -1
- package/dist/lib/daemon/install.js +3 -3
- package/dist/lib/lanes/hire-grants.d.ts +25 -0
- package/dist/lib/lanes/hire-grants.js +85 -0
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +3 -1
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/store.d.ts +5 -1
- package/dist/lib/lanes/store.js +25 -3
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/live/daemon/install.js +2 -2
- package/dist/lib/service-manager/launchd.d.ts +44 -7
- package/dist/lib/service-manager/launchd.js +69 -14
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
|
16
16
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
17
17
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
18
18
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
19
|
+
export { LANE_HIRE_GRANTS_FLAG };
|
|
19
20
|
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
20
21
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
21
22
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -24,7 +25,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
24
25
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
25
26
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
26
27
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
27
|
-
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";
|
|
28
|
+
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, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, 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, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
28
29
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
29
30
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
30
31
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -51,6 +52,9 @@ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
|
51
52
|
function defaultLanePlacementEnabled() {
|
|
52
53
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
53
54
|
}
|
|
55
|
+
function defaultLaneHireGrantsEnabled() {
|
|
56
|
+
return resolveProcessFlagGate(LANE_HIRE_GRANTS_FLAG, { fallback: false }, () => false);
|
|
57
|
+
}
|
|
54
58
|
function withoutProviderHome(value) {
|
|
55
59
|
if (Array.isArray(value))
|
|
56
60
|
return value.map(withoutProviderHome);
|
|
@@ -242,6 +246,7 @@ function resolveDeps(deps = {}) {
|
|
|
242
246
|
})),
|
|
243
247
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
244
248
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
249
|
+
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
245
250
|
};
|
|
246
251
|
}
|
|
247
252
|
function livenessDepsFromCtx(ctx) {
|
|
@@ -286,6 +291,16 @@ export function parseSeniorRef(raw) {
|
|
|
286
291
|
}
|
|
287
292
|
return { kind, id };
|
|
288
293
|
}
|
|
294
|
+
function parseHireGrant(raw) {
|
|
295
|
+
if (!/^(0|[1-9][0-9]*)$/.test(raw)) {
|
|
296
|
+
throw new LanesError("hire_grant_invalid", `Hire grant must be a non-negative safe integer; received ${JSON.stringify(raw)}`, { value: raw });
|
|
297
|
+
}
|
|
298
|
+
const value = Number(raw);
|
|
299
|
+
if (!Number.isSafeInteger(value)) {
|
|
300
|
+
throw new LanesError("hire_grant_invalid", `Hire grant must be a non-negative safe integer; received ${JSON.stringify(raw)}`, { value: raw });
|
|
301
|
+
}
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
289
304
|
function seniorKey(ref) {
|
|
290
305
|
return ref.kind === "user" ? "user" : `${ref.kind}:${ref.id ?? ""}`;
|
|
291
306
|
}
|
|
@@ -416,6 +431,174 @@ function requireQuestionBody(hqRoot, question) {
|
|
|
416
431
|
function loadAllLanes(hqRoot) {
|
|
417
432
|
return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
418
433
|
}
|
|
434
|
+
function laneHireFields(lane, lanes) {
|
|
435
|
+
const aliveChildren = aliveDirectChildren(lanes, lane.lane_id);
|
|
436
|
+
const hireGrant = lane.hire_grant ?? 0;
|
|
437
|
+
const hireSummary = `hire ${aliveChildren.length}/${hireGrant}`;
|
|
438
|
+
return {
|
|
439
|
+
hire_grant: hireGrant,
|
|
440
|
+
hire_alive_children: aliveChildren.length,
|
|
441
|
+
hire: `${aliveChildren.length}/${hireGrant}`,
|
|
442
|
+
hire_summary: hireSummary,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function ownerLaneHireFields(ctx, lane) {
|
|
446
|
+
if (lane.host.kind !== "outpost") {
|
|
447
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
448
|
+
lane_id: lane.lane_id,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const outpostId = lane.host.id;
|
|
452
|
+
const outpostCliEnvKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
453
|
+
const configuredHqBinary = ctx.env[outpostCliEnvKey];
|
|
454
|
+
const hqBinary = configuredHqBinary === undefined ? undefined : configuredHqBinary.trim();
|
|
455
|
+
if (configuredHqBinary !== undefined &&
|
|
456
|
+
(!hqBinary ||
|
|
457
|
+
!path.posix.isAbsolute(hqBinary) ||
|
|
458
|
+
hqBinary.includes("\0") ||
|
|
459
|
+
hqBinary.includes("\r") ||
|
|
460
|
+
hqBinary.includes("\n"))) {
|
|
461
|
+
throw new LanesError("outpost_lane_cli_invalid", `${outpostCliEnvKey} must be an absolute remote executable path`, { outpost_id: outpostId });
|
|
462
|
+
}
|
|
463
|
+
let dispatched;
|
|
464
|
+
try {
|
|
465
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
466
|
+
outpostId,
|
|
467
|
+
command: buildOutpostLaneCommand({
|
|
468
|
+
action: "show",
|
|
469
|
+
laneId: lane.lane_id,
|
|
470
|
+
hqBinary,
|
|
471
|
+
}),
|
|
472
|
+
timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
|
|
473
|
+
waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
throw new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summary for lane ${lane.lane_id} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
478
|
+
}
|
|
479
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
480
|
+
throw new LanesError("outpost_lane_hire_summary_unavailable", `Outpost ${outpostId} did not confirm the hire summary for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: dispatched.commandId });
|
|
481
|
+
}
|
|
482
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane hire summary");
|
|
483
|
+
if (!remote || remote.ok !== true || remote.action !== "show") {
|
|
484
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid hire summary for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: dispatched.commandId });
|
|
485
|
+
}
|
|
486
|
+
let ownerLane;
|
|
487
|
+
try {
|
|
488
|
+
ownerLane = parseLaneRecord(remote.lane);
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, cause: dispatchErrorMessage(err) });
|
|
492
|
+
}
|
|
493
|
+
const grant = remote.hire_grant;
|
|
494
|
+
const alive = remote.hire_alive_children;
|
|
495
|
+
if (ownerLane.lane_id !== lane.lane_id ||
|
|
496
|
+
ownerLane.host.kind !== "outpost" ||
|
|
497
|
+
ownerLane.host.id !== outpostId ||
|
|
498
|
+
!Number.isSafeInteger(grant) ||
|
|
499
|
+
grant < 0 ||
|
|
500
|
+
!Number.isSafeInteger(alive) ||
|
|
501
|
+
alive < 0 ||
|
|
502
|
+
remote.hire !== `${String(alive)}/${String(grant)}` ||
|
|
503
|
+
remote.hire_summary !== `hire ${String(alive)}/${String(grant)}`) {
|
|
504
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an inconsistent hire summary for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: dispatched.commandId });
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
hire_grant: grant,
|
|
508
|
+
hire_alive_children: alive,
|
|
509
|
+
hire: `${alive}/${grant}`,
|
|
510
|
+
hire_summary: `hire ${alive}/${grant}`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS = 20;
|
|
514
|
+
const OUTPOST_HIRE_SUMMARY_WAIT_MS = 15_000;
|
|
515
|
+
const OUTPOST_HIRE_SUMMARY_CONCURRENCY = 4;
|
|
516
|
+
async function laneHirePresentation(ctx, lane, lanes) {
|
|
517
|
+
const local = laneHireFields(lane, lanes);
|
|
518
|
+
if (!ctx.laneHireGrantsEnabled() ||
|
|
519
|
+
lane.host.kind !== "outpost" ||
|
|
520
|
+
isTerminalLaneState(lane.state) ||
|
|
521
|
+
laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
522
|
+
return { ...local, hire_source: "local" };
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
return { ...(await ownerLaneHireFields(ctx, lane)), hire_source: "owner" };
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
const detail = isLanesError(err)
|
|
529
|
+
? `${err.error}: ${err.message}`
|
|
530
|
+
: `outpost_lane_hire_summary_unavailable: ${dispatchErrorMessage(err)}`;
|
|
531
|
+
return {
|
|
532
|
+
...local,
|
|
533
|
+
hire_source: "local",
|
|
534
|
+
hire_warning: detail,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function mapWithConcurrency(values, concurrency, map) {
|
|
539
|
+
const results = new Array(values.length);
|
|
540
|
+
let nextIndex = 0;
|
|
541
|
+
const workerCount = Math.min(concurrency, values.length);
|
|
542
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
543
|
+
while (nextIndex < values.length) {
|
|
544
|
+
const index = nextIndex++;
|
|
545
|
+
results[index] = await map(values[index], index);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
await Promise.all(workers);
|
|
549
|
+
return results;
|
|
550
|
+
}
|
|
551
|
+
function withResumeHireAdmission(ctx, lane, fn) {
|
|
552
|
+
const hqRoot = ctx.hqRoot();
|
|
553
|
+
if (!ctx.laneHireGrantsEnabled() ||
|
|
554
|
+
!isTerminalLaneState(lane.state) ||
|
|
555
|
+
lane.senior.kind !== "lane" ||
|
|
556
|
+
!lane.senior.id) {
|
|
557
|
+
return fn();
|
|
558
|
+
}
|
|
559
|
+
return withHireGrantAdmission(hqRoot, lane.senior.id, ({ parent, children, parentGrant, availableChildGrant }) => {
|
|
560
|
+
const current = readLane(hqRoot, lane.lane_id);
|
|
561
|
+
// A different resume already reactivated this lane; it occupies the slot
|
|
562
|
+
// in the shared snapshot and is not a second hire.
|
|
563
|
+
if (!isTerminalLaneState(current.state))
|
|
564
|
+
return fn();
|
|
565
|
+
if (isTerminalLaneState(parent.state)) {
|
|
566
|
+
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot resume child lanes`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
567
|
+
}
|
|
568
|
+
if (children.length >= (parent.hire_grant ?? 0)) {
|
|
569
|
+
throw hireGrantCapacityError(parent, children);
|
|
570
|
+
}
|
|
571
|
+
const requestedGrant = current.hire_grant ?? 0;
|
|
572
|
+
if (requestedGrant > availableChildGrant) {
|
|
573
|
+
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
574
|
+
senior_lane_id: parent.lane_id,
|
|
575
|
+
parent_grant: parentGrant,
|
|
576
|
+
alive_children: children.map((child) => child.lane_id),
|
|
577
|
+
assigned_child_grants: assignedChildHireGrants(children),
|
|
578
|
+
requested_hire_grant: requestedGrant,
|
|
579
|
+
available_hire_grant: availableChildGrant,
|
|
580
|
+
ask_guidance: {
|
|
581
|
+
envelope: "decision=ask; include the desired total grant and the reason",
|
|
582
|
+
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return fn();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function hireGrantCapacityError(parent, children) {
|
|
590
|
+
const grant = parent.hire_grant ?? 0;
|
|
591
|
+
return new LanesError("hire_grant_exhausted", `Senior lane ${parent.lane_id} has hire grant ${grant} and ${children.length} alive direct children (${children.map((child) => child.lane_id).join(", ") || "none"}). Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
592
|
+
senior_lane_id: parent.lane_id,
|
|
593
|
+
parent_grant: grant,
|
|
594
|
+
alive_children: children.map((child) => child.lane_id),
|
|
595
|
+
alive_child_count: children.length,
|
|
596
|
+
ask_guidance: {
|
|
597
|
+
envelope: "decision=ask; include the desired total grant and the reason",
|
|
598
|
+
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
419
602
|
const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
|
|
420
603
|
const MAX_PLACEMENT_OUTPOSTS = 16;
|
|
421
604
|
function outpostIdentityUnknownError(message, expectedId, details = {}) {
|
|
@@ -1229,6 +1412,13 @@ export function composeWorkerBrief(worker, taskBrief) {
|
|
|
1229
1412
|
]
|
|
1230
1413
|
.filter(Boolean)
|
|
1231
1414
|
.join("\n");
|
|
1415
|
+
const laneHiringGuidance = worker.profile?.id === "hq-lanes-manager"
|
|
1416
|
+
? [
|
|
1417
|
+
"",
|
|
1418
|
+
"## Lane hiring grants",
|
|
1419
|
+
"A child lane may hire only within its assigned grant. If you need more direct child lanes than your current grant allows, stop before creating them and write a decision=ask envelope. Include the total grant you want and why in the question and context. Use your lane id from HQ_LANE_ID so your senior can run `hq lanes grant <lane-id> --hire <n>`.",
|
|
1420
|
+
]
|
|
1421
|
+
: [];
|
|
1232
1422
|
return [
|
|
1233
1423
|
`# ${name}`,
|
|
1234
1424
|
description,
|
|
@@ -1257,6 +1447,7 @@ export function composeWorkerBrief(worker, taskBrief) {
|
|
|
1257
1447
|
"## Verification checkpoints",
|
|
1258
1448
|
checkpoints || "(none)",
|
|
1259
1449
|
...(checkpoints ? ["", checkpointInstruction(worker)] : []),
|
|
1450
|
+
...laneHiringGuidance,
|
|
1260
1451
|
"",
|
|
1261
1452
|
"## Task",
|
|
1262
1453
|
taskBrief.replace(/\s+$/, ""),
|
|
@@ -1602,6 +1793,9 @@ function mintMessageId() {
|
|
|
1602
1793
|
}
|
|
1603
1794
|
async function runCreate(ctx, opts) {
|
|
1604
1795
|
const hqRoot = ctx.hqRoot();
|
|
1796
|
+
const requestedHireGrant = opts.hireGrant === undefined
|
|
1797
|
+
? undefined
|
|
1798
|
+
: parseHireGrant(opts.hireGrant);
|
|
1605
1799
|
const requestedPlacement = opts.placement !== undefined
|
|
1606
1800
|
? parsePlacementPolicy(opts.placement)
|
|
1607
1801
|
: undefined;
|
|
@@ -1664,6 +1858,17 @@ async function runCreate(ctx, opts) {
|
|
|
1664
1858
|
}
|
|
1665
1859
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
|
|
1666
1860
|
assertSeniorExists(hqRoot, senior, ctx);
|
|
1861
|
+
const hireGrantsEnabled = ctx.laneHireGrantsEnabled();
|
|
1862
|
+
if (requestedHireGrant !== undefined &&
|
|
1863
|
+
!callerMatchesSenior(ctx, senior)) {
|
|
1864
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(senior)} may set a lane's hire grant`, { senior, hire_grant: requestedHireGrant });
|
|
1865
|
+
}
|
|
1866
|
+
if (hireGrantsEnabled &&
|
|
1867
|
+
ctx.env.HQ_LANE_ID?.trim() &&
|
|
1868
|
+
!callerMatchesSenior(ctx, senior)) {
|
|
1869
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior lane ${senior.id} may create a direct child lane`, { senior_lane_id: senior.id });
|
|
1870
|
+
}
|
|
1871
|
+
const hireGrant = requestedHireGrant ?? 0;
|
|
1667
1872
|
const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
|
|
1668
1873
|
if (laneDepth > MAX_LANE_DEPTH) {
|
|
1669
1874
|
throw new LanesError("max_lane_depth", `Lane depth ${laneDepth} exceeds cap ${MAX_LANE_DEPTH}`, { depth: laneDepth, max: MAX_LANE_DEPTH });
|
|
@@ -2065,7 +2270,7 @@ async function runCreate(ctx, opts) {
|
|
|
2065
2270
|
: { kind: "local", id: ctx.hostname() });
|
|
2066
2271
|
const queuedReason = admission.action === "queue" ? admission.reason : undefined;
|
|
2067
2272
|
const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
|
|
2068
|
-
const
|
|
2273
|
+
const laneInit = {
|
|
2069
2274
|
company,
|
|
2070
2275
|
...(company === null && meshCompany
|
|
2071
2276
|
? { mesh_company: meshCompany }
|
|
@@ -2081,6 +2286,7 @@ async function runCreate(ctx, opts) {
|
|
|
2081
2286
|
})),
|
|
2082
2287
|
depth: laneDepth,
|
|
2083
2288
|
senior,
|
|
2289
|
+
hire_grant: hireGrant,
|
|
2084
2290
|
senior_history: [],
|
|
2085
2291
|
state: "queued",
|
|
2086
2292
|
queued_reason: queuedReason,
|
|
@@ -2124,7 +2330,29 @@ async function runCreate(ctx, opts) {
|
|
|
2124
2330
|
? { origin_host_source: originHostSource }
|
|
2125
2331
|
: {}),
|
|
2126
2332
|
attribution_history: [],
|
|
2127
|
-
}
|
|
2333
|
+
};
|
|
2334
|
+
const lane = senior.kind === "lane"
|
|
2335
|
+
? withHireGrantAdmission(hqRoot, senior.id, ({ parent, children, parentGrant, availableChildGrant }) => {
|
|
2336
|
+
if (hireGrantsEnabled && isTerminalLaneState(parent.state)) {
|
|
2337
|
+
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot hire lanes`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
2338
|
+
}
|
|
2339
|
+
if (hireGrantsEnabled && children.length >= parentGrant) {
|
|
2340
|
+
throw hireGrantCapacityError(parent, children);
|
|
2341
|
+
}
|
|
2342
|
+
if (hireGrantsEnabled && hireGrant > availableChildGrant) {
|
|
2343
|
+
throw new LanesError("hire_grant_carve_exceeded", `Lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining after its other alive children. Ask its senior to raise its grant before assigning ${hireGrant}.`, {
|
|
2344
|
+
senior_lane_id: parent.lane_id,
|
|
2345
|
+
parent_grant: parentGrant,
|
|
2346
|
+
alive_children: children.map((child) => child.lane_id),
|
|
2347
|
+
assigned_child_grants: assignedChildHireGrants(children),
|
|
2348
|
+
requested_hire_grant: hireGrant,
|
|
2349
|
+
available_hire_grant: availableChildGrant,
|
|
2350
|
+
ask_guidance: `Write an ask envelope with the desired total grant and reason; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`,
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
return createLane(hqRoot, laneInit);
|
|
2354
|
+
})
|
|
2355
|
+
: createLane(hqRoot, laneInit);
|
|
2128
2356
|
return { lane, admission, validation };
|
|
2129
2357
|
};
|
|
2130
2358
|
let persisted;
|
|
@@ -2478,6 +2706,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
2478
2706
|
story: options.story,
|
|
2479
2707
|
account: options.account,
|
|
2480
2708
|
model: options.model,
|
|
2709
|
+
hire: options.hire,
|
|
2481
2710
|
compact: options.compact,
|
|
2482
2711
|
}),
|
|
2483
2712
|
});
|
|
@@ -2514,8 +2743,10 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
2514
2743
|
remote.lane_id === lane.lane_id &&
|
|
2515
2744
|
(action === "message"
|
|
2516
2745
|
? remote.accepted === true
|
|
2517
|
-
:
|
|
2518
|
-
remote.
|
|
2746
|
+
: action === "grant"
|
|
2747
|
+
? remote.hire_grant === Number(options.hire)
|
|
2748
|
+
: remote.delivery_status === "worker_spawned" ||
|
|
2749
|
+
remote.delivery_status === "queued_on_target_account");
|
|
2519
2750
|
if (!resultConfirmed) {
|
|
2520
2751
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
2521
2752
|
lane_id: lane.lane_id,
|
|
@@ -2523,6 +2754,18 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
2523
2754
|
command_id: dispatched.commandId,
|
|
2524
2755
|
});
|
|
2525
2756
|
}
|
|
2757
|
+
if (action === "grant") {
|
|
2758
|
+
const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
|
|
2759
|
+
printJson({
|
|
2760
|
+
...remote,
|
|
2761
|
+
lane_id: ownerLane.lane_id,
|
|
2762
|
+
lane: ownerLane,
|
|
2763
|
+
outpost_id: outpostId,
|
|
2764
|
+
outpost_command_id: dispatched.commandId,
|
|
2765
|
+
outpost_transport: "outposts_exec",
|
|
2766
|
+
});
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2526
2769
|
printJson({
|
|
2527
2770
|
...remote,
|
|
2528
2771
|
outpost_id: outpostId,
|
|
@@ -2763,7 +3006,8 @@ async function runList(ctx, opts) {
|
|
|
2763
3006
|
}
|
|
2764
3007
|
const now = ctx.now();
|
|
2765
3008
|
const host = reconcileHostFromCtx(ctx);
|
|
2766
|
-
const
|
|
3009
|
+
const allLanes = loadAllLanes(hqRoot);
|
|
3010
|
+
const selectedLanes = allLanes
|
|
2767
3011
|
.filter((lane) => {
|
|
2768
3012
|
if (state && lane.state !== state)
|
|
2769
3013
|
return false;
|
|
@@ -2779,13 +3023,18 @@ async function runList(ctx, opts) {
|
|
|
2779
3023
|
return false;
|
|
2780
3024
|
return true;
|
|
2781
3025
|
})
|
|
2782
|
-
.sort(compareLaneQueueOrder)
|
|
2783
|
-
|
|
3026
|
+
.sort(compareLaneQueueOrder);
|
|
3027
|
+
const hireByLane = new Map(await mapWithConcurrency(selectedLanes, OUTPOST_HIRE_SUMMARY_CONCURRENCY, async (lane) => [
|
|
3028
|
+
lane.lane_id,
|
|
3029
|
+
await laneHirePresentation(ctx, lane, allLanes),
|
|
3030
|
+
]));
|
|
3031
|
+
const lanes = selectedLanes.map((lane) => {
|
|
2784
3032
|
const remoteQueued = lane.state === "queued" &&
|
|
2785
3033
|
lane.host.kind === "outpost" &&
|
|
2786
3034
|
!laneRunsHere(lane, host);
|
|
2787
3035
|
return {
|
|
2788
3036
|
...lane,
|
|
3037
|
+
...hireByLane.get(lane.lane_id),
|
|
2789
3038
|
...(remoteQueued
|
|
2790
3039
|
? { state: "remote", remote_state: lane.state, queued_reason: undefined }
|
|
2791
3040
|
: {}),
|
|
@@ -3220,6 +3469,8 @@ async function runMoveTarget(ctx, laneId, opts) {
|
|
|
3220
3469
|
async function runShow(ctx, laneId) {
|
|
3221
3470
|
const hqRoot = ctx.hqRoot();
|
|
3222
3471
|
const storedLane = readLane(hqRoot, laneId);
|
|
3472
|
+
const allLanes = loadAllLanes(hqRoot);
|
|
3473
|
+
const hire = await laneHirePresentation(ctx, storedLane, allLanes);
|
|
3223
3474
|
const remoteQueued = storedLane.state === "queued" &&
|
|
3224
3475
|
storedLane.host.kind === "outpost" &&
|
|
3225
3476
|
!laneRunsHere(storedLane, reconcileHostFromCtx(ctx));
|
|
@@ -3231,6 +3482,7 @@ async function runShow(ctx, laneId) {
|
|
|
3231
3482
|
ok: true,
|
|
3232
3483
|
action: "show",
|
|
3233
3484
|
lane,
|
|
3485
|
+
...hire,
|
|
3234
3486
|
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, ctx.now()),
|
|
3235
3487
|
stalled: Boolean(lane.stalled),
|
|
3236
3488
|
stalled_at: lane.stalled_at ?? null,
|
|
@@ -3256,17 +3508,17 @@ async function runShow(ctx, laneId) {
|
|
|
3256
3508
|
: null,
|
|
3257
3509
|
});
|
|
3258
3510
|
}
|
|
3259
|
-
function
|
|
3511
|
+
function callerMatchesSenior(ctx, seniorRef) {
|
|
3260
3512
|
const sessionId = sessionIdFromEnv(ctx.env);
|
|
3261
3513
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
|
|
3262
|
-
if (
|
|
3263
|
-
return callerLaneId !== undefined && callerLaneId ===
|
|
3514
|
+
if (seniorRef.kind === "lane") {
|
|
3515
|
+
return callerLaneId !== undefined && callerLaneId === seniorRef.id;
|
|
3264
3516
|
}
|
|
3265
3517
|
if (!sessionId || callerLaneId !== undefined)
|
|
3266
3518
|
return false;
|
|
3267
|
-
if (
|
|
3268
|
-
return sessionId ===
|
|
3269
|
-
if (
|
|
3519
|
+
if (seniorRef.kind === "session")
|
|
3520
|
+
return sessionId === seniorRef.id;
|
|
3521
|
+
if (seniorRef.kind !== "user")
|
|
3270
3522
|
return false;
|
|
3271
3523
|
const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
|
|
3272
3524
|
if (loaded.status !== "ok")
|
|
@@ -3276,6 +3528,173 @@ function forceAdmissionAuthorized(ctx, lane) {
|
|
|
3276
3528
|
(senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
|
|
3277
3529
|
senior.kind === "user");
|
|
3278
3530
|
}
|
|
3531
|
+
function forceAdmissionAuthorized(ctx, lane) {
|
|
3532
|
+
return callerMatchesSenior(ctx, lane.senior);
|
|
3533
|
+
}
|
|
3534
|
+
async function runGrant(ctx, laneId, opts) {
|
|
3535
|
+
if (opts.hire === undefined) {
|
|
3536
|
+
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
3537
|
+
}
|
|
3538
|
+
const hireGrant = parseHireGrant(opts.hire);
|
|
3539
|
+
const hqRoot = ctx.hqRoot();
|
|
3540
|
+
const initial = readLane(hqRoot, laneId);
|
|
3541
|
+
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
3542
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
|
|
3543
|
+
}
|
|
3544
|
+
if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
|
|
3545
|
+
await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) });
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
const lockedLaneIds = [
|
|
3549
|
+
laneId,
|
|
3550
|
+
...(initial.senior.kind === "lane" && initial.senior.id
|
|
3551
|
+
? [initial.senior.id]
|
|
3552
|
+
: []),
|
|
3553
|
+
];
|
|
3554
|
+
const updated = withLaneHireLocks(hqRoot, lockedLaneIds, () => {
|
|
3555
|
+
const current = readLane(hqRoot, laneId);
|
|
3556
|
+
if (!callerMatchesSenior(ctx, current.senior)) {
|
|
3557
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: current.senior });
|
|
3558
|
+
}
|
|
3559
|
+
const hierarchy = loadAllLanes(hqRoot);
|
|
3560
|
+
const enforce = ctx.laneHireGrantsEnabled();
|
|
3561
|
+
if (enforce && current.senior.kind === "lane") {
|
|
3562
|
+
if (!current.senior.id) {
|
|
3563
|
+
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
3564
|
+
lane_id: laneId,
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
const parent = readLane(hqRoot, current.senior.id);
|
|
3568
|
+
if (isTerminalLaneState(parent.state)) {
|
|
3569
|
+
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot change grants`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
3570
|
+
}
|
|
3571
|
+
const siblings = aliveDirectChildren(hierarchy, parent.lane_id);
|
|
3572
|
+
const parentGrant = parent.hire_grant ?? 0;
|
|
3573
|
+
const assigned = assignedChildHireGrants(siblings, current.lane_id);
|
|
3574
|
+
const available = Math.max(0, parentGrant - assigned);
|
|
3575
|
+
if (hireGrant > available) {
|
|
3576
|
+
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
|
|
3577
|
+
senior_lane_id: parent.lane_id,
|
|
3578
|
+
parent_grant: parentGrant,
|
|
3579
|
+
alive_children: siblings.map((child) => child.lane_id),
|
|
3580
|
+
assigned_other_child_grants: assigned,
|
|
3581
|
+
requested_hire_grant: hireGrant,
|
|
3582
|
+
available_hire_grant: available,
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
if (enforce) {
|
|
3587
|
+
const children = aliveDirectChildren(hierarchy, current.lane_id);
|
|
3588
|
+
const allocated = assignedChildHireGrants(children);
|
|
3589
|
+
const minimumGrant = Math.max(allocated, children.length);
|
|
3590
|
+
if (hireGrant < minimumGrant) {
|
|
3591
|
+
throw new LanesError(allocated > children.length ? "hire_grant_below_allocated" : "hire_grant_below_occupancy", `Lane ${laneId} cannot lower its grant to ${hireGrant}; it has ${children.length} alive children and ${allocated} grant allocated to them`, {
|
|
3592
|
+
lane_id: laneId,
|
|
3593
|
+
requested_hire_grant: hireGrant,
|
|
3594
|
+
alive_children: children.map((child) => child.lane_id),
|
|
3595
|
+
alive_child_count: children.length,
|
|
3596
|
+
assigned_child_grants: allocated,
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
return retryUpdateLane(hqRoot, laneId, "command", (record) => ({
|
|
3601
|
+
...record,
|
|
3602
|
+
hire_grant: hireGrant,
|
|
3603
|
+
}));
|
|
3604
|
+
});
|
|
3605
|
+
const fields = laneHireFields(updated, loadAllLanes(hqRoot));
|
|
3606
|
+
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
3607
|
+
}
|
|
3608
|
+
async function runBackfillGrants(ctx, opts) {
|
|
3609
|
+
if (opts.apply && opts.dryRun) {
|
|
3610
|
+
throw new LanesError("hire_grant_backfill_mode_conflict", "Pass either --dry-run or --apply, not both", {});
|
|
3611
|
+
}
|
|
3612
|
+
const apply = Boolean(opts.apply);
|
|
3613
|
+
const hqRoot = ctx.hqRoot();
|
|
3614
|
+
if (apply && ctx.laneHireGrantsEnabled()) {
|
|
3615
|
+
throw new LanesError("hire_grant_backfill_gate_enabled", "Backfill must be applied and verified before lanes.hire-grants is enabled", { flag: LANE_HIRE_GRANTS_FLAG });
|
|
3616
|
+
}
|
|
3617
|
+
if (apply && ctx.env.HQ_LANE_ID?.trim()) {
|
|
3618
|
+
throw new LanesError("hire_grant_backfill_unauthorized", "A lane cannot apply the hire-grant backfill; run it from an operator session", {});
|
|
3619
|
+
}
|
|
3620
|
+
const allLanes = loadAllLanes(hqRoot);
|
|
3621
|
+
const profileIds = new Set(allLanes
|
|
3622
|
+
.filter((lane) => !isTerminalLaneState(lane.state) &&
|
|
3623
|
+
(aliveDirectChildren(allLanes, lane.lane_id).length > 0 ||
|
|
3624
|
+
lane.worker_profile === "hq-lanes-manager"))
|
|
3625
|
+
.map((lane) => lane.worker_profile)
|
|
3626
|
+
.filter((id) => Boolean(id)));
|
|
3627
|
+
const profileMaxConcurrent = new Map();
|
|
3628
|
+
for (const profileId of profileIds) {
|
|
3629
|
+
let profile;
|
|
3630
|
+
try {
|
|
3631
|
+
profile = loadWorkerProfile(hqRoot, profileId);
|
|
3632
|
+
}
|
|
3633
|
+
catch (err) {
|
|
3634
|
+
throw new LanesError("hire_grant_backfill_profile_unavailable", `Cannot calculate a hire-grant backfill for worker profile ${profileId}: ${err instanceof Error ? err.message : String(err)}`, { worker_profile: profileId });
|
|
3635
|
+
}
|
|
3636
|
+
const maxConcurrent = profile.meta.max_concurrent;
|
|
3637
|
+
if (maxConcurrent !== undefined && !Number.isSafeInteger(maxConcurrent)) {
|
|
3638
|
+
throw new LanesError("hire_grant_backfill_profile_invalid", `Worker profile ${profileId} has max_concurrent outside the safe integer range`, { worker_profile: profileId, max_concurrent: maxConcurrent });
|
|
3639
|
+
}
|
|
3640
|
+
if (maxConcurrent !== undefined)
|
|
3641
|
+
profileMaxConcurrent.set(profileId, maxConcurrent);
|
|
3642
|
+
}
|
|
3643
|
+
let rows = planHireGrantBackfill(allLanes, profileMaxConcurrent);
|
|
3644
|
+
if (apply) {
|
|
3645
|
+
const shortfalls = rows.filter((row) => row.grant_shortfall > 0);
|
|
3646
|
+
if (shortfalls.length > 0) {
|
|
3647
|
+
throw new LanesError("hire_grant_backfill_allocation_shortfall", "Existing parent grants do not cover the grants allocated to alive children; resolve these rows before applying the backfill", { lanes: shortfalls });
|
|
3648
|
+
}
|
|
3649
|
+
rows = rows.map((row) => {
|
|
3650
|
+
if (row.action === "unchanged")
|
|
3651
|
+
return row;
|
|
3652
|
+
const lane = readLane(hqRoot, row.lane_id);
|
|
3653
|
+
const lockIds = [
|
|
3654
|
+
lane.lane_id,
|
|
3655
|
+
...(lane.senior.kind === "lane" && lane.senior.id ? [lane.senior.id] : []),
|
|
3656
|
+
];
|
|
3657
|
+
return withLaneHireLocks(hqRoot, lockIds, () => {
|
|
3658
|
+
const current = readLane(hqRoot, row.lane_id);
|
|
3659
|
+
if (current.hire_grant !== undefined) {
|
|
3660
|
+
return {
|
|
3661
|
+
...row,
|
|
3662
|
+
existing_grant: current.hire_grant,
|
|
3663
|
+
proposed_grant: current.hire_grant,
|
|
3664
|
+
action: "unchanged",
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
const latestLanes = loadAllLanes(hqRoot);
|
|
3668
|
+
const latestRow = planHireGrantBackfill(latestLanes, profileMaxConcurrent)
|
|
3669
|
+
.find((candidate) => candidate.lane_id === current.lane_id);
|
|
3670
|
+
if (!latestRow) {
|
|
3671
|
+
throw new LanesError("hire_grant_backfill_target_changed", `Lane ${current.lane_id} no longer qualifies for the hire-grant backfill`, { lane_id: current.lane_id });
|
|
3672
|
+
}
|
|
3673
|
+
if (latestRow.grant_shortfall > 0) {
|
|
3674
|
+
throw new LanesError("hire_grant_backfill_allocation_shortfall", `Lane ${current.lane_id}'s existing grant does not cover grants allocated to its alive children`, { lane: latestRow });
|
|
3675
|
+
}
|
|
3676
|
+
const proposed = latestRow.proposed_grant;
|
|
3677
|
+
const written = retryUpdateLane(hqRoot, current.lane_id, "command", (record) => ({
|
|
3678
|
+
...record,
|
|
3679
|
+
...(record.hire_grant === undefined ? { hire_grant: proposed } : {}),
|
|
3680
|
+
}));
|
|
3681
|
+
const value = written.hire_grant ?? 0;
|
|
3682
|
+
return {
|
|
3683
|
+
...row,
|
|
3684
|
+
existing_grant: value,
|
|
3685
|
+
proposed_grant: value,
|
|
3686
|
+
action: value === proposed ? "set" : "unchanged",
|
|
3687
|
+
};
|
|
3688
|
+
});
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
printJson({
|
|
3692
|
+
ok: true,
|
|
3693
|
+
action: "backfill-grants",
|
|
3694
|
+
mode: apply ? "apply" : "dry-run",
|
|
3695
|
+
lanes: rows,
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3279
3698
|
async function runAdmit(ctx, laneId, opts) {
|
|
3280
3699
|
const hqRoot = ctx.hqRoot();
|
|
3281
3700
|
let lane = readLane(hqRoot, laneId);
|
|
@@ -3590,7 +4009,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
3590
4009
|
if (admission.action === "refuse" && admission.error)
|
|
3591
4010
|
throw admission.error;
|
|
3592
4011
|
const queued = admission.action === "queue";
|
|
3593
|
-
const moved = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4012
|
+
const moved = withResumeHireAdmission(ctx, lane, () => casUpdateLane(hqRoot, laneId, (current) => {
|
|
3594
4013
|
// The copy and every check above were made from `lane`; a record
|
|
3595
4014
|
// that moved on since (another move, a spawn, a cancel) invalidates
|
|
3596
4015
|
// them, so the write is refused rather than applied to a state it
|
|
@@ -3624,7 +4043,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
3624
4043
|
next.queued_at = undefined;
|
|
3625
4044
|
}
|
|
3626
4045
|
return next;
|
|
3627
|
-
});
|
|
4046
|
+
}));
|
|
3628
4047
|
return { lane: moved, from, to: target, copy, queued };
|
|
3629
4048
|
}, ctx.sleep);
|
|
3630
4049
|
}
|
|
@@ -3640,6 +4059,9 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
3640
4059
|
return;
|
|
3641
4060
|
}
|
|
3642
4061
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
4062
|
+
// Check before transcript recovery, account probing, or message writes.
|
|
4063
|
+
// The final state transition repeats this check under the senior lock.
|
|
4064
|
+
withResumeHireAdmission(ctx, lane, () => undefined);
|
|
3643
4065
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3644
4066
|
// An account move is refused on its own terms first: a running lane and a
|
|
3645
4067
|
// provider without session resume get the move's named errors, not the
|
|
@@ -3725,62 +4147,64 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
3725
4147
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
3726
4148
|
if (admission.action === "refuse" && admission.error)
|
|
3727
4149
|
throw admission.error;
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
provider: lane.provider,
|
|
3734
|
-
});
|
|
3735
|
-
}
|
|
3736
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
3737
|
-
queuedOnAdmission = true;
|
|
3738
|
-
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
3739
|
-
if (current.version !== lane.version || current.state !== lane.state) {
|
|
3740
|
-
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
4150
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
4151
|
+
if (admission.action === "queue") {
|
|
4152
|
+
const sessionId = lane.provider_session_id;
|
|
4153
|
+
if (!sessionId) {
|
|
4154
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
|
|
3741
4155
|
lane_id: laneId,
|
|
3742
|
-
|
|
3743
|
-
version: current.version,
|
|
3744
|
-
state: current.state,
|
|
4156
|
+
provider: lane.provider,
|
|
3745
4157
|
});
|
|
3746
4158
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
4159
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
4160
|
+
queuedOnAdmission = true;
|
|
4161
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
4162
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
4163
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
4164
|
+
lane_id: laneId,
|
|
4165
|
+
prepared_version: lane.version,
|
|
4166
|
+
version: current.version,
|
|
4167
|
+
state: current.state,
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return {
|
|
4171
|
+
...current,
|
|
4172
|
+
state: "queued",
|
|
4173
|
+
queued_reason: admission.reason ?? "throttle",
|
|
4174
|
+
queued_at: at,
|
|
4175
|
+
pending_resume: {
|
|
4176
|
+
session_id: sessionId,
|
|
4177
|
+
trigger: "message",
|
|
4178
|
+
text,
|
|
4179
|
+
message_id: messageId,
|
|
4180
|
+
compact: opts.compact !== false,
|
|
4181
|
+
at,
|
|
4182
|
+
},
|
|
4183
|
+
};
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
3768
4187
|
env: ctx.env,
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4188
|
+
modelDeps: {
|
|
4189
|
+
readFileSync: ctx.readFileSync,
|
|
4190
|
+
probe: ctx.probeProvider,
|
|
4191
|
+
env: ctx.env,
|
|
4192
|
+
},
|
|
4193
|
+
existsSync: ctx.existsSync,
|
|
4194
|
+
spawnProcess: ctx.spawnProcess,
|
|
4195
|
+
proveDetached: ctx.proveDetached,
|
|
4196
|
+
killProcess: ctx.killProcess,
|
|
4197
|
+
now: ctx.now,
|
|
4198
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
4199
|
+
runCompaction: ctx.runCompaction,
|
|
4200
|
+
locateTranscript: ctx.locateTranscript,
|
|
4201
|
+
}, {
|
|
4202
|
+
trigger: { kind: "message", text, message_id: messageId },
|
|
4203
|
+
compact: opts.compact !== false,
|
|
4204
|
+
});
|
|
4205
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
4206
|
+
return started;
|
|
3781
4207
|
});
|
|
3782
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
3783
|
-
return started;
|
|
3784
4208
|
}, ctx.sleep);
|
|
3785
4209
|
}
|
|
3786
4210
|
// Every accepted message moves the compaction reference forward.
|
|
@@ -3915,6 +4339,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
3915
4339
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
3916
4340
|
return;
|
|
3917
4341
|
}
|
|
4342
|
+
// Fail before provider/session work if a terminal child would consume a
|
|
4343
|
+
// slot that its senior has already assigned elsewhere.
|
|
4344
|
+
withResumeHireAdmission(ctx, lane, () => undefined);
|
|
3918
4345
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
|
|
3919
4346
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3920
4347
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
@@ -4001,64 +4428,66 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4001
4428
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
4002
4429
|
if (admission.action === "refuse" && admission.error)
|
|
4003
4430
|
throw admission.error;
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
provider: lane.provider,
|
|
4010
|
-
});
|
|
4011
|
-
}
|
|
4012
|
-
queuedOnAdmission = true;
|
|
4013
|
-
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
4014
|
-
if (current.version !== lane.version || current.state !== lane.state) {
|
|
4015
|
-
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
|
|
4431
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
4432
|
+
if (admission.action === "queue") {
|
|
4433
|
+
const sessionId = lane.provider_session_id;
|
|
4434
|
+
if (!sessionId) {
|
|
4435
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
|
|
4016
4436
|
lane_id: laneId,
|
|
4017
|
-
|
|
4018
|
-
version: current.version,
|
|
4019
|
-
state: current.state,
|
|
4437
|
+
provider: lane.provider,
|
|
4020
4438
|
});
|
|
4021
4439
|
}
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
state
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4440
|
+
queuedOnAdmission = true;
|
|
4441
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
4442
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
4443
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
|
|
4444
|
+
lane_id: laneId,
|
|
4445
|
+
prepared_version: lane.version,
|
|
4446
|
+
version: current.version,
|
|
4447
|
+
state: current.state,
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
return {
|
|
4451
|
+
...current,
|
|
4452
|
+
state: "queued",
|
|
4453
|
+
queued_reason: admission.reason ?? "throttle",
|
|
4454
|
+
queued_at: at,
|
|
4455
|
+
pending_resume: {
|
|
4456
|
+
session_id: sessionId,
|
|
4457
|
+
trigger: "resume",
|
|
4458
|
+
resumed_from: resumedFrom,
|
|
4459
|
+
...(text !== undefined ? { text } : {}),
|
|
4460
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
4461
|
+
compact: opts.compact !== false,
|
|
4462
|
+
at,
|
|
4463
|
+
},
|
|
4464
|
+
};
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
return spawnLaneWorker(hqRoot, lane, {
|
|
4044
4468
|
env: ctx.env,
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4469
|
+
modelDeps: {
|
|
4470
|
+
readFileSync: ctx.readFileSync,
|
|
4471
|
+
probe: ctx.probeProvider,
|
|
4472
|
+
env: ctx.env,
|
|
4473
|
+
},
|
|
4474
|
+
existsSync: ctx.existsSync,
|
|
4475
|
+
spawnProcess: ctx.spawnProcess,
|
|
4476
|
+
proveDetached: ctx.proveDetached,
|
|
4477
|
+
killProcess: ctx.killProcess,
|
|
4478
|
+
now: ctx.now,
|
|
4479
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
4480
|
+
runCompaction: ctx.runCompaction,
|
|
4481
|
+
locateTranscript: ctx.locateTranscript,
|
|
4482
|
+
}, {
|
|
4483
|
+
trigger: {
|
|
4484
|
+
kind: "resume",
|
|
4485
|
+
from: resumedFrom,
|
|
4486
|
+
...(text !== undefined ? { text } : {}),
|
|
4487
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
4488
|
+
},
|
|
4489
|
+
compact: opts.compact !== false,
|
|
4490
|
+
});
|
|
4062
4491
|
});
|
|
4063
4492
|
}, ctx.sleep);
|
|
4064
4493
|
}
|
|
@@ -4477,6 +4906,21 @@ function archiveResumeMessage(hqRoot, laneId, message) {
|
|
|
4477
4906
|
fs.writeFileSync(path.join(dir, `${message.message_id}.resume.jsonl`), `${JSON.stringify(message)}\n`, "utf8");
|
|
4478
4907
|
}
|
|
4479
4908
|
async function runReparent(ctx, laneId, opts) {
|
|
4909
|
+
const hqRoot = ctx.hqRoot();
|
|
4910
|
+
if (!opts.senior) {
|
|
4911
|
+
throw new LanesError("missing_senior", "--senior is required", {});
|
|
4912
|
+
}
|
|
4913
|
+
const nextSenior = parseSeniorRef(opts.senior);
|
|
4914
|
+
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
4915
|
+
const initial = readLane(hqRoot, laneId);
|
|
4916
|
+
const lockIds = [
|
|
4917
|
+
laneId,
|
|
4918
|
+
...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
|
|
4919
|
+
...(nextSenior.kind === "lane" && nextSenior.id ? [nextSenior.id] : []),
|
|
4920
|
+
];
|
|
4921
|
+
withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
|
|
4922
|
+
}
|
|
4923
|
+
function runReparentLocked(ctx, laneId, opts) {
|
|
4480
4924
|
const hqRoot = ctx.hqRoot();
|
|
4481
4925
|
if (!opts.senior) {
|
|
4482
4926
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
@@ -4490,6 +4934,29 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4490
4934
|
}
|
|
4491
4935
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
4492
4936
|
assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
|
|
4937
|
+
if (ctx.laneHireGrantsEnabled() &&
|
|
4938
|
+
nextSenior.kind === "lane" &&
|
|
4939
|
+
seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
4940
|
+
const parent = readLane(hqRoot, nextSenior.id);
|
|
4941
|
+
if (isTerminalLaneState(parent.state)) {
|
|
4942
|
+
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot accept lane children`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
4943
|
+
}
|
|
4944
|
+
const children = aliveDirectChildren(loadAllLanes(hqRoot), parent.lane_id);
|
|
4945
|
+
if (children.length >= (parent.hire_grant ?? 0)) {
|
|
4946
|
+
throw hireGrantCapacityError(parent, children);
|
|
4947
|
+
}
|
|
4948
|
+
const available = Math.max(0, (parent.hire_grant ?? 0) - assignedChildHireGrants(children));
|
|
4949
|
+
const requested = lane.hire_grant ?? 0;
|
|
4950
|
+
if (requested > available) {
|
|
4951
|
+
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
|
|
4952
|
+
senior_lane_id: parent.lane_id,
|
|
4953
|
+
parent_grant: parent.hire_grant ?? 0,
|
|
4954
|
+
assigned_child_grants: assignedChildHireGrants(children),
|
|
4955
|
+
requested_hire_grant: requested,
|
|
4956
|
+
available_hire_grant: available,
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4493
4960
|
if (walkHitsLane(hqRoot, nextSenior, laneId, ctx)) {
|
|
4494
4961
|
throw new LanesError("senior_cycle", `Re-parent would create a cycle through ${laneId}`, { lane_id: laneId, senior: nextSenior });
|
|
4495
4962
|
}
|
|
@@ -5515,6 +5982,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
5515
5982
|
.option("--model <id>", "Model id for the provider CLI")
|
|
5516
5983
|
.option("--effort <level>", "Effort level; claude: low|medium|high|xhigh|max, codex: minimal|low|medium|high|xhigh|max|ultra, grok: low|medium|high|xhigh")
|
|
5517
5984
|
.option("--senior <ref>", "session:<id> | lane:<id> | user")
|
|
5985
|
+
.option("--hire-grant <n>", "Maximum alive direct child lanes assigned to this lane; set by its senior")
|
|
5518
5986
|
.option("--timeout <secs>", "Deadline in seconds (warns, does not kill)")
|
|
5519
5987
|
.option("--priority <n>", "Queue priority; higher runs first", "1")
|
|
5520
5988
|
.option("--outpost <id>", "Run on this Outpost instead of the local host")
|
|
@@ -5523,6 +5991,22 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
5523
5991
|
.action(wrap(async (opts) => {
|
|
5524
5992
|
await runCreate(ctx, opts);
|
|
5525
5993
|
}));
|
|
5994
|
+
lanes
|
|
5995
|
+
.command("grant")
|
|
5996
|
+
.description("Set a lane's alive child-lane grant as its senior")
|
|
5997
|
+
.argument("<lane-id>", "Lane id")
|
|
5998
|
+
.requiredOption("--hire <n>", "Non-negative safe integer hire grant")
|
|
5999
|
+
.action(wrap(async (laneId, opts) => {
|
|
6000
|
+
await runGrant(ctx, laneId, opts);
|
|
6001
|
+
}));
|
|
6002
|
+
lanes
|
|
6003
|
+
.command("backfill-grants")
|
|
6004
|
+
.description("Preview or apply missing hire grants for alive manager lanes")
|
|
6005
|
+
.option("--dry-run", "Print proposed values without writing (default)")
|
|
6006
|
+
.option("--apply", "Write missing hire grants; only run before enforcement is enabled")
|
|
6007
|
+
.action(wrap(async (opts) => {
|
|
6008
|
+
await runBackfillGrants(ctx, opts);
|
|
6009
|
+
}));
|
|
5526
6010
|
lanes
|
|
5527
6011
|
.command("list")
|
|
5528
6012
|
.description("List lanes")
|