@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
2
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
+
import { coreNativePipelineEnabled } from "./core-native-pipeline-flag.js";
|
|
5
|
+
export const WORKFLOW_RUNNER_ASSET = "core/scripts/workflow-runner.mjs";
|
|
6
|
+
export function registerCoreWorkflowRunCommand(core) {
|
|
7
|
+
const workflow = core.command("workflow").description("Workflow orchestration");
|
|
8
|
+
const command = workflow.command("run")
|
|
9
|
+
.description("Run a workflow script or pipeline lane")
|
|
10
|
+
.helpOption(false)
|
|
11
|
+
.allowUnknownOption()
|
|
12
|
+
.allowExcessArguments(true)
|
|
13
|
+
.argument("[args...]")
|
|
14
|
+
.action(async (args, _options, current) => {
|
|
15
|
+
const argv = current.args.length > 0 ? current.args : args;
|
|
16
|
+
const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
|
|
17
|
+
if (!await coreNativePipelineEnabled()) {
|
|
18
|
+
const result = runBundledScript({ asset: WORKFLOW_RUNNER_ASSET, args: argv, cwd: process.cwd(), hqRoot: root });
|
|
19
|
+
if (result.code !== 0)
|
|
20
|
+
process.exit(result.code);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const previousRoot = process.env.HQ_ROOT;
|
|
24
|
+
process.env.HQ_ROOT = root;
|
|
25
|
+
try {
|
|
26
|
+
const native = await import("./core-pipeline-workflow-runner-native.js");
|
|
27
|
+
const runner = native;
|
|
28
|
+
const code = await runner.runWorkflowRunnerNative(argv, root);
|
|
29
|
+
if (code !== 0)
|
|
30
|
+
process.exit(code);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (previousRoot === undefined)
|
|
34
|
+
delete process.env.HQ_ROOT;
|
|
35
|
+
else
|
|
36
|
+
process.env.HQ_ROOT = previousRoot;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
describeCoreCommand(command, { execution: "native", asset: WORKFLOW_RUNNER_ASSET, root: "live" });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=core-workflow-run-command.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -52,6 +52,7 @@ import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.j
|
|
|
52
52
|
import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
|
|
53
53
|
import { registerCorePipelineWorkerTableCommand } from "./core-pipeline-workers-command.js";
|
|
54
54
|
import { registerCorePipelineRuntimeCommands } from "./core-pipeline-conductor-command.js";
|
|
55
|
+
import { registerCoreWorkflowRunCommand } from "./core-workflow-run-command.js";
|
|
55
56
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
56
57
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
57
58
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -304,6 +305,8 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
304
305
|
{ asset: "core/scripts/session-title.sh", root: "live", summary: "Rollback companion for native session title" },
|
|
305
306
|
{ asset: "core/scripts/session-title-config.sh", root: "live", summary: "Rollback companion for native session-title settings" },
|
|
306
307
|
{ asset: "core/scripts/pipeline-worker-table.sh", root: "live", summary: "Rollback companion for native pipeline worker-table" },
|
|
308
|
+
{ asset: "core/scripts/workflow-runner.mjs", root: "live", summary: "Rollback companion for native workflow runner" },
|
|
309
|
+
{ asset: "core/scripts/lib/codex-output-schema.mjs", root: "live", summary: "Schema helper required by the bundled workflow-runner rollback companion" },
|
|
307
310
|
{ asset: "core/scripts/share-suggestion-state.sh", root: "live", summary: "Rollback shell for native share-suggestion-state" },
|
|
308
311
|
{ asset: "core/scripts/repo-run-registry.sh", root: "live", summary: "Rollback shell for native repo-run-registry" },
|
|
309
312
|
{ asset: "core/scripts/detect-stale-review-base.sh", root: "live", summary: "Rollback shell for native detect-stale-review-base" },
|
|
@@ -456,6 +459,7 @@ export function registerCoreCommands(program) {
|
|
|
456
459
|
registerCoreHookStateHelpers(core);
|
|
457
460
|
registerCoreHookBatchCommand(core);
|
|
458
461
|
registerCorePipelineRuntimeCommands(core);
|
|
462
|
+
registerCoreWorkflowRunCommand(core);
|
|
459
463
|
const sessionHelperCommands = [
|
|
460
464
|
{
|
|
461
465
|
name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
|
package/dist/commands/daemon.js
CHANGED
|
@@ -907,6 +907,7 @@ async function runTmuxService(d, opts) {
|
|
|
907
907
|
cwd: hqRoot,
|
|
908
908
|
argv: opts.argv,
|
|
909
909
|
label: opts.label,
|
|
910
|
+
restartOnToolUpdate: opts.restartOnToolUpdate,
|
|
910
911
|
// Only a new version of the tool restarts the session: not a same-version
|
|
911
912
|
// reinstall, and not an hq-cli release (the host's own code rarely matters).
|
|
912
913
|
binaryFingerprint: () => toolFingerprint(opts.watch),
|
|
@@ -1094,6 +1095,7 @@ async function runServiceRemoteControlPoolHost(d, accountId) {
|
|
|
1094
1095
|
argv: [claude, ...remoteControlPoolArgs(pool)],
|
|
1095
1096
|
label: `claude remote-control pool ${accountId}`,
|
|
1096
1097
|
watch: claude,
|
|
1098
|
+
restartOnToolUpdate: false,
|
|
1097
1099
|
signal: controller.signal,
|
|
1098
1100
|
sessionEnv: {
|
|
1099
1101
|
HOME: d.home,
|
|
@@ -290,7 +290,6 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
290
290
|
balance_gate_enabled: balanceGateEnabled,
|
|
291
291
|
accounts,
|
|
292
292
|
selections,
|
|
293
|
-
lead_reserve_per_host: config.lead_reserve_per_host,
|
|
294
293
|
queued_leads: loaded.snapshots.flatMap((host) => {
|
|
295
294
|
const leads = leadLaneIds(host.lanes);
|
|
296
295
|
return host.lanes
|
package/dist/commands/lanes.js
CHANGED
|
@@ -24,6 +24,7 @@ export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
|
24
24
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
25
25
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
26
26
|
import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
|
|
27
|
+
import { finishReconcilePass } from "../lib/lanes/reconcile-result.js";
|
|
27
28
|
import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
|
|
28
29
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
29
30
|
import { registerLaneDropboxRepairCommand } from "./lanes-dropbox-repair.js";
|
|
@@ -41,6 +42,7 @@ import { runResidentCommandWithPackageLease } from "../utils/resident-package-pr
|
|
|
41
42
|
import { codexEnvEvidence, grokEnvEvidence } from "../lib/doctor/platform.js";
|
|
42
43
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
43
44
|
import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
|
|
45
|
+
import { registerDaemonLanesReconcileHandler } from "../lib/lanes/daemon-reconcile.js";
|
|
44
46
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
45
47
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
46
48
|
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
@@ -9538,21 +9540,21 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
9538
9540
|
summary.errors.push({ code: error instanceof LanesError ? error.error : "lead_autopilot_failed", message });
|
|
9539
9541
|
process.stderr.write(`hq lanes autopilot: ${message}\n`);
|
|
9540
9542
|
}
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
process.exit(1);
|
|
9543
|
+
if (!opts.inProcess) {
|
|
9544
|
+
printJson({
|
|
9545
|
+
...summary,
|
|
9546
|
+
...(autopilotRegistrySync ? { autopilot_registry_sync: autopilotRegistrySync } : {}),
|
|
9547
|
+
...(autopilot ? { autopilot } : {}),
|
|
9548
|
+
...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
|
|
9549
|
+
...(mirrorRefresh.unverifiedRemote.size > 0
|
|
9550
|
+
? {
|
|
9551
|
+
unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()]
|
|
9552
|
+
.filter((id) => !terminalizedRemoteIds.has(id)),
|
|
9553
|
+
}
|
|
9554
|
+
: {}),
|
|
9555
|
+
});
|
|
9555
9556
|
}
|
|
9557
|
+
finishReconcilePass(summary, opts.inProcess ? "daemon" : "cli");
|
|
9556
9558
|
}
|
|
9557
9559
|
async function runReconcileInstall(ctx, opts) {
|
|
9558
9560
|
// `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
|
|
@@ -9757,6 +9759,16 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9757
9759
|
withResumeHireAdmission,
|
|
9758
9760
|
};
|
|
9759
9761
|
const messageHandlers = createLaneMessageHandlers(messageHandlerDeps);
|
|
9762
|
+
const daemonReconcileCtx = { ...ctx, env: { ...ctx.env } };
|
|
9763
|
+
registerDaemonLanesReconcileHandler(async () => {
|
|
9764
|
+
const hqRoot = daemonReconcileCtx.hqRoot();
|
|
9765
|
+
daemonReconcileCtx.env.HQ_ROOT = hqRoot;
|
|
9766
|
+
daemonReconcileCtx.cwd = () => hqRoot;
|
|
9767
|
+
daemonReconcileCtx.callerCwd = () => hqRoot;
|
|
9768
|
+
await runReconcile(daemonReconcileCtx, { inProcess: true }, async (lane) => {
|
|
9769
|
+
await messageHandlers.recoverQueuedMessages(daemonReconcileCtx, lane.lane_id);
|
|
9770
|
+
});
|
|
9771
|
+
});
|
|
9760
9772
|
monitorLaneMessageApis.set(parent, (laneId, payload) => messageHandlers.deliverMonitorLaneMessage(ctx, laneId, payload));
|
|
9761
9773
|
const resumeHandlers = createLaneResumeHandlers({
|
|
9762
9774
|
onLaneWorkerStarted: deps.onLaneWorkerStarted,
|
package/dist/commands/mesh.js
CHANGED
|
@@ -307,16 +307,21 @@ function parseProjectReposJson(raw, source) {
|
|
|
307
307
|
throw err;
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
-
function projectStoriesFromJson(value, source) {
|
|
310
|
+
function projectStoriesFromJson(value, source, options = {}) {
|
|
311
311
|
const plan = Array.isArray(value) ? null : record(value);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
312
|
+
if (options.requireUserStories && (!plan || !Array.isArray(plan.userStories))) {
|
|
313
|
+
fail(`${source} must be a PRD object with a userStories array.`);
|
|
314
|
+
}
|
|
315
|
+
const rawStories = options.requireUserStories
|
|
316
|
+
? plan?.userStories ?? null
|
|
317
|
+
: Array.isArray(value)
|
|
318
|
+
? value
|
|
319
|
+
: Array.isArray(plan?.userStories)
|
|
320
|
+
? plan.userStories
|
|
321
|
+
: Array.isArray(plan?.stories)
|
|
322
|
+
? plan.stories
|
|
323
|
+
: null;
|
|
324
|
+
if (!Array.isArray(rawStories)) {
|
|
320
325
|
fail(`${source} must be a JSON story array or an object with userStories.`);
|
|
321
326
|
}
|
|
322
327
|
const rejectedStories = [];
|
|
@@ -398,7 +403,7 @@ function failProjectStoryStates(rejected, json) {
|
|
|
398
403
|
}
|
|
399
404
|
function parseProjectPlan(raw, source, options = {}) {
|
|
400
405
|
try {
|
|
401
|
-
const plan = projectStoriesFromJson(JSON.parse(raw), source);
|
|
406
|
+
const plan = projectStoriesFromJson(JSON.parse(raw), source, options);
|
|
402
407
|
if (plan.rejectedStories.length && !options.allowRejectedStories) {
|
|
403
408
|
throw new RejectedProjectStoryStatesError(plan.rejectedStories);
|
|
404
409
|
}
|
|
@@ -431,7 +436,7 @@ async function runProjectSet(projectId, opts) {
|
|
|
431
436
|
fail("Pass only one of --repos-json or --repo.");
|
|
432
437
|
}
|
|
433
438
|
const source = opts.storiesFile
|
|
434
|
-
? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile)
|
|
439
|
+
? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { requireUserStories: true })
|
|
435
440
|
: opts.storiesJson
|
|
436
441
|
? parseProjectPlan(opts.storiesJson, "--stories-json")
|
|
437
442
|
: undefined;
|
|
@@ -588,12 +593,17 @@ async function runProjectMemberAdd(projectId, personUid, opts) {
|
|
|
588
593
|
async function runProjectEnsure(projectId, opts) {
|
|
589
594
|
if (!opts.storiesFile?.trim())
|
|
590
595
|
fail("`--stories-file <prd.json>` is required.");
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
596
|
+
let source;
|
|
597
|
+
try {
|
|
598
|
+
source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { requireUserStories: true });
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
if (!(err instanceof RejectedProjectStoryStatesError) || !opts.json)
|
|
602
|
+
throw err;
|
|
603
|
+
failProjectStoryStates(err.rejected, true);
|
|
595
604
|
return;
|
|
596
605
|
}
|
|
606
|
+
const { token, company } = await withCompany(opts);
|
|
597
607
|
let current;
|
|
598
608
|
try {
|
|
599
609
|
current = await getProjectView(token, company.companyUid, projectId);
|
|
@@ -738,7 +748,10 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
738
748
|
async function runProjectReconcile(projectId, opts) {
|
|
739
749
|
if (!opts.storiesFile?.trim())
|
|
740
750
|
fail("`--stories-file <prd.json>` is required.");
|
|
741
|
-
const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, {
|
|
751
|
+
const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, {
|
|
752
|
+
requireUserStories: true,
|
|
753
|
+
allowRejectedStories: true,
|
|
754
|
+
});
|
|
742
755
|
const { token, company } = await withCompany(opts);
|
|
743
756
|
let current;
|
|
744
757
|
try {
|
|
@@ -852,7 +865,7 @@ async function runPrdSync(opts) {
|
|
|
852
865
|
return;
|
|
853
866
|
}
|
|
854
867
|
try {
|
|
855
|
-
const source = parseProjectPlan(readFileSync(file, "utf8"), file);
|
|
868
|
+
const source = parseProjectPlan(readFileSync(file, "utf8"), file, { requireUserStories: true });
|
|
856
869
|
await ensureMissingBoardStories({
|
|
857
870
|
token,
|
|
858
871
|
company: {
|
|
@@ -1091,7 +1104,7 @@ async function runBindProject(target, opts) {
|
|
|
1091
1104
|
}
|
|
1092
1105
|
if (!current.companyUid)
|
|
1093
1106
|
fail(`Session ${sessionId} has no company. Bind a company first.`);
|
|
1094
|
-
const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
|
|
1107
|
+
const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath, { requireUserStories: true });
|
|
1095
1108
|
const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
|
|
1096
1109
|
await ensureProjectBoard({
|
|
1097
1110
|
token,
|
|
@@ -1234,7 +1247,7 @@ async function runContextReconcile(opts) {
|
|
|
1234
1247
|
validateMembership,
|
|
1235
1248
|
ensureProjectBoard: token
|
|
1236
1249
|
? async ({ companyUid, projectId, prdPath }) => {
|
|
1237
|
-
const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
|
|
1250
|
+
const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath, { requireUserStories: true });
|
|
1238
1251
|
await ensureProjectBoard({
|
|
1239
1252
|
token,
|
|
1240
1253
|
company: { companyUid, companySlug: companyUid },
|
|
@@ -2,6 +2,8 @@ import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-c
|
|
|
2
2
|
import { type BotConfig } from "./config.js";
|
|
3
3
|
export declare const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
|
|
4
4
|
export declare const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
|
|
5
|
+
export declare const INBOX_MQTT_WAKE_FLAG = "agents.bot-inbox-mqtt-wake";
|
|
6
|
+
export declare const INBOX_MQTT_WAKE_FLAG_FALLBACK = true;
|
|
5
7
|
export declare const INBOX_POLL_BACKOFF_FLAG_TIMEOUT_MS = 2000;
|
|
6
8
|
export declare const INBOX_POLL_BACKOFF_FLAG_CACHE_MS: number;
|
|
7
9
|
type InboxPollBackoffFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
@@ -14,11 +16,7 @@ export interface InboxPollBackoffFlagReaderDependencies {
|
|
|
14
16
|
reportFailure?: (error: unknown) => void;
|
|
15
17
|
}
|
|
16
18
|
export declare function safeErrorClass(error: unknown): string;
|
|
17
|
-
/**
|
|
18
|
-
* Create a per-bot reader. The flag client is opened at most once per three
|
|
19
|
-
* minutes; a missing identity, failed request, or malformed snapshot keeps
|
|
20
|
-
* idle polling at its existing two-second interval.
|
|
21
|
-
*/
|
|
22
19
|
export declare function createInboxPollBackoffFlagReader(config: Pick<BotConfig, "kind" | "workerId" | "companies" | "companySlug" | "hqRoot">, dependencies?: InboxPollBackoffFlagReaderDependencies): () => Promise<boolean>;
|
|
20
|
+
export declare function createInboxMqttWakeFlagReader(config: Pick<BotConfig, "kind" | "workerId" | "companies" | "companySlug" | "hqRoot">, dependencies?: InboxPollBackoffFlagReaderDependencies): () => Promise<boolean>;
|
|
23
21
|
export {};
|
|
24
22
|
//# sourceMappingURL=inbox-poll-backoff-flag.d.ts.map
|
|
@@ -5,6 +5,8 @@ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
|
5
5
|
import { effectiveBotCompanies, effectiveBotKind } from "./config.js";
|
|
6
6
|
export const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
|
|
7
7
|
export const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
|
|
8
|
+
export const INBOX_MQTT_WAKE_FLAG = "agents.bot-inbox-mqtt-wake";
|
|
9
|
+
export const INBOX_MQTT_WAKE_FLAG_FALLBACK = true;
|
|
8
10
|
// One read every three minutes on a fresh connection. From a home network the
|
|
9
11
|
// TCP and TLS handshakes plus the resolve call take about 250-400 ms, so a
|
|
10
12
|
// 150 ms budget timed out on every read and kept the gate off.
|
|
@@ -15,8 +17,8 @@ export function safeErrorClass(error) {
|
|
|
15
17
|
const name = error instanceof Error ? error.name : "UnknownError";
|
|
16
18
|
return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
|
|
17
19
|
}
|
|
18
|
-
function reportFlagFailure(error) {
|
|
19
|
-
process.stderr.write(`HQ local bot inbox
|
|
20
|
+
function reportFlagFailure(error, fallback) {
|
|
21
|
+
process.stderr.write(`HQ local bot inbox flag lookup failed (${safeErrorClass(error)}); using default ${fallback ? "on" : "off"}.\n`);
|
|
20
22
|
}
|
|
21
23
|
function companyContexts(config, env) {
|
|
22
24
|
if (effectiveBotKind(config) !== "company")
|
|
@@ -44,9 +46,9 @@ function companyContexts(config, env) {
|
|
|
44
46
|
/**
|
|
45
47
|
* Create a per-bot reader. The flag client is opened at most once per three
|
|
46
48
|
* minutes; a missing identity, failed request, or malformed snapshot keeps
|
|
47
|
-
*
|
|
49
|
+
* registered fallback.
|
|
48
50
|
*/
|
|
49
|
-
|
|
51
|
+
function createInboxFlagReader(config, flagKey, fallback, dependencies = {}) {
|
|
50
52
|
const env = dependencies.env ?? process.env;
|
|
51
53
|
const contexts = companyContexts(config, env);
|
|
52
54
|
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
@@ -54,15 +56,15 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
|
54
56
|
const createClient = dependencies.createClient ?? createFlagClient;
|
|
55
57
|
const getToken = dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? "");
|
|
56
58
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
57
|
-
const report = dependencies.reportFailure ?? reportFlagFailure;
|
|
58
|
-
let cachedValue =
|
|
59
|
+
const report = dependencies.reportFailure ?? ((error) => reportFlagFailure(error, fallback));
|
|
60
|
+
let cachedValue = fallback;
|
|
59
61
|
let lastReadAt = Number.NEGATIVE_INFINITY;
|
|
60
62
|
let pending = null;
|
|
61
63
|
const readContext = async (context) => {
|
|
62
64
|
let client;
|
|
63
65
|
let failed = false;
|
|
64
66
|
let failureReported = false;
|
|
65
|
-
let enabled =
|
|
67
|
+
let enabled = fallback;
|
|
66
68
|
const reportFailureOnce = (error) => {
|
|
67
69
|
failed = true;
|
|
68
70
|
if (failureReported)
|
|
@@ -84,7 +86,7 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
|
84
86
|
});
|
|
85
87
|
await client.ready();
|
|
86
88
|
if (!failed) {
|
|
87
|
-
enabled = resolveFlagGate(client,
|
|
89
|
+
enabled = resolveFlagGate(client, flagKey, { fallback }, () => fallback);
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
catch (error) {
|
|
@@ -98,12 +100,12 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
|
98
100
|
reportFailureOnce(error);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
|
-
return failed ?
|
|
103
|
+
return failed ? fallback : enabled;
|
|
102
104
|
};
|
|
103
105
|
const read = async () => {
|
|
104
106
|
if (!contexts || contexts.length === 0) {
|
|
105
|
-
report(new Error("local bot inbox
|
|
106
|
-
return
|
|
107
|
+
report(new Error("local bot inbox flag has no resolvable company identity"));
|
|
108
|
+
return fallback;
|
|
107
109
|
}
|
|
108
110
|
const values = await Promise.all(contexts.map(readContext));
|
|
109
111
|
return values.every(Boolean);
|
|
@@ -123,7 +125,7 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
|
123
125
|
})
|
|
124
126
|
.catch((error) => {
|
|
125
127
|
report(error);
|
|
126
|
-
cachedValue =
|
|
128
|
+
cachedValue = fallback;
|
|
127
129
|
return cachedValue;
|
|
128
130
|
})
|
|
129
131
|
.finally(() => {
|
|
@@ -132,4 +134,10 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
|
132
134
|
return pending;
|
|
133
135
|
};
|
|
134
136
|
}
|
|
137
|
+
export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
138
|
+
return createInboxFlagReader(config, INBOX_POLL_BACKOFF_FLAG, INBOX_POLL_BACKOFF_FLAG_FALLBACK, dependencies);
|
|
139
|
+
}
|
|
140
|
+
export function createInboxMqttWakeFlagReader(config, dependencies = {}) {
|
|
141
|
+
return createInboxFlagReader(config, INBOX_MQTT_WAKE_FLAG, INBOX_MQTT_WAKE_FLAG_FALLBACK, dependencies);
|
|
142
|
+
}
|
|
135
143
|
//# sourceMappingURL=inbox-poll-backoff-flag.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
export { AGENT_INBOX_PUSH_STATE_BASENAME } from "../mesh/live/paths.js";
|
|
3
|
+
type WakeEvent = "wake" | "timeout" | "aborted";
|
|
4
|
+
type Watcher = Pick<fs.FSWatcher, "close"> & {
|
|
5
|
+
on?: (event: "error", listener: (error: Error) => void) => unknown;
|
|
6
|
+
};
|
|
7
|
+
export interface InboxWakeWatcher {
|
|
8
|
+
wait(delayMs: number, signal?: AbortSignal): Promise<WakeEvent>;
|
|
9
|
+
isLiveFor?(agentUid: string): boolean;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Watches the content-free Work Mesh inbox sequence, with a bounded poll fallback. */
|
|
13
|
+
export declare function createInboxWakeWatcher(root: string, options?: {
|
|
14
|
+
watch?: (root: string, listener: (event: string, filename: string | Buffer | null) => void) => Watcher;
|
|
15
|
+
onError?: (error: unknown) => void;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}): InboxWakeWatcher;
|
|
18
|
+
//# sourceMappingURL=inbox-wake.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { AGENT_INBOX_PUSH_STATE_BASENAME } from "../mesh/live/paths.js";
|
|
4
|
+
export { AGENT_INBOX_PUSH_STATE_BASENAME } from "../mesh/live/paths.js";
|
|
5
|
+
const INBOX_PUSH_SUBSCRIPTION_STALE_MS = 15_000;
|
|
6
|
+
function readWakeState(root, onError) {
|
|
7
|
+
const target = path.join(root, AGENT_INBOX_PUSH_STATE_BASENAME);
|
|
8
|
+
try {
|
|
9
|
+
const info = fs.lstatSync(target);
|
|
10
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
11
|
+
return { wakeSequence: 0, connected: false, heartbeatAtMs: 0 };
|
|
12
|
+
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
13
|
+
return {
|
|
14
|
+
wakeSequence: Number.isSafeInteger(parsed.wakeSequence) && Number(parsed.wakeSequence) >= 0
|
|
15
|
+
? Number(parsed.wakeSequence)
|
|
16
|
+
: 0,
|
|
17
|
+
connected: parsed.connected === true,
|
|
18
|
+
heartbeatAtMs: typeof parsed.heartbeatAtMs === "number" && Number.isFinite(parsed.heartbeatAtMs)
|
|
19
|
+
? parsed.heartbeatAtMs
|
|
20
|
+
: 0,
|
|
21
|
+
...(typeof parsed.agentUid === "string" ? { agentUid: parsed.agentUid } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT")
|
|
26
|
+
onError?.(error);
|
|
27
|
+
return { wakeSequence: 0, connected: false, heartbeatAtMs: 0 };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Watches the content-free Work Mesh inbox sequence, with a bounded poll fallback. */
|
|
31
|
+
export function createInboxWakeWatcher(root, options = {}) {
|
|
32
|
+
const now = options.now ?? Date.now;
|
|
33
|
+
let lastState = readWakeState(root, options.onError);
|
|
34
|
+
let lastSequence = lastState.wakeSequence;
|
|
35
|
+
let closed = false;
|
|
36
|
+
let pendingWake = false;
|
|
37
|
+
const waiters = new Set();
|
|
38
|
+
let watcher;
|
|
39
|
+
const signalWake = () => {
|
|
40
|
+
pendingWake = waiters.size === 0;
|
|
41
|
+
for (const resolve of waiters)
|
|
42
|
+
resolve("wake");
|
|
43
|
+
waiters.clear();
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
watcher = (options.watch ?? ((directory, listener) => fs.watch(directory, listener)))(root, (_event, filename) => {
|
|
47
|
+
if (filename !== null && filename.toString() !== AGENT_INBOX_PUSH_STATE_BASENAME)
|
|
48
|
+
return;
|
|
49
|
+
const nextState = readWakeState(root, options.onError);
|
|
50
|
+
const sequenceAdvanced = nextState.wakeSequence > lastSequence;
|
|
51
|
+
const subscriptionChanged = nextState.connected !== lastState.connected || nextState.agentUid !== lastState.agentUid;
|
|
52
|
+
lastState = nextState;
|
|
53
|
+
if (sequenceAdvanced) {
|
|
54
|
+
lastSequence = nextState.wakeSequence;
|
|
55
|
+
signalWake();
|
|
56
|
+
}
|
|
57
|
+
else if (subscriptionChanged) {
|
|
58
|
+
signalWake();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
watcher.on?.("error", (error) => {
|
|
62
|
+
options.onError?.(error);
|
|
63
|
+
const failedWatcher = watcher;
|
|
64
|
+
watcher = undefined;
|
|
65
|
+
try {
|
|
66
|
+
failedWatcher?.close();
|
|
67
|
+
}
|
|
68
|
+
catch (closeError) {
|
|
69
|
+
options.onError?.(closeError);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Close the read/register race: a writer can advance the sequence after
|
|
73
|
+
// the baseline read but before fs.watch is attached.
|
|
74
|
+
const currentState = readWakeState(root, options.onError);
|
|
75
|
+
if (currentState.wakeSequence > lastSequence) {
|
|
76
|
+
lastSequence = currentState.wakeSequence;
|
|
77
|
+
signalWake();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
options.onError?.(error);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
wait(delayMs, signal) {
|
|
85
|
+
if (closed || signal?.aborted)
|
|
86
|
+
return Promise.resolve("aborted");
|
|
87
|
+
if (pendingWake) {
|
|
88
|
+
pendingWake = false;
|
|
89
|
+
return Promise.resolve("wake");
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let settled = false;
|
|
93
|
+
const finish = (event) => {
|
|
94
|
+
if (settled)
|
|
95
|
+
return;
|
|
96
|
+
settled = true;
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
waiters.delete(finish);
|
|
99
|
+
signal?.removeEventListener("abort", abort);
|
|
100
|
+
resolve(event);
|
|
101
|
+
};
|
|
102
|
+
const abort = () => finish("aborted");
|
|
103
|
+
const timer = setTimeout(() => finish("timeout"), Math.max(0, delayMs));
|
|
104
|
+
waiters.add(finish);
|
|
105
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
isLiveFor(agentUid) {
|
|
109
|
+
const state = readWakeState(root, options.onError);
|
|
110
|
+
const ageMs = now() - state.heartbeatAtMs;
|
|
111
|
+
return state.connected && state.agentUid === agentUid &&
|
|
112
|
+
ageMs >= 0 && ageMs <= INBOX_PUSH_SUBSCRIPTION_STALE_MS;
|
|
113
|
+
},
|
|
114
|
+
close() {
|
|
115
|
+
if (closed)
|
|
116
|
+
return;
|
|
117
|
+
closed = true;
|
|
118
|
+
watcher?.close();
|
|
119
|
+
for (const resolve of waiters)
|
|
120
|
+
resolve("aborted");
|
|
121
|
+
waiters.clear();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=inbox-wake.js.map
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -58,7 +58,9 @@ import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js"
|
|
|
58
58
|
import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
|
|
59
59
|
import { type BotState, type BotStatusFile } from "./status.js";
|
|
60
60
|
import { type CliUpdate, type CliUpdateWatch } from "./cli-update.js";
|
|
61
|
+
import { createInboxWakeWatcher } from "./inbox-wake.js";
|
|
61
62
|
export declare const INBOX_POLL_INTERVAL_MS = 2000;
|
|
63
|
+
export declare const INBOX_MQTT_WAKE_FALLBACK_MS = 60000;
|
|
62
64
|
export declare const HEARTBEAT_INTERVAL_MS = 30000;
|
|
63
65
|
/** First 429/5xx wait when Retry-After is absent (inbox polls at 2s). */
|
|
64
66
|
export declare const API_BACKOFF_BASE_MS = 4000;
|
|
@@ -154,6 +156,12 @@ export interface BotRunDeps {
|
|
|
154
156
|
signInRetryMs?: number;
|
|
155
157
|
/** How long model turns are held after FAILURE_LIMIT failures in a window (default FAILURE_WINDOW_MS). */
|
|
156
158
|
modelHoldMs?: number;
|
|
159
|
+
/** Kill-switch reader for the Work Mesh MQTT inbox wake path. */
|
|
160
|
+
inboxMqttWakeEnabled?: () => Promise<boolean>;
|
|
161
|
+
/** Work Mesh state directory; tests use a scratch directory. */
|
|
162
|
+
inboxWakeRoot?: string;
|
|
163
|
+
/** Test seam for the inbox wake source. */
|
|
164
|
+
createInboxWakeWatcher?: typeof createInboxWakeWatcher;
|
|
157
165
|
/**
|
|
158
166
|
* Downloads a chat attachment's bytes with the bot's own machine identity
|
|
159
167
|
* (vault presign). Unset = attachments are listed with a "cannot download"
|