@indigoai-us/hq-cli 5.201.2 → 5.201.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.201.4] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Claude account balancing on lane resume now respects the attached worker
|
|
10
|
+
profile's account pin, skips accounts pinned to a different model, and treats
|
|
11
|
+
`--account` naming the current account as a keep request for message/resume.
|
|
12
|
+
|
|
13
|
+
## [5.201.3] — 2026-09-25
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- A flag-registry worker whose startup fails because the working directory was deleted or its package files were being replaced no longer creates a Sentry issue. Other flag-registry worker errors remain visible with a worker tag.
|
|
18
|
+
|
|
5
19
|
## [5.201.2] — 2026-09-25
|
|
6
20
|
|
|
7
21
|
### Changed
|
package/dist/commands/lanes.js
CHANGED
|
@@ -4905,8 +4905,26 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4905
4905
|
}
|
|
4906
4906
|
}
|
|
4907
4907
|
function recordResumeSetup(ctx, lane, input) {
|
|
4908
|
+
if (input.cliModel !== undefined) {
|
|
4909
|
+
const model = input.cliModel.trim();
|
|
4910
|
+
if (!model)
|
|
4911
|
+
throw new LanesError("empty_model", "--model is empty or whitespace-only", { value: input.cliModel });
|
|
4912
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
4913
|
+
const account = lane.account === undefined
|
|
4914
|
+
? undefined
|
|
4915
|
+
: config.providers[lane.provider].accounts[lane.account];
|
|
4916
|
+
if (account === undefined) {
|
|
4917
|
+
throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
4918
|
+
}
|
|
4919
|
+
validateLaneModel(lane.provider, model, account.home, {
|
|
4920
|
+
readFileSync: ctx.readFileSync,
|
|
4921
|
+
probe: ctx.probeProvider,
|
|
4922
|
+
env: ctx.env,
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4908
4925
|
if (input.profile === undefined &&
|
|
4909
4926
|
input.effort === undefined &&
|
|
4927
|
+
input.cliModel === undefined &&
|
|
4910
4928
|
input.meshCompany === undefined)
|
|
4911
4929
|
return lane;
|
|
4912
4930
|
return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
|
|
@@ -4915,6 +4933,10 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4915
4933
|
}
|
|
4916
4934
|
const next = { ...current };
|
|
4917
4935
|
applyResumeProfileAndEffort(current, next, input);
|
|
4936
|
+
if (input.cliModel !== undefined) {
|
|
4937
|
+
next.model = input.cliModel.trim();
|
|
4938
|
+
next.model_source = "cli";
|
|
4939
|
+
}
|
|
4918
4940
|
if (input.meshCompany !== undefined) {
|
|
4919
4941
|
if (current.company === null)
|
|
4920
4942
|
next.mesh_company = input.meshCompany;
|
|
@@ -5147,7 +5169,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5147
5169
|
return { lane: moved, from, to: target, copy, queued };
|
|
5148
5170
|
}, ctx.sleep);
|
|
5149
5171
|
}
|
|
5150
|
-
async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
5172
|
+
async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
5151
5173
|
if (lane.provider !== "claude")
|
|
5152
5174
|
return undefined;
|
|
5153
5175
|
if (!await ctx.laneClaudeAccountBalanceEnabled(lane.company)) {
|
|
@@ -5171,6 +5193,16 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
|
5171
5193
|
}
|
|
5172
5194
|
const hqRoot = ctx.hqRoot();
|
|
5173
5195
|
const config = loadLanesConfig(hqRoot);
|
|
5196
|
+
const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
|
|
5197
|
+
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
5198
|
+
: undefined);
|
|
5199
|
+
const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
|
|
5200
|
+
const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
|
|
5201
|
+
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
5202
|
+
const profilePin = profileAccounts ?? (profileAccount === undefined ? undefined : [profileAccount]);
|
|
5203
|
+
const allowedAccounts = profilePin !== undefined && lane.admission_accounts !== undefined
|
|
5204
|
+
? profilePin.filter((account) => lane.admission_accounts.includes(account))
|
|
5205
|
+
: profilePin ?? lane.admission_accounts;
|
|
5174
5206
|
const currentConfig = lane.account === undefined
|
|
5175
5207
|
? undefined
|
|
5176
5208
|
: config.providers.claude.accounts[lane.account];
|
|
@@ -5181,20 +5213,71 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
|
5181
5213
|
probe: ctx.probeProvider,
|
|
5182
5214
|
env: ctx.env,
|
|
5183
5215
|
}).logged_in;
|
|
5216
|
+
const pinnedModel = workerProfile !== undefined
|
|
5217
|
+
? lane.model_source === "cli" ? lane.model : undefined
|
|
5218
|
+
: lane.model_source === undefined || lane.model_source === "cli"
|
|
5219
|
+
? lane.model
|
|
5220
|
+
: undefined;
|
|
5221
|
+
const model = resolveLaneModel({
|
|
5222
|
+
cliModel: pinnedModel,
|
|
5223
|
+
profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
|
|
5224
|
+
accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
|
|
5225
|
+
providerModel: config.providers.claude.model,
|
|
5226
|
+
});
|
|
5184
5227
|
const accountNeedsRepair = currentConfig?.enabled !== true || currentLogin === false;
|
|
5185
5228
|
const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
5186
5229
|
if (!accountNeedsRepair && !mayRebalance)
|
|
5187
5230
|
return undefined;
|
|
5188
5231
|
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
5232
|
+
const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
|
|
5233
|
+
const currentTargetAccount = lane.account === undefined
|
|
5234
|
+
? undefined
|
|
5235
|
+
: targetHost?.capacity.providers.claude.accounts[lane.account];
|
|
5236
|
+
if (profilePin !== undefined &&
|
|
5237
|
+
lane.account !== undefined &&
|
|
5238
|
+
allowedAccounts?.includes(lane.account) &&
|
|
5239
|
+
currentConfig?.enabled === true &&
|
|
5240
|
+
currentTargetAccount?.enabled === true &&
|
|
5241
|
+
currentTargetAccount.logged_in === true &&
|
|
5242
|
+
(model === undefined || currentConfig.model === undefined || currentConfig.model === model))
|
|
5243
|
+
return undefined;
|
|
5189
5244
|
const selected = selectClaudeAccount({
|
|
5190
5245
|
hosts: balanceHosts.snapshots,
|
|
5191
5246
|
targetHost: lane.host,
|
|
5192
5247
|
config,
|
|
5193
|
-
...(
|
|
5248
|
+
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
5249
|
+
...(model !== undefined ? { model } : {}),
|
|
5194
5250
|
unavailableHosts: balanceHosts.unavailableHosts,
|
|
5195
5251
|
});
|
|
5196
5252
|
return { ...selected, selected_at: ctx.now().toISOString() };
|
|
5197
5253
|
}
|
|
5254
|
+
function accountRequestKeepsCurrentLane(lane, rawAccount) {
|
|
5255
|
+
const raw = rawAccount.trim();
|
|
5256
|
+
const colon = raw.indexOf(":");
|
|
5257
|
+
const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
|
|
5258
|
+
const requestedAccount = colon > 0 ? raw.slice(colon + 1) : raw;
|
|
5259
|
+
return requestedAccount.length > 0 &&
|
|
5260
|
+
requestedAccount === lane.account &&
|
|
5261
|
+
(requestedProvider === undefined || requestedProvider === lane.provider);
|
|
5262
|
+
}
|
|
5263
|
+
function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
5264
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
5265
|
+
const account = lane.account === undefined
|
|
5266
|
+
? undefined
|
|
5267
|
+
: config.providers[lane.provider].accounts[lane.account];
|
|
5268
|
+
if (account === undefined || !account.enabled) {
|
|
5269
|
+
throw new AccountResolutionError(account === undefined ? "unknown_account" : "account_disabled", account === undefined
|
|
5270
|
+
? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`
|
|
5271
|
+
: `Account ${JSON.stringify(lane.account)} is disabled${account.disabled_reason ? `: ${account.disabled_reason}` : ""}`, { provider: lane.provider, account: lane.account ?? "", ...(account?.disabled_reason !== undefined ? { reason: account.disabled_reason } : {}) });
|
|
5272
|
+
}
|
|
5273
|
+
if (accountStatus(lane.provider, lane.account, config, {
|
|
5274
|
+
existsSync: ctx.existsSync,
|
|
5275
|
+
probe: ctx.probeProvider,
|
|
5276
|
+
env: ctx.env,
|
|
5277
|
+
}).logged_in !== true) {
|
|
5278
|
+
throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: lane.account, lane_id: lane.lane_id });
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5198
5281
|
async function runMessage(ctx, laneId, opts) {
|
|
5199
5282
|
const hqRoot = ctx.hqRoot();
|
|
5200
5283
|
const text = opts.text;
|
|
@@ -5215,16 +5298,19 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5215
5298
|
// The final state transition repeats this check under the senior lock.
|
|
5216
5299
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
5217
5300
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5301
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5302
|
+
if (keepCurrentAccount)
|
|
5303
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5304
|
+
// A real account move is refused on its own terms first: a running lane and
|
|
5305
|
+
// a provider without session resume get the move's named errors, not the
|
|
5306
|
+
// generic delivery refusal below. Naming the current account is a keep.
|
|
5307
|
+
if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
|
|
5222
5308
|
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
5223
5309
|
lane_id: laneId,
|
|
5224
5310
|
state: lane.state,
|
|
5225
5311
|
});
|
|
5226
5312
|
}
|
|
5227
|
-
if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
5313
|
+
if (opts.account !== undefined && !keepCurrentAccount && !providerSupportsSessionResume(lane.provider)) {
|
|
5228
5314
|
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
5229
5315
|
}
|
|
5230
5316
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
@@ -5239,6 +5325,9 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5239
5325
|
message,
|
|
5240
5326
|
});
|
|
5241
5327
|
}
|
|
5328
|
+
if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
|
|
5329
|
+
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5330
|
+
}
|
|
5242
5331
|
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5243
5332
|
throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5244
5333
|
}
|
|
@@ -5252,12 +5341,12 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5252
5341
|
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5253
5342
|
}
|
|
5254
5343
|
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5255
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
5344
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5256
5345
|
: undefined;
|
|
5257
5346
|
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5258
5347
|
cliEffort,
|
|
5259
5348
|
profile: resumeProfile,
|
|
5260
|
-
account: opts.account ?? automaticAccountSelection?.account,
|
|
5349
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
|
|
5261
5350
|
model: opts.model,
|
|
5262
5351
|
});
|
|
5263
5352
|
const messageId = mintMessageId();
|
|
@@ -5280,7 +5369,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5280
5369
|
let resumed = null;
|
|
5281
5370
|
let move = null;
|
|
5282
5371
|
let queuedOnAdmission = false;
|
|
5283
|
-
if (opts.account !== undefined) {
|
|
5372
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5284
5373
|
// A moved message is always a resume on the target account: a running
|
|
5285
5374
|
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5286
5375
|
move = await prepareAccountMove(ctx, lane, {
|
|
@@ -5321,13 +5410,14 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5321
5410
|
}
|
|
5322
5411
|
}
|
|
5323
5412
|
}
|
|
5324
|
-
if (resumeProfile !== undefined && opts.account === undefined) {
|
|
5413
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5325
5414
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5326
5415
|
}
|
|
5327
|
-
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined)) {
|
|
5416
|
+
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
|
|
5328
5417
|
lane = recordResumeSetup(ctx, lane, {
|
|
5329
5418
|
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5330
5419
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5420
|
+
...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5331
5421
|
});
|
|
5332
5422
|
}
|
|
5333
5423
|
if (move?.queued) {
|
|
@@ -5751,20 +5841,23 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5751
5841
|
const resumeProfile = opts.worker !== undefined
|
|
5752
5842
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5753
5843
|
: undefined;
|
|
5754
|
-
|
|
5844
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5845
|
+
if (keepCurrentAccount)
|
|
5846
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5847
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5755
5848
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5756
5849
|
}
|
|
5757
5850
|
const automaticAccountSelection = opts.account === undefined
|
|
5758
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
5851
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5759
5852
|
: undefined;
|
|
5760
5853
|
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5761
5854
|
cliEffort,
|
|
5762
5855
|
profile: resumeProfile,
|
|
5763
|
-
account: opts.account ?? automaticAccountSelection?.account,
|
|
5856
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
|
|
5764
5857
|
model: opts.model,
|
|
5765
5858
|
});
|
|
5766
5859
|
let move = null;
|
|
5767
|
-
if (opts.account !== undefined) {
|
|
5860
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5768
5861
|
move = await prepareAccountMove(ctx, lane, {
|
|
5769
5862
|
account: opts.account,
|
|
5770
5863
|
cliModel: opts.model?.trim() || undefined,
|
|
@@ -5805,6 +5898,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5805
5898
|
lane = recordResumeSetup(ctx, lane, {
|
|
5806
5899
|
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5807
5900
|
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5901
|
+
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5808
5902
|
...(requestedMeshCompany !== undefined
|
|
5809
5903
|
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
5810
5904
|
: {}),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* keep a short-lived, offline CLI alive until the client's network deadline.
|
|
7
7
|
*/
|
|
8
8
|
import { Worker } from "node:worker_threads";
|
|
9
|
+
import * as Sentry from "@sentry/node";
|
|
9
10
|
import { isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
10
11
|
import { readMachineCompanyBinding } from "./work-context/company.js";
|
|
11
12
|
import { readSessionState } from "./work-context/state.js";
|
|
@@ -116,12 +117,19 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
116
117
|
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
117
118
|
processReader = processSnapshotReader(companyIdentifiers);
|
|
118
119
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// The SDK's child-process integration captures every Worker error unless
|
|
121
|
+
// its process-wide captureWorkerErrors option is disabled. Scope only this
|
|
122
|
+
// worker so the reporting boundary can classify its startup errors without
|
|
123
|
+
// changing capture for unrelated workers.
|
|
124
|
+
const worker = Sentry.withScope((scope) => {
|
|
125
|
+
scope.setTag("worker", "flag-registry");
|
|
126
|
+
return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
|
|
127
|
+
workerData: {
|
|
128
|
+
endpoint,
|
|
129
|
+
...(companyUid ? { companyUid } : {}),
|
|
130
|
+
...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
125
133
|
});
|
|
126
134
|
worker.on("message", (value) => {
|
|
127
135
|
if (isFlagSnapshot(value))
|
|
@@ -67,6 +67,7 @@ export declare function selectClaudeAccount(input: {
|
|
|
67
67
|
targetHost: LaneHost;
|
|
68
68
|
config: LanesConfig;
|
|
69
69
|
allowedAccounts?: readonly string[];
|
|
70
|
+
model?: string;
|
|
70
71
|
explicitAccount?: string;
|
|
71
72
|
unavailableHosts?: readonly BalanceHostGap[];
|
|
72
73
|
}): ClaudeAccountSelection;
|
|
@@ -91,7 +91,10 @@ function buildLoads(input) {
|
|
|
91
91
|
const loads = Object.keys(configAccounts).sort().map((account) => {
|
|
92
92
|
const hosts = input.hosts.map((host) => accountHostLoad(host, account));
|
|
93
93
|
const targetLoad = hosts.find((host) => sameHost(host.host, targetHost));
|
|
94
|
-
const
|
|
94
|
+
const configuredModel = configAccounts[account]?.model;
|
|
95
|
+
const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (input.model !== undefined && configuredModel !== undefined && configuredModel !== input.model
|
|
96
|
+
? "model_mismatch"
|
|
97
|
+
: undefined);
|
|
95
98
|
if (ineligible !== undefined)
|
|
96
99
|
skipped.push({ account, host: targetHost, reason: ineligible });
|
|
97
100
|
const knownCounts = hosts.flatMap((host) => host.live_lanes === null ? [] : [host.live_lanes]);
|
package/dist/sentry.js
CHANGED
|
@@ -20,7 +20,14 @@ import { installTreeTornSentryTagsFor, sentryFingerprintFor, sentryMcpCollisionC
|
|
|
20
20
|
* Consistent with the swallow-EPIPE posture established in #138.
|
|
21
21
|
*/
|
|
22
22
|
export function epipeAwareBeforeSend(event, hint) {
|
|
23
|
-
|
|
23
|
+
const flagRegistryWorkerEvent = isFlagRegistryWorkerEvent(event);
|
|
24
|
+
if (flagRegistryWorkerEvent) {
|
|
25
|
+
if (isExpectedFlagRegistryWorkerStartupFailure(hint?.originalException))
|
|
26
|
+
return null;
|
|
27
|
+
event.level = "warning";
|
|
28
|
+
event.tags = { ...event.tags, worker: "flag-registry" };
|
|
29
|
+
}
|
|
30
|
+
if (!flagRegistryWorkerEvent && isEpipe(hint?.originalException))
|
|
24
31
|
return null;
|
|
25
32
|
// Path-independent belt for environmental filesystem failures (ENOSPC /
|
|
26
33
|
// EDQUOT / EROFS — a full disk, exhausted quota, or read-only mount, e.g.
|
|
@@ -31,7 +38,7 @@ export function epipeAwareBeforeSend(event, hint) {
|
|
|
31
38
|
// either. handleTopLevelError already prints an actionable message for the
|
|
32
39
|
// boundary route; dropping the event here suppresses the fatal regardless of
|
|
33
40
|
// route, while the CLI still exits non-zero. HQ-CLI-R (Sentry 7671416365).
|
|
34
|
-
if (environmentalFsErrorMessage(hint?.originalException))
|
|
41
|
+
if (!flagRegistryWorkerEvent && environmentalFsErrorMessage(hint?.originalException))
|
|
35
42
|
return null;
|
|
36
43
|
// Path-independent belt for an IN-PROCESS incomplete-install module-load
|
|
37
44
|
// failure — hq-cli's own globally installed tree is not intact at load time
|
|
@@ -50,7 +57,7 @@ export function epipeAwareBeforeSend(event, hint) {
|
|
|
50
57
|
// node_modules, so an hq-cli packaging fault (a dist/ miss, or a bare miss the
|
|
51
58
|
// requirer does NOT declare — a possible undeclared/peer dependency) and the
|
|
52
59
|
// path-less unattributable shape are NOT dropped here and stay captured.
|
|
53
|
-
if (incompleteInstallMessage(hint?.originalException))
|
|
60
|
+
if (!flagRegistryWorkerEvent && incompleteInstallMessage(hint?.originalException))
|
|
54
61
|
return null;
|
|
55
62
|
// An incomplete-install shape that is NOT suppressed (a bare miss the requirer
|
|
56
63
|
// does not declare, or a path-less loader/resolver ENOENT) should still arrive
|
|
@@ -95,6 +102,27 @@ export function epipeAwareBeforeSend(event, hint) {
|
|
|
95
102
|
}
|
|
96
103
|
return beforeSend(event, hint);
|
|
97
104
|
}
|
|
105
|
+
function isFlagRegistryWorkerEvent(event) {
|
|
106
|
+
return event.tags?.worker === "flag-registry" && Boolean(event.exception?.values?.some((value) => value.mechanism?.type === "auto.child_process.worker_thread"));
|
|
107
|
+
}
|
|
108
|
+
function isExpectedFlagRegistryWorkerStartupFailure(error) {
|
|
109
|
+
if (typeof error !== "object" || error === null)
|
|
110
|
+
return false;
|
|
111
|
+
const details = error;
|
|
112
|
+
if (details.code === "ENOENT" && details.syscall === "uv_cwd") {
|
|
113
|
+
const stack = typeof details.stack === "string" ? details.stack : "";
|
|
114
|
+
return stack.includes("node:internal/main/worker_thread") && /\bat process\.cwd \(/.test(stack);
|
|
115
|
+
}
|
|
116
|
+
if (details.code !== "MODULE_NOT_FOUND" && details.code !== "ERR_MODULE_NOT_FOUND") {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const message = typeof details.message === "string" ? details.message : "";
|
|
120
|
+
const missingModule = /Cannot find module\s+['"]([^'"]+)['"]/.exec(message)?.[1];
|
|
121
|
+
if (!missingModule)
|
|
122
|
+
return false;
|
|
123
|
+
return missingModule.replaceAll("\\", "/").endsWith("/flag-registry-worker.js") ||
|
|
124
|
+
missingModule === "flag-registry-worker.js";
|
|
125
|
+
}
|
|
98
126
|
/**
|
|
99
127
|
* Tests substitute only the transport so the real SDK lifecycle can be
|
|
100
128
|
* exercised without an ingest request. Production callers pass nothing.
|