@iam-brain/opencode-codex-auth 1.7.0 → 1.8.0
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/cache-lock.d.ts +36 -0
- package/dist/lib/cache-lock.d.ts.map +1 -1
- package/dist/lib/cache-lock.js +149 -3
- package/dist/lib/cache-lock.js.map +1 -1
- package/dist/lib/codex-native/acquire-auth.d.ts +2 -0
- package/dist/lib/codex-native/acquire-auth.d.ts.map +1 -1
- package/dist/lib/codex-native/acquire-auth.js +52 -0
- package/dist/lib/codex-native/acquire-auth.js.map +1 -1
- package/dist/lib/codex-native/oauth-auth-methods.d.ts.map +1 -1
- package/dist/lib/codex-native/oauth-auth-methods.js +3 -2
- package/dist/lib/codex-native/oauth-auth-methods.js.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch.d.ts +2 -0
- package/dist/lib/codex-native/openai-loader-fetch.d.ts.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch.js +87 -1
- package/dist/lib/codex-native/openai-loader-fetch.js.map +1 -1
- package/dist/lib/codex-native/request-transform-model-service-tier.js +1 -1
- package/dist/lib/codex-native/request-transform-model-service-tier.js.map +1 -1
- package/dist/lib/codex-native/request-transform-model.js +1 -1
- package/dist/lib/codex-native/request-transform-model.js.map +1 -1
- package/dist/lib/codex-native.d.ts +1 -0
- package/dist/lib/codex-native.d.ts.map +1 -1
- package/dist/lib/codex-native.js +17 -3
- package/dist/lib/codex-native.js.map +1 -1
- package/dist/lib/config/file.d.ts.map +1 -1
- package/dist/lib/config/file.js +3 -0
- package/dist/lib/config/file.js.map +1 -1
- package/dist/lib/config/resolve.d.ts +1 -0
- package/dist/lib/config/resolve.d.ts.map +1 -1
- package/dist/lib/config/resolve.js +5 -0
- package/dist/lib/config/resolve.js.map +1 -1
- package/dist/lib/config/types.d.ts +3 -2
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/config/types.js +11 -5
- package/dist/lib/config/types.js.map +1 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/model-catalog/shared.d.ts +2 -2
- package/dist/lib/model-catalog/shared.d.ts.map +1 -1
- package/dist/lib/model-catalog/shared.js +4 -8
- package/dist/lib/model-catalog/shared.js.map +1 -1
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +4 -0
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/shareable-debug.d.ts +105 -0
- package/dist/lib/shareable-debug.d.ts.map +1 -0
- package/dist/lib/shareable-debug.js +1071 -0
- package/dist/lib/shareable-debug.js.map +1 -0
- package/package.json +2 -2
- package/schemas/codex-config.schema.json +3 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import fsSync from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { enforceOwnerOnlyPermissions, isFsErrorCode, readJsonFileBestEffort, writeJsonFileAtomic, writeJsonFileAtomicBestEffort } from "./cache-io.js";
|
|
6
|
+
import { defaultShareableDebugLogPath } from "./paths.js";
|
|
7
|
+
const PROCESS_SECRET = randomBytes(32);
|
|
8
|
+
const DEFAULT_SUMMARY_MAX_BYTES = 256 * 1024;
|
|
9
|
+
const DEFAULT_SUMMARY_MAX_FILES = 2;
|
|
10
|
+
const DEFAULT_SEGMENT_MAX_BYTES = 16 * 1024;
|
|
11
|
+
const DEFAULT_ROLLING_BUFFER_MAX_BYTES = 256 * 1024;
|
|
12
|
+
const DEFAULT_PRE_TRIGGER_EVENT_COUNT = 40;
|
|
13
|
+
const DEFAULT_POST_TRIGGER_EVENT_COUNT = 20;
|
|
14
|
+
const DEFAULT_MAX_INCIDENT_FILES = 8;
|
|
15
|
+
const DEFAULT_MAX_INCIDENT_BYTES = 512 * 1024;
|
|
16
|
+
function pseudonym(prefix, raw) {
|
|
17
|
+
const normalized = raw?.trim();
|
|
18
|
+
if (!normalized)
|
|
19
|
+
return undefined;
|
|
20
|
+
const digest = createHmac("sha256", PROCESS_SECRET).update(normalized).digest("hex").slice(0, 8);
|
|
21
|
+
return `${prefix}_${digest}`;
|
|
22
|
+
}
|
|
23
|
+
function normalizeEndpoint(input) {
|
|
24
|
+
if (!input)
|
|
25
|
+
return undefined;
|
|
26
|
+
try {
|
|
27
|
+
return new URL(input).pathname || undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function extractPromptCacheKey(request) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await request.clone().text();
|
|
36
|
+
if (!raw)
|
|
37
|
+
return undefined;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
41
|
+
const candidate = parsed.prompt_cache_key;
|
|
42
|
+
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate : undefined;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
const params = new URLSearchParams(raw);
|
|
48
|
+
const candidate = params.get("prompt_cache_key");
|
|
49
|
+
return candidate && candidate.trim().length > 0 ? candidate : undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function createNoopShareableDebugLogger() {
|
|
57
|
+
const noop = async () => { };
|
|
58
|
+
return {
|
|
59
|
+
enabled: false,
|
|
60
|
+
emitRotationBegin: noop,
|
|
61
|
+
emitRotationDecision: noop,
|
|
62
|
+
emitRotationCandidateSelected: noop,
|
|
63
|
+
emitFetchAttemptRequest: noop,
|
|
64
|
+
emitFetchAttemptResponse: noop,
|
|
65
|
+
emitRetryAfter429: noop,
|
|
66
|
+
emitAuthFailure: noop,
|
|
67
|
+
emitSyntheticFatalError: noop
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function defaultStateDirForLogPath(filePath) {
|
|
71
|
+
const parsed = path.parse(filePath);
|
|
72
|
+
return path.join(parsed.dir, `${parsed.name}-state`);
|
|
73
|
+
}
|
|
74
|
+
function parsePositiveInteger(value, fallback) {
|
|
75
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
76
|
+
return fallback;
|
|
77
|
+
return Math.max(1, Math.floor(value));
|
|
78
|
+
}
|
|
79
|
+
function segmentFileName(startSeq) {
|
|
80
|
+
return `segment-${startSeq.toString().padStart(16, "0")}.jsonl`;
|
|
81
|
+
}
|
|
82
|
+
function incidentFileName(incidentId, timestamp) {
|
|
83
|
+
const stamp = timestamp.replaceAll(/[:.]/g, "-");
|
|
84
|
+
return `incident-${stamp}-${incidentId}.jsonl`;
|
|
85
|
+
}
|
|
86
|
+
function sortLexicallyAscending(values) {
|
|
87
|
+
return [...values].sort((left, right) => left.localeCompare(right));
|
|
88
|
+
}
|
|
89
|
+
function incidentFileReference(filePath) {
|
|
90
|
+
return path.basename(filePath);
|
|
91
|
+
}
|
|
92
|
+
function contiguousWindow(rows, startSeq, maxCount) {
|
|
93
|
+
const deduped = Array.from(new Map(rows.map((row) => [row.seq, row])).values()).sort((left, right) => left.seq - right.seq);
|
|
94
|
+
const window = [];
|
|
95
|
+
let expectedSeq = startSeq;
|
|
96
|
+
for (const row of deduped) {
|
|
97
|
+
if (window.length >= maxCount)
|
|
98
|
+
break;
|
|
99
|
+
if (row.seq < expectedSeq)
|
|
100
|
+
continue;
|
|
101
|
+
if (row.seq > expectedSeq) {
|
|
102
|
+
return {
|
|
103
|
+
rows: window,
|
|
104
|
+
hasGapAfterPrefix: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
window.push(row);
|
|
108
|
+
expectedSeq += 1;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
rows: window,
|
|
112
|
+
hasGapAfterPrefix: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function parseSegmentStartSeq(filePath) {
|
|
116
|
+
const match = /^segment-(\d+)\.jsonl$/u.exec(path.basename(filePath));
|
|
117
|
+
if (!match)
|
|
118
|
+
return undefined;
|
|
119
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
120
|
+
return Number.isFinite(value) && value >= 1 ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
async function readJsonlFile(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
125
|
+
const lines = raw
|
|
126
|
+
.split("\n")
|
|
127
|
+
.map((line) => line.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
const records = [];
|
|
130
|
+
let hadTruncatedTail = false;
|
|
131
|
+
for (const [index, line] of lines.entries()) {
|
|
132
|
+
try {
|
|
133
|
+
records.push(JSON.parse(line));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (index === lines.length - 1) {
|
|
137
|
+
hadTruncatedTail = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
records,
|
|
145
|
+
hadTruncatedTail
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (isFsErrorCode(error, "ENOENT")) {
|
|
150
|
+
return {
|
|
151
|
+
records: [],
|
|
152
|
+
hadTruncatedTail: false
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function readJsonlFileSync(filePath) {
|
|
159
|
+
try {
|
|
160
|
+
const raw = fsSync.readFileSync(filePath, "utf8");
|
|
161
|
+
const lines = raw
|
|
162
|
+
.split("\n")
|
|
163
|
+
.map((line) => line.trim())
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
const records = [];
|
|
166
|
+
let hadTruncatedTail = false;
|
|
167
|
+
for (const [index, line] of lines.entries()) {
|
|
168
|
+
try {
|
|
169
|
+
records.push(JSON.parse(line));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (index === lines.length - 1) {
|
|
173
|
+
hadTruncatedTail = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
records,
|
|
181
|
+
hadTruncatedTail
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (isFsErrorCode(error, "ENOENT")) {
|
|
186
|
+
return {
|
|
187
|
+
records: [],
|
|
188
|
+
hadTruncatedTail: false
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function readJsonlRecords(filePath) {
|
|
195
|
+
return (await readJsonlFile(filePath)).records;
|
|
196
|
+
}
|
|
197
|
+
function readJsonlRecordsSync(filePath) {
|
|
198
|
+
return readJsonlFileSync(filePath).records;
|
|
199
|
+
}
|
|
200
|
+
async function lastSeqFromSegment(filePath) {
|
|
201
|
+
const rows = await readJsonlRecords(filePath);
|
|
202
|
+
const last = rows.at(-1);
|
|
203
|
+
return typeof last?.seq === "number" ? last.seq : 0;
|
|
204
|
+
}
|
|
205
|
+
async function appendJsonlLine(filePath, line) {
|
|
206
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
207
|
+
await fs.appendFile(filePath, line, { mode: 0o600 });
|
|
208
|
+
await enforceOwnerOnlyPermissions(filePath);
|
|
209
|
+
}
|
|
210
|
+
async function writeJsonlLines(filePath, lines) {
|
|
211
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
212
|
+
await fs.writeFile(filePath, lines.join(""), { mode: 0o600 });
|
|
213
|
+
await enforceOwnerOnlyPermissions(filePath);
|
|
214
|
+
}
|
|
215
|
+
function appendJsonlLineSync(filePath, line) {
|
|
216
|
+
fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
217
|
+
fsSync.appendFileSync(filePath, line, { mode: 0o600 });
|
|
218
|
+
try {
|
|
219
|
+
fsSync.chmodSync(filePath, 0o600);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// best-effort only in crash path
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function writeJsonlLinesSync(filePath, lines) {
|
|
226
|
+
fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
227
|
+
fsSync.writeFileSync(filePath, lines.join(""), { mode: 0o600 });
|
|
228
|
+
try {
|
|
229
|
+
fsSync.chmodSync(filePath, 0o600);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// best-effort only in crash path
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function rotateLogFileIfNeeded(filePath, nextLineBytes, maxBytes, maxFiles) {
|
|
236
|
+
const keepFiles = Math.max(1, maxFiles);
|
|
237
|
+
let currentSize = 0;
|
|
238
|
+
try {
|
|
239
|
+
currentSize = (await fs.stat(filePath)).size;
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
if (currentSize === 0 || currentSize + nextLineBytes <= maxBytes)
|
|
246
|
+
return;
|
|
247
|
+
for (let index = keepFiles - 1; index >= 1; index -= 1) {
|
|
248
|
+
const source = index === 1 ? filePath : `${filePath}.${index - 1}`;
|
|
249
|
+
const dest = `${filePath}.${index}`;
|
|
250
|
+
try {
|
|
251
|
+
await deleteFileIfExists(dest);
|
|
252
|
+
await fs.rename(source, dest);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function deleteFileIfExists(filePath) {
|
|
261
|
+
try {
|
|
262
|
+
await fs.unlink(filePath);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isIncidentManifest(value) {
|
|
270
|
+
return (typeof value === "object" &&
|
|
271
|
+
value !== null &&
|
|
272
|
+
"version" in value &&
|
|
273
|
+
value.version === 1 &&
|
|
274
|
+
"incidentId" in value &&
|
|
275
|
+
typeof value.incidentId === "string" &&
|
|
276
|
+
"outputFilePath" in value &&
|
|
277
|
+
typeof value.outputFilePath === "string" &&
|
|
278
|
+
"triggerSeq" in value &&
|
|
279
|
+
typeof value.triggerSeq === "number" &&
|
|
280
|
+
"triggerEvent" in value &&
|
|
281
|
+
typeof value.triggerEvent === "string" &&
|
|
282
|
+
"triggerReason" in value &&
|
|
283
|
+
typeof value.triggerReason === "string" &&
|
|
284
|
+
"preTriggerStartSeq" in value &&
|
|
285
|
+
typeof value.preTriggerStartSeq === "number" &&
|
|
286
|
+
"preTriggerEndSeq" in value &&
|
|
287
|
+
typeof value.preTriggerEndSeq === "number" &&
|
|
288
|
+
"postWindowCount" in value &&
|
|
289
|
+
typeof value.postWindowCount === "number" &&
|
|
290
|
+
"postRemaining" in value &&
|
|
291
|
+
typeof value.postRemaining === "number" &&
|
|
292
|
+
"status" in value &&
|
|
293
|
+
value.status === "open" &&
|
|
294
|
+
"createdAt" in value &&
|
|
295
|
+
typeof value.createdAt === "string" &&
|
|
296
|
+
"updatedAt" in value &&
|
|
297
|
+
typeof value.updatedAt === "string");
|
|
298
|
+
}
|
|
299
|
+
function isManifestlessRecoveryMarker(value) {
|
|
300
|
+
return (typeof value === "object" &&
|
|
301
|
+
value !== null &&
|
|
302
|
+
"version" in value &&
|
|
303
|
+
value.version === 1 &&
|
|
304
|
+
"triggerSeq" in value &&
|
|
305
|
+
typeof value.triggerSeq === "number");
|
|
306
|
+
}
|
|
307
|
+
export function createShareableDebugLogger(input) {
|
|
308
|
+
if (!input.enabled)
|
|
309
|
+
return createNoopShareableDebugLogger();
|
|
310
|
+
const filePath = input.filePath ?? defaultShareableDebugLogPath(input.env);
|
|
311
|
+
const stateDir = input.stateDir ?? defaultStateDirForLogPath(filePath);
|
|
312
|
+
const segmentsDir = path.join(stateDir, "segments");
|
|
313
|
+
const incidentsDir = path.join(stateDir, "incidents");
|
|
314
|
+
const manifestPath = path.join(stateDir, "incident-state.json");
|
|
315
|
+
const manifestlessRecoveryPath = path.join(stateDir, "manifestless-recovery.json");
|
|
316
|
+
const summaryMaxBytes = parsePositiveInteger(input.incidentConfig?.summaryMaxBytes, DEFAULT_SUMMARY_MAX_BYTES);
|
|
317
|
+
const summaryMaxFiles = parsePositiveInteger(input.incidentConfig?.summaryMaxFiles, DEFAULT_SUMMARY_MAX_FILES);
|
|
318
|
+
const segmentMaxBytes = parsePositiveInteger(input.incidentConfig?.segmentMaxBytes, DEFAULT_SEGMENT_MAX_BYTES);
|
|
319
|
+
const rollingBufferMaxBytes = parsePositiveInteger(input.incidentConfig?.rollingBufferMaxBytes, DEFAULT_ROLLING_BUFFER_MAX_BYTES);
|
|
320
|
+
const preTriggerEventCount = parsePositiveInteger(input.incidentConfig?.preTriggerEventCount, DEFAULT_PRE_TRIGGER_EVENT_COUNT);
|
|
321
|
+
const postTriggerEventCount = parsePositiveInteger(input.incidentConfig?.postTriggerEventCount, DEFAULT_POST_TRIGGER_EVENT_COUNT);
|
|
322
|
+
const maxIncidentFiles = parsePositiveInteger(input.incidentConfig?.maxIncidentFiles, DEFAULT_MAX_INCIDENT_FILES);
|
|
323
|
+
const maxIncidentBytes = parsePositiveInteger(input.incidentConfig?.maxIncidentBytes, DEFAULT_MAX_INCIDENT_BYTES);
|
|
324
|
+
let nextSeq = 1;
|
|
325
|
+
let currentSegmentPath;
|
|
326
|
+
let currentSegmentBytes = 0;
|
|
327
|
+
let openIncident;
|
|
328
|
+
let pendingWrite = Promise.resolve();
|
|
329
|
+
let initPromise = Promise.resolve();
|
|
330
|
+
let handlersRegistered = false;
|
|
331
|
+
let signalInFlight = false;
|
|
332
|
+
const sanitizeJsonlTail = async (targetPath) => {
|
|
333
|
+
const result = await readJsonlFile(targetPath);
|
|
334
|
+
if (result.hadTruncatedTail) {
|
|
335
|
+
await writeJsonlLines(targetPath, result.records.map((record) => `${JSON.stringify(record)}\n`));
|
|
336
|
+
}
|
|
337
|
+
return result.records;
|
|
338
|
+
};
|
|
339
|
+
const appendSummaryLifecycleSync = (event, payload) => {
|
|
340
|
+
appendSummaryLineSync(`${JSON.stringify({ timestamp: new Date().toISOString(), event, ...payload })}\n`);
|
|
341
|
+
};
|
|
342
|
+
const reconcileIncidentFile = async (incident) => {
|
|
343
|
+
let incidentRecords;
|
|
344
|
+
try {
|
|
345
|
+
incidentRecords = await sanitizeJsonlTail(incident.outputFilePath);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (isFsErrorCode(error, "ENOENT")) {
|
|
349
|
+
incidentRecords = [];
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
await sealIncidentIncomplete({
|
|
353
|
+
incident,
|
|
354
|
+
reason: "interrupted"
|
|
355
|
+
});
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const baseRowsFromSegments = await readEventsInRange(incident.preTriggerStartSeq, incident.preTriggerEndSeq);
|
|
360
|
+
const baseRowsFromIncident = incidentRecords.filter((row) => typeof row.seq === "number" && row.seq >= incident.preTriggerStartSeq && row.seq <= incident.triggerSeq);
|
|
361
|
+
const expectedBaseCount = incident.triggerSeq - incident.preTriggerStartSeq + 1;
|
|
362
|
+
const baseWindow = contiguousWindow([...baseRowsFromIncident, ...baseRowsFromSegments], incident.preTriggerStartSeq, expectedBaseCount);
|
|
363
|
+
if (baseWindow.hasGapAfterPrefix ||
|
|
364
|
+
baseWindow.rows.length !== expectedBaseCount ||
|
|
365
|
+
!baseWindow.rows.some((row) => row.seq === incident.triggerSeq && row.event === incident.triggerEvent)) {
|
|
366
|
+
await sealIncidentIncomplete({
|
|
367
|
+
incident,
|
|
368
|
+
reason: "interrupted"
|
|
369
|
+
});
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
const closedRow = incidentRecords.find((row) => row.event === "incident_closed" && row.incidentId === incident.incidentId);
|
|
373
|
+
if (closedRow) {
|
|
374
|
+
await deleteFileIfExists(manifestPath);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const postRowsFromSegments = await readEventsInRange(incident.triggerSeq + 1, incident.triggerSeq + incident.postWindowCount);
|
|
378
|
+
const recoveredPostWindow = contiguousWindow([
|
|
379
|
+
...incidentRecords.filter((row) => typeof row.seq === "number" && row.seq > incident.triggerSeq && row.event !== "incident_closed"),
|
|
380
|
+
...postRowsFromSegments
|
|
381
|
+
], incident.triggerSeq + 1, incident.postWindowCount);
|
|
382
|
+
if (recoveredPostWindow.hasGapAfterPrefix) {
|
|
383
|
+
await sealIncidentIncomplete({
|
|
384
|
+
incident,
|
|
385
|
+
reason: "interrupted"
|
|
386
|
+
});
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
const recoveredPostRows = recoveredPostWindow.rows;
|
|
390
|
+
await writeJsonlLines(incident.outputFilePath, [...baseWindow.rows, ...recoveredPostRows].map((row) => `${JSON.stringify(row)}\n`));
|
|
391
|
+
const postRemaining = Math.max(0, incident.postWindowCount - recoveredPostRows.length);
|
|
392
|
+
const recoveredIncident = {
|
|
393
|
+
...incident,
|
|
394
|
+
postRemaining,
|
|
395
|
+
updatedAt: new Date().toISOString()
|
|
396
|
+
};
|
|
397
|
+
if (recoveredIncident.postRemaining <= 0) {
|
|
398
|
+
await closeIncident(recoveredIncident, "recovered");
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
await writeManifest(recoveredIncident);
|
|
402
|
+
await appendSummaryLifecycle("incident_recovered", {
|
|
403
|
+
incidentId: recoveredIncident.incidentId,
|
|
404
|
+
incidentFile: incidentFileReference(recoveredIncident.outputFilePath),
|
|
405
|
+
triggerSeq: recoveredIncident.triggerSeq,
|
|
406
|
+
triggerEvent: recoveredIncident.triggerEvent,
|
|
407
|
+
postRemaining: recoveredIncident.postRemaining
|
|
408
|
+
});
|
|
409
|
+
return recoveredIncident;
|
|
410
|
+
};
|
|
411
|
+
const initializeState = async () => {
|
|
412
|
+
await fs.mkdir(segmentsDir, { recursive: true });
|
|
413
|
+
await fs.mkdir(incidentsDir, { recursive: true });
|
|
414
|
+
const segmentFiles = sortLexicallyAscending(await fs.readdir(segmentsDir).catch(() => []));
|
|
415
|
+
const latestSegment = segmentFiles.at(-1);
|
|
416
|
+
let latestSegmentRecord;
|
|
417
|
+
if (latestSegment) {
|
|
418
|
+
currentSegmentPath = path.join(segmentsDir, latestSegment);
|
|
419
|
+
const latestSegmentRows = await sanitizeJsonlTail(currentSegmentPath);
|
|
420
|
+
currentSegmentBytes = (await fs.stat(currentSegmentPath)).size;
|
|
421
|
+
const segmentStartSeq = parseSegmentStartSeq(currentSegmentPath) ?? 1;
|
|
422
|
+
nextSeq = Math.max(segmentStartSeq, (await lastSeqFromSegment(currentSegmentPath)) + 1);
|
|
423
|
+
latestSegmentRecord = latestSegmentRows.at(-1);
|
|
424
|
+
}
|
|
425
|
+
const manifest = await readJsonFileBestEffort(manifestPath);
|
|
426
|
+
const manifestlessRecovery = await readJsonFileBestEffort(manifestlessRecoveryPath);
|
|
427
|
+
if (isManifestlessRecoveryMarker(manifestlessRecovery) &&
|
|
428
|
+
manifestlessRecovery.triggerSeq !== latestSegmentRecord?.seq) {
|
|
429
|
+
await deleteFileIfExists(manifestlessRecoveryPath);
|
|
430
|
+
}
|
|
431
|
+
if (!isIncidentManifest(manifest)) {
|
|
432
|
+
await deleteFileIfExists(manifestPath);
|
|
433
|
+
const triggerReason = latestSegmentRecord ? triggerReasonFor(latestSegmentRecord) : undefined;
|
|
434
|
+
if (latestSegmentRecord &&
|
|
435
|
+
triggerReason &&
|
|
436
|
+
(!isManifestlessRecoveryMarker(manifestlessRecovery) ||
|
|
437
|
+
manifestlessRecovery.triggerSeq !== latestSegmentRecord.seq)) {
|
|
438
|
+
const incident = createIncidentManifest(latestSegmentRecord, triggerReason);
|
|
439
|
+
const preTriggerEvents = await readEventsInRange(incident.preTriggerStartSeq, incident.preTriggerEndSeq);
|
|
440
|
+
const expectedBaseCount = incident.triggerSeq - incident.preTriggerStartSeq + 1;
|
|
441
|
+
const baseWindow = contiguousWindow(preTriggerEvents, incident.preTriggerStartSeq, expectedBaseCount);
|
|
442
|
+
await writeJsonlLines(incident.outputFilePath, baseWindow.rows.map((entry) => `${JSON.stringify(entry)}\n`));
|
|
443
|
+
if (baseWindow.hasGapAfterPrefix || baseWindow.rows.length !== expectedBaseCount) {
|
|
444
|
+
await writeJsonFileAtomic(manifestlessRecoveryPath, {
|
|
445
|
+
version: 1,
|
|
446
|
+
triggerSeq: latestSegmentRecord.seq
|
|
447
|
+
});
|
|
448
|
+
await sealIncidentIncomplete({
|
|
449
|
+
incident,
|
|
450
|
+
reason: "interrupted"
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
await writeManifest(incident);
|
|
455
|
+
await deleteFileIfExists(manifestlessRecoveryPath);
|
|
456
|
+
await appendSummaryLifecycle("incident_recovered", {
|
|
457
|
+
incidentId: incident.incidentId,
|
|
458
|
+
incidentFile: incidentFileReference(incident.outputFilePath),
|
|
459
|
+
triggerSeq: incident.triggerSeq,
|
|
460
|
+
triggerEvent: incident.triggerEvent,
|
|
461
|
+
postRemaining: incident.postRemaining
|
|
462
|
+
});
|
|
463
|
+
openIncident = incident;
|
|
464
|
+
await pruneIncidents();
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
openIncident = await reconcileIncidentFile(manifest);
|
|
469
|
+
};
|
|
470
|
+
const runQueued = (work) => {
|
|
471
|
+
pendingWrite = pendingWrite
|
|
472
|
+
.then(() => initPromise)
|
|
473
|
+
.then(work)
|
|
474
|
+
.catch((error) => {
|
|
475
|
+
input.log?.warn("shareable debug write failed", {
|
|
476
|
+
error: error instanceof Error ? error.message : String(error)
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
return pendingWrite;
|
|
480
|
+
};
|
|
481
|
+
const writeManifest = async (manifest) => {
|
|
482
|
+
await writeJsonFileAtomic(manifestPath, manifest);
|
|
483
|
+
};
|
|
484
|
+
const writeManifestBestEffort = async (manifest) => {
|
|
485
|
+
await writeJsonFileAtomicBestEffort(manifestPath, manifest);
|
|
486
|
+
};
|
|
487
|
+
const appendSummaryLine = async (line) => {
|
|
488
|
+
await rotateLogFileIfNeeded(filePath, Buffer.byteLength(line), summaryMaxBytes, summaryMaxFiles);
|
|
489
|
+
await appendJsonlLine(filePath, line);
|
|
490
|
+
};
|
|
491
|
+
const appendSummaryLifecycle = async (event, payload) => {
|
|
492
|
+
const line = `${JSON.stringify({ timestamp: new Date().toISOString(), event, ...payload })}\n`;
|
|
493
|
+
await appendSummaryLine(line);
|
|
494
|
+
};
|
|
495
|
+
const appendSummaryLineSync = (line) => {
|
|
496
|
+
try {
|
|
497
|
+
const existingSize = fsSync.existsSync(filePath) ? fsSync.statSync(filePath).size : 0;
|
|
498
|
+
if (existingSize > 0 && existingSize + Buffer.byteLength(line) > summaryMaxBytes) {
|
|
499
|
+
for (let index = summaryMaxFiles - 1; index >= 1; index -= 1) {
|
|
500
|
+
const source = index === 1 ? filePath : `${filePath}.${index - 1}`;
|
|
501
|
+
const dest = `${filePath}.${index}`;
|
|
502
|
+
if (fsSync.existsSync(source)) {
|
|
503
|
+
if (fsSync.existsSync(dest)) {
|
|
504
|
+
fsSync.unlinkSync(dest);
|
|
505
|
+
}
|
|
506
|
+
fsSync.renameSync(source, dest);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
appendJsonlLineSync(filePath, line);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// best-effort only in crash path
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const ensureSegmentForLine = async (lineBytes, seq) => {
|
|
517
|
+
if (!currentSegmentPath || currentSegmentBytes + lineBytes > segmentMaxBytes) {
|
|
518
|
+
currentSegmentPath = path.join(segmentsDir, segmentFileName(seq));
|
|
519
|
+
currentSegmentBytes = 0;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const pruneSegments = async () => {
|
|
523
|
+
const files = sortLexicallyAscending(await fs.readdir(segmentsDir));
|
|
524
|
+
let totalBytes = 0;
|
|
525
|
+
const entries = [];
|
|
526
|
+
for (const name of files) {
|
|
527
|
+
const filePath = path.join(segmentsDir, name);
|
|
528
|
+
const stat = await fs.stat(filePath);
|
|
529
|
+
entries.push({ name, filePath, size: stat.size });
|
|
530
|
+
totalBytes += stat.size;
|
|
531
|
+
}
|
|
532
|
+
for (const entry of entries) {
|
|
533
|
+
if (totalBytes <= rollingBufferMaxBytes)
|
|
534
|
+
break;
|
|
535
|
+
if (entry.filePath === currentSegmentPath)
|
|
536
|
+
continue;
|
|
537
|
+
await deleteFileIfExists(entry.filePath);
|
|
538
|
+
totalBytes -= entry.size;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const pruneIncidents = async () => {
|
|
542
|
+
const incidentFiles = sortLexicallyAscending(await fs.readdir(incidentsDir).catch(() => []));
|
|
543
|
+
const entries = [];
|
|
544
|
+
let totalBytes = 0;
|
|
545
|
+
for (const name of incidentFiles) {
|
|
546
|
+
const filePath = path.join(incidentsDir, name);
|
|
547
|
+
const stat = await fs.stat(filePath);
|
|
548
|
+
entries.push({ filePath, name, size: stat.size });
|
|
549
|
+
totalBytes += stat.size;
|
|
550
|
+
}
|
|
551
|
+
for (const entry of entries) {
|
|
552
|
+
if (entries.length <= maxIncidentFiles && totalBytes <= maxIncidentBytes)
|
|
553
|
+
break;
|
|
554
|
+
if (entry.filePath === openIncident?.outputFilePath)
|
|
555
|
+
continue;
|
|
556
|
+
await deleteFileIfExists(entry.filePath);
|
|
557
|
+
totalBytes -= entry.size;
|
|
558
|
+
const index = entries.findIndex((candidate) => candidate.filePath === entry.filePath);
|
|
559
|
+
if (index >= 0) {
|
|
560
|
+
entries.splice(index, 1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const appendToSegment = async (record) => {
|
|
565
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
566
|
+
const lineBytes = Buffer.byteLength(line);
|
|
567
|
+
await ensureSegmentForLine(lineBytes, record.seq);
|
|
568
|
+
if (!currentSegmentPath) {
|
|
569
|
+
throw new Error("shareable_debug_missing_segment_path");
|
|
570
|
+
}
|
|
571
|
+
await appendJsonlLine(currentSegmentPath, line);
|
|
572
|
+
currentSegmentBytes += lineBytes;
|
|
573
|
+
await pruneSegments();
|
|
574
|
+
};
|
|
575
|
+
const appendToSegmentSync = (record) => {
|
|
576
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
577
|
+
const lineBytes = Buffer.byteLength(line);
|
|
578
|
+
if (!currentSegmentPath || currentSegmentBytes + lineBytes > segmentMaxBytes) {
|
|
579
|
+
currentSegmentPath = path.join(segmentsDir, segmentFileName(record.seq));
|
|
580
|
+
currentSegmentBytes = 0;
|
|
581
|
+
}
|
|
582
|
+
appendJsonlLineSync(currentSegmentPath, line);
|
|
583
|
+
currentSegmentBytes += lineBytes;
|
|
584
|
+
};
|
|
585
|
+
const readEventsInRange = async (startSeq, endSeq) => {
|
|
586
|
+
const files = sortLexicallyAscending(await fs.readdir(segmentsDir));
|
|
587
|
+
const events = [];
|
|
588
|
+
for (const name of files) {
|
|
589
|
+
const rows = await readJsonlRecords(path.join(segmentsDir, name));
|
|
590
|
+
for (const row of rows) {
|
|
591
|
+
if (typeof row.seq !== "number")
|
|
592
|
+
continue;
|
|
593
|
+
if (row.seq < startSeq || row.seq > endSeq)
|
|
594
|
+
continue;
|
|
595
|
+
events.push(row);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return events.sort((left, right) => left.seq - right.seq);
|
|
599
|
+
};
|
|
600
|
+
const readEventsInRangeSync = (startSeq, endSeq) => {
|
|
601
|
+
const files = sortLexicallyAscending(fsSync.readdirSync(segmentsDir, "utf8"));
|
|
602
|
+
const events = [];
|
|
603
|
+
for (const name of files) {
|
|
604
|
+
const rows = readJsonlRecordsSync(path.join(segmentsDir, name));
|
|
605
|
+
for (const row of rows) {
|
|
606
|
+
if (typeof row.seq !== "number")
|
|
607
|
+
continue;
|
|
608
|
+
if (row.seq < startSeq || row.seq > endSeq)
|
|
609
|
+
continue;
|
|
610
|
+
events.push(row);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return events.sort((left, right) => left.seq - right.seq);
|
|
614
|
+
};
|
|
615
|
+
const createIncidentManifest = (record, triggerReason) => {
|
|
616
|
+
const incidentId = randomUUID();
|
|
617
|
+
const timestamp = new Date().toISOString();
|
|
618
|
+
return {
|
|
619
|
+
version: 1,
|
|
620
|
+
incidentId,
|
|
621
|
+
outputFilePath: path.join(incidentsDir, incidentFileName(incidentId, timestamp)),
|
|
622
|
+
triggerSeq: record.seq,
|
|
623
|
+
triggerEvent: record.event,
|
|
624
|
+
triggerReason,
|
|
625
|
+
preTriggerStartSeq: Math.max(1, record.seq - preTriggerEventCount),
|
|
626
|
+
preTriggerEndSeq: record.seq,
|
|
627
|
+
postWindowCount: postTriggerEventCount,
|
|
628
|
+
postRemaining: postTriggerEventCount,
|
|
629
|
+
status: "open",
|
|
630
|
+
createdAt: timestamp,
|
|
631
|
+
updatedAt: timestamp
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
const appendIncidentLine = async (incident, record) => {
|
|
635
|
+
await appendJsonlLine(incident.outputFilePath, `${JSON.stringify(record)}\n`);
|
|
636
|
+
};
|
|
637
|
+
const appendIncidentLifecycle = async (incident, event, payload) => {
|
|
638
|
+
await appendJsonlLine(incident.outputFilePath, `${JSON.stringify({
|
|
639
|
+
seq: nextSeq++,
|
|
640
|
+
timestamp: new Date().toISOString(),
|
|
641
|
+
event,
|
|
642
|
+
incidentId: incident.incidentId,
|
|
643
|
+
...payload
|
|
644
|
+
})}\n`);
|
|
645
|
+
};
|
|
646
|
+
const appendIncidentLifecycleSync = (incident, event, payload) => {
|
|
647
|
+
appendJsonlLineSync(incident.outputFilePath, `${JSON.stringify({
|
|
648
|
+
seq: nextSeq++,
|
|
649
|
+
timestamp: new Date().toISOString(),
|
|
650
|
+
event,
|
|
651
|
+
incidentId: incident.incidentId,
|
|
652
|
+
...payload
|
|
653
|
+
})}\n`);
|
|
654
|
+
};
|
|
655
|
+
const closeIncident = async (incident, reason) => {
|
|
656
|
+
await appendIncidentLifecycle(incident, "incident_closed", {
|
|
657
|
+
reason,
|
|
658
|
+
triggerSeq: incident.triggerSeq,
|
|
659
|
+
triggerEvent: incident.triggerEvent,
|
|
660
|
+
incomplete: false
|
|
661
|
+
});
|
|
662
|
+
await appendSummaryLifecycle("incident_closed", {
|
|
663
|
+
incidentId: incident.incidentId,
|
|
664
|
+
incidentFile: incidentFileReference(incident.outputFilePath),
|
|
665
|
+
triggerSeq: incident.triggerSeq,
|
|
666
|
+
triggerEvent: incident.triggerEvent,
|
|
667
|
+
reason
|
|
668
|
+
});
|
|
669
|
+
await deleteFileIfExists(manifestPath);
|
|
670
|
+
openIncident = undefined;
|
|
671
|
+
await pruneIncidents();
|
|
672
|
+
};
|
|
673
|
+
const sealIncidentIncomplete = async (inputState) => {
|
|
674
|
+
try {
|
|
675
|
+
await sanitizeJsonlTail(inputState.incident.outputFilePath);
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (isFsErrorCode(error, "ENOENT")) {
|
|
679
|
+
await writeJsonlLines(inputState.incident.outputFilePath, []);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
await writeJsonFileAtomic(manifestlessRecoveryPath, {
|
|
683
|
+
version: 1,
|
|
684
|
+
triggerSeq: inputState.incident.triggerSeq
|
|
685
|
+
});
|
|
686
|
+
await appendIncidentLifecycle(inputState.incident, "incident_closed", {
|
|
687
|
+
reason: inputState.reason,
|
|
688
|
+
triggerSeq: inputState.incident.triggerSeq,
|
|
689
|
+
triggerEvent: inputState.incident.triggerEvent,
|
|
690
|
+
incomplete: true
|
|
691
|
+
});
|
|
692
|
+
await appendSummaryLifecycle("incident_closed", {
|
|
693
|
+
incidentId: inputState.incident.incidentId,
|
|
694
|
+
incidentFile: incidentFileReference(inputState.incident.outputFilePath),
|
|
695
|
+
triggerSeq: inputState.incident.triggerSeq,
|
|
696
|
+
triggerEvent: inputState.incident.triggerEvent,
|
|
697
|
+
reason: inputState.reason,
|
|
698
|
+
incomplete: true
|
|
699
|
+
});
|
|
700
|
+
await deleteFileIfExists(manifestPath);
|
|
701
|
+
};
|
|
702
|
+
const writeManifestSync = (incident) => {
|
|
703
|
+
fsSync.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
704
|
+
fsSync.writeFileSync(manifestPath, `${JSON.stringify(incident, null, 2)}\n`, { mode: 0o600 });
|
|
705
|
+
try {
|
|
706
|
+
fsSync.chmodSync(manifestPath, 0o600);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
// best-effort only in crash path
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const sealIncidentIncompleteSync = (incident, reason) => {
|
|
713
|
+
try {
|
|
714
|
+
fsSync.mkdirSync(path.dirname(manifestlessRecoveryPath), { recursive: true });
|
|
715
|
+
fsSync.writeFileSync(manifestlessRecoveryPath, `${JSON.stringify({ version: 1, triggerSeq: incident.triggerSeq }, null, 2)}\n`, { mode: 0o600 });
|
|
716
|
+
fsSync.chmodSync(manifestlessRecoveryPath, 0o600);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// best-effort only in crash path
|
|
720
|
+
}
|
|
721
|
+
appendIncidentLifecycleSync(incident, "incident_closed", {
|
|
722
|
+
reason,
|
|
723
|
+
triggerSeq: incident.triggerSeq,
|
|
724
|
+
triggerEvent: incident.triggerEvent,
|
|
725
|
+
incomplete: true
|
|
726
|
+
});
|
|
727
|
+
appendSummaryLifecycleSync("incident_closed", {
|
|
728
|
+
incidentId: incident.incidentId,
|
|
729
|
+
incidentFile: incidentFileReference(incident.outputFilePath),
|
|
730
|
+
triggerSeq: incident.triggerSeq,
|
|
731
|
+
triggerEvent: incident.triggerEvent,
|
|
732
|
+
reason,
|
|
733
|
+
incomplete: true
|
|
734
|
+
});
|
|
735
|
+
try {
|
|
736
|
+
fsSync.unlinkSync(manifestPath);
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const openIncidentCaptureSync = (record, triggerReason) => {
|
|
744
|
+
const incident = createIncidentManifest(record, triggerReason);
|
|
745
|
+
writeManifestSync(incident);
|
|
746
|
+
const preTriggerEvents = readEventsInRangeSync(incident.preTriggerStartSeq, record.seq);
|
|
747
|
+
const expectedBaseCount = incident.triggerSeq - incident.preTriggerStartSeq + 1;
|
|
748
|
+
const baseWindow = contiguousWindow(preTriggerEvents, incident.preTriggerStartSeq, expectedBaseCount);
|
|
749
|
+
writeJsonlLinesSync(incident.outputFilePath, baseWindow.rows.map((entry) => `${JSON.stringify(entry)}\n`));
|
|
750
|
+
if (baseWindow.hasGapAfterPrefix || baseWindow.rows.length !== expectedBaseCount) {
|
|
751
|
+
sealIncidentIncompleteSync(incident, "interrupted");
|
|
752
|
+
return incident;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
fsSync.unlinkSync(manifestlessRecoveryPath);
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
appendSummaryLifecycleSync("incident_opened", {
|
|
762
|
+
incidentId: incident.incidentId,
|
|
763
|
+
incidentFile: incidentFileReference(incident.outputFilePath),
|
|
764
|
+
triggerSeq: record.seq,
|
|
765
|
+
triggerEvent: record.event,
|
|
766
|
+
triggerReason
|
|
767
|
+
});
|
|
768
|
+
return incident;
|
|
769
|
+
};
|
|
770
|
+
initPromise = initializeState().catch((error) => {
|
|
771
|
+
input.log?.warn("shareable debug initialization failed", {
|
|
772
|
+
error: error instanceof Error ? error.message : String(error)
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
const openIncidentCapture = async (record, triggerReason) => {
|
|
776
|
+
if (openIncident)
|
|
777
|
+
return;
|
|
778
|
+
const incident = createIncidentManifest(record, triggerReason);
|
|
779
|
+
await writeManifest(incident);
|
|
780
|
+
const preTriggerEvents = await readEventsInRange(incident.preTriggerStartSeq, record.seq);
|
|
781
|
+
const expectedBaseCount = incident.triggerSeq - incident.preTriggerStartSeq + 1;
|
|
782
|
+
const baseWindow = contiguousWindow(preTriggerEvents, incident.preTriggerStartSeq, expectedBaseCount);
|
|
783
|
+
await writeJsonlLines(incident.outputFilePath, baseWindow.rows.map((entry) => `${JSON.stringify(entry)}\n`));
|
|
784
|
+
if (baseWindow.hasGapAfterPrefix || baseWindow.rows.length !== expectedBaseCount) {
|
|
785
|
+
await sealIncidentIncomplete({
|
|
786
|
+
incident,
|
|
787
|
+
reason: "interrupted"
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
await deleteFileIfExists(manifestlessRecoveryPath);
|
|
792
|
+
await appendSummaryLifecycle("incident_opened", {
|
|
793
|
+
incidentId: incident.incidentId,
|
|
794
|
+
incidentFile: incidentFileReference(incident.outputFilePath),
|
|
795
|
+
triggerSeq: record.seq,
|
|
796
|
+
triggerEvent: record.event,
|
|
797
|
+
triggerReason
|
|
798
|
+
});
|
|
799
|
+
if (incident.postRemaining <= 0) {
|
|
800
|
+
await closeIncident(incident, "trigger");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
openIncident = incident;
|
|
804
|
+
await pruneIncidents();
|
|
805
|
+
};
|
|
806
|
+
const appendToOpenIncident = async (record) => {
|
|
807
|
+
if (!openIncident || record.seq <= openIncident.triggerSeq)
|
|
808
|
+
return;
|
|
809
|
+
await appendIncidentLine(openIncident, record);
|
|
810
|
+
openIncident = {
|
|
811
|
+
...openIncident,
|
|
812
|
+
postRemaining: Math.max(0, openIncident.postRemaining - 1),
|
|
813
|
+
updatedAt: new Date().toISOString()
|
|
814
|
+
};
|
|
815
|
+
if (openIncident.postRemaining <= 0) {
|
|
816
|
+
await closeIncident(openIncident, "trigger");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
await writeManifestBestEffort(openIncident);
|
|
820
|
+
};
|
|
821
|
+
const triggerReasonFor = (record) => {
|
|
822
|
+
if (record.event === "auth_failure")
|
|
823
|
+
return "auth_failure";
|
|
824
|
+
if (record.event === "retry_after_429")
|
|
825
|
+
return "retry_after_429";
|
|
826
|
+
if (record.event === "synthetic_fatal_error")
|
|
827
|
+
return "synthetic_fatal_error";
|
|
828
|
+
if (record.event === "process_failure")
|
|
829
|
+
return "process_failure";
|
|
830
|
+
if (record.event === "fetch_attempt_response") {
|
|
831
|
+
const status = typeof record.status === "number" ? record.status : undefined;
|
|
832
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
833
|
+
return "http_status";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return undefined;
|
|
837
|
+
};
|
|
838
|
+
const appendRecord = async (record) => {
|
|
839
|
+
await appendToSegment(record);
|
|
840
|
+
const triggerReason = triggerReasonFor(record);
|
|
841
|
+
if (triggerReason && !openIncident) {
|
|
842
|
+
await openIncidentCapture(record, triggerReason);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
await appendToOpenIncident(record);
|
|
846
|
+
}
|
|
847
|
+
await appendSummaryLine(`${JSON.stringify(record)}\n`);
|
|
848
|
+
};
|
|
849
|
+
const buildRecord = (event, payload) => ({
|
|
850
|
+
seq: nextSeq++,
|
|
851
|
+
timestamp: new Date().toISOString(),
|
|
852
|
+
event,
|
|
853
|
+
...payload
|
|
854
|
+
});
|
|
855
|
+
const emitEvent = async (event, payload) => {
|
|
856
|
+
await runQueued(async () => {
|
|
857
|
+
const record = buildRecord(event, payload);
|
|
858
|
+
await appendRecord(record);
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
const captureProcessFailureSync = (event, payload) => {
|
|
862
|
+
try {
|
|
863
|
+
const record = buildRecord(event, payload);
|
|
864
|
+
appendToSegmentSync(record);
|
|
865
|
+
const triggerReason = triggerReasonFor(record);
|
|
866
|
+
if (triggerReason && !openIncident) {
|
|
867
|
+
openIncidentCaptureSync(record, triggerReason);
|
|
868
|
+
}
|
|
869
|
+
else if (openIncident && record.seq > openIncident.triggerSeq) {
|
|
870
|
+
appendJsonlLineSync(openIncident.outputFilePath, `${JSON.stringify(record)}\n`);
|
|
871
|
+
openIncident = {
|
|
872
|
+
...openIncident,
|
|
873
|
+
postRemaining: Math.max(0, openIncident.postRemaining - 1),
|
|
874
|
+
updatedAt: new Date().toISOString()
|
|
875
|
+
};
|
|
876
|
+
if (openIncident.postRemaining <= 0) {
|
|
877
|
+
appendIncidentLifecycleSync(openIncident, "incident_closed", {
|
|
878
|
+
reason: "trigger",
|
|
879
|
+
triggerSeq: openIncident.triggerSeq,
|
|
880
|
+
triggerEvent: openIncident.triggerEvent,
|
|
881
|
+
incomplete: false
|
|
882
|
+
});
|
|
883
|
+
try {
|
|
884
|
+
fsSync.unlinkSync(manifestPath);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
if (!isFsErrorCode(error, "ENOENT"))
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
appendSummaryLifecycleSync("incident_closed", {
|
|
891
|
+
incidentId: openIncident.incidentId,
|
|
892
|
+
incidentFile: incidentFileReference(openIncident.outputFilePath),
|
|
893
|
+
triggerSeq: openIncident.triggerSeq,
|
|
894
|
+
triggerEvent: openIncident.triggerEvent,
|
|
895
|
+
reason: "trigger"
|
|
896
|
+
});
|
|
897
|
+
openIncident = undefined;
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
writeManifestSync(openIncident);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
appendSummaryLineSync(`${JSON.stringify(record)}\n`);
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// best-effort only in crash path
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
const flushPending = async () => {
|
|
910
|
+
await pendingWrite;
|
|
911
|
+
};
|
|
912
|
+
const installProcessHandlers = () => {
|
|
913
|
+
if (input.registerProcessHandlers === false || handlersRegistered)
|
|
914
|
+
return;
|
|
915
|
+
handlersRegistered = true;
|
|
916
|
+
const handleSignal = (signal) => {
|
|
917
|
+
if (signalInFlight)
|
|
918
|
+
return;
|
|
919
|
+
signalInFlight = true;
|
|
920
|
+
captureProcessFailureSync("process_failure", {
|
|
921
|
+
authMode: "codex",
|
|
922
|
+
outcome: signal.toLowerCase(),
|
|
923
|
+
status: 499
|
|
924
|
+
});
|
|
925
|
+
const signalHandler = signalHandlers[signal];
|
|
926
|
+
if (signalHandler) {
|
|
927
|
+
process.removeListener(signal, signalHandler);
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
process.kill(process.pid, signal);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// best-effort only
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const beforeExitHandler = () => {
|
|
937
|
+
void flushPending();
|
|
938
|
+
};
|
|
939
|
+
const uncaughtExceptionMonitorHandler = (error) => {
|
|
940
|
+
captureProcessFailureSync("process_failure", {
|
|
941
|
+
authMode: "codex",
|
|
942
|
+
outcome: "uncaught_exception",
|
|
943
|
+
status: 500,
|
|
944
|
+
errorName: error.name
|
|
945
|
+
});
|
|
946
|
+
};
|
|
947
|
+
const signalHandlers = {
|
|
948
|
+
SIGINT: () => handleSignal("SIGINT"),
|
|
949
|
+
SIGTERM: () => handleSignal("SIGTERM"),
|
|
950
|
+
SIGHUP: () => handleSignal("SIGHUP"),
|
|
951
|
+
SIGBREAK: () => handleSignal("SIGBREAK")
|
|
952
|
+
};
|
|
953
|
+
process.on("beforeExit", beforeExitHandler);
|
|
954
|
+
process.on("uncaughtExceptionMonitor", uncaughtExceptionMonitorHandler);
|
|
955
|
+
process.on("SIGINT", signalHandlers.SIGINT ?? (() => { }));
|
|
956
|
+
process.on("SIGTERM", signalHandlers.SIGTERM ?? (() => { }));
|
|
957
|
+
if (process.platform === "win32") {
|
|
958
|
+
process.on("SIGBREAK", signalHandlers.SIGBREAK ?? (() => { }));
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
process.on("SIGHUP", signalHandlers.SIGHUP ?? (() => { }));
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
installProcessHandlers();
|
|
965
|
+
return {
|
|
966
|
+
enabled: true,
|
|
967
|
+
async emitRotationBegin(event) {
|
|
968
|
+
await emitEvent("rotation_begin", {
|
|
969
|
+
authMode: event.authMode,
|
|
970
|
+
rotationStrategy: event.rotationStrategy,
|
|
971
|
+
totalAccounts: event.totalAccounts,
|
|
972
|
+
enabledAccounts: event.enabledAccounts,
|
|
973
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
974
|
+
session: pseudonym("sess", event.sessionKey)
|
|
975
|
+
});
|
|
976
|
+
},
|
|
977
|
+
async emitRotationDecision(event) {
|
|
978
|
+
await emitEvent("rotation_decision", {
|
|
979
|
+
authMode: event.authMode,
|
|
980
|
+
rotationStrategy: event.rotationStrategy,
|
|
981
|
+
decision: event.decision,
|
|
982
|
+
totalCount: event.totalCount,
|
|
983
|
+
disabledCount: event.disabledCount,
|
|
984
|
+
cooldownCount: event.cooldownCount,
|
|
985
|
+
refreshLeaseCount: event.refreshLeaseCount,
|
|
986
|
+
eligibleCount: event.eligibleCount,
|
|
987
|
+
attemptedCount: event.attemptedCount,
|
|
988
|
+
selectedIndex: event.selectedIndex,
|
|
989
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
990
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
991
|
+
session: pseudonym("sess", event.sessionKey),
|
|
992
|
+
attempt: pseudonym("attempt", event.attemptKey)
|
|
993
|
+
});
|
|
994
|
+
},
|
|
995
|
+
async emitRotationCandidateSelected(event) {
|
|
996
|
+
await emitEvent("rotation_candidate_selected", {
|
|
997
|
+
authMode: event.authMode,
|
|
998
|
+
selectedIndex: event.selectedIndex,
|
|
999
|
+
selectedEnabled: event.selectedEnabled,
|
|
1000
|
+
selectedCooldownUntil: event.selectedCooldownUntil,
|
|
1001
|
+
selectedExpires: event.selectedExpires,
|
|
1002
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1003
|
+
attempt: pseudonym("attempt", event.attemptKey)
|
|
1004
|
+
});
|
|
1005
|
+
},
|
|
1006
|
+
async emitFetchAttemptRequest(event) {
|
|
1007
|
+
await emitEvent("fetch_attempt_request", {
|
|
1008
|
+
authMode: event.authMode,
|
|
1009
|
+
rotationStrategy: event.rotationStrategy,
|
|
1010
|
+
attempt: event.attempt,
|
|
1011
|
+
maxAttempts: event.maxAttempts,
|
|
1012
|
+
attemptReasonCode: event.attemptReasonCode,
|
|
1013
|
+
method: event.request.method.toUpperCase(),
|
|
1014
|
+
endpoint: normalizeEndpoint(event.request.url),
|
|
1015
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1016
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
1017
|
+
session: pseudonym("sess", event.sessionKey),
|
|
1018
|
+
promptCacheKey: pseudonym("pck", await extractPromptCacheKey(event.request))
|
|
1019
|
+
});
|
|
1020
|
+
},
|
|
1021
|
+
async emitFetchAttemptResponse(event) {
|
|
1022
|
+
await emitEvent("fetch_attempt_response", {
|
|
1023
|
+
authMode: event.authMode,
|
|
1024
|
+
rotationStrategy: event.rotationStrategy,
|
|
1025
|
+
attempt: event.attempt,
|
|
1026
|
+
maxAttempts: event.maxAttempts,
|
|
1027
|
+
attemptReasonCode: event.attemptReasonCode,
|
|
1028
|
+
endpoint: normalizeEndpoint(event.endpoint),
|
|
1029
|
+
status: event.status,
|
|
1030
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1031
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
1032
|
+
session: pseudonym("sess", event.sessionKey)
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
async emitRetryAfter429(event) {
|
|
1036
|
+
await emitEvent("retry_after_429", {
|
|
1037
|
+
authMode: event.authMode,
|
|
1038
|
+
rotationStrategy: event.rotationStrategy,
|
|
1039
|
+
attempt: event.attempt,
|
|
1040
|
+
maxAttempts: event.maxAttempts,
|
|
1041
|
+
attemptReasonCode: event.attemptReasonCode,
|
|
1042
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1043
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
1044
|
+
session: pseudonym("sess", event.sessionKey)
|
|
1045
|
+
});
|
|
1046
|
+
},
|
|
1047
|
+
async emitAuthFailure(event) {
|
|
1048
|
+
await emitEvent("auth_failure", {
|
|
1049
|
+
authMode: event.authMode,
|
|
1050
|
+
outcome: event.outcome,
|
|
1051
|
+
status: event.status,
|
|
1052
|
+
waitMs: event.waitMs,
|
|
1053
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1054
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
1055
|
+
session: pseudonym("sess", event.sessionKey)
|
|
1056
|
+
});
|
|
1057
|
+
},
|
|
1058
|
+
async emitSyntheticFatalError(event) {
|
|
1059
|
+
await emitEvent("synthetic_fatal_error", {
|
|
1060
|
+
authMode: event.authMode,
|
|
1061
|
+
outcome: event.outcome,
|
|
1062
|
+
status: event.status,
|
|
1063
|
+
endpoint: normalizeEndpoint(event.endpoint),
|
|
1064
|
+
selectedIdentity: pseudonym("ident", event.selectedIdentityKey),
|
|
1065
|
+
activeIdentity: pseudonym("ident", event.activeIdentityKey),
|
|
1066
|
+
session: pseudonym("sess", event.sessionKey)
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
//# sourceMappingURL=shareable-debug.js.map
|