@indigoai-us/hq-cli 5.154.0 → 5.156.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 +48 -0
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/lanes.d.ts +3 -11
- package/dist/commands/lanes.js +162 -55
- package/dist/commands/mesh.js +35 -8
- package/dist/lib/lanes/accounts.d.ts +16 -11
- package/dist/lib/lanes/accounts.js +50 -8
- package/dist/lib/lanes/admission-host.d.ts +19 -0
- package/dist/lib/lanes/admission-host.js +33 -0
- package/dist/lib/lanes/config.js +25 -0
- package/dist/lib/lanes/mesh-events.d.ts +7 -3
- package/dist/lib/lanes/mesh-events.js +116 -20
- package/dist/lib/lanes/provider-rejection.d.ts +4 -4
- package/dist/lib/lanes/provider-rejection.js +24 -6
- package/dist/lib/lanes/reconcile.js +85 -2
- package/dist/lib/lanes/spawn.d.ts +9 -1
- package/dist/lib/lanes/spawn.js +420 -14
- package/dist/lib/lanes/types.d.ts +7 -3
- package/dist/lib/lanes/types.js +3 -2
- package/dist/lib/lanes/watcher.d.ts +8 -1
- package/dist/lib/lanes/watcher.js +66 -17
- package/dist/lib/mesh/api.d.ts +27 -1
- package/dist/lib/mesh/api.js +41 -12
- package/dist/lib/mesh/project-board.d.ts +1 -0
- package/dist/lib/mesh/project-board.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.156.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Project-thread lookup treats an omitted server `kind` as the project thread,
|
|
10
|
+
preventing duplicate project threads when a newer lane thread is present.
|
|
11
|
+
- Lane admission reservations are refreshed after Work Mesh lane-thread
|
|
12
|
+
registration so slow registration cannot age the reservation before spawn.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `hq mesh project register --channel` and `project set --channel` can bind an
|
|
17
|
+
existing channel to project events. `hq mesh check --project` shows lane
|
|
18
|
+
identity and status, including terminal lane threads.
|
|
19
|
+
- Lane mesh threads keep a stable lane id across resume and account moves.
|
|
20
|
+
Events include the worker profile, model, account, parent lane, and senior
|
|
21
|
+
owner. Stop and dead-host recovery events close terminal threads.
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
|
|
25
|
+
- Cover server-shaped project-thread results and reservation timing after
|
|
26
|
+
lane-thread registration.
|
|
27
|
+
|
|
28
|
+
## [5.155.0] — 2026-09-24
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Lane respawns keep their recorded account to preserve provider sessions and
|
|
33
|
+
prompt caches. Automatic moves now record why they happened and are limited
|
|
34
|
+
to model changes, usage limits, and unavailable or logged-out accounts.
|
|
35
|
+
- Account retries share the provider admission lock and the host-scoped provider,
|
|
36
|
+
account, and profile checks used by normal admission. Active reservations and
|
|
37
|
+
earlier eligible queue entries keep their place; blocked retries remain queued
|
|
38
|
+
for normal admission. Retries skip accounts already rejected for login and
|
|
39
|
+
retain message, resume, and answer context when queued.
|
|
40
|
+
- Watcher retries resolve the current process hostname and Outpost identity, so
|
|
41
|
+
local-kind and Outpost-kind lanes on one Outpost share admission caps.
|
|
42
|
+
- Account enable and registration now require a positive login check. Provider
|
|
43
|
+
startup messages for logged-out accounts trigger failover to another eligible
|
|
44
|
+
account or a named `account_not_logged_in` failure.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- Provider config can list accounts in preference order for a lane's first
|
|
49
|
+
assignment. Disabled, logged-out, usage-limited, and capped accounts are
|
|
50
|
+
skipped. If no preferred account is eligible, hq-cli chooses the least-loaded
|
|
51
|
+
eligible account. Running and resumed lanes keep their recorded account.
|
|
52
|
+
|
|
5
53
|
## [5.154.0] — 2026-09-24
|
|
6
54
|
|
|
7
55
|
### Fixed
|
|
@@ -5708,6 +5708,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5708
5708
|
}, {
|
|
5709
5709
|
readonly flags: "--repo <path[@branch]>";
|
|
5710
5710
|
readonly description: "Repository binding (repeatable)";
|
|
5711
|
+
}, {
|
|
5712
|
+
readonly flags: "--channel <channelId>";
|
|
5713
|
+
readonly description: "Bind the project to an existing company or project channel";
|
|
5711
5714
|
}, {
|
|
5712
5715
|
readonly flags: "--create";
|
|
5713
5716
|
readonly description: "Create the Board view when the GET is a 404";
|
|
@@ -5754,7 +5757,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5754
5757
|
readonly description: "Company slug or cloud uid";
|
|
5755
5758
|
}, {
|
|
5756
5759
|
readonly flags: "--channel <channelId>";
|
|
5757
|
-
readonly description: "
|
|
5760
|
+
readonly description: "Bind the project to an existing company or project channel";
|
|
5758
5761
|
}, {
|
|
5759
5762
|
readonly flags: "--json";
|
|
5760
5763
|
readonly description: "Print machine-readable JSON";
|
|
@@ -7367,6 +7367,10 @@ export const COMMAND_CATALOG = [
|
|
|
7367
7367
|
"flags": "--repo <path[@branch]>",
|
|
7368
7368
|
"description": "Repository binding (repeatable)"
|
|
7369
7369
|
},
|
|
7370
|
+
{
|
|
7371
|
+
"flags": "--channel <channelId>",
|
|
7372
|
+
"description": "Bind the project to an existing company or project channel"
|
|
7373
|
+
},
|
|
7370
7374
|
{
|
|
7371
7375
|
"flags": "--create",
|
|
7372
7376
|
"description": "Create the Board view when the GET is a 404"
|
|
@@ -7427,7 +7431,7 @@ export const COMMAND_CATALOG = [
|
|
|
7427
7431
|
},
|
|
7428
7432
|
{
|
|
7429
7433
|
"flags": "--channel <channelId>",
|
|
7430
|
-
"description": "
|
|
7434
|
+
"description": "Bind the project to an existing company or project channel"
|
|
7431
7435
|
},
|
|
7432
7436
|
{
|
|
7433
7437
|
"flags": "--json",
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* `watch` and the monitor reminder are the line-oriented text surfaces.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
+
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
10
|
+
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
11
|
+
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
9
12
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
10
13
|
import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
|
|
11
14
|
import { MAX_LANE_DEPTH, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
@@ -16,17 +19,7 @@ import { spawnLaneWorker, type KillProcessFn, type ProveDetachedFn, type SpawnPr
|
|
|
16
19
|
export { spawnLaneWorker };
|
|
17
20
|
export { questionBodyAbsPath };
|
|
18
21
|
export { MAX_LANE_DEPTH };
|
|
19
|
-
export declare const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
20
|
-
export declare const OUTPOST_DIR = "/etc/outpost";
|
|
21
22
|
export declare const LANES_RUNS_REL: string;
|
|
22
|
-
export type OutpostIdentity = {
|
|
23
|
-
kind: "id";
|
|
24
|
-
id: string;
|
|
25
|
-
} | {
|
|
26
|
-
kind: "unknown";
|
|
27
|
-
} | {
|
|
28
|
-
kind: "not_outpost";
|
|
29
|
-
};
|
|
30
23
|
export interface SpawnLoginResult {
|
|
31
24
|
status: number;
|
|
32
25
|
}
|
|
@@ -79,7 +72,6 @@ export interface LanesCommandDeps {
|
|
|
79
72
|
locateTranscript?: (home: string, sessionId: string) => string | null;
|
|
80
73
|
monitorEvidence?: MonitorEvidenceReader;
|
|
81
74
|
}
|
|
82
|
-
export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
|
|
83
75
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
84
76
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
85
77
|
/**
|
package/dist/commands/lanes.js
CHANGED
|
@@ -12,13 +12,15 @@ import * as os from "node:os";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
14
14
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
15
|
+
import { readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
16
|
+
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
15
17
|
import { buildOutpostLaneCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
16
18
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
17
19
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
18
20
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
19
|
-
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
21
|
+
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
20
22
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
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,
|
|
23
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
22
24
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
23
25
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
24
26
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -33,8 +35,6 @@ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ON
|
|
|
33
35
|
export { spawnLaneWorker };
|
|
34
36
|
export { questionBodyAbsPath };
|
|
35
37
|
export { MAX_LANE_DEPTH };
|
|
36
|
-
export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
37
|
-
export const OUTPOST_DIR = "/etc/outpost";
|
|
38
38
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
39
39
|
const VENDOR_LOGIN = {
|
|
40
40
|
claude: { command: "claude", args: ["auth", "login"] },
|
|
@@ -133,27 +133,6 @@ async function defaultFetchText(url) {
|
|
|
133
133
|
}
|
|
134
134
|
return res.text();
|
|
135
135
|
}
|
|
136
|
-
export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
|
|
137
|
-
const hasMarker = existsSync(OUTPOST_ID_PATH);
|
|
138
|
-
const hasDir = existsSync(OUTPOST_DIR);
|
|
139
|
-
if (hasMarker) {
|
|
140
|
-
let text;
|
|
141
|
-
try {
|
|
142
|
-
text = readFileSync(OUTPOST_ID_PATH);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
process.stderr.write(`hq lanes: failed to read ${OUTPOST_ID_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
const id = text.trim();
|
|
149
|
-
if (!id)
|
|
150
|
-
return hasDir ? { kind: "unknown" } : { kind: "not_outpost" };
|
|
151
|
-
return { kind: "id", id };
|
|
152
|
-
}
|
|
153
|
-
if (hasDir)
|
|
154
|
-
return { kind: "unknown" };
|
|
155
|
-
return { kind: "not_outpost" };
|
|
156
|
-
}
|
|
157
136
|
function defaultSpawnLogin(opts) {
|
|
158
137
|
return new Promise((resolve, reject) => {
|
|
159
138
|
const child = spawn(opts.command, opts.args, {
|
|
@@ -263,11 +242,10 @@ function livenessDepsFromCtx(ctx) {
|
|
|
263
242
|
};
|
|
264
243
|
}
|
|
265
244
|
function reconcileHostFromCtx(ctx) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
245
|
+
return resolveAdmissionHost({
|
|
246
|
+
hostname: ctx.hostname,
|
|
247
|
+
readOutpostIdentity: ctx.readOutpostIdentity,
|
|
248
|
+
});
|
|
271
249
|
}
|
|
272
250
|
export function parseSeniorRef(raw) {
|
|
273
251
|
const trimmed = raw.trim();
|
|
@@ -1172,18 +1150,45 @@ function askDeliveryFailedPath(hqRoot, laneId) {
|
|
|
1172
1150
|
return path.join(runDir(hqRoot, laneId), "ask_delivery_failed.json");
|
|
1173
1151
|
}
|
|
1174
1152
|
async function emitLaneMeshEvent(ctx, company, threadId, event, payload) {
|
|
1175
|
-
if (!
|
|
1153
|
+
if (!company)
|
|
1176
1154
|
return;
|
|
1177
|
-
|
|
1155
|
+
let targetThreadId = threadId;
|
|
1156
|
+
let eventPayload = payload;
|
|
1157
|
+
const laneId = typeof payload.lane_id === "string" ? payload.lane_id.trim() : "";
|
|
1158
|
+
if (laneId && ctx.mesh.ensureLaneThread) {
|
|
1159
|
+
const lane = readLane(ctx.hqRoot(), laneId);
|
|
1160
|
+
const ready = await ensureLaneThreadRecord(ctx, lane, lane.project_id, lane.story_id, "running");
|
|
1161
|
+
targetThreadId = ready.thread_id;
|
|
1162
|
+
eventPayload = { ...laneMeshEventIdentity(ready), ...payload };
|
|
1163
|
+
}
|
|
1164
|
+
if (!targetThreadId)
|
|
1165
|
+
return;
|
|
1166
|
+
await ctx.mesh.appendThreadEvent(company, targetThreadId, event, eventPayload);
|
|
1167
|
+
}
|
|
1168
|
+
async function emitLaneTerminalStateSoft(ctx, lane) {
|
|
1169
|
+
if (lane.state !== "failed" && lane.state !== "interrupted" &&
|
|
1170
|
+
lane.state !== "cancelled" && lane.state !== "killed")
|
|
1171
|
+
return;
|
|
1172
|
+
try {
|
|
1173
|
+
await emitLaneTerminalStateEvent(ctx.mesh, lane, lane.state, lane.terminal_reason, AbortSignal.timeout(10_000));
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
process.stderr.write(`hq lanes: terminal mesh update failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1177
|
+
}
|
|
1178
1178
|
}
|
|
1179
1179
|
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
1180
|
-
|
|
1180
|
+
const ready = ctx.mesh.ensureLaneThread
|
|
1181
|
+
? await ensureLaneThreadRecord(ctx, lane, lane.project_id, lane.story_id, "running")
|
|
1182
|
+
: lane;
|
|
1183
|
+
const stableThreadId = ready.thread_id ?? threadId;
|
|
1184
|
+
if (!stableThreadId)
|
|
1181
1185
|
return;
|
|
1182
1186
|
const failures = [];
|
|
1183
|
-
for (const story of laneStories(
|
|
1187
|
+
for (const story of laneStories(ready)) {
|
|
1184
1188
|
try {
|
|
1185
|
-
await emitLaneMeshEvent(ctx, company,
|
|
1186
|
-
|
|
1189
|
+
await emitLaneMeshEvent(ctx, company, stableThreadId, "lane_started", {
|
|
1190
|
+
...laneMeshEventIdentity(ready),
|
|
1191
|
+
lane_id: ready.lane_id,
|
|
1187
1192
|
project_id: story.project_id,
|
|
1188
1193
|
story_id: story.story_id,
|
|
1189
1194
|
});
|
|
@@ -1261,9 +1266,17 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
1261
1266
|
try {
|
|
1262
1267
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
1263
1268
|
hqRoot,
|
|
1269
|
+
accountDeps: {
|
|
1270
|
+
probe: modelDeps.probe,
|
|
1271
|
+
env: modelDeps.env,
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
1275
|
+
existsSync: (p) => fs.existsSync(p),
|
|
1276
|
+
probe: modelDeps.probe,
|
|
1277
|
+
env: modelDeps.env,
|
|
1264
1278
|
});
|
|
1265
|
-
|
|
1266
|
-
if (isLoggedOut(status)) {
|
|
1279
|
+
if (status.logged_in !== true) {
|
|
1267
1280
|
return markPickupFailure(hqRoot, laneId, "account_not_logged_in", {
|
|
1268
1281
|
provider: lane.provider,
|
|
1269
1282
|
account: resolved.id,
|
|
@@ -1549,8 +1562,10 @@ async function runCreate(ctx, opts) {
|
|
|
1549
1562
|
resolveAccount(provider, undefined, config, {
|
|
1550
1563
|
hqRoot,
|
|
1551
1564
|
existsSync: ctx.existsSync,
|
|
1565
|
+
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
1552
1566
|
cliModel,
|
|
1553
1567
|
model: modelToValidate,
|
|
1568
|
+
preferredAccounts: config.providers[provider].preferred_accounts ?? [],
|
|
1554
1569
|
...(profileAccounts !== undefined
|
|
1555
1570
|
? { allowedAccounts: profileAccounts }
|
|
1556
1571
|
: {}),
|
|
@@ -1563,7 +1578,11 @@ async function runCreate(ctx, opts) {
|
|
|
1563
1578
|
.filter((id) => id !== candidate &&
|
|
1564
1579
|
accounts[id]?.enabled &&
|
|
1565
1580
|
(profileAccounts === undefined || profileAccounts.includes(id)) &&
|
|
1566
|
-
accountStatus(provider, id, config, {
|
|
1581
|
+
accountStatus(provider, id, config, {
|
|
1582
|
+
existsSync: ctx.existsSync,
|
|
1583
|
+
probe: ctx.probeProvider,
|
|
1584
|
+
env: ctx.env,
|
|
1585
|
+
}).logged_in === true)
|
|
1567
1586
|
.sort(),
|
|
1568
1587
|
];
|
|
1569
1588
|
const checkedAccounts = [];
|
|
@@ -1638,9 +1657,8 @@ async function runCreate(ctx, opts) {
|
|
|
1638
1657
|
(profileAccounts === undefined || profileAccounts.includes(id)))
|
|
1639
1658
|
.map(([id]) => id))]
|
|
1640
1659
|
: undefined;
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
: undefined;
|
|
1660
|
+
// The lane id is created in `persist`; its Work Mesh thread is ensured
|
|
1661
|
+
// immediately afterward from that stable identity.
|
|
1644
1662
|
const persist = async () => {
|
|
1645
1663
|
let admission;
|
|
1646
1664
|
let validation;
|
|
@@ -1648,8 +1666,10 @@ async function runCreate(ctx, opts) {
|
|
|
1648
1666
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
1649
1667
|
hqRoot,
|
|
1650
1668
|
existsSync: ctx.existsSync,
|
|
1669
|
+
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
1651
1670
|
cliModel,
|
|
1652
1671
|
model: cliModel ?? profileModel,
|
|
1672
|
+
preferredAccounts: config.providers[provider].preferred_accounts ?? [],
|
|
1653
1673
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
1654
1674
|
}).id;
|
|
1655
1675
|
}
|
|
@@ -1718,7 +1738,6 @@ async function runCreate(ctx, opts) {
|
|
|
1718
1738
|
added_at: createdAt,
|
|
1719
1739
|
updated_at: createdAt,
|
|
1720
1740
|
})),
|
|
1721
|
-
thread_id: threadId,
|
|
1722
1741
|
depth: laneDepth,
|
|
1723
1742
|
senior,
|
|
1724
1743
|
senior_history: [],
|
|
@@ -1771,7 +1790,35 @@ async function runCreate(ctx, opts) {
|
|
|
1771
1790
|
else {
|
|
1772
1791
|
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
1773
1792
|
}
|
|
1774
|
-
|
|
1793
|
+
let lane = persisted.lane;
|
|
1794
|
+
const { admission, validation } = persisted;
|
|
1795
|
+
if (meshCompany) {
|
|
1796
|
+
try {
|
|
1797
|
+
lane = await ensureLaneThreadRecord(ctx, lane);
|
|
1798
|
+
}
|
|
1799
|
+
catch {
|
|
1800
|
+
try {
|
|
1801
|
+
lane = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
1802
|
+
...current,
|
|
1803
|
+
state: "failed",
|
|
1804
|
+
queued_reason: undefined,
|
|
1805
|
+
terminal_reason: "mesh_thread_registration_failed",
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed for ${lane.lane_id}, and its local record could not be marked terminal.`, { lane_id: lane.lane_id });
|
|
1810
|
+
}
|
|
1811
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed; lane ${lane.lane_id} was marked failed.`, { lane_id: lane.lane_id, state: lane.state });
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// Thread registration runs after persistence; anchor the reservation after
|
|
1815
|
+
// its network work so a slow response cannot age the slot before spawn.
|
|
1816
|
+
if (meshCompany && admission.action === "start" && lane.admission_reserved_at) {
|
|
1817
|
+
lane = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
1818
|
+
...current,
|
|
1819
|
+
admission_reserved_at: ctx.now().toISOString(),
|
|
1820
|
+
}));
|
|
1821
|
+
}
|
|
1775
1822
|
const dest = runDir(hqRoot, lane.lane_id);
|
|
1776
1823
|
ensureDir(path.join(dest, "logs"));
|
|
1777
1824
|
ensureDir(path.join(dest, "envelopes"));
|
|
@@ -1797,6 +1844,7 @@ async function runCreate(ctx, opts) {
|
|
|
1797
1844
|
probe: ctx.probeProvider,
|
|
1798
1845
|
env: ctx.env,
|
|
1799
1846
|
},
|
|
1847
|
+
currentModelValidated: !outpostId && modelToValidate !== undefined,
|
|
1800
1848
|
existsSync: ctx.existsSync,
|
|
1801
1849
|
spawnProcess: ctx.spawnProcess,
|
|
1802
1850
|
proveDetached: ctx.proveDetached,
|
|
@@ -1807,7 +1855,7 @@ async function runCreate(ctx, opts) {
|
|
|
1807
1855
|
locateTranscript: ctx.locateTranscript,
|
|
1808
1856
|
});
|
|
1809
1857
|
if (meshCompany)
|
|
1810
|
-
await emitLaneStartedForStories(ctx, meshCompany, started,
|
|
1858
|
+
await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id);
|
|
1811
1859
|
}
|
|
1812
1860
|
else if (outpostId) {
|
|
1813
1861
|
started = await dispatchCreatedOutpostLane(ctx, {
|
|
@@ -1869,13 +1917,15 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1869
1917
|
const dispatched = await ctx.dispatchOutpostExec({ outpostId, command });
|
|
1870
1918
|
const remote = parseRemotePickupJson(dispatched.stdout);
|
|
1871
1919
|
if (remote?.error === "outpost_self_target") {
|
|
1872
|
-
|
|
1920
|
+
const cancelled = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
1873
1921
|
...current,
|
|
1874
1922
|
state: "cancelled",
|
|
1875
1923
|
queued_reason: undefined,
|
|
1876
1924
|
terminal_reason: "outpost_self_target",
|
|
1877
1925
|
outpost_command_id: dispatched.commandId,
|
|
1878
1926
|
}));
|
|
1927
|
+
await emitLaneTerminalStateSoft(ctx, cancelled);
|
|
1928
|
+
return cancelled;
|
|
1879
1929
|
}
|
|
1880
1930
|
const stdoutTrimmed = dispatched.stdout.trim();
|
|
1881
1931
|
if (remote === null && stdoutTrimmed !== "" && dispatched.status !== "Success") {
|
|
@@ -1887,6 +1937,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1887
1937
|
stderr: dispatched.stderr.slice(0, 2000),
|
|
1888
1938
|
parse_error: "unparseable_pickup_stdout",
|
|
1889
1939
|
});
|
|
1940
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1890
1941
|
throw new LanesError(DISPATCH_TERMINAL_REASON, "Outpost pickup command failed", {
|
|
1891
1942
|
outpost_id: outpostId,
|
|
1892
1943
|
lane_id: failed.lane_id,
|
|
@@ -1904,6 +1955,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1904
1955
|
status: dispatched.status,
|
|
1905
1956
|
stderr: dispatched.stderr.slice(0, 2000),
|
|
1906
1957
|
});
|
|
1958
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1907
1959
|
throw new LanesError(DISPATCH_TERMINAL_REASON, "Outpost pickup command failed", {
|
|
1908
1960
|
outpost_id: outpostId,
|
|
1909
1961
|
lane_id: failed.lane_id,
|
|
@@ -1928,6 +1980,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1928
1980
|
outpost_id: outpostId,
|
|
1929
1981
|
message,
|
|
1930
1982
|
});
|
|
1983
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1931
1984
|
throw new LanesError(DISPATCH_TERMINAL_REASON, `Failed to dispatch lane to Outpost ${outpostId}: ${message}`, {
|
|
1932
1985
|
outpost_id: outpostId,
|
|
1933
1986
|
lane_id: failed.lane_id,
|
|
@@ -2131,6 +2184,7 @@ async function runPickup(ctx, opts) {
|
|
|
2131
2184
|
env: ctx.env,
|
|
2132
2185
|
});
|
|
2133
2186
|
if (isTerminalLaneState(afterAccount.state)) {
|
|
2187
|
+
await emitLaneTerminalStateSoft(ctx, afterAccount);
|
|
2134
2188
|
printJson({
|
|
2135
2189
|
ok: true,
|
|
2136
2190
|
action: "pickup",
|
|
@@ -2196,6 +2250,7 @@ async function runPickup(ctx, opts) {
|
|
|
2196
2250
|
probe: ctx.probeProvider,
|
|
2197
2251
|
env: ctx.env,
|
|
2198
2252
|
},
|
|
2253
|
+
currentModelValidated: afterAccount.model !== undefined,
|
|
2199
2254
|
existsSync: ctx.existsSync,
|
|
2200
2255
|
spawnProcess: ctx.spawnProcess,
|
|
2201
2256
|
proveDetached: ctx.proveDetached,
|
|
@@ -2512,7 +2567,11 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2512
2567
|
if (!targetCfg.enabled) {
|
|
2513
2568
|
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 ?? "" });
|
|
2514
2569
|
}
|
|
2515
|
-
if (
|
|
2570
|
+
if (accountStatus(lane.provider, target, config, {
|
|
2571
|
+
existsSync: ctx.existsSync,
|
|
2572
|
+
probe: ctx.probeProvider,
|
|
2573
|
+
env: ctx.env,
|
|
2574
|
+
}).logged_in !== true) {
|
|
2516
2575
|
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 });
|
|
2517
2576
|
}
|
|
2518
2577
|
const profile = lane.worker_profile !== undefined
|
|
@@ -3150,17 +3209,32 @@ function callerMayVerifyStory(ctx, lane) {
|
|
|
3150
3209
|
return false;
|
|
3151
3210
|
return sessionIdFromEnv(ctx.env) === lane.senior.id;
|
|
3152
3211
|
}
|
|
3153
|
-
async function ensureStoryThread(ctx, lane, projectId) {
|
|
3212
|
+
async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
|
|
3213
|
+
const company = lane.mesh_company ?? lane.company;
|
|
3214
|
+
if (!company)
|
|
3215
|
+
return lane;
|
|
3216
|
+
return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
|
|
3217
|
+
}
|
|
3218
|
+
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
3154
3219
|
const company = lane.mesh_company ?? lane.company;
|
|
3155
3220
|
if (!company)
|
|
3156
3221
|
return lane;
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3222
|
+
const descriptorLane = { ...lane, project_id: projectId, story_id: storyId };
|
|
3223
|
+
const threadId = ctx.mesh.ensureLaneThread
|
|
3224
|
+
? await ctx.mesh.ensureLaneThread(company, descriptorLane, laneStatus)
|
|
3225
|
+
: await ctx.mesh.ensureProjectThread(company, projectId);
|
|
3226
|
+
if (threadId === lane.thread_id && !lane.thread_id.startsWith("proj-"))
|
|
3159
3227
|
return lane;
|
|
3160
|
-
|
|
3228
|
+
const updated = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
3161
3229
|
...current,
|
|
3162
3230
|
thread_id: threadId,
|
|
3163
3231
|
}));
|
|
3232
|
+
// The CAS changes the lane version. Refresh once so the server descriptor
|
|
3233
|
+
// and the durable local record converge before the worker starts.
|
|
3234
|
+
if (ctx.mesh.ensureLaneThread) {
|
|
3235
|
+
await ctx.mesh.ensureLaneThread(company, { ...updated, project_id: projectId, story_id: storyId }, laneStatus);
|
|
3236
|
+
}
|
|
3237
|
+
return updated;
|
|
3164
3238
|
}
|
|
3165
3239
|
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
|
|
3166
3240
|
const company = lane.mesh_company ?? lane.company;
|
|
@@ -3174,6 +3248,7 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
3174
3248
|
project_id: projectId,
|
|
3175
3249
|
story_id: storyId,
|
|
3176
3250
|
...payload,
|
|
3251
|
+
lane_status: lane.state,
|
|
3177
3252
|
});
|
|
3178
3253
|
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
3179
3254
|
}
|
|
@@ -3514,6 +3589,7 @@ async function runStop(ctx, laneId) {
|
|
|
3514
3589
|
sleepMs: ctx.stopSleepMs,
|
|
3515
3590
|
});
|
|
3516
3591
|
}
|
|
3592
|
+
await emitLaneTerminalStateSoft(ctx, updated);
|
|
3517
3593
|
const cancelled = cancelPendingQuestionsForLane(hqRoot, laneId, at);
|
|
3518
3594
|
printJson({
|
|
3519
3595
|
ok: true,
|
|
@@ -3829,7 +3905,11 @@ async function runAccountsList(ctx, opts) {
|
|
|
3829
3905
|
// model and effort are what a spawn on each account would start from
|
|
3830
3906
|
// (spec: --model/--effort, then the account, then the provider).
|
|
3831
3907
|
const quota = loadQuotaState(hqRoot);
|
|
3832
|
-
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config
|
|
3908
|
+
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
3909
|
+
existsSync: ctx.existsSync,
|
|
3910
|
+
probe: ctx.probeProvider,
|
|
3911
|
+
env: ctx.env,
|
|
3912
|
+
}).map((status) => {
|
|
3833
3913
|
const accountCfg = config.providers[provider].accounts[status.id];
|
|
3834
3914
|
const model = resolveLaneModel({
|
|
3835
3915
|
accountModel: accountCfg?.model,
|
|
@@ -3930,8 +4010,17 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
3930
4010
|
home,
|
|
3931
4011
|
enabled: true,
|
|
3932
4012
|
};
|
|
4013
|
+
const status = accountStatus(provider, id, config, {
|
|
4014
|
+
existsSync: ctx.existsSync,
|
|
4015
|
+
probe: ctx.probeProvider,
|
|
4016
|
+
env: ctx.env,
|
|
4017
|
+
});
|
|
4018
|
+
if (status.logged_in !== true) {
|
|
4019
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
4020
|
+
? `Account ${id} for ${provider} is not logged in`
|
|
4021
|
+
: `Could not verify that account ${id} for ${provider} is logged in`, { provider, account: id, home });
|
|
4022
|
+
}
|
|
3933
4023
|
writeLanesConfig(hqRoot, config);
|
|
3934
|
-
const status = accountStatus(provider, id, loadLanesConfig(hqRoot));
|
|
3935
4024
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
3936
4025
|
}
|
|
3937
4026
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
@@ -3948,13 +4037,27 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
3948
4037
|
.map(([id]) => id),
|
|
3949
4038
|
});
|
|
3950
4039
|
}
|
|
4040
|
+
const status = accountStatus(provider, accountId, config, {
|
|
4041
|
+
existsSync: ctx.existsSync,
|
|
4042
|
+
probe: ctx.probeProvider,
|
|
4043
|
+
env: ctx.env,
|
|
4044
|
+
});
|
|
4045
|
+
if (status.logged_in !== true) {
|
|
4046
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
4047
|
+
? `Account ${accountId} for ${provider} is not logged in`
|
|
4048
|
+
: `Could not verify that account ${accountId} for ${provider} is logged in`, { provider, account: accountId });
|
|
4049
|
+
}
|
|
3951
4050
|
account.enabled = true;
|
|
3952
4051
|
delete account.disabled_reason;
|
|
3953
4052
|
writeLanesConfig(hqRoot, config);
|
|
3954
4053
|
printJson({
|
|
3955
4054
|
ok: true,
|
|
3956
4055
|
action: "accounts.enable",
|
|
3957
|
-
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot)
|
|
4056
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
4057
|
+
existsSync: ctx.existsSync,
|
|
4058
|
+
probe: ctx.probeProvider,
|
|
4059
|
+
env: ctx.env,
|
|
4060
|
+
}),
|
|
3958
4061
|
});
|
|
3959
4062
|
}
|
|
3960
4063
|
async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
@@ -3978,7 +4081,11 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
|
3978
4081
|
printJson({
|
|
3979
4082
|
ok: true,
|
|
3980
4083
|
action: "accounts.disable",
|
|
3981
|
-
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot)
|
|
4084
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
4085
|
+
existsSync: ctx.existsSync,
|
|
4086
|
+
probe: ctx.probeProvider,
|
|
4087
|
+
env: ctx.env,
|
|
4088
|
+
}),
|
|
3982
4089
|
});
|
|
3983
4090
|
}
|
|
3984
4091
|
async function runSessionsList(ctx, opts) {
|