@indigoai-us/hq-cli 5.134.0 → 5.136.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 +28 -0
- package/dist/command-catalog.generated.d.ts +71 -0
- package/dist/command-catalog.generated.js +93 -0
- package/dist/commands/bot-jobs.d.ts +127 -0
- package/dist/commands/bot-jobs.js +445 -0
- package/dist/commands/bot.js +3 -0
- package/dist/commands/lanes.d.ts +4 -2
- package/dist/commands/lanes.js +150 -2
- package/dist/lib/bot/api.d.ts +66 -0
- package/dist/lib/bot/api.js +38 -0
- package/dist/lib/bot/index.d.ts +3 -0
- package/dist/lib/bot/index.js +3 -0
- package/dist/lib/bot/inflight.d.ts +42 -0
- package/dist/lib/bot/job-completions.d.ts +91 -0
- package/dist/lib/bot/job-completions.js +132 -0
- package/dist/lib/bot/job-run.d.ts +109 -0
- package/dist/lib/bot/job-run.js +246 -0
- package/dist/lib/bot/paths.d.ts +9 -1
- package/dist/lib/bot/paths.js +14 -0
- package/dist/lib/bot/prompt.d.ts +12 -0
- package/dist/lib/bot/prompt.js +31 -0
- package/dist/lib/bot/room-policy.d.ts +18 -1
- package/dist/lib/bot/room-policy.js +20 -2
- package/dist/lib/bot/run.d.ts +23 -1
- package/dist/lib/bot/run.js +404 -7
- package/dist/lib/lanes/config.d.ts +7 -0
- package/dist/lib/lanes/config.js +28 -1
- package/dist/lib/lanes/index.d.ts +5 -1
- package/dist/lib/lanes/index.js +3 -1
- package/dist/lib/lanes/monitor.d.ts +48 -0
- package/dist/lib/lanes/monitor.js +409 -0
- package/dist/lib/lanes/spawn.d.ts +10 -1
- package/dist/lib/lanes/spawn.js +60 -5
- package/dist/lib/lanes/types.d.ts +6 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watch.d.ts +22 -0
- package/dist/lib/lanes/watch.js +122 -0
- package/package.json +1 -1
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, } from "./accounts.js";
|
|
15
15
|
import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSession, } from "./compaction.js";
|
|
16
16
|
import { installDrainHook } from "./hooks-install.js";
|
|
17
|
-
import { loadLanesConfig, providerCompactAfterMs } from "./config.js";
|
|
17
|
+
import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
|
|
18
18
|
import { appendEffortFlag, resolveLaneEffort } from "./effort.js";
|
|
19
19
|
import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
20
20
|
import { AccountResolutionError, LanesError } from "./errors.js";
|
|
@@ -64,6 +64,14 @@ export const PROVIDER_MODEL_FLAG = {
|
|
|
64
64
|
codex: "-m",
|
|
65
65
|
grok: "-m",
|
|
66
66
|
};
|
|
67
|
+
export const CLAUDE_AUTO_COMPACT_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
|
|
68
|
+
/** Codex CLI overrides required for its ChatGPT fast service tier. */
|
|
69
|
+
export const CODEX_FAST_MODE_FLAGS = [
|
|
70
|
+
"-c",
|
|
71
|
+
'service_tier="fast"',
|
|
72
|
+
"--enable",
|
|
73
|
+
"fast_mode",
|
|
74
|
+
];
|
|
67
75
|
export function parseCliModel(raw) {
|
|
68
76
|
if (raw === undefined)
|
|
69
77
|
return undefined;
|
|
@@ -101,6 +109,16 @@ export function appendModelFlag(provider, args, model) {
|
|
|
101
109
|
}
|
|
102
110
|
return [...args, flag, model];
|
|
103
111
|
}
|
|
112
|
+
/** Add Codex fast-mode flags before the prompt/positional separator. */
|
|
113
|
+
export function appendFastModeFlags(provider, args, enabled) {
|
|
114
|
+
if (provider !== "codex" || enabled !== true)
|
|
115
|
+
return args;
|
|
116
|
+
const flags = [...CODEX_FAST_MODE_FLAGS];
|
|
117
|
+
const end = args.indexOf("--");
|
|
118
|
+
if (end === -1)
|
|
119
|
+
return [...args, ...flags];
|
|
120
|
+
return [...args.slice(0, end), ...flags, ...args.slice(end)];
|
|
121
|
+
}
|
|
104
122
|
export const PID_LIVENESS_DECISIONS = [
|
|
105
123
|
"esrch",
|
|
106
124
|
"starttime_mismatch",
|
|
@@ -695,10 +713,10 @@ export function buildProviderCommand(provider, input) {
|
|
|
695
713
|
}
|
|
696
714
|
return {
|
|
697
715
|
command,
|
|
698
|
-
args: appendEffortFlag(parsed, appendModelFlag(parsed, args, input.model), input.effort),
|
|
716
|
+
args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, args, input.model), input.effort), input.fastMode),
|
|
699
717
|
};
|
|
700
718
|
}
|
|
701
|
-
export function workerEnv(base, resolved, laneId, hqRoot) {
|
|
719
|
+
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens) {
|
|
702
720
|
const accepted = Object.values(PROVIDER_HOME_ENV);
|
|
703
721
|
if (!accepted.includes(resolved.envVar)) {
|
|
704
722
|
throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
|
|
@@ -707,6 +725,10 @@ export function workerEnv(base, resolved, laneId, hqRoot) {
|
|
|
707
725
|
delete env.HQ_PROJECT_ID;
|
|
708
726
|
delete env.HQ_STORY_ID;
|
|
709
727
|
env[resolved.envVar] = resolved.home;
|
|
728
|
+
delete env[CLAUDE_AUTO_COMPACT_ENV];
|
|
729
|
+
if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
|
|
730
|
+
env[CLAUDE_AUTO_COMPACT_ENV] = String(autoCompactTokens);
|
|
731
|
+
}
|
|
710
732
|
env.HQ_LANE_ID = laneId;
|
|
711
733
|
env.HQ_ROOT = hqRoot;
|
|
712
734
|
return env;
|
|
@@ -883,6 +905,7 @@ function writeSpawnManifest(runDir, input) {
|
|
|
883
905
|
envelope_path: input.envelopePath,
|
|
884
906
|
resume: input.resume,
|
|
885
907
|
worker_out_offset: input.workerOutOffset,
|
|
908
|
+
...(input.fastMode ? { fast_mode: true } : {}),
|
|
886
909
|
}, null, 2)}\n`);
|
|
887
910
|
}
|
|
888
911
|
/** Arm (or re-arm) the detached per-lane watcher. */
|
|
@@ -944,6 +967,8 @@ export function readSpawnManifest(runDir) {
|
|
|
944
967
|
rec.worker_out_offset >= 0) {
|
|
945
968
|
out.worker_out_offset = rec.worker_out_offset;
|
|
946
969
|
}
|
|
970
|
+
if (typeof rec.fast_mode === "boolean")
|
|
971
|
+
out.fast_mode = rec.fast_mode;
|
|
947
972
|
if (rec.resume &&
|
|
948
973
|
typeof rec.resume === "object" &&
|
|
949
974
|
typeof rec.resume.session_id === "string") {
|
|
@@ -1042,7 +1067,6 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1042
1067
|
process.stderr.write(`hq lanes: drain hook install failed for ${resolved.home}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1043
1068
|
throw err;
|
|
1044
1069
|
}
|
|
1045
|
-
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot);
|
|
1046
1070
|
const briefPath = path.join(runDir, "brief.md");
|
|
1047
1071
|
const envelopePath = nextEnvelopePath(runDir);
|
|
1048
1072
|
const lastMessagePath = path.join(logsDir, "last-message.txt");
|
|
@@ -1051,6 +1075,10 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1051
1075
|
? `Previously answered questions are at ${answersPath}; treat those choices as operator decisions that outrank the brief.`
|
|
1052
1076
|
: undefined;
|
|
1053
1077
|
const accountCfg = config.providers[lane.provider].accounts[resolved.id];
|
|
1078
|
+
const autoCompactTokens = lane.provider === "claude"
|
|
1079
|
+
? providerAutoCompactTokens(config.providers.claude)
|
|
1080
|
+
: undefined;
|
|
1081
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens);
|
|
1054
1082
|
const model = resolveLaneModel({
|
|
1055
1083
|
cliModel: lane.model,
|
|
1056
1084
|
accountModel: accountCfg?.model,
|
|
@@ -1062,6 +1090,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1062
1090
|
providerEffort: config.providers[lane.provider].effort,
|
|
1063
1091
|
model,
|
|
1064
1092
|
});
|
|
1093
|
+
const fastMode = lane.provider === "codex" &&
|
|
1094
|
+
model !== undefined &&
|
|
1095
|
+
(config.providers.codex.fast_mode_models ?? []).includes(model);
|
|
1065
1096
|
const resume = plan.mode === "resume"
|
|
1066
1097
|
? {
|
|
1067
1098
|
session_id: plan.session_id,
|
|
@@ -1090,6 +1121,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1090
1121
|
extraPrompt,
|
|
1091
1122
|
model,
|
|
1092
1123
|
effort: effort.effort,
|
|
1124
|
+
fastMode,
|
|
1093
1125
|
resume,
|
|
1094
1126
|
});
|
|
1095
1127
|
// A systemd PATH mismatch is an environment defect, not a lane failure.
|
|
@@ -1137,6 +1169,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1137
1169
|
lastMessagePath,
|
|
1138
1170
|
model,
|
|
1139
1171
|
effort: effort.effort,
|
|
1172
|
+
fastMode,
|
|
1140
1173
|
resume: { session_id: resume.session_id, prompt: COMPACTION_PROMPT },
|
|
1141
1174
|
});
|
|
1142
1175
|
compaction = compactLaneSession({
|
|
@@ -1187,6 +1220,10 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1187
1220
|
next.effort = effort.effort;
|
|
1188
1221
|
else
|
|
1189
1222
|
delete next.effort;
|
|
1223
|
+
if (fastMode)
|
|
1224
|
+
next.fast_mode = true;
|
|
1225
|
+
else
|
|
1226
|
+
delete next.fast_mode;
|
|
1190
1227
|
// Freeze the counters of the run that followed the previous compaction
|
|
1191
1228
|
// before this launch replaces last_resume.
|
|
1192
1229
|
if (current.last_compaction && current.last_compaction.after === null) {
|
|
@@ -1257,6 +1294,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1257
1294
|
envelopePath,
|
|
1258
1295
|
resume: resume ? { session_id: resume.session_id } : null,
|
|
1259
1296
|
workerOutOffset,
|
|
1297
|
+
fastMode,
|
|
1260
1298
|
});
|
|
1261
1299
|
}
|
|
1262
1300
|
catch (err) {
|
|
@@ -1300,6 +1338,7 @@ export function envelopeRepairMode(provider) {
|
|
|
1300
1338
|
* prompt as a fresh single turn on the same account (`fresh_turn` mode). It
|
|
1301
1339
|
* replaces `worker.pid` and rewrites `spawn.json` so the watcher's next tick
|
|
1302
1340
|
* follows the new process. The account is the one already bound on the record.
|
|
1341
|
+
* Fast-mode evidence is updated after the replacement is proven detached.
|
|
1303
1342
|
*/
|
|
1304
1343
|
export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
1305
1344
|
const env = deps.env ?? process.env;
|
|
@@ -1321,7 +1360,10 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
1321
1360
|
existsSync,
|
|
1322
1361
|
model: lane.model,
|
|
1323
1362
|
});
|
|
1324
|
-
const
|
|
1363
|
+
const autoCompactTokens = lane.provider === "claude"
|
|
1364
|
+
? providerAutoCompactTokens(config.providers.claude)
|
|
1365
|
+
: undefined;
|
|
1366
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens);
|
|
1325
1367
|
const accountCfg = config.providers[lane.provider].accounts[resolved.id];
|
|
1326
1368
|
const model = resolveLaneModel({
|
|
1327
1369
|
cliModel: lane.model,
|
|
@@ -1334,6 +1376,9 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
1334
1376
|
providerEffort: config.providers[lane.provider].effort,
|
|
1335
1377
|
model,
|
|
1336
1378
|
});
|
|
1379
|
+
const fastMode = lane.provider === "codex" &&
|
|
1380
|
+
model !== undefined &&
|
|
1381
|
+
(config.providers.codex.fast_mode_models ?? []).includes(model);
|
|
1337
1382
|
const repairPrompt = envelopeRepairPrompt({
|
|
1338
1383
|
envelopePath: input.envelopePath,
|
|
1339
1384
|
error: input.error,
|
|
@@ -1346,6 +1391,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
1346
1391
|
lastMessagePath: path.join(logsDir, "last-message.txt"),
|
|
1347
1392
|
model,
|
|
1348
1393
|
effort: effort.effort,
|
|
1394
|
+
fastMode,
|
|
1349
1395
|
...(mode === "resume" && sessionId !== null
|
|
1350
1396
|
? { resume: { session_id: sessionId, prompt: repairPrompt } }
|
|
1351
1397
|
: { freshTurnPrompt: repairPrompt }),
|
|
@@ -1371,6 +1417,15 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
1371
1417
|
envelopePath: previous.envelope_path ?? input.envelopePath,
|
|
1372
1418
|
resume: mode === "resume" && sessionId !== null ? { session_id: sessionId } : null,
|
|
1373
1419
|
workerOutOffset,
|
|
1420
|
+
fastMode,
|
|
1421
|
+
});
|
|
1422
|
+
retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
1423
|
+
const next = { ...current };
|
|
1424
|
+
if (fastMode)
|
|
1425
|
+
next.fast_mode = true;
|
|
1426
|
+
else
|
|
1427
|
+
delete next.fast_mode;
|
|
1428
|
+
return next;
|
|
1374
1429
|
});
|
|
1375
1430
|
}
|
|
1376
1431
|
catch (err) {
|
|
@@ -300,6 +300,8 @@ export interface LaneRecord {
|
|
|
300
300
|
effort?: string;
|
|
301
301
|
/** Where `effort` came from. `cli` pins it across later spawns. */
|
|
302
302
|
effort_source?: "cli" | "account" | "provider" | "default_rule" | "none";
|
|
303
|
+
/** Whether the last spawn used Codex fast mode, including false when disabled. */
|
|
304
|
+
fast_mode?: boolean;
|
|
303
305
|
/** Time of the last operator message accepted for this lane. Drives compaction. */
|
|
304
306
|
last_message_at?: string;
|
|
305
307
|
/** Last automatic compaction of a resumed session (compaction.ts). */
|
|
@@ -540,11 +542,15 @@ export interface ProviderConfig {
|
|
|
540
542
|
model?: string;
|
|
541
543
|
/** Default effort for this provider when the account does not set one. */
|
|
542
544
|
effort?: string;
|
|
545
|
+
/** Codex model ids that should use the provider's fast service tier. */
|
|
546
|
+
fast_mode_models?: string[];
|
|
543
547
|
/**
|
|
544
548
|
* Resume-time compaction threshold: a resumed session whose last message is
|
|
545
549
|
* at least this old is compacted first. Absent means the one-hour default.
|
|
546
550
|
*/
|
|
547
551
|
compact_after_ms?: number;
|
|
552
|
+
/** Claude Code auto-compaction threshold, in tokens. */
|
|
553
|
+
auto_compact_tokens?: number;
|
|
548
554
|
/**
|
|
549
555
|
* Watcher throttle for `lane_progress` thread events: at most one per this
|
|
550
556
|
* interval, and only on a tick that saw new tool activity or output. Absent
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -203,7 +203,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
203
203
|
priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"],
|
|
204
204
|
provider: ["creator"], account: ["creator", "spawn", "command"], account_history: ["command"], pending_resume: ["command", "spawn"], host: ["creator"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
205
205
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
206
|
-
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"],
|
|
206
|
+
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
|
|
207
207
|
last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
208
208
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
209
209
|
admitted_reason: ["reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type LaneRecord, type QuestionRecord } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_LANE_WATCH_INTERVAL_MS = 5000;
|
|
3
|
+
export declare const MAX_LANE_WATCH_INTERVAL_MS = 30000;
|
|
4
|
+
export declare const MAX_WATCHED_LANES = 64;
|
|
5
|
+
export declare const MAX_WATCH_OUTPUT_LINES = 512;
|
|
6
|
+
export interface LaneWatchDeps {
|
|
7
|
+
readLane?: (hqRoot: string, laneId: string) => LaneRecord | null;
|
|
8
|
+
listQuestionIds?: (hqRoot: string) => string[];
|
|
9
|
+
readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
|
|
10
|
+
sleep?: (ms: number) => Promise<void>;
|
|
11
|
+
intervalMs?: number;
|
|
12
|
+
maxOutputLines?: number;
|
|
13
|
+
writeLine?: (line: string) => void;
|
|
14
|
+
writeError?: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface LaneWatchResult {
|
|
17
|
+
polls: number;
|
|
18
|
+
lines: number;
|
|
19
|
+
suppressed: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function runLaneWatch(hqRoot: string, laneIds: readonly string[], deps?: LaneWatchDeps): Promise<LaneWatchResult>;
|
|
22
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
|
+
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
|
+
import { isTerminalLaneState } from "./types.js";
|
|
4
|
+
export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
|
|
5
|
+
export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
|
|
6
|
+
export const MAX_WATCHED_LANES = 64;
|
|
7
|
+
export const MAX_WATCH_OUTPUT_LINES = 512;
|
|
8
|
+
function defaultSleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
function readableError(err) {
|
|
12
|
+
return err instanceof Error ? err.message : String(err);
|
|
13
|
+
}
|
|
14
|
+
function boundedInterval(intervalMs) {
|
|
15
|
+
const value = intervalMs ?? DEFAULT_LANE_WATCH_INTERVAL_MS;
|
|
16
|
+
if (!Number.isFinite(value))
|
|
17
|
+
return DEFAULT_LANE_WATCH_INTERVAL_MS;
|
|
18
|
+
return Math.min(MAX_LANE_WATCH_INTERVAL_MS, Math.max(100, Math.floor(value)));
|
|
19
|
+
}
|
|
20
|
+
function validateLaneIds(laneIds) {
|
|
21
|
+
const unique = [...new Set(laneIds.map((laneId) => laneId.trim()))];
|
|
22
|
+
if (unique.some((laneId) => laneId.length === 0)) {
|
|
23
|
+
throw new LanesError("watch_invalid_lane_id", "Lane ids must not be empty");
|
|
24
|
+
}
|
|
25
|
+
if (unique.length === 0) {
|
|
26
|
+
throw new LanesError("watch_no_lanes", "hq lanes watch requires at least one lane id");
|
|
27
|
+
}
|
|
28
|
+
if (unique.length > MAX_WATCHED_LANES) {
|
|
29
|
+
throw new LanesError("watch_too_many_lanes", `hq lanes watch accepts at most ${MAX_WATCHED_LANES} lane ids`, { max: MAX_WATCHED_LANES });
|
|
30
|
+
}
|
|
31
|
+
return unique;
|
|
32
|
+
}
|
|
33
|
+
export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
34
|
+
const watchedIds = validateLaneIds(laneIds);
|
|
35
|
+
const readLaneRecord = deps.readLane ?? readLane;
|
|
36
|
+
const listQuestions = deps.listQuestionIds ?? listQuestionIds;
|
|
37
|
+
const readQuestionRecord = deps.readQuestion ?? readQuestion;
|
|
38
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
39
|
+
const writeLine = deps.writeLine ?? ((line) => console.log(line));
|
|
40
|
+
const writeError = deps.writeError ?? ((line) => process.stderr.write(`${line}\n`));
|
|
41
|
+
const maxOutputLines = Math.max(1, Math.floor(deps.maxOutputLines ?? MAX_WATCH_OUTPUT_LINES));
|
|
42
|
+
const intervalMs = boundedInterval(deps.intervalMs);
|
|
43
|
+
const previousStates = new Map();
|
|
44
|
+
const previousQuestionStatuses = new Map();
|
|
45
|
+
let polls = 0;
|
|
46
|
+
let lines = 0;
|
|
47
|
+
let suppressed = false;
|
|
48
|
+
const emit = (line) => {
|
|
49
|
+
if (lines < maxOutputLines - 1) {
|
|
50
|
+
writeLine(line);
|
|
51
|
+
lines += 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!suppressed) {
|
|
55
|
+
writeLine("lane watch: output limit reached; further events suppressed");
|
|
56
|
+
lines = maxOutputLines;
|
|
57
|
+
suppressed = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const unreadable = (kind, id, err) => {
|
|
61
|
+
const message = `${kind} ${id}: unreadable record (${readableError(err)})`;
|
|
62
|
+
writeError(`lane watch: ${message}`);
|
|
63
|
+
throw new LanesError("watch_record_unreadable", message, {
|
|
64
|
+
kind,
|
|
65
|
+
id,
|
|
66
|
+
cause: readableError(err),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
while (true) {
|
|
70
|
+
polls += 1;
|
|
71
|
+
const records = [];
|
|
72
|
+
for (const laneId of watchedIds) {
|
|
73
|
+
let record = null;
|
|
74
|
+
try {
|
|
75
|
+
record = readLaneRecord(hqRoot, laneId);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
unreadable("lane", laneId, err);
|
|
79
|
+
}
|
|
80
|
+
if (!record) {
|
|
81
|
+
unreadable("lane", laneId, new LaneNotFoundError("lane", laneId));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
records.push(record);
|
|
85
|
+
if (previousStates.get(laneId) !== record.state) {
|
|
86
|
+
emit(`lane ${laneId}: state=${record.state}${isTerminalLaneState(record.state) ? " (terminal)" : ""}`);
|
|
87
|
+
previousStates.set(laneId, record.state);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let questionIds = [];
|
|
91
|
+
try {
|
|
92
|
+
questionIds = listQuestions(hqRoot);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
unreadable("question", "<index>", err);
|
|
96
|
+
}
|
|
97
|
+
const watchedSet = new Set(watchedIds);
|
|
98
|
+
for (const questionId of questionIds) {
|
|
99
|
+
let record = null;
|
|
100
|
+
try {
|
|
101
|
+
record = readQuestionRecord(hqRoot, questionId);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
unreadable("question", questionId, err);
|
|
105
|
+
}
|
|
106
|
+
if (!record) {
|
|
107
|
+
unreadable("question", questionId, new LaneNotFoundError("question", questionId));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (watchedSet.has(record.lane_id) &&
|
|
111
|
+
previousQuestionStatuses.get(questionId) !== record.status) {
|
|
112
|
+
emit(`lane ${record.lane_id}: question=${record.question_id} status=${record.status}`);
|
|
113
|
+
}
|
|
114
|
+
previousQuestionStatuses.set(questionId, record.status);
|
|
115
|
+
}
|
|
116
|
+
if (records.every((record) => isTerminalLaneState(record.state))) {
|
|
117
|
+
return { polls, lines, suppressed };
|
|
118
|
+
}
|
|
119
|
+
await sleep(intervalMs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=watch.js.map
|