@indigoai-us/hq-cli 5.97.3-rc.0 → 5.97.3
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
CHANGED
|
@@ -27,6 +27,44 @@ import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
|
27
27
|
export declare const VALID_EFFORTS: Set<string>;
|
|
28
28
|
/** Service-tier (speed) values hq-pro accepts on `runtime-config`. */
|
|
29
29
|
export declare const VALID_TIERS: Set<string>;
|
|
30
|
+
/** Agent runtimes hq-pro accepts on `POST /v1/agents` (`AgentProvider`). */
|
|
31
|
+
export declare const VALID_PROVIDERS: Set<string>;
|
|
32
|
+
/** Auth modes hq-pro accepts on `POST /v1/agents` (`CodexAuthMode`). */
|
|
33
|
+
export declare const VALID_AUTH_MODES: Set<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a closed-set option value, or exit(1) with a message naming the
|
|
36
|
+
* offending input and the legal set.
|
|
37
|
+
*
|
|
38
|
+
* WHY THIS EXISTS. `--effort` and `--tier` were already validated this way, but
|
|
39
|
+
* `--provider` and `--auth-mode` were resolved with a ternary that FELL BACK to
|
|
40
|
+
* the default on anything unrecognised:
|
|
41
|
+
*
|
|
42
|
+
* opts.provider === "grok" ? "grok" : opts.provider ? "codex" : undefined
|
|
43
|
+
* opts.authMode === "apiKey" ? "apiKey" : "subscription"
|
|
44
|
+
*
|
|
45
|
+
* So `--provider claude` (before claude was supported) silently provisioned a
|
|
46
|
+
* CODEX box, and `--auth-mode typo` silently provisioned in SUBSCRIPTION mode.
|
|
47
|
+
* Both paths are behind a `--yes` charge gate, so the operator paid $100/month
|
|
48
|
+
* for a runtime they did not ask for, with nothing in the output to say a
|
|
49
|
+
* substitution had happened. A wrong flag must fail loudly, never resolve to a
|
|
50
|
+
* plausible default.
|
|
51
|
+
*
|
|
52
|
+
* `undefined` in ⇒ `undefined` out: an OMITTED option is not an invalid one, and
|
|
53
|
+
* keeps the server-side default authoritative.
|
|
54
|
+
*
|
|
55
|
+
* Matching is trim + lowercase to be forgiving of shell padding and casing,
|
|
56
|
+
* EXCEPT that the returned value is the canonical member of the set — so the
|
|
57
|
+
* wire value is always exactly what hq-pro's validators expect. Note `apiKey`
|
|
58
|
+
* is camelCase on the wire, so the comparison set is keyed on the lowercased
|
|
59
|
+
* form and mapped back (see {@link canonicalizeOptionValue}).
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseEnumOption(raw: string | undefined, allowed: Set<string>, flagName: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Case-insensitively map `raw` onto its canonical member of `allowed`, or
|
|
64
|
+
* `undefined` when it is not a member. Split out from {@link parseEnumOption}
|
|
65
|
+
* so the mapping is unit-testable without a process exit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function canonicalizeOptionValue(raw: string, allowed: Set<string>): string | undefined;
|
|
30
68
|
/**
|
|
31
69
|
* A non-2xx from the `/v1/agents` control plane. Carries the HTTP status and
|
|
32
70
|
* the registry error `code` (when present) so callers can branch — notably a
|
|
@@ -89,7 +127,7 @@ export interface ProvisionAgentInput {
|
|
|
89
127
|
name: string;
|
|
90
128
|
slug: string;
|
|
91
129
|
codexAuthMode: "subscription" | "apiKey";
|
|
92
|
-
provider?: "codex" | "grok";
|
|
130
|
+
provider?: "codex" | "grok" | "claude";
|
|
93
131
|
codexApiKey?: string;
|
|
94
132
|
idempotencyKey: string;
|
|
95
133
|
title?: string;
|
package/dist/commands/agents.js
CHANGED
|
@@ -37,6 +37,61 @@ export const VALID_EFFORTS = new Set([
|
|
|
37
37
|
]);
|
|
38
38
|
/** Service-tier (speed) values hq-pro accepts on `runtime-config`. */
|
|
39
39
|
export const VALID_TIERS = new Set(["default", "priority"]);
|
|
40
|
+
/** Agent runtimes hq-pro accepts on `POST /v1/agents` (`AgentProvider`). */
|
|
41
|
+
export const VALID_PROVIDERS = new Set(["codex", "grok", "claude"]);
|
|
42
|
+
/** Auth modes hq-pro accepts on `POST /v1/agents` (`CodexAuthMode`). */
|
|
43
|
+
export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a closed-set option value, or exit(1) with a message naming the
|
|
46
|
+
* offending input and the legal set.
|
|
47
|
+
*
|
|
48
|
+
* WHY THIS EXISTS. `--effort` and `--tier` were already validated this way, but
|
|
49
|
+
* `--provider` and `--auth-mode` were resolved with a ternary that FELL BACK to
|
|
50
|
+
* the default on anything unrecognised:
|
|
51
|
+
*
|
|
52
|
+
* opts.provider === "grok" ? "grok" : opts.provider ? "codex" : undefined
|
|
53
|
+
* opts.authMode === "apiKey" ? "apiKey" : "subscription"
|
|
54
|
+
*
|
|
55
|
+
* So `--provider claude` (before claude was supported) silently provisioned a
|
|
56
|
+
* CODEX box, and `--auth-mode typo` silently provisioned in SUBSCRIPTION mode.
|
|
57
|
+
* Both paths are behind a `--yes` charge gate, so the operator paid $100/month
|
|
58
|
+
* for a runtime they did not ask for, with nothing in the output to say a
|
|
59
|
+
* substitution had happened. A wrong flag must fail loudly, never resolve to a
|
|
60
|
+
* plausible default.
|
|
61
|
+
*
|
|
62
|
+
* `undefined` in ⇒ `undefined` out: an OMITTED option is not an invalid one, and
|
|
63
|
+
* keeps the server-side default authoritative.
|
|
64
|
+
*
|
|
65
|
+
* Matching is trim + lowercase to be forgiving of shell padding and casing,
|
|
66
|
+
* EXCEPT that the returned value is the canonical member of the set — so the
|
|
67
|
+
* wire value is always exactly what hq-pro's validators expect. Note `apiKey`
|
|
68
|
+
* is camelCase on the wire, so the comparison set is keyed on the lowercased
|
|
69
|
+
* form and mapped back (see {@link canonicalizeOptionValue}).
|
|
70
|
+
*/
|
|
71
|
+
export function parseEnumOption(raw, allowed, flagName) {
|
|
72
|
+
if (raw === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const canonical = canonicalizeOptionValue(raw, allowed);
|
|
75
|
+
if (canonical === undefined) {
|
|
76
|
+
const legal = [...allowed].join(", ");
|
|
77
|
+
console.error(chalk.red(`Invalid ${flagName} '${raw}': must be one of ${legal}`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return canonical;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Case-insensitively map `raw` onto its canonical member of `allowed`, or
|
|
84
|
+
* `undefined` when it is not a member. Split out from {@link parseEnumOption}
|
|
85
|
+
* so the mapping is unit-testable without a process exit.
|
|
86
|
+
*/
|
|
87
|
+
export function canonicalizeOptionValue(raw, allowed) {
|
|
88
|
+
const needle = raw.trim().toLowerCase();
|
|
89
|
+
for (const candidate of allowed) {
|
|
90
|
+
if (candidate.toLowerCase() === needle)
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
40
95
|
/**
|
|
41
96
|
* A non-2xx from the `/v1/agents` control plane. Carries the HTTP status and
|
|
42
97
|
* the registry error `code` (when present) so callers can branch — notably a
|
|
@@ -335,16 +390,28 @@ export function registerAgentsCommand(program) {
|
|
|
335
390
|
.description("Provision a new cloud agent ($100/month — requires --yes)")
|
|
336
391
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
337
392
|
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
338
|
-
.option("--provider <provider>", "Runtime: codex | grok (default codex)")
|
|
339
|
-
.option("--auth-mode <mode>", "
|
|
393
|
+
.option("--provider <provider>", "Runtime: codex | grok | claude (default codex). claude is subscription-only")
|
|
394
|
+
.option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
|
|
340
395
|
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
341
396
|
.option("--title <title>", "Org-chart job title")
|
|
342
397
|
.option("--description <text>", "Short description / bio")
|
|
343
398
|
.option("--yes", "Confirm the $100/month charge (required to provision)")
|
|
344
399
|
.action(async function (name, opts) {
|
|
345
400
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
401
|
+
// Both of these previously fell back to their default on an
|
|
402
|
+
// unrecognised value, silently provisioning a billable box the operator
|
|
403
|
+
// did not ask for. They now exit(1) instead — see parseEnumOption.
|
|
404
|
+
const authMode = parseEnumOption(opts.authMode, VALID_AUTH_MODES, "--auth-mode") ?? "subscription";
|
|
405
|
+
const provider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
|
|
406
|
+
// claude is subscription-only on hq-pro (rejectIncompatibleProviderAuthMode
|
|
407
|
+
// returns AGENT_PROVIDER_INCOMPATIBLE_WITH_AUTH_MODE). Catch it here so the
|
|
408
|
+
// operator gets a direct message instead of a 400 from the control plane
|
|
409
|
+
// AFTER clearing the charge gate. The server stays authoritative; this is
|
|
410
|
+
// only a friendlier, earlier copy of the same rule.
|
|
411
|
+
if (provider === "claude" && authMode === "apiKey") {
|
|
412
|
+
console.error(chalk.red("The claude provider is subscription-only — drop --auth-mode apiKey."));
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
348
415
|
// Resolve the API key from the environment (never from a flag value) when
|
|
349
416
|
// apiKey mode is requested — validated BEFORE the paid gate so we don't
|
|
350
417
|
// charge and then fail.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StdioOptions } from 'node:child_process';
|
|
1
2
|
import { type QmdProcessResult, type RunQmdOptions } from './index.js';
|
|
2
3
|
export type BackgroundResult = {
|
|
3
4
|
state: 'skipped-agent' | 'skipped' | 'quiet' | 'busy' | 'completed' | 'update-failed' | 'terminated';
|
|
@@ -17,6 +18,7 @@ export type BackgroundDependencies = {
|
|
|
17
18
|
runQmd: (args: string[], options?: RunQmdOptions) => QmdProcessResult;
|
|
18
19
|
spawnWorker: (options: {
|
|
19
20
|
logPath: string;
|
|
21
|
+
fallbackLogPath?: string;
|
|
20
22
|
}) => number;
|
|
21
23
|
/** Test seam for simulating a competing owner replacing the atomic record. */
|
|
22
24
|
afterOwnerPublish?: (ownerFile: string) => void;
|
|
@@ -28,6 +30,60 @@ export type BackgroundStatus = {
|
|
|
28
30
|
lock: 'held' | 'stale' | 'free';
|
|
29
31
|
completedAt?: number;
|
|
30
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Injectable seams for the launcher's worker-log open and detached spawn. Split
|
|
35
|
+
* out purely as a test seam so a synthetic open failure can be forced without
|
|
36
|
+
* touching real files or the invoking uid.
|
|
37
|
+
*/
|
|
38
|
+
export type SpawnWorkerIo = {
|
|
39
|
+
mkdirSync: (directory: string) => void;
|
|
40
|
+
openSync: (file: string) => number;
|
|
41
|
+
closeSync: (fd: number) => void;
|
|
42
|
+
spawn: (command: string, args: string[], options: {
|
|
43
|
+
detached: boolean;
|
|
44
|
+
stdio: StdioOptions;
|
|
45
|
+
}) => {
|
|
46
|
+
pid?: number;
|
|
47
|
+
unref: () => void;
|
|
48
|
+
};
|
|
49
|
+
/** Report a degraded open without swallowing it (stderr notice + breadcrumb). */
|
|
50
|
+
report: (info: WorkerLogDegradation) => void;
|
|
51
|
+
};
|
|
52
|
+
export type WorkerLogDegradation = {
|
|
53
|
+
requested: string;
|
|
54
|
+
used: string | null;
|
|
55
|
+
usedFallback: boolean;
|
|
56
|
+
code?: string;
|
|
57
|
+
syscall?: string;
|
|
58
|
+
};
|
|
59
|
+
type WorkerLogOpen = {
|
|
60
|
+
fd: number | null;
|
|
61
|
+
logPath: string | null;
|
|
62
|
+
usedFallback: boolean;
|
|
63
|
+
error?: {
|
|
64
|
+
code?: string;
|
|
65
|
+
syscall?: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Open the detached worker's log, degrading instead of crashing. Try the
|
|
70
|
+
* requested path; on ANY open failure retry once against a per-user fallback
|
|
71
|
+
* under the caller's own $HOME; if that also fails, return a no-log result so
|
|
72
|
+
* the launcher still spawns the worker.
|
|
73
|
+
*
|
|
74
|
+
* This is the fix for Sentry indigo-d0/hq-cli 7663380187: HQ's /handoff always
|
|
75
|
+
* points --log at the fixed, shared, world-writable /tmp/qmd-handoff.log, and an
|
|
76
|
+
* unguarded fs.openSync(logPath, 'a') here threw EACCES whenever that file
|
|
77
|
+
* already existed owned by another uid, so the reindex worker never spawned and
|
|
78
|
+
* the raw errno reached Sentry. A diagnostic side-channel must not take down the
|
|
79
|
+
* feature it exists to observe — every other worker-log writer in this module
|
|
80
|
+
* (appendWorkerLog, capWorkerLog) already swallows I/O failures the same way.
|
|
81
|
+
*/
|
|
82
|
+
export declare function openWorkerLog(logPath: string, fallbackLogPath: string | undefined, io: Pick<SpawnWorkerIo, 'mkdirSync' | 'openSync'>): WorkerLogOpen;
|
|
83
|
+
export declare function defaultSpawnWorker({ logPath, fallbackLogPath }: {
|
|
84
|
+
logPath: string;
|
|
85
|
+
fallbackLogPath?: string;
|
|
86
|
+
}, io?: SpawnWorkerIo): number;
|
|
31
87
|
/** Defaults used by the CLI; tests supply every nondeterministic dependency. */
|
|
32
88
|
export declare function defaultBackgroundDependencies(hqRoot: string): BackgroundDependencies;
|
|
33
89
|
/** Match the shell forwarder's hosted-agent markers before looking up qmd. */
|
|
@@ -39,4 +95,5 @@ export declare function runBackgroundLauncher(dependencies: BackgroundDependenci
|
|
|
39
95
|
export declare function runBackgroundWorker(dependencies: BackgroundDependencies): Promise<BackgroundResult>;
|
|
40
96
|
/** Report the background lock and latest successful completion for `hq index status`. */
|
|
41
97
|
export declare function backgroundStatus(dependencies: BackgroundDependencies): BackgroundStatus;
|
|
98
|
+
export {};
|
|
42
99
|
//# sourceMappingURL=background.d.ts.map
|
|
@@ -1,20 +1,99 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { Sentry } from '../../sentry.js';
|
|
4
5
|
import { reconcileCollections as defaultReconcileCollections, resolveQmdBin as defaultResolveQmdBin, runQmd as defaultRunQmd, } from './index.js';
|
|
5
6
|
const LOCK_NAME = 'qmd-reindex-bg.lock';
|
|
6
7
|
const COMPLETE_NAME = 'qmd-reindex-bg.completed';
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function errnoInfo(error) {
|
|
9
|
+
const e = error;
|
|
10
|
+
return { code: e?.code, syscall: e?.syscall };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Open the detached worker's log, degrading instead of crashing. Try the
|
|
14
|
+
* requested path; on ANY open failure retry once against a per-user fallback
|
|
15
|
+
* under the caller's own $HOME; if that also fails, return a no-log result so
|
|
16
|
+
* the launcher still spawns the worker.
|
|
17
|
+
*
|
|
18
|
+
* This is the fix for Sentry indigo-d0/hq-cli 7663380187: HQ's /handoff always
|
|
19
|
+
* points --log at the fixed, shared, world-writable /tmp/qmd-handoff.log, and an
|
|
20
|
+
* unguarded fs.openSync(logPath, 'a') here threw EACCES whenever that file
|
|
21
|
+
* already existed owned by another uid, so the reindex worker never spawned and
|
|
22
|
+
* the raw errno reached Sentry. A diagnostic side-channel must not take down the
|
|
23
|
+
* feature it exists to observe — every other worker-log writer in this module
|
|
24
|
+
* (appendWorkerLog, capWorkerLog) already swallows I/O failures the same way.
|
|
25
|
+
*/
|
|
26
|
+
export function openWorkerLog(logPath, fallbackLogPath, io) {
|
|
27
|
+
try {
|
|
28
|
+
io.mkdirSync(path.dirname(logPath));
|
|
29
|
+
return { fd: io.openSync(logPath), logPath, usedFallback: false };
|
|
30
|
+
}
|
|
31
|
+
catch (primaryError) {
|
|
32
|
+
const error = errnoInfo(primaryError);
|
|
33
|
+
if (fallbackLogPath && fallbackLogPath !== logPath) {
|
|
34
|
+
try {
|
|
35
|
+
io.mkdirSync(path.dirname(fallbackLogPath));
|
|
36
|
+
return { fd: io.openSync(fallbackLogPath), logPath: fallbackLogPath, usedFallback: true, error };
|
|
37
|
+
}
|
|
38
|
+
catch { /* fall through to the no-log result below */ }
|
|
39
|
+
}
|
|
40
|
+
return { fd: null, logPath: null, usedFallback: false, error };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Surface a degraded worker-log open without swallowing it: one best-effort
|
|
45
|
+
* stderr line naming the rejected path and where output went, plus a Sentry
|
|
46
|
+
* breadcrumb carrying only the errno — never the fallback path, which lives
|
|
47
|
+
* under $HOME. The condition is now handled and degraded, so it is deliberately
|
|
48
|
+
* NOT captured as an exception; the breadcrumb just gives the next genuine
|
|
49
|
+
* failure at this site the errno evidence this event lacked.
|
|
50
|
+
*/
|
|
51
|
+
function reportWorkerLogDegradation(info) {
|
|
52
|
+
const reason = info.code ? ` (${info.code}${info.syscall ? ` on ${info.syscall}` : ''})` : '';
|
|
53
|
+
const destination = info.used ? `writing worker output to ${info.used} instead` : 'disabling worker output';
|
|
54
|
+
try {
|
|
55
|
+
process.stderr.write(`hq: cannot open background reindex log ${info.requested}${reason}; ${destination}.\n`);
|
|
56
|
+
}
|
|
57
|
+
catch { /* the notice itself is best-effort and must never crash the launcher */ }
|
|
58
|
+
try {
|
|
59
|
+
Sentry.addBreadcrumb({
|
|
60
|
+
category: 'qmd.background',
|
|
61
|
+
level: 'warning',
|
|
62
|
+
message: 'worker log open degraded',
|
|
63
|
+
data: { code: info.code, syscall: info.syscall, usedFallback: info.usedFallback },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch { /* breadcrumb is best-effort; no Sentry client is active in tests */ }
|
|
67
|
+
}
|
|
68
|
+
const defaultSpawnWorkerIo = {
|
|
69
|
+
mkdirSync: (directory) => { fs.mkdirSync(directory, { recursive: true }); },
|
|
70
|
+
openSync: (file) => fs.openSync(file, 'a'),
|
|
71
|
+
closeSync: (fd) => { fs.closeSync(fd); },
|
|
72
|
+
spawn: (command, args, options) => spawn(command, args, options),
|
|
73
|
+
report: reportWorkerLogDegradation,
|
|
74
|
+
};
|
|
75
|
+
export function defaultSpawnWorker({ logPath, fallbackLogPath }, io = defaultSpawnWorkerIo) {
|
|
76
|
+
const opened = openWorkerLog(logPath, fallbackLogPath, io);
|
|
77
|
+
if (opened.error) {
|
|
78
|
+
io.report({ requested: logPath, used: opened.logPath, usedFallback: opened.usedFallback, ...opened.error });
|
|
79
|
+
}
|
|
10
80
|
const entry = process.argv[1];
|
|
11
81
|
if (!entry)
|
|
12
82
|
throw new Error('Cannot determine hq CLI entrypoint for background worker');
|
|
13
|
-
|
|
83
|
+
// Hand the child the log path we actually opened so its own appendWorkerLog /
|
|
84
|
+
// capWorkerLog write to the same file. When nothing could be opened, omit
|
|
85
|
+
// --log entirely so the child resolves its own default rather than re-failing
|
|
86
|
+
// on the path we already rejected — the reindex itself must still run.
|
|
87
|
+
const logArgs = opened.logPath ? ['--log', opened.logPath] : [];
|
|
88
|
+
const stdio = opened.fd === null
|
|
89
|
+
? ['ignore', 'ignore', 'ignore']
|
|
90
|
+
: ['ignore', opened.fd, opened.fd];
|
|
91
|
+
const child = io.spawn(process.execPath, [entry, 'index', 'background', '--worker', ...logArgs], {
|
|
14
92
|
detached: true,
|
|
15
|
-
stdio
|
|
93
|
+
stdio,
|
|
16
94
|
});
|
|
17
|
-
|
|
95
|
+
if (opened.fd !== null)
|
|
96
|
+
io.closeSync(opened.fd);
|
|
18
97
|
child.unref();
|
|
19
98
|
if (!child.pid)
|
|
20
99
|
throw new Error('Unable to start qmd background worker');
|
|
@@ -358,7 +437,15 @@ export function runBackgroundLauncher(dependencies) {
|
|
|
358
437
|
catch {
|
|
359
438
|
return { state: 'skipped' };
|
|
360
439
|
}
|
|
361
|
-
|
|
440
|
+
// Fall back to a per-user log under the caller's own $HOME when the requested
|
|
441
|
+
// (often the shared, world-writable /tmp) log cannot be opened — see
|
|
442
|
+
// openWorkerLog. `home` is validated non-empty above, so the fallback is
|
|
443
|
+
// always inside a directory this user owns.
|
|
444
|
+
const fallbackLogPath = path.join(home, '.hq', 'logs', 'qmd-handoff.log');
|
|
445
|
+
return {
|
|
446
|
+
state: 'launched',
|
|
447
|
+
pid: dependencies.spawnWorker({ logPath: workerLogPath(dependencies.env), fallbackLogPath }),
|
|
448
|
+
};
|
|
362
449
|
}
|
|
363
450
|
/** Run the single-flight cleanup → update → embed pipeline in a worker only. */
|
|
364
451
|
export async function runBackgroundWorker(dependencies) {
|