@indigoai-us/hq-cli 5.131.0 → 5.132.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 +111 -0
- package/dist/command-catalog.generated.d.ts +12 -0
- package/dist/command-catalog.generated.js +16 -0
- package/dist/commands/lanes.d.ts +13 -2
- package/dist/commands/lanes.js +394 -42
- package/dist/commands/mesh.js +20 -1
- package/dist/commands/update.js +1 -1
- package/dist/lib/bot/paths.d.ts +7 -5
- package/dist/lib/bot/paths.js +8 -1
- package/dist/lib/bot/prompt.d.ts +0 -2
- package/dist/lib/bot/prompt.js +5 -11
- package/dist/lib/bot/room-policy.d.ts +1 -24
- package/dist/lib/bot/room-policy.js +2 -35
- package/dist/lib/bot/run.d.ts +7 -8
- package/dist/lib/bot/run.js +21 -33
- package/dist/lib/bot/session.d.ts +1 -1
- package/dist/lib/bot/session.js +1 -1
- package/dist/lib/lanes/accounts.d.ts +6 -0
- package/dist/lib/lanes/accounts.js +2 -1
- package/dist/lib/lanes/config.d.ts +4 -0
- package/dist/lib/lanes/config.js +9 -0
- package/dist/lib/lanes/index.d.ts +10 -3
- package/dist/lib/lanes/index.js +5 -1
- package/dist/lib/lanes/mesh-events.js +4 -0
- package/dist/lib/lanes/models.d.ts +78 -0
- package/dist/lib/lanes/models.js +205 -0
- package/dist/lib/lanes/move-account.d.ts +56 -0
- package/dist/lib/lanes/move-account.js +193 -0
- package/dist/lib/lanes/provider-rejection.d.ts +86 -0
- package/dist/lib/lanes/provider-rejection.js +258 -0
- package/dist/lib/lanes/quota.d.ts +17 -0
- package/dist/lib/lanes/quota.js +24 -0
- package/dist/lib/lanes/reconcile-install.d.ts +24 -3
- package/dist/lib/lanes/reconcile-install.js +48 -12
- package/dist/lib/lanes/reconcile.js +41 -11
- package/dist/lib/lanes/spawn.js +26 -5
- package/dist/lib/lanes/store.js +10 -0
- package/dist/lib/lanes/types.d.ts +49 -2
- package/dist/lib/lanes/types.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +27 -0
- package/dist/lib/lanes/watcher.js +68 -5
- package/dist/lib/mesh/live/backfill-held.d.ts +4 -3
- package/dist/lib/mesh/live/backfill-held.js +37 -14
- package/dist/lib/mesh/live/daemon/doctor.js +5 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/install.js +84 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +14 -4
- package/dist/lib/mesh/live/emit-evidence.d.ts +19 -0
- package/dist/lib/mesh/live/emit-evidence.js +20 -2
- package/dist/lib/work-context/company.d.ts +16 -0
- package/dist/lib/work-context/company.js +46 -14
- package/dist/utils/self-update.js +4 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -17,13 +17,15 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
17
17
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
18
18
|
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
19
19
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
20
|
-
import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, isLoggedOut, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
20
|
+
import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, isLoggedOut, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
21
21
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
22
22
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
23
23
|
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
24
24
|
import { parseCliEffort, resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
25
25
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
26
|
-
import { accountQuotaView, loadQuotaState } from "../lib/lanes/quota.js";
|
|
26
|
+
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
27
|
+
import { SESSION_MOVE_REASON, copySessionFiles, planSessionCopy } from "../lib/lanes/move-account.js";
|
|
28
|
+
import { defaultProbe, validateLaneModel, } from "../lib/lanes/models.js";
|
|
27
29
|
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
28
30
|
export { spawnLaneWorker };
|
|
29
31
|
export { questionBodyAbsPath };
|
|
@@ -201,6 +203,7 @@ function resolveDeps(deps = {}) {
|
|
|
201
203
|
};
|
|
202
204
|
}),
|
|
203
205
|
resolveMachineIdentity: deps.resolveMachineIdentity ?? lookupMachineIdentity,
|
|
206
|
+
probeProvider: deps.probeProvider ?? defaultProbe,
|
|
204
207
|
runCompaction: deps.runCompaction,
|
|
205
208
|
locateTranscript: deps.locateTranscript,
|
|
206
209
|
stageOutpostPayload: deps.stageOutpostPayload ?? defaultStageOutpostPayload,
|
|
@@ -1047,6 +1050,7 @@ export const PICKUP_TERMINAL_REASONS = [
|
|
|
1047
1050
|
"account_not_logged_in",
|
|
1048
1051
|
"unknown_account",
|
|
1049
1052
|
"account_disabled",
|
|
1053
|
+
"model_unsupported",
|
|
1050
1054
|
];
|
|
1051
1055
|
export const DISPATCH_TERMINAL_REASON = "outpost_dispatch_failed";
|
|
1052
1056
|
function parsePickupTerminalReason(value) {
|
|
@@ -1069,28 +1073,38 @@ function markDispatchFailure(hqRoot, laneId, details) {
|
|
|
1069
1073
|
};
|
|
1070
1074
|
});
|
|
1071
1075
|
}
|
|
1072
|
-
|
|
1076
|
+
/**
|
|
1077
|
+
* The CAS updater a pickup failure applies. A lane that is already terminal
|
|
1078
|
+
* for any reason (a cancel that landed after the caller's last read, an
|
|
1079
|
+
* earlier pickup failure) is returned unchanged: the guard lives inside the
|
|
1080
|
+
* same compare-and-swap that writes the failure, so no read-then-write gap
|
|
1081
|
+
* can overwrite another writer's terminal decision.
|
|
1082
|
+
*/
|
|
1083
|
+
export function pickupFailureUpdater(reasonRaw, details) {
|
|
1073
1084
|
const reason = parsePickupTerminalReason(reasonRaw);
|
|
1074
|
-
return
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if (isTerminalLaneState(current.state)
|
|
1085
|
+
return (current) => {
|
|
1086
|
+
// A running record is never overwritten either: a worker that was
|
|
1087
|
+
// admitted meanwhile keeps its lane, and the watcher classifies it.
|
|
1088
|
+
if (isTerminalLaneState(current.state) || current.state === "running")
|
|
1078
1089
|
return current;
|
|
1079
|
-
}
|
|
1080
1090
|
return {
|
|
1081
1091
|
...current,
|
|
1082
1092
|
state: "failed",
|
|
1083
1093
|
queued_reason: undefined,
|
|
1094
|
+
pickup_validation_pending: undefined,
|
|
1084
1095
|
terminal_reason: reason,
|
|
1085
1096
|
terminal_details: details,
|
|
1086
1097
|
};
|
|
1087
|
-
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function markPickupFailure(hqRoot, laneId, reasonRaw, details) {
|
|
1101
|
+
return casUpdateLane(hqRoot, laneId, pickupFailureUpdater(reasonRaw, details));
|
|
1088
1102
|
}
|
|
1089
1103
|
/**
|
|
1090
1104
|
* Outpost-side account bind (spec §5.2). Writes a named terminal reason,
|
|
1091
1105
|
* never a plain `failed`.
|
|
1092
1106
|
*/
|
|
1093
|
-
export function applyOutpostAccountPickup(hqRoot, laneId) {
|
|
1107
|
+
export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
1094
1108
|
const lane = readLane(hqRoot, laneId);
|
|
1095
1109
|
const config = loadLanesConfig(hqRoot);
|
|
1096
1110
|
try {
|
|
@@ -1104,16 +1118,55 @@ export function applyOutpostAccountPickup(hqRoot, laneId) {
|
|
|
1104
1118
|
account: resolved.id,
|
|
1105
1119
|
});
|
|
1106
1120
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1121
|
+
// The executing host is the first place the model can be checked
|
|
1122
|
+
// against this account's provider list: an outpost create without
|
|
1123
|
+
// --account has no local account home to validate against.
|
|
1124
|
+
// The probe can block for up to MODEL_PROBE_TIMEOUT_MS. A cancel or any
|
|
1125
|
+
// other terminal decision taken meanwhile is final: after every probe
|
|
1126
|
+
// result the lane is re-read and a terminal lane is returned as it is
|
|
1127
|
+
// (so runPickup never proceeds into admission for it). The failure
|
|
1128
|
+
// write itself refuses a terminal record inside its CAS
|
|
1129
|
+
// (pickupFailureUpdater), so a cancel landing between this re-read and
|
|
1130
|
+
// that write is kept too.
|
|
1131
|
+
let current = lane;
|
|
1132
|
+
if (lane.model !== undefined) {
|
|
1133
|
+
let unsupported = null;
|
|
1134
|
+
try {
|
|
1135
|
+
validateLaneModel(lane.provider, lane.model, resolved.home, modelDeps);
|
|
1136
|
+
}
|
|
1137
|
+
catch (err) {
|
|
1138
|
+
if (isLanesError(err) && err.error === "model_unsupported") {
|
|
1139
|
+
unsupported = err;
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
throw err;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
current = readLane(hqRoot, laneId);
|
|
1146
|
+
if (isTerminalLaneState(current.state) || current.state === "running")
|
|
1147
|
+
return current;
|
|
1148
|
+
if (unsupported) {
|
|
1149
|
+
return markPickupFailure(hqRoot, laneId, "model_unsupported", {
|
|
1150
|
+
...unsupported.details,
|
|
1151
|
+
account: resolved.id,
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (current.account === resolved.id && current.pickup_validation_pending === undefined) {
|
|
1156
|
+
return current;
|
|
1157
|
+
}
|
|
1109
1158
|
// The bind is the executing host's pre-spawn account resolution, the same
|
|
1110
1159
|
// write spawnLaneWorker makes for a local lane, so it carries the spawn
|
|
1111
1160
|
// writer. `account` stays closed to `command`: an operator command never
|
|
1112
|
-
// re-pins the account of a lane it does not execute.
|
|
1161
|
+
// re-pins the account of a lane it does not execute. The bind also
|
|
1162
|
+
// clears the pickup marker: from here the reconcile pass may admit it.
|
|
1113
1163
|
return retryUpdateLane(hqRoot, laneId, "spawn", (current) => {
|
|
1114
|
-
if (current.
|
|
1164
|
+
if (isTerminalLaneState(current.state) || current.state === "running")
|
|
1165
|
+
return current;
|
|
1166
|
+
if (current.account === resolved.id && current.pickup_validation_pending === undefined) {
|
|
1115
1167
|
return current;
|
|
1116
|
-
|
|
1168
|
+
}
|
|
1169
|
+
return { ...current, account: resolved.id, pickup_validation_pending: undefined };
|
|
1117
1170
|
});
|
|
1118
1171
|
}
|
|
1119
1172
|
catch (err) {
|
|
@@ -1255,6 +1308,64 @@ async function runCreate(ctx, opts) {
|
|
|
1255
1308
|
}
|
|
1256
1309
|
const now = ctx.now();
|
|
1257
1310
|
const createdAt = now.toISOString();
|
|
1311
|
+
// `--model` is validated against the provider's own model list (models.ts)
|
|
1312
|
+
// before anything is written or posted: ahead of the mesh thread, so a
|
|
1313
|
+
// refusal leaves no orphan thread, and ahead of the admission lock, so a
|
|
1314
|
+
// slow `grok models` / `claude --help` probe never holds the lock other
|
|
1315
|
+
// creates wait on. A pinned create checks the pinned account's home. An
|
|
1316
|
+
// unpinned create is not bound to an account here (persist re-resolves the
|
|
1317
|
+
// balanced account under the lock, so overlapping creates still spread),
|
|
1318
|
+
// so it checks the balanced candidate first and then every other enabled
|
|
1319
|
+
// account the locked selection could land on (codex catalogs are per home
|
|
1320
|
+
// and can differ). `validatedAccounts` records which accepted the model
|
|
1321
|
+
// or had no readable list, and the balanced pick under the lock is
|
|
1322
|
+
// restricted to that set (resolveAccount's allowedAccounts): an account
|
|
1323
|
+
// whose list refused the model is never selected, so nothing has to be
|
|
1324
|
+
// refused after the mesh thread exists, and no probe or catalog read runs
|
|
1325
|
+
// while the lock is held. Every outpost create,
|
|
1326
|
+
// pinned or not, is validated at pickup on the executing host instead:
|
|
1327
|
+
// its provider list and CLI are the ones that run the lane.
|
|
1328
|
+
const modelDeps = {
|
|
1329
|
+
readFileSync: ctx.readFileSync,
|
|
1330
|
+
probe: ctx.probeProvider,
|
|
1331
|
+
env: ctx.env,
|
|
1332
|
+
};
|
|
1333
|
+
const validatedAccounts = new Set();
|
|
1334
|
+
if (cliModel !== undefined && !outpostId) {
|
|
1335
|
+
const accounts = config.providers[provider].accounts;
|
|
1336
|
+
const candidate = pinnedAccount ??
|
|
1337
|
+
resolveAccount(provider, undefined, config, {
|
|
1338
|
+
hqRoot,
|
|
1339
|
+
existsSync: ctx.existsSync,
|
|
1340
|
+
cliModel,
|
|
1341
|
+
}).id;
|
|
1342
|
+
const toValidate = pinnedAccount !== undefined
|
|
1343
|
+
? [candidate]
|
|
1344
|
+
: [
|
|
1345
|
+
candidate,
|
|
1346
|
+
...Object.keys(accounts)
|
|
1347
|
+
.filter((id) => id !== candidate && accounts[id]?.enabled)
|
|
1348
|
+
.sort(),
|
|
1349
|
+
];
|
|
1350
|
+
for (const id of toValidate) {
|
|
1351
|
+
const home = accounts[id]?.home;
|
|
1352
|
+
if (home === undefined)
|
|
1353
|
+
continue;
|
|
1354
|
+
try {
|
|
1355
|
+
validateLaneModel(provider, cliModel, home, modelDeps);
|
|
1356
|
+
validatedAccounts.add(id);
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
if (!(isLanesError(err) && err.error === "model_unsupported"))
|
|
1360
|
+
throw err;
|
|
1361
|
+
// The account that would run the lane refuses it: refuse now, before
|
|
1362
|
+
// the other lists are read. Another account's refusal only drops it
|
|
1363
|
+
// from the locked selection.
|
|
1364
|
+
if (id === candidate)
|
|
1365
|
+
throw err;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1258
1369
|
const threadId = company
|
|
1259
1370
|
? await ctx.mesh.ensureProjectThread(company, project)
|
|
1260
1371
|
: undefined;
|
|
@@ -1266,6 +1377,7 @@ async function runCreate(ctx, opts) {
|
|
|
1266
1377
|
hqRoot,
|
|
1267
1378
|
existsSync: ctx.existsSync,
|
|
1268
1379
|
cliModel,
|
|
1380
|
+
...(validatedAccounts.size > 0 ? { allowedAccounts: [...validatedAccounts] } : {}),
|
|
1269
1381
|
}).id;
|
|
1270
1382
|
}
|
|
1271
1383
|
const accountCfg = pinnedAccount !== undefined
|
|
@@ -1531,7 +1643,13 @@ function ingestPickupPayload(hqRoot, payload) {
|
|
|
1531
1643
|
throw new LanesError("outpost_payload_invalid", "Pickup payload lane.lane_id is required", {});
|
|
1532
1644
|
}
|
|
1533
1645
|
const existing = tryReadLane(hqRoot, laneId);
|
|
1534
|
-
|
|
1646
|
+
// The lane is stored ineligible for admission until the account bind
|
|
1647
|
+
// clears the marker (see LaneRecord.pickup_validation_pending).
|
|
1648
|
+
const lane = existing === null
|
|
1649
|
+
? createLane(hqRoot, { ...payload.lane, pickup_validation_pending: true })
|
|
1650
|
+
: existing.state === "queued" && existing.pickup_validation_pending === undefined
|
|
1651
|
+
? retryUpdateLane(hqRoot, laneId, "command", (current) => current.state === "queued" ? { ...current, pickup_validation_pending: true } : current)
|
|
1652
|
+
: existing;
|
|
1535
1653
|
const dest = runDir(hqRoot, lane.lane_id);
|
|
1536
1654
|
ensureDir(path.join(dest, "logs"));
|
|
1537
1655
|
ensureDir(path.join(dest, "envelopes"));
|
|
@@ -1599,7 +1717,11 @@ async function runPickup(ctx, opts) {
|
|
|
1599
1717
|
throw err;
|
|
1600
1718
|
}
|
|
1601
1719
|
}
|
|
1602
|
-
const afterAccount = applyOutpostAccountPickup(hqRoot, ingested.lane_id
|
|
1720
|
+
const afterAccount = applyOutpostAccountPickup(hqRoot, ingested.lane_id, {
|
|
1721
|
+
readFileSync: ctx.readFileSync,
|
|
1722
|
+
probe: ctx.probeProvider,
|
|
1723
|
+
env: ctx.env,
|
|
1724
|
+
});
|
|
1603
1725
|
if (isTerminalLaneState(afterAccount.state)) {
|
|
1604
1726
|
printJson({
|
|
1605
1727
|
ok: true,
|
|
@@ -1741,23 +1863,182 @@ async function runShow(ctx, laneId) {
|
|
|
1741
1863
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
1742
1864
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
1743
1865
|
last_resume: lane.last_resume ?? null,
|
|
1866
|
+
account: lane.account ?? null,
|
|
1867
|
+
account_history: lane.account_history ?? [],
|
|
1868
|
+
pending_resume: lane.pending_resume ?? null,
|
|
1744
1869
|
last_run_usage: lane.last_run_usage ?? null,
|
|
1745
1870
|
model: lane.model ?? null,
|
|
1746
1871
|
effort: lane.effort ?? null,
|
|
1747
1872
|
effort_source: lane.effort_source ?? null,
|
|
1873
|
+
terminal_reason: lane.terminal_reason ?? null,
|
|
1874
|
+
terminal_details: lane.terminal_details ?? null,
|
|
1748
1875
|
last_message_at: lane.last_message_at ?? null,
|
|
1749
1876
|
last_compaction: lane.last_compaction
|
|
1750
1877
|
? { ...lane.last_compaction, after: compactionAfterUsage(lane) }
|
|
1751
1878
|
: null,
|
|
1752
1879
|
});
|
|
1753
1880
|
}
|
|
1881
|
+
/**
|
|
1882
|
+
* US-023: move the lane's provider session to `--account` (same provider)
|
|
1883
|
+
* before a resume or a message resume. Every check runs before anything is
|
|
1884
|
+
* copied or written; the copy runs before the record changes, so a failed
|
|
1885
|
+
* copy leaves the lane on its source account. The turn is started by the
|
|
1886
|
+
* caller, or parked as `pending_resume` when the target is at its cap so the
|
|
1887
|
+
* reconcile pass resumes it there.
|
|
1888
|
+
*/
|
|
1889
|
+
async function prepareAccountMove(ctx, lane, input) {
|
|
1890
|
+
const hqRoot = ctx.hqRoot();
|
|
1891
|
+
const now = ctx.now();
|
|
1892
|
+
const laneId = lane.lane_id;
|
|
1893
|
+
if (lane.state === "running") {
|
|
1894
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
1895
|
+
lane_id: laneId,
|
|
1896
|
+
state: lane.state,
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
// `--account <id>` or `--account <provider>:<id>`: a provider prefix that
|
|
1900
|
+
// is not the lane's is a cross-provider move, which no session survives.
|
|
1901
|
+
const raw = input.account.trim();
|
|
1902
|
+
const colon = raw.indexOf(":");
|
|
1903
|
+
const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
|
|
1904
|
+
const target = colon > 0 ? raw.slice(colon + 1) : raw;
|
|
1905
|
+
if (requestedProvider !== undefined && requestedProvider !== lane.provider) {
|
|
1906
|
+
throw new LanesError("account_provider_mismatch", `Lane ${laneId} runs on ${lane.provider}; account ${JSON.stringify(raw)} names provider ${requestedProvider}. Sessions do not move between providers`, { lane_id: laneId, provider: lane.provider, requested_provider: requestedProvider, account: target });
|
|
1907
|
+
}
|
|
1908
|
+
if (!target) {
|
|
1909
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(input.account)} for ${lane.provider}`, {
|
|
1910
|
+
provider: lane.provider,
|
|
1911
|
+
account: input.account,
|
|
1912
|
+
enabled: [],
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
if (target === lane.account) {
|
|
1916
|
+
throw new LanesError("account_unchanged", `Lane ${laneId} already runs on account ${target}`, {
|
|
1917
|
+
lane_id: laneId,
|
|
1918
|
+
account: target,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
if (!providerSupportsSessionResume(lane.provider)) {
|
|
1922
|
+
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 });
|
|
1923
|
+
}
|
|
1924
|
+
if (!hasRecordedSessionId(lane)) {
|
|
1925
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no recorded provider session id, so there is no session to move`, { lane_id: laneId, provider: lane.provider });
|
|
1926
|
+
}
|
|
1927
|
+
const sessionId = lane.provider_session_id;
|
|
1928
|
+
const config = loadLanesConfig(hqRoot);
|
|
1929
|
+
const providerCfg = config.providers[lane.provider];
|
|
1930
|
+
const targetCfg = providerCfg.accounts[target];
|
|
1931
|
+
if (targetCfg === undefined) {
|
|
1932
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
|
|
1933
|
+
}
|
|
1934
|
+
if (!targetCfg.enabled) {
|
|
1935
|
+
throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
|
|
1936
|
+
}
|
|
1937
|
+
if (isLoggedOut(accountStatus(lane.provider, target, config, { existsSync: ctx.existsSync }))) {
|
|
1938
|
+
throw new LanesError("account_not_logged_in", `Account ${target} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: target, lane_id: laneId });
|
|
1939
|
+
}
|
|
1940
|
+
// The model the moved run would use, resolved the way spawn resolves it:
|
|
1941
|
+
// the operator's --model, else the lane's own, else the target account's
|
|
1942
|
+
// default, else the provider's. The quota and validation checks use this
|
|
1943
|
+
// effective model so a per-model hold on the target's default is seen
|
|
1944
|
+
// here, before anything is copied. A target that pins a different model
|
|
1945
|
+
// refuses the lane's model unless the operator names one for the move.
|
|
1946
|
+
const model = resolveLaneModel({
|
|
1947
|
+
cliModel: input.cliModel ?? lane.model,
|
|
1948
|
+
accountModel: targetCfg.model,
|
|
1949
|
+
providerModel: providerCfg.model,
|
|
1950
|
+
});
|
|
1951
|
+
if (input.cliModel === undefined && lane.model !== undefined && targetCfg.model !== undefined && targetCfg.model !== lane.model) {
|
|
1952
|
+
throw new LanesError("model_unsupported", `Account ${target} pins model ${targetCfg.model}; lane ${laneId} runs ${lane.model}. Pass --model ${targetCfg.model} to move it onto that model`, { provider: lane.provider, account: target, model: lane.model, account_model: targetCfg.model, lane_id: laneId });
|
|
1953
|
+
}
|
|
1954
|
+
const block = quotaBlockFor(loadQuotaState(hqRoot), lane.provider, target, model, now);
|
|
1955
|
+
if (block.blocked) {
|
|
1956
|
+
throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
|
|
1957
|
+
}
|
|
1958
|
+
if (model !== undefined) {
|
|
1959
|
+
validateLaneModel(lane.provider, model, targetCfg.home, {
|
|
1960
|
+
readFileSync: ctx.readFileSync,
|
|
1961
|
+
probe: ctx.probeProvider,
|
|
1962
|
+
env: ctx.env,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
const sourceCfg = lane.account !== undefined ? providerCfg.accounts[lane.account] : undefined;
|
|
1966
|
+
if (sourceCfg === undefined) {
|
|
1967
|
+
throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
1968
|
+
}
|
|
1969
|
+
// Checks done. Copy the session files, then write the record.
|
|
1970
|
+
const copy = copySessionFiles(planSessionCopy(lane.provider, {
|
|
1971
|
+
sourceHome: sourceCfg.home,
|
|
1972
|
+
targetHome: targetCfg.home,
|
|
1973
|
+
cwd: hqRoot,
|
|
1974
|
+
sessionId,
|
|
1975
|
+
}));
|
|
1976
|
+
const at = now.toISOString();
|
|
1977
|
+
const from = lane.account ?? null;
|
|
1978
|
+
// The capacity decision and the record write run under the target
|
|
1979
|
+
// account's admission lock, the same serialization lane creation uses. A
|
|
1980
|
+
// slot is taken by a running lane or by a lane another move has already
|
|
1981
|
+
// pointed at the account whose spawn has not claimed it yet
|
|
1982
|
+
// (`pending_resume` set by this write, cleared by the claim), so two moves
|
|
1983
|
+
// into one free slot cannot both start.
|
|
1984
|
+
return withAdmissionLock(admissionLockPath(hqRoot, lane.provider, target), async () => {
|
|
1985
|
+
const lanes = loadAllLanes(hqRoot);
|
|
1986
|
+
// A queued lane with a pending resume waits for admission and holds
|
|
1987
|
+
// no slot; one in any other state with a pending resume is a move
|
|
1988
|
+
// whose spawn is about to claim, and holds the slot it was granted.
|
|
1989
|
+
const occupies = (row) => row.lane_id !== laneId &&
|
|
1990
|
+
(row.state === "running" || (row.pending_resume !== undefined && row.state !== "queued"));
|
|
1991
|
+
const onProvider = lanes.filter((row) => row.provider === lane.provider && occupies(row));
|
|
1992
|
+
const atCap = onProvider.length >= providerCfg.max_running ||
|
|
1993
|
+
onProvider.filter((row) => row.account === target).length >= accountMaxRunning(providerCfg, target);
|
|
1994
|
+
const moved = casUpdateLane(hqRoot, laneId, (current) => {
|
|
1995
|
+
// The copy and every check above were made from `lane`; a record
|
|
1996
|
+
// that moved on since (another move, a spawn, a cancel) invalidates
|
|
1997
|
+
// them, so the write is refused rather than applied to a state it
|
|
1998
|
+
// was not prepared for.
|
|
1999
|
+
if (current.version !== lane.version) {
|
|
2000
|
+
throw new LanesError("account_move_conflict", `Lane ${laneId} changed while the move was prepared (version ${lane.version} → ${current.version}, state ${current.state}, account ${current.account ?? "(none)"}); run the move again`, { lane_id: laneId, prepared_version: lane.version, version: current.version, state: current.state, account: current.account ?? null });
|
|
2001
|
+
}
|
|
2002
|
+
const next = {
|
|
2003
|
+
...current,
|
|
2004
|
+
account: target,
|
|
2005
|
+
...(input.cliModel !== undefined ? { model: input.cliModel } : {}),
|
|
2006
|
+
account_history: appendCappedHistory(current.account_history ?? [], {
|
|
2007
|
+
from,
|
|
2008
|
+
to: target,
|
|
2009
|
+
at,
|
|
2010
|
+
reason: SESSION_MOVE_REASON,
|
|
2011
|
+
}),
|
|
2012
|
+
pending_resume: { session_id: sessionId, at, ...input.pending },
|
|
2013
|
+
};
|
|
2014
|
+
if (atCap) {
|
|
2015
|
+
next.state = "queued";
|
|
2016
|
+
next.queued_reason = "cap";
|
|
2017
|
+
}
|
|
2018
|
+
return next;
|
|
2019
|
+
});
|
|
2020
|
+
return { lane: moved, from, to: target, copy, queued: atCap };
|
|
2021
|
+
}, ctx.sleep);
|
|
2022
|
+
}
|
|
1754
2023
|
async function runMessage(ctx, laneId, opts) {
|
|
1755
2024
|
const hqRoot = ctx.hqRoot();
|
|
1756
2025
|
const text = opts.text;
|
|
1757
2026
|
if (!text) {
|
|
1758
2027
|
throw new LanesError("missing_text", "--text is required", {});
|
|
1759
2028
|
}
|
|
1760
|
-
|
|
2029
|
+
let lane = readLane(hqRoot, laneId);
|
|
2030
|
+
// An account move is refused on its own terms first: a running lane and a
|
|
2031
|
+
// provider without session resume get the move's named errors, not the
|
|
2032
|
+
// generic delivery refusal below.
|
|
2033
|
+
if (opts.account !== undefined && lane.state === "running") {
|
|
2034
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
2035
|
+
lane_id: laneId,
|
|
2036
|
+
state: lane.state,
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
2040
|
+
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 });
|
|
2041
|
+
}
|
|
1761
2042
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
1762
2043
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
1763
2044
|
const mechanism = laneDeliveryMechanism(lane);
|
|
@@ -1770,6 +2051,9 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
1770
2051
|
message,
|
|
1771
2052
|
});
|
|
1772
2053
|
}
|
|
2054
|
+
if (opts.model !== undefined && opts.account === undefined) {
|
|
2055
|
+
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
2056
|
+
}
|
|
1773
2057
|
const messageId = mintMessageId();
|
|
1774
2058
|
const at = ctx.now().toISOString();
|
|
1775
2059
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
@@ -1782,7 +2066,24 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
1782
2066
|
story_id: storyId,
|
|
1783
2067
|
};
|
|
1784
2068
|
let resumed = null;
|
|
1785
|
-
|
|
2069
|
+
let move = null;
|
|
2070
|
+
if (opts.account !== undefined) {
|
|
2071
|
+
// A moved message is always a resume on the target account: a running
|
|
2072
|
+
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
2073
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
2074
|
+
account: opts.account,
|
|
2075
|
+
cliModel: opts.model?.trim() || undefined,
|
|
2076
|
+
pending: { trigger: "message", text, message_id: messageId, compact: opts.compact !== false },
|
|
2077
|
+
});
|
|
2078
|
+
lane = move.lane;
|
|
2079
|
+
if (move.queued) {
|
|
2080
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (move?.queued) {
|
|
2084
|
+
// Parked at the target's cap: reconcile resumes the session there.
|
|
2085
|
+
}
|
|
2086
|
+
else if (mechanism === "dropbox") {
|
|
1786
2087
|
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
1787
2088
|
if (!laneAcceptsOperatorMessage(lane)) {
|
|
1788
2089
|
throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
|
|
@@ -1841,7 +2142,11 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
1841
2142
|
// started from; `resume` has started a new worker with the message as its
|
|
1842
2143
|
// prompt. Neither is "delivered", so the response never says so.
|
|
1843
2144
|
const drainHook = drainHookStatusFor(ctx, next);
|
|
1844
|
-
const deliveryStatus =
|
|
2145
|
+
const deliveryStatus = move?.queued
|
|
2146
|
+
? "queued_on_target_account"
|
|
2147
|
+
: mechanism === "dropbox"
|
|
2148
|
+
? "awaiting_next_tool_call"
|
|
2149
|
+
: "worker_spawned";
|
|
1845
2150
|
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
1846
2151
|
? `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.`
|
|
1847
2152
|
: mechanism === "dropbox" && next.stalled === true
|
|
@@ -1863,11 +2168,24 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
1863
2168
|
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
1864
2169
|
? resumed.last_compaction
|
|
1865
2170
|
: null,
|
|
2171
|
+
account_move: move ? accountMoveSummary(move) : null,
|
|
1866
2172
|
project_id: next.project_id,
|
|
1867
2173
|
story_id: next.story_id,
|
|
1868
2174
|
lane: next,
|
|
1869
2175
|
});
|
|
1870
2176
|
}
|
|
2177
|
+
function accountMoveSummary(move) {
|
|
2178
|
+
return {
|
|
2179
|
+
from: move.from,
|
|
2180
|
+
to: move.to,
|
|
2181
|
+
queued: move.queued,
|
|
2182
|
+
copied: move.copy.copied,
|
|
2183
|
+
sources: move.copy.sources,
|
|
2184
|
+
targets: move.copy.targets,
|
|
2185
|
+
source_dir: move.copy.source_dir,
|
|
2186
|
+
target_dir: move.copy.target_dir,
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
1871
2189
|
/**
|
|
1872
2190
|
* `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
|
|
1873
2191
|
* continues a lane in `interrupted`, `done` or `blocked` on its recorded
|
|
@@ -1879,7 +2197,19 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
1879
2197
|
*/
|
|
1880
2198
|
async function runResume(ctx, laneId, opts) {
|
|
1881
2199
|
const hqRoot = ctx.hqRoot();
|
|
1882
|
-
|
|
2200
|
+
let lane = readLane(hqRoot, laneId);
|
|
2201
|
+
if (opts.model !== undefined && opts.account === undefined) {
|
|
2202
|
+
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
2203
|
+
}
|
|
2204
|
+
if (opts.account !== undefined && lane.state === "running") {
|
|
2205
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
2206
|
+
lane_id: laneId,
|
|
2207
|
+
state: lane.state,
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
2211
|
+
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 });
|
|
2212
|
+
}
|
|
1883
2213
|
const refusal = resumeCommandRefusal(lane);
|
|
1884
2214
|
if (refusal !== null) {
|
|
1885
2215
|
throw new LanesError(refusal.error, refusal.message, {
|
|
@@ -1896,25 +2226,42 @@ async function runResume(ctx, laneId, opts) {
|
|
|
1896
2226
|
const storyId = opts.story?.trim() || lane.story_id;
|
|
1897
2227
|
const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
|
|
1898
2228
|
const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2229
|
+
let move = null;
|
|
2230
|
+
if (opts.account !== undefined) {
|
|
2231
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
2232
|
+
account: opts.account,
|
|
2233
|
+
cliModel: opts.model?.trim() || undefined,
|
|
2234
|
+
pending: {
|
|
2235
|
+
trigger: "resume",
|
|
2236
|
+
resumed_from: resumedFrom,
|
|
2237
|
+
...(text !== undefined ? { text } : {}),
|
|
2238
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
2239
|
+
compact: opts.compact !== false,
|
|
2240
|
+
},
|
|
2241
|
+
});
|
|
2242
|
+
lane = move.lane;
|
|
2243
|
+
}
|
|
2244
|
+
const resumed = move?.queued
|
|
2245
|
+
? lane
|
|
2246
|
+
: spawnLaneWorker(hqRoot, lane, {
|
|
2247
|
+
env: ctx.env,
|
|
2248
|
+
existsSync: ctx.existsSync,
|
|
2249
|
+
spawnProcess: ctx.spawnProcess,
|
|
2250
|
+
proveDetached: ctx.proveDetached,
|
|
2251
|
+
killProcess: ctx.killProcess,
|
|
2252
|
+
now: ctx.now,
|
|
2253
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
2254
|
+
runCompaction: ctx.runCompaction,
|
|
2255
|
+
locateTranscript: ctx.locateTranscript,
|
|
2256
|
+
}, {
|
|
2257
|
+
trigger: {
|
|
2258
|
+
kind: "resume",
|
|
2259
|
+
from: resumedFrom,
|
|
2260
|
+
...(text !== undefined ? { text } : {}),
|
|
2261
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
2262
|
+
},
|
|
2263
|
+
compact: opts.compact !== false,
|
|
2264
|
+
});
|
|
1918
2265
|
if (text !== undefined && messageId !== undefined) {
|
|
1919
2266
|
// Same audit copy a message resume leaves; nothing drains it. Archive only
|
|
1920
2267
|
// after a successful spawn so a concurrent resume cannot leave a phantom.
|
|
@@ -1961,7 +2308,8 @@ async function runResume(ctx, laneId, opts) {
|
|
|
1961
2308
|
message_id: messageId ?? null,
|
|
1962
2309
|
text: text ?? null,
|
|
1963
2310
|
delivery: "resume",
|
|
1964
|
-
delivery_status: "worker_spawned",
|
|
2311
|
+
delivery_status: move?.queued ? "queued_on_target_account" : "worker_spawned",
|
|
2312
|
+
account_move: move ? accountMoveSummary(move) : null,
|
|
1965
2313
|
resumed_session_id: resumed.last_resume?.session_id ?? null,
|
|
1966
2314
|
last_resume: next.last_resume ?? null,
|
|
1967
2315
|
compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
@@ -2692,7 +3040,7 @@ async function runReconcileInstall(ctx, opts) {
|
|
|
2692
3040
|
hqRoot: ctx.hqRoot(),
|
|
2693
3041
|
enable: Boolean(opts.enable),
|
|
2694
3042
|
runCommand: opts.enable ? ctx.runSystemctl : undefined,
|
|
2695
|
-
|
|
3043
|
+
providerDirs: providerPath.resolved_dirs,
|
|
2696
3044
|
});
|
|
2697
3045
|
printJson({
|
|
2698
3046
|
...result,
|
|
@@ -2781,6 +3129,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
2781
3129
|
.requiredOption("--text <msg>", "Message text")
|
|
2782
3130
|
.option("--project <slug>", "Re-point project attribution from this message")
|
|
2783
3131
|
.option("--story <id>", "Re-point story attribution from this message")
|
|
3132
|
+
.option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
|
|
3133
|
+
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
2784
3134
|
.option("--no-compact", "Skip the resume-time compaction check for this message")
|
|
2785
3135
|
.action(wrap(async (laneId, opts) => {
|
|
2786
3136
|
await runMessage(ctx, laneId, opts);
|
|
@@ -2792,6 +3142,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
2792
3142
|
.option("--text <msg>", "Operator message for the resumed turn")
|
|
2793
3143
|
.option("--project <slug>", "Re-point project attribution from this resume")
|
|
2794
3144
|
.option("--story <id>", "Re-point story attribution from this resume")
|
|
3145
|
+
.option("--account <id>", "Move the lane's session to this account of its provider before resuming")
|
|
3146
|
+
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
2795
3147
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
2796
3148
|
.action(wrap(async (laneId, opts) => {
|
|
2797
3149
|
await runResume(ctx, laneId, opts);
|
package/dist/commands/mesh.js
CHANGED
|
@@ -23,7 +23,8 @@ import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
|
23
23
|
import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
|
|
24
24
|
import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
|
|
25
25
|
const MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
|
|
26
|
-
import { buildEmitEvidence } from "../lib/mesh/live/emit-evidence.js";
|
|
26
|
+
import { buildEmitEvidence, machineEmitDisposition } from "../lib/mesh/live/emit-evidence.js";
|
|
27
|
+
import { appendHeldLine } from "../lib/mesh/live/spool.js";
|
|
27
28
|
import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
|
|
28
29
|
import { emitEvents, formatEmitRejectionLines } from "../lib/mesh/live/emit.js";
|
|
29
30
|
import { readEmitRetry, writeEmitRetry } from "../lib/mesh/live/emit-retry.js";
|
|
@@ -1070,6 +1071,7 @@ async function runSessionEnqueue(cliKind, opts) {
|
|
|
1070
1071
|
companySlug: opts.companySlug,
|
|
1071
1072
|
project: opts.project,
|
|
1072
1073
|
task: opts.task,
|
|
1074
|
+
env: process.env,
|
|
1073
1075
|
});
|
|
1074
1076
|
const event = buildMeshEmitEvent({
|
|
1075
1077
|
eventId: opts.eventId?.trim() || generateUlid(Date.now()),
|
|
@@ -1088,6 +1090,23 @@ async function runSessionEnqueue(cliKind, opts) {
|
|
|
1088
1090
|
evidence,
|
|
1089
1091
|
});
|
|
1090
1092
|
const meshRoot = workMeshHomeRoot();
|
|
1093
|
+
// A machine credential with no company must not be posted. The server files
|
|
1094
|
+
// that as company NONE. Hold it and say why.
|
|
1095
|
+
if (machineEmitDisposition({ machine: isMachineIdentity(), companySlug: evidence.companySlug }) === "hold") {
|
|
1096
|
+
appendHeldLine(JSON.stringify({
|
|
1097
|
+
event,
|
|
1098
|
+
heldReason: "NEEDS_COMPANY",
|
|
1099
|
+
heldAt: new Date().toISOString(),
|
|
1100
|
+
reason: "machine session has no company; event was not sent",
|
|
1101
|
+
}), meshRoot);
|
|
1102
|
+
const heldMessage = `work-mesh: held ${kind}; needs_company (machine session has no company; not sent)`;
|
|
1103
|
+
if (opts.json) {
|
|
1104
|
+
console.log(JSON.stringify({ ok: true, action: "emit", kind, held: true, reason: "needs_company" }, null, 2));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
console.error(heldMessage);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1091
1110
|
// Resolve a token non-interactively (same path as every CLI call). If the box
|
|
1092
1111
|
// is not logged in, retain the event locally for the next invocation to drain.
|
|
1093
1112
|
let token;
|