@indigoai-us/hq-cli 5.345.21 → 5.345.23
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 +9 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/core-audit-log-fast.d.ts +2 -0
- package/dist/commands/core-audit-log-fast.js +12 -0
- package/dist/commands/core-detect-stale-core-policy-mirror-fast.d.ts +2 -0
- package/dist/commands/core-detect-stale-core-policy-mirror-fast.js +5 -0
- package/dist/commands/core-dm-bind-fast.d.ts +2 -0
- package/dist/commands/core-dm-bind-fast.js +14 -0
- package/dist/commands/core-generate-workers-registry-fast.d.ts +2 -0
- package/dist/commands/core-generate-workers-registry-fast.js +5 -0
- package/dist/commands/core-git-pack-extension-check-fast.d.ts +2 -0
- package/dist/commands/core-git-pack-extension-check-fast.js +12 -0
- package/dist/commands/core-refresh-vault-access-fast.d.ts +2 -0
- package/dist/commands/core-refresh-vault-access-fast.js +12 -0
- package/dist/commands/core-refresh-vault-access-native.d.ts +1 -0
- package/dist/commands/core-refresh-vault-access-native.js +10 -5
- package/dist/commands/core-scan-packages-fast.d.ts +2 -0
- package/dist/commands/core-scan-packages-fast.js +5 -0
- package/dist/commands/core-work-mesh-bind-trusted-fast.d.ts +2 -0
- package/dist/commands/core-work-mesh-bind-trusted-fast.js +12 -0
- package/dist/commands/lanes.js +1 -0
- package/dist/commands/personal-targeted-push-flag.d.ts +1 -1
- package/dist/commands/personal-targeted-push-flag.js +6 -2
- package/dist/fast-core.js +6 -3
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +101 -0
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -1
- package/dist/lib/daemon/server.d.ts +2 -1
- package/dist/lib/daemon/server.js +38 -21
- package/dist/lib/daemon/session-registry.d.ts +1 -0
- package/dist/lib/daemon/session-registry.js +3 -0
- package/dist/lib/lanes/host-capacity.d.ts +5 -0
- package/dist/lib/lanes/host-capacity.js +18 -0
- package/dist/lib/lanes/provider-rejection.d.ts +2 -0
- package/dist/lib/lanes/provider-rejection.js +16 -1
- package/dist/lib/lanes/quota.d.ts +5 -5
- package/dist/lib/lanes/quota.js +45 -9
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +1 -0
- package/dist/lib/lanes/watcher.d.ts +0 -12
- package/dist/lib/lanes/watcher.js +8 -2
- package/dist/lib/scan-packages/index.js +1 -1
- package/dist/utils/pack-contribution-links.d.ts +15 -0
- package/dist/utils/pack-contribution-links.js +33 -0
- package/dist/utils/pack-contributions.d.ts +2 -14
- package/dist/utils/pack-contributions.js +3 -55
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.23] — 2026-10-06
|
|
6
|
+
- Route eight additional isolated native core commands through the fast CLI entry.
|
|
7
|
+
- hqd applies the runtime kill switch to new hook and MCP operations within the existing 60-second freshness bound. A local opt-out also disables the daemon, while a local opt-in cannot override an operator-off value.
|
|
8
|
+
|
|
9
|
+
## [5.345.22] — 2026-10-06
|
|
10
|
+
- Fix Claude quota holds so time-only reset messages keep accounts unavailable until the provider's stated UTC reset, and expose the hold state in host capacity output.
|
|
11
|
+
|
|
5
12
|
## [5.345.21] — 2026-10-06
|
|
13
|
+
- `hq sync push --personal <path>` now waits up to 500 ms for the startup flag registry snapshot before evaluating `cli.personal-targeted-push-paths`, while preserving the legacy refusal when the registry is unavailable.
|
|
14
|
+
|
|
6
15
|
- Hook timeout reports now show elapsed time against the configured budget and include available phase and hook attribution.
|
|
7
16
|
- Reconcile hire-grant carve accounting across resume, reparent and grant updates; decreases no longer fail because sibling grants exceed the senior allocation.
|
|
8
17
|
- Route eligible Linux pull-request checks and local release guards through the registered ARM64 self-hosted runner. Keep the shared npm/OIDC publish job on its existing runner.
|
package/dist/commands/cloud.js
CHANGED
|
@@ -840,7 +840,7 @@ export function registerCloudCommands(program) {
|
|
|
840
840
|
"to have already resolved entity + credentials. Pick one.");
|
|
841
841
|
}
|
|
842
842
|
// Closes hq-cli#25 — see `assertNoPersonalPositionalPaths` doc-block.
|
|
843
|
-
const targetedPersonalPathsEnabled = Boolean(options.personal && paths && paths.length > 0 && personalTargetedPushPathsEnabled());
|
|
843
|
+
const targetedPersonalPathsEnabled = Boolean(options.personal && paths && paths.length > 0 && await personalTargetedPushPathsEnabled());
|
|
844
844
|
assertNoPersonalPositionalPaths(options, paths, targetedPersonalPathsEnabled);
|
|
845
845
|
if (targetedPersonalPathsEnabled) {
|
|
846
846
|
explicitPersonalPaths = resolveExplicitPersonalPushPaths(options.hqRoot, paths);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runAuditLog } from "./core-audit-log-native.js";
|
|
2
|
+
export function runAuditLogFast(args, _stdin, _cwd, hqRoot) {
|
|
3
|
+
const result = runAuditLog(args, { root: hqRoot ?? process.cwd(), now: () => new Date() });
|
|
4
|
+
if (result.stdout)
|
|
5
|
+
process.stdout.write(result.stdout);
|
|
6
|
+
if (result.stderrBytes)
|
|
7
|
+
process.stderr.write(result.stderrBytes);
|
|
8
|
+
else if (result.stderr)
|
|
9
|
+
process.stderr.write(result.stderr);
|
|
10
|
+
return result.code;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=core-audit-log-fast.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { detectStaleCorePolicyMirror } from "../lib/core-utils/detect-stale-core-policy-mirror.js";
|
|
2
|
+
export function runDetectStaleCorePolicyMirrorFast(args, _stdin, _cwd, hqRoot) {
|
|
3
|
+
return detectStaleCorePolicyMirror(args, { root: hqRoot ?? process.cwd() });
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=core-detect-stale-core-policy-mirror-fast.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createDmBindDeps, runDmBind } from "./core-dm-bind-native.js";
|
|
2
|
+
export async function runDmBindFast(args, stdin, cwd, hqRoot) {
|
|
3
|
+
const deps = createDmBindDeps(hqRoot ?? cwd, cwd, process.env, () => new Date());
|
|
4
|
+
deps.readStdin = async () => stdin;
|
|
5
|
+
const result = await runDmBind(args, deps);
|
|
6
|
+
if (result.stdout)
|
|
7
|
+
process.stdout.write(result.stdout);
|
|
8
|
+
if (result.stderrBytes)
|
|
9
|
+
process.stderr.write(result.stderrBytes);
|
|
10
|
+
else if (result.stderr)
|
|
11
|
+
process.stderr.write(result.stderr);
|
|
12
|
+
return result.code;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=core-dm-bind-fast.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { generateWorkersRegistry } from "../lib/workers-registry/index.js";
|
|
2
|
+
export function runGenerateWorkersRegistryFast(_args, _stdin, _cwd, hqRoot) {
|
|
3
|
+
return generateWorkersRegistry(hqRoot ?? process.cwd()).status;
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=core-generate-workers-registry-fast.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runGitPackExtensionCheck } from "./core-git-pack-extension-check-native.js";
|
|
2
|
+
export function runGitPackExtensionCheckFast(args, _stdin, cwd) {
|
|
3
|
+
const result = runGitPackExtensionCheck(args, { cwd, env: process.env });
|
|
4
|
+
if (result.stdout)
|
|
5
|
+
process.stdout.write(result.stdout);
|
|
6
|
+
if (result.stderrBytes)
|
|
7
|
+
process.stderr.write(result.stderrBytes);
|
|
8
|
+
else if (result.stderr)
|
|
9
|
+
process.stderr.write(result.stderr);
|
|
10
|
+
return result.code;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=core-git-pack-extension-check-fast.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRefreshVaultAccessDependencies, runRefreshVaultAccess } from "./core-refresh-vault-access-native.js";
|
|
2
|
+
export function runRefreshVaultAccessFast(args, _stdin, cwd) {
|
|
3
|
+
const result = runRefreshVaultAccess(args, createRefreshVaultAccessDependencies(cwd, process.env, () => new Date()));
|
|
4
|
+
if (result.stdout)
|
|
5
|
+
process.stdout.write(result.stdout);
|
|
6
|
+
if (result.stderrBytes)
|
|
7
|
+
process.stderr.write(result.stderrBytes);
|
|
8
|
+
else if (result.stderr)
|
|
9
|
+
process.stderr.write(result.stderr);
|
|
10
|
+
return result.code;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=core-refresh-vault-access-fast.js.map
|
|
@@ -15,6 +15,7 @@ export type RefreshVaultAccessDependencies = {
|
|
|
15
15
|
hqAvailable: boolean;
|
|
16
16
|
runHq?: (args: string[], cwd: string) => HqResult;
|
|
17
17
|
};
|
|
18
|
+
export declare function createRefreshVaultAccessDependencies(cwd: string, env: NodeJS.ProcessEnv, now: () => Date): RefreshVaultAccessDependencies;
|
|
18
19
|
export declare function runRefreshVaultAccess(args: string[], deps: RefreshVaultAccessDependencies): UtilityOutput;
|
|
19
20
|
export declare function registerCoreRefreshVaultAccessNative(core: Command): void;
|
|
20
21
|
//# sourceMappingURL=core-refresh-vault-access-native.d.ts.map
|
|
@@ -59,6 +59,15 @@ function executableOnPath(name, value) {
|
|
|
59
59
|
}
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
|
+
export function createRefreshVaultAccessDependencies(cwd, env, now) {
|
|
63
|
+
return {
|
|
64
|
+
cwd,
|
|
65
|
+
env,
|
|
66
|
+
now,
|
|
67
|
+
hqAvailable: executableOnPath("hq", env.PATH),
|
|
68
|
+
runHq: (args, directory) => defaultHq(args, directory, env),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
62
71
|
function isHqRoot(root) {
|
|
63
72
|
const candidate = root.endsWith(path.sep) ? root.slice(0, -1) : root;
|
|
64
73
|
if (!candidate)
|
|
@@ -219,11 +228,7 @@ export function registerCoreRefreshVaultAccessNative(core) {
|
|
|
219
228
|
registerNativeUtility(core, {
|
|
220
229
|
name: "refresh-vault-access", asset: REFRESH_VAULT_ACCESS_ASSET, root: "cwd", summary: "Refresh the local vault access manifest",
|
|
221
230
|
resolveRoot: (cwd) => cwd,
|
|
222
|
-
run: (args, context) => runRefreshVaultAccess(args,
|
|
223
|
-
cwd: context.cwd, env: context.env, now: context.now,
|
|
224
|
-
hqAvailable: executableOnPath("hq", context.env.PATH),
|
|
225
|
-
runHq: (args, cwd) => defaultHq(args, cwd, context.env),
|
|
226
|
-
}),
|
|
231
|
+
run: (args, context) => runRefreshVaultAccess(args, createRefreshVaultAccessDependencies(context.cwd, context.env, context.now)),
|
|
227
232
|
});
|
|
228
233
|
}
|
|
229
234
|
//# sourceMappingURL=core-refresh-vault-access-native.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { scanPackages } from "../lib/scan-packages/index.js";
|
|
2
|
+
export function runScanPackagesFast(_args, _stdin, _cwd, hqRoot) {
|
|
3
|
+
return scanPackages(hqRoot ?? process.cwd(), { quiet: process.env.HQ_SCAN_QUIET === "1" }).status;
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=core-scan-packages-fast.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createWorkMeshBindDeps, runWorkMeshBindTrusted } from "./core-work-mesh-bind-trusted-native.js";
|
|
2
|
+
export function runWorkMeshBindTrustedFast(args, _stdin, cwd, hqRoot) {
|
|
3
|
+
const result = runWorkMeshBindTrusted(args, createWorkMeshBindDeps(hqRoot ?? cwd, cwd, process.env, () => new Date()));
|
|
4
|
+
if (result.stdout)
|
|
5
|
+
process.stdout.write(result.stdout);
|
|
6
|
+
if (result.stderrBytes)
|
|
7
|
+
process.stderr.write(result.stderrBytes);
|
|
8
|
+
else if (result.stderr)
|
|
9
|
+
process.stderr.write(result.stderr);
|
|
10
|
+
return result.code;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=core-work-mesh-bind-trusted-fast.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
2
2
|
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
3
|
-
export declare function personalTargetedPushPathsEnabled(): boolean
|
|
3
|
+
export declare function personalTargetedPushPathsEnabled(): Promise<boolean>;
|
|
4
4
|
//# sourceMappingURL=personal-targeted-push-flag.d.ts.map
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
1
|
+
import { resolveProcessFlagGate, waitForFlagRegistryRefresh, } from "../lib/flag-registry.js";
|
|
2
2
|
export const CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG = "cli.personal-targeted-push-paths";
|
|
3
3
|
/** The established full-scope behavior remains in force until an explicit hq-flags enablement. */
|
|
4
|
-
export function personalTargetedPushPathsEnabled() {
|
|
4
|
+
export async function personalTargetedPushPathsEnabled() {
|
|
5
|
+
// This command reads the gate immediately after startup. Give the existing
|
|
6
|
+
// unref'd registry worker one bounded window to publish its company snapshot.
|
|
7
|
+
// An unavailable registry leaves resolveProcessFlagGate on the legacy false.
|
|
8
|
+
await waitForFlagRegistryRefresh(500);
|
|
5
9
|
return resolveProcessFlagGate(CLI_PERSONAL_TARGETED_PUSH_PATHS_FLAG, { fallback: false }, () => false);
|
|
6
10
|
}
|
|
7
11
|
//# sourceMappingURL=personal-targeted-push-flag.js.map
|
package/dist/fast-core.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
65
65
|
worktreeScopeFixEnabled = await coreRrrWorktreeScopeFixFastFlagEnabled(process.env);
|
|
66
66
|
}
|
|
67
67
|
const shouldInjectRoot = useShell
|
|
68
|
-
? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
|
|
68
|
+
? entry.shellHqRoot === "cwd" || entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
|
|
69
69
|
!parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
|
|
70
70
|
: entry.kind === "shell"
|
|
71
71
|
? true
|
|
@@ -76,7 +76,10 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
76
76
|
// its expected-user-error formatting remains authoritative.
|
|
77
77
|
let hqRoot;
|
|
78
78
|
const rootResolutionStartedAt = profileMark();
|
|
79
|
-
if (
|
|
79
|
+
if (useShell && entry.kind === "native" && entry.shellHqRoot === "cwd") {
|
|
80
|
+
hqRoot = process.cwd();
|
|
81
|
+
}
|
|
82
|
+
else if (shouldInjectRoot) {
|
|
80
83
|
const shortcutRoot = entry.name === "checkpoint-stop-gate" || entry.name === "session-journal"
|
|
81
84
|
? resolveExplicitFastLiveRoot(parsed.hqRootArg, process.env)
|
|
82
85
|
: undefined;
|
|
@@ -146,7 +149,7 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
146
149
|
? hqRoot ?? process.cwd()
|
|
147
150
|
: process.cwd();
|
|
148
151
|
const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
|
|
149
|
-
(entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
|
|
152
|
+
(entry.shellHqRoot === "cwd" || entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
|
|
150
153
|
const childEnv = {
|
|
151
154
|
...(entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : {}),
|
|
152
155
|
...(entry.name === "migrate-policy-triggers"
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type FastScaffoldScript = {
|
|
|
39
39
|
/** Cwd used for a shell rollback companion. Defaults to the caller cwd. */
|
|
40
40
|
shellCwd?: "caller" | "live";
|
|
41
41
|
/** Root injection behavior for a shell rollback companion. */
|
|
42
|
-
shellHqRoot?: "none" | "live" | "reconcile-default";
|
|
42
|
+
shellHqRoot?: "none" | "cwd" | "live" | "reconcile-default";
|
|
43
43
|
/** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
|
|
44
44
|
readsStdin?: boolean;
|
|
45
45
|
/** Bound bytes buffered from stdin; overflow is treated as empty input. */
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -432,6 +432,107 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
432
432
|
shellCwd: "caller",
|
|
433
433
|
shellHqRoot: name === "source-yaml-validate" ? "none" : "live",
|
|
434
434
|
})),
|
|
435
|
+
{
|
|
436
|
+
name: "audit-log",
|
|
437
|
+
kind: "native",
|
|
438
|
+
modulePath: "./commands/core-audit-log-fast.js",
|
|
439
|
+
exportName: "runAuditLogFast",
|
|
440
|
+
flagKey: "hq-core-native-fast-route",
|
|
441
|
+
familyFlagKey: "core-native-utilities",
|
|
442
|
+
asset: "core/scripts/audit-log.sh",
|
|
443
|
+
root: "live",
|
|
444
|
+
shellCwd: "live",
|
|
445
|
+
shellHqRoot: "live",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "refresh-vault-access",
|
|
449
|
+
kind: "native",
|
|
450
|
+
modulePath: "./commands/core-refresh-vault-access-fast.js",
|
|
451
|
+
exportName: "runRefreshVaultAccessFast",
|
|
452
|
+
flagKey: "hq-core-native-fast-route",
|
|
453
|
+
familyFlagKey: "core-native-utilities",
|
|
454
|
+
asset: "core/scripts/refresh-vault-access.sh",
|
|
455
|
+
root: "cwd",
|
|
456
|
+
shellCwd: "caller",
|
|
457
|
+
shellHqRoot: "cwd",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "generate-workers-registry",
|
|
461
|
+
kind: "native",
|
|
462
|
+
modulePath: "./commands/core-generate-workers-registry-fast.js",
|
|
463
|
+
exportName: "runGenerateWorkersRegistryFast",
|
|
464
|
+
flagKey: "hq-core-native-fast-route",
|
|
465
|
+
root: "live",
|
|
466
|
+
asset: "core/scripts/generate-workers-registry.sh",
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: "dm-bind",
|
|
470
|
+
kind: "native",
|
|
471
|
+
modulePath: "./commands/core-dm-bind-fast.js",
|
|
472
|
+
exportName: "runDmBindFast",
|
|
473
|
+
flagKey: "hq-core-native-fast-route",
|
|
474
|
+
familyFlagKey: "core-native-utilities",
|
|
475
|
+
asset: "core/scripts/hq-dm-bind.sh",
|
|
476
|
+
root: "live",
|
|
477
|
+
shellCwd: "live",
|
|
478
|
+
shellHqRoot: "live",
|
|
479
|
+
readsStdinFor: (args) => {
|
|
480
|
+
if (args[0] !== "post")
|
|
481
|
+
return false;
|
|
482
|
+
const valueOptions = new Set(["--topic", "--under", "--to", "--title", "--state", "--line", "--next", "--ask"]);
|
|
483
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
484
|
+
if (args[index] === "-")
|
|
485
|
+
return true;
|
|
486
|
+
if (valueOptions.has(args[index] ?? ""))
|
|
487
|
+
index += 1;
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
},
|
|
491
|
+
requiresNodeNetworkCompatibility: true,
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "git-pack-extension-check",
|
|
495
|
+
kind: "native",
|
|
496
|
+
modulePath: "./commands/core-git-pack-extension-check-fast.js",
|
|
497
|
+
exportName: "runGitPackExtensionCheckFast",
|
|
498
|
+
flagKey: "hq-core-native-fast-route",
|
|
499
|
+
familyFlagKey: "core-native-utilities",
|
|
500
|
+
asset: "core/scripts/git-pack-extension-check.sh",
|
|
501
|
+
root: "cwd",
|
|
502
|
+
shellCwd: "caller",
|
|
503
|
+
shellHqRoot: "cwd",
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "scan-packages",
|
|
507
|
+
kind: "native",
|
|
508
|
+
modulePath: "./commands/core-scan-packages-fast.js",
|
|
509
|
+
exportName: "runScanPackagesFast",
|
|
510
|
+
flagKey: "hq-core-native-fast-route",
|
|
511
|
+
asset: "core/scripts/scan-packages.sh",
|
|
512
|
+
root: "live",
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "detect-stale-core-policy-mirror",
|
|
516
|
+
kind: "native",
|
|
517
|
+
modulePath: "./commands/core-detect-stale-core-policy-mirror-fast.js",
|
|
518
|
+
exportName: "runDetectStaleCorePolicyMirrorFast",
|
|
519
|
+
flagKey: "hq-core-native-fast-route",
|
|
520
|
+
asset: "core/scripts/detect-stale-core-policy-mirror.sh",
|
|
521
|
+
root: "live",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "work-mesh-bind-trusted",
|
|
525
|
+
kind: "native",
|
|
526
|
+
modulePath: "./commands/core-work-mesh-bind-trusted-fast.js",
|
|
527
|
+
exportName: "runWorkMeshBindTrustedFast",
|
|
528
|
+
flagKey: "hq-core-native-fast-route",
|
|
529
|
+
familyFlagKey: "core-native-utilities",
|
|
530
|
+
asset: "core/scripts/work-mesh-live-bind-trusted.sh",
|
|
531
|
+
root: "live",
|
|
532
|
+
shellCwd: "live",
|
|
533
|
+
shellHqRoot: "live",
|
|
534
|
+
requiresNodeNetworkCompatibility: true,
|
|
535
|
+
},
|
|
435
536
|
];
|
|
436
537
|
/**
|
|
437
538
|
* Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
|
|
@@ -17,6 +17,8 @@ export interface DaemonFlagClient {
|
|
|
17
17
|
}
|
|
18
18
|
export interface ReadDaemonFlagOptions {
|
|
19
19
|
env?: NodeJS.ProcessEnv;
|
|
20
|
+
/** Controls how this read combines a local person setting with the registry value. */
|
|
21
|
+
localOverrideMode?: "respect" | "off-only" | "ignore";
|
|
20
22
|
createClient?: (options: FlagClientOptions) => DaemonFlagClient;
|
|
21
23
|
getToken?: () => Promise<string>;
|
|
22
24
|
onError?: (error: unknown) => void;
|
package/dist/lib/daemon/flag.js
CHANGED
|
@@ -15,7 +15,8 @@ const READ_TIMEOUT_MS = 5_000;
|
|
|
15
15
|
export async function readDaemonFlag(flagKey, options = {}) {
|
|
16
16
|
const env = options.env ?? process.env;
|
|
17
17
|
const override = readLocalOverride(flagKey, env);
|
|
18
|
-
|
|
18
|
+
const localOverrideMode = options.localOverrideMode ?? "respect";
|
|
19
|
+
if (override !== null && (localOverrideMode === "respect" || (localOverrideMode === "off-only" && !override)))
|
|
19
20
|
return override;
|
|
20
21
|
let failed = false;
|
|
21
22
|
let reportedError;
|
|
@@ -12,6 +12,7 @@ import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
|
|
|
12
12
|
import { SessionRegistry } from "./session-registry.js";
|
|
13
13
|
import { HqMcpBridge, type RunWorkerRequest } from "./ops/mcp.js";
|
|
14
14
|
import { type StdinMachineCredentials } from "./machine-login.js";
|
|
15
|
+
import { type ReadDaemonFlagOptions } from "./flag.js";
|
|
15
16
|
import { type WindowsPipeAclReport, type WindowsPipeHost } from "./windows-pipe-host.js";
|
|
16
17
|
export interface HqdServerOptions {
|
|
17
18
|
socketPath: string;
|
|
@@ -41,7 +42,7 @@ export interface HqdServerOptions {
|
|
|
41
42
|
/** Test seam for the hq-anywhere-runtime default-off gate. */
|
|
42
43
|
mcpEnabled?: () => boolean | Promise<boolean>;
|
|
43
44
|
/** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
|
|
44
|
-
runtimeFlagReader?: () => Promise<boolean>;
|
|
45
|
+
runtimeFlagReader?: (options: ReadDaemonFlagOptions) => Promise<boolean>;
|
|
45
46
|
/** Test seam for the hosted Streamable HTTP MCP transport. */
|
|
46
47
|
mcpBridge?: HqMcpBridge;
|
|
47
48
|
/** Test seam for machine login while keeping production credentials off disk. */
|
|
@@ -396,14 +396,22 @@ export async function startHqdServer(opts) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
const runtimeFlagMonitor = opts.mcpEnabled ? undefined : createBoundedDaemonFlagMonitor({
|
|
399
|
-
read:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
399
|
+
read: () => {
|
|
400
|
+
const options = {
|
|
401
|
+
env,
|
|
402
|
+
throwOnError: true,
|
|
403
|
+
// A local opt-out is honored, while a local opt-in cannot override the operator kill switch.
|
|
404
|
+
localOverrideMode: "off-only",
|
|
405
|
+
};
|
|
406
|
+
return opts.runtimeFlagReader
|
|
407
|
+
? opts.runtimeFlagReader(options)
|
|
408
|
+
: readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, options);
|
|
409
|
+
},
|
|
403
410
|
onChange: (enabled) => {
|
|
404
411
|
if (!enabled) {
|
|
405
412
|
flagRevocationGeneration += 1;
|
|
406
413
|
ephemeralMachineToken = undefined;
|
|
414
|
+
sessions.clear();
|
|
407
415
|
}
|
|
408
416
|
},
|
|
409
417
|
onReadError: (error) => {
|
|
@@ -426,8 +434,22 @@ export async function startHqdServer(opts) {
|
|
|
426
434
|
socket: healthSocket(socketPath, isPipe),
|
|
427
435
|
};
|
|
428
436
|
};
|
|
429
|
-
|
|
437
|
+
const alwaysAvailableOps = new Set(["ping", "health", "auth.status", "shutdown"]);
|
|
438
|
+
const allowedWhileDisabled = new Set(["session.get", "session.close"]);
|
|
439
|
+
/** Gate each external hqd request once, leaving diagnostics and cleanup available while disabled. */
|
|
430
440
|
const handle = (req) => {
|
|
441
|
+
const requestStartedAt = Date.now();
|
|
442
|
+
if (alwaysAvailableOps.has(req.op))
|
|
443
|
+
return dispatch(req, requestStartedAt);
|
|
444
|
+
return Promise.resolve(mcpEnabled()).then((enabled) => {
|
|
445
|
+
if (!enabled && !allowedWhileDisabled.has(req.op)) {
|
|
446
|
+
throw new OpError("anywhere_disabled", `HQ Anywhere is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
|
|
447
|
+
}
|
|
448
|
+
return dispatch(req, requestStartedAt);
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
/** Internal MCP sub-operations reuse the already-gated external request. */
|
|
452
|
+
const dispatch = (req, requestStartedAt = Date.now()) => {
|
|
431
453
|
const args = req.args ?? {};
|
|
432
454
|
switch (req.op) {
|
|
433
455
|
case "ping":
|
|
@@ -565,7 +587,6 @@ export async function startHqdServer(opts) {
|
|
|
565
587
|
}, { hqRoot: opts.hqRoot, workMeshRoot: opts.workMeshRoot, env, now: clock });
|
|
566
588
|
}
|
|
567
589
|
case "mcp.call": {
|
|
568
|
-
const requestStartedAt = Date.now();
|
|
569
590
|
const name = stringArg(args, "name");
|
|
570
591
|
const raw = args.arguments ?? {};
|
|
571
592
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -581,7 +602,7 @@ export async function startHqdServer(opts) {
|
|
|
581
602
|
companyArgs.runtime = runtime;
|
|
582
603
|
if (sessionId !== undefined)
|
|
583
604
|
companyArgs.sessionId = sessionId;
|
|
584
|
-
const call = (op, a) =>
|
|
605
|
+
const call = (op, a) => dispatch({ id: req.id, op, args: a });
|
|
585
606
|
const asResult = (value) => (value instanceof Promise ? value.then(jsonResult) : jsonResult(value));
|
|
586
607
|
const policyGuardedCall = (run) => {
|
|
587
608
|
const company = companyFor(companyArgs);
|
|
@@ -601,7 +622,11 @@ export async function startHqdServer(opts) {
|
|
|
601
622
|
const reason = Array.isArray(decision.reasons) ? decision.reasons.filter((r) => typeof r === "string").join("; ") : "policy check did not allow this tool";
|
|
602
623
|
throw new OpError("policy_blocked", reason || "policy check did not allow this tool");
|
|
603
624
|
}
|
|
604
|
-
return
|
|
625
|
+
return Promise.resolve(mcpEnabled()).then((enabled) => {
|
|
626
|
+
if (!enabled)
|
|
627
|
+
throw new OpError("anywhere_disabled", `HQ Anywhere is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
|
|
628
|
+
return run();
|
|
629
|
+
});
|
|
605
630
|
});
|
|
606
631
|
};
|
|
607
632
|
switch (name) {
|
|
@@ -628,10 +653,7 @@ export async function startHqdServer(opts) {
|
|
|
628
653
|
? Math.max(1, Math.min(args.timeoutMs, 120_000))
|
|
629
654
|
: HQD_DEFAULT_REQUEST_TIMEOUT_MS;
|
|
630
655
|
const deadline = requestStartedAt + timeoutMs;
|
|
631
|
-
const forward = () =>
|
|
632
|
-
if (!enabled) {
|
|
633
|
-
throw new OpError("anywhere_disabled", `Hosted hq-mcp forwarding is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
|
|
634
|
-
}
|
|
656
|
+
const forward = () => {
|
|
635
657
|
const remainingMs = Math.floor(deadline - Date.now());
|
|
636
658
|
if (remainingMs <= 0) {
|
|
637
659
|
throw new McpOpError("hq_mcp_timeout", `Hosted hq-mcp request exceeded its ${timeoutMs}ms end-to-end deadline before forwarding.`);
|
|
@@ -650,15 +672,10 @@ export async function startHqdServer(opts) {
|
|
|
650
672
|
},
|
|
651
673
|
});
|
|
652
674
|
return hqMcp.callTool(name, raw, remainingMs);
|
|
653
|
-
}
|
|
654
|
-
return
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
return !MCP_READ_ONLY_TOOL_NAMES.has(name)
|
|
659
|
-
? policyGuardedCall(forward)
|
|
660
|
-
: forward();
|
|
661
|
-
});
|
|
675
|
+
};
|
|
676
|
+
return !MCP_READ_ONLY_TOOL_NAMES.has(name)
|
|
677
|
+
? policyGuardedCall(forward)
|
|
678
|
+
: forward();
|
|
662
679
|
}
|
|
663
680
|
}
|
|
664
681
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AuthState } from "./accounts.js";
|
|
2
|
+
import { type QuotaState } from "./quota.js";
|
|
2
3
|
import type { LaneHost, LaneProvider, LaneRecord, LanesConfig } from "./types.js";
|
|
3
4
|
export interface HostCapacityAccount {
|
|
4
5
|
enabled: boolean;
|
|
@@ -8,6 +9,9 @@ export interface HostCapacityAccount {
|
|
|
8
9
|
max_running: number;
|
|
9
10
|
running: number;
|
|
10
11
|
queued: number;
|
|
12
|
+
/** Account-wide quota status when this snapshot is produced on its host. */
|
|
13
|
+
quota_held?: boolean;
|
|
14
|
+
quota_reset_at?: string;
|
|
11
15
|
/** Exact lane states used for Claude account balancing; absent on older Outposts. */
|
|
12
16
|
lane_counts?: {
|
|
13
17
|
running: number;
|
|
@@ -54,6 +58,7 @@ export declare function buildHostCapacitySnapshot(input: {
|
|
|
54
58
|
env?: NodeJS.ProcessEnv;
|
|
55
59
|
hostname?: string;
|
|
56
60
|
now: Date;
|
|
61
|
+
quotaState?: QuotaState;
|
|
57
62
|
}): HostCapacitySnapshot;
|
|
58
63
|
/** Reject malformed remote snapshots before they can influence host selection. */
|
|
59
64
|
export declare function parseHostCapacitySnapshot(raw: unknown): HostCapacitySnapshot;
|
|
@@ -2,6 +2,7 @@ import { accountStatus } from "./accounts.js";
|
|
|
2
2
|
import { accountMaxRunning } from "./config.js";
|
|
3
3
|
import { laneOccupiesRunningSlot, laneRunsOnHost, } from "./admission.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
+
import { quotaBlockFor } from "./quota.js";
|
|
5
6
|
function admissionHost(host) {
|
|
6
7
|
return host.kind === "outpost"
|
|
7
8
|
? { hostname: "", outpostId: host.id }
|
|
@@ -33,6 +34,10 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
33
34
|
});
|
|
34
35
|
const accountLanes = providerLanes.filter((lane) => lane.account === id);
|
|
35
36
|
const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
|
|
37
|
+
const quotaBlock = input.quotaState
|
|
38
|
+
? quotaBlockFor(input.quotaState, provider, id, undefined, input.now)
|
|
39
|
+
: undefined;
|
|
40
|
+
const quotaResetAt = input.quotaState?.providers[provider]?.[id]?.all_models_quota_reset_at;
|
|
36
41
|
accounts[id] = {
|
|
37
42
|
enabled: account.enabled,
|
|
38
43
|
logged_in: status.logged_in,
|
|
@@ -40,6 +45,12 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
40
45
|
max_running: accountMaxRunning(providerConfig, id),
|
|
41
46
|
running: running.filter((lane) => lane.account === id).length,
|
|
42
47
|
queued: queued.filter((lane) => lane.account === id).length,
|
|
48
|
+
...(quotaBlock !== undefined
|
|
49
|
+
? {
|
|
50
|
+
quota_held: quotaBlock.blocked,
|
|
51
|
+
...(quotaResetAt ? { quota_reset_at: quotaResetAt } : {}),
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
43
54
|
lane_counts: {
|
|
44
55
|
running: liveLanes.filter((lane) => lane.state === "running").length,
|
|
45
56
|
queued: liveLanes.filter((lane) => lane.state === "queued").length,
|
|
@@ -108,6 +119,9 @@ export function parseHostCapacitySnapshot(raw) {
|
|
|
108
119
|
if (id.trim() === "" || account === undefined ||
|
|
109
120
|
typeof account.enabled !== "boolean" ||
|
|
110
121
|
!(account.logged_in === null || typeof account.logged_in === "boolean") ||
|
|
122
|
+
(account.quota_held !== undefined && typeof account.quota_held !== "boolean") ||
|
|
123
|
+
(account.quota_reset_at !== undefined &&
|
|
124
|
+
(typeof account.quota_reset_at !== "string" || !Number.isFinite(Date.parse(account.quota_reset_at)))) ||
|
|
111
125
|
(account.auth_state !== undefined &&
|
|
112
126
|
!["file", "status", "absent", "unknown", "home_missing"].includes(account.auth_state))) {
|
|
113
127
|
throw new LanesError("host_capacity_invalid", `Malformed ${provider} account capacity`, {
|
|
@@ -124,6 +138,10 @@ export function parseHostCapacitySnapshot(raw) {
|
|
|
124
138
|
max_running: requireCount(account.max_running, `${provider}.${id}.max_running`, true),
|
|
125
139
|
running: requireCount(account.running, `${provider}.${id}.running`),
|
|
126
140
|
queued: requireCount(account.queued, `${provider}.${id}.queued`),
|
|
141
|
+
...(account.quota_held !== undefined ? { quota_held: account.quota_held } : {}),
|
|
142
|
+
...(account.quota_reset_at !== undefined
|
|
143
|
+
? { quota_reset_at: account.quota_reset_at }
|
|
144
|
+
: {}),
|
|
127
145
|
...(account.lane_counts !== undefined
|
|
128
146
|
? { lane_counts: parseLaneCounts(account.lane_counts, `${provider}.${id}.lane_counts`) }
|
|
129
147
|
: {}),
|
|
@@ -36,6 +36,8 @@ export interface ProviderRejection {
|
|
|
36
36
|
retryable: boolean;
|
|
37
37
|
/** Which event on the stream carried the failure. */
|
|
38
38
|
event: "codex.turn.failed" | "claude.result.is_error" | "claude.result.login_failure" | "grok.error";
|
|
39
|
+
/** Filesystem time when the provider output was appended, when read from worker.out. */
|
|
40
|
+
output_at?: string;
|
|
39
41
|
}
|
|
40
42
|
export declare function isProviderLoginFailure(failure: ProviderRejection | null): boolean;
|
|
41
43
|
/**
|
|
@@ -271,9 +271,21 @@ export function parseProviderRejection(provider, text) {
|
|
|
271
271
|
export function readProviderRejection(runDir, provider, workerOutOffset = 0) {
|
|
272
272
|
const outPath = path.join(runDir, "logs", "worker.out");
|
|
273
273
|
let text;
|
|
274
|
+
let outputAt;
|
|
274
275
|
try {
|
|
275
276
|
const bytes = fs.readFileSync(outPath);
|
|
276
277
|
text = bytes.subarray(Math.min(workerOutOffset, bytes.length)).toString("utf8");
|
|
278
|
+
const outputMtime = fs.statSync(outPath).mtime;
|
|
279
|
+
let spawnMtime;
|
|
280
|
+
try {
|
|
281
|
+
spawnMtime = fs.statSync(path.join(runDir, "spawn.json")).mtime;
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
if (err.code !== "ENOENT")
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
if (!spawnMtime || outputMtime >= spawnMtime)
|
|
288
|
+
outputAt = outputMtime.toISOString();
|
|
277
289
|
}
|
|
278
290
|
catch (err) {
|
|
279
291
|
const code = err.code;
|
|
@@ -281,6 +293,9 @@ export function readProviderRejection(runDir, provider, workerOutOffset = 0) {
|
|
|
281
293
|
return null;
|
|
282
294
|
throw err;
|
|
283
295
|
}
|
|
284
|
-
|
|
296
|
+
const failure = parseProviderRejection(provider, text);
|
|
297
|
+
return failure
|
|
298
|
+
? { ...failure, ...(outputAt ? { output_at: outputAt } : {}) }
|
|
299
|
+
: null;
|
|
285
300
|
}
|
|
286
301
|
//# sourceMappingURL=provider-rejection.js.map
|
|
@@ -26,13 +26,13 @@ export interface ModelQuota {
|
|
|
26
26
|
/** Operator-facing marker, e.g. "needs credits for claude-fable-5-1". */
|
|
27
27
|
marker?: string;
|
|
28
28
|
}
|
|
29
|
-
export type QuotaResetSource = "
|
|
29
|
+
export type QuotaResetSource = "parsed" | "derived";
|
|
30
30
|
export interface AccountQuota {
|
|
31
31
|
/** Set when the 429 is an account-wide usage limit with a reset time. */
|
|
32
32
|
all_models_quota_reset_at?: string;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Whether `all_models_quota_reset_at` came from a parsed provider reset or
|
|
35
|
+
* the configured default hold applied to a 429 with no parseable reset.
|
|
36
36
|
*/
|
|
37
37
|
reset_source?: QuotaResetSource;
|
|
38
38
|
models: Record<string, ModelQuota>;
|
|
@@ -57,8 +57,8 @@ export interface QuotaSignal {
|
|
|
57
57
|
* otherwise record no hold at all, and the next lane on the account would
|
|
58
58
|
* hit the same 429. Give such a signal the provider's configured default
|
|
59
59
|
* hold (`quota_default_hold_ms`, fifteen minutes unless set) as its reset,
|
|
60
|
-
* marked `
|
|
61
|
-
*
|
|
60
|
+
* marked `derived`; a signal that already carries a reset keeps it, marked
|
|
61
|
+
* `parsed`. A per-model credits refusal is untouched.
|
|
62
62
|
*/
|
|
63
63
|
export declare function applyDefaultQuotaHold(signal: QuotaSignal, defaultHoldMs: number, now: Date): QuotaSignal;
|
|
64
64
|
export declare function quotaStatePath(hqRoot: string): string;
|
package/dist/lib/lanes/quota.js
CHANGED
|
@@ -27,19 +27,19 @@ export const QUOTA_SCHEMA_VERSION = "hq-lanes-quota.v1";
|
|
|
27
27
|
* otherwise record no hold at all, and the next lane on the account would
|
|
28
28
|
* hit the same 429. Give such a signal the provider's configured default
|
|
29
29
|
* hold (`quota_default_hold_ms`, fifteen minutes unless set) as its reset,
|
|
30
|
-
* marked `
|
|
31
|
-
*
|
|
30
|
+
* marked `derived`; a signal that already carries a reset keeps it, marked
|
|
31
|
+
* `parsed`. A per-model credits refusal is untouched.
|
|
32
32
|
*/
|
|
33
33
|
export function applyDefaultQuotaHold(signal, defaultHoldMs, now) {
|
|
34
34
|
if (signal.reason !== "account_quota_exhausted" || signal.needs_credits)
|
|
35
35
|
return signal;
|
|
36
36
|
if (signal.quota_reset_at)
|
|
37
|
-
return { ...signal, reset_source: "
|
|
37
|
+
return { ...signal, reset_source: "parsed" };
|
|
38
38
|
return {
|
|
39
39
|
...signal,
|
|
40
40
|
quota_reset_at: new Date(now.getTime() + defaultHoldMs).toISOString(),
|
|
41
41
|
account_wide: true,
|
|
42
|
-
reset_source: "
|
|
42
|
+
reset_source: "derived",
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
const MONTHS = {
|
|
@@ -119,8 +119,13 @@ export function loadQuotaState(hqRoot) {
|
|
|
119
119
|
if (typeof raw.all_models_quota_reset_at === "string" &&
|
|
120
120
|
raw.all_models_quota_reset_at.length > 0) {
|
|
121
121
|
account.all_models_quota_reset_at = raw.all_models_quota_reset_at;
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// Normalize values written by older CLI versions while preserving the
|
|
123
|
+
// distinction between provider-parsed and configured fallback times.
|
|
124
|
+
if (raw.reset_source === "provider" || raw.reset_source === "parsed") {
|
|
125
|
+
account.reset_source = "parsed";
|
|
126
|
+
}
|
|
127
|
+
else if (raw.reset_source === "default_hold" || raw.reset_source === "derived") {
|
|
128
|
+
account.reset_source = "derived";
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
accounts[id] = account;
|
|
@@ -145,6 +150,22 @@ function writeQuotaState(hqRoot, state) {
|
|
|
145
150
|
* An explicit provider year is kept as stated.
|
|
146
151
|
*/
|
|
147
152
|
export function parseProviderResetTime(text, now) {
|
|
153
|
+
const timeOnlyMatch = /\bresets\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i.exec(text);
|
|
154
|
+
if (timeOnlyMatch) {
|
|
155
|
+
let hour = Number(timeOnlyMatch[1]);
|
|
156
|
+
const minute = Number(timeOnlyMatch[2] ?? 0);
|
|
157
|
+
const meridiem = (timeOnlyMatch[3] ?? "").toLowerCase();
|
|
158
|
+
if (!Number.isInteger(hour) || hour < 1 || hour > 12 || minute < 0 || minute > 59)
|
|
159
|
+
return null;
|
|
160
|
+
if (meridiem === "pm" && hour < 12)
|
|
161
|
+
hour += 12;
|
|
162
|
+
if (meridiem === "am" && hour === 12)
|
|
163
|
+
hour = 0;
|
|
164
|
+
const candidate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hour, minute, 0, 0));
|
|
165
|
+
if (candidate.getTime() <= now.getTime())
|
|
166
|
+
candidate.setUTCDate(candidate.getUTCDate() + 1);
|
|
167
|
+
return candidate.toISOString();
|
|
168
|
+
}
|
|
148
169
|
const resetMatch = /\bresets\s+([A-Za-z]+)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,\s*(?:(\d{4})\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm))?(?:\s*\((UTC)\))?/i.exec(text);
|
|
149
170
|
const codexMatch = /\btry\s+again\s+at\s+([A-Za-z]+)\s+(\d{1,2})(?:st|nd|rd|th)?,\s*(\d{4})\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s*\((UTC)\))?/i.exec(text);
|
|
150
171
|
const match = resetMatch ?? codexMatch;
|
|
@@ -263,9 +284,23 @@ export function quotaSignalFromFields(fields, model, now) {
|
|
|
263
284
|
export function readQuotaSignal(runDir, model, now, workerOutOffset = 0) {
|
|
264
285
|
const outPath = path.join(runDir, "logs", "worker.out");
|
|
265
286
|
let text;
|
|
287
|
+
let outputAt;
|
|
266
288
|
try {
|
|
267
289
|
const bytes = fs.readFileSync(outPath);
|
|
268
290
|
text = bytes.subarray(Math.min(workerOutOffset, bytes.length)).toString("utf8");
|
|
291
|
+
// worker.out is appended as provider output arrives. Its mtime anchors a
|
|
292
|
+
// time-only reset to the output event, not to a later reconcile pass.
|
|
293
|
+
outputAt = fs.statSync(outPath).mtime;
|
|
294
|
+
let spawnAt;
|
|
295
|
+
try {
|
|
296
|
+
spawnAt = fs.statSync(path.join(runDir, "spawn.json")).mtime;
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (err.code !== "ENOENT")
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
if (outputAt > now || (spawnAt && outputAt < spawnAt))
|
|
303
|
+
outputAt = now;
|
|
269
304
|
}
|
|
270
305
|
catch (err) {
|
|
271
306
|
const code = err.code;
|
|
@@ -276,7 +311,8 @@ export function readQuotaSignal(runDir, model, now, workerOutOffset = 0) {
|
|
|
276
311
|
const fields = quotaFieldsFromOutput(text);
|
|
277
312
|
if (!fields)
|
|
278
313
|
return null;
|
|
279
|
-
|
|
314
|
+
const observedAt = Number.isFinite(outputAt.getTime()) ? outputAt : now;
|
|
315
|
+
return quotaSignalFromFields(fields, model, observedAt);
|
|
280
316
|
}
|
|
281
317
|
export function recordAccountQuota(hqRoot, provider, accountId, signal) {
|
|
282
318
|
return withLaneRecordFileLock(quotaStatePath(hqRoot), () => {
|
|
@@ -303,11 +339,11 @@ export function recordAccountQuota(hqRoot, provider, accountId, signal) {
|
|
|
303
339
|
if (signal.account_wide && signal.quota_reset_at) {
|
|
304
340
|
if (shouldReplaceReset(current.all_models_quota_reset_at, signal.quota_reset_at)) {
|
|
305
341
|
account.all_models_quota_reset_at = signal.quota_reset_at;
|
|
306
|
-
account.reset_source = signal.reset_source ?? "
|
|
342
|
+
account.reset_source = signal.reset_source ?? "parsed";
|
|
307
343
|
}
|
|
308
344
|
else if (current.all_models_quota_reset_at) {
|
|
309
345
|
account.all_models_quota_reset_at = current.all_models_quota_reset_at;
|
|
310
|
-
account.reset_source = current.reset_source ?? "
|
|
346
|
+
account.reset_source = current.reset_source ?? "parsed";
|
|
311
347
|
}
|
|
312
348
|
}
|
|
313
349
|
accounts[accountId] = account;
|
|
@@ -19,7 +19,7 @@ import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHi
|
|
|
19
19
|
export declare const LANES_RUNS_REL: string;
|
|
20
20
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
21
21
|
export declare const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
|
|
22
|
-
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported"];
|
|
22
|
+
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported", "account_quota_exhausted"];
|
|
23
23
|
export type SpawnTerminalReason = (typeof SPAWN_TERMINAL_REASONS)[number];
|
|
24
24
|
declare const PROVIDER_COMMAND: Record<LaneProvider, string>;
|
|
25
25
|
export declare function providerCommandOnPath(command: string, pathEnv: string | undefined): boolean;
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -108,18 +108,6 @@ export declare function classifyLaneExit(input: {
|
|
|
108
108
|
*/
|
|
109
109
|
providerRejection?: ProviderRejection | null;
|
|
110
110
|
}): TerminalClassification;
|
|
111
|
-
/**
|
|
112
|
-
* One decision for an exited worker's output. An explicit provider status
|
|
113
|
-
* wins over the quota text heuristics: a 429 from any provider (including
|
|
114
|
-
* codex `turn.failed` and grok error objects, which the claude-shaped quota
|
|
115
|
-
* reader never sees) becomes a quota signal; any other explicit status is a
|
|
116
|
-
* rejection even when its text sounds like a quota message (a 402 "requires
|
|
117
|
-
* usage credits" is a rejection, not a hold). A failure with no status at
|
|
118
|
-
* all is matched against the quota text heuristics first: a status-less
|
|
119
|
-
* "usage limit" / "rate limit" turn.failed still creates the account hold;
|
|
120
|
-
* any other status-less message is a rejection. Only when the stream
|
|
121
|
-
* carries no failure object does the quota reader's own read apply.
|
|
122
|
-
*/
|
|
123
111
|
export declare function routeProviderFailure(failure: ProviderRejection | null, readQuota: () => QuotaSignal | null, model: string | undefined, now: Date): {
|
|
124
112
|
quota: QuotaSignal | null;
|
|
125
113
|
rejection: ProviderRejection | null;
|
|
@@ -139,15 +139,21 @@ export function classifyLaneExit(input) {
|
|
|
139
139
|
* any other status-less message is a rejection. Only when the stream
|
|
140
140
|
* carries no failure object does the quota reader's own read apply.
|
|
141
141
|
*/
|
|
142
|
+
function validFailureOutputTime(value, fallback) {
|
|
143
|
+
const parsed = new Date(value);
|
|
144
|
+
return Number.isFinite(parsed.getTime()) && parsed.getTime() <= fallback.getTime()
|
|
145
|
+
? parsed
|
|
146
|
+
: fallback;
|
|
147
|
+
}
|
|
142
148
|
export function routeProviderFailure(failure, readQuota, model, now) {
|
|
143
149
|
if (failure && failure.status === QUOTA_STATUS) {
|
|
144
150
|
return {
|
|
145
|
-
quota: quotaSignalFromFields({ api_error_status: QUOTA_STATUS, is_error: true, result: failure.message }, model, now),
|
|
151
|
+
quota: quotaSignalFromFields({ api_error_status: QUOTA_STATUS, is_error: true, result: failure.message }, model, failure.output_at ? validFailureOutputTime(failure.output_at, now) : now),
|
|
146
152
|
rejection: null,
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
if (failure && failure.status === null) {
|
|
150
|
-
const quota = quotaSignalFromFields({ api_error_status: undefined, is_error: true, result: failure.message }, model, now);
|
|
156
|
+
const quota = quotaSignalFromFields({ api_error_status: undefined, is_error: true, result: failure.message }, model, failure.output_at ? validFailureOutputTime(failure.output_at, now) : now);
|
|
151
157
|
if (quota)
|
|
152
158
|
return { quota, rejection: null };
|
|
153
159
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as yaml from 'js-yaml';
|
|
5
|
-
import { contributionLinks } from '../../utils/pack-
|
|
5
|
+
import { contributionLinks } from '../../utils/pack-contribution-links.js';
|
|
6
6
|
import { CONTRIBUTION_TABLE } from '../../utils/contribution-table.js';
|
|
7
7
|
function isDirectory(target) { try {
|
|
8
8
|
return fs.statSync(target).isDirectory();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PackContributeKey } from '../types.js';
|
|
2
|
+
/** A single symlink a pack contributes: dst (host path) -> src (inside pack). */
|
|
3
|
+
export interface WiredLink {
|
|
4
|
+
key: PackContributeKey;
|
|
5
|
+
item: string;
|
|
6
|
+
src: string;
|
|
7
|
+
dst: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Every SYMLINK a pack's `contributes` block declares. Empty subfields and
|
|
11
|
+
* non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
|
|
12
|
+
* keys (e.g. `mcp`) produce no symlink and are skipped here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
|
|
15
|
+
//# sourceMappingURL=pack-contribution-links.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { CONTRIBUTION_TABLE, payloadFor } from './contribution-table.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every SYMLINK a pack's `contributes` block declares. Empty subfields and
|
|
5
|
+
* non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
|
|
6
|
+
* keys (e.g. `mcp`) produce no symlink and are skipped here.
|
|
7
|
+
*/
|
|
8
|
+
export function contributionLinks(hqRoot, packDir, contributes) {
|
|
9
|
+
const links = [];
|
|
10
|
+
for (const [key, items] of Object.entries(contributes)) {
|
|
11
|
+
if (!(key in CONTRIBUTION_TABLE))
|
|
12
|
+
continue;
|
|
13
|
+
if (!Array.isArray(items))
|
|
14
|
+
continue;
|
|
15
|
+
if (CONTRIBUTION_TABLE[key].wire !== 'symlink')
|
|
16
|
+
continue;
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
if (typeof item !== 'string' || item.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
const row = CONTRIBUTION_TABLE[key];
|
|
21
|
+
const payloadRel = payloadFor(key, item);
|
|
22
|
+
const dstRel = path.join(row.host, path.basename(payloadRel));
|
|
23
|
+
links.push({
|
|
24
|
+
key,
|
|
25
|
+
item,
|
|
26
|
+
src: path.join(packDir, payloadRel),
|
|
27
|
+
dst: path.join(hqRoot, dstRel),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return links;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=pack-contribution-links.js.map
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import type { PackManifest, PackContributeKey } from '../types.js';
|
|
40
40
|
import { type WireMode } from './contribution-table.js';
|
|
41
|
+
import { type WiredLink } from './pack-contribution-links.js';
|
|
42
|
+
export { contributionLinks, type WiredLink } from './pack-contribution-links.js';
|
|
41
43
|
import { type UnregisterServerResult, type SafeWriteEnv, type AcquireLockOptions } from '../commands/mcp-registration.js';
|
|
42
44
|
export { readHqVersion, readRecommendedPackages } from './hq-core-yaml.js';
|
|
43
45
|
export type { WireMode };
|
|
@@ -62,21 +64,7 @@ export declare function mcpServerNameForContribution(packName: string, contribut
|
|
|
62
64
|
* order, de-duplicated.
|
|
63
65
|
*/
|
|
64
66
|
export declare function mergeKeys(contributes: Partial<Record<PackContributeKey, string[]>>): PackContributeKey[];
|
|
65
|
-
/** A single symlink a pack contributes: dst (host path) -> src (inside pack). */
|
|
66
|
-
export interface WiredLink {
|
|
67
|
-
key: PackContributeKey;
|
|
68
|
-
item: string;
|
|
69
|
-
src: string;
|
|
70
|
-
dst: string;
|
|
71
|
-
}
|
|
72
67
|
export type LinkStatus = 'live' | 'broken' | 'missing' | 'foreign';
|
|
73
|
-
/**
|
|
74
|
-
* Every SYMLINK a pack's `contributes` block declares. Empty subfields and
|
|
75
|
-
* non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
|
|
76
|
-
* keys (e.g. `mcp`) produce no symlink and are skipped here -- their host
|
|
77
|
-
* effect (a config merge) is handled separately (US-004/US-005).
|
|
78
|
-
*/
|
|
79
|
-
export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
|
|
80
68
|
/** Classify a host path against the link that should own it. */
|
|
81
69
|
export declare function linkStatus(link: WiredLink, packDir?: string): LinkStatus;
|
|
82
70
|
/** A content pack's manifest plus the install-time stamped source. */
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
import * as fs from 'fs';
|
|
40
40
|
import * as path from 'path';
|
|
41
41
|
import * as yaml from 'js-yaml';
|
|
42
|
-
import { CONTRIBUTION_TABLE
|
|
42
|
+
import { CONTRIBUTION_TABLE } from './contribution-table.js';
|
|
43
|
+
import { contributionLinks } from './pack-contribution-links.js';
|
|
44
|
+
export { contributionLinks } from './pack-contribution-links.js';
|
|
43
45
|
import { unregisterMcpServers, } from '../commands/mcp-registration.js';
|
|
44
46
|
export { readHqVersion, readRecommendedPackages } from './hq-core-yaml.js';
|
|
45
47
|
/**
|
|
@@ -78,60 +80,6 @@ export function mergeKeys(contributes) {
|
|
|
78
80
|
}
|
|
79
81
|
return out;
|
|
80
82
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Map one SYMLINK contributes entry to its source/host paths, READING the
|
|
83
|
-
* single-source `CONTRIBUTION_TABLE` (US-003). The src is the expanded pack
|
|
84
|
-
* payload; the dst is `<host-dir>/<payload-basename>` so per-key suffixes
|
|
85
|
-
* (`.md`, `.sh`, none) carry through.
|
|
86
|
-
*
|
|
87
|
-
* Returns `null` for a `wire: 'merge'` key (e.g. `mcp`): a merge contribution
|
|
88
|
-
* is not a symlink and has no `WiredLink`. Callers that only handle symlinks
|
|
89
|
-
* (contributionLinks/unwirePack) skip those keys.
|
|
90
|
-
*/
|
|
91
|
-
function linkFor(hqRoot, packDir, key, item) {
|
|
92
|
-
const row = CONTRIBUTION_TABLE[key];
|
|
93
|
-
if (row.wire !== 'symlink')
|
|
94
|
-
return null;
|
|
95
|
-
const payloadRel = payloadFor(key, item); // e.g. commands/foo.md
|
|
96
|
-
// dst keeps the payload's basename (so the suffix carries through) under the
|
|
97
|
-
// host directory declared in the table.
|
|
98
|
-
const dstRel = path.join(row.host, path.basename(payloadRel));
|
|
99
|
-
return {
|
|
100
|
-
key,
|
|
101
|
-
item,
|
|
102
|
-
src: path.join(packDir, payloadRel),
|
|
103
|
-
dst: path.join(hqRoot, dstRel),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Every SYMLINK a pack's `contributes` block declares. Empty subfields and
|
|
108
|
-
* non-array values are ignored, mirroring scan-packages.sh. `wire: 'merge'`
|
|
109
|
-
* keys (e.g. `mcp`) produce no symlink and are skipped here -- their host
|
|
110
|
-
* effect (a config merge) is handled separately (US-004/US-005).
|
|
111
|
-
*/
|
|
112
|
-
export function contributionLinks(hqRoot, packDir, contributes) {
|
|
113
|
-
const links = [];
|
|
114
|
-
for (const [key, items] of Object.entries(contributes)) {
|
|
115
|
-
if (!(key in CONTRIBUTION_TABLE))
|
|
116
|
-
continue; // unknown key -> ignore
|
|
117
|
-
if (!Array.isArray(items))
|
|
118
|
-
continue;
|
|
119
|
-
// INVARIANT (US-005): merge keys are NEVER symlinked. They route to
|
|
120
|
-
// registerMcpServers (commands/mcp-registration.ts) instead. Skip them
|
|
121
|
-
// explicitly here -- not just via linkFor returning null -- so the symlink
|
|
122
|
-
// path provably excludes them.
|
|
123
|
-
if (routeContribution(key) === 'merge')
|
|
124
|
-
continue;
|
|
125
|
-
for (const item of items) {
|
|
126
|
-
if (typeof item !== 'string' || item.length === 0)
|
|
127
|
-
continue;
|
|
128
|
-
const link = linkFor(hqRoot, packDir, key, item);
|
|
129
|
-
if (link)
|
|
130
|
-
links.push(link); // null for merge keys -- skip
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return links;
|
|
134
|
-
}
|
|
135
83
|
/**
|
|
136
84
|
* Canonical absolute path — resolves symlinks and platform aliases such as
|
|
137
85
|
* macOS `/var` ↔ `/private/var` so equivalent paths compare equal.
|