@indigoai-us/hq-cli 5.345.128 → 5.345.130
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 +22 -0
- package/assets/scaffold/core/scripts/lib/codex-output-schema.mjs +304 -0
- package/assets/scaffold/core/scripts/workflow-runner.mjs +2890 -0
- package/dist/command-catalog.generated.d.ts +29 -0
- package/dist/command-catalog.generated.js +34 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
- package/dist/commands/core-pipeline-workflow-runner-native.d.ts +175 -0
- package/dist/commands/core-pipeline-workflow-runner-native.js +3237 -0
- package/dist/commands/core-workflow-run-command.d.ts +4 -0
- package/dist/commands/core-workflow-run-command.js +41 -0
- package/dist/commands/core.js +4 -0
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/lanes-accounts.js +0 -1
- package/dist/commands/lanes.js +26 -14
- package/dist/commands/mesh.js +32 -19
- package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +3 -5
- package/dist/lib/bot/inbox-poll-backoff-flag.js +20 -12
- package/dist/lib/bot/inbox-wake.d.ts +18 -0
- package/dist/lib/bot/inbox-wake.js +125 -0
- package/dist/lib/bot/run.d.ts +8 -0
- package/dist/lib/bot/run.js +51 -7
- package/dist/lib/bot/runtime/messages-stream.js +31 -3
- package/dist/lib/core-utils/workflow-runner-schema.d.ts +7 -0
- package/dist/lib/core-utils/workflow-runner-schema.js +259 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +6 -3
- package/dist/lib/daemon/flag-monitor.d.ts +4 -1
- package/dist/lib/daemon/flag-monitor.js +12 -6
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +11 -5
- package/dist/lib/daemon/hq-child-bootstrap.mjs +47 -7
- package/dist/lib/daemon/kill-switch-flags.d.ts +5 -0
- package/dist/lib/daemon/kill-switch-flags.js +5 -0
- package/dist/lib/daemon/lanes-reconcile-loader.d.ts +7 -0
- package/dist/lib/daemon/lanes-reconcile-loader.js +24 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.d.ts +17 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.js +80 -0
- package/dist/lib/daemon/remote-control.d.ts +8 -5
- package/dist/lib/daemon/remote-control.js +8 -7
- package/dist/lib/daemon/run.d.ts +4 -1
- package/dist/lib/daemon/run.js +133 -20
- package/dist/lib/doctor/json-output.d.ts +15 -3
- package/dist/lib/doctor/json-output.js +13 -1
- package/dist/lib/lanes/config.d.ts +8 -4
- package/dist/lib/lanes/config.js +30 -36
- package/dist/lib/lanes/daemon-reconcile.d.ts +3 -0
- package/dist/lib/lanes/daemon-reconcile.js +8 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile-result.d.ts +10 -0
- package/dist/lib/lanes/reconcile-result.js +11 -0
- package/dist/lib/lanes/reconcile.js +3 -26
- package/dist/lib/lanes/throttle.js +5 -9
- package/dist/lib/lanes/types.d.ts +4 -5
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.js +8 -1
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/mesh/story-status.d.ts +2 -2
- package/dist/lib/mesh/story-status.js +2 -1
- package/dist/utils/resident-package-preload.js +6 -0
- package/package.json +1 -1
package/dist/lib/bot/run.js
CHANGED
|
@@ -89,8 +89,11 @@ import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
|
89
89
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
90
90
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
91
91
|
import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
|
|
92
|
-
import { createInboxPollBackoffFlagReader, safeErrorClass } from "./inbox-poll-backoff-flag.js";
|
|
92
|
+
import { createInboxMqttWakeFlagReader, createInboxPollBackoffFlagReader, safeErrorClass } from "./inbox-poll-backoff-flag.js";
|
|
93
|
+
import { createInboxWakeWatcher } from "./inbox-wake.js";
|
|
94
|
+
import { workMeshRoot } from "../mesh/live/paths.js";
|
|
93
95
|
export const INBOX_POLL_INTERVAL_MS = 2_000;
|
|
96
|
+
export const INBOX_MQTT_WAKE_FALLBACK_MS = 60_000;
|
|
94
97
|
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
95
98
|
/** First 429/5xx wait when Retry-After is absent (inbox polls at 2s). */
|
|
96
99
|
export const API_BACKOFF_BASE_MS = 4_000;
|
|
@@ -337,6 +340,21 @@ export async function runBot(deps) {
|
|
|
337
340
|
const runTurn = deps.runTurn ?? ((rt, input) => runRuntimeTurn(rt, input));
|
|
338
341
|
const inboxIntervalMs = deps.inboxIntervalMs ?? INBOX_POLL_INTERVAL_MS;
|
|
339
342
|
const inboxPollBackoffEnabled = deps.inboxPollBackoffEnabled ?? createInboxPollBackoffFlagReader(config);
|
|
343
|
+
const inboxMqttWakeEnabled = deps.inboxMqttWakeEnabled ?? createInboxMqttWakeFlagReader(config);
|
|
344
|
+
const makeInboxWakeWatcher = deps.createInboxWakeWatcher ?? createInboxWakeWatcher;
|
|
345
|
+
const inboxWakeWatcherRef = { current: null };
|
|
346
|
+
const getInboxWakeWatcher = () => {
|
|
347
|
+
const watcher = inboxWakeWatcherRef.current ?? makeInboxWakeWatcher(deps.inboxWakeRoot ?? workMeshRoot(), {
|
|
348
|
+
onError: (error) => log("warn", `Work Mesh inbox wake unavailable (${safeErrorClass(error)}); retaining the fallback inbox poll`),
|
|
349
|
+
});
|
|
350
|
+
inboxWakeWatcherRef.current = watcher;
|
|
351
|
+
return watcher;
|
|
352
|
+
};
|
|
353
|
+
const closeInboxWakeWatcher = () => {
|
|
354
|
+
const watcher = inboxWakeWatcherRef.current;
|
|
355
|
+
watcher?.close();
|
|
356
|
+
inboxWakeWatcherRef.current = null;
|
|
357
|
+
};
|
|
340
358
|
const jobQuietOptionEnabled = deps.jobQuietOptionEnabled ?? botJobQuietOptionEnabled;
|
|
341
359
|
const heartbeatIntervalMs = deps.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
342
360
|
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
@@ -443,6 +461,7 @@ export async function runBot(deps) {
|
|
|
443
461
|
if (state === "stopped" || state === "failed")
|
|
444
462
|
return;
|
|
445
463
|
state = final;
|
|
464
|
+
closeInboxWakeWatcher();
|
|
446
465
|
setStatus({ state: final, ...(final === "failed" ? { lastError: reason.slice(0, 400) } : {}) });
|
|
447
466
|
log(final === "failed" ? "error" : "info", `bot ${final}: ${reason}`);
|
|
448
467
|
if (!deps.skipPidLock)
|
|
@@ -771,7 +790,7 @@ export async function runBot(deps) {
|
|
|
771
790
|
inboxWaitGeneration += 1;
|
|
772
791
|
activeInboxWait?.abort();
|
|
773
792
|
};
|
|
774
|
-
const waitForInbox = async (delayMs, adaptive) => {
|
|
793
|
+
const waitForInbox = async (delayMs, adaptive, useMqttWake = false) => {
|
|
775
794
|
if (!adaptive) {
|
|
776
795
|
await sleep(delayMs);
|
|
777
796
|
return;
|
|
@@ -781,8 +800,13 @@ export async function runBot(deps) {
|
|
|
781
800
|
while (remainingMs > 0 && !stopping && state === "running") {
|
|
782
801
|
const controller = new AbortController();
|
|
783
802
|
activeInboxWait = controller;
|
|
784
|
-
const chunkMs = Math.min(inboxIntervalMs, remainingMs);
|
|
785
|
-
|
|
803
|
+
const chunkMs = useMqttWake ? remainingMs : Math.min(inboxIntervalMs, remainingMs);
|
|
804
|
+
if (useMqttWake) {
|
|
805
|
+
await getInboxWakeWatcher().wait(chunkMs, controller.signal);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
await sleep(chunkMs, controller.signal);
|
|
809
|
+
}
|
|
786
810
|
if (activeInboxWait === controller)
|
|
787
811
|
activeInboxWait = null;
|
|
788
812
|
if (stopping || state !== "running")
|
|
@@ -795,9 +819,8 @@ export async function runBot(deps) {
|
|
|
795
819
|
continue;
|
|
796
820
|
}
|
|
797
821
|
remainingMs -= chunkMs;
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
if (remainingMs > 0 && pollForCliUpdate())
|
|
822
|
+
// The MQTT wake or bounded fallback also checks for CLI updates.
|
|
823
|
+
if (remainingMs > 0 && !useMqttWake && pollForCliUpdate())
|
|
801
824
|
break;
|
|
802
825
|
}
|
|
803
826
|
};
|
|
@@ -2332,6 +2355,18 @@ export async function runBot(deps) {
|
|
|
2332
2355
|
log("warn", `inbox poll backoff flag lookup failed (${safeErrorClass(error)}); using default-off`);
|
|
2333
2356
|
return false;
|
|
2334
2357
|
});
|
|
2358
|
+
const useMqttWake = await inboxMqttWakeEnabled().catch((error) => {
|
|
2359
|
+
log("warn", `inbox MQTT wake flag lookup failed (${safeErrorClass(error)}); using default-on`);
|
|
2360
|
+
return true;
|
|
2361
|
+
});
|
|
2362
|
+
// Arm before pulling so a wake arriving between the pull and wait is not
|
|
2363
|
+
// lost. The watcher also checks the sequence again after registration.
|
|
2364
|
+
if (useMqttWake) {
|
|
2365
|
+
getInboxWakeWatcher();
|
|
2366
|
+
}
|
|
2367
|
+
else {
|
|
2368
|
+
closeInboxWakeWatcher();
|
|
2369
|
+
}
|
|
2335
2370
|
let items;
|
|
2336
2371
|
try {
|
|
2337
2372
|
items = await api.pullInbox(config.agentUid);
|
|
@@ -2361,6 +2396,15 @@ export async function runBot(deps) {
|
|
|
2361
2396
|
}
|
|
2362
2397
|
if (stopping || state !== "running")
|
|
2363
2398
|
break;
|
|
2399
|
+
if (useMqttWake) {
|
|
2400
|
+
const hasLiveInboxSubscription = items.length === 0 &&
|
|
2401
|
+
getInboxWakeWatcher().isLiveFor?.(config.agentUid) === true;
|
|
2402
|
+
const delayMs = items.length > 0 || !hasLiveInboxSubscription
|
|
2403
|
+
? inboxIntervalMs
|
|
2404
|
+
: INBOX_MQTT_WAKE_FALLBACK_MS;
|
|
2405
|
+
await waitForInbox(delayMs, true, true);
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2364
2408
|
if (!useInboxBackoff) {
|
|
2365
2409
|
inboxBackoffIndex = 0;
|
|
2366
2410
|
await waitForInbox(inboxIntervalMs, false);
|
|
@@ -87,7 +87,7 @@ export function messagesStreamResult(stdout, runtimeName) {
|
|
|
87
87
|
if (!sawJson)
|
|
88
88
|
throw new RuntimeError(`${runtimeName} returned non-JSON output`, stdout.trim().slice(-400), null, { excerptIsOutputTail: true });
|
|
89
89
|
if (result?.is_error === true) {
|
|
90
|
-
const detail = typeof result.result === "string" && result.result ? result.result : String(result.subtype ?? "error");
|
|
90
|
+
const detail = typeof result.result === "string" && result.result ? result.result : (resultErrors(result) ?? String(result.subtype ?? "error"));
|
|
91
91
|
throw new RuntimeError(`${runtimeName} reported an error: ${detail.slice(0, 200)}`, detail.slice(0, 400), null);
|
|
92
92
|
}
|
|
93
93
|
const text = (typeof result?.result === "string" && result.result.trim() ? result.result : lastText) ?? "";
|
|
@@ -143,8 +143,12 @@ export function messagesStreamFailure(stdout) {
|
|
|
143
143
|
let reason;
|
|
144
144
|
for (const line of stdout.split("\n")) {
|
|
145
145
|
const ev = parseJsonLine(line);
|
|
146
|
-
if (ev?.type === "result" && ev.is_error === true
|
|
147
|
-
|
|
146
|
+
if (ev?.type === "result" && ev.is_error === true) {
|
|
147
|
+
if (typeof ev.result === "string" && ev.result.trim())
|
|
148
|
+
reason = ev.result.trim();
|
|
149
|
+
else
|
|
150
|
+
reason = resultErrors(ev) ?? reason;
|
|
151
|
+
}
|
|
148
152
|
if (ev?.type === "error" && typeof ev.message === "string" && ev.message.trim())
|
|
149
153
|
reason = ev.message.trim();
|
|
150
154
|
}
|
|
@@ -187,8 +191,32 @@ export function prettyJsonFailure(stdout) {
|
|
|
187
191
|
return ev.result.trim();
|
|
188
192
|
if (typeof ev.message === "string" && ev.message.trim())
|
|
189
193
|
return ev.message.trim();
|
|
194
|
+
const errors = resultErrors(ev);
|
|
195
|
+
if (errors)
|
|
196
|
+
return errors;
|
|
190
197
|
if (typeof ev.subtype === "string" && ev.subtype.trim() && ev.subtype !== "success")
|
|
191
198
|
return ev.subtype.trim();
|
|
192
199
|
return undefined;
|
|
193
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The `errors` array on a Claude result envelope. A failed `--resume` ends
|
|
203
|
+
* with `{"subtype":"error_during_execution","is_error":true,"errors":["No
|
|
204
|
+
* conversation found with session ID: …"]}` and no `result` text; without
|
|
205
|
+
* this the failure reads as the bare subtype, the stale-session check never
|
|
206
|
+
* matches, and every turn on that scope fails the same way.
|
|
207
|
+
*/
|
|
208
|
+
function resultErrors(ev) {
|
|
209
|
+
if (!Array.isArray(ev.errors))
|
|
210
|
+
return undefined;
|
|
211
|
+
const parts = ev.errors.flatMap((e) => {
|
|
212
|
+
if (typeof e === "string")
|
|
213
|
+
return e.trim() ? [e.trim()] : [];
|
|
214
|
+
if (e && typeof e === "object" && typeof e.message === "string") {
|
|
215
|
+
const m = e.message.trim();
|
|
216
|
+
return m ? [m] : [];
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
});
|
|
220
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
221
|
+
}
|
|
194
222
|
//# sourceMappingURL=messages-stream.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
|
|
2
|
+
export declare function normalizeWorkflowRunnerEnvelope(raw: string): string;
|
|
3
|
+
/** Makes a copy in Codex's strict structured-output dialect. */
|
|
4
|
+
export declare function strictifySchemaForCodex(schema: unknown): unknown;
|
|
5
|
+
/** Removes nulls that Codex had to add for originally optional properties. */
|
|
6
|
+
export declare function stripStrictNulls(value: unknown, schema: unknown): unknown;
|
|
7
|
+
//# sourceMappingURL=workflow-runner-schema.d.ts.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
function isJsonObject(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function jsonDumps(value) {
|
|
5
|
+
if (value === null || typeof value !== "object")
|
|
6
|
+
return JSON.stringify(value) ?? "null";
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return `[${value.map(jsonDumps).join(", ")}]`;
|
|
9
|
+
const entries = Object.entries(value);
|
|
10
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}: ${jsonDumps(child)}`).join(", ")}}`;
|
|
11
|
+
}
|
|
12
|
+
function balancedJsonEnd(text, start) {
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let inString = false;
|
|
15
|
+
for (let i = start; i < text.length; i += 1) {
|
|
16
|
+
const char = text[i];
|
|
17
|
+
if (inString) {
|
|
18
|
+
if (char === "\\")
|
|
19
|
+
i += 1;
|
|
20
|
+
else if (char === '"')
|
|
21
|
+
inString = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === '"')
|
|
25
|
+
inString = true;
|
|
26
|
+
else if (char === "{" || char === "[")
|
|
27
|
+
depth += 1;
|
|
28
|
+
else if (char === "}" || char === "]") {
|
|
29
|
+
depth -= 1;
|
|
30
|
+
if (depth === 0)
|
|
31
|
+
return i + 1;
|
|
32
|
+
if (depth < 0)
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
/** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
|
|
39
|
+
export function normalizeWorkflowRunnerEnvelope(raw) {
|
|
40
|
+
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
41
|
+
const candidates = [];
|
|
42
|
+
const fence = /```[A-Za-z0-9_-]*\s*\n([\s\S]*?)```/g;
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = fence.exec(text)) !== null) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(match[1]?.trim() ?? "");
|
|
47
|
+
if (isJsonObject(parsed))
|
|
48
|
+
candidates.push(parsed);
|
|
49
|
+
}
|
|
50
|
+
catch { /* malformed fences are skipped by the shell normalizer */ }
|
|
51
|
+
}
|
|
52
|
+
if (candidates.length === 0) {
|
|
53
|
+
let offset = 0;
|
|
54
|
+
while (offset < text.length) {
|
|
55
|
+
const start = text.indexOf("{", offset);
|
|
56
|
+
if (start < 0)
|
|
57
|
+
break;
|
|
58
|
+
const end = balancedJsonEnd(text, start);
|
|
59
|
+
if (end > 0) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(text.slice(start, end));
|
|
62
|
+
if (isJsonObject(parsed))
|
|
63
|
+
candidates.push(parsed);
|
|
64
|
+
offset = end;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
catch { /* continue scanning after this opening brace */ }
|
|
68
|
+
}
|
|
69
|
+
offset = start + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (candidates.length === 0)
|
|
73
|
+
throw new Error("normalize: no JSON object found");
|
|
74
|
+
if (candidates.length > 1) {
|
|
75
|
+
const tagged = candidates.filter((candidate) => candidate.schema === "hq-phase-handoff/v1" || candidate.schema === "hq-phase-envelope/v1");
|
|
76
|
+
if (tagged.length !== 1)
|
|
77
|
+
throw new Error(`normalize: ${candidates.length} JSON objects found, expected exactly one`);
|
|
78
|
+
return `${jsonDumps(tagged[0])}\n`;
|
|
79
|
+
}
|
|
80
|
+
return `${jsonDumps(candidates[0])}\n`;
|
|
81
|
+
}
|
|
82
|
+
function isRecord(value) {
|
|
83
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
function typesOf(node) {
|
|
86
|
+
if (node.type === undefined)
|
|
87
|
+
return [];
|
|
88
|
+
return Array.isArray(node.type) ? [...node.type] : [node.type];
|
|
89
|
+
}
|
|
90
|
+
function isObjectNode(node) {
|
|
91
|
+
const types = typesOf(node);
|
|
92
|
+
return types.length ? types.includes("object") : isRecord(node.properties);
|
|
93
|
+
}
|
|
94
|
+
const MAP_KEYS = ["properties", "$defs", "definitions"];
|
|
95
|
+
const LIST_KEYS = ["anyOf", "oneOf", "allOf", "prefixItems"];
|
|
96
|
+
const SINGLE_KEYS = ["items", "not", "if", "then", "else", "contains", "propertyNames"];
|
|
97
|
+
function schemaError(at, message) {
|
|
98
|
+
return new Error(`codex output schema at ${at}: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function widenToNullable(node, at) {
|
|
101
|
+
if (!isRecord(node))
|
|
102
|
+
return;
|
|
103
|
+
const types = typesOf(node);
|
|
104
|
+
if (types.length) {
|
|
105
|
+
if (!types.includes("null"))
|
|
106
|
+
node.type = [...types, "null"];
|
|
107
|
+
if (Array.isArray(node.enum) && !node.enum.includes(null))
|
|
108
|
+
node.enum = [...node.enum, null];
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
112
|
+
const branches = node[key];
|
|
113
|
+
if (!Array.isArray(branches) || branches.length === 0)
|
|
114
|
+
continue;
|
|
115
|
+
if (!branches.some((branch) => isRecord(branch) && typesOf(branch).includes("null")))
|
|
116
|
+
branches.push({ type: "null" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (typeof node.$ref === "string" || Array.isArray(node.allOf)) {
|
|
120
|
+
const inner = { ...node };
|
|
121
|
+
for (const key of Object.keys(node))
|
|
122
|
+
delete node[key];
|
|
123
|
+
node.anyOf = [inner, { type: "null" }];
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isRecord(node.properties)) {
|
|
127
|
+
node.type = ["object", "null"];
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw schemaError(at, "this property is optional (absent from `required`) but its schema declares neither a type nor a composition, so there is no way to express \"no answer\" in the strict dialect. Give it a type, or list it in `required`.");
|
|
131
|
+
}
|
|
132
|
+
function strictifyInPlace(node, at) {
|
|
133
|
+
if (Array.isArray(node)) {
|
|
134
|
+
node.forEach((child, index) => strictifyInPlace(child, `${at}[${index}]`));
|
|
135
|
+
return node;
|
|
136
|
+
}
|
|
137
|
+
if (!isRecord(node))
|
|
138
|
+
return node;
|
|
139
|
+
for (const key of MAP_KEYS) {
|
|
140
|
+
if (!isRecord(node[key]))
|
|
141
|
+
continue;
|
|
142
|
+
for (const [name, child] of Object.entries(node[key]))
|
|
143
|
+
strictifyInPlace(child, `${at}.${key}.${name}`);
|
|
144
|
+
}
|
|
145
|
+
for (const key of LIST_KEYS) {
|
|
146
|
+
if (Array.isArray(node[key]))
|
|
147
|
+
node[key].forEach((child, index) => strictifyInPlace(child, `${at}.${key}[${index}]`));
|
|
148
|
+
}
|
|
149
|
+
for (const key of SINGLE_KEYS)
|
|
150
|
+
if (node[key] !== undefined)
|
|
151
|
+
strictifyInPlace(node[key], `${at}.${key}`);
|
|
152
|
+
if (!isObjectNode(node))
|
|
153
|
+
return node;
|
|
154
|
+
if (node.additionalProperties !== undefined && node.additionalProperties !== false) {
|
|
155
|
+
throw schemaError(at, "strict structured output permits no dynamic keys, so `additionalProperties` must be false (got "
|
|
156
|
+
+ `${JSON.stringify(node.additionalProperties)}). Model a map as an array of {key, value} entries, or run this agent on the grok or claude engine, which have no strict-schema flag.`);
|
|
157
|
+
}
|
|
158
|
+
node.additionalProperties = false;
|
|
159
|
+
if (typesOf(node).length === 0)
|
|
160
|
+
node.type = "object";
|
|
161
|
+
const props = isRecord(node.properties) ? node.properties : null;
|
|
162
|
+
if (!props)
|
|
163
|
+
return node;
|
|
164
|
+
const keys = Object.keys(props);
|
|
165
|
+
const declared = Array.isArray(node.required) ? node.required.filter((key) => typeof key === "string") : [];
|
|
166
|
+
const orphans = declared.filter((key) => !keys.includes(key));
|
|
167
|
+
if (orphans.length)
|
|
168
|
+
throw schemaError(at, `\`required\` lists ${orphans.map((key) => JSON.stringify(key)).join(", ")} with no matching property. Strict rejects a required name that is not in \`properties\`, and dropping it would leave the result validator demanding a key the model can no longer emit. Add the property, or remove the name from \`required\`.`);
|
|
169
|
+
const required = [];
|
|
170
|
+
for (const key of declared)
|
|
171
|
+
if (!required.includes(key))
|
|
172
|
+
required.push(key);
|
|
173
|
+
for (const key of keys) {
|
|
174
|
+
if (required.includes(key))
|
|
175
|
+
continue;
|
|
176
|
+
widenToNullable(props[key], `${at}.properties.${key}`);
|
|
177
|
+
required.push(key);
|
|
178
|
+
}
|
|
179
|
+
node.required = required;
|
|
180
|
+
return node;
|
|
181
|
+
}
|
|
182
|
+
/** Makes a copy in Codex's strict structured-output dialect. */
|
|
183
|
+
export function strictifySchemaForCodex(schema) {
|
|
184
|
+
if (!isRecord(schema) && !Array.isArray(schema))
|
|
185
|
+
return schema;
|
|
186
|
+
return strictifyInPlace(JSON.parse(JSON.stringify(schema)), "<root>");
|
|
187
|
+
}
|
|
188
|
+
const MAX_REF_DEPTH = 16;
|
|
189
|
+
function resolveRef(node, root, depth = 0) {
|
|
190
|
+
if (!isRecord(node) || typeof node.$ref !== "string" || depth >= MAX_REF_DEPTH || !isRecord(root))
|
|
191
|
+
return node;
|
|
192
|
+
const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(node.$ref);
|
|
193
|
+
if (!match)
|
|
194
|
+
return node;
|
|
195
|
+
const bucket = root[match[1] ?? ""];
|
|
196
|
+
const target = isRecord(bucket) ? bucket[match[2] ?? ""] : undefined;
|
|
197
|
+
return isRecord(target) ? resolveRef(target, root, depth + 1) : node;
|
|
198
|
+
}
|
|
199
|
+
function allowsNull(node, root, depth = 0) {
|
|
200
|
+
const resolved = resolveRef(node, root, depth);
|
|
201
|
+
if (!isRecord(resolved))
|
|
202
|
+
return true;
|
|
203
|
+
const types = typesOf(resolved);
|
|
204
|
+
if (types.length)
|
|
205
|
+
return types.includes("null");
|
|
206
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
207
|
+
if (Array.isArray(resolved[key]))
|
|
208
|
+
return resolved[key].some((branch) => depth < MAX_REF_DEPTH && allowsNull(branch, root, depth + 1));
|
|
209
|
+
}
|
|
210
|
+
return isRecord(resolved.properties) ? false : true;
|
|
211
|
+
}
|
|
212
|
+
function effectiveSchema(node, root, depth = 0) {
|
|
213
|
+
const resolved = resolveRef(node, root, depth);
|
|
214
|
+
if (!isRecord(resolved))
|
|
215
|
+
return null;
|
|
216
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
217
|
+
if (!Array.isArray(resolved[key]))
|
|
218
|
+
continue;
|
|
219
|
+
const branches = resolved[key].map((branch) => resolveRef(branch, root, depth + 1))
|
|
220
|
+
.filter(isRecord).filter((branch) => !(typesOf(branch).length === 1 && typesOf(branch)[0] === "null"));
|
|
221
|
+
if (branches.length !== 1 || depth >= MAX_REF_DEPTH)
|
|
222
|
+
return null;
|
|
223
|
+
return effectiveSchema(branches[0], root, depth + 1);
|
|
224
|
+
}
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
function stripInPlace(value, schema, root, depth = 0) {
|
|
228
|
+
const effective = effectiveSchema(schema, root, depth);
|
|
229
|
+
if (!effective)
|
|
230
|
+
return value;
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
if (effective.items !== undefined)
|
|
233
|
+
value.forEach((item) => stripInPlace(item, effective.items, root, depth + 1));
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
if (!isRecord(value))
|
|
237
|
+
return value;
|
|
238
|
+
const props = isRecord(effective.properties) ? effective.properties : null;
|
|
239
|
+
if (!props)
|
|
240
|
+
return value;
|
|
241
|
+
const required = Array.isArray(effective.required) ? effective.required : [];
|
|
242
|
+
for (const [key, sub] of Object.entries(props)) {
|
|
243
|
+
if (!(key in value))
|
|
244
|
+
continue;
|
|
245
|
+
if (value[key] === null && !required.includes(key) && !allowsNull(sub, root)) {
|
|
246
|
+
delete value[key];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
stripInPlace(value[key], sub, root, depth + 1);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
/** Removes nulls that Codex had to add for originally optional properties. */
|
|
254
|
+
export function stripStrictNulls(value, schema) {
|
|
255
|
+
if (!isRecord(schema) || value === null || typeof value !== "object")
|
|
256
|
+
return value;
|
|
257
|
+
return stripInPlace(JSON.parse(JSON.stringify(value)), schema, schema);
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=workflow-runner-schema.js.map
|
|
@@ -109,6 +109,10 @@ export interface PlanContext {
|
|
|
109
109
|
syncRunnerProcesses: readonly SyncRunnerProcess[] | null;
|
|
110
110
|
/** PID recorded in the watch-owner status, when it is readable. */
|
|
111
111
|
syncRunnerOwnerPid: number | null;
|
|
112
|
+
/** The daemon's in-process lane watcher owns reconcile while its kill switch is on. */
|
|
113
|
+
lanesReconcileInProcess?: boolean;
|
|
114
|
+
/** The active hq-cloud sync runner owns post-sync QMD indexing. */
|
|
115
|
+
searchIndexOwnedBySyncRunner?: boolean;
|
|
112
116
|
/** Sync runner children already started by this daemon before their lease is written. */
|
|
113
117
|
daemonSyncRunnerPids?: ReadonlySet<number>;
|
|
114
118
|
/** The protocol-event heartbeat watchdog timeout for the sync runner. */
|
|
@@ -227,14 +227,15 @@ function planService(service, ctx) {
|
|
|
227
227
|
}),
|
|
228
228
|
];
|
|
229
229
|
case "lanes-reconcile":
|
|
230
|
+
if (ctx.lanesReconcileInProcess)
|
|
231
|
+
return [];
|
|
230
232
|
return [
|
|
231
233
|
base(service, {
|
|
232
234
|
kind: "scheduled",
|
|
233
235
|
// Polls every 15 s so local keep-alive resumes are admitted on time (#1396).
|
|
234
236
|
// Each pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS), so
|
|
235
|
-
// desktops
|
|
236
|
-
//
|
|
237
|
-
// cadence, or no run, when no lane can need the 15 s poll.
|
|
237
|
+
// desktops reconcile in-process on lane-store changes with a bounded
|
|
238
|
+
// timer for due keep-alive work, avoiding a fresh CLI process per poll.
|
|
238
239
|
intervalSec: 15,
|
|
239
240
|
initialDelaySec: 30,
|
|
240
241
|
command: { type: "hq", args: ["lanes", "reconcile"] },
|
|
@@ -246,6 +247,8 @@ function planService(service, ctx) {
|
|
|
246
247
|
}),
|
|
247
248
|
];
|
|
248
249
|
case "search-index":
|
|
250
|
+
if (ctx.searchIndexOwnedBySyncRunner)
|
|
251
|
+
return [];
|
|
249
252
|
return [
|
|
250
253
|
base(service, {
|
|
251
254
|
kind: "scheduled",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface BoundedDaemonFlagMonitorOptions {
|
|
2
2
|
read: () => Promise<boolean>;
|
|
3
|
+
/** Registry default, also used for absent rows and stale reads. */
|
|
4
|
+
fallback?: boolean;
|
|
3
5
|
now?: () => number;
|
|
4
6
|
onChange?: (enabled: boolean) => void;
|
|
5
7
|
onReadError?: (error: unknown) => void;
|
|
@@ -15,7 +17,8 @@ export interface BoundedDaemonFlagMonitor {
|
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Refreshes a long-running daemon's flag independently of incoming requests.
|
|
18
|
-
* A failed read keeps
|
|
20
|
+
* A failed read keeps a default-off setting only until its 60s age limit; a
|
|
21
|
+
* default-on kill switch immediately returns to its registered safe default.
|
|
19
22
|
*/
|
|
20
23
|
export declare function createBoundedDaemonFlagMonitor(options: BoundedDaemonFlagMonitorOptions): BoundedDaemonFlagMonitor;
|
|
21
24
|
//# sourceMappingURL=flag-monitor.d.ts.map
|
|
@@ -3,11 +3,13 @@ const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30_000;
|
|
|
3
3
|
const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60_000;
|
|
4
4
|
/**
|
|
5
5
|
* Refreshes a long-running daemon's flag independently of incoming requests.
|
|
6
|
-
* A failed read keeps
|
|
6
|
+
* A failed read keeps a default-off setting only until its 60s age limit; a
|
|
7
|
+
* default-on kill switch immediately returns to its registered safe default.
|
|
7
8
|
*/
|
|
8
9
|
export function createBoundedDaemonFlagMonitor(options) {
|
|
9
10
|
const now = options.now ?? (() => performance.now());
|
|
10
|
-
|
|
11
|
+
const fallback = options.fallback ?? false;
|
|
12
|
+
let value = fallback ? true : undefined;
|
|
11
13
|
let lastSuccessAt;
|
|
12
14
|
let lastAttemptAt;
|
|
13
15
|
let lastReadFailed = false;
|
|
@@ -23,7 +25,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
23
25
|
};
|
|
24
26
|
const expireIfStale = () => {
|
|
25
27
|
if (!closed && lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
|
|
26
|
-
publish(
|
|
28
|
+
publish(fallback);
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
31
|
const refresh = () => {
|
|
@@ -44,13 +46,17 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
44
46
|
// Expire from the elapsed timer itself, not a wall-clock comparison.
|
|
45
47
|
expiryTimer = setTimeout(() => {
|
|
46
48
|
if (!closed && successVersion === thisSuccessVersion)
|
|
47
|
-
publish(
|
|
49
|
+
publish(fallback);
|
|
48
50
|
}, HQ_ANYWHERE_FLAG_MAX_STALENESS_MS);
|
|
49
51
|
expiryTimer.unref?.();
|
|
50
52
|
return next;
|
|
51
53
|
}).catch((error) => {
|
|
52
54
|
lastReadFailed = true;
|
|
53
55
|
options.onReadError?.(error);
|
|
56
|
+
if (fallback) {
|
|
57
|
+
publish(true);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
54
60
|
expireIfStale();
|
|
55
61
|
return value ?? false;
|
|
56
62
|
}).finally(() => {
|
|
@@ -69,7 +75,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
69
75
|
expireIfStale();
|
|
70
76
|
if (lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
|
|
71
77
|
void refresh();
|
|
72
|
-
return
|
|
78
|
+
return fallback;
|
|
73
79
|
}
|
|
74
80
|
if (value === undefined)
|
|
75
81
|
return refresh();
|
|
@@ -82,7 +88,7 @@ export function createBoundedDaemonFlagMonitor(options) {
|
|
|
82
88
|
const age = lastSuccessAt === undefined ? null : Math.max(0, now() - lastSuccessAt);
|
|
83
89
|
const fresh = age !== null && age < HQ_ANYWHERE_FLAG_MAX_STALENESS_MS;
|
|
84
90
|
return {
|
|
85
|
-
enabled: value === true && fresh,
|
|
91
|
+
enabled: value === true && (fresh || fallback),
|
|
86
92
|
lastSuccessfulReadAgeMs: age,
|
|
87
93
|
heldThroughReadError: lastReadFailed && fresh,
|
|
88
94
|
};
|
|
@@ -17,6 +17,8 @@ export interface DaemonFlagClient {
|
|
|
17
17
|
}
|
|
18
18
|
export interface ReadDaemonFlagOptions {
|
|
19
19
|
env?: NodeJS.ProcessEnv;
|
|
20
|
+
/** Registry default for this key; new kill switches use true. */
|
|
21
|
+
fallback?: boolean;
|
|
20
22
|
/** Controls how this read combines a local person setting with the registry value. */
|
|
21
23
|
localOverrideMode?: "respect" | "off-only" | "ignore";
|
|
22
24
|
createClient?: (options: FlagClientOptions) => DaemonFlagClient;
|
package/dist/lib/daemon/flag.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
17
17
|
const env = options.env ?? process.env;
|
|
18
18
|
if (flagKey === HQ_ANYWHERE_RUNTIME_FLAG && hqAnywhereRuntimeLocallyDisabled(env))
|
|
19
19
|
return false;
|
|
20
|
-
const fallback = flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
|
|
20
|
+
const fallback = options.fallback ?? flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
|
|
21
21
|
const override = readLocalOverride(flagKey, env);
|
|
22
22
|
const localOverrideMode = options.localOverrideMode ?? "respect";
|
|
23
23
|
if (override !== null && (localOverrideMode === "respect" || (localOverrideMode === "off-only" && !override)))
|
|
@@ -53,8 +53,11 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
53
53
|
onError(error);
|
|
54
54
|
}
|
|
55
55
|
const resolution = client.explain(flagKey, { fallback });
|
|
56
|
-
if (failed
|
|
57
|
-
|
|
56
|
+
if (failed) {
|
|
57
|
+
if (options.throwOnError && !fallback) {
|
|
58
|
+
throw new Error("hq-flags registry read failed", { cause: reportedError });
|
|
59
|
+
}
|
|
60
|
+
return fallback;
|
|
58
61
|
}
|
|
59
62
|
if (resolution.source === "snapshot")
|
|
60
63
|
return resolution.value === true;
|
|
@@ -63,8 +66,11 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
63
66
|
catch (error) {
|
|
64
67
|
if (!failed)
|
|
65
68
|
onError(error);
|
|
66
|
-
if (
|
|
67
|
-
|
|
69
|
+
if (failed) {
|
|
70
|
+
if (options.throwOnError && !fallback) {
|
|
71
|
+
throw new Error("hq-flags registry read failed", { cause: reportedError ?? error });
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
68
74
|
}
|
|
69
75
|
return fallback;
|
|
70
76
|
}
|