@brunobracaioli/aibrain 0.3.0-beta.7 → 0.4.0-beta.2

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.
@@ -0,0 +1,481 @@
1
+ // @ts-check
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { constants } from 'node:fs';
5
+ import { mkdtemp, open, rm, lstat } from 'node:fs/promises';
6
+ import { spawn } from 'node:child_process';
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import fingerprint from './codex-protocol-fingerprint.json' with { type: 'json' };
10
+
11
+ const MAX_OUTPUT_BYTES = 64 * 1024;
12
+ const MAX_SCHEMA_BYTES = 8 * 1024 * 1024;
13
+ const MAX_SCHEMA_SET_BYTES = 24 * 1024 * 1024;
14
+ const MAX_SCHEMA_DEPTH = 96;
15
+ const MAX_SCHEMA_NODES = 250_000;
16
+ const ROOTS = Object.keys(fingerprint.roots);
17
+ const PAGINATION_CONTRACT = fingerprint.consumed_pagination ?? null;
18
+ const PAGINATION_CONTRACT_VALID =
19
+ PAGINATION_CONTRACT === null ||
20
+ (PAGINATION_CONTRACT.method === 'thread/turns/list' &&
21
+ PAGINATION_CONTRACT.request_schema === 'v2/ThreadTurnsListParams.json' &&
22
+ PAGINATION_CONTRACT.response_schema === 'v2/ThreadTurnsListResponse.json' &&
23
+ PAGINATION_CONTRACT.request_schema_sha256 ===
24
+ fingerprint.roots[PAGINATION_CONTRACT.request_schema]?.sha256 &&
25
+ PAGINATION_CONTRACT.response_schema_sha256 ===
26
+ fingerprint.roots[PAGINATION_CONTRACT.response_schema]?.sha256 &&
27
+ typeof PAGINATION_CONTRACT.graph_sha256 === 'string' &&
28
+ PAGINATION_CONTRACT.graph_sha256.length === 64);
29
+ const QUALIFIED_VERSIONS = new Set(['0.161.0', '0.162.0', '0.162.1', '0.163.0', '0.164.0']);
30
+ const ANNOTATIONS = new Set([
31
+ '$comment',
32
+ 'title',
33
+ 'description',
34
+ 'examples',
35
+ 'deprecated',
36
+ 'readOnly',
37
+ 'writeOnly',
38
+ 'externalDocs',
39
+ ]);
40
+ const SCHEMA_MAP_KEYS = new Set([
41
+ 'properties',
42
+ 'definitions',
43
+ '$defs',
44
+ 'patternProperties',
45
+ 'dependentSchemas',
46
+ ]);
47
+ const LITERAL_KEYS = new Set(['enum', 'const', 'default']);
48
+ /** @typedef {Record<string, unknown>} JsonObject */
49
+
50
+ /** @param {unknown} value @param {boolean} [map] @param {boolean} [literal] @returns {unknown} */
51
+ function normalize(value, map = false, literal = false) {
52
+ if (literal) return value;
53
+ if (Array.isArray(value)) return value.map((item) => normalize(item));
54
+ if (value === null || typeof value !== 'object') return value;
55
+ /** @type {JsonObject} */
56
+ const output = {};
57
+ for (const [key, child] of Object.entries(value)) {
58
+ if (!map && ANNOTATIONS.has(key)) continue;
59
+ if (LITERAL_KEYS.has(key)) {
60
+ output[key] = normalize(child, false, true);
61
+ } else if (
62
+ SCHEMA_MAP_KEYS.has(key) &&
63
+ child &&
64
+ typeof child === 'object' &&
65
+ !Array.isArray(child)
66
+ ) {
67
+ output[key] = Object.fromEntries(
68
+ Object.entries(child).map(([name, schema]) => [name, normalize(schema)]),
69
+ );
70
+ } else output[key] = normalize(child);
71
+ }
72
+ return output;
73
+ }
74
+
75
+ /** @param {unknown} value @returns {string} */
76
+ function canonical(value) {
77
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
78
+ if (value && typeof value === 'object') {
79
+ const object = /** @type {JsonObject} */ (value);
80
+ return `{${Object.keys(object)
81
+ .sort()
82
+ .map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`)
83
+ .join(',')}}`;
84
+ }
85
+ return JSON.stringify(value);
86
+ }
87
+
88
+ /** @param {unknown} value */
89
+ function sha(value) {
90
+ return createHash('sha256').update(canonical(value), 'utf8').digest('hex');
91
+ }
92
+
93
+ /** @param {string} output */
94
+ function parseStableVersion(output) {
95
+ const match = output
96
+ .trim()
97
+ .match(
98
+ /^(?:codex(?:-cli)?\s+)?(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})(-[A-Za-z0-9.-]+)?$/iu,
99
+ );
100
+ if (!match) return null;
101
+ return {
102
+ major: Number(match[1]),
103
+ minor: Number(match[2]),
104
+ patch: Number(match[3]),
105
+ prerelease: match[4] !== undefined,
106
+ text: `${match[1]}.${match[2]}.${match[3]}`,
107
+ };
108
+ }
109
+
110
+ /** @param {unknown} value */
111
+ function assertBoundedSchema(value) {
112
+ const state = { nodes: 0 };
113
+ /** @param {unknown} current @param {number} depth */
114
+ function visit(current, depth) {
115
+ state.nodes += 1;
116
+ if (depth > MAX_SCHEMA_DEPTH || state.nodes > MAX_SCHEMA_NODES)
117
+ throw new Error('schema_shape_too_large');
118
+ if (Array.isArray(current)) {
119
+ for (const item of current) visit(item, depth + 1);
120
+ } else if (current && typeof current === 'object') {
121
+ for (const child of Object.values(current)) visit(child, depth + 1);
122
+ }
123
+ }
124
+ visit(value, 0);
125
+ }
126
+
127
+ /** @param {JsonObject} parsed @param {string} reference @returns {unknown} */
128
+ function localDefinition(parsed, reference) {
129
+ const prefix = reference.startsWith('#/definitions/') ? '#/definitions/' : '#/$defs/';
130
+ const name = reference.slice(prefix.length);
131
+ let current = prefix === '#/definitions/' ? parsed.definitions : parsed.$defs;
132
+ for (const part of name.split('/')) {
133
+ if (part === '__proto__' || part === 'prototype' || part === 'constructor') return undefined;
134
+ if (!current || typeof current !== 'object' || Array.isArray(current)) return undefined;
135
+ current = /** @type {JsonObject} */ (current)[part];
136
+ }
137
+ return current;
138
+ }
139
+
140
+ /** @param {JsonObject} target @param {string} reference @param {unknown} value */
141
+ function setLocalDefinition(target, reference, value) {
142
+ const prefix = reference.startsWith('#/definitions/') ? '#/definitions/' : '#/$defs/';
143
+ const parts = reference.slice(prefix.length).split('/');
144
+ let current = target;
145
+ for (const part of parts.slice(0, -1)) {
146
+ if (part === '__proto__' || part === 'prototype' || part === 'constructor')
147
+ throw new Error('schema_reference_unsafe');
148
+ if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part]))
149
+ current[part] = {};
150
+ current = /** @type {JsonObject} */ (current[part]);
151
+ }
152
+ if (
153
+ parts.at(-1) === '__proto__' ||
154
+ parts.at(-1) === 'prototype' ||
155
+ parts.at(-1) === 'constructor'
156
+ )
157
+ throw new Error('schema_reference_unsafe');
158
+ current[parts.at(-1) ?? ''] = value;
159
+ }
160
+
161
+ /**
162
+ * Project one generated schema root using the same consumed-method and reachable-reference
163
+ * rules used by the runtime compatibility check. A missing or duplicated method is reported
164
+ * separately so the runtime can retain its bounded diagnostic.
165
+ * @param {unknown} parsed
166
+ * @param {string[]|null} methods
167
+ * @returns {{normalized:unknown, methodMissing:boolean}}
168
+ */
169
+ export function projectCodexSchema(parsed, methods) {
170
+ if (methods === null) return { normalized: normalize(parsed), methodMissing: false };
171
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
172
+ return { normalized: null, methodMissing: true };
173
+ const root = /** @type {JsonObject} */ (parsed);
174
+ const oneOf = Array.isArray(root.oneOf) ? root.oneOf : [];
175
+ const selected = [];
176
+ for (const methodName of methods) {
177
+ const matches = oneOf.filter((/** @type {unknown} */ rawBranch) => {
178
+ if (!rawBranch || typeof rawBranch !== 'object' || Array.isArray(rawBranch)) return false;
179
+ const branch = /** @type {JsonObject} */ (rawBranch);
180
+ const properties = /** @type {JsonObject} */ (branch.properties ?? {});
181
+ const method = /** @type {JsonObject} */ (properties.method ?? {});
182
+ const values = Array.isArray(method.enum) ? method.enum : [];
183
+ return values.length === 1 && values[0] === methodName;
184
+ });
185
+ if (matches.length !== 1) return { normalized: null, methodMissing: true };
186
+ selected.push(matches[0]);
187
+ }
188
+ /** @type {JsonObject} */
189
+ const definitions = {};
190
+ const found = new Set();
191
+ /** @param {unknown} value @param {boolean} [map] */
192
+ const visit = (value, map = false) => {
193
+ if (!value || typeof value !== 'object') return;
194
+ if (Array.isArray(value)) {
195
+ value.forEach((item) => visit(item));
196
+ return;
197
+ }
198
+ const object = /** @type {JsonObject} */ (value);
199
+ if (typeof object.$ref === 'string') {
200
+ if (!object.$ref.startsWith('#/definitions/') && !object.$ref.startsWith('#/$defs/'))
201
+ throw new Error('schema_reference_nonlocal');
202
+ const prefix = object.$ref.startsWith('#/definitions/') ? '#/definitions/' : '#/$defs/';
203
+ const name = object.$ref.slice(prefix.length);
204
+ const source = localDefinition(root, object.$ref);
205
+ if (source === undefined) throw new Error('schema_reference_missing');
206
+ if (!found.has(`${prefix}${name}`)) {
207
+ found.add(`${prefix}${name}`);
208
+ visit(source);
209
+ }
210
+ }
211
+ for (const [key, child] of Object.entries(object)) {
212
+ if (map) {
213
+ visit(child);
214
+ } else if (
215
+ key !== 'definitions' &&
216
+ key !== '$defs' &&
217
+ !LITERAL_KEYS.has(key) &&
218
+ !ANNOTATIONS.has(key)
219
+ ) {
220
+ if (
221
+ SCHEMA_MAP_KEYS.has(key) &&
222
+ child &&
223
+ typeof child === 'object' &&
224
+ !Array.isArray(child)
225
+ ) {
226
+ for (const schema of Object.values(child)) visit(schema);
227
+ } else visit(child);
228
+ }
229
+ }
230
+ };
231
+ for (const [key, child] of Object.entries(root)) {
232
+ if (
233
+ key === 'oneOf' ||
234
+ key === 'definitions' ||
235
+ key === '$defs' ||
236
+ LITERAL_KEYS.has(key) ||
237
+ ANNOTATIONS.has(key)
238
+ )
239
+ continue;
240
+ if (SCHEMA_MAP_KEYS.has(key) && child && typeof child === 'object' && !Array.isArray(child)) {
241
+ for (const schema of Object.values(child)) visit(schema);
242
+ } else visit(child);
243
+ }
244
+ visit(selected);
245
+ for (const reference of [...found].sort()) {
246
+ setLocalDefinition(definitions, reference, normalize(localDefinition(root, reference)));
247
+ }
248
+ const projectedRoot = /** @type {JsonObject} */ (normalize(root));
249
+ delete projectedRoot.oneOf;
250
+ delete projectedRoot.definitions;
251
+ delete projectedRoot.$defs;
252
+ projectedRoot.oneOf = selected.map((/** @type {unknown} */ branch) => normalize(branch));
253
+ projectedRoot.definitions = definitions;
254
+ return { normalized: projectedRoot, methodMissing: false };
255
+ }
256
+
257
+ /** @param {unknown} value @returns {string} */
258
+ export function codexSchemaHash(value) {
259
+ return sha(value);
260
+ }
261
+
262
+ /** @param {string} path @param {number} maximumBytes @returns {Promise<string>} */
263
+ async function readBoundedFile(path, maximumBytes) {
264
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
265
+ try {
266
+ const info = await handle.stat();
267
+ if (!info.isFile() || info.size > maximumBytes) throw new Error('schema_file_too_large');
268
+ const buffer = Buffer.alloc(info.size);
269
+ let offset = 0;
270
+ while (offset < buffer.length) {
271
+ const result = await handle.read(buffer, offset, buffer.length - offset, offset);
272
+ if (result.bytesRead === 0) break;
273
+ offset += result.bytesRead;
274
+ }
275
+ if (offset !== info.size) throw new Error('schema_file_short_read');
276
+ return buffer.toString('utf8');
277
+ } finally {
278
+ await handle.close();
279
+ }
280
+ }
281
+
282
+ /** @param {string} path */
283
+ async function assertOwnedDirectory(path) {
284
+ const info = await lstat(path);
285
+ const uid = process.getuid?.();
286
+ if (!info.isDirectory() || info.isSymbolicLink() || (uid !== undefined && info.uid !== uid))
287
+ throw new Error('schema_directory_invalid');
288
+ }
289
+
290
+ /** @param {string} executable @param {string[]} args @param {NodeJS.ProcessEnv} environment */
291
+ function runBounded(executable, args, environment) {
292
+ return new Promise((resolvePromise) => {
293
+ const child = spawn(executable, args, {
294
+ cwd: environment.CODEX_COMPAT_CWD,
295
+ env: environment,
296
+ stdio: ['ignore', 'pipe', 'pipe'],
297
+ detached: process.platform !== 'win32',
298
+ });
299
+ let output = '';
300
+ let error = '';
301
+ const outputState = { bytes: 0, overflow: false };
302
+ const errorState = { bytes: 0, overflow: false };
303
+ let settled = false;
304
+ /** @type {NodeJS.Timeout|undefined} */
305
+ let timeoutTimer;
306
+ /** @param {number|null} code @param {boolean} [timedOut] */
307
+ const finish = (code, timedOut = false) => {
308
+ if (settled) return;
309
+ settled = true;
310
+ clearTimeout(timer);
311
+ clearTimeout(timeoutTimer);
312
+ resolvePromise({
313
+ code,
314
+ output,
315
+ error,
316
+ outputOverflow: outputState.overflow,
317
+ errorOverflow: errorState.overflow,
318
+ timedOut,
319
+ });
320
+ };
321
+ const killOwnedProcess = () => {
322
+ if (child.pid === undefined) return;
323
+ try {
324
+ if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL');
325
+ else child.kill('SIGKILL');
326
+ } catch {
327
+ try {
328
+ child.kill('SIGKILL');
329
+ } catch {
330
+ // The owned child may have exited between the group and direct kill.
331
+ }
332
+ }
333
+ };
334
+ const timer = setTimeout(() => {
335
+ killOwnedProcess();
336
+ timeoutTimer = setTimeout(() => finish(null, true), 1_000);
337
+ }, 15_000);
338
+ /** @param {string} target @param {Buffer|string} chunk @param {{bytes:number, overflow:boolean}} state */
339
+ const collect = (target, chunk, state) => {
340
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8');
341
+ const remaining = MAX_OUTPUT_BYTES - state.bytes;
342
+ if (remaining <= 0) {
343
+ state.overflow = true;
344
+ return target;
345
+ }
346
+ if (bytes.length > remaining) state.overflow = true;
347
+ const accepted = bytes.subarray(0, remaining);
348
+ state.bytes += accepted.length;
349
+ return `${target}${accepted.toString('utf8')}`;
350
+ };
351
+ child.stdout.on('data', (chunk) => {
352
+ output = collect(output, chunk, outputState);
353
+ });
354
+ child.stderr.on('data', (chunk) => {
355
+ error = collect(error, chunk, errorState);
356
+ });
357
+ child.once('error', () => finish(null));
358
+ child.once('close', (code) => finish(code));
359
+ });
360
+ }
361
+
362
+ /** @param {string} executable @param {NodeJS.ProcessEnv} [baseEnvironment] */
363
+ export async function resolveCodexCompatibility(executable, baseEnvironment = process.env) {
364
+ if (!PAGINATION_CONTRACT_VALID)
365
+ return { status: 'rejected', reason: 'protocol_fingerprint_invalid', version: null };
366
+ const environment = { PATH: baseEnvironment.PATH ?? '', LANG: 'C.UTF-8' };
367
+ const versionRun = await runBounded(executable, ['--version'], {
368
+ ...environment,
369
+ CODEX_COMPAT_CWD: process.cwd(),
370
+ });
371
+ if (versionRun.code !== 0 || versionRun.outputOverflow)
372
+ return { status: 'rejected', reason: 'version_unavailable', version: null };
373
+ const version = parseStableVersion(versionRun.output);
374
+ if (version === null) return { status: 'rejected', reason: 'version_unavailable', version: null };
375
+ if (version.prerelease || version.major !== 0)
376
+ return { status: 'rejected', reason: 'version_not_stable_0x', version: version.text };
377
+ if (QUALIFIED_VERSIONS.has(version.text))
378
+ return { status: 'qualified', reason: 'native_qualified', version: version.text };
379
+ if (version.minor < 163)
380
+ return { status: 'rejected', reason: 'version_unqualified', version: version.text };
381
+ const root = await mkdtemp(join(tmpdir(), 'aibrain-codex-compat-'));
382
+ const schemaDir = join(root, 'schema');
383
+ try {
384
+ const env = { ...environment, CODEX_HOME: join(root, 'codex-home'), CODEX_COMPAT_CWD: root };
385
+ const generated = await runBounded(
386
+ executable,
387
+ ['app-server', 'generate-json-schema', '--experimental', '--out', schemaDir],
388
+ env,
389
+ );
390
+ if (generated.code !== 0)
391
+ return { status: 'rejected', reason: 'schema_generation_failed', version: version.text };
392
+ if (generated.outputOverflow)
393
+ return { status: 'rejected', reason: 'schema_generation_failed', version: version.text };
394
+ /** @type {Record<string, string>} */
395
+ const hashes = {};
396
+ let aggregateBytes = 0;
397
+ await assertOwnedDirectory(schemaDir);
398
+ for (const file of ROOTS) {
399
+ const path = join(schemaDir, file);
400
+ let directory = schemaDir;
401
+ for (const part of file.split('/').slice(0, -1)) {
402
+ directory = join(directory, part);
403
+ const directoryEntry = await lstat(directory).catch(() => null);
404
+ if (directoryEntry === null)
405
+ return {
406
+ status: 'rejected',
407
+ reason: 'schema_root_missing_or_invalid',
408
+ version: version.text,
409
+ };
410
+ await assertOwnedDirectory(directory);
411
+ }
412
+ const entry = await lstat(path).catch(() => null);
413
+ if (
414
+ entry === null ||
415
+ entry.isSymbolicLink() ||
416
+ !entry.isFile() ||
417
+ (process.getuid?.() !== undefined && entry.uid !== process.getuid()) ||
418
+ entry.size > MAX_SCHEMA_BYTES
419
+ )
420
+ return {
421
+ status: 'rejected',
422
+ reason: 'schema_root_missing_or_invalid',
423
+ version: version.text,
424
+ };
425
+ aggregateBytes += entry.size;
426
+ if (aggregateBytes > MAX_SCHEMA_SET_BYTES)
427
+ return { status: 'rejected', reason: 'schema_set_too_large', version: version.text };
428
+ const parsed = JSON.parse(await readBoundedFile(path, MAX_SCHEMA_BYTES));
429
+ assertBoundedSchema(parsed);
430
+ const rootMap = /** @type {Record<string, {methods: string[]|null, sha256: string}>} */ (
431
+ fingerprint.roots
432
+ );
433
+ const expectedRoot = rootMap[file];
434
+ const projection = projectCodexSchema(parsed, expectedRoot.methods);
435
+ if (projection.methodMissing)
436
+ return { status: 'rejected', reason: 'protocol_method_missing', version: version.text };
437
+ hashes[file] = sha(projection.normalized);
438
+ if (hashes[file] !== expectedRoot.sha256)
439
+ return { status: 'rejected', reason: 'protocol_schema_mismatch', version: version.text };
440
+ }
441
+ const help = await runBounded(executable, ['app-server', '--help'], env);
442
+ const terminalHelp = await runBounded(executable, ['--help'], env);
443
+ if (
444
+ help.code !== 0 ||
445
+ help.outputOverflow ||
446
+ !help.output.includes('--listen') ||
447
+ !help.output.includes('stdio://') ||
448
+ terminalHelp.code !== 0 ||
449
+ terminalHelp.outputOverflow ||
450
+ !terminalHelp.output.includes('--remote') ||
451
+ !terminalHelp.output.includes('unix://')
452
+ )
453
+ return { status: 'rejected', reason: 'transport_flags_missing', version: version.text };
454
+ const recheck = await runBounded(executable, ['--version'], {
455
+ ...environment,
456
+ CODEX_COMPAT_CWD: process.cwd(),
457
+ });
458
+ const recheckedVersion = parseStableVersion(recheck.output);
459
+ if (
460
+ recheck.code !== 0 ||
461
+ recheck.outputOverflow ||
462
+ recheckedVersion?.text !== version.text ||
463
+ recheckedVersion?.prerelease
464
+ )
465
+ return { status: 'rejected', reason: 'version_changed_during_probe', version: version.text };
466
+ return {
467
+ status: 'protocol_match',
468
+ reason: 'protocol_fingerprint_match',
469
+ version: version.text,
470
+ };
471
+ } catch {
472
+ return { status: 'rejected', reason: 'protocol_probe_failed', version: version.text };
473
+ } finally {
474
+ await rm(root, { recursive: true, force: true });
475
+ }
476
+ }
477
+
478
+ /** @param {{reason:string, version:string|null}} decision @returns {string} */
479
+ export function codexCompatibilityError(decision) {
480
+ return `codex_protocol_incompatible:${decision.reason}:detected=${decision.version ?? 'unavailable'};run aisac doctor --json`;
481
+ }
@@ -2,14 +2,32 @@
2
2
 
3
3
  import { createHash } from 'node:crypto';
4
4
  import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from 'node:fs/promises';
5
- import { constants } from 'node:fs';
5
+ import { constants, readFileSync } from 'node:fs';
6
6
  import { join, resolve } from 'node:path';
7
7
 
8
8
  export const CODEX_DIAGNOSTIC_SCHEMA_VERSION = 1;
9
- export const CODEX_DIAGNOSTIC_ADAPTER_VERSION = '0.3.0-beta.7';
9
+ const PACKAGE_VERSION_PATTERN =
10
+ /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u;
11
+ function packageVersion() {
12
+ for (const packageUrl of [
13
+ new URL('./package.json', import.meta.url),
14
+ new URL('../package.json', import.meta.url),
15
+ ]) {
16
+ try {
17
+ const value = JSON.parse(readFileSync(packageUrl, 'utf8'));
18
+ if (typeof value?.version === 'string' && PACKAGE_VERSION_PATTERN.test(value.version))
19
+ return value.version;
20
+ } catch {
21
+ // Source and packaged dist use different relative package metadata locations.
22
+ }
23
+ }
24
+ throw new Error('codex_diagnostics_package_version_unavailable');
25
+ }
26
+ export const CODEX_DIAGNOSTIC_ADAPTER_VERSION = packageVersion();
10
27
  export const CODEX_DIAGNOSTIC_MAX_LOG_BYTES = 512 * 1024;
11
28
  export const CODEX_DIAGNOSTIC_MAX_LOG_FILES = 3;
12
29
  export const CODEX_DIAGNOSTIC_MAX_RECORDS = 128;
30
+ const MAX_DIAGNOSTIC_BYTES = 256 * 1024 * 1024;
13
31
 
14
32
  const LOCK_WAIT_MS = 250;
15
33
  const LOCK_STALE_MS = 30_000;
@@ -63,6 +81,14 @@ const REASONS = new Set([
63
81
  'timeout',
64
82
  'thread_closed',
65
83
  'protocol_error',
84
+ 'capture_size_exceeded',
85
+ 'capture_item_count_exceeded',
86
+ 'private_storage_failed',
87
+ 'storage_capacity_exceeded',
88
+ 'capture_lock_timeout',
89
+ 'capture_lock_legacy',
90
+ 'capture_lock_owner_unverifiable',
91
+ 'capture_lock_backend_unavailable',
66
92
  'attached',
67
93
  'uncertain',
68
94
  'disconnected',
@@ -101,6 +127,12 @@ const ERROR_CODES = new Set([
101
127
  'unknown',
102
128
  ]);
103
129
 
130
+ /** @param {unknown} value @returns {number|null} */
131
+ function safeRpcCode(value) {
132
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) return null;
133
+ return value >= -32_768 && value <= 32_767 ? value : null;
134
+ }
135
+
104
136
  /** @param {unknown} value @returns {value is Record<string, unknown>} */
105
137
  function record(value) {
106
138
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -304,6 +336,8 @@ function normalize(event, clientVersion, sessionKey, instanceKey, now) {
304
336
  if (phase !== undefined) output.phase = phase;
305
337
  if (reason !== undefined) output.reason = reason;
306
338
  if (errorCode !== undefined) output.error_code = errorCode;
339
+ const rpcCode = safeRpcCode(input.rpcCode ?? input.rpc_code);
340
+ if (rpcCode !== null) output.rpc_code = rpcCode;
307
341
  if (safeVersion(clientVersion)) output.client_version = clientVersion;
308
342
  const operationId =
309
343
  typeof input.operation_id === 'string' ? input.operation_id : input.operationId;
@@ -317,6 +351,8 @@ function normalize(event, clientVersion, sessionKey, instanceKey, now) {
317
351
  ['batch_count', 'batch_count', 4],
318
352
  ['selectedCount', 'selected_count', 1_000_000],
319
353
  ['deferredCount', 'deferred_count', 1_000_000],
354
+ ['pageCount', 'page_count', 32],
355
+ ['captureBytes', 'capture_bytes', MAX_DIAGNOSTIC_BYTES],
320
356
  ['durationMs', 'duration_ms', 86_400_000],
321
357
  ];
322
358
  for (const [source, target, max] of numericFields) {
@@ -333,7 +369,7 @@ function normalize(event, clientVersion, sessionKey, instanceKey, now) {
333
369
  typeof value === 'number' &&
334
370
  Number.isSafeInteger(value) &&
335
371
  value >= 0 &&
336
- value <= 1_000_000
372
+ value <= (/_bytes$/u.test(key) ? MAX_DIAGNOSTIC_BYTES : 1_000_000)
337
373
  )
338
374
  counts[key] = value;
339
375
  }