@indigoai-us/hq-cli 5.345.101 → 5.345.103
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 +10 -0
- package/dist/commands/doctor.js +3 -12
- package/dist/lib/doctor/checks/sync-health.d.ts +17 -0
- package/dist/lib/doctor/checks/sync-health.js +53 -2
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/throttle.d.ts +21 -3
- package/dist/lib/lanes/throttle.js +68 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.103] — 2026-10-12
|
|
6
|
+
- On Macs, idle `hq lanes` work no longer sits queued on the memory throttle
|
|
7
|
+
just because reclaimable cache made free RAM look too low.
|
|
8
|
+
|
|
9
|
+
## [5.345.102] — 2026-10-12
|
|
10
|
+
- Session-start health no longer files a leftover company journal as an
|
|
11
|
+
unrepaired doctor finding, and company manifest rows keep a real check id
|
|
12
|
+
instead of showing up as unlabeled `unknown`. `hq doctor --fix` still
|
|
13
|
+
refreshes that company journal when hook files have uncommitted changes.
|
|
14
|
+
|
|
5
15
|
## [5.345.101] — 2026-10-12
|
|
6
16
|
- Doctor JSON no longer counts an ordinary HQ Core upgrade warning in its
|
|
7
17
|
summary after omitting it from results. Session-start health that only saw
|
package/dist/commands/doctor.js
CHANGED
|
@@ -33,7 +33,7 @@ import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/
|
|
|
33
33
|
import { applyFixes } from "../lib/doctor/fix/apply.js";
|
|
34
34
|
import { renderDoctorFixJson } from "../lib/doctor/fix/json-output.js";
|
|
35
35
|
import { clientHealthCheckStatusEnabled as isActiveCompanyClientHealthCheckStatusEnabled, clientHealthCheckStatusEnabledForCompany, clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
|
|
36
|
-
import {
|
|
36
|
+
import { clientHealthCompanyScopeFromCheckId } 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";
|
|
39
39
|
import { runSideEffectTests } from "../lib/doctor/deep/sandbox.js";
|
|
@@ -207,15 +207,7 @@ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompan
|
|
|
207
207
|
(result.status !== "FAIL" && result.status !== "WARN")) {
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
|
-
const
|
|
211
|
-
const companyUidScope = scoped
|
|
212
|
-
? /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(scoped[1])
|
|
213
|
-
: null;
|
|
214
|
-
const companySlug = companyUidScope
|
|
215
|
-
? `company:${companyUidScope[1]}`
|
|
216
|
-
: scoped && isCompanyJournalSlug(scoped[1])
|
|
217
|
-
? scoped[1]
|
|
218
|
-
: null;
|
|
210
|
+
const companySlug = clientHealthCompanyScopeFromCheckId(result.checkId);
|
|
219
211
|
const checkIds = checksByCompany.get(companySlug) ?? [];
|
|
220
212
|
checkIds.push(result.checkId);
|
|
221
213
|
checksByCompany.set(companySlug, checkIds);
|
|
@@ -242,8 +234,7 @@ async function resolveClientHealthCheckStatusCheckIds(families, resolveForCompan
|
|
|
242
234
|
for (const result of run.results) {
|
|
243
235
|
if (result.status !== "FAIL" && result.status !== "WARN")
|
|
244
236
|
continue;
|
|
245
|
-
const
|
|
246
|
-
const companySlug = scoped && isCompanyJournalSlug(scoped[1]) ? scoped[1] : null;
|
|
237
|
+
const companySlug = clientHealthCompanyScopeFromCheckId(result.checkId);
|
|
247
238
|
const checkIds = checksByCompany.get(companySlug) ?? [];
|
|
248
239
|
checkIds.push(result.checkId);
|
|
249
240
|
checksByCompany.set(companySlug, checkIds);
|
|
@@ -107,6 +107,23 @@ export interface SyncHealthDeps {
|
|
|
107
107
|
* outside `companies/`.
|
|
108
108
|
*/
|
|
109
109
|
export declare function isSafeCompanySlug(slug: string): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* SessionStart health only ships `checkId` values matching
|
|
112
|
+
* `^sync.[A-Za-z0-9._-]{1,160}$`. A colon (hq-cloud's `company:cmp_…` scope
|
|
113
|
+
* key) becomes the unlabeled `unknown` row in those reports.
|
|
114
|
+
*/
|
|
115
|
+
export declare const CLIENT_HEALTH_SYNC_CHECK_ID: RegExp;
|
|
116
|
+
/**
|
|
117
|
+
* Doctor JSON `checkId` for a manifest scope. hq-cloud keys company rows
|
|
118
|
+
* `company:cmp_…`; the colon is replaced so SessionStart health can report
|
|
119
|
+
* the real id instead of `unknown`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function manifestCheckIdFromScopeKey(scopeKey: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Company slug or `company:cmp_…` uid used to resolve per-company health
|
|
124
|
+
* flags from a sync finding. Manifest ids may use a colon or a dot.
|
|
125
|
+
*/
|
|
126
|
+
export declare function clientHealthCompanyScopeFromCheckId(checkId: string): string | null;
|
|
110
127
|
/** True when this journal slug names a company rather than the personal tree. */
|
|
111
128
|
export declare function isCompanyJournalSlug(slug: string): boolean;
|
|
112
129
|
/** True when this journal slug names the personal tree or personal vault. */
|
|
@@ -194,13 +194,64 @@ const NON_COMPANY_JOURNAL_SLUGS = new Set([
|
|
|
194
194
|
PERSONAL_SCOPE_SLUG,
|
|
195
195
|
PERSONAL_VAULT_SCOPE_SLUG,
|
|
196
196
|
]);
|
|
197
|
+
/**
|
|
198
|
+
* SessionStart health only ships `checkId` values matching
|
|
199
|
+
* `^sync.[A-Za-z0-9._-]{1,160}$`. A colon (hq-cloud's `company:cmp_…` scope
|
|
200
|
+
* key) becomes the unlabeled `unknown` row in those reports.
|
|
201
|
+
*/
|
|
202
|
+
export const CLIENT_HEALTH_SYNC_CHECK_ID = /^sync\.[A-Za-z0-9._-]{1,160}$/;
|
|
203
|
+
const COMPANY_SCOPE_UID = /^company[.:](cmp_[A-Za-z0-9]{3,128})$/;
|
|
197
204
|
/** Keep malformed scopes distinct without letting them imitate valid slugs. */
|
|
198
205
|
function journalSlugForCheckId(slug) {
|
|
199
206
|
if (NON_COMPANY_JOURNAL_SLUGS.has(slug) || isSafeCompanySlug(slug)) {
|
|
200
207
|
return slug;
|
|
201
208
|
}
|
|
202
209
|
const encoded = Array.from(slug, (character) => character.codePointAt(0).toString(16)).join("-");
|
|
203
|
-
return `__hq_invalid_journal_scope__${encoded}
|
|
210
|
+
return boundInvalidScopeToken(`__hq_invalid_journal_scope__${encoded}`);
|
|
211
|
+
}
|
|
212
|
+
function boundInvalidScopeToken(token) {
|
|
213
|
+
// `sync.` + token must fit the health-hook 160-char suffix budget.
|
|
214
|
+
const maxSuffix = 160;
|
|
215
|
+
if (token.length <= maxSuffix)
|
|
216
|
+
return token;
|
|
217
|
+
return token.slice(0, maxSuffix);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Doctor JSON `checkId` for a manifest scope. hq-cloud keys company rows
|
|
221
|
+
* `company:cmp_…`; the colon is replaced so SessionStart health can report
|
|
222
|
+
* the real id instead of `unknown`.
|
|
223
|
+
*/
|
|
224
|
+
export function manifestCheckIdFromScopeKey(scopeKey) {
|
|
225
|
+
if (scopeKey.startsWith("company:")) {
|
|
226
|
+
return `sync.manifest.${scopeKey.replace(/:/g, ".")}`;
|
|
227
|
+
}
|
|
228
|
+
if (scopeKey === "personal" || isSafeCompanySlug(scopeKey)) {
|
|
229
|
+
return `sync.manifest.${scopeKey}`;
|
|
230
|
+
}
|
|
231
|
+
const encoded = Array.from(scopeKey, (character) => character.codePointAt(0).toString(16)).join("-");
|
|
232
|
+
return `sync.manifest.${boundInvalidScopeToken(`__hq_invalid_scope__${encoded}`)}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Company slug or `company:cmp_…` uid used to resolve per-company health
|
|
236
|
+
* flags from a sync finding. Manifest ids may use a colon or a dot.
|
|
237
|
+
*/
|
|
238
|
+
export function clientHealthCompanyScopeFromCheckId(checkId) {
|
|
239
|
+
const journalSlug = journalSlugFromCheckId(checkId);
|
|
240
|
+
if (journalSlug && isCompanyJournalSlug(journalSlug) && isSafeCompanySlug(journalSlug)) {
|
|
241
|
+
return journalSlug;
|
|
242
|
+
}
|
|
243
|
+
const manifest = /^sync\.manifest\.(.+)$/.exec(checkId);
|
|
244
|
+
if (!manifest)
|
|
245
|
+
return null;
|
|
246
|
+
const rest = manifest[1];
|
|
247
|
+
const companyUid = COMPANY_SCOPE_UID.exec(rest)?.[1];
|
|
248
|
+
if (companyUid)
|
|
249
|
+
return `company:${companyUid}`;
|
|
250
|
+
if (rest === "personal" || NON_COMPANY_JOURNAL_SLUGS.has(rest))
|
|
251
|
+
return null;
|
|
252
|
+
if (isSafeCompanySlug(rest))
|
|
253
|
+
return rest;
|
|
254
|
+
return null;
|
|
204
255
|
}
|
|
205
256
|
/** True when this journal slug names a company rather than the personal tree. */
|
|
206
257
|
export function isCompanyJournalSlug(slug) {
|
|
@@ -553,7 +604,7 @@ function manifestResults(deps, journals) {
|
|
|
553
604
|
}
|
|
554
605
|
const nowMs = deps.now().getTime();
|
|
555
606
|
const reported = statuses.map((entry) => {
|
|
556
|
-
const checkId =
|
|
607
|
+
const checkId = manifestCheckIdFromScopeKey(entry.scopeKey);
|
|
557
608
|
if (!entry.lastUploadAt) {
|
|
558
609
|
return {
|
|
559
610
|
status: "UNTESTED",
|
|
@@ -36,7 +36,7 @@ export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQu
|
|
|
36
36
|
export type { EnvelopeRepairDecision, LatestEnvelopeFile } from "./watcher.js";
|
|
37
37
|
export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
|
|
38
38
|
export type { CreateLaneMeshOptions, LaneMesh, LaneMeshEvent, WireEventKind, WireThreadEvent, } from "./mesh-events.js";
|
|
39
|
-
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
39
|
+
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
40
40
|
export type { CpuTimes, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
|
|
41
41
|
export { effectiveAccountHome, } from "./account-home.js";
|
|
42
42
|
export type { EffectiveAccountHome, } from "./account-home.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { PID_LIVENESS_DECISIONS, PROVIDER_COMMAND, PROVIDER_MODEL_FLAG, SPAWN_TE
|
|
|
20
20
|
export { SILENCE_MS, WATCHER_DECISION_EVENTS, WATCHER_OUT_MAX_BYTES, WATCHER_POLL_MS, WATCHER_SIGNAL_KINDS, appendCappedLog, appendWatcherSignal, applyTerminalToLane, classifyLaneExit, clearLaneStalled, isPidAlive, laneIdentityFromRecord, markLaneStalled, notifySlotFreed, parseWatcherSignalKind, readLatestEnvelope, runWatcherLoop, stalledReason, tickWatcher, watcherEventsPath, watcherLastSignalPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
21
21
|
export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQuestionIds, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
|
|
22
22
|
export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
|
|
23
|
-
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
23
|
+
export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
export { effectiveAccountHome, } from "./account-home.js";
|
|
25
25
|
export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, credentialPaths, hasCredentialFile, isCredentialProven, isLoggedOut, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
|
|
26
26
|
export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
|
|
@@ -16,6 +16,7 @@ export declare const CPU_SAMPLE_MS = 5000;
|
|
|
16
16
|
export declare const STARVATION_MAX_DEFERRALS = 10;
|
|
17
17
|
export declare const STARVATION_MAX_MS: number;
|
|
18
18
|
export declare const PROC_MEMINFO = "/proc/meminfo";
|
|
19
|
+
export declare const VM_STAT_BIN = "/usr/bin/vm_stat";
|
|
19
20
|
export declare const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
20
21
|
export declare const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
21
22
|
export interface CpuTimes {
|
|
@@ -30,9 +31,19 @@ export interface ThrottleDeps {
|
|
|
30
31
|
readFileSync: (file: string, encoding: "utf8") => string;
|
|
31
32
|
/**
|
|
32
33
|
* Fallback only where MemAvailable is absent from /proc/meminfo (or the
|
|
33
|
-
* file itself is missing). Do not use this as the
|
|
34
|
+
* file itself is missing on a non-darwin host). Do not use this as the
|
|
35
|
+
* Linux primary reading, and do not use it on darwin: os.freemem() there
|
|
36
|
+
* excludes reclaimable page cache and will hold every lane.
|
|
34
37
|
*/
|
|
35
38
|
freemem: () => number;
|
|
39
|
+
platform: () => NodeJS.Platform;
|
|
40
|
+
/**
|
|
41
|
+
* Darwin only: read vm_stat when /proc/meminfo is absent. Unused on Linux.
|
|
42
|
+
*/
|
|
43
|
+
execFileSync: (file: string, args: readonly string[], options: {
|
|
44
|
+
encoding: "utf8";
|
|
45
|
+
timeout: number;
|
|
46
|
+
}) => string;
|
|
36
47
|
existsSync: (file: string) => boolean;
|
|
37
48
|
mkdirSync: typeof fs.mkdirSync;
|
|
38
49
|
writeFileSync: typeof fs.writeFileSync;
|
|
@@ -70,8 +81,15 @@ export declare function recordCpuBusyObservation(state: ThrottleState, observati
|
|
|
70
81
|
/** Parse MemAvailable kB from /proc/meminfo. */
|
|
71
82
|
export declare function parseMemAvailableKb(contents: string): number | null;
|
|
72
83
|
/**
|
|
73
|
-
* Available memory in MiB
|
|
74
|
-
* os.freemem()
|
|
84
|
+
* Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
|
|
85
|
+
* pages. That is the reclaimable set os.freemem() omits on macOS.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseVmStatAvailableMb(contents: string): number | null;
|
|
88
|
+
/**
|
|
89
|
+
* Available memory in MiB. Prefers kernel MemAvailable. On darwin without
|
|
90
|
+
* /proc/meminfo, uses vm_stat (free+inactive+speculative) and never os.freemem().
|
|
91
|
+
* Elsewhere, falls back to os.freemem() only where MemAvailable is absent.
|
|
92
|
+
* Returning null skips the memory gate instead of holding every lane.
|
|
75
93
|
*/
|
|
76
94
|
export declare function readAvailableMemMb(deps?: ThrottleDeps): number | null;
|
|
77
95
|
export interface ThrottleState {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Overrides HQ_LANES_CPU_BUSY_OVERRIDE and HQ_LANES_CPU_CHECK are deliberately
|
|
10
10
|
* distinct from the runner's HQ_WORKFLOW_* pair — this module never reads those.
|
|
11
11
|
*/
|
|
12
|
+
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
12
13
|
import * as fs from "node:fs";
|
|
13
14
|
import * as os from "node:os";
|
|
14
15
|
import * as path from "node:path";
|
|
@@ -20,6 +21,8 @@ const CPU_CUMULATIVE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
|
20
21
|
export const STARVATION_MAX_DEFERRALS = 10;
|
|
21
22
|
export const STARVATION_MAX_MS = 30 * 60 * 1000;
|
|
22
23
|
export const PROC_MEMINFO = "/proc/meminfo";
|
|
24
|
+
export const VM_STAT_BIN = "/usr/bin/vm_stat";
|
|
25
|
+
const VM_STAT_TIMEOUT_MS = 2_000;
|
|
23
26
|
export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
24
27
|
export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
25
28
|
function defaultSleep(ms) {
|
|
@@ -35,8 +38,13 @@ export function defaultThrottleDeps(env = process.env) {
|
|
|
35
38
|
sleep: defaultSleep,
|
|
36
39
|
readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
|
|
37
40
|
// os.freemem() reports MemFree on Linux (excludes page cache). Used only
|
|
38
|
-
// where MemAvailable is absent — see readAvailableMemMb.
|
|
41
|
+
// where MemAvailable is absent on a non-darwin host — see readAvailableMemMb.
|
|
39
42
|
freemem: () => os.freemem(),
|
|
43
|
+
platform: () => process.platform,
|
|
44
|
+
execFileSync: (file, args, options) => nodeExecFileSync(file, [...args], {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: options.timeout,
|
|
47
|
+
}),
|
|
40
48
|
existsSync: (file) => fs.existsSync(file),
|
|
41
49
|
mkdirSync: fs.mkdirSync.bind(fs),
|
|
42
50
|
writeFileSync: fs.writeFileSync.bind(fs),
|
|
@@ -184,9 +192,63 @@ export function parseMemAvailableKb(contents) {
|
|
|
184
192
|
}
|
|
185
193
|
return null;
|
|
186
194
|
}
|
|
195
|
+
function parseVmStatPages(contents, label) {
|
|
196
|
+
const match = new RegExp(`^Pages ${label}:\\s+([0-9.]+)\\s*$`, "m").exec(contents);
|
|
197
|
+
if (!match)
|
|
198
|
+
return null;
|
|
199
|
+
const n = Number(match[1].replace(/\.$/, ""));
|
|
200
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
|
|
204
|
+
* pages. That is the reclaimable set os.freemem() omits on macOS.
|
|
205
|
+
*/
|
|
206
|
+
export function parseVmStatAvailableMb(contents) {
|
|
207
|
+
const pageSizeMatch = /page size of (\d+) bytes/i.exec(contents);
|
|
208
|
+
const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : Number.NaN;
|
|
209
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0)
|
|
210
|
+
return null;
|
|
211
|
+
const free = parseVmStatPages(contents, "free");
|
|
212
|
+
const inactive = parseVmStatPages(contents, "inactive");
|
|
213
|
+
const speculative = parseVmStatPages(contents, "speculative");
|
|
214
|
+
if (free === null || inactive === null || speculative === null)
|
|
215
|
+
return null;
|
|
216
|
+
return ((free + inactive + speculative) * pageSize) / (1024 * 1024);
|
|
217
|
+
}
|
|
218
|
+
function readDarwinAvailableMemMb(deps) {
|
|
219
|
+
try {
|
|
220
|
+
const contents = deps.execFileSync(VM_STAT_BIN, [], {
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
timeout: VM_STAT_TIMEOUT_MS,
|
|
223
|
+
});
|
|
224
|
+
const mb = parseVmStatAvailableMb(contents);
|
|
225
|
+
if (mb !== null)
|
|
226
|
+
return mb;
|
|
227
|
+
process.stderr.write("lanes throttle: darwin vm_stat output was unreadable; skipping the memory gate\n");
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
process.stderr.write(`lanes throttle: darwin vm_stat failed: ${err instanceof Error ? err.message : String(err)}; skipping the memory gate\n`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function readFreememMb(deps) {
|
|
236
|
+
try {
|
|
237
|
+
const bytes = deps.freemem();
|
|
238
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
239
|
+
return null;
|
|
240
|
+
return bytes / (1024 * 1024);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
process.stderr.write(`lanes throttle: freemem fallback failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
187
247
|
/**
|
|
188
|
-
* Available memory in MiB. Prefers kernel MemAvailable
|
|
189
|
-
*
|
|
248
|
+
* Available memory in MiB. Prefers kernel MemAvailable. On darwin without
|
|
249
|
+
* /proc/meminfo, uses vm_stat (free+inactive+speculative) and never os.freemem().
|
|
250
|
+
* Elsewhere, falls back to os.freemem() only where MemAvailable is absent.
|
|
251
|
+
* Returning null skips the memory gate instead of holding every lane.
|
|
190
252
|
*/
|
|
191
253
|
export function readAvailableMemMb(deps = defaultThrottleDeps()) {
|
|
192
254
|
try {
|
|
@@ -203,18 +265,10 @@ export function readAvailableMemMb(deps = defaultThrottleDeps()) {
|
|
|
203
265
|
process.stderr.write(`lanes throttle: failed to read ${PROC_MEMINFO}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
204
266
|
return null;
|
|
205
267
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const bytes = deps.freemem();
|
|
210
|
-
if (!Number.isFinite(bytes) || bytes < 0)
|
|
211
|
-
return null;
|
|
212
|
-
return bytes / (1024 * 1024);
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
process.stderr.write(`lanes throttle: freemem fallback failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
216
|
-
return null;
|
|
268
|
+
if (deps.platform() === "darwin")
|
|
269
|
+
return readDarwinAvailableMemMb(deps);
|
|
217
270
|
}
|
|
271
|
+
return readFreememMb(deps);
|
|
218
272
|
}
|
|
219
273
|
export function emptyThrottleState() {
|
|
220
274
|
return {
|