@indigoai-us/hq-cli 5.232.0 → 5.233.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 +8 -0
- package/dist/command-catalog.generated.d.ts +21 -0
- package/dist/command-catalog.generated.js +27 -0
- package/dist/commands/lanes.js +40 -0
- package/dist/lib/lanes/wait.d.ts +39 -0
- package/dist/lib/lanes/wait.js +159 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.233.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq lanes wait` exits on the first selected envelope, pending-question, or
|
|
10
|
+
state change across the requested lanes. It uses a startup baseline and a
|
|
11
|
+
bounded two-second poll interval.
|
|
12
|
+
|
|
5
13
|
## [5.232.0] — 2026-09-27
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -7773,6 +7773,27 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7773
7773
|
readonly description: "JSON on stdout (always)";
|
|
7774
7774
|
}];
|
|
7775
7775
|
readonly subcommands: readonly [];
|
|
7776
|
+
}, {
|
|
7777
|
+
readonly name: "wait";
|
|
7778
|
+
readonly description: "Wait for the first requested change across lanes";
|
|
7779
|
+
readonly aliases: readonly [];
|
|
7780
|
+
readonly hidden: false;
|
|
7781
|
+
readonly usage: "[options]";
|
|
7782
|
+
readonly arguments: readonly [];
|
|
7783
|
+
readonly options: readonly [{
|
|
7784
|
+
readonly flags: "--any <lane-id...>";
|
|
7785
|
+
readonly description: "Lane ids to monitor";
|
|
7786
|
+
}, {
|
|
7787
|
+
readonly flags: "--for <change-type>";
|
|
7788
|
+
readonly description: "envelope | question | state";
|
|
7789
|
+
}, {
|
|
7790
|
+
readonly flags: "--timeout <seconds>";
|
|
7791
|
+
readonly description: "Positive timeout in seconds";
|
|
7792
|
+
}, {
|
|
7793
|
+
readonly flags: "--json";
|
|
7794
|
+
readonly description: "JSON output";
|
|
7795
|
+
}];
|
|
7796
|
+
readonly subcommands: readonly [];
|
|
7776
7797
|
}, {
|
|
7777
7798
|
readonly name: "watch";
|
|
7778
7799
|
readonly description: "Watch lane state and question changes until all lanes are terminal";
|
|
@@ -10051,6 +10051,33 @@ export const COMMAND_CATALOG = [
|
|
|
10051
10051
|
],
|
|
10052
10052
|
"subcommands": []
|
|
10053
10053
|
},
|
|
10054
|
+
{
|
|
10055
|
+
"name": "wait",
|
|
10056
|
+
"description": "Wait for the first requested change across lanes",
|
|
10057
|
+
"aliases": [],
|
|
10058
|
+
"hidden": false,
|
|
10059
|
+
"usage": "[options]",
|
|
10060
|
+
"arguments": [],
|
|
10061
|
+
"options": [
|
|
10062
|
+
{
|
|
10063
|
+
"flags": "--any <lane-id...>",
|
|
10064
|
+
"description": "Lane ids to monitor"
|
|
10065
|
+
},
|
|
10066
|
+
{
|
|
10067
|
+
"flags": "--for <change-type>",
|
|
10068
|
+
"description": "envelope | question | state"
|
|
10069
|
+
},
|
|
10070
|
+
{
|
|
10071
|
+
"flags": "--timeout <seconds>",
|
|
10072
|
+
"description": "Positive timeout in seconds"
|
|
10073
|
+
},
|
|
10074
|
+
{
|
|
10075
|
+
"flags": "--json",
|
|
10076
|
+
"description": "JSON output"
|
|
10077
|
+
}
|
|
10078
|
+
],
|
|
10079
|
+
"subcommands": []
|
|
10080
|
+
},
|
|
10054
10081
|
{
|
|
10055
10082
|
"name": "watch",
|
|
10056
10083
|
"description": "Watch lane state and question changes until all lanes are terminal",
|
package/dist/commands/lanes.js
CHANGED
|
@@ -52,6 +52,7 @@ import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
|
52
52
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
53
53
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
54
54
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
55
|
+
import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
|
|
55
56
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
56
57
|
import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
|
|
57
58
|
import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
|
|
@@ -2127,6 +2128,37 @@ function yieldEventLoop() {
|
|
|
2127
2128
|
setImmediate(resolve);
|
|
2128
2129
|
});
|
|
2129
2130
|
}
|
|
2131
|
+
function laneWaitHumanLine(result) {
|
|
2132
|
+
const details = Object.entries(result.change)
|
|
2133
|
+
.filter(([key]) => key !== "type")
|
|
2134
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
2135
|
+
.join(" ");
|
|
2136
|
+
return `lane=${result.lane_id} change=${result.change.type}${details ? ` ${details}` : ""} at=${result.at}`;
|
|
2137
|
+
}
|
|
2138
|
+
async function runWait(ctx, opts) {
|
|
2139
|
+
const timeoutSeconds = opts.timeout === undefined ? Number.NaN : Number(opts.timeout);
|
|
2140
|
+
try {
|
|
2141
|
+
const result = await runLaneWait(ctx.hqRoot(), opts.any ?? [], opts.for ?? [], timeoutSeconds * 1000, { now: ctx.now, sleep: ctx.sleep });
|
|
2142
|
+
if (result) {
|
|
2143
|
+
console.log(opts.json ? JSON.stringify(result) : laneWaitHumanLine(result));
|
|
2144
|
+
return 0;
|
|
2145
|
+
}
|
|
2146
|
+
console.log(opts.json
|
|
2147
|
+
? JSON.stringify({ ok: false, error: "wait_timeout" })
|
|
2148
|
+
: "wait_timeout");
|
|
2149
|
+
return LANE_WAIT_TIMEOUT_EXIT_CODE;
|
|
2150
|
+
}
|
|
2151
|
+
catch (err) {
|
|
2152
|
+
if (!isLanesError(err) || err.error !== "wait_invalid_input")
|
|
2153
|
+
throw err;
|
|
2154
|
+
const output = { ok: false, error: err.error, message: err.message };
|
|
2155
|
+
if (opts.json)
|
|
2156
|
+
console.log(JSON.stringify(output));
|
|
2157
|
+
else
|
|
2158
|
+
process.stderr.write(`${err.message}\n`);
|
|
2159
|
+
return LANE_WAIT_INVALID_INPUT_EXIT_CODE;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2130
2162
|
function withCasRetry(fn, attempts = 5) {
|
|
2131
2163
|
let last;
|
|
2132
2164
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -9492,6 +9524,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9492
9524
|
.action(wrap(async (laneId, opts) => {
|
|
9493
9525
|
await runAdmit(ctx, laneId, opts);
|
|
9494
9526
|
}));
|
|
9527
|
+
lanes
|
|
9528
|
+
.command("wait")
|
|
9529
|
+
.description("Wait for the first requested change across lanes")
|
|
9530
|
+
.option("--any <lane-id...>", "Lane ids to monitor")
|
|
9531
|
+
.option("--for <change-type>", "envelope | question | state", collectRepeatable, [])
|
|
9532
|
+
.option("--timeout <seconds>", "Positive timeout in seconds")
|
|
9533
|
+
.option("--json", "JSON output")
|
|
9534
|
+
.action(wrapText(async (opts) => runWait(ctx, opts)));
|
|
9495
9535
|
lanes
|
|
9496
9536
|
.command("watch")
|
|
9497
9537
|
.description("Watch lane state and question changes until all lanes are terminal")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LaneRecord, QuestionRecord } from "./types.js";
|
|
2
|
+
export declare const LANE_WAIT_POLL_INTERVAL_MS = 2000;
|
|
3
|
+
export declare const LANE_WAIT_MAX_LANES = 64;
|
|
4
|
+
export declare const LANE_WAIT_TIMEOUT_EXIT_CODE = 4;
|
|
5
|
+
export declare const LANE_WAIT_INVALID_INPUT_EXIT_CODE = 2;
|
|
6
|
+
export declare const LANE_WAIT_CHANGE_TYPES: readonly ["envelope", "question", "state"];
|
|
7
|
+
export type LaneWaitChangeType = (typeof LANE_WAIT_CHANGE_TYPES)[number];
|
|
8
|
+
export type LaneWaitChange = {
|
|
9
|
+
type: "envelope";
|
|
10
|
+
ref: string;
|
|
11
|
+
decision: "done" | "ask" | "blocked";
|
|
12
|
+
} | {
|
|
13
|
+
type: "question";
|
|
14
|
+
question_id: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "state";
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
};
|
|
20
|
+
export interface LaneWaitResult {
|
|
21
|
+
ok: true;
|
|
22
|
+
lane_id: string;
|
|
23
|
+
change: LaneWaitChange;
|
|
24
|
+
at: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LaneWaitDeps {
|
|
27
|
+
readLane?: (hqRoot: string, laneId: string) => LaneRecord | null;
|
|
28
|
+
listQuestionIds?: (hqRoot: string) => string[];
|
|
29
|
+
readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
sleep?: (ms: number) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Wait for the first requested change after taking a read-only baseline.
|
|
35
|
+
* Each poll reads every selected lane once and runs no lane-store mutation or
|
|
36
|
+
* lock. Question records are scanned in one pass when question changes matter.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runLaneWait(hqRoot: string, laneIds: readonly string[], changeTypes: readonly string[], timeoutMs: number, deps?: LaneWaitDeps): Promise<LaneWaitResult | null>;
|
|
39
|
+
//# sourceMappingURL=wait.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { listQuestionIds, readLane, readQuestion } from "./store.js";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
3
|
+
export const LANE_WAIT_POLL_INTERVAL_MS = 2_000;
|
|
4
|
+
export const LANE_WAIT_MAX_LANES = 64;
|
|
5
|
+
export const LANE_WAIT_TIMEOUT_EXIT_CODE = 4;
|
|
6
|
+
export const LANE_WAIT_INVALID_INPUT_EXIT_CODE = 2;
|
|
7
|
+
export const LANE_WAIT_CHANGE_TYPES = ["envelope", "question", "state"];
|
|
8
|
+
function defaultSleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
function invalidInput(message, details = {}) {
|
|
12
|
+
throw new LanesError("wait_invalid_input", message, details);
|
|
13
|
+
}
|
|
14
|
+
function validateInput(laneIds, changeTypes, timeoutMs) {
|
|
15
|
+
const normalizedLaneIds = laneIds.map((laneId) => laneId.trim());
|
|
16
|
+
if (normalizedLaneIds.length === 0 || normalizedLaneIds.some((laneId) => laneId.length === 0)) {
|
|
17
|
+
invalidInput("hq lanes wait requires at least one non-empty lane id", { lane_ids: laneIds });
|
|
18
|
+
}
|
|
19
|
+
if (normalizedLaneIds.some((laneId) => laneId === "." || laneId === ".." || /[\\/]/.test(laneId))) {
|
|
20
|
+
invalidInput("Lane ids must not contain path separators", { lane_ids: laneIds });
|
|
21
|
+
}
|
|
22
|
+
const uniqueLaneIds = [...new Set(normalizedLaneIds)];
|
|
23
|
+
if (uniqueLaneIds.length > LANE_WAIT_MAX_LANES) {
|
|
24
|
+
invalidInput(`hq lanes wait accepts at most ${LANE_WAIT_MAX_LANES} lane ids`, {
|
|
25
|
+
max: LANE_WAIT_MAX_LANES,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (changeTypes.length === 0 || changeTypes.some((kind) => !LANE_WAIT_CHANGE_TYPES.includes(kind))) {
|
|
29
|
+
invalidInput(`hq lanes wait requires one or more --for values: ${LANE_WAIT_CHANGE_TYPES.join(", ")}`, { accepted: [...LANE_WAIT_CHANGE_TYPES], values: changeTypes });
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isSafeInteger(Math.ceil(timeoutMs))) {
|
|
32
|
+
invalidInput("hq lanes wait requires a positive finite timeout", { timeout_ms: timeoutMs });
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
laneIds: uniqueLaneIds,
|
|
36
|
+
changeTypes: [...new Set(changeTypes)],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function snapshotLanes(hqRoot, laneIds, changeTypes, deps) {
|
|
40
|
+
const readLaneRecord = deps.readLane ?? readLane;
|
|
41
|
+
const lanes = new Map();
|
|
42
|
+
for (const laneId of laneIds) {
|
|
43
|
+
const lane = readLaneRecord(hqRoot, laneId);
|
|
44
|
+
if (!lane) {
|
|
45
|
+
throw new LanesError("wait_lane_unreadable", `Lane ${laneId} could not be read while waiting`, { lane_id: laneId });
|
|
46
|
+
}
|
|
47
|
+
lanes.set(laneId, lane);
|
|
48
|
+
}
|
|
49
|
+
const pendingQuestions = new Map();
|
|
50
|
+
if (changeTypes.includes("question")) {
|
|
51
|
+
const listIds = deps.listQuestionIds ?? listQuestionIds;
|
|
52
|
+
const readQuestionRecord = deps.readQuestion ?? readQuestion;
|
|
53
|
+
for (const questionId of new Set(listIds(hqRoot))) {
|
|
54
|
+
const question = readQuestionRecord(hqRoot, questionId);
|
|
55
|
+
if (!question) {
|
|
56
|
+
throw new LanesError("wait_question_unreadable", `Question ${questionId} disappeared while waiting`, { question_id: questionId });
|
|
57
|
+
}
|
|
58
|
+
if (question.status === "pending" && lanes.has(question.lane_id)) {
|
|
59
|
+
pendingQuestions.set(questionId, question);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { lanes, pendingQuestions };
|
|
64
|
+
}
|
|
65
|
+
function timestamp(value, fallbackMs) {
|
|
66
|
+
const parsed = value === undefined ? Number.NaN : Date.parse(value);
|
|
67
|
+
const atMs = Number.isFinite(parsed) ? parsed : fallbackMs;
|
|
68
|
+
return { at: new Date(atMs).toISOString(), atMs };
|
|
69
|
+
}
|
|
70
|
+
function envelopeSignature(lane) {
|
|
71
|
+
const envelope = lane.last_envelope;
|
|
72
|
+
return envelope === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: `${envelope.ref}\0${envelope.decision}\0${envelope.at}`;
|
|
75
|
+
}
|
|
76
|
+
function candidatesForSnapshot(previous, current, laneIds, changeTypes, observedAtMs) {
|
|
77
|
+
const candidates = [];
|
|
78
|
+
laneIds.forEach((laneId, laneOrder) => {
|
|
79
|
+
const before = previous.lanes.get(laneId);
|
|
80
|
+
const after = current.lanes.get(laneId);
|
|
81
|
+
if (!before || !after)
|
|
82
|
+
return;
|
|
83
|
+
changeTypes.forEach((type, typeOrder) => {
|
|
84
|
+
if (type === "envelope" && envelopeSignature(before) !== envelopeSignature(after)) {
|
|
85
|
+
const envelope = after.last_envelope;
|
|
86
|
+
if (envelope) {
|
|
87
|
+
const when = timestamp(envelope.at, observedAtMs);
|
|
88
|
+
candidates.push({
|
|
89
|
+
laneId,
|
|
90
|
+
change: { type, ref: envelope.ref, decision: envelope.decision },
|
|
91
|
+
...when,
|
|
92
|
+
laneOrder,
|
|
93
|
+
typeOrder,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (type === "state" && before.state !== after.state) {
|
|
98
|
+
const when = timestamp(after.updated_at, observedAtMs);
|
|
99
|
+
candidates.push({
|
|
100
|
+
laneId,
|
|
101
|
+
change: { type, from: before.state, to: after.state },
|
|
102
|
+
...when,
|
|
103
|
+
laneOrder,
|
|
104
|
+
typeOrder,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (type === "question") {
|
|
108
|
+
for (const [questionId, question] of current.pendingQuestions) {
|
|
109
|
+
if (question.lane_id !== laneId || previous.pendingQuestions.has(questionId))
|
|
110
|
+
continue;
|
|
111
|
+
const when = timestamp(question.asked_at, observedAtMs);
|
|
112
|
+
candidates.push({
|
|
113
|
+
laneId,
|
|
114
|
+
change: { type, question_id: questionId },
|
|
115
|
+
...when,
|
|
116
|
+
laneOrder,
|
|
117
|
+
typeOrder,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return candidates.sort((a, b) => a.atMs - b.atMs || a.laneOrder - b.laneOrder || a.typeOrder - b.typeOrder);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Wait for the first requested change after taking a read-only baseline.
|
|
127
|
+
* Each poll reads every selected lane once and runs no lane-store mutation or
|
|
128
|
+
* lock. Question records are scanned in one pass when question changes matter.
|
|
129
|
+
*/
|
|
130
|
+
export async function runLaneWait(hqRoot, laneIds, changeTypes, timeoutMs, deps = {}) {
|
|
131
|
+
const input = validateInput(laneIds, changeTypes, timeoutMs);
|
|
132
|
+
const now = deps.now ?? (() => new Date());
|
|
133
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
134
|
+
const startedAtMs = now().getTime();
|
|
135
|
+
const deadlineMs = startedAtMs + timeoutMs;
|
|
136
|
+
let previous = snapshotLanes(hqRoot, input.laneIds, input.changeTypes, deps);
|
|
137
|
+
while (true) {
|
|
138
|
+
const remainingMs = deadlineMs - now().getTime();
|
|
139
|
+
if (remainingMs <= 0)
|
|
140
|
+
return null;
|
|
141
|
+
await sleep(Math.min(LANE_WAIT_POLL_INTERVAL_MS, remainingMs));
|
|
142
|
+
const observedAtMs = now().getTime();
|
|
143
|
+
const current = snapshotLanes(hqRoot, input.laneIds, input.changeTypes, deps);
|
|
144
|
+
const candidates = candidatesForSnapshot(previous, current, input.laneIds, input.changeTypes, observedAtMs);
|
|
145
|
+
const first = candidates.find((candidate) => candidate.atMs <= deadlineMs);
|
|
146
|
+
if (first) {
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
lane_id: first.laneId,
|
|
150
|
+
change: first.change,
|
|
151
|
+
at: first.at,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (observedAtMs >= deadlineMs)
|
|
155
|
+
return null;
|
|
156
|
+
previous = current;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=wait.js.map
|