@indigoai-us/hq-cli 5.345.19 → 5.345.21
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/assets/scaffold/core/scripts/hq-delegate-secrets.sh +1 -1
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +33 -23
- package/assets/scaffold/core/scripts/ontology-garden.mjs +86 -3
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +141 -4
- package/assets/scaffold/core/scripts/register-project.sh +82 -14
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +5 -5
- package/assets/scaffold/core/scripts/share-suggestion-state.sh +10 -2
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +36 -4
- package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
- package/dist/commands/core-ontology-native.js +110 -4
- package/dist/commands/core-register-project-native.js +36 -3
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +78 -4
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes.js +40 -26
- package/dist/lib/bot/continuity-download.js +4 -1
- package/dist/lib/core-utils/sentry-report.js +69 -13
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +6 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +28 -7
- package/dist/lib/doctor/json-output.d.ts +4 -0
- package/dist/lib/doctor/json-output.js +5 -2
- package/dist/lib/lanes/autopilot.js +5 -1
- package/dist/lib/lanes/grants.d.ts +3 -0
- package/dist/lib/lanes/grants.js +10 -3
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/utils/safe-caught-error.d.ts +3 -0
- package/dist/utils/safe-caught-error.js +16 -0
- package/package.json +1 -1
|
@@ -144,9 +144,41 @@ if [ "${HQ_WORK_MESH_RECONCILE_STUB:-}" = "1" ]; then
|
|
|
144
144
|
fi
|
|
145
145
|
|
|
146
146
|
HQ_BIN="$(command -v hq 2>/dev/null || true)"
|
|
147
|
-
if [ -
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if [ -z "$HQ_BIN" ]; then
|
|
148
|
+
printf '%s\n' 'Work Mesh reconcile skipped: hq CLI not found on PATH.' >&2
|
|
149
|
+
else
|
|
150
|
+
flag_reader="$ROOT/.claude/hooks/work-mesh-daemon-not-loaded-flag.cjs"
|
|
151
|
+
daemon_warning_enabled=false
|
|
152
|
+
if [[ -n "${HQ_FLAGS_API_URL:-}" \
|
|
153
|
+
&& "${HQ_COMPANY_UID:-}" =~ ^cmp_[A-Za-z0-9]{3,128}$ ]] \
|
|
154
|
+
&& command -v node >/dev/null 2>&1 && [[ -f "$flag_reader" ]]; then
|
|
155
|
+
daemon_warning_enabled="$(HQ_CLI_BIN="$HQ_BIN" node "$flag_reader")" || daemon_warning_enabled=false
|
|
156
|
+
fi
|
|
157
|
+
if [ "$daemon_warning_enabled" = "true" ]; then
|
|
158
|
+
# Status only reads the installed service and its live state. Keep this hook
|
|
159
|
+
# bounded, and fail quiet when the CLI or service manager cannot answer.
|
|
160
|
+
daemon_status_file="$(mktemp "${TMPDIR:-/tmp}/hq-work-mesh-daemon-status.XXXXXX" 2>/dev/null)" || daemon_status_file=""
|
|
161
|
+
if [ -n "$daemon_status_file" ]; then
|
|
162
|
+
HQ_NO_UPDATE_CHECK=1 "$HQ_BIN" mesh daemon status --json >"$daemon_status_file" 2>/dev/null &
|
|
163
|
+
daemon_status_pid=$!
|
|
164
|
+
( sleep 2; kill "$daemon_status_pid" 2>/dev/null || true ) &
|
|
165
|
+
daemon_status_watchdog=$!
|
|
166
|
+
daemon_status_rc=0
|
|
167
|
+
wait "$daemon_status_pid" || daemon_status_rc=$?
|
|
168
|
+
kill "$daemon_status_watchdog" 2>/dev/null || true
|
|
169
|
+
wait "$daemon_status_watchdog" 2>/dev/null || true
|
|
170
|
+
if [ "$daemon_status_rc" -eq 0 ] && command -v jq >/dev/null 2>&1 && \
|
|
171
|
+
jq -e '(.ok == true) and (.running == false) and (.message | type == "string" and contains("installed ("))' \
|
|
172
|
+
"$daemon_status_file" >/dev/null 2>&1; then
|
|
173
|
+
printf '%s\n' 'Work Mesh daemon is installed but not loaded; start it with: hq mesh daemon install' >&2
|
|
174
|
+
fi
|
|
175
|
+
rm -f "$daemon_status_file" 2>/dev/null || true
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
if [ -f "$OBS_FILE" ]; then
|
|
179
|
+
nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
|
|
180
|
+
>/dev/null 2>&1 </dev/null &
|
|
181
|
+
disown 2>/dev/null || true
|
|
182
|
+
fi
|
|
151
183
|
fi
|
|
152
184
|
exit 0
|
|
@@ -249,7 +249,7 @@ for wt in "${CANDIDATES[@]:-}"; do
|
|
|
249
249
|
created_epoch="$wt_mtime"
|
|
250
250
|
stamp_file="$GC_META_DIR/$(path_key "$wt").json"
|
|
251
251
|
if [[ -f "$stamp_file" ]]; then
|
|
252
|
-
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null || echo "")
|
|
252
|
+
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null | tr -d '\r' || echo "")
|
|
253
253
|
[[ -n "$stamped" ]] && created_epoch="$stamped"
|
|
254
254
|
fi
|
|
255
255
|
age_days=$(( (NOW - created_epoch) / 86400 ))
|
|
@@ -799,15 +799,119 @@ export function worklistCommand(args, root) {
|
|
|
799
799
|
if (validation.code !== 0)
|
|
800
800
|
return err(2, `${validation.stderr}${validation.stdout}${validation.stderr}`);
|
|
801
801
|
const ledger = path.join(directory, ".processed");
|
|
802
|
+
const skipLedger = path.join(directory, ".skipped");
|
|
803
|
+
const parserVersion = 2;
|
|
802
804
|
if (doneItem) {
|
|
803
|
-
fs.appendFileSync(ledger, `${path.basename(doneItem)}\n`);
|
|
805
|
+
fs.appendFileSync(ledger, `${path.basename(doneItem)}\t${parserVersion}\n`);
|
|
804
806
|
return ok();
|
|
805
807
|
}
|
|
808
|
+
const hasFrontmatterField = (file, field) => {
|
|
809
|
+
if (!fs.existsSync(file))
|
|
810
|
+
return false;
|
|
811
|
+
let frontmatter = 0;
|
|
812
|
+
for (const rawLine of fs.readFileSync(file, "utf8").split("\n")) {
|
|
813
|
+
const line = rawLine.replace(/\r$/, "");
|
|
814
|
+
if (line === "---") {
|
|
815
|
+
frontmatter += 1;
|
|
816
|
+
if (frontmatter === 2)
|
|
817
|
+
return false;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (frontmatter === 1 && line.startsWith(`${field}:`))
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
};
|
|
825
|
+
const rewriteIfChanged = (file, original, output) => {
|
|
826
|
+
if (output === original)
|
|
827
|
+
return;
|
|
828
|
+
const temporary = `${file}.native-${process.pid}`;
|
|
829
|
+
fs.writeFileSync(temporary, output);
|
|
830
|
+
fs.renameSync(temporary, file);
|
|
831
|
+
};
|
|
832
|
+
// Match the shell's parser-version migration. Unstamped legacy rows are
|
|
833
|
+
// stamped unless they carry the old resolved_participants bug signature;
|
|
834
|
+
// older parser versions are dropped so those sources are re-evaluated.
|
|
835
|
+
let processed = "";
|
|
836
|
+
if (fs.existsSync(ledger)) {
|
|
837
|
+
const original = fs.readFileSync(ledger, "utf8");
|
|
838
|
+
const kept = [];
|
|
839
|
+
let changed = false;
|
|
840
|
+
const rows = original.split("\n");
|
|
841
|
+
if (rows.at(-1) === "")
|
|
842
|
+
rows.pop();
|
|
843
|
+
for (const rawLine of rows) {
|
|
844
|
+
const fields = rawLine.split("\t");
|
|
845
|
+
const basename = (fields[0] ?? "").replace(/\r$/, "");
|
|
846
|
+
const version = (fields[1] ?? "").replace(/\r$/, "");
|
|
847
|
+
if (!basename)
|
|
848
|
+
continue;
|
|
849
|
+
if (version) {
|
|
850
|
+
if (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion) {
|
|
851
|
+
changed = true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
kept.push(`${basename}\t${version}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
changed = true;
|
|
858
|
+
if (hasFrontmatterField(path.join(directory, basename), "resolved_participants"))
|
|
859
|
+
continue;
|
|
860
|
+
kept.push(`${basename}\t${parserVersion}`);
|
|
861
|
+
}
|
|
862
|
+
processed = kept.join("\n");
|
|
863
|
+
if (kept.length > 0)
|
|
864
|
+
processed += "\n";
|
|
865
|
+
if (changed)
|
|
866
|
+
rewriteIfChanged(ledger, original, processed);
|
|
867
|
+
}
|
|
868
|
+
let skipped = "";
|
|
869
|
+
if (fs.existsSync(skipLedger)) {
|
|
870
|
+
const original = fs.readFileSync(skipLedger, "utf8");
|
|
871
|
+
const kept = [];
|
|
872
|
+
let changed = false;
|
|
873
|
+
const rows = original.split("\n");
|
|
874
|
+
if (rows.at(-1) === "")
|
|
875
|
+
rows.pop();
|
|
876
|
+
for (const rawLine of rows) {
|
|
877
|
+
const fields = rawLine.split("\t");
|
|
878
|
+
const basename = fields[0] ?? "";
|
|
879
|
+
const reason = fields[1] ?? "";
|
|
880
|
+
const version = fields[2] ?? "";
|
|
881
|
+
if (!basename)
|
|
882
|
+
continue;
|
|
883
|
+
if (!version || (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion)) {
|
|
884
|
+
changed = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
kept.push(`${basename}\t${reason}\t${version}`);
|
|
888
|
+
}
|
|
889
|
+
skipped = kept.join("\n");
|
|
890
|
+
if (kept.length > 0)
|
|
891
|
+
skipped += "\n";
|
|
892
|
+
if (changed)
|
|
893
|
+
rewriteIfChanged(skipLedger, original, skipped);
|
|
894
|
+
}
|
|
895
|
+
const isProcessed = (basename) => processed.split("\n").some((line) => {
|
|
896
|
+
if (!line)
|
|
897
|
+
return false;
|
|
898
|
+
const [name, version] = line.split("\t");
|
|
899
|
+
if (name !== basename)
|
|
900
|
+
return false;
|
|
901
|
+
if (version === undefined || version === "")
|
|
902
|
+
return true;
|
|
903
|
+
return Number(version) >= parserVersion;
|
|
904
|
+
});
|
|
905
|
+
const isSkippedAtCurrentVersion = (basename) => skipped.split("\n").some((line) => {
|
|
906
|
+
if (!line)
|
|
907
|
+
return false;
|
|
908
|
+
const fields = line.split("\t");
|
|
909
|
+
return fields[0] === basename && fields[2] === String(parserVersion);
|
|
910
|
+
});
|
|
806
911
|
const run = awkField(sourceFile, "run", false);
|
|
807
912
|
const rule = awkField(sourceFile, "audience_rule", false);
|
|
808
913
|
if (run === "cloud" && !force)
|
|
809
914
|
return err(4, "assigned to cloud agent (run: cloud); pass --force to run locally\n");
|
|
810
|
-
const processed = fs.existsSync(ledger) ? fs.readFileSync(ledger, "utf8").split("\n") : [];
|
|
811
915
|
const entries = fs.existsSync(directory)
|
|
812
916
|
? fs.readdirSync(directory).filter((name) => name.endsWith(".md") && !name.startsWith("."))
|
|
813
917
|
.filter((name) => { try {
|
|
@@ -821,12 +925,12 @@ export function worklistCommand(args, root) {
|
|
|
821
925
|
const output = [];
|
|
822
926
|
let count = 0;
|
|
823
927
|
for (const name of entries) {
|
|
824
|
-
if (
|
|
928
|
+
if (isProcessed(name) || isSkippedAtCurrentVersion(name))
|
|
825
929
|
continue;
|
|
826
930
|
if (/^[+-]?\d+$/.test(limit) && count >= Number(limit))
|
|
827
931
|
break;
|
|
828
932
|
if (!/^[+-]?\d+$/.test(limit)) {
|
|
829
|
-
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line
|
|
933
|
+
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line 186: [: ${limit}: integer expression expected\n`);
|
|
830
934
|
}
|
|
831
935
|
count += 1;
|
|
832
936
|
const file = path.join(directory, name);
|
|
@@ -860,6 +964,8 @@ export function worklistCommand(args, root) {
|
|
|
860
964
|
}
|
|
861
965
|
}
|
|
862
966
|
if (!audience) {
|
|
967
|
+
fs.appendFileSync(skipLedger, `${name}\tno-audience\t${parserVersion}\n`);
|
|
968
|
+
skipped += `${name}\tno-audience\t${parserVersion}\n`;
|
|
863
969
|
output.push(`${JSON.stringify({ file: relative, skip: "no-audience" })}\n`);
|
|
864
970
|
continue;
|
|
865
971
|
}
|
|
@@ -51,11 +51,44 @@ function companyUid(company, deps) {
|
|
|
51
51
|
return direct;
|
|
52
52
|
const file = path.join(deps.root, "companies", company, ".company-uid");
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
const fromFile = deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
|
|
55
|
+
if (fromFile)
|
|
56
|
+
return fromFile;
|
|
57
|
+
}
|
|
58
|
+
catch { /* shell falls through to companies/manifest.yaml */ }
|
|
59
|
+
const manifest = path.join(deps.root, "companies", "manifest.yaml");
|
|
60
|
+
let contents;
|
|
61
|
+
try {
|
|
62
|
+
contents = deps.fs.readFileSync(manifest, "utf8");
|
|
55
63
|
}
|
|
56
64
|
catch {
|
|
57
65
|
return null;
|
|
58
66
|
}
|
|
67
|
+
let inCompanies = false;
|
|
68
|
+
let inCompany = false;
|
|
69
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
70
|
+
if (/^companies:[\t ]*$/u.test(line)) {
|
|
71
|
+
inCompanies = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inCompanies && /^[^\t ]/u.test(line)) {
|
|
75
|
+
inCompanies = false;
|
|
76
|
+
inCompany = false;
|
|
77
|
+
}
|
|
78
|
+
if (inCompanies && /^ {2}[^ #][^:]*:/u.test(line)) {
|
|
79
|
+
const slug = line.trim().split(/[\t ]+/u, 1)[0]?.replace(/:$/u, "");
|
|
80
|
+
inCompany = slug === company;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inCompany && /^ {2}[^\t ]/u.test(line))
|
|
84
|
+
break;
|
|
85
|
+
if (inCompany) {
|
|
86
|
+
const fields = line.trim().split(/[\t ]+/u);
|
|
87
|
+
if (fields[0] === "cloud_uid:" && fields[1])
|
|
88
|
+
return fields[1].replace(/["']/gu, "").replace(/[\t ]+$/u, "");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
59
92
|
}
|
|
60
93
|
function boardPath(company, deps) { return path.join(deps.root, "companies", company, "board.json"); }
|
|
61
94
|
function runHelpProbe(deps) {
|
|
@@ -217,7 +250,7 @@ function audit(company, deps) {
|
|
|
217
250
|
return false;
|
|
218
251
|
const uid = companyUid(company, deps);
|
|
219
252
|
if (uid === null) {
|
|
220
|
-
stderr += `no company uid for ${company} (set HQ_COMPANY_UID
|
|
253
|
+
stderr += `no company uid for ${company} (set HQ_COMPANY_UID, companies/${company}/.company-uid, or manifest cloud_uid)\n`;
|
|
221
254
|
return true;
|
|
222
255
|
}
|
|
223
256
|
return !deps.fs.existsSync(path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/cache/projects", uid, `${project}.json`));
|
|
@@ -243,7 +276,7 @@ function storyNotDone(prd, deps, diagnostics) {
|
|
|
243
276
|
diagnostics.push(jq.stderr);
|
|
244
277
|
else if (jq.error?.code === "ENOENT") {
|
|
245
278
|
const script = resolveBundledAsset(REGISTER_PROJECT_ASSET);
|
|
246
|
-
diagnostics.push(`${script}: line
|
|
279
|
+
diagnostics.push(`${script}: line 65: jq: command not found\n`);
|
|
247
280
|
}
|
|
248
281
|
return false;
|
|
249
282
|
}
|
|
@@ -57,6 +57,8 @@ export interface RunDoctorOptions {
|
|
|
57
57
|
json?: boolean;
|
|
58
58
|
/** Resolve hq-flags independently for each company represented in sync results. */
|
|
59
59
|
resolveClientHealthReasonCodesForCompany?: (companySlug: string | null) => Promise<boolean>;
|
|
60
|
+
/** Resolve the separate status-report flag per company in sync results. */
|
|
61
|
+
resolveClientHealthCheckStatusForCompany?: (companySlug: string | null) => Promise<boolean>;
|
|
60
62
|
/** In text mode, also print every PASS result. Default: false. */
|
|
61
63
|
verbose?: boolean;
|
|
62
64
|
/** In text mode, emit ANSI colour. Default: false (no escape sequences). */
|
|
@@ -117,6 +119,7 @@ export interface RunDoctorResult {
|
|
|
117
119
|
* read; all other default tiers remain local unless explicitly opted in.
|
|
118
120
|
*/
|
|
119
121
|
export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
|
|
122
|
+
export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
120
123
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
121
124
|
export declare function registerDoctorCommand(program: Command): void;
|
|
122
125
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -32,7 +32,7 @@ import { DEEP_FAMILY_ID, DEEP_FAMILY_TITLE, runDeepGuardTests, } from "../lib/do
|
|
|
32
32
|
import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/doctor/deep/parity.js";
|
|
33
33
|
import { applyFixes } from "../lib/doctor/fix/apply.js";
|
|
34
34
|
import { renderDoctorFixJson } from "../lib/doctor/fix/json-output.js";
|
|
35
|
-
import { clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
|
|
35
|
+
import { clientHealthCheckStatusEnabled as isActiveCompanyClientHealthCheckStatusEnabled, clientHealthCheckStatusEnabledForCompany, clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
|
|
36
36
|
import { isCompanyJournalSlug } from "../lib/doctor/checks/sync-health.js";
|
|
37
37
|
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
38
38
|
import * as readline from "node:readline";
|
|
@@ -166,15 +166,21 @@ export async function runDoctor(options = {}) {
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
if (options.json) {
|
|
169
|
-
const clientHealthReasonCodesEnabledCheckIds =
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
const [clientHealthReasonCodesEnabledCheckIds, clientHealthCheckStatusEnabledCheckIds] = await Promise.all([
|
|
170
|
+
options.resolveClientHealthReasonCodesForCompany
|
|
171
|
+
? resolveClientHealthReasonCodesCheckIds(families, options.resolveClientHealthReasonCodesForCompany)
|
|
172
|
+
: undefined,
|
|
173
|
+
options.resolveClientHealthCheckStatusForCompany
|
|
174
|
+
? resolveClientHealthCheckStatusCheckIds(families, options.resolveClientHealthCheckStatusForCompany)
|
|
175
|
+
: undefined,
|
|
176
|
+
]);
|
|
172
177
|
write(renderJson(buildDoctorJson({
|
|
173
178
|
hqRoot: hqRoot ?? "",
|
|
174
179
|
families,
|
|
175
180
|
platform,
|
|
176
181
|
scope,
|
|
177
182
|
clientHealthReasonCodesEnabledCheckIds,
|
|
183
|
+
clientHealthCheckStatusEnabledCheckIds,
|
|
178
184
|
})));
|
|
179
185
|
}
|
|
180
186
|
else {
|
|
@@ -220,6 +226,34 @@ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompan
|
|
|
220
226
|
}
|
|
221
227
|
return enabledChecks;
|
|
222
228
|
}
|
|
229
|
+
async function resolveClientHealthCheckStatusCheckIds(families, resolveForCompany) {
|
|
230
|
+
const checksByCompany = new Map();
|
|
231
|
+
for (const run of families) {
|
|
232
|
+
if (run.family.id !== "sync")
|
|
233
|
+
continue;
|
|
234
|
+
for (const result of run.results) {
|
|
235
|
+
if (result.status !== "FAIL" && result.status !== "WARN")
|
|
236
|
+
continue;
|
|
237
|
+
const scoped = /^sync\.(?:journal|manifest)\.(.+)$/.exec(result.checkId);
|
|
238
|
+
const companySlug = scoped && isCompanyJournalSlug(scoped[1]) ? scoped[1] : null;
|
|
239
|
+
const checkIds = checksByCompany.get(companySlug) ?? [];
|
|
240
|
+
checkIds.push(result.checkId);
|
|
241
|
+
checksByCompany.set(companySlug, checkIds);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const enabledChecks = new Set();
|
|
245
|
+
const decisions = await Promise.all([...checksByCompany.entries()].map(async ([companySlug, checkIds]) => ({
|
|
246
|
+
checkIds,
|
|
247
|
+
enabled: await resolveForCompany(companySlug),
|
|
248
|
+
})));
|
|
249
|
+
for (const decision of decisions) {
|
|
250
|
+
if (decision.enabled) {
|
|
251
|
+
for (const checkId of decision.checkIds)
|
|
252
|
+
enabledChecks.add(checkId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return enabledChecks;
|
|
256
|
+
}
|
|
223
257
|
function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
224
258
|
const companies = readCompaniesManifestMap(hqRoot);
|
|
225
259
|
return async (companySlug) => {
|
|
@@ -245,6 +279,37 @@ function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
|
245
279
|
});
|
|
246
280
|
};
|
|
247
281
|
}
|
|
282
|
+
export function createClientHealthCheckStatusCompanyResolver(hqRoot) {
|
|
283
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
284
|
+
return async (companySlug) => {
|
|
285
|
+
if (companySlug === null) {
|
|
286
|
+
return isActiveCompanyClientHealthCheckStatusEnabled();
|
|
287
|
+
}
|
|
288
|
+
const scopedCompanyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(companySlug);
|
|
289
|
+
if (scopedCompanyUid) {
|
|
290
|
+
return clientHealthCheckStatusEnabledForCompany({
|
|
291
|
+
companyUid: scopedCompanyUid[1],
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const manifestUid = companies?.[companySlug]?.cloud_uid;
|
|
298
|
+
const activeSlug = process.env.HQ_COMPANY_SLUG?.trim() || "";
|
|
299
|
+
const activeUid = process.env.HQ_COMPANY_UID?.trim() || "";
|
|
300
|
+
const companyUid = typeof manifestUid === "string" && manifestUid.trim()
|
|
301
|
+
? manifestUid.trim()
|
|
302
|
+
: companySlug === activeSlug
|
|
303
|
+
? activeUid
|
|
304
|
+
: "";
|
|
305
|
+
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid))
|
|
306
|
+
return false;
|
|
307
|
+
return clientHealthCheckStatusEnabledForCompany({
|
|
308
|
+
companyUid,
|
|
309
|
+
companySlug,
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
}
|
|
248
313
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
249
314
|
export function registerDoctorCommand(program) {
|
|
250
315
|
program
|
|
@@ -334,6 +399,14 @@ export function registerDoctorCommand(program) {
|
|
|
334
399
|
: undefined;
|
|
335
400
|
})()
|
|
336
401
|
: undefined;
|
|
402
|
+
const clientHealthCheckStatusResolver = json && opts.clientHealthReport === true
|
|
403
|
+
? (() => {
|
|
404
|
+
const hqRoot = resolveHqRoot();
|
|
405
|
+
return hqRoot
|
|
406
|
+
? createClientHealthCheckStatusCompanyResolver(hqRoot)
|
|
407
|
+
: undefined;
|
|
408
|
+
})()
|
|
409
|
+
: undefined;
|
|
337
410
|
const result = await runDoctor({
|
|
338
411
|
json,
|
|
339
412
|
// `src/main.ts` also declares --verbose on the root program. When
|
|
@@ -350,6 +423,7 @@ export function registerDoctorCommand(program) {
|
|
|
350
423
|
company: opts.company,
|
|
351
424
|
only: parseOnly(opts.only),
|
|
352
425
|
resolveClientHealthReasonCodesForCompany: clientHealthReasonCodesResolver,
|
|
426
|
+
resolveClientHealthCheckStatusForCompany: clientHealthCheckStatusResolver,
|
|
353
427
|
});
|
|
354
428
|
// Set the exit code rather than calling process.exit, so the CLI's
|
|
355
429
|
// normal shutdown (telemetry flush) still runs. Non-zero means either an
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import chalk from "chalk";
|
|
20
20
|
import open from "open";
|
|
21
21
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
22
|
+
import { safeCaughtError } from "../utils/safe-caught-error.js";
|
|
22
23
|
import { runResidentCommandWithPackageLease, } from "../utils/resident-package-preload.js";
|
|
23
24
|
import { consoleOrigin } from "../utils/console-origin.js";
|
|
24
25
|
export { consoleOrigin };
|
|
@@ -29,6 +30,9 @@ import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBluep
|
|
|
29
30
|
import { OAUTH_LOOPBACK_TIMEOUT_CODE, startLoopbackListener, } from "./integrations-oauth.js";
|
|
30
31
|
/** hq-pro's machine code for "this endpoint needs a browser sign-in". */
|
|
31
32
|
const OAUTH_REQUIRED_CODE = "INTEGRATION_FACTORY_OAUTH_REQUIRED";
|
|
33
|
+
function warnBrowserOpenFailure(error) {
|
|
34
|
+
console.warn("[hq-cli:integrations-connect] browser open failed", safeCaughtError(error));
|
|
35
|
+
}
|
|
32
36
|
/**
|
|
33
37
|
* A server-authoritative "this endpoint is OAuth-protected, run the browser
|
|
34
38
|
* sign-in instead" signal, in EITHER of the two shapes hq-pro emits it:
|
|
@@ -679,7 +683,7 @@ async function consoleHandoff(target, opts) {
|
|
|
679
683
|
console.error(chalk.yellow(`Note: --scopes cannot be carried into a console sign-in, so this connect will use ${target.label}'s default permissions.`));
|
|
680
684
|
}
|
|
681
685
|
if (url && opts.browser !== false)
|
|
682
|
-
await open(url).catch(() =>
|
|
686
|
+
await open(url).catch((error) => warnBrowserOpenFailure(error));
|
|
683
687
|
console.error(chalk.dim("When the browser says it connected, run `hq integrations list` to confirm."));
|
|
684
688
|
return null;
|
|
685
689
|
}
|
|
@@ -712,7 +716,7 @@ export async function bringYourOwnClientHandoff(target, opts, reason, redirectUr
|
|
|
712
716
|
console.error("The console shows the callback URL to register and takes the credentials.");
|
|
713
717
|
printConsoleDestination(target, opts, url);
|
|
714
718
|
if (url && opts.browser !== false)
|
|
715
|
-
await open(url).catch(() =>
|
|
719
|
+
await open(url).catch((error) => warnBrowserOpenFailure(error));
|
|
716
720
|
return null;
|
|
717
721
|
}
|
|
718
722
|
/**
|
|
@@ -837,7 +841,7 @@ async function userClientSignIn(token, companyUid, target, opts, client, request
|
|
|
837
841
|
if (googleHint)
|
|
838
842
|
console.error(chalk.yellow(googleHint));
|
|
839
843
|
if (opts.browser !== false)
|
|
840
|
-
await open(started.authorizationUrl).catch(() =>
|
|
844
|
+
await open(started.authorizationUrl).catch((error) => warnBrowserOpenFailure(error));
|
|
841
845
|
console.error(chalk.dim("The browser finishes on the HQ console. When it says connected, " +
|
|
842
846
|
"run `hq integrations list` to confirm."));
|
|
843
847
|
return null;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -62,7 +62,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
62
62
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
63
63
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
64
64
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
65
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
65
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, aliveDirectChildren, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
66
66
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
67
67
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
68
68
|
import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
@@ -507,7 +507,12 @@ function laneHireFields(lane, lanes, aliveChildrenByParent, finishedChildrenDoNo
|
|
|
507
507
|
};
|
|
508
508
|
}
|
|
509
509
|
function hireGrantFinishedChildrenDoNotCount(ctx, lane) {
|
|
510
|
-
const resolution = resolveLaneCompany(ctx.hqRoot(),
|
|
510
|
+
const resolution = resolveLaneCompany(ctx.hqRoot(), {
|
|
511
|
+
lane_id: lane.lane_id,
|
|
512
|
+
company: lane.company ?? null,
|
|
513
|
+
mesh_company: lane.mesh_company,
|
|
514
|
+
project_id: lane.project_id ?? "",
|
|
515
|
+
});
|
|
511
516
|
const company = resolution.company ?? undefined;
|
|
512
517
|
return readLanesRegistryFlagFailOff(LANE_HIRE_GRANT_FINISHED_LANES_FLAG, {
|
|
513
518
|
company,
|
|
@@ -1036,9 +1041,15 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
1036
1041
|
!sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
|
|
1037
1042
|
throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its Outpost dispatch`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
|
|
1038
1043
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
:
|
|
1044
|
+
let finishedChildrenDoNotCount = false;
|
|
1045
|
+
if (isTerminalLaneState(lane.state) || isFinishedResumableHireChild(lane.state)) {
|
|
1046
|
+
const seniorLaneId = lane.senior.kind === "lane" ? lane.senior.id : undefined;
|
|
1047
|
+
const resumeSenior = seniorLaneId
|
|
1048
|
+
? grantContext?.hierarchy.find((entry) => entry.lane_id === seniorLaneId) ??
|
|
1049
|
+
(listLaneIds(hqRoot).includes(seniorLaneId) ? readLane(hqRoot, seniorLaneId) : lane)
|
|
1050
|
+
: lane;
|
|
1051
|
+
finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, resumeSenior);
|
|
1052
|
+
}
|
|
1042
1053
|
const shouldCheckGrant = isTerminalLaneState(lane.state) ||
|
|
1043
1054
|
(finishedChildrenDoNotCount && isFinishedResumableHireChild(lane.state));
|
|
1044
1055
|
if (!shouldCheckGrant ||
|
|
@@ -5736,6 +5747,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5736
5747
|
initial.senior.kind === "lane" && initial.senior.id
|
|
5737
5748
|
? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
|
|
5738
5749
|
: undefined;
|
|
5750
|
+
const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, initial);
|
|
5739
5751
|
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
5740
5752
|
const current = readLane(hqRoot, laneId);
|
|
5741
5753
|
const currentCallerAuthorization = authorization === "senior"
|
|
@@ -5779,11 +5791,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5779
5791
|
if (isTerminalLaneState(parent.state)) {
|
|
5780
5792
|
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot change grants`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
5781
5793
|
}
|
|
5782
|
-
const siblings =
|
|
5794
|
+
const siblings = childrenHoldingHireGrantCarve(hierarchy, parent.lane_id, finishedChildrenDoNotCount);
|
|
5783
5795
|
const parentGrant = parent.hire_grant ?? 0;
|
|
5784
5796
|
const assigned = assignedChildHireGrants(siblings, current.lane_id);
|
|
5785
5797
|
const available = Math.max(0, parentGrant - assigned);
|
|
5786
|
-
if (hireGrant > available) {
|
|
5798
|
+
if (hireGrant > (current.hire_grant ?? 0) && hireGrant > available) {
|
|
5787
5799
|
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
|
|
5788
5800
|
senior_lane_id: parent.lane_id,
|
|
5789
5801
|
parent_grant: parentGrant,
|
|
@@ -5794,7 +5806,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5794
5806
|
});
|
|
5795
5807
|
}
|
|
5796
5808
|
}
|
|
5797
|
-
const children =
|
|
5809
|
+
const children = childrenHoldingHireGrantCarve(hierarchy, current.lane_id, finishedChildrenDoNotCount);
|
|
5798
5810
|
const allocated = assignedChildHireGrants(children);
|
|
5799
5811
|
const minimumGrant = Math.max(allocated, children.length);
|
|
5800
5812
|
if (hireGrant < minimumGrant) {
|
|
@@ -7063,6 +7075,7 @@ async function reparentOnOwner(ctx, laneId, opts) {
|
|
|
7063
7075
|
const nextSenior = parseSeniorRef(opts.senior);
|
|
7064
7076
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
7065
7077
|
const initial = readLane(hqRoot, laneId);
|
|
7078
|
+
const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, initial);
|
|
7066
7079
|
const refreshParents = new Set();
|
|
7067
7080
|
if (initial.senior.kind === "lane" && initial.senior.id)
|
|
7068
7081
|
refreshParents.add(initial.senior.id);
|
|
@@ -7074,7 +7087,7 @@ async function reparentOnOwner(ctx, laneId, opts) {
|
|
|
7074
7087
|
...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
|
|
7075
7088
|
...(nextSenior.kind === "lane" && nextSenior.id ? [nextSenior.id] : []),
|
|
7076
7089
|
];
|
|
7077
|
-
return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
|
|
7090
|
+
return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount));
|
|
7078
7091
|
}
|
|
7079
7092
|
async function runReparentOwnerTarget(ctx, laneId, opts) {
|
|
7080
7093
|
if (!ctx.isSsmOwnerDispatch()) {
|
|
@@ -7094,7 +7107,7 @@ async function runReparentOwnerTarget(ctx, laneId, opts) {
|
|
|
7094
7107
|
changed_descendant_ids: result.changedDescendantIds,
|
|
7095
7108
|
});
|
|
7096
7109
|
}
|
|
7097
|
-
function runReparentLocked(ctx, laneId, opts) {
|
|
7110
|
+
function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
|
|
7098
7111
|
const hqRoot = ctx.hqRoot();
|
|
7099
7112
|
if (!opts.senior) {
|
|
7100
7113
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
@@ -7108,26 +7121,27 @@ function runReparentLocked(ctx, laneId, opts) {
|
|
|
7108
7121
|
}
|
|
7109
7122
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
7110
7123
|
assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
|
|
7111
|
-
if (nextSenior.kind === "lane" &&
|
|
7112
|
-
seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
7124
|
+
if (nextSenior.kind === "lane" && seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
7113
7125
|
const parent = readLane(hqRoot, nextSenior.id);
|
|
7114
7126
|
if (isTerminalLaneState(parent.state)) {
|
|
7115
7127
|
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot accept lane children`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
7116
7128
|
}
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7129
|
+
if (laneHoldsHireGrantCarve(lane, finishedChildrenDoNotCount)) {
|
|
7130
|
+
const children = childrenHoldingHireGrantCarve(loadAllLanes(hqRoot), parent.lane_id, finishedChildrenDoNotCount);
|
|
7131
|
+
if (children.length >= (parent.hire_grant ?? 0)) {
|
|
7132
|
+
throw hireGrantCapacityError(parent, children);
|
|
7133
|
+
}
|
|
7134
|
+
const available = Math.max(0, (parent.hire_grant ?? 0) - assignedChildHireGrants(children));
|
|
7135
|
+
const requested = lane.hire_grant ?? 0;
|
|
7136
|
+
if (requested > available) {
|
|
7137
|
+
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
|
|
7138
|
+
senior_lane_id: parent.lane_id,
|
|
7139
|
+
parent_grant: parent.hire_grant ?? 0,
|
|
7140
|
+
assigned_child_grants: assignedChildHireGrants(children),
|
|
7141
|
+
requested_hire_grant: requested,
|
|
7142
|
+
available_hire_grant: available,
|
|
7143
|
+
});
|
|
7144
|
+
}
|
|
7131
7145
|
}
|
|
7132
7146
|
}
|
|
7133
7147
|
if (walkHitsLane(hqRoot, nextSenior, laneId, ctx)) {
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
5
5
|
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
|
6
6
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
7
|
+
import { safeCaughtError } from "../../utils/safe-caught-error.js";
|
|
7
8
|
async function readOwnVault(input) {
|
|
8
9
|
const client = new S3Client({ region: "us-east-1", credentials: input.credentials });
|
|
9
10
|
try {
|
|
@@ -52,7 +53,9 @@ export async function downloadContinuitySnapshot(input, deps = { credential: ens
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
finally {
|
|
55
|
-
await reader.cancel().catch(() => {
|
|
56
|
+
await reader.cancel().catch((error) => {
|
|
57
|
+
console.warn("[hq-cli:bot/continuity-download] reader.cancel failed", safeCaughtError(error));
|
|
58
|
+
});
|
|
56
59
|
download.close();
|
|
57
60
|
}
|
|
58
61
|
const bytes = Buffer.concat(chunks);
|