@indigoai-us/hq-cli 5.149.1 → 5.151.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 +10 -0
- package/dist/command-catalog.generated.d.ts +19 -0
- package/dist/command-catalog.generated.js +25 -0
- package/dist/commands/lanes.js +495 -246
- package/dist/commands/report.d.ts +15 -1
- package/dist/commands/report.js +41 -6
- package/dist/lib/lanes/admission.d.ts +24 -0
- package/dist/lib/lanes/admission.js +193 -0
- package/dist/lib/lanes/delivery.js +5 -3
- package/dist/lib/lanes/index.d.ts +2 -0
- package/dist/lib/lanes/index.js +1 -0
- package/dist/lib/lanes/reconcile.js +155 -43
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.js +48 -1
- package/dist/lib/lanes/types.d.ts +8 -1
- package/dist/lib/lanes/types.js +15 -4
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -18,7 +18,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
18
18
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
19
19
|
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
20
20
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
21
|
-
import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS,
|
|
21
|
+
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, isLoggedOut, 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";
|
|
22
22
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
23
23
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
24
24
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -840,36 +840,25 @@ function isOverdue(hqRoot, laneId, now) {
|
|
|
840
840
|
return false;
|
|
841
841
|
return now.getTime() > at;
|
|
842
842
|
}
|
|
843
|
-
function compareQueueOrder(a, b) {
|
|
844
|
-
if (b.priority !== a.priority)
|
|
845
|
-
return b.priority - a.priority;
|
|
846
|
-
return a.created_at.localeCompare(b.created_at);
|
|
847
|
-
}
|
|
848
843
|
function admitCreate(input) {
|
|
849
|
-
const { hqRoot, provider, pinnedAccount, profileId, profileMaxConcurrent, priority, createdAt, config, } = input;
|
|
844
|
+
const { hqRoot, provider, pinnedAccount, profileId, profileMaxConcurrent, host, priority, createdAt, nowMs, config, } = input;
|
|
850
845
|
const providerCfg = config.providers[provider];
|
|
851
846
|
const lanes = loadAllLanes(hqRoot).filter((lane) => lane.lane_id !== input.excludeLaneId);
|
|
852
|
-
const
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
847
|
+
const localLanes = lanes.filter((lane) => laneRunsHere(lane, host));
|
|
848
|
+
const runningProvider = lanes.filter((lane) => lane.provider === provider &&
|
|
849
|
+
(lane.state === "running" ||
|
|
850
|
+
hasActiveAdmissionReservation(lane, nowMs) ||
|
|
851
|
+
hasActivePendingResume(lane, nowMs)) &&
|
|
852
|
+
laneRunsHere(lane, host));
|
|
853
|
+
const queuedProvider = localLanes.filter((lane) => lane.provider === provider &&
|
|
854
|
+
lane.state === "queued" &&
|
|
855
|
+
!hasActiveAdmissionReservation(lane, nowMs));
|
|
860
856
|
const runningCount = pinnedAccount
|
|
861
857
|
? runningProvider.filter((lane) => lane.account === pinnedAccount).length
|
|
862
858
|
: runningProvider.length;
|
|
863
859
|
const providerRunning = runningProvider.length;
|
|
864
|
-
const queuedCount =
|
|
860
|
+
const queuedCount = queuedProvider.length;
|
|
865
861
|
const maxQueued = providerCfg.max_queued;
|
|
866
|
-
const profileRunning = profileId === undefined
|
|
867
|
-
? 0
|
|
868
|
-
: lanes.filter((lane) => lane.worker_profile === profileId && lane.state === "running").length;
|
|
869
|
-
const atCap = providerRunning >= providerCfg.max_running ||
|
|
870
|
-
(pinnedAccount !== undefined &&
|
|
871
|
-
runningCount >= accountMaxRunning(providerCfg, pinnedAccount)) ||
|
|
872
|
-
(profileMaxConcurrent !== undefined && profileRunning >= profileMaxConcurrent);
|
|
873
862
|
const synthetic = {
|
|
874
863
|
schema_version: "hq-lane.v1",
|
|
875
864
|
lane_id: "__incoming__",
|
|
@@ -883,19 +872,24 @@ function admitCreate(input) {
|
|
|
883
872
|
priority,
|
|
884
873
|
provider,
|
|
885
874
|
account: pinnedAccount,
|
|
886
|
-
host:
|
|
875
|
+
host: host.outpostId
|
|
876
|
+
? { kind: "outpost", id: host.outpostId }
|
|
877
|
+
: { kind: "local", id: host.hostname },
|
|
878
|
+
...(profileId !== undefined ? { worker_profile: profileId } : {}),
|
|
887
879
|
version: 0,
|
|
888
880
|
created_at: createdAt,
|
|
889
881
|
updated_at: createdAt,
|
|
890
882
|
attribution_history: [],
|
|
891
883
|
};
|
|
892
|
-
const
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
884
|
+
const capReason = laneAdmissionCapReason(localLanes, synthetic, config, host, profileMaxConcurrent, nowMs);
|
|
885
|
+
const earlierEligible = !capReason && hasEarlierEligibleQueuedLane(localLanes, synthetic, config, host, (lane) => lane.worker_profile === undefined
|
|
886
|
+
? undefined
|
|
887
|
+
: loadWorkerProfile(hqRoot, lane.worker_profile).meta.max_concurrent, nowMs);
|
|
888
|
+
const queuedReason = capReason ??
|
|
889
|
+
(earlierEligible ? "queue_position" : undefined);
|
|
890
|
+
if (!queuedReason)
|
|
897
891
|
return { action: "start" };
|
|
898
|
-
if (queuedCount >= maxQueued) {
|
|
892
|
+
if (!input.alreadyQueued && queuedCount >= maxQueued) {
|
|
899
893
|
return {
|
|
900
894
|
action: "refuse",
|
|
901
895
|
error: new LanesError("lane_queue_full", `Lane queue is full for ${pinnedAccount ? `account ${pinnedAccount}` : provider}`, {
|
|
@@ -906,27 +900,38 @@ function admitCreate(input) {
|
|
|
906
900
|
}),
|
|
907
901
|
};
|
|
908
902
|
}
|
|
909
|
-
|
|
910
|
-
return { action: "queue", reason: isHead ? "cap" : "queue_position" };
|
|
911
|
-
}
|
|
912
|
-
return { action: "queue", reason: "queue_position" };
|
|
903
|
+
return { action: "queue", reason: queuedReason };
|
|
913
904
|
}
|
|
914
|
-
async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccount) {
|
|
915
|
-
if (current.action !== "start"
|
|
905
|
+
async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccount, options = {}) {
|
|
906
|
+
if (current.action !== "start" &&
|
|
907
|
+
!(current.action === "queue" && current.reason === "queue_position"))
|
|
916
908
|
return current;
|
|
917
909
|
const { decision } = await decideAdmission(hqRoot, config.throttle, ctx.throttleDeps);
|
|
918
910
|
if (decision.admit)
|
|
919
911
|
return current;
|
|
912
|
+
if (current.action === "queue")
|
|
913
|
+
return { action: "queue", reason: "throttle" };
|
|
920
914
|
const providerCfg = config.providers[provider];
|
|
915
|
+
const host = options.host ?? reconcileHostFromCtx(ctx);
|
|
916
|
+
const nowMs = options.nowMs ?? ctx.now().getTime();
|
|
921
917
|
const queuedCount = loadAllLanes(hqRoot).filter((lane) => {
|
|
922
|
-
if (lane.
|
|
918
|
+
if (lane.lane_id === options.excludeLaneId ||
|
|
919
|
+
lane.provider !== provider ||
|
|
920
|
+
lane.state !== "queued" ||
|
|
921
|
+
hasActiveAdmissionReservation(lane, nowMs) ||
|
|
922
|
+
!laneRunsHere(lane, host))
|
|
923
923
|
return false;
|
|
924
924
|
if (pinnedAccount)
|
|
925
925
|
return lane.account === pinnedAccount;
|
|
926
926
|
return !lane.account;
|
|
927
927
|
}).length;
|
|
928
|
-
if (queuedCount >= providerCfg.max_queued) {
|
|
929
|
-
const running = loadAllLanes(hqRoot).filter((lane) => lane.
|
|
928
|
+
if (!options.alreadyQueued && queuedCount >= providerCfg.max_queued) {
|
|
929
|
+
const running = loadAllLanes(hqRoot).filter((lane) => lane.lane_id !== options.excludeLaneId &&
|
|
930
|
+
lane.provider === provider &&
|
|
931
|
+
(lane.state === "running" ||
|
|
932
|
+
hasActiveAdmissionReservation(lane, nowMs) ||
|
|
933
|
+
hasActivePendingResume(lane, nowMs)) &&
|
|
934
|
+
laneRunsHere(lane, host)).length;
|
|
930
935
|
return {
|
|
931
936
|
action: "refuse",
|
|
932
937
|
error: new LanesError("lane_queue_full", `Lane queue is full for ${pinnedAccount ? `account ${pinnedAccount}` : provider}`, {
|
|
@@ -939,6 +944,45 @@ async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccou
|
|
|
939
944
|
}
|
|
940
945
|
return { action: "queue", reason: "throttle" };
|
|
941
946
|
}
|
|
947
|
+
async function assessLaneAdmission(ctx, lane, options) {
|
|
948
|
+
const hqRoot = ctx.hqRoot();
|
|
949
|
+
const config = loadLanesConfig(hqRoot);
|
|
950
|
+
const account = options.account ?? lane.account;
|
|
951
|
+
const profile = lane.worker_profile
|
|
952
|
+
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
953
|
+
: undefined;
|
|
954
|
+
const nowMs = ctx.now().getTime();
|
|
955
|
+
let admission = admitCreate({
|
|
956
|
+
hqRoot,
|
|
957
|
+
provider: lane.provider,
|
|
958
|
+
pinnedAccount: account,
|
|
959
|
+
...(profile
|
|
960
|
+
? { profileId: profile.id, profileMaxConcurrent: profile.meta.max_concurrent }
|
|
961
|
+
: {}),
|
|
962
|
+
host: reconcileHostFromCtx(ctx),
|
|
963
|
+
priority: lane.priority,
|
|
964
|
+
createdAt: options.queueAt,
|
|
965
|
+
nowMs,
|
|
966
|
+
config,
|
|
967
|
+
excludeLaneId: lane.lane_id,
|
|
968
|
+
alreadyQueued: options.alreadyQueued,
|
|
969
|
+
});
|
|
970
|
+
if (admission.action === "start" ||
|
|
971
|
+
(admission.action === "queue" && admission.reason === "queue_position")) {
|
|
972
|
+
admission = await applyThrottle(hqRoot, config, ctx, admission, lane.provider, account, {
|
|
973
|
+
host: reconcileHostFromCtx(ctx),
|
|
974
|
+
excludeLaneId: lane.lane_id,
|
|
975
|
+
alreadyQueued: options.alreadyQueued,
|
|
976
|
+
nowMs,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return admission;
|
|
980
|
+
}
|
|
981
|
+
function assertLaneRunsHereForAdmission(ctx, lane) {
|
|
982
|
+
if (laneRunsHere(lane, reconcileHostFromCtx(ctx)))
|
|
983
|
+
return;
|
|
984
|
+
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 });
|
|
985
|
+
}
|
|
942
986
|
function assertKnownOutpost(requested, known) {
|
|
943
987
|
if (!known.includes(requested)) {
|
|
944
988
|
throw new LanesError("unknown_outpost", `Unknown Outpost ${JSON.stringify(requested)}. Accepted: ${known.join(", ") || "(none)"}`, { accepted: known, value: requested });
|
|
@@ -954,8 +998,6 @@ function copyHookRails(fromHome, toHome) {
|
|
|
954
998
|
function railsPresent(home) {
|
|
955
999
|
return fs.existsSync(path.join(home, "hooks"));
|
|
956
1000
|
}
|
|
957
|
-
const ADMISSION_LOCK_STALE_MS = 30_000;
|
|
958
|
-
const ADMISSION_LOCK_TIMEOUT_MS = 5_000;
|
|
959
1001
|
/**
|
|
960
1002
|
* Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
|
|
961
1003
|
* being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
|
|
@@ -969,83 +1011,6 @@ function yieldEventLoop() {
|
|
|
969
1011
|
setImmediate(resolve);
|
|
970
1012
|
});
|
|
971
1013
|
}
|
|
972
|
-
function admissionLockPath(hqRoot, provider, pinnedAccount) {
|
|
973
|
-
const name = pinnedAccount
|
|
974
|
-
? `${provider}-${pinnedAccount}`
|
|
975
|
-
: `${provider}-unpinned`;
|
|
976
|
-
return path.join(hqRoot, LANES_RUNS_REL, ".admission-locks", `${name}.lock`);
|
|
977
|
-
}
|
|
978
|
-
async function withAdmissionLock(lockPath, fn, sleep) {
|
|
979
|
-
ensureDir(path.dirname(lockPath));
|
|
980
|
-
const started = Date.now();
|
|
981
|
-
while (true) {
|
|
982
|
-
let fd;
|
|
983
|
-
try {
|
|
984
|
-
fd = fs.openSync(lockPath, "wx");
|
|
985
|
-
try {
|
|
986
|
-
fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n`, "utf8"));
|
|
987
|
-
}
|
|
988
|
-
catch (err) {
|
|
989
|
-
fs.closeSync(fd);
|
|
990
|
-
fd = undefined;
|
|
991
|
-
throw err;
|
|
992
|
-
}
|
|
993
|
-
try {
|
|
994
|
-
await yieldEventLoop();
|
|
995
|
-
return await fn();
|
|
996
|
-
}
|
|
997
|
-
finally {
|
|
998
|
-
try {
|
|
999
|
-
fs.closeSync(fd);
|
|
1000
|
-
}
|
|
1001
|
-
catch (err) {
|
|
1002
|
-
process.stderr.write(`hq lanes: admission lock close failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1003
|
-
}
|
|
1004
|
-
try {
|
|
1005
|
-
fs.unlinkSync(lockPath);
|
|
1006
|
-
}
|
|
1007
|
-
catch (err) {
|
|
1008
|
-
const code = err.code;
|
|
1009
|
-
if (code !== "ENOENT") {
|
|
1010
|
-
process.stderr.write(`hq lanes: admission lock unlink failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
catch (err) {
|
|
1016
|
-
const code = err.code;
|
|
1017
|
-
if (code !== "EEXIST")
|
|
1018
|
-
throw err;
|
|
1019
|
-
if (Date.now() - started > ADMISSION_LOCK_TIMEOUT_MS) {
|
|
1020
|
-
throw new LanesError("store_lock_timeout", `Timed out waiting for admission lock ${lockPath}`, { path: lockPath });
|
|
1021
|
-
}
|
|
1022
|
-
try {
|
|
1023
|
-
const raw = fs.readFileSync(lockPath, "utf8");
|
|
1024
|
-
const tsLine = raw.split("\n")[1];
|
|
1025
|
-
const ts = tsLine ? Number(tsLine) : NaN;
|
|
1026
|
-
if (Number.isFinite(ts) && Date.now() - ts > ADMISSION_LOCK_STALE_MS) {
|
|
1027
|
-
try {
|
|
1028
|
-
fs.unlinkSync(lockPath);
|
|
1029
|
-
}
|
|
1030
|
-
catch (unlinkErr) {
|
|
1031
|
-
const unlinkCode = unlinkErr.code;
|
|
1032
|
-
if (unlinkCode !== "ENOENT")
|
|
1033
|
-
throw unlinkErr;
|
|
1034
|
-
}
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
catch (statErr) {
|
|
1039
|
-
const statCode = statErr.code;
|
|
1040
|
-
if (statCode !== "ENOENT") {
|
|
1041
|
-
process.stderr.write(`hq lanes: stale admission-lock probe failed for ${lockPath}: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
await sleep(10);
|
|
1045
|
-
await yieldEventLoop();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
1014
|
function withCasRetry(fn, attempts = 5) {
|
|
1050
1015
|
let last;
|
|
1051
1016
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -1666,6 +1631,13 @@ async function runCreate(ctx, opts) {
|
|
|
1666
1631
|
}
|
|
1667
1632
|
}
|
|
1668
1633
|
const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
|
|
1634
|
+
const balancedAdmissionAccounts = !outpostId && pinnedAccount === undefined
|
|
1635
|
+
? [...new Set(allowedAccounts ??
|
|
1636
|
+
Object.entries(config.providers[provider].accounts)
|
|
1637
|
+
.filter(([id, account]) => account.enabled &&
|
|
1638
|
+
(profileAccounts === undefined || profileAccounts.includes(id)))
|
|
1639
|
+
.map(([id]) => id))]
|
|
1640
|
+
: undefined;
|
|
1669
1641
|
const threadId = meshCompany
|
|
1670
1642
|
? await ctx.mesh.ensureProjectThread(meshCompany, project)
|
|
1671
1643
|
: undefined;
|
|
@@ -1704,6 +1676,7 @@ async function runCreate(ctx, opts) {
|
|
|
1704
1676
|
validation = "pending";
|
|
1705
1677
|
}
|
|
1706
1678
|
else {
|
|
1679
|
+
const admissionNowMs = ctx.now().getTime();
|
|
1707
1680
|
admission = admitCreate({
|
|
1708
1681
|
hqRoot,
|
|
1709
1682
|
provider,
|
|
@@ -1712,14 +1685,16 @@ async function runCreate(ctx, opts) {
|
|
|
1712
1685
|
...(profile?.meta.max_concurrent !== undefined
|
|
1713
1686
|
? { profileMaxConcurrent: profile.meta.max_concurrent }
|
|
1714
1687
|
: {}),
|
|
1688
|
+
host: reconcileHostFromCtx(ctx),
|
|
1715
1689
|
priority,
|
|
1716
1690
|
createdAt,
|
|
1691
|
+
nowMs: admissionNowMs,
|
|
1717
1692
|
config,
|
|
1718
1693
|
});
|
|
1719
1694
|
if (admission.action === "refuse" && admission.error) {
|
|
1720
1695
|
throw admission.error;
|
|
1721
1696
|
}
|
|
1722
|
-
admission = await applyThrottle(hqRoot, config, ctx, admission, provider, pinnedAccount);
|
|
1697
|
+
admission = await applyThrottle(hqRoot, config, ctx, admission, provider, pinnedAccount, { nowMs: admissionNowMs });
|
|
1723
1698
|
if (admission.action === "refuse" && admission.error) {
|
|
1724
1699
|
throw admission.error;
|
|
1725
1700
|
}
|
|
@@ -1728,6 +1703,7 @@ async function runCreate(ctx, opts) {
|
|
|
1728
1703
|
? { kind: "outpost", id: outpostId }
|
|
1729
1704
|
: { kind: "local", id: ctx.hostname() };
|
|
1730
1705
|
const queuedReason = admission.action === "queue" ? admission.reason : undefined;
|
|
1706
|
+
const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
|
|
1731
1707
|
const lane = createLane(hqRoot, {
|
|
1732
1708
|
company,
|
|
1733
1709
|
...(company === null && meshCompany
|
|
@@ -1748,6 +1724,9 @@ async function runCreate(ctx, opts) {
|
|
|
1748
1724
|
senior_history: [],
|
|
1749
1725
|
state: "queued",
|
|
1750
1726
|
queued_reason: queuedReason,
|
|
1727
|
+
...(admissionReservedAt !== undefined
|
|
1728
|
+
? { admission_reserved_at: admissionReservedAt }
|
|
1729
|
+
: {}),
|
|
1751
1730
|
priority,
|
|
1752
1731
|
worker: resolvedWorker?.id ?? opts.worker,
|
|
1753
1732
|
...(resolvedWorker ? { worker_path: resolvedWorker.path } : {}),
|
|
@@ -1761,6 +1740,9 @@ async function runCreate(ctx, opts) {
|
|
|
1761
1740
|
: {}),
|
|
1762
1741
|
provider,
|
|
1763
1742
|
account: pinnedAccount,
|
|
1743
|
+
...(balancedAdmissionAccounts !== undefined
|
|
1744
|
+
? { admission_accounts: balancedAdmissionAccounts }
|
|
1745
|
+
: {}),
|
|
1764
1746
|
...(model !== undefined ? { model } : {}),
|
|
1765
1747
|
model_source: cliModel !== undefined
|
|
1766
1748
|
? "cli"
|
|
@@ -1787,7 +1769,7 @@ async function runCreate(ctx, opts) {
|
|
|
1787
1769
|
persisted = await persist();
|
|
1788
1770
|
}
|
|
1789
1771
|
else {
|
|
1790
|
-
persisted = await
|
|
1772
|
+
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
1791
1773
|
}
|
|
1792
1774
|
const { lane, admission, validation } = persisted;
|
|
1793
1775
|
const dest = runDir(hqRoot, lane.lane_id);
|
|
@@ -2160,38 +2142,54 @@ async function runPickup(ctx, opts) {
|
|
|
2160
2142
|
});
|
|
2161
2143
|
return;
|
|
2162
2144
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
hqRoot,
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
return {
|
|
2186
|
-
...current,
|
|
2187
|
-
state: "queued",
|
|
2188
|
-
queued_reason: admission.reason,
|
|
2189
|
-
};
|
|
2145
|
+
let selfTargetCancelled = false;
|
|
2146
|
+
const started = await withLaneAdmissionLock(hqRoot, afterAccount.provider, async () => {
|
|
2147
|
+
const current = readLane(hqRoot, afterAccount.lane_id);
|
|
2148
|
+
const config = loadLanesConfig(hqRoot);
|
|
2149
|
+
const nowMs = ctx.now().getTime();
|
|
2150
|
+
let admission = admitCreate({
|
|
2151
|
+
hqRoot,
|
|
2152
|
+
provider: current.provider,
|
|
2153
|
+
pinnedAccount: current.account,
|
|
2154
|
+
...(current.worker_profile !== undefined
|
|
2155
|
+
? {
|
|
2156
|
+
profileId: current.worker_profile,
|
|
2157
|
+
profileMaxConcurrent: loadWorkerProfile(hqRoot, current.worker_profile).meta.max_concurrent,
|
|
2158
|
+
}
|
|
2159
|
+
: {}),
|
|
2160
|
+
host: reconcileHostFromCtx(ctx),
|
|
2161
|
+
priority: current.priority,
|
|
2162
|
+
createdAt: current.queued_at ?? current.created_at,
|
|
2163
|
+
nowMs,
|
|
2164
|
+
config,
|
|
2165
|
+
excludeLaneId: current.lane_id,
|
|
2166
|
+
alreadyQueued: true,
|
|
2190
2167
|
});
|
|
2191
|
-
|
|
2192
|
-
|
|
2168
|
+
if (admission.action === "refuse" && admission.error)
|
|
2169
|
+
throw admission.error;
|
|
2170
|
+
admission = await applyThrottle(hqRoot, config, ctx, admission, current.provider, current.account, { host: reconcileHostFromCtx(ctx), excludeLaneId: current.lane_id, alreadyQueued: true, nowMs });
|
|
2171
|
+
if (admission.action === "refuse" && admission.error)
|
|
2172
|
+
throw admission.error;
|
|
2173
|
+
if (admission.action === "queue") {
|
|
2174
|
+
return casUpdateLane(hqRoot, current.lane_id, (live) => {
|
|
2175
|
+
if (live.version !== current.version || isTerminalLaneState(live.state)) {
|
|
2176
|
+
throw new LanesError("lane_admission_conflict", `Lane ${current.lane_id} changed while pickup admission was checked`, {
|
|
2177
|
+
lane_id: current.lane_id,
|
|
2178
|
+
prepared_version: current.version,
|
|
2179
|
+
version: live.version,
|
|
2180
|
+
state: live.state,
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
return {
|
|
2184
|
+
...live,
|
|
2185
|
+
state: "queued",
|
|
2186
|
+
queued_reason: admission.reason ?? "throttle",
|
|
2187
|
+
queued_at: live.queued_at ?? ctx.now().toISOString(),
|
|
2188
|
+
};
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2193
2191
|
try {
|
|
2194
|
-
|
|
2192
|
+
return spawnLaneWorker(hqRoot, current, {
|
|
2195
2193
|
env: ctx.env,
|
|
2196
2194
|
modelDeps: {
|
|
2197
2195
|
readFileSync: ctx.readFileSync,
|
|
@@ -2210,19 +2208,22 @@ async function runPickup(ctx, opts) {
|
|
|
2210
2208
|
}
|
|
2211
2209
|
catch (err) {
|
|
2212
2210
|
if (isLanesError(err) && err.error === "outpost_self_target") {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
ok: true,
|
|
2216
|
-
action: "pickup",
|
|
2217
|
-
lane_id: cancelled.lane_id,
|
|
2218
|
-
state: cancelled.state,
|
|
2219
|
-
error: "outpost_self_target",
|
|
2220
|
-
lane: cancelled,
|
|
2221
|
-
});
|
|
2222
|
-
return;
|
|
2211
|
+
selfTargetCancelled = true;
|
|
2212
|
+
return readLane(hqRoot, afterAccount.lane_id);
|
|
2223
2213
|
}
|
|
2224
2214
|
throw err;
|
|
2225
2215
|
}
|
|
2216
|
+
}, ctx.sleep);
|
|
2217
|
+
if (selfTargetCancelled) {
|
|
2218
|
+
printJson({
|
|
2219
|
+
ok: true,
|
|
2220
|
+
action: "pickup",
|
|
2221
|
+
lane_id: started.lane_id,
|
|
2222
|
+
state: started.state,
|
|
2223
|
+
error: "outpost_self_target",
|
|
2224
|
+
lane: started,
|
|
2225
|
+
});
|
|
2226
|
+
return;
|
|
2226
2227
|
}
|
|
2227
2228
|
printJson({
|
|
2228
2229
|
ok: true,
|
|
@@ -2242,6 +2243,7 @@ async function runList(ctx, opts) {
|
|
|
2242
2243
|
company = await ctx.resolveCompany(opts.company);
|
|
2243
2244
|
}
|
|
2244
2245
|
const now = ctx.now();
|
|
2246
|
+
const host = reconcileHostFromCtx(ctx);
|
|
2245
2247
|
const lanes = loadAllLanes(hqRoot)
|
|
2246
2248
|
.filter((lane) => {
|
|
2247
2249
|
if (state && lane.state !== state)
|
|
@@ -2258,28 +2260,42 @@ async function runList(ctx, opts) {
|
|
|
2258
2260
|
return false;
|
|
2259
2261
|
return true;
|
|
2260
2262
|
})
|
|
2261
|
-
.sort(
|
|
2262
|
-
.map((lane) =>
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2263
|
+
.sort(compareLaneQueueOrder)
|
|
2264
|
+
.map((lane) => {
|
|
2265
|
+
const remoteQueued = lane.state === "queued" &&
|
|
2266
|
+
lane.host.kind === "outpost" &&
|
|
2267
|
+
!laneRunsHere(lane, host);
|
|
2268
|
+
return {
|
|
2269
|
+
...lane,
|
|
2270
|
+
...(remoteQueued
|
|
2271
|
+
? { state: "remote", remote_state: lane.state, queued_reason: undefined }
|
|
2272
|
+
: {}),
|
|
2273
|
+
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
|
|
2274
|
+
stalled: Boolean(lane.stalled),
|
|
2275
|
+
// Listing is observational. Reconcile owns durable liveness transitions.
|
|
2276
|
+
senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
|
|
2277
|
+
? (() => {
|
|
2278
|
+
const state = sessionLiveness(hqRoot, lane.senior.id, {
|
|
2279
|
+
nowMs: now.getTime(),
|
|
2280
|
+
deps: livenessDepsFromCtx(ctx),
|
|
2281
|
+
presenceConnected: ctx.presenceConnected,
|
|
2282
|
+
});
|
|
2283
|
+
return state === null || state === "ended" || state === "lost";
|
|
2284
|
+
})()
|
|
2285
|
+
: false,
|
|
2286
|
+
};
|
|
2287
|
+
});
|
|
2278
2288
|
printJson({ ok: true, action: "list", lanes });
|
|
2279
2289
|
}
|
|
2280
2290
|
async function runShow(ctx, laneId) {
|
|
2281
2291
|
const hqRoot = ctx.hqRoot();
|
|
2282
|
-
const
|
|
2292
|
+
const storedLane = readLane(hqRoot, laneId);
|
|
2293
|
+
const remoteQueued = storedLane.state === "queued" &&
|
|
2294
|
+
storedLane.host.kind === "outpost" &&
|
|
2295
|
+
!laneRunsHere(storedLane, reconcileHostFromCtx(ctx));
|
|
2296
|
+
const lane = remoteQueued
|
|
2297
|
+
? { ...storedLane, state: "remote", remote_state: storedLane.state, queued_reason: undefined }
|
|
2298
|
+
: storedLane;
|
|
2283
2299
|
const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
|
|
2284
2300
|
printJson({
|
|
2285
2301
|
ok: true,
|
|
@@ -2290,7 +2306,7 @@ async function runShow(ctx, laneId) {
|
|
|
2290
2306
|
stalled_at: lane.stalled_at ?? null,
|
|
2291
2307
|
stalled_reason: lane.stalled_reason ?? null,
|
|
2292
2308
|
ask_delivery_failed: askDeliveryFailed,
|
|
2293
|
-
delivery: laneDeliveryMechanism(
|
|
2309
|
+
delivery: laneDeliveryMechanism(storedLane),
|
|
2294
2310
|
provider_session_id: lane.provider_session_id ?? null,
|
|
2295
2311
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
2296
2312
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
@@ -2306,10 +2322,136 @@ async function runShow(ctx, laneId) {
|
|
|
2306
2322
|
terminal_details: lane.terminal_details ?? null,
|
|
2307
2323
|
last_message_at: lane.last_message_at ?? null,
|
|
2308
2324
|
last_compaction: lane.last_compaction
|
|
2309
|
-
? { ...lane.last_compaction, after: compactionAfterUsage(
|
|
2325
|
+
? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
|
|
2310
2326
|
: null,
|
|
2311
2327
|
});
|
|
2312
2328
|
}
|
|
2329
|
+
function forceAdmissionAuthorized(ctx, lane) {
|
|
2330
|
+
const sessionId = sessionIdFromEnv(ctx.env);
|
|
2331
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
|
|
2332
|
+
if (lane.senior.kind === "lane") {
|
|
2333
|
+
return callerLaneId !== undefined && callerLaneId === lane.senior.id;
|
|
2334
|
+
}
|
|
2335
|
+
if (!sessionId || callerLaneId !== undefined)
|
|
2336
|
+
return false;
|
|
2337
|
+
if (lane.senior.kind === "session")
|
|
2338
|
+
return sessionId === lane.senior.id;
|
|
2339
|
+
if (lane.senior.kind !== "user")
|
|
2340
|
+
return false;
|
|
2341
|
+
const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
|
|
2342
|
+
if (loaded.status !== "ok")
|
|
2343
|
+
return false;
|
|
2344
|
+
const senior = loaded.doc.senior;
|
|
2345
|
+
return senior === "user" ||
|
|
2346
|
+
(senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
|
|
2347
|
+
senior.kind === "user");
|
|
2348
|
+
}
|
|
2349
|
+
async function runAdmit(ctx, laneId, opts) {
|
|
2350
|
+
const hqRoot = ctx.hqRoot();
|
|
2351
|
+
let lane = readLane(hqRoot, laneId);
|
|
2352
|
+
if (lane.state !== "queued") {
|
|
2353
|
+
throw new LanesError("lane_not_queued", `Lane ${laneId} is ${lane.state}; only a queued lane can be admitted`, {
|
|
2354
|
+
lane_id: laneId,
|
|
2355
|
+
state: lane.state,
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
const host = reconcileHostFromCtx(ctx);
|
|
2359
|
+
if (!laneRunsHere(lane, host)) {
|
|
2360
|
+
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (lane.pickup_validation_pending === true) {
|
|
2364
|
+
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
2368
|
+
lane = readLane(hqRoot, laneId);
|
|
2369
|
+
if (lane.state !== "queued") {
|
|
2370
|
+
throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
|
|
2371
|
+
lane_id: laneId,
|
|
2372
|
+
state: lane.state,
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
let blockedBy;
|
|
2376
|
+
if (opts.force) {
|
|
2377
|
+
if (!forceAdmissionAuthorized(ctx, lane)) {
|
|
2378
|
+
throw new LanesError("force_not_authorized", `Only lane ${lane.senior.kind === "lane" ? lane.senior.id : lane.senior.kind === "session" ? lane.senior.id : "senior/operator"} may force admission of ${laneId}`, { lane_id: laneId, senior: lane.senior });
|
|
2379
|
+
}
|
|
2380
|
+
const config = loadLanesConfig(hqRoot);
|
|
2381
|
+
const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
|
|
2382
|
+
const { decision } = await decideAdmission(hqRoot, config.throttle, ctx.throttleDeps);
|
|
2383
|
+
if (!decision.admit) {
|
|
2384
|
+
blockedBy = "throttle";
|
|
2385
|
+
}
|
|
2386
|
+
else if (hasEarlierEligibleQueuedLane(lanes, lane, config, host, (row) => row.worker_profile === undefined
|
|
2387
|
+
? undefined
|
|
2388
|
+
: loadWorkerProfile(hqRoot, row.worker_profile).meta.max_concurrent)) {
|
|
2389
|
+
blockedBy = "queue_position";
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
else {
|
|
2393
|
+
const admission = await assessLaneAdmission(ctx, lane, {
|
|
2394
|
+
queueAt: lane.queued_at ?? lane.created_at,
|
|
2395
|
+
alreadyQueued: true,
|
|
2396
|
+
});
|
|
2397
|
+
if (admission.action === "refuse" && admission.error) {
|
|
2398
|
+
throw admission.error;
|
|
2399
|
+
}
|
|
2400
|
+
if (admission.action === "queue")
|
|
2401
|
+
blockedBy = admission.reason ?? "throttle";
|
|
2402
|
+
}
|
|
2403
|
+
if (blockedBy !== undefined) {
|
|
2404
|
+
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
2405
|
+
if (current.version !== lane.version || current.state !== "queued") {
|
|
2406
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while admission was checked`, {
|
|
2407
|
+
lane_id: laneId,
|
|
2408
|
+
prepared_version: lane.version,
|
|
2409
|
+
version: current.version,
|
|
2410
|
+
state: current.state,
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
return { ...current, queued_reason: blockedBy };
|
|
2414
|
+
});
|
|
2415
|
+
printJson({
|
|
2416
|
+
ok: true,
|
|
2417
|
+
action: "admit",
|
|
2418
|
+
lane_id: laneId,
|
|
2419
|
+
admitted: false,
|
|
2420
|
+
blocked_by: blockedBy,
|
|
2421
|
+
lane: updated,
|
|
2422
|
+
});
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
2426
|
+
env: ctx.env,
|
|
2427
|
+
modelDeps: {
|
|
2428
|
+
readFileSync: ctx.readFileSync,
|
|
2429
|
+
probe: ctx.probeProvider,
|
|
2430
|
+
env: ctx.env,
|
|
2431
|
+
},
|
|
2432
|
+
existsSync: ctx.existsSync,
|
|
2433
|
+
spawnProcess: ctx.spawnProcess,
|
|
2434
|
+
proveDetached: ctx.proveDetached,
|
|
2435
|
+
killProcess: ctx.killProcess,
|
|
2436
|
+
now: ctx.now,
|
|
2437
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
2438
|
+
runCompaction: ctx.runCompaction,
|
|
2439
|
+
locateTranscript: ctx.locateTranscript,
|
|
2440
|
+
});
|
|
2441
|
+
const admitted = opts.force && started.state === "running"
|
|
2442
|
+
? casUpdateLane(hqRoot, laneId, (current) => ({ ...current, admitted_reason: "operator_force" }))
|
|
2443
|
+
: started;
|
|
2444
|
+
printJson({
|
|
2445
|
+
ok: true,
|
|
2446
|
+
action: "admit",
|
|
2447
|
+
lane_id: laneId,
|
|
2448
|
+
admitted: admitted.state === "running",
|
|
2449
|
+
forced: Boolean(opts.force),
|
|
2450
|
+
blocked_by: admitted.state === "running" ? null : admitted.queued_reason ?? null,
|
|
2451
|
+
lane: admitted,
|
|
2452
|
+
});
|
|
2453
|
+
}, ctx.sleep);
|
|
2454
|
+
}
|
|
2313
2455
|
/**
|
|
2314
2456
|
* US-023: move the lane's provider session to `--account` (same provider)
|
|
2315
2457
|
* before a resume or a message resume. Every check runs before anything is
|
|
@@ -2322,6 +2464,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2322
2464
|
const hqRoot = ctx.hqRoot();
|
|
2323
2465
|
const now = ctx.now();
|
|
2324
2466
|
const laneId = lane.lane_id;
|
|
2467
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
2325
2468
|
if (lane.state === "running") {
|
|
2326
2469
|
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
2327
2470
|
lane_id: laneId,
|
|
@@ -2432,16 +2575,14 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2432
2575
|
// pointed at the account whose spawn has not claimed it yet
|
|
2433
2576
|
// (`pending_resume` set by this write, cleared by the claim), so two moves
|
|
2434
2577
|
// into one free slot cannot both start.
|
|
2435
|
-
return
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
const
|
|
2443
|
-
const atCap = onProvider.length >= providerCfg.max_running ||
|
|
2444
|
-
onProvider.filter((row) => row.account === target).length >= accountMaxRunning(providerCfg, target);
|
|
2578
|
+
return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
2579
|
+
const admission = await assessLaneAdmission(ctx, lane, {
|
|
2580
|
+
account: target,
|
|
2581
|
+
queueAt: at,
|
|
2582
|
+
});
|
|
2583
|
+
if (admission.action === "refuse" && admission.error)
|
|
2584
|
+
throw admission.error;
|
|
2585
|
+
const queued = admission.action === "queue";
|
|
2445
2586
|
const moved = casUpdateLane(hqRoot, laneId, (current) => {
|
|
2446
2587
|
// The copy and every check above were made from `lane`; a record
|
|
2447
2588
|
// that moved on since (another move, a spawn, a cancel) invalidates
|
|
@@ -2453,6 +2594,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2453
2594
|
const next = {
|
|
2454
2595
|
...current,
|
|
2455
2596
|
account: target,
|
|
2597
|
+
admission_accounts: undefined,
|
|
2456
2598
|
provider_session_id: sessionId,
|
|
2457
2599
|
provider_session_id_absent: undefined,
|
|
2458
2600
|
...(input.cliModel !== undefined
|
|
@@ -2466,13 +2608,17 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2466
2608
|
}),
|
|
2467
2609
|
pending_resume: { session_id: sessionId, at, ...input.pending },
|
|
2468
2610
|
};
|
|
2469
|
-
if (
|
|
2611
|
+
if (queued) {
|
|
2470
2612
|
next.state = "queued";
|
|
2471
|
-
next.queued_reason = "
|
|
2613
|
+
next.queued_reason = admission.reason ?? "throttle";
|
|
2614
|
+
next.queued_at = at;
|
|
2615
|
+
}
|
|
2616
|
+
else {
|
|
2617
|
+
next.queued_at = undefined;
|
|
2472
2618
|
}
|
|
2473
2619
|
return next;
|
|
2474
2620
|
});
|
|
2475
|
-
return { lane: moved, from, to: target, copy, queued
|
|
2621
|
+
return { lane: moved, from, to: target, copy, queued };
|
|
2476
2622
|
}, ctx.sleep);
|
|
2477
2623
|
}
|
|
2478
2624
|
async function runMessage(ctx, laneId, opts) {
|
|
@@ -2527,6 +2673,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2527
2673
|
};
|
|
2528
2674
|
let resumed = null;
|
|
2529
2675
|
let move = null;
|
|
2676
|
+
let queuedOnAdmission = false;
|
|
2530
2677
|
if (opts.account !== undefined) {
|
|
2531
2678
|
// A moved message is always a resume on the target account: a running
|
|
2532
2679
|
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
@@ -2551,31 +2698,70 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2551
2698
|
appendDropboxMessage(hqRoot, laneId, dropboxMessage);
|
|
2552
2699
|
}
|
|
2553
2700
|
else {
|
|
2554
|
-
// Exited
|
|
2555
|
-
//
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2701
|
+
// Exited worker with a recorded session: queue the message when capacity
|
|
2702
|
+
// is occupied, otherwise start its next turn under the shared admission lock.
|
|
2703
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
2704
|
+
resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
2705
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
2706
|
+
if (admission.action === "refuse" && admission.error)
|
|
2707
|
+
throw admission.error;
|
|
2708
|
+
if (admission.action === "queue") {
|
|
2709
|
+
const sessionId = lane.provider_session_id;
|
|
2710
|
+
if (!sessionId) {
|
|
2711
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
|
|
2712
|
+
lane_id: laneId,
|
|
2713
|
+
provider: lane.provider,
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2717
|
+
queuedOnAdmission = true;
|
|
2718
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
2719
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
2720
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
2721
|
+
lane_id: laneId,
|
|
2722
|
+
prepared_version: lane.version,
|
|
2723
|
+
version: current.version,
|
|
2724
|
+
state: current.state,
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
...current,
|
|
2729
|
+
state: "queued",
|
|
2730
|
+
queued_reason: admission.reason ?? "throttle",
|
|
2731
|
+
queued_at: at,
|
|
2732
|
+
pending_resume: {
|
|
2733
|
+
session_id: sessionId,
|
|
2734
|
+
trigger: "message",
|
|
2735
|
+
text,
|
|
2736
|
+
message_id: messageId,
|
|
2737
|
+
compact: opts.compact !== false,
|
|
2738
|
+
at,
|
|
2739
|
+
},
|
|
2740
|
+
};
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
2564
2744
|
env: ctx.env,
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2745
|
+
modelDeps: {
|
|
2746
|
+
readFileSync: ctx.readFileSync,
|
|
2747
|
+
probe: ctx.probeProvider,
|
|
2748
|
+
env: ctx.env,
|
|
2749
|
+
},
|
|
2750
|
+
existsSync: ctx.existsSync,
|
|
2751
|
+
spawnProcess: ctx.spawnProcess,
|
|
2752
|
+
proveDetached: ctx.proveDetached,
|
|
2753
|
+
killProcess: ctx.killProcess,
|
|
2754
|
+
now: ctx.now,
|
|
2755
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
2756
|
+
runCompaction: ctx.runCompaction,
|
|
2757
|
+
locateTranscript: ctx.locateTranscript,
|
|
2758
|
+
}, {
|
|
2759
|
+
trigger: { kind: "message", text, message_id: messageId },
|
|
2760
|
+
compact: opts.compact !== false,
|
|
2761
|
+
});
|
|
2762
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2763
|
+
return started;
|
|
2764
|
+
}, ctx.sleep);
|
|
2579
2765
|
}
|
|
2580
2766
|
// Every accepted message moves the compaction reference forward.
|
|
2581
2767
|
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
@@ -2621,9 +2807,11 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2621
2807
|
const drainHook = drainHookStatusFor(ctx, next);
|
|
2622
2808
|
const deliveryStatus = move?.queued
|
|
2623
2809
|
? "queued_on_target_account"
|
|
2624
|
-
:
|
|
2625
|
-
? "
|
|
2626
|
-
: "
|
|
2810
|
+
: queuedOnAdmission
|
|
2811
|
+
? "queued_on_admission"
|
|
2812
|
+
: mechanism === "dropbox"
|
|
2813
|
+
? "awaiting_next_tool_call"
|
|
2814
|
+
: "worker_spawned";
|
|
2627
2815
|
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
2628
2816
|
? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
|
|
2629
2817
|
: mechanism === "dropbox" && next.stalled === true
|
|
@@ -2718,6 +2906,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2718
2906
|
if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
2719
2907
|
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
2720
2908
|
}
|
|
2909
|
+
if (lane.state === "queued") {
|
|
2910
|
+
throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
|
|
2911
|
+
}
|
|
2721
2912
|
const refusal = resumeCommandRefusal(lane);
|
|
2722
2913
|
if (refusal !== null) {
|
|
2723
2914
|
throw new LanesError(refusal.error, refusal.message, {
|
|
@@ -2727,6 +2918,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2727
2918
|
message: refusal.message,
|
|
2728
2919
|
});
|
|
2729
2920
|
}
|
|
2921
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
2730
2922
|
const resumedFrom = lane.state;
|
|
2731
2923
|
const text = opts.text?.trim() ? opts.text : undefined;
|
|
2732
2924
|
const at = ctx.now().toISOString();
|
|
@@ -2749,32 +2941,77 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2749
2941
|
});
|
|
2750
2942
|
lane = move.lane;
|
|
2751
2943
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2944
|
+
let queuedOnAdmission = false;
|
|
2945
|
+
let resumed;
|
|
2946
|
+
if (move?.queued) {
|
|
2947
|
+
resumed = lane;
|
|
2948
|
+
}
|
|
2949
|
+
else {
|
|
2950
|
+
resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
2951
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
2952
|
+
if (admission.action === "refuse" && admission.error)
|
|
2953
|
+
throw admission.error;
|
|
2954
|
+
if (admission.action === "queue") {
|
|
2955
|
+
const sessionId = lane.provider_session_id;
|
|
2956
|
+
if (!sessionId) {
|
|
2957
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
|
|
2958
|
+
lane_id: laneId,
|
|
2959
|
+
provider: lane.provider,
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
queuedOnAdmission = true;
|
|
2963
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
2964
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
2965
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
|
|
2966
|
+
lane_id: laneId,
|
|
2967
|
+
prepared_version: lane.version,
|
|
2968
|
+
version: current.version,
|
|
2969
|
+
state: current.state,
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
return {
|
|
2973
|
+
...current,
|
|
2974
|
+
state: "queued",
|
|
2975
|
+
queued_reason: admission.reason ?? "throttle",
|
|
2976
|
+
queued_at: at,
|
|
2977
|
+
pending_resume: {
|
|
2978
|
+
session_id: sessionId,
|
|
2979
|
+
trigger: "resume",
|
|
2980
|
+
resumed_from: resumedFrom,
|
|
2981
|
+
...(text !== undefined ? { text } : {}),
|
|
2982
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
2983
|
+
compact: opts.compact !== false,
|
|
2984
|
+
at,
|
|
2985
|
+
},
|
|
2986
|
+
};
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
return spawnLaneWorker(hqRoot, lane, {
|
|
2759
2990
|
env: ctx.env,
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2991
|
+
modelDeps: {
|
|
2992
|
+
readFileSync: ctx.readFileSync,
|
|
2993
|
+
probe: ctx.probeProvider,
|
|
2994
|
+
env: ctx.env,
|
|
2995
|
+
},
|
|
2996
|
+
existsSync: ctx.existsSync,
|
|
2997
|
+
spawnProcess: ctx.spawnProcess,
|
|
2998
|
+
proveDetached: ctx.proveDetached,
|
|
2999
|
+
killProcess: ctx.killProcess,
|
|
3000
|
+
now: ctx.now,
|
|
3001
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
3002
|
+
runCompaction: ctx.runCompaction,
|
|
3003
|
+
locateTranscript: ctx.locateTranscript,
|
|
3004
|
+
}, {
|
|
3005
|
+
trigger: {
|
|
3006
|
+
kind: "resume",
|
|
3007
|
+
from: resumedFrom,
|
|
3008
|
+
...(text !== undefined ? { text } : {}),
|
|
3009
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
3010
|
+
},
|
|
3011
|
+
compact: opts.compact !== false,
|
|
3012
|
+
});
|
|
3013
|
+
}, ctx.sleep);
|
|
3014
|
+
}
|
|
2778
3015
|
if (text !== undefined && messageId !== undefined) {
|
|
2779
3016
|
// Same audit copy a message resume leaves; nothing drains it. Archive only
|
|
2780
3017
|
// after a successful spawn so a concurrent resume cannot leave a phantom.
|
|
@@ -2833,7 +3070,11 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2833
3070
|
message_id: messageId ?? null,
|
|
2834
3071
|
text: text ?? null,
|
|
2835
3072
|
delivery: "resume",
|
|
2836
|
-
delivery_status: move?.queued
|
|
3073
|
+
delivery_status: move?.queued
|
|
3074
|
+
? "queued_on_target_account"
|
|
3075
|
+
: queuedOnAdmission
|
|
3076
|
+
? "queued_on_admission"
|
|
3077
|
+
: "worker_spawned",
|
|
2837
3078
|
account_move: move ? accountMoveSummary(move) : null,
|
|
2838
3079
|
resumed_session_id: resumed.last_resume?.session_id ?? null,
|
|
2839
3080
|
last_resume: next.last_resume ?? null,
|
|
@@ -3903,6 +4144,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
3903
4144
|
.action(wrap(async (opts) => {
|
|
3904
4145
|
await runList(ctx, opts);
|
|
3905
4146
|
}));
|
|
4147
|
+
lanes
|
|
4148
|
+
.command("admit")
|
|
4149
|
+
.description("Try to admit the oldest eligible queued lane")
|
|
4150
|
+
.argument("<lane-id>", "Lane id")
|
|
4151
|
+
.option("--force", "Senior/operator override for provider, account, and profile caps")
|
|
4152
|
+
.action(wrap(async (laneId, opts) => {
|
|
4153
|
+
await runAdmit(ctx, laneId, opts);
|
|
4154
|
+
}));
|
|
3906
4155
|
lanes
|
|
3907
4156
|
.command("watch")
|
|
3908
4157
|
.description("Watch lane state and question changes until all lanes are terminal")
|