@indigoai-us/hq-cli 5.222.0 → 5.224.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 +24 -1
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +31 -0
- package/dist/commands/lanes-import.d.ts +15 -0
- package/dist/commands/lanes-import.js +1213 -0
- package/dist/commands/lanes.js +164 -50
- package/dist/lib/daemon/hq-cloud.d.ts +2 -0
- package/dist/lib/daemon/hq-cloud.js +7 -1
- package/dist/lib/daemon/run.js +8 -0
- package/dist/lib/lanes/reconcile.js +1 -1
- package/dist/lib/lanes/spawn.js +62 -11
- package/dist/lib/lanes/types.d.ts +3 -3
- package/dist/lib/lanes/watch.d.ts +18 -1
- package/dist/lib/lanes/watch.js +122 -2
- package/package.json +1 -1
|
@@ -1,16 +1,33 @@
|
|
|
1
|
-
import { type LaneRecord, type QuestionRecord } from "./types.js";
|
|
1
|
+
import { type LaneHost, type LaneProvider, type LaneRecord, type QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const DEFAULT_LANE_WATCH_INTERVAL_MS = 5000;
|
|
3
3
|
export declare const MAX_LANE_WATCH_INTERVAL_MS = 30000;
|
|
4
4
|
export declare const MAX_WATCHED_LANES = 64;
|
|
5
5
|
export declare const MAX_WATCH_OUTPUT_LINES = 512;
|
|
6
|
+
export interface LaneWatchAccountStatus {
|
|
7
|
+
lane_id: string;
|
|
8
|
+
provider: LaneProvider;
|
|
9
|
+
account: string;
|
|
10
|
+
host: LaneHost;
|
|
11
|
+
logged_in: boolean | null;
|
|
12
|
+
}
|
|
13
|
+
export interface LaneAccountLoggedOutEvent {
|
|
14
|
+
event: "account_logged_out";
|
|
15
|
+
provider: LaneProvider;
|
|
16
|
+
account: string;
|
|
17
|
+
host: LaneHost;
|
|
18
|
+
lane_ids: string[];
|
|
19
|
+
}
|
|
6
20
|
export interface LaneWatchDeps {
|
|
7
21
|
readLane?: (hqRoot: string, laneId: string) => LaneRecord | null;
|
|
8
22
|
listQuestionIds?: (hqRoot: string) => string[];
|
|
9
23
|
readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
|
|
24
|
+
readAccountStatuses?: (lanes: readonly LaneRecord[]) => Promise<readonly LaneWatchAccountStatus[]>;
|
|
25
|
+
keepAliveEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
|
|
10
26
|
sleep?: (ms: number) => Promise<void>;
|
|
11
27
|
intervalMs?: number;
|
|
12
28
|
maxOutputLines?: number;
|
|
13
29
|
writeLine?: (line: string) => void;
|
|
30
|
+
writeAccountEvent?: (event: LaneAccountLoggedOutEvent) => void;
|
|
14
31
|
writeError?: (line: string) => void;
|
|
15
32
|
}
|
|
16
33
|
export interface LaneWatchResult {
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
2
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { providerSupportsSessionResume } from "./delivery.js";
|
|
4
|
+
import { LEAD_LANE_KEEP_ALIVE_MAX_RESUMES } from "./keep-alive.js";
|
|
5
|
+
import { isTerminalLaneState, } from "./types.js";
|
|
4
6
|
export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
|
|
5
7
|
export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
|
|
6
8
|
export const MAX_WATCHED_LANES = 64;
|
|
@@ -30,6 +32,30 @@ function validateLaneIds(laneIds) {
|
|
|
30
32
|
}
|
|
31
33
|
return unique;
|
|
32
34
|
}
|
|
35
|
+
function canResumeKeepAliveLane(lane) {
|
|
36
|
+
if (lane.keep_alive !== true ||
|
|
37
|
+
lane.keep_alive_opted_in_at === undefined ||
|
|
38
|
+
!Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
|
|
39
|
+
lane.keep_alive_exhausted_at !== undefined ||
|
|
40
|
+
(lane.keep_alive_resume_count ?? 0) >= LEAD_LANE_KEEP_ALIVE_MAX_RESUMES ||
|
|
41
|
+
lane.keep_alive_blocked_reason === "retired" ||
|
|
42
|
+
lane.keep_alive_blocked_reason === "superseded" ||
|
|
43
|
+
lane.keep_alive_blocked_reason === "retired_list_invalid" ||
|
|
44
|
+
lane.keep_alive_blocked_reason === "terminal_state" ||
|
|
45
|
+
!providerSupportsSessionResume(lane.provider) ||
|
|
46
|
+
!(lane.provider_session_id ?? lane.last_resume?.session_id))
|
|
47
|
+
return false;
|
|
48
|
+
if (lane.state === "interrupted") {
|
|
49
|
+
return lane.terminal_reason === "exited_with_partial_output";
|
|
50
|
+
}
|
|
51
|
+
if (lane.state === "done") {
|
|
52
|
+
return lane.last_envelope?.decision === "done" &&
|
|
53
|
+
lane.story_complete !== true &&
|
|
54
|
+
lane.terminal_reason !== "provider_rejected" &&
|
|
55
|
+
lane.terminal_reason !== "account_not_logged_in";
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
33
59
|
export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
34
60
|
const watchedIds = validateLaneIds(laneIds);
|
|
35
61
|
const readLaneRecord = deps.readLane ?? readLane;
|
|
@@ -37,11 +63,15 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
37
63
|
const readQuestionRecord = deps.readQuestion ?? readQuestion;
|
|
38
64
|
const sleep = deps.sleep ?? defaultSleep;
|
|
39
65
|
const writeLine = deps.writeLine ?? ((line) => console.log(line));
|
|
66
|
+
const writeAccountEvent = deps.writeAccountEvent ?? ((event) => {
|
|
67
|
+
writeLine(`account_logged_out provider=${event.provider} account=${event.account} host=${event.host.kind}:${event.host.id} lane_ids=${event.lane_ids.join(",")}`);
|
|
68
|
+
});
|
|
40
69
|
const writeError = deps.writeError ?? ((line) => process.stderr.write(`${line}\n`));
|
|
41
70
|
const maxOutputLines = Math.max(1, Math.floor(deps.maxOutputLines ?? MAX_WATCH_OUTPUT_LINES));
|
|
42
71
|
const intervalMs = boundedInterval(deps.intervalMs);
|
|
43
72
|
const previousStates = new Map();
|
|
44
73
|
const previousQuestionStatuses = new Map();
|
|
74
|
+
const previousAccountLogin = new Map();
|
|
45
75
|
let polls = 0;
|
|
46
76
|
let lines = 0;
|
|
47
77
|
let suppressed = false;
|
|
@@ -57,6 +87,18 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
57
87
|
suppressed = true;
|
|
58
88
|
}
|
|
59
89
|
};
|
|
90
|
+
const emitAccountEvent = (event) => {
|
|
91
|
+
if (lines < maxOutputLines - 1) {
|
|
92
|
+
writeAccountEvent(event);
|
|
93
|
+
lines += 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!suppressed) {
|
|
97
|
+
writeLine("lane watch: output limit reached; further events suppressed");
|
|
98
|
+
lines = maxOutputLines;
|
|
99
|
+
suppressed = true;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
60
102
|
const unreadable = (kind, id, err) => {
|
|
61
103
|
const message = `${kind} ${id}: unreadable record (${readableError(err)})`;
|
|
62
104
|
writeError(`lane watch: ${message}`);
|
|
@@ -87,6 +129,15 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
87
129
|
previousStates.set(laneId, record.state);
|
|
88
130
|
}
|
|
89
131
|
}
|
|
132
|
+
const keepAliveWaiting = new Set();
|
|
133
|
+
if (deps.keepAliveEnabled !== undefined) {
|
|
134
|
+
for (const record of records) {
|
|
135
|
+
if (isTerminalLaneState(record.state) &&
|
|
136
|
+
canResumeKeepAliveLane(record) &&
|
|
137
|
+
await deps.keepAliveEnabled(record))
|
|
138
|
+
keepAliveWaiting.add(record.lane_id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
90
141
|
let questionIds = [];
|
|
91
142
|
try {
|
|
92
143
|
questionIds = listQuestions(hqRoot);
|
|
@@ -113,7 +164,76 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
113
164
|
}
|
|
114
165
|
previousQuestionStatuses.set(questionId, record.status);
|
|
115
166
|
}
|
|
116
|
-
if (
|
|
167
|
+
if (deps.readAccountStatuses !== undefined) {
|
|
168
|
+
const accountLanes = records.filter((record) => record.account !== undefined &&
|
|
169
|
+
(!isTerminalLaneState(record.state) || keepAliveWaiting.has(record.lane_id)));
|
|
170
|
+
let statuses;
|
|
171
|
+
try {
|
|
172
|
+
statuses = await deps.readAccountStatuses(accountLanes);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const message = `account login status: ${readableError(err)}`;
|
|
176
|
+
writeError(`lane watch: ${message}`);
|
|
177
|
+
throw new LanesError("watch_account_status_unavailable", message, {
|
|
178
|
+
cause: readableError(err),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const statusLanes = new Map(accountLanes.map((record) => [record.lane_id, record]));
|
|
182
|
+
const grouped = new Map();
|
|
183
|
+
const seenLaneIds = new Set();
|
|
184
|
+
for (const status of statuses) {
|
|
185
|
+
const lane = statusLanes.get(status.lane_id);
|
|
186
|
+
if (!lane || lane.provider !== status.provider || lane.account !== status.account ||
|
|
187
|
+
lane.host.kind !== status.host.kind || lane.host.id !== status.host.id ||
|
|
188
|
+
(status.logged_in !== true && status.logged_in !== false && status.logged_in !== null) ||
|
|
189
|
+
seenLaneIds.has(status.lane_id)) {
|
|
190
|
+
const message = `malformed account login status for lane ${JSON.stringify(status.lane_id)}`;
|
|
191
|
+
writeError(`lane watch: ${message}`);
|
|
192
|
+
throw new LanesError("watch_account_status_invalid", message, {
|
|
193
|
+
lane_id: status.lane_id,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
seenLaneIds.add(status.lane_id);
|
|
197
|
+
const key = JSON.stringify([status.provider, status.account, status.host.kind, status.host.id]);
|
|
198
|
+
const existing = grouped.get(key);
|
|
199
|
+
if (existing && existing.status.logged_in !== status.logged_in) {
|
|
200
|
+
const message = `conflicting account login statuses for ${status.provider} account ${status.account} on ${status.host.kind}:${status.host.id}`;
|
|
201
|
+
writeError(`lane watch: ${message}`);
|
|
202
|
+
throw new LanesError("watch_account_status_invalid", message, {
|
|
203
|
+
provider: status.provider,
|
|
204
|
+
account: status.account,
|
|
205
|
+
host: status.host,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const group = existing ?? { status, laneIds: new Set() };
|
|
209
|
+
group.laneIds.add(status.lane_id);
|
|
210
|
+
grouped.set(key, group);
|
|
211
|
+
}
|
|
212
|
+
const missingLane = accountLanes.find((lane) => !seenLaneIds.has(lane.lane_id));
|
|
213
|
+
if (missingLane) {
|
|
214
|
+
const message = `account login status is missing for lane ${missingLane.lane_id}`;
|
|
215
|
+
writeError(`lane watch: ${message}`);
|
|
216
|
+
throw new LanesError("watch_account_status_invalid", message, {
|
|
217
|
+
lane_id: missingLane.lane_id,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
for (const [key, group] of grouped) {
|
|
221
|
+
const { status } = group;
|
|
222
|
+
const prior = previousAccountLogin.get(key);
|
|
223
|
+
if (prior === true && status.logged_in === false) {
|
|
224
|
+
emitAccountEvent({
|
|
225
|
+
event: "account_logged_out",
|
|
226
|
+
provider: status.provider,
|
|
227
|
+
account: status.account,
|
|
228
|
+
host: status.host,
|
|
229
|
+
lane_ids: [...group.laneIds].sort(),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (status.logged_in !== null)
|
|
233
|
+
previousAccountLogin.set(key, status.logged_in);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (records.every((record) => isTerminalLaneState(record.state) && !keepAliveWaiting.has(record.lane_id))) {
|
|
117
237
|
return { polls, lines, suppressed };
|
|
118
238
|
}
|
|
119
239
|
await sleep(intervalMs);
|