@indigoai-us/hq-cli 5.345.12 → 5.345.14
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 +7 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +16 -16
- package/dist/commands/core-archive-old-threads-fast.d.ts +2 -0
- package/dist/commands/core-archive-old-threads-fast.js +5 -0
- package/dist/commands/core-codex-skill-bridge-fast.d.ts +2 -0
- package/dist/commands/core-codex-skill-bridge-fast.js +5 -0
- package/dist/commands/core-delegate-w1b.js +14 -9
- package/dist/commands/core-delegate.d.ts +1 -0
- package/dist/commands/core-delegate.js +10 -2
- package/dist/commands/core-hq-status-summary-fast.d.ts +2 -0
- package/dist/commands/core-hq-status-summary-fast.js +11 -0
- package/dist/commands/core-resize-screenshot-fast.d.ts +2 -0
- package/dist/commands/core-resize-screenshot-fast.js +5 -0
- package/dist/commands/core-sentry-report-fast.d.ts +2 -0
- package/dist/commands/core-sentry-report-fast.js +33 -0
- package/dist/commands/core-soft-timeout-fast.d.ts +2 -0
- package/dist/commands/core-soft-timeout-fast.js +5 -0
- package/dist/commands/core-token-usage-report-fast.d.ts +2 -0
- package/dist/commands/core-token-usage-report-fast.js +5 -0
- package/dist/commands/core-worktree-fast.d.ts +2 -0
- package/dist/commands/core-worktree-fast.js +5 -0
- package/dist/commands/lanes.js +45 -4
- package/dist/fast-core.d.ts +1 -1
- package/dist/fast-core.js +10 -4
- package/dist/fast-scaffold.d.ts +6 -3
- package/dist/fast-scaffold.js +103 -4
- package/dist/lib/core-utils/sentry-report.js +8 -2
- package/dist/lib/lanes/compaction.d.ts +2 -2
- package/dist/lib/lanes/compaction.js +221 -54
- package/dist/lib/lanes/constants.d.ts +1 -1
- package/dist/lib/lanes/constants.js +1 -1
- package/dist/lib/lanes/move-account.js +24 -1
- package/dist/lib/lanes/reconcile.d.ts +4 -1
- package/dist/lib/lanes/reconcile.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.14] — 2026-10-06
|
|
6
|
+
- Route frequently called native core commands through isolated fast handlers.
|
|
7
|
+
- Allow lane hierarchies up to four levels and include actionable senior-chain details when depth limits refuse a create or re-parent.
|
|
8
|
+
|
|
9
|
+
## [5.345.13] — 2026-10-06
|
|
10
|
+
- Stream provider transcript scans and account-move comparisons so large session files do not require whole-file reads.
|
|
11
|
+
|
|
5
12
|
## [5.345.12] — 2026-10-06
|
|
6
13
|
- Route rebuild-index, pin, scheduled-job commands, and worker-skill lint through the fast CLI entry path to reduce command startup time.
|
|
7
14
|
- `hq daemon` on desktop machines no longer starts a lane reconcile process every 15 seconds when no lane needs it. It checks the lane store first: live lanes, keep-alive lanes, pending resumes and pending questions keep the 15-second cadence; a store with only settled lanes is reconciled once a minute, as before 15-second polling; an HQ folder with no lanes, questions, lead registry or lanes config is not reconciled at all. Outposts and self-hosted machines are unchanged.
|
|
@@ -65,11 +65,11 @@ done
|
|
|
65
65
|
[ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
|
|
66
66
|
jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
|
|
67
67
|
|
|
68
|
-
COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
|
|
69
|
-
PROJECT="$(jq -r '.project.name // empty' "$MANIFEST")"
|
|
70
|
-
PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST")"
|
|
71
|
-
DISPLAY="$(jq -r '.to.displayName // .to.principal // empty' "$MANIFEST")"
|
|
72
|
-
STATUS="$(jq -r '.status // empty' "$MANIFEST")"
|
|
68
|
+
COMPANY="$(jq -r '.company // empty' "$MANIFEST" | tr -d '\r')"
|
|
69
|
+
PROJECT="$(jq -r '.project.name // empty' "$MANIFEST" | tr -d '\r')"
|
|
70
|
+
PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST" | tr -d '\r')"
|
|
71
|
+
DISPLAY="$(jq -r '.to.displayName // .to.principal // empty' "$MANIFEST" | tr -d '\r')"
|
|
72
|
+
STATUS="$(jq -r '.status // empty' "$MANIFEST" | tr -d '\r')"
|
|
73
73
|
[ -n "$COMPANY" ] || die "manifest has no company"
|
|
74
74
|
[ -n "$PROJECT" ] || die "manifest has no project.name"
|
|
75
75
|
[ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
|
|
@@ -96,7 +96,7 @@ case "$PRINCIPAL" in
|
|
|
96
96
|
TAIL="$(printf '%s' "$PRINCIPAL" | tail -c 9 | tr '[:upper:]' '[:lower:]')"
|
|
97
97
|
LEGACY_GROUP="grp_dlg-$TAIL"
|
|
98
98
|
NEXT_GROUP="$(agent_group_id "$PRINCIPAL")"
|
|
99
|
-
RECORDED_GROUP="$(jq -r '.grantPrincipal // empty' "$MANIFEST")"
|
|
99
|
+
RECORDED_GROUP="$(jq -r '.grantPrincipal // empty' "$MANIFEST" | tr -d '\r')"
|
|
100
100
|
case "$RECORDED_GROUP" in
|
|
101
101
|
"$LEGACY_GROUP"|"$NEXT_GROUP") GRANT_PRINCIPAL="$RECORDED_GROUP" ;;
|
|
102
102
|
*) GRANT_PRINCIPAL="$NEXT_GROUP" ;;
|
|
@@ -109,7 +109,7 @@ case "$STATUS" in
|
|
|
109
109
|
*) die "manifest status is '$STATUS' — grants run from 'building' (or re-run from 'granted'), not from there" ;;
|
|
110
110
|
esac
|
|
111
111
|
|
|
112
|
-
PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
|
|
112
|
+
PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST" | tr -d '\r')"
|
|
113
113
|
[ "$PREFIX_COUNT" -gt 0 ] || die "manifest has no vaultPrefixes"
|
|
114
114
|
|
|
115
115
|
# --- validate every prefix BEFORE touching anything --------------------------
|
|
@@ -118,7 +118,7 @@ PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
|
|
|
118
118
|
# not exist in the bucket. Either is a hard error, never a silent grant.
|
|
119
119
|
|
|
120
120
|
BAD="$(jq -r '.vaultPrefixes[] | .prefix
|
|
121
|
-
| select((endswith("/") | not) or startswith("companies/") or startswith("/"))' "$MANIFEST")"
|
|
121
|
+
| select((endswith("/") | not) or startswith("companies/") or startswith("/"))' "$MANIFEST" | tr -d '\r')"
|
|
122
122
|
if [ -n "$BAD" ]; then
|
|
123
123
|
die "prefix violates the hq-files prefix conventions (must be company-relative folder form ending in '/'): $BAD"
|
|
124
124
|
fi
|
|
@@ -130,8 +130,8 @@ if [ "$GRANT_PRINCIPAL" != "$PRINCIPAL" ]; then
|
|
|
130
130
|
echo " 0. Agent recipient: grants flow through group $GRANT_PRINCIPAL (created if absent, containing exactly $DISPLAY)"
|
|
131
131
|
fi
|
|
132
132
|
echo " 1. Push companies/$COMPANY/projects/$PROJECT/ to the vault (on-conflict keep)"
|
|
133
|
-
jq -r '.vaultPrefixes[] | " 2. Grant \(.permission) on \(.prefix) — \(.reason // "")"' "$MANIFEST"
|
|
134
|
-
WRITE_PREFIXES="$(jq -r '.vaultPrefixes[] | select(.permission == "write") | .prefix' "$MANIFEST")"
|
|
133
|
+
jq -r '.vaultPrefixes[] | " 2. Grant \(.permission) on \(.prefix) — \(.reason // "")"' "$MANIFEST" | tr -d '\r'
|
|
134
|
+
WRITE_PREFIXES="$(jq -r '.vaultPrefixes[] | select(.permission == "write") | .prefix' "$MANIFEST" | tr -d '\r')"
|
|
135
135
|
if [ -n "$WRITE_PREFIXES" ]; then
|
|
136
136
|
echo
|
|
137
137
|
echo "NOTE: granting 'write' is a privilege escalation. The recipient will be able"
|
|
@@ -155,7 +155,7 @@ hq sync push "companies/$COMPANY/projects/$PROJECT/" --company "$COMPANY" --on-c
|
|
|
155
155
|
|
|
156
156
|
jq -r --arg co "$COMPANY" \
|
|
157
157
|
'((.knowledge // []) + (.policies // []))[] | select(startswith("companies/" + $co + "/"))' \
|
|
158
|
-
"$MANIFEST" | while IFS= read -r kpath; do
|
|
158
|
+
"$MANIFEST" | tr -d '\r' | while IFS= read -r kpath; do
|
|
159
159
|
if [ -e "$HQ_ROOT/$kpath" ]; then
|
|
160
160
|
hq sync push "$kpath" --company "$COMPANY" --on-conflict keep \
|
|
161
161
|
|| die "vault push failed for referenced knowledge: $kpath"
|
|
@@ -195,8 +195,8 @@ case "$GRANT_PRINCIPAL" in
|
|
|
195
195
|
if printf '%s' "$RESOLVED" | jq -e '.status == "found" and (.person.personUid | type == "string") and (.person.companyUid | type == "string")' >/dev/null; then
|
|
196
196
|
[ "$RESOLVE_RC" -eq 0 ] || die "recipient resolution failed"
|
|
197
197
|
EXPECTED_TYPE=person
|
|
198
|
-
EXPECTED_ID="$(printf '%s' "$RESOLVED" | jq -r '.person.personUid')"
|
|
199
|
-
EXPECTED_COMPANY="$(printf '%s' "$RESOLVED" | jq -r '.person.companyUid')"
|
|
198
|
+
EXPECTED_ID="$(printf '%s' "$RESOLVED" | jq -r '.person.personUid' | tr -d '\r')"
|
|
199
|
+
EXPECTED_COMPANY="$(printf '%s' "$RESOLVED" | jq -r '.person.companyUid' | tr -d '\r')"
|
|
200
200
|
elif printf '%s' "$RESOLVED" | jq -e '.status == "not_found"' >/dev/null; then
|
|
201
201
|
case "$PRINCIPAL" in *@*) EXPECTED_TYPE=email; EXPECTED_ID="$(printf '%s' "$PRINCIPAL" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" ;; *) die "unresolved recipient" ;; esac
|
|
202
202
|
else
|
|
@@ -223,15 +223,15 @@ read_acl() {
|
|
|
223
223
|
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
224
224
|
i=0
|
|
225
225
|
while [ "$i" -lt "$PREFIX_COUNT" ]; do
|
|
226
|
-
PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST")"
|
|
227
|
-
PERM="$(jq -r ".vaultPrefixes[$i].permission" "$MANIFEST")"
|
|
226
|
+
PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST" | tr -d '\r')"
|
|
227
|
+
PERM="$(jq -r ".vaultPrefixes[$i].permission" "$MANIFEST" | tr -d '\r')"
|
|
228
228
|
# Always address the recursive "folder/*" pattern explicitly. Since hq-pro
|
|
229
229
|
# #3662 (2026-09-23) the server echoes the raw pattern back on read, and a
|
|
230
230
|
# bare trailing "/" can mean a PRIVATE folder pattern when that flag is on.
|
|
231
231
|
# A delegation grant is always the recursive share, never a private folder.
|
|
232
232
|
ACL_PATTERN="${PFX}*"
|
|
233
233
|
BEFORE="$(read_acl "$ACL_PATTERN")" || die "ACL preflight failed; no share attempted for $PFX"
|
|
234
|
-
EXPECTED_COMPANY="$(printf '%s' "$BEFORE" | jq -r '.companyUid')"
|
|
234
|
+
EXPECTED_COMPANY="$(printf '%s' "$BEFORE" | jq -r '.companyUid' | tr -d '\r')"
|
|
235
235
|
SHARE_RC=0
|
|
236
236
|
hq files share "$ACL_PATTERN" --with "$GRANT_PRINCIPAL" --permission "$PERM" --company "$COMPANY" || SHARE_RC=$?
|
|
237
237
|
[ "$SHARE_RC" -eq 0 ] || echo "hq-delegate-grant: share reported an error on $PFX; checking exact grant" >&2
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { runCodexSkillBridgeCommand } from "../lib/core-utils/codex-skill-bridge-entry.js";
|
|
2
|
+
export function runCodexSkillBridgeFast(args, _stdin, _cwd, hqRoot) {
|
|
3
|
+
return runCodexSkillBridgeCommand([...args], { root: hqRoot });
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=core-codex-skill-bridge-fast.js.map
|
|
@@ -7,7 +7,7 @@ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
|
7
7
|
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
8
8
|
import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
|
|
9
9
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
10
|
-
import { commandExists, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
|
|
10
|
+
import { commandExists, delegateDiagnosticPath, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
|
|
11
11
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
12
12
|
import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
|
|
13
13
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
@@ -179,11 +179,16 @@ function bashPath(value) {
|
|
|
179
179
|
const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
|
|
180
180
|
return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
|
|
181
181
|
}
|
|
182
|
+
function shellFallbackArgs(args) {
|
|
183
|
+
if (process.platform !== "win32")
|
|
184
|
+
return args;
|
|
185
|
+
return args.map((arg, index) => args[index - 1] === "--manifest" && /^[a-z]:[\\/]/i.test(arg) ? bashPath(arg) : arg);
|
|
186
|
+
}
|
|
182
187
|
function runShellFallback(name, args, context) {
|
|
183
188
|
const root = context.root ?? context.cwd;
|
|
184
189
|
const result = runBundledScript({
|
|
185
190
|
asset: ASSETS[name],
|
|
186
|
-
args,
|
|
191
|
+
args: shellFallbackArgs(args),
|
|
187
192
|
cwd: context.cwd,
|
|
188
193
|
hqRoot: bashPath(root),
|
|
189
194
|
captureOutput: true,
|
|
@@ -404,16 +409,16 @@ function runGrant(args, context) {
|
|
|
404
409
|
if (!manifestPath)
|
|
405
410
|
return fail("grant", "--manifest is required");
|
|
406
411
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
407
|
-
return fail("grant", `manifest not found: ${
|
|
412
|
+
return fail("grant", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
|
|
408
413
|
let manifest;
|
|
409
414
|
try {
|
|
410
415
|
manifest = readJsonFile(manifestPath);
|
|
411
416
|
}
|
|
412
417
|
catch {
|
|
413
|
-
return fail("grant", `manifest is not valid JSON: ${
|
|
418
|
+
return fail("grant", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
|
|
414
419
|
}
|
|
415
420
|
if (manifest === null || manifest === false)
|
|
416
|
-
return fail("grant", `manifest is not valid JSON: ${
|
|
421
|
+
return fail("grant", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
|
|
417
422
|
if (!isRecord(manifest))
|
|
418
423
|
return fail("grant", "manifest has no company");
|
|
419
424
|
const company = jqRawPath(manifest, "company");
|
|
@@ -790,10 +795,10 @@ function runSend(args, context) {
|
|
|
790
795
|
if (!manifestPath)
|
|
791
796
|
return fail("send", "--manifest is required");
|
|
792
797
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
793
|
-
return fail("send", `manifest not found: ${
|
|
798
|
+
return fail("send", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
|
|
794
799
|
const manifest = jsonFile(manifestPath);
|
|
795
800
|
if (!manifest)
|
|
796
|
-
return fail("send", `manifest is not valid JSON: ${
|
|
801
|
+
return fail("send", `manifest is not valid JSON: ${delegateDiagnosticPath(manifestPath)}`);
|
|
797
802
|
if (send) {
|
|
798
803
|
const publishedPath = path.join(context.root ?? context.cwd, jqRawPath(manifest, "publication.manifestPath"));
|
|
799
804
|
if (malformedJsonFile(publishedPath)) {
|
|
@@ -1047,12 +1052,12 @@ function runVerify(args, context) {
|
|
|
1047
1052
|
if (!manifestPath)
|
|
1048
1053
|
return fail("verify", "--manifest is required (or --dry-run with --company/--project/--to)");
|
|
1049
1054
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
1050
|
-
return fail("verify", `manifest not found: ${
|
|
1055
|
+
return fail("verify", `manifest not found: ${delegateDiagnosticPath(manifestPath)}`);
|
|
1051
1056
|
if (malformedJsonFile(manifestPath))
|
|
1052
1057
|
return runShellFallback("verify", args, context);
|
|
1053
1058
|
const manifest = jsonFile(manifestPath);
|
|
1054
1059
|
if (!manifest)
|
|
1055
|
-
return fail("verify", `jq: error: ${
|
|
1060
|
+
return fail("verify", `jq: error: ${delegateDiagnosticPath(manifestPath)}: Invalid JSON`);
|
|
1056
1061
|
const lock = `${manifestPath}.send-lock`;
|
|
1057
1062
|
try {
|
|
1058
1063
|
fs.mkdirSync(lock);
|
|
@@ -40,6 +40,7 @@ export declare function jqRawPaths(value: unknown, dottedPaths: string[], fallba
|
|
|
40
40
|
export declare function requireRecord(value: unknown, property: string): JsonRecord;
|
|
41
41
|
export declare function moveFile(source: string, target: string): void;
|
|
42
42
|
export declare function writeJsonAtomically(target: string, value: unknown, siblingTemp?: boolean): void;
|
|
43
|
+
export declare function delegateDiagnosticPath(argument: string): string;
|
|
43
44
|
/** Fast-entry adapter; family gating and shell fallback happen before import. */
|
|
44
45
|
export declare function runDelegateFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, commandName: string): number;
|
|
45
46
|
/** Register the Wave 1a delegate commands and preserve the shell rollback path. */
|
|
@@ -188,6 +188,7 @@ export function moveFile(source, target) {
|
|
|
188
188
|
}
|
|
189
189
|
export function writeJsonAtomically(target, value, siblingTemp = false) {
|
|
190
190
|
const serialized = serializeJqJson(value, "pretty");
|
|
191
|
+
// Native delegate JSON writers use LF on every platform.
|
|
191
192
|
if (siblingTemp) {
|
|
192
193
|
const temporary = `${target}.tmp`;
|
|
193
194
|
fs.writeFileSync(temporary, serialized);
|
|
@@ -557,6 +558,13 @@ function parseStandard(name, args) {
|
|
|
557
558
|
}
|
|
558
559
|
return { values };
|
|
559
560
|
}
|
|
561
|
+
export function delegateDiagnosticPath(argument) {
|
|
562
|
+
// Git Bash receives drive-qualified native paths in MSYS spelling. Preserve
|
|
563
|
+
// relative and already-MSYS path arguments exactly as supplied.
|
|
564
|
+
if (process.platform === "win32" && /^[a-z]:[\\/]/i.test(argument))
|
|
565
|
+
return shellDisplayPath(argument);
|
|
566
|
+
return argument;
|
|
567
|
+
}
|
|
560
568
|
function existingManifest(name, args, context) {
|
|
561
569
|
const prefix = `hq-delegate-${name}:`;
|
|
562
570
|
const parsed = parseStandard(name, args);
|
|
@@ -575,11 +583,11 @@ function existingManifest(name, args, context) {
|
|
|
575
583
|
}
|
|
576
584
|
const file = path.resolve(context.cwd, manifestArg);
|
|
577
585
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
578
|
-
writeErr(`${prefix} manifest not found: ${
|
|
586
|
+
writeErr(`${prefix} manifest not found: ${delegateDiagnosticPath(manifestArg)}\n`);
|
|
579
587
|
return { code: 1, parsed };
|
|
580
588
|
}
|
|
581
589
|
if (!jqValid(file)) {
|
|
582
|
-
writeErr(`${prefix} manifest is not valid JSON: ${
|
|
590
|
+
writeErr(`${prefix} manifest is not valid JSON: ${delegateDiagnosticPath(manifestArg)}\n`);
|
|
583
591
|
return { code: 1, parsed };
|
|
584
592
|
}
|
|
585
593
|
return { file, parsed, document: readJsonFile(file) };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { summarizeHqStatus } from "../lib/core-utils/hq-status-summary.js";
|
|
2
|
+
export function runHqStatusSummaryFast(args, _stdin, cwd, _hqRoot, _commandName, hqRootArg) {
|
|
3
|
+
// The full graph canonicalizes an explicit --hq-root through resolveLiveRoot.
|
|
4
|
+
// Keep that spelling on the authoritative path so the command graph can
|
|
5
|
+
// preserve its canonicalization and diagnostics. This adapter owns only the
|
|
6
|
+
// ordinary cwd-rooted invocation.
|
|
7
|
+
if (hqRootArg)
|
|
8
|
+
return undefined;
|
|
9
|
+
return summarizeHqStatus([...args], { cwd });
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=core-hq-status-summary-fast.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { reportSentryEvent } from "../lib/core-utils/sentry-report.js";
|
|
2
|
+
export function runSentryReportFast(args, stdin, _cwd, hqRoot) {
|
|
3
|
+
let dryRun = false;
|
|
4
|
+
let timeoutMs;
|
|
5
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
6
|
+
const arg = args[index];
|
|
7
|
+
if (arg === "--dry-run") {
|
|
8
|
+
dryRun = true;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (arg === "--timeout-ms") {
|
|
12
|
+
const value = args[index + 1];
|
|
13
|
+
if (value === undefined || value.startsWith("-"))
|
|
14
|
+
return Promise.resolve(undefined);
|
|
15
|
+
timeoutMs = Number(value);
|
|
16
|
+
index += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg?.startsWith("--timeout-ms=")) {
|
|
20
|
+
timeoutMs = Number(arg.slice("--timeout-ms=".length));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Let Commander retain its authoritative diagnostics for forms this
|
|
24
|
+
// lightweight parser does not own. This branch emits no output.
|
|
25
|
+
return Promise.resolve(undefined);
|
|
26
|
+
}
|
|
27
|
+
// Live sends need main.ts's initialized Sentry transport. Keep those calls on
|
|
28
|
+
// the authoritative command graph; only side-effect-free validation is fast.
|
|
29
|
+
if (!dryRun)
|
|
30
|
+
return Promise.resolve(undefined);
|
|
31
|
+
return reportSentryEvent(stdin, { dryRun, timeoutMs, hqRoot }).then((result) => result.code);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=core-sentry-report-fast.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -2029,6 +2029,37 @@ function depthOfRef(hqRoot, ref, ctx, visited = new Set(), override) {
|
|
|
2029
2029
|
function derivedLaneDepth(hqRoot, senior, ctx, override) {
|
|
2030
2030
|
return depthOfRef(hqRoot, senior, ctx, new Set(), override) + 1;
|
|
2031
2031
|
}
|
|
2032
|
+
function seniorLaneChain(hqRoot, ref, ctx, visited = new Set(), override) {
|
|
2033
|
+
if (ref.kind === "user")
|
|
2034
|
+
return [];
|
|
2035
|
+
const key = seniorKey(ref);
|
|
2036
|
+
if (visited.has(key)) {
|
|
2037
|
+
throw new LanesError("senior_cycle", `Cycle in senior chain at ${key}`, { at: ref });
|
|
2038
|
+
}
|
|
2039
|
+
visited.add(key);
|
|
2040
|
+
if (ref.kind === "session") {
|
|
2041
|
+
if (!ref.id) {
|
|
2042
|
+
throw new LanesError("invalid_senior_ref", "Session senior is missing an id", { senior: ref });
|
|
2043
|
+
}
|
|
2044
|
+
return seniorLaneChain(hqRoot, seniorOfSession(hqRoot, ref.id, ctx), ctx, visited, override);
|
|
2045
|
+
}
|
|
2046
|
+
if (!ref.id) {
|
|
2047
|
+
throw new LanesError("invalid_senior_ref", "Lane senior is missing an id", { senior: ref });
|
|
2048
|
+
}
|
|
2049
|
+
const lane = readLane(hqRoot, ref.id);
|
|
2050
|
+
const isOverridden = override?.laneId === lane.lane_id;
|
|
2051
|
+
const senior = isOverridden ? override.senior : lane.senior;
|
|
2052
|
+
const prefix = seniorLaneChain(hqRoot, senior, ctx, visited, override);
|
|
2053
|
+
const depth = isOverridden ? override.depth : derivedLaneDepth(hqRoot, lane.senior, ctx, override);
|
|
2054
|
+
return [...prefix, { lane_id: lane.lane_id, depth }];
|
|
2055
|
+
}
|
|
2056
|
+
function maxLaneDepthError(depth, chain, description, extraDetails = {}) {
|
|
2057
|
+
const parentLane = [...chain].reverse().find((entry) => entry.depth < MAX_LANE_DEPTH);
|
|
2058
|
+
const nextStep = parentLane
|
|
2059
|
+
? `Ask your senior lane ${parentLane.lane_id} to create this worker instead, or re-parent this lane under a shallower senior.`
|
|
2060
|
+
: "Ask a shallower senior to create this lane, or re-parent it to a shallower senior.";
|
|
2061
|
+
return new LanesError("max_lane_depth", `${description}: senior chain length ${depth} exceeds cap ${MAX_LANE_DEPTH}`, { ...extraDetails, depth, max: MAX_LANE_DEPTH, senior_chain: chain, next_step: nextStep });
|
|
2062
|
+
}
|
|
2032
2063
|
function callerDepth(hqRoot, ctx) {
|
|
2033
2064
|
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
2034
2065
|
if (laneId) {
|
|
@@ -3012,7 +3043,13 @@ async function runCreate(ctx, opts) {
|
|
|
3012
3043
|
}
|
|
3013
3044
|
const depth = callerDepth(hqRoot, ctx);
|
|
3014
3045
|
if (depth >= MAX_LANE_DEPTH) {
|
|
3015
|
-
|
|
3046
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
3047
|
+
const chain = callerLaneId
|
|
3048
|
+
? seniorLaneChain(hqRoot, { kind: "lane", id: callerLaneId }, ctx)
|
|
3049
|
+
: seniorLaneChain(hqRoot, sessionIdFromEnv(ctx.env)
|
|
3050
|
+
? { kind: "session", id: sessionIdFromEnv(ctx.env) }
|
|
3051
|
+
: { kind: "user" }, ctx);
|
|
3052
|
+
throw maxLaneDepthError(depth + 1, chain, `A depth-${depth} caller may not spawn a depth-${depth + 1} lane`);
|
|
3016
3053
|
}
|
|
3017
3054
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
|
|
3018
3055
|
assertSeniorExists(hqRoot, senior, ctx);
|
|
@@ -3033,7 +3070,7 @@ async function runCreate(ctx, opts) {
|
|
|
3033
3070
|
const hireGrant = requestedHireGrant ?? 0;
|
|
3034
3071
|
const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
|
|
3035
3072
|
if (laneDepth > MAX_LANE_DEPTH) {
|
|
3036
|
-
throw
|
|
3073
|
+
throw maxLaneDepthError(laneDepth, seniorLaneChain(hqRoot, senior, ctx), `Creating lane at depth ${laneDepth} is refused`);
|
|
3037
3074
|
}
|
|
3038
3075
|
// A no-company lane must not call company resolution (openMeshTransport).
|
|
3039
3076
|
if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
|
|
@@ -7081,7 +7118,9 @@ function runReparentLocked(ctx, laneId, opts) {
|
|
|
7081
7118
|
}
|
|
7082
7119
|
const newDepth = derivedLaneDepth(hqRoot, nextSenior, ctx);
|
|
7083
7120
|
if (newDepth > MAX_LANE_DEPTH) {
|
|
7084
|
-
|
|
7121
|
+
const chain = seniorLaneChain(hqRoot, nextSenior, ctx);
|
|
7122
|
+
chain.push({ lane_id: lane.lane_id, depth: newDepth });
|
|
7123
|
+
throw maxLaneDepthError(newDepth, chain, `Re-parent would put lane at depth ${newDepth}`);
|
|
7085
7124
|
}
|
|
7086
7125
|
const pending = pendingQuestionsForLane(hqRoot, laneId);
|
|
7087
7126
|
const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
|
|
@@ -7099,7 +7138,9 @@ function runReparentLocked(ctx, laneId, opts) {
|
|
|
7099
7138
|
const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
|
|
7100
7139
|
const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
|
|
7101
7140
|
if (descendantDepth > MAX_LANE_DEPTH) {
|
|
7102
|
-
|
|
7141
|
+
const chain = seniorLaneChain(hqRoot, other.senior, ctx, new Set(), override);
|
|
7142
|
+
chain.push({ lane_id: other.lane_id, depth: descendantDepth });
|
|
7143
|
+
throw maxLaneDepthError(descendantDepth, chain, `Re-parent would put descendant ${other.lane_id} at depth ${descendantDepth}`, { lane_id: other.lane_id });
|
|
7103
7144
|
}
|
|
7104
7145
|
if (descendantDepth > oldDescendantDepth &&
|
|
7105
7146
|
pendingQuestionsForLane(hqRoot, other.lane_id).length > 0) {
|
package/dist/fast-core.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* module. The registration in `core.ts` remains the complete fallback path.
|
|
8
8
|
*/
|
|
9
9
|
import { parseFastCore } from "./fast-scaffold.js";
|
|
10
|
-
export declare function readStdinIfPiped(stream: NodeJS.ReadStream): Promise<string>;
|
|
10
|
+
export declare function readStdinIfPiped(stream: NodeJS.ReadStream, maxBytes?: number): Promise<string>;
|
|
11
11
|
/**
|
|
12
12
|
* Run the fast-path core command named in `argv` and exit with its code.
|
|
13
13
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
package/dist/fast-core.js
CHANGED
|
@@ -14,12 +14,18 @@ const FAST_ROUTE_PROFILE_TOKEN = "hq-fast-route";
|
|
|
14
14
|
function wantsFastRouteProfile(env) {
|
|
15
15
|
return env.NODE_DEBUG === FAST_ROUTE_PROFILE_TOKEN;
|
|
16
16
|
}
|
|
17
|
-
export async function readStdinIfPiped(stream) {
|
|
17
|
+
export async function readStdinIfPiped(stream, maxBytes) {
|
|
18
18
|
if (stream.isTTY)
|
|
19
19
|
return "";
|
|
20
20
|
const chunks = [];
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
let bytesRead = 0;
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
+
bytesRead += buffer.length;
|
|
25
|
+
if (maxBytes !== undefined && bytesRead > maxBytes)
|
|
26
|
+
return "";
|
|
27
|
+
chunks.push(buffer);
|
|
28
|
+
}
|
|
23
29
|
return Buffer.concat(chunks).toString("utf8");
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
@@ -105,7 +111,7 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
105
111
|
handlerImportMs = profileElapsed(handlerImportStartedAt, profileMark());
|
|
106
112
|
const stdinStartedAt = profileMark();
|
|
107
113
|
const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
|
|
108
|
-
const stdin = readsStdin ? await readStdinIfPiped(process.stdin) : "";
|
|
114
|
+
const stdin = readsStdin ? await readStdinIfPiped(process.stdin, entry.kind === "native" ? entry.stdinMaxBytes : undefined) : "";
|
|
109
115
|
stdinReadMs = profileElapsed(stdinStartedAt, profileMark());
|
|
110
116
|
if (profile && profileStart !== undefined) {
|
|
111
117
|
process.stderr.write(`HQ_FAST_ROUTE_PROFILE ${JSON.stringify({
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export type FastScaffoldScript = {
|
|
|
42
42
|
shellHqRoot?: "none" | "live" | "reconcile-default";
|
|
43
43
|
/** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
|
|
44
44
|
readsStdin?: boolean;
|
|
45
|
+
/** Bound bytes buffered from stdin; overflow is treated as empty input. */
|
|
46
|
+
stdinMaxBytes?: number;
|
|
45
47
|
/** Per-invocation stdin policy for handlers whose shell reads conditionally. */
|
|
46
48
|
readsStdinFor?: (args: readonly string[]) => boolean;
|
|
47
49
|
/** Whether the bundled fast-route module closure needs Node's network timeout patch. */
|
|
@@ -78,9 +80,10 @@ export declare function parseSecretsEnvFastRequest(argv: readonly string[]): Par
|
|
|
78
80
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
79
81
|
* fast-path script. Returns null for anything else so the caller defers to the
|
|
80
82
|
* full CLI. Mirrors the `core` group's single `--hq-root` option (both the
|
|
81
|
-
* `--hq-root <path>` and `--hq-root=<path>` forms)
|
|
82
|
-
*
|
|
83
|
-
*
|
|
83
|
+
* `--hq-root <path>` and `--hq-root=<path>` forms) before the command name.
|
|
84
|
+
* Commander also accepts that parent option after a subcommand; those
|
|
85
|
+
* invocations are deferred because the fast parser cannot safely distinguish
|
|
86
|
+
* a parent option from a command's pass-through arguments.
|
|
84
87
|
*/
|
|
85
88
|
export declare function parseFastCore(argv: readonly string[]): ParsedFastCore | null;
|
|
86
89
|
/** Whether this invocation is a fast-path scaffold dispatch. */
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -11,6 +11,101 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
13
13
|
export const FAST_SCAFFOLD_SCRIPTS = [
|
|
14
|
+
{
|
|
15
|
+
name: "sentry report",
|
|
16
|
+
kind: "native",
|
|
17
|
+
modulePath: "./commands/core-sentry-report-fast.js",
|
|
18
|
+
exportName: "runSentryReportFast",
|
|
19
|
+
flagKey: "hq-core-native-fast-route",
|
|
20
|
+
root: "live",
|
|
21
|
+
readsStdinFor: (args) => {
|
|
22
|
+
let dryRun = false;
|
|
23
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
24
|
+
const arg = args[index];
|
|
25
|
+
if (arg === "--dry-run") {
|
|
26
|
+
dryRun = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--timeout-ms") {
|
|
30
|
+
const value = args[index + 1];
|
|
31
|
+
if (value === undefined || value.startsWith("-"))
|
|
32
|
+
return false;
|
|
33
|
+
index += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg?.startsWith("--timeout-ms="))
|
|
37
|
+
continue;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return dryRun;
|
|
41
|
+
},
|
|
42
|
+
stdinMaxBytes: 2_048,
|
|
43
|
+
requiresNodeNetworkCompatibility: true,
|
|
44
|
+
fallbackToCommandGraph: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "soft-timeout",
|
|
48
|
+
kind: "native",
|
|
49
|
+
modulePath: "./commands/core-soft-timeout-fast.js",
|
|
50
|
+
exportName: "runSoftTimeoutFast",
|
|
51
|
+
flagKey: "hq-core-native-fast-route",
|
|
52
|
+
root: "cwd",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "hq-status-summary",
|
|
56
|
+
kind: "native",
|
|
57
|
+
modulePath: "./commands/core-hq-status-summary-fast.js",
|
|
58
|
+
exportName: "runHqStatusSummaryFast",
|
|
59
|
+
flagKey: "hq-core-native-fast-route",
|
|
60
|
+
asset: "core/scripts/hq-status-summary.sh",
|
|
61
|
+
root: "cwd",
|
|
62
|
+
fallbackToCommandGraph: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "archive-old-threads",
|
|
66
|
+
kind: "native",
|
|
67
|
+
modulePath: "./commands/core-archive-old-threads-fast.js",
|
|
68
|
+
exportName: "runArchiveOldThreadsFast",
|
|
69
|
+
flagKey: "hq-core-native-fast-route",
|
|
70
|
+
asset: "core/scripts/archive-old-threads.sh",
|
|
71
|
+
root: "live",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "worktree",
|
|
75
|
+
kind: "native",
|
|
76
|
+
modulePath: "./commands/core-worktree-fast.js",
|
|
77
|
+
exportName: "runWorktreeFast",
|
|
78
|
+
flagKey: "hq-core-native-fast-route",
|
|
79
|
+
asset: "core/scripts/worktree.sh",
|
|
80
|
+
root: "live",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "token-usage-report",
|
|
84
|
+
kind: "native",
|
|
85
|
+
modulePath: "./commands/core-token-usage-report-fast.js",
|
|
86
|
+
exportName: "runTokenUsageReportFast",
|
|
87
|
+
flagKey: "hq-core-native-fast-route",
|
|
88
|
+
asset: "core/scripts/token-usage-report.sh",
|
|
89
|
+
root: "cwd",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "resize-screenshot",
|
|
93
|
+
kind: "native",
|
|
94
|
+
modulePath: "./commands/core-resize-screenshot-fast.js",
|
|
95
|
+
exportName: "runResizeScreenshotFast",
|
|
96
|
+
flagKey: "hq-core-native-fast-route",
|
|
97
|
+
asset: "core/scripts/resize-screenshot.sh",
|
|
98
|
+
root: "cwd",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "codex-skill-bridge",
|
|
102
|
+
kind: "native",
|
|
103
|
+
modulePath: "./commands/core-codex-skill-bridge-fast.js",
|
|
104
|
+
exportName: "runCodexSkillBridgeFast",
|
|
105
|
+
flagKey: "hq-core-native-fast-route",
|
|
106
|
+
asset: "core/scripts/codex-skill-bridge.sh",
|
|
107
|
+
root: "live",
|
|
108
|
+
},
|
|
14
109
|
...["notify", "probe", "remediate", "run"].map((name) => ({
|
|
15
110
|
name: `job ${name}`,
|
|
16
111
|
kind: "native",
|
|
@@ -454,9 +549,10 @@ export function parseSecretsEnvFastRequest(argv) {
|
|
|
454
549
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
455
550
|
* fast-path script. Returns null for anything else so the caller defers to the
|
|
456
551
|
* full CLI. Mirrors the `core` group's single `--hq-root` option (both the
|
|
457
|
-
* `--hq-root <path>` and `--hq-root=<path>` forms)
|
|
458
|
-
*
|
|
459
|
-
*
|
|
552
|
+
* `--hq-root <path>` and `--hq-root=<path>` forms) before the command name.
|
|
553
|
+
* Commander also accepts that parent option after a subcommand; those
|
|
554
|
+
* invocations are deferred because the fast parser cannot safely distinguish
|
|
555
|
+
* a parent option from a command's pass-through arguments.
|
|
460
556
|
*/
|
|
461
557
|
export function parseFastCore(argv) {
|
|
462
558
|
// argv is process.argv: [node, hq, core, ...rest].
|
|
@@ -482,7 +578,10 @@ export function parseFastCore(argv) {
|
|
|
482
578
|
.find((candidate) => candidate.name.split(" ").every((part, index) => commandArgs[index] === part));
|
|
483
579
|
if (!entry)
|
|
484
580
|
return null;
|
|
485
|
-
|
|
581
|
+
const routeArgs = argv.slice(i + entry.name.split(" ").length);
|
|
582
|
+
if (routeArgs.some((arg) => arg === "--hq-root" || arg.startsWith("--hq-root=")))
|
|
583
|
+
return null;
|
|
584
|
+
return { entry, hqRootArg, args: routeArgs };
|
|
486
585
|
}
|
|
487
586
|
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
488
587
|
export function isFastCoreRequest(argv) {
|
|
@@ -24,7 +24,6 @@ import { gte as semverGte, valid as validSemver } from "semver";
|
|
|
24
24
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
25
25
|
import { beforeSend } from "../../sentry-before-send.js";
|
|
26
26
|
import { resolveLiveRoot } from "../../utils/hq-roots.js";
|
|
27
|
-
import { hookTimeoutDebugContextEnabled } from "./hook-timeout-debug-context-flag.js";
|
|
28
27
|
const requireSentry = createRequire(import.meta.url);
|
|
29
28
|
let sentryModule;
|
|
30
29
|
function loadSentryModule() {
|
|
@@ -208,7 +207,14 @@ const defaultDependencies = {
|
|
|
208
207
|
},
|
|
209
208
|
stdout: process.stdout,
|
|
210
209
|
stderr: process.stderr,
|
|
211
|
-
timeoutDebugContextEnabled: (signal) =>
|
|
210
|
+
timeoutDebugContextEnabled: async (signal) => {
|
|
211
|
+
// This optional diagnostic gate is needed only for reports that carry
|
|
212
|
+
// hook-timeout debug context. Load it on demand so ordinary dry-run
|
|
213
|
+
// validation stays on the isolated fast route without importing the full
|
|
214
|
+
// flag registry.
|
|
215
|
+
const { hookTimeoutDebugContextEnabled } = await import("./hook-timeout-debug-context-flag.js");
|
|
216
|
+
return hookTimeoutDebugContextEnabled({}, signal);
|
|
217
|
+
},
|
|
212
218
|
};
|
|
213
219
|
/**
|
|
214
220
|
* Read no more than the accepted event size. Returning early keeps a malformed
|
|
@@ -53,7 +53,7 @@ export interface ProviderCompactionDelta {
|
|
|
53
53
|
replaced: boolean;
|
|
54
54
|
}
|
|
55
55
|
/** Read newly appended, newline-complete marker records after a saved byte offset. */
|
|
56
|
-
export declare function probeProviderCompactionsSince(transcriptPath: string, provider: LaneProvider, startOffset: number): ProviderCompactionDelta;
|
|
56
|
+
export declare function probeProviderCompactionsSince(transcriptPath: string, provider: LaneProvider, startOffset: number, maxLineBytes?: number): ProviderCompactionDelta;
|
|
57
57
|
export declare function providerTranscriptSize(transcriptPath: string): number;
|
|
58
58
|
/**
|
|
59
59
|
* Read only provider compaction markers from a lane transcript. Claude writes
|
|
@@ -64,7 +64,7 @@ export declare function probeProviderCompactions(transcriptPath: string | null,
|
|
|
64
64
|
/** Locate `<home>/projects/<cwd-slug>/<sid>.jsonl` without deriving the slug. */
|
|
65
65
|
export declare function findSessionTranscript(home: string, sessionId: string): string | null;
|
|
66
66
|
/** Count compact boundaries in a transcript. Other lines are never parsed. */
|
|
67
|
-
export declare function probeCompactBoundaries(transcriptPath: string | null): CompactBoundaryProbe;
|
|
67
|
+
export declare function probeCompactBoundaries(transcriptPath: string | null, maxLineBytes?: number): CompactBoundaryProbe;
|
|
68
68
|
interface CompactionCommandResult {
|
|
69
69
|
status: number | null;
|
|
70
70
|
error?: string;
|
|
@@ -49,10 +49,116 @@ export function compactionDue(lane, nowMs, thresholdMs) {
|
|
|
49
49
|
threshold_ms: thresholdMs,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
function
|
|
52
|
+
function readJsonStringAt(input, start) {
|
|
53
|
+
if (input[start] !== '"')
|
|
54
|
+
return null;
|
|
55
|
+
let index = start + 1;
|
|
56
|
+
while (index < input.length) {
|
|
57
|
+
if (input[index] === "\\") {
|
|
58
|
+
index += 2;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (input[index] === '"') {
|
|
62
|
+
try {
|
|
63
|
+
return { value: JSON.parse(input.slice(start, index + 1)), end: index + 1 };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
index += 1;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function skipJsonWhitespace(input, start) {
|
|
74
|
+
let index = start;
|
|
75
|
+
while (/\s/.test(input[index] ?? ""))
|
|
76
|
+
index += 1;
|
|
77
|
+
return index;
|
|
78
|
+
}
|
|
79
|
+
function skipJsonValue(input, start) {
|
|
80
|
+
const first = input[start];
|
|
81
|
+
if (first === '"')
|
|
82
|
+
return readJsonStringAt(input, start)?.end ?? null;
|
|
83
|
+
if (first !== "{" && first !== "[") {
|
|
84
|
+
let index = start;
|
|
85
|
+
while (index < input.length && input[index] !== "," && input[index] !== "}" && input[index] !== "]")
|
|
86
|
+
index += 1;
|
|
87
|
+
return index < input.length ? index : null;
|
|
88
|
+
}
|
|
89
|
+
const stack = [first === "{" ? "}" : "]"];
|
|
90
|
+
let index = start + 1;
|
|
91
|
+
while (index < input.length) {
|
|
92
|
+
const char = input[index];
|
|
93
|
+
if (char === '"') {
|
|
94
|
+
const parsed = readJsonStringAt(input, index);
|
|
95
|
+
if (!parsed)
|
|
96
|
+
return null;
|
|
97
|
+
index = parsed.end;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "{" || char === "[")
|
|
101
|
+
stack.push(char === "{" ? "}" : "]");
|
|
102
|
+
else if (char === "}" || char === "]") {
|
|
103
|
+
if (stack.pop() !== char)
|
|
104
|
+
return null;
|
|
105
|
+
if (stack.length === 0)
|
|
106
|
+
return index + 1;
|
|
107
|
+
}
|
|
108
|
+
index += 1;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/** Extract a top-level JSON value from a bounded prefix, even if its tail is truncated. */
|
|
113
|
+
function topLevelJsonValue(input, targetKey) {
|
|
114
|
+
let index = skipJsonWhitespace(input, 0);
|
|
115
|
+
if (input[index++] !== "{")
|
|
116
|
+
return null;
|
|
117
|
+
while (index < input.length) {
|
|
118
|
+
index = skipJsonWhitespace(input, index);
|
|
119
|
+
if (input[index] === "}")
|
|
120
|
+
return null;
|
|
121
|
+
const key = readJsonStringAt(input, index);
|
|
122
|
+
if (!key)
|
|
123
|
+
return null;
|
|
124
|
+
index = skipJsonWhitespace(input, key.end);
|
|
125
|
+
if (input[index++] !== ":")
|
|
126
|
+
return null;
|
|
127
|
+
index = skipJsonWhitespace(input, index);
|
|
128
|
+
const end = skipJsonValue(input, index);
|
|
129
|
+
if (key.value === targetKey)
|
|
130
|
+
return end === null ? null : input.slice(index, end);
|
|
131
|
+
if (end === null)
|
|
132
|
+
return null;
|
|
133
|
+
index = skipJsonWhitespace(input, end);
|
|
134
|
+
if (input[index] === ",")
|
|
135
|
+
index += 1;
|
|
136
|
+
else
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function topLevelString(input, targetKey) {
|
|
142
|
+
const raw = topLevelJsonValue(input, targetKey);
|
|
143
|
+
if (!raw?.startsWith('"'))
|
|
144
|
+
return null;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(raw);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function isProviderMarkerLine(line, lineNumber, transcriptPath, provider, oversized = false) {
|
|
53
153
|
const needle = provider === "claude" ? "compact_boundary" : "compacted";
|
|
54
154
|
if (!line.includes(needle))
|
|
55
155
|
return false;
|
|
156
|
+
if (oversized) {
|
|
157
|
+
const type = topLevelString(line, "type");
|
|
158
|
+
const subtype = topLevelString(line, "subtype");
|
|
159
|
+
return (provider === "claude" && type === "system" && subtype === "compact_boundary") ||
|
|
160
|
+
(provider === "codex" && type === "compacted");
|
|
161
|
+
}
|
|
56
162
|
let record;
|
|
57
163
|
try {
|
|
58
164
|
const parsed = JSON.parse(line);
|
|
@@ -67,25 +173,61 @@ function isProviderMarkerLine(line, lineNumber, transcriptPath, provider) {
|
|
|
67
173
|
return ((provider === "claude" && record.type === "system" && record.subtype === "compact_boundary") ||
|
|
68
174
|
(provider === "codex" && record.type === "compacted"));
|
|
69
175
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
176
|
+
const TRANSCRIPT_READ_CHUNK_BYTES = 1024 * 1024;
|
|
177
|
+
const TRANSCRIPT_MAX_LINE_BYTES = 64 * 1024 * 1024;
|
|
178
|
+
const TRANSCRIPT_OVERSIZED_PREFIX_BYTES = 64 * 1024;
|
|
179
|
+
/** Read JSONL incrementally, retaining at most one bounded line plus one chunk. */
|
|
180
|
+
function scanTranscriptLines(transcriptPath, startOffset, includeUnterminatedLast, onLine, maxLineBytes = TRANSCRIPT_MAX_LINE_BYTES) {
|
|
74
181
|
const fd = fs.openSync(transcriptPath, "r");
|
|
75
182
|
try {
|
|
76
|
-
const
|
|
77
|
-
if (startOffset >
|
|
78
|
-
return {
|
|
79
|
-
|
|
80
|
-
return { count: 0, next_offset: size, replaced: false };
|
|
81
|
-
const chunkSize = 1024 * 1024;
|
|
82
|
-
const lineParts = [];
|
|
83
|
-
let count = 0;
|
|
183
|
+
const fileSize = fs.fstatSync(fd).size;
|
|
184
|
+
if (startOffset > fileSize) {
|
|
185
|
+
return { file_size: fileSize, next_offset: fileSize, replaced: true };
|
|
186
|
+
}
|
|
84
187
|
let position = startOffset;
|
|
85
|
-
let nextOffset = startOffset;
|
|
86
188
|
let lineNumber = 0;
|
|
87
|
-
|
|
88
|
-
|
|
189
|
+
let lineParts = [];
|
|
190
|
+
let lineBytes = 0;
|
|
191
|
+
let skipLongLine = false;
|
|
192
|
+
let nextOffset = startOffset;
|
|
193
|
+
const appendPart = (part) => {
|
|
194
|
+
if (part.length === 0 || skipLongLine)
|
|
195
|
+
return;
|
|
196
|
+
lineBytes += part.length;
|
|
197
|
+
if (lineBytes > maxLineBytes) {
|
|
198
|
+
skipLongLine = true;
|
|
199
|
+
const prefix = Buffer.alloc(Math.min(TRANSCRIPT_OVERSIZED_PREFIX_BYTES, lineBytes));
|
|
200
|
+
let copied = 0;
|
|
201
|
+
for (const previous of lineParts) {
|
|
202
|
+
const length = Math.min(previous.length, prefix.length - copied);
|
|
203
|
+
if (length <= 0)
|
|
204
|
+
break;
|
|
205
|
+
previous.copy(prefix, copied, 0, length);
|
|
206
|
+
copied += length;
|
|
207
|
+
}
|
|
208
|
+
const partLength = Math.min(part.length, prefix.length - copied);
|
|
209
|
+
if (partLength > 0)
|
|
210
|
+
part.copy(prefix, copied, 0, partLength);
|
|
211
|
+
lineParts = prefix.length > 0 ? [prefix] : [];
|
|
212
|
+
lineBytes = prefix.length;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
lineParts.push(part);
|
|
216
|
+
};
|
|
217
|
+
const finishLine = (endOffset) => {
|
|
218
|
+
lineNumber += 1;
|
|
219
|
+
const recognizedMarker = onLine(Buffer.concat(lineParts, lineBytes).toString("utf8"), lineNumber, skipLongLine);
|
|
220
|
+
if (skipLongLine && recognizedMarker !== true) {
|
|
221
|
+
process.stderr.write(`hq lanes: skipped oversized transcript line ${lineNumber} in ${transcriptPath} (over ${maxLineBytes} bytes)\n`);
|
|
222
|
+
}
|
|
223
|
+
lineParts = [];
|
|
224
|
+
lineBytes = 0;
|
|
225
|
+
skipLongLine = false;
|
|
226
|
+
nextOffset = endOffset;
|
|
227
|
+
};
|
|
228
|
+
while (position < fileSize) {
|
|
229
|
+
const chunk = Buffer.allocUnsafe(Math.min(TRANSCRIPT_READ_CHUNK_BYTES, fileSize - position));
|
|
230
|
+
const chunkStart = position;
|
|
89
231
|
const read = fs.readSync(fd, chunk, 0, chunk.length, position);
|
|
90
232
|
if (read === 0)
|
|
91
233
|
break;
|
|
@@ -94,28 +236,34 @@ export function probeProviderCompactionsSince(transcriptPath, provider, startOff
|
|
|
94
236
|
for (let index = 0; index < read; index += 1) {
|
|
95
237
|
if (chunk[index] !== 0x0a)
|
|
96
238
|
continue;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
lineParts.length = 0;
|
|
100
|
-
lineNumber += 1;
|
|
101
|
-
if (isProviderMarkerLine(line, lineNumber, transcriptPath, provider))
|
|
102
|
-
count += 1;
|
|
239
|
+
appendPart(chunk.subarray(lineStart, index));
|
|
240
|
+
finishLine(chunkStart + index + 1);
|
|
103
241
|
lineStart = index + 1;
|
|
104
|
-
nextOffset = position - (read - lineStart);
|
|
105
242
|
}
|
|
106
243
|
if (lineStart < read)
|
|
107
|
-
|
|
244
|
+
appendPart(chunk.subarray(lineStart, read));
|
|
108
245
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
replaced: false,
|
|
113
|
-
};
|
|
246
|
+
if (includeUnterminatedLast && (lineBytes > 0 || skipLongLine))
|
|
247
|
+
finishLine(fileSize);
|
|
248
|
+
return { file_size: fileSize, next_offset: nextOffset, replaced: false };
|
|
114
249
|
}
|
|
115
250
|
finally {
|
|
116
251
|
fs.closeSync(fd);
|
|
117
252
|
}
|
|
118
253
|
}
|
|
254
|
+
/** Read newly appended, newline-complete marker records after a saved byte offset. */
|
|
255
|
+
export function probeProviderCompactionsSince(transcriptPath, provider, startOffset, maxLineBytes = TRANSCRIPT_MAX_LINE_BYTES) {
|
|
256
|
+
if (provider === "grok")
|
|
257
|
+
return { count: 0, next_offset: startOffset, replaced: false };
|
|
258
|
+
let count = 0;
|
|
259
|
+
const scan = scanTranscriptLines(transcriptPath, startOffset, false, (line, lineNumber, oversized) => {
|
|
260
|
+
const marker = isProviderMarkerLine(line, lineNumber, transcriptPath, provider, oversized);
|
|
261
|
+
if (marker)
|
|
262
|
+
count += 1;
|
|
263
|
+
return marker;
|
|
264
|
+
}, maxLineBytes);
|
|
265
|
+
return { count, next_offset: scan.next_offset, replaced: scan.replaced };
|
|
266
|
+
}
|
|
119
267
|
export function providerTranscriptSize(transcriptPath) {
|
|
120
268
|
try {
|
|
121
269
|
return fs.statSync(transcriptPath).size;
|
|
@@ -166,39 +314,58 @@ export function findSessionTranscript(home, sessionId) {
|
|
|
166
314
|
return null;
|
|
167
315
|
}
|
|
168
316
|
/** Count compact boundaries in a transcript. Other lines are never parsed. */
|
|
169
|
-
export function probeCompactBoundaries(transcriptPath) {
|
|
317
|
+
export function probeCompactBoundaries(transcriptPath, maxLineBytes = TRANSCRIPT_MAX_LINE_BYTES) {
|
|
170
318
|
if (transcriptPath === null)
|
|
171
319
|
return { count: 0, last_pre_tokens: null };
|
|
172
|
-
let
|
|
320
|
+
let count = 0;
|
|
321
|
+
let last = null;
|
|
173
322
|
try {
|
|
174
|
-
|
|
323
|
+
scanTranscriptLines(transcriptPath, 0, true, (line, lineNumber, oversized) => {
|
|
324
|
+
if (oversized) {
|
|
325
|
+
const marker = topLevelString(line, "type") === "system" && topLevelString(line, "subtype") === "compact_boundary";
|
|
326
|
+
if (!marker)
|
|
327
|
+
return false;
|
|
328
|
+
count += 1;
|
|
329
|
+
const rawMetadata = topLevelJsonValue(line, "compactMetadata");
|
|
330
|
+
if (rawMetadata) {
|
|
331
|
+
try {
|
|
332
|
+
const meta = JSON.parse(rawMetadata);
|
|
333
|
+
const pre = meta.preTokens;
|
|
334
|
+
last = typeof pre === "number" && Number.isFinite(pre) ? pre : null;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
last = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
last = null;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (!line.includes('"compact_boundary"'))
|
|
346
|
+
return;
|
|
347
|
+
let rec;
|
|
348
|
+
try {
|
|
349
|
+
rec = JSON.parse(line);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
process.stderr.write(`hq lanes: skipped an unparseable line in ${transcriptPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (rec.type !== "system" || rec.subtype !== "compact_boundary")
|
|
356
|
+
return false;
|
|
357
|
+
count += 1;
|
|
358
|
+
const meta = rec.compactMetadata;
|
|
359
|
+
const pre = meta?.preTokens;
|
|
360
|
+
last = typeof pre === "number" && Number.isFinite(pre) ? pre : null;
|
|
361
|
+
return true;
|
|
362
|
+
}, maxLineBytes);
|
|
175
363
|
}
|
|
176
364
|
catch (err) {
|
|
177
|
-
|
|
178
|
-
if (code === "ENOENT")
|
|
365
|
+
if (err.code === "ENOENT")
|
|
179
366
|
return { count: 0, last_pre_tokens: null };
|
|
180
367
|
throw err;
|
|
181
368
|
}
|
|
182
|
-
let count = 0;
|
|
183
|
-
let last = null;
|
|
184
|
-
for (const line of text.split("\n")) {
|
|
185
|
-
if (!line.includes('"compact_boundary"'))
|
|
186
|
-
continue;
|
|
187
|
-
let rec;
|
|
188
|
-
try {
|
|
189
|
-
rec = JSON.parse(line);
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
process.stderr.write(`hq lanes: skipped an unparseable line in ${transcriptPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (rec.type !== "system" || rec.subtype !== "compact_boundary")
|
|
196
|
-
continue;
|
|
197
|
-
count += 1;
|
|
198
|
-
const meta = rec.compactMetadata;
|
|
199
|
-
const pre = meta?.preTokens;
|
|
200
|
-
last = typeof pre === "number" && Number.isFinite(pre) ? pre : null;
|
|
201
|
-
}
|
|
202
369
|
return { count, last_pre_tokens: last };
|
|
203
370
|
}
|
|
204
371
|
/** Synchronous `/compact`. stdout is discarded; stderr goes to the run dir. */
|
|
@@ -123,16 +123,39 @@ function refuseCredential(provider, file) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
function sameContent(a, b) {
|
|
126
|
+
const chunkBytes = 1024 * 1024;
|
|
127
|
+
let aFd;
|
|
128
|
+
let bFd;
|
|
126
129
|
try {
|
|
127
130
|
const sa = fs.statSync(a);
|
|
128
131
|
const sb = fs.statSync(b);
|
|
129
132
|
if (sa.size !== sb.size)
|
|
130
133
|
return false;
|
|
131
|
-
|
|
134
|
+
aFd = fs.openSync(a, "r");
|
|
135
|
+
bFd = fs.openSync(b, "r");
|
|
136
|
+
const aChunk = Buffer.allocUnsafe(chunkBytes);
|
|
137
|
+
const bChunk = Buffer.allocUnsafe(chunkBytes);
|
|
138
|
+
let position = 0;
|
|
139
|
+
while (position < sa.size) {
|
|
140
|
+
const requested = Math.min(chunkBytes, sa.size - position);
|
|
141
|
+
const aRead = fs.readSync(aFd, aChunk, 0, requested, position);
|
|
142
|
+
const bRead = fs.readSync(bFd, bChunk, 0, requested, position);
|
|
143
|
+
if (aRead !== requested || bRead !== requested || !aChunk.subarray(0, aRead).equals(bChunk.subarray(0, bRead))) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
position += requested;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
132
149
|
}
|
|
133
150
|
catch {
|
|
134
151
|
return false;
|
|
135
152
|
}
|
|
153
|
+
finally {
|
|
154
|
+
if (aFd !== undefined)
|
|
155
|
+
fs.closeSync(aFd);
|
|
156
|
+
if (bFd !== undefined)
|
|
157
|
+
fs.closeSync(bFd);
|
|
158
|
+
}
|
|
136
159
|
}
|
|
137
160
|
/**
|
|
138
161
|
* Copy one file via a temp name and rename, so a partial copy is never a
|
|
@@ -15,7 +15,10 @@ import type { LaneCompanyRef, LaneAccountSelection, LaneRecord, LivenessState, Q
|
|
|
15
15
|
import { type LaneMesh } from "./mesh-events.js";
|
|
16
16
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
17
17
|
export declare const QUESTION_AGE_MS: number;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Questions can advance through a session and its senior lane separately at
|
|
20
|
+
* each lane level, so allow both routing hops per lane before reaching user.
|
|
21
|
+
*/
|
|
19
22
|
export declare const MAX_AGED_ADVANCES: number;
|
|
20
23
|
/** Aligns with watcher silence: a host that has not reported for this long is dead. */
|
|
21
24
|
export declare const DEAD_HOST_MS: number;
|
|
@@ -41,8 +41,11 @@ import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from
|
|
|
41
41
|
import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
42
42
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
43
43
|
export const QUESTION_AGE_MS = 60 * 60 * 1000;
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Questions can advance through a session and its senior lane separately at
|
|
46
|
+
* each lane level, so allow both routing hops per lane before reaching user.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_AGED_ADVANCES = MAX_LANE_DEPTH * 2;
|
|
46
49
|
/** Aligns with watcher silence: a host that has not reported for this long is dead. */
|
|
47
50
|
export const DEAD_HOST_MS = SILENCE_MS;
|
|
48
51
|
const RECONCILE_LOCK_STALE_MS = 30_000;
|