wowdump 0.2.1 → 0.3.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.
Files changed (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -111
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +563 -0
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +99 -0
  10. package/dist/reader/broker.js +475 -0
  11. package/dist/reader/client.js +1 -0
  12. package/dist/reader/launcher.js +219 -0
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +2 -0
  17. package/dist/toolchain.js +123 -0
  18. package/package.json +19 -37
  19. package/skills/wowdump/SKILL.md +22 -0
  20. package/skills/wowdump/references/commands.md +63 -0
  21. package/skills/wowdump/references/disassemble.md +18 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +44 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1335
  29. package/dist/analysis-path.js +0 -38
  30. package/dist/analysis-process-log.js +0 -146
  31. package/dist/broker-client.js +0 -411
  32. package/dist/broker-codec.js +0 -148
  33. package/dist/broker-core.js +0 -1045
  34. package/dist/broker-gateway.js +0 -447
  35. package/dist/broker-ledger.js +0 -196
  36. package/dist/broker-main.js +0 -291
  37. package/dist/broker-protocol.js +0 -119
  38. package/dist/broker-runtime.js +0 -1283
  39. package/dist/broker-server.js +0 -466
  40. package/dist/build-bundle-loader.js +0 -183
  41. package/dist/build-bundle.js +0 -11
  42. package/dist/discovery.js +0 -59
  43. package/dist/dry-run.js +0 -38
  44. package/dist/error-log.js +0 -71
  45. package/dist/focus-errors.js +0 -63
  46. package/dist/focus-service.js +0 -1855
  47. package/dist/focused-session.js +0 -1357
  48. package/dist/mcp-main.js +0 -51
  49. package/dist/mcp.js +0 -924
  50. package/dist/observability.js +0 -41
  51. package/dist/process-log-lock.js +0 -195
  52. package/dist/processes.js +0 -47
  53. package/dist/runtime-config.js +0 -399
  54. package/dist/session.js +0 -145
  55. package/dist/storage.js +0 -12
  56. package/dist/wow-analysis.js +0 -1430
  57. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  58. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  59. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  60. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  61. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
  62. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  63. /package/dist/{types.js → core/types.js} +0 -0
@@ -1,1357 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { z } from "zod";
3
- export const TargetKindSchema = z.enum([
4
- "lua_wrapper",
5
- "cpp_function",
6
- "data_source",
7
- "object",
8
- "address_range",
9
- ]);
10
- const HexSchema = z.string().regex(/^0x[0-9a-fA-F]+$/);
11
- const SelectorRangeSchema = z
12
- .object({ rva: HexSchema, size: z.number().int().positive().max(0x7fffffff) })
13
- .strict();
14
- export const FocusSelectorSchema = z
15
- .object({
16
- schema: z.literal("selector-schema.v1").default("selector-schema.v1"),
17
- kind: TargetKindSchema,
18
- combine: z.enum(["all", "any"]).default("any"),
19
- apis: z.array(z.string().min(1)).max(256).optional(),
20
- namespaces: z.array(z.string().min(1)).max(256).optional(),
21
- globs: z.array(z.string().min(1)).max(256).optional(),
22
- rvas: z.array(HexSchema).max(256).optional(),
23
- dataSourceIds: z.array(z.string().min(1)).max(256).optional(),
24
- objectIds: z.array(z.string().min(1)).max(256).optional(),
25
- ranges: z.array(SelectorRangeSchema).max(256).optional(),
26
- maxTargets: z.number().int().min(1).max(256).default(64),
27
- // Compatibility inputs are normalized into the plural v1 fields.
28
- namespace: z.string().min(1).optional(),
29
- glob: z.string().min(1).optional(),
30
- rva: HexSchema.optional(),
31
- address: HexSchema.optional(),
32
- start: HexSchema.optional(),
33
- end: HexSchema.optional(),
34
- })
35
- .strict();
36
- const VERBATIM_KEYS = new Set([
37
- "combine",
38
- "apis",
39
- "namespaces",
40
- "globs",
41
- "rvas",
42
- "dataSourceIds",
43
- "objectIds",
44
- "ranges",
45
- "maxTargets",
46
- ]);
47
- export function validateSelector(input, targetKind) {
48
- if (!input || typeof input !== "object" || Array.isArray(input))
49
- throw new Error("selector must be an object");
50
- if (targetKind)
51
- for (const key of Object.keys(input))
52
- if (!VERBATIM_KEYS.has(key))
53
- throw new Error(`selector field ${key} is not allowed`);
54
- const parsed = FocusSelectorSchema.parse(targetKind ? { ...input, kind: targetKind } : input);
55
- const apis = utf8Sorted(parsed.apis ?? []);
56
- const namespaces = utf8Sorted([
57
- ...(parsed.namespaces ?? []),
58
- ...(parsed.namespace ? [parsed.namespace] : []),
59
- ]);
60
- const globs = utf8Sorted([
61
- ...(parsed.globs ?? []),
62
- ...(parsed.glob ? [parsed.glob] : []),
63
- ]);
64
- for (const glob of globs)
65
- validateGlob(glob);
66
- const rvas = hexSorted([
67
- ...(parsed.rvas ?? []),
68
- ...(parsed.rva ? [parsed.rva] : []),
69
- ]);
70
- const dataSourceIds = utf8Sorted(parsed.dataSourceIds ?? []);
71
- const objectIds = utf8Sorted(parsed.objectIds ?? []);
72
- const ranges = normalizeRanges(parsed.ranges ?? [], parsed.start, parsed.end);
73
- const populated = {
74
- apis,
75
- namespaces,
76
- globs,
77
- rvas,
78
- dataSourceIds,
79
- objectIds,
80
- ranges,
81
- };
82
- const allowed = {
83
- lua_wrapper: new Set(["apis", "namespaces", "globs", "rvas"]),
84
- cpp_function: new Set(["rvas"]),
85
- data_source: new Set(["dataSourceIds"]),
86
- object: new Set(["objectIds"]),
87
- address_range: new Set(["ranges"]),
88
- };
89
- for (const [field, values] of Object.entries(populated)) {
90
- if (values.length > 0 && !allowed[parsed.kind].has(field))
91
- throw new Error(`${field} is incompatible with ${parsed.kind}`);
92
- }
93
- if (parsed.kind === "cpp_function" && rvas.length === 0)
94
- throw new Error("cpp_function requires rvas");
95
- if (parsed.kind === "data_source" && dataSourceIds.length === 0)
96
- throw new Error("data_source requires dataSourceIds");
97
- if (parsed.kind === "object" && objectIds.length === 0 && !parsed.address)
98
- throw new Error("object requires objectIds");
99
- if (parsed.kind === "address_range" && ranges.length === 0)
100
- throw new Error("address_range requires ranges");
101
- if (parsed.kind === "lua_wrapper" &&
102
- apis.length + namespaces.length + globs.length + rvas.length === 0)
103
- throw new Error("selector must identify a target");
104
- const resolvedCount = apis.length +
105
- namespaces.length +
106
- globs.length +
107
- rvas.length +
108
- dataSourceIds.length +
109
- objectIds.length +
110
- ranges.length;
111
- if (resolvedCount > parsed.maxTargets)
112
- throw new Error(`selector resolves to ${resolvedCount} inputs, exceeding maxTargets=${parsed.maxTargets}`);
113
- return {
114
- schema: "selector-schema.v1",
115
- kind: parsed.kind,
116
- combine: parsed.combine,
117
- ...(apis.length ? { apis } : {}),
118
- ...(namespaces.length ? { namespaces } : {}),
119
- ...(globs.length ? { globs } : {}),
120
- ...(rvas.length ? { rvas, rva: rvas[0] } : {}),
121
- ...(dataSourceIds.length ? { dataSourceIds } : {}),
122
- ...(objectIds.length ? { objectIds } : {}),
123
- ...(ranges.length
124
- ? {
125
- ranges,
126
- start: ranges[0].rva,
127
- end: canonicalHex(BigInt(ranges[0].rva) + BigInt(ranges[0].size)),
128
- }
129
- : {}),
130
- ...(parsed.address ? { address: canonicalHex(parsed.address) } : {}),
131
- maxTargets: parsed.maxTargets,
132
- };
133
- }
134
- /** Canonical byte representation used for selector identity and replay checks. */
135
- export const canonicalizeSelector = (input) => {
136
- const record = input;
137
- const explicitKind = record && !record.kind && typeof record.targetKind === "string"
138
- ? TargetKindSchema.parse(record.targetKind)
139
- : undefined;
140
- const { targetKind: _targetKind, ...selector } = record ?? {};
141
- const value = validateSelector(selector, explicitKind);
142
- return JSON.stringify({
143
- schemaVersion: value.schema,
144
- targetKind: value.kind,
145
- combine: value.combine,
146
- apis: value.apis ?? [],
147
- namespaces: value.namespaces ?? [],
148
- globs: value.globs ?? [],
149
- rvas: value.rvas ?? [],
150
- dataSourceIds: value.dataSourceIds ?? [],
151
- objectIds: value.objectIds ?? [],
152
- ranges: value.ranges ?? [],
153
- maxTargets: value.maxTargets,
154
- });
155
- };
156
- function canonicalHex(value) {
157
- return `0x${BigInt(value).toString(16).toUpperCase()}`;
158
- }
159
- function utf8Sorted(values) {
160
- return [...new Set(values)].sort((left, right) => Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")));
161
- }
162
- function hexSorted(values) {
163
- return [...new Set(values.map(canonicalHex))].sort((left, right) => BigInt(left) < BigInt(right) ? -1 : BigInt(left) > BigInt(right) ? 1 : 0);
164
- }
165
- function normalizeRanges(values, start, end) {
166
- const ranges = values.map((value) => ({
167
- rva: canonicalHex(value.rva),
168
- size: value.size,
169
- }));
170
- if (start || end) {
171
- if (!start || !end || BigInt(end) <= BigInt(start))
172
- throw new Error("address_range requires start < end");
173
- const size = Number(BigInt(end) - BigInt(start));
174
- if (!Number.isSafeInteger(size) || size <= 0)
175
- throw new Error("address range size is invalid");
176
- ranges.push({ rva: canonicalHex(start), size });
177
- }
178
- const unique = new Map(ranges.map((range) => [`${range.rva}:${range.size}`, range]));
179
- return [...unique.values()].sort((left, right) => BigInt(left.rva) < BigInt(right.rva)
180
- ? -1
181
- : BigInt(left.rva) > BigInt(right.rva)
182
- ? 1
183
- : left.size - right.size);
184
- }
185
- function validateGlob(value) {
186
- let escaped = false;
187
- for (const character of value) {
188
- if (escaped) {
189
- escaped = false;
190
- continue;
191
- }
192
- if (character === "\\") {
193
- escaped = true;
194
- continue;
195
- }
196
- }
197
- if (escaped)
198
- throw new Error("glob cannot end with an isolated escape");
199
- }
200
- const JsonValueSchema = z.lazy(() => z.union([
201
- z.null(),
202
- z.boolean(),
203
- z.number().finite(),
204
- z.string(),
205
- z.array(JsonValueSchema),
206
- z.record(JsonValueSchema),
207
- ]));
208
- const TriggerActionSchema = z.union([
209
- z.string().min(1),
210
- z
211
- .object({
212
- userAction: z.string().min(1).optional(),
213
- expectedTransition: z
214
- .union([z.string().min(1), JsonValueSchema])
215
- .optional(),
216
- description: z.string().min(1).optional(),
217
- })
218
- .strict(),
219
- ]);
220
- const TriggerPhaseSchema = z
221
- .object({
222
- phase: z.enum(["before", "during", "after"]),
223
- phaseIndex: z.number().int().nonnegative(),
224
- actions: z.array(TriggerActionSchema).default([]),
225
- userAction: z.string().min(1).optional(),
226
- expectedTransition: z
227
- .union([z.string().min(1), JsonValueSchema])
228
- .optional(),
229
- })
230
- .strict();
231
- const RawTriggerPlanSchema = z
232
- .object({
233
- planVersion: z.string().min(1).default("trigger-plan.v1"),
234
- workloadId: z.string().min(1),
235
- description: z.string().optional(),
236
- phases: z.array(TriggerPhaseSchema).optional(),
237
- before: z.array(TriggerActionSchema).default([]),
238
- during: z.array(TriggerActionSchema).default([]),
239
- after: z.array(TriggerActionSchema).default([]),
240
- expectedChanges: z.array(z.string()).default([]),
241
- })
242
- .strict();
243
- export const TriggerPlanSchema = RawTriggerPlanSchema.transform((value) => ({
244
- ...value,
245
- phases: value.phases ?? [
246
- ...value.before.map((_, phaseIndex) => ({
247
- phase: "before",
248
- phaseIndex,
249
- actions: [value.before[phaseIndex]],
250
- })),
251
- ...value.during.map((_, phaseIndex) => ({
252
- phase: "during",
253
- phaseIndex,
254
- actions: [value.during[phaseIndex]],
255
- })),
256
- ...value.after.map((_, phaseIndex) => ({
257
- phase: "after",
258
- phaseIndex,
259
- actions: [value.after[phaseIndex]],
260
- })),
261
- ],
262
- }));
263
- export function canonicalizeEnvironment(input) {
264
- const parsedInput = z.record(JsonValueSchema).parse(input);
265
- const parsed = {
266
- descriptorVersion: "environment.v1",
267
- capturedAt: "1970-01-01T00:00:00.000Z",
268
- ...parsedInput,
269
- };
270
- const canonical = stableJson(parsed);
271
- return {
272
- value: parsed,
273
- canonical,
274
- sha256: createHash("sha256").update(canonical).digest("hex"),
275
- };
276
- }
277
- function stableJson(value) {
278
- if (Array.isArray(value))
279
- return `[${value.map(stableJson).join(",")}]`;
280
- if (value !== null && typeof value === "object")
281
- return `{${Object.keys(value)
282
- .sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
283
- .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
284
- .join(",")}}`;
285
- return JSON.stringify(value);
286
- }
287
- const systemClock = {
288
- now: () => Date.now(),
289
- monotonicNow: () => performance.now(),
290
- };
291
- export class FocusedSession {
292
- state = "created";
293
- seq = 0;
294
- ingressOrdinal = 0;
295
- bytes = 0;
296
- hooks = [];
297
- stacks = new Map();
298
- invocationCounter = 0;
299
- phase = "before";
300
- droppedEvents = 0;
301
- droppedBytes = 0;
302
- droppedReasons = {};
303
- dropWindows = {};
304
- externalDroppedEvents = 0;
305
- externalDroppedBytes = 0;
306
- externalDroppedReasons = {};
307
- startedAt = 0;
308
- stoppedAt;
309
- pausedAt;
310
- pauseGaps = [];
311
- cleanupStatus = { hooks: 0, scripts: 0, interceptors: 0 };
312
- cleanupPromise;
313
- cleanupCoreComplete = false;
314
- cleanupCoreFailed = false;
315
- cleanupTerminalComplete = false;
316
- cleanupDetachRetryable = false;
317
- cleanupIntendedTerminal;
318
- cleanupStreamPrefixes = [];
319
- cleanupFragment;
320
- terminalStatus;
321
- cleanupSteps = [];
322
- environmentInfo;
323
- plan;
324
- failureClass;
325
- errorCode;
326
- fatalNotified = new Set();
327
- selector;
328
- verbatimSelector;
329
- receiveOpen = true;
330
- frozen = false;
331
- draining = false;
332
- appendTail = Promise.resolve();
333
- appendInFlight = 0;
334
- appendControllers = new Set();
335
- appendTransactionsAborted = false;
336
- cleanupDetachedOperations = [];
337
- cleanupLateCompletions = 0;
338
- options;
339
- constructor(options) {
340
- this.verbatimSelector =
341
- options.selectorVerbatim ?? stripSelectorMetadata(options.target);
342
- this.selector = validateSelector(stripSelectorMetadata(options.target), options.targetKind ?? options.target.kind);
343
- if (!Number.isInteger(options.pid) || options.pid <= 0)
344
- throw new Error("pid must be positive");
345
- if (!options.sessionId || !options.buildKey)
346
- throw new Error("sessionId and buildKey are required");
347
- if (options.cleanupTimeoutMs !== undefined &&
348
- (!Number.isFinite(options.cleanupTimeoutMs) ||
349
- options.cleanupTimeoutMs < 0))
350
- throw new Error("cleanupTimeoutMs must be a non-negative finite number");
351
- if (options.cleanupDeadline !== undefined &&
352
- !Number.isFinite(options.cleanupDeadline))
353
- throw new Error("cleanupDeadline must be finite");
354
- this.environmentInfo = canonicalizeEnvironment(options.environment);
355
- this.plan = TriggerPlanSchema.parse(options.triggerPlan);
356
- this.options = options;
357
- }
358
- get status() {
359
- return this.state;
360
- }
361
- get resolvedSelector() {
362
- return this.selector;
363
- }
364
- async validateIdentity() {
365
- await this.revalidate();
366
- }
367
- get manifest() {
368
- return {
369
- schema: "wow.focus-session.v1",
370
- sessionId: this.options.sessionId,
371
- pid: this.options.pid,
372
- buildKey: this.options.buildKey,
373
- moduleBase: this.options.moduleBase,
374
- imageBase: this.options.imageBase,
375
- processStartTime: this.options.processStartTime ?? null,
376
- moduleIdentity: this.options.moduleIdentity ?? null,
377
- targetKind: this.selector.kind,
378
- selector: this.verbatimSelector,
379
- selectorVerbatim: this.verbatimSelector,
380
- selectorCanonical: canonicalizeSelector({
381
- ...this.verbatimSelector,
382
- targetKind: this.selector.kind,
383
- }),
384
- resolvedTargets: selectorTargets(this.selector),
385
- environment: this.environmentInfo.value,
386
- environmentSha256: this.environmentInfo.sha256,
387
- triggerPlan: this.plan,
388
- triggerPhase: this.phase,
389
- effectiveLimits: {
390
- snapshotIntervalMs: this.options.snapshotIntervalMs ?? 1000,
391
- maxEvents: this.options.maxEvents ?? 10_000,
392
- maxBytes: this.options.maxBytes ?? 8 * 1024 * 1024,
393
- durationMs: this.options.durationMs ?? 0,
394
- sampling: this.options.sampling ?? 1,
395
- },
396
- startTime: this.startedAt ? new Date(this.startedAt).toISOString() : null,
397
- stopTime: this.stoppedAt ? new Date(this.stoppedAt).toISOString() : null,
398
- hookIds: this.hooks,
399
- eventCount: this.seq,
400
- ingressCount: this.ingressOrdinal,
401
- eventBytes: this.bytes,
402
- droppedEventCount: this.droppedEvents,
403
- droppedByteCount: this.droppedBytes,
404
- droppedByReason: this.droppedReasons,
405
- dropWindows: this.dropWindows,
406
- pauseGaps: this.pauseGaps,
407
- cleanupStatus: this.cleanupStatus,
408
- cleanupSteps: this.cleanupSteps,
409
- cleanupDetachedOperations: this.cleanupDetachedOperations,
410
- cleanupLateCompletions: this.cleanupLateCompletions,
411
- frozen: this.frozen,
412
- failureClass: this.failureClass ?? null,
413
- errorCode: this.errorCode ?? null,
414
- state: this.state,
415
- ...(this.terminalStatus ? { terminalStatus: this.terminalStatus } : {}),
416
- };
417
- }
418
- async start() {
419
- if (this.state !== "created")
420
- throw new Error("session already started");
421
- this.state = "attaching";
422
- let captureStage = "attachment";
423
- try {
424
- await this.revalidate();
425
- await this.options.transport.attach?.(this.identity());
426
- captureStage = "capture";
427
- this.hooks = await this.options.transport.installHooks(this.selector, {
428
- captureArgs: this.options.captureArgs === true,
429
- captureReturns: this.options.captureReturns === true,
430
- captureCallStack: this.options.captureCallStack === true,
431
- captureMemoryWrites: this.options.captureMemoryWrites === true,
432
- captureObjectDiff: this.options.captureObjectDiff === true,
433
- snapshotIntervalMs: this.options.snapshotIntervalMs ?? 1000,
434
- maxEvents: this.options.maxEvents ?? 10_000,
435
- maxBytes: this.options.maxBytes ?? 8 * 1024 * 1024,
436
- sampling: this.options.sampling ?? 1,
437
- });
438
- this.state = "armed";
439
- this.startedAt = this.clock.now();
440
- const before = await this.options.transport.snapshot?.("before");
441
- if (before !== undefined)
442
- await this.rawControlEvent("snapshot", {
443
- trigger: {
444
- phase: "before",
445
- workloadId: this.plan.workloadId,
446
- snapshotPhase: "before",
447
- },
448
- before,
449
- status: "confirmed",
450
- });
451
- await this.options.transport.resumeProducer?.();
452
- await this.writeManifest();
453
- this.state = "running";
454
- await this.writeManifest();
455
- return this.manifest;
456
- }
457
- catch (error) {
458
- this.markFailure(captureStage === "capture" ? "capture_fatal" : "attachment_fatal", error);
459
- try {
460
- await this.cleanup("start_failure");
461
- }
462
- catch (cleanupError) {
463
- // A failed start remains a failed observation, but its cleanup evidence
464
- // is still terminal and reviewable when all owned resources reached zero.
465
- if (!this.frozen || this.terminalStatus !== "frozen")
466
- throw cleanupError;
467
- }
468
- throw error;
469
- }
470
- }
471
- async pause() {
472
- await this.revalidate();
473
- if (this.state !== "running")
474
- throw new Error("session is not running");
475
- await this.options.transport.pauseProducer?.();
476
- this.pausedAt = this.clock.now();
477
- await this.controlEvent("pause");
478
- this.state = "paused";
479
- await this.writeManifest();
480
- }
481
- async resume() {
482
- await this.revalidate();
483
- if (this.state !== "paused")
484
- throw new Error("session is not paused");
485
- const end = this.clock.now();
486
- const start = this.pausedAt ?? end;
487
- await this.options.transport.resumeProducer?.();
488
- this.state = "running";
489
- this.pauseGaps.push({
490
- start: new Date(start).toISOString(),
491
- end: new Date(end).toISOString(),
492
- durationMs: end - start,
493
- });
494
- await this.controlEvent("resume", {
495
- before: { timestamp: new Date(start).toISOString() },
496
- after: { timestamp: new Date(end).toISOString() },
497
- diff: { durationMs: end - start, continuityClaimed: false },
498
- });
499
- this.pausedAt = undefined;
500
- await this.writeManifest();
501
- }
502
- async checkpoint(label = "checkpoint", trigger = {}, snapshotNow = false) {
503
- await this.revalidate();
504
- if (this.state !== "running")
505
- throw new Error("CHECKPOINT_SESSION_NOT_RUNNING");
506
- const snapshot = snapshotNow
507
- ? await this.options.transport.snapshot?.("checkpoint")
508
- : undefined;
509
- await this.emit({
510
- target: { kind: this.selector.kind },
511
- trigger: {
512
- phase: this.phase,
513
- workloadId: this.plan.workloadId,
514
- ...trigger,
515
- },
516
- status: "dynamic",
517
- event: { kind: "checkpoint", depth: 0 },
518
- ...(snapshot !== undefined ? { after: snapshot } : {}),
519
- });
520
- const prefixes = (await this.options.eventSink.prefixes?.()) ?? [];
521
- await this.writeManifest({
522
- checkpoint: label,
523
- seq: this.seq,
524
- streamPrefixes: prefixes,
525
- });
526
- return { ...this.manifest, streamPrefixes: prefixes };
527
- }
528
- async mergeExternalDrops(snapshot, source = "transport") {
529
- const totalEvents = safeNonNegativeInteger(snapshot.droppedEventCount);
530
- const totalBytes = safeNonNegativeInteger(snapshot.droppedByteCount);
531
- const eventDelta = Math.max(0, totalEvents - this.externalDroppedEvents);
532
- const byteDelta = Math.max(0, totalBytes - this.externalDroppedBytes);
533
- const reasons = isRecord(snapshot.droppedByReason)
534
- ? snapshot.droppedByReason
535
- : {};
536
- if (isRecord(snapshot.dropWindows))
537
- for (const [reason, window] of Object.entries(snapshot.dropWindows))
538
- this.dropWindows[`${source}:${reason}`] = window;
539
- const reasonDelta = {};
540
- for (const [reason, raw] of Object.entries(reasons)) {
541
- const value = safeNonNegativeInteger(raw);
542
- const delta = Math.max(0, value - (this.externalDroppedReasons[reason] ?? 0));
543
- if (delta)
544
- reasonDelta[reason] = delta;
545
- this.externalDroppedReasons[reason] = value;
546
- }
547
- this.externalDroppedEvents = Math.max(this.externalDroppedEvents, totalEvents);
548
- this.externalDroppedBytes = Math.max(this.externalDroppedBytes, totalBytes);
549
- if (!eventDelta && !byteDelta && !Object.keys(reasonDelta).length)
550
- return;
551
- this.droppedEvents += eventDelta;
552
- this.droppedBytes += byteDelta;
553
- for (const [reason, delta] of Object.entries(reasonDelta))
554
- this.droppedReasons[`${source}:${reason}`] =
555
- (this.droppedReasons[`${source}:${reason}`] ?? 0) + delta;
556
- if (["running", "paused"].includes(this.state))
557
- await this.rawControlEvent("overflow", {
558
- diff: {
559
- source,
560
- droppedEvents: eventDelta,
561
- droppedBytes: byteDelta,
562
- droppedByReason: reasonDelta,
563
- },
564
- });
565
- }
566
- async emit(input) {
567
- const ingressOrdinal = ++this.ingressOrdinal;
568
- if (!this.receiveOpen || this.state !== "running")
569
- return this.drop("not_running", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
570
- return this.enqueue(async () => this.persistInput(input, ingressOrdinal));
571
- }
572
- async emitTransport(input) {
573
- const ingressOrdinal = ++this.ingressOrdinal;
574
- if (!this.receiveOpen || this.state !== "running")
575
- return this.drop("not_running", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
576
- return this.enqueue(async () => this.persistInput(input, ingressOrdinal));
577
- }
578
- async persistInput(input, ingressOrdinal) {
579
- if (this.frozen)
580
- return this.drop("artifact_frozen", Buffer.byteLength(JSON.stringify(input)), ingressOrdinal);
581
- const phase = phaseOf(input.trigger);
582
- if (!this.advancePhase(phase))
583
- return this.drop("phase_regression", Buffer.byteLength(JSON.stringify(input)));
584
- if (this.options.sampling !== undefined &&
585
- this.options.sampling < 1 &&
586
- (this.options.random ?? Math.random)() > this.options.sampling)
587
- return this.drop("sampling", Buffer.byteLength(JSON.stringify(input)));
588
- const event = this.makeEvent(input.event.kind, input, ingressOrdinal, this.seq + 1);
589
- const size = Buffer.byteLength(JSON.stringify(event));
590
- if (this.options.maxEvents && this.seq >= this.options.maxEvents)
591
- return this.drop("max_events", size);
592
- if (this.options.maxBytes && this.bytes + size > this.options.maxBytes)
593
- return this.drop("max_bytes", size);
594
- this.seq++;
595
- this.bytes += size;
596
- try {
597
- await this.appendEvent(event);
598
- }
599
- catch (error) {
600
- this.seq--;
601
- this.bytes -= size;
602
- this.drop(error instanceof Error && error.name === "AbortError"
603
- ? "append_aborted"
604
- : "writer_failure", size, ingressOrdinal, false);
605
- throw error;
606
- }
607
- return true;
608
- }
609
- async appendEvent(event) {
610
- const controller = new AbortController();
611
- this.appendControllers.add(controller);
612
- if (this.appendTransactionsAborted)
613
- controller.abort();
614
- try {
615
- await this.options.eventSink.append(event, controller.signal);
616
- }
617
- finally {
618
- this.appendControllers.delete(controller);
619
- }
620
- }
621
- async enter(threadId, input) {
622
- const stack = this.stacks.get(threadId) ?? [];
623
- const invocationId = `${this.options.sessionId}:${++this.invocationCounter}`;
624
- const parentInvocationId = stack.at(-1);
625
- stack.push(invocationId);
626
- this.stacks.set(threadId, stack);
627
- return this.emit({
628
- ...input,
629
- event: {
630
- kind: "enter",
631
- threadId,
632
- depth: stack.length,
633
- invocationId,
634
- parentInvocationId,
635
- recursive: stack.length > 1,
636
- },
637
- });
638
- }
639
- async leave(threadId, input) {
640
- const stack = this.stacks.get(threadId) ?? [];
641
- const invocationId = stack.at(-1);
642
- const parentInvocationId = stack.at(-2);
643
- const ok = await this.emit({
644
- ...input,
645
- event: {
646
- kind: "leave",
647
- threadId,
648
- depth: Math.max(1, stack.length),
649
- invocationId,
650
- parentInvocationId,
651
- recursive: stack.length > 1,
652
- },
653
- });
654
- stack.pop();
655
- if (!stack.length)
656
- this.stacks.delete(threadId);
657
- return ok;
658
- }
659
- async captureDiff(kind, before, after, base) {
660
- if (!this.options.diffValidator)
661
- throw new Error("DIFF_VALIDATOR_REQUIRED");
662
- const diff = await this.options.diffValidator.diff(kind, before, after);
663
- const validation = await this.options.diffValidator.validate(kind, before, after, diff);
664
- return this.emit({
665
- ...base,
666
- before,
667
- after,
668
- diff: { value: diff, validation },
669
- status: validation.valid ? base.status : "partial",
670
- event: { kind: `${kind}_change`, depth: 0 },
671
- });
672
- }
673
- async targetExited(cleanupDeadline) {
674
- if (["stopped", "stopping"].includes(this.state))
675
- return this.manifest;
676
- this.state = "target_exited";
677
- await this.rawControlEvent("exception", {
678
- diff: { reason: "target_exit" },
679
- });
680
- try {
681
- await this.cleanup("target_exit", cleanupDeadline);
682
- }
683
- catch (error) {
684
- if (isAbortError(error) || String(error).includes("CAPTURE_CLEANUP_INCOMPLETE"))
685
- throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
686
- throw error;
687
- }
688
- return this.manifest;
689
- }
690
- async stop(cleanupDeadline) {
691
- try {
692
- await this.cleanup("requested", cleanupDeadline);
693
- }
694
- catch (error) {
695
- if (isAbortError(error) || String(error).includes("CAPTURE_CLEANUP_INCOMPLETE"))
696
- throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
697
- throw error;
698
- }
699
- return this.manifest;
700
- }
701
- get clock() {
702
- return this.options.clock ?? systemClock;
703
- }
704
- identity() {
705
- return {
706
- pid: this.options.pid,
707
- buildKey: this.options.buildKey,
708
- moduleBase: this.options.moduleBase,
709
- imageBase: this.options.imageBase,
710
- processStartTime: this.options.processStartTime,
711
- moduleIdentity: this.options.moduleIdentity,
712
- };
713
- }
714
- async revalidate() {
715
- const actual = await this.options.transport.revalidateIdentity(this.identity());
716
- if (actual.pid !== this.options.pid ||
717
- actual.buildKey !== this.options.buildKey ||
718
- actual.moduleBase.toLowerCase() !==
719
- this.options.moduleBase.toLowerCase() ||
720
- (this.options.processStartTime !== undefined &&
721
- actual.processStartTime !== this.options.processStartTime) ||
722
- (this.options.moduleIdentity !== undefined &&
723
- actual.moduleIdentity !== this.options.moduleIdentity)) {
724
- this.markFailure("attachment_fatal", new Error("TARGET_IDENTITY_CHANGED"));
725
- throw new Error("TARGET_IDENTITY_CHANGED");
726
- }
727
- }
728
- advancePhase(next) {
729
- const rank = { before: 0, during: 1, after: 2 };
730
- if (rank[next] < rank[this.phase])
731
- return false;
732
- this.phase = next;
733
- return true;
734
- }
735
- drop(reason, bytes, ingressOrdinal = this.ingressOrdinal, emitOverflow = true) {
736
- this.droppedEvents++;
737
- this.droppedBytes += bytes;
738
- this.droppedReasons[reason] = (this.droppedReasons[reason] ?? 0) + 1;
739
- const timestamp = new Date(this.clock.now()).toISOString();
740
- const current = isRecord(this.dropWindows[reason])
741
- ? this.dropWindows[reason]
742
- : {
743
- firstLostIngressOrdinal: ingressOrdinal,
744
- firstTimestamp: timestamp,
745
- producer: "host",
746
- threadIds: [],
747
- chainCompletenessAffected: true,
748
- };
749
- current.lastLostIngressOrdinal = ingressOrdinal;
750
- current.lastTimestamp = timestamp;
751
- this.dropWindows[reason] = current;
752
- if (emitOverflow && this.receiveOpen && !this.frozen)
753
- void this.rawControlEvent("overflow", {
754
- diff: {
755
- reason,
756
- truncation: reason === "max_bytes",
757
- droppedEvents: this.droppedEvents,
758
- droppedBytes: this.droppedBytes,
759
- dropWindow: current,
760
- },
761
- });
762
- return false;
763
- }
764
- async controlEvent(kind, extra = {}) {
765
- return this.emit({
766
- target: { kind: this.selector.kind },
767
- trigger: { phase: this.phase, workloadId: this.plan.workloadId },
768
- status: "dynamic",
769
- event: { kind, depth: 0 },
770
- ...extra,
771
- });
772
- }
773
- async rawControlEvent(kind, extra = {}) {
774
- const ingressOrdinal = ++this.ingressOrdinal;
775
- return this.enqueue(async () => {
776
- if (this.frozen)
777
- return;
778
- const event = this.makeEvent(kind, extra, ingressOrdinal, this.seq + 1);
779
- const size = Buffer.byteLength(JSON.stringify(event));
780
- this.seq++;
781
- this.bytes += size;
782
- try {
783
- await this.appendEvent(event);
784
- }
785
- catch (error) {
786
- this.seq--;
787
- this.bytes -= size;
788
- this.drop(error instanceof Error && error.name === "AbortError"
789
- ? "append_aborted"
790
- : "writer_failure", size, ingressOrdinal, false);
791
- throw error;
792
- }
793
- });
794
- }
795
- markFailure(kind, error) {
796
- this.failureClass = kind;
797
- this.errorCode = {
798
- capture_fatal: "CAPTURE_FATAL",
799
- attachment_fatal: "ATTACHMENT_FATAL",
800
- broker_fatal: "BROKER_FATAL",
801
- }[kind];
802
- this.state = "failed";
803
- if (!this.fatalNotified.has(kind)) {
804
- this.fatalNotified.add(kind);
805
- try {
806
- void Promise.resolve(this.options.onFatal?.({
807
- failureClass: kind,
808
- errorCode: this.errorCode,
809
- sessionId: this.options.sessionId,
810
- pid: this.options.pid,
811
- buildKey: this.options.buildKey,
812
- error,
813
- })).catch(() => undefined);
814
- }
815
- catch { /* reporting cannot mask the originating failure */ }
816
- }
817
- }
818
- async cleanup(reason, requestedCleanupDeadline) {
819
- if (this.cleanupTerminalComplete && this.cleanupPromise)
820
- return this.cleanupPromise;
821
- if (this.cleanupPromise)
822
- return this.cleanupPromise;
823
- this.receiveOpen = false;
824
- this.draining = true;
825
- this.cleanupPromise = (async () => {
826
- if (!this.cleanupIntendedTerminal) {
827
- this.cleanupIntendedTerminal =
828
- this.state === "target_exited"
829
- ? "target_exited"
830
- : this.state === "failed"
831
- ? "failed"
832
- : "stopped";
833
- }
834
- const terminal = this.cleanupIntendedTerminal;
835
- if (!this.cleanupCoreComplete) {
836
- this.state = "stopping";
837
- this.stoppedAt = this.clock.now();
838
- }
839
- const cleanupNow = () => this.clock.monotonicNow?.() ?? this.clock.now();
840
- const cleanupStartedAt = cleanupNow();
841
- const cleanupBudgetMs = Math.min(2_000, this.options.cleanupTimeoutMs ?? 2_000);
842
- const localDeadline = cleanupStartedAt + cleanupBudgetMs;
843
- const deadline = Math.min(localDeadline, requestedCleanupDeadline ?? localDeadline, this.options.cleanupDeadline ?? localDeadline);
844
- const effectiveCleanupBudgetMs = Math.max(0, deadline - cleanupStartedAt);
845
- const remainingCleanupMs = () => Math.max(0, deadline - cleanupNow());
846
- const errors = [];
847
- const steps = this.cleanupSteps;
848
- if (!this.cleanupCoreComplete)
849
- steps.push({
850
- step: "stop_accepting_events",
851
- status: "passed",
852
- remainingMs: effectiveCleanupBudgetMs,
853
- });
854
- let bestEffortTail = Promise.resolve();
855
- let tailDetached = false;
856
- const detachedOperations = [];
857
- const run = async (step, operation) => {
858
- const remaining = remainingCleanupMs();
859
- const pending = (tailDetached ? Promise.resolve() : bestEffortTail).then(operation);
860
- let timedOut = false;
861
- void pending.then(() => {
862
- if (timedOut)
863
- this.cleanupLateCompletions++;
864
- }, () => {
865
- if (timedOut)
866
- this.cleanupLateCompletions++;
867
- });
868
- bestEffortTail = pending.then(() => undefined, () => undefined);
869
- if (remaining === 0) {
870
- // Invoke the remaining best-effort operation even when its wait budget is exhausted.
871
- timedOut = true;
872
- tailDetached = true;
873
- detachedOperations.push({ step, promise: pending });
874
- bestEffortTail = Promise.resolve();
875
- this.cleanupDetachedOperations.push({
876
- step,
877
- status: "detached",
878
- detachedAt: this.clock.now(),
879
- });
880
- const failure = {
881
- step,
882
- errorCode: "CLEANUP_TIMEOUT",
883
- remainingMs: 0,
884
- };
885
- errors.push(failure);
886
- steps.push({ ...failure, status: "failed" });
887
- return undefined;
888
- }
889
- let timer;
890
- try {
891
- const deadlineWait = this.clock.sleep
892
- ? Promise.resolve()
893
- // Give the queued operation its first microtask before a
894
- // deterministic clock advances the aggregate deadline.
895
- .then(() => this.clock.sleep(remaining))
896
- .then(() => Promise.reject(new Error("CLEANUP_TIMEOUT")))
897
- : new Promise((_, reject) => {
898
- timer = setTimeout(() => reject(new Error("CLEANUP_TIMEOUT")), remaining);
899
- });
900
- const result = await Promise.race([
901
- pending,
902
- deadlineWait,
903
- ]);
904
- steps.push({
905
- step,
906
- status: "passed",
907
- remainingMs: remainingCleanupMs(),
908
- });
909
- return result;
910
- }
911
- catch (error) {
912
- const code = String(error).includes("CLEANUP_TIMEOUT")
913
- ? "CLEANUP_TIMEOUT"
914
- : "CLEANUP_STEP_FAILED";
915
- if (code === "CLEANUP_TIMEOUT") {
916
- timedOut = true;
917
- tailDetached = true;
918
- detachedOperations.push({ step, promise: pending });
919
- bestEffortTail = Promise.resolve();
920
- this.cleanupDetachedOperations.push({
921
- step,
922
- status: "detached",
923
- detachedAt: this.clock.now(),
924
- });
925
- }
926
- const failure = {
927
- step,
928
- errorCode: code,
929
- error: String(error),
930
- remainingMs: remainingCleanupMs(),
931
- };
932
- errors.push(failure);
933
- steps.push({ ...failure, status: "failed" });
934
- return undefined;
935
- }
936
- finally {
937
- if (timer)
938
- clearTimeout(timer);
939
- }
940
- };
941
- if (!this.cleanupCoreComplete) {
942
- const after = await run("after_snapshot", () => this.options.transport.snapshot?.("after"));
943
- if (after !== undefined)
944
- await run("append_after_snapshot", () => this.rawControlEvent("snapshot", {
945
- trigger: {
946
- phase: "after",
947
- workloadId: this.plan.workloadId,
948
- snapshotPhase: "after",
949
- },
950
- after,
951
- status: "confirmed",
952
- }));
953
- const cleaned = await run("trace_hook_script_cleanup", () => this.options.transport.cleanup(this.hooks));
954
- this.cleanupStatus = cleaned ?? {
955
- hooks: this.hooks.length,
956
- scripts: 1,
957
- interceptors: this.hooks.length,
958
- residualErrors: errors,
959
- };
960
- await run("append_cleanup_evidence", () => this.rawControlEvent("cleanup", {
961
- diff: { reason, ...this.cleanupStatus, steps, errors },
962
- }));
963
- await run("drain_append_queue", () => this.appendTail);
964
- if (this.appendInFlight > 0 &&
965
- detachedOperations.some(operation => operation.step === "drain_append_queue")) {
966
- this.abortPendingAppends();
967
- void this.appendTail.then(() => { this.cleanupLateCompletions++; }, () => { this.cleanupLateCompletions++; });
968
- errors.push({
969
- step: "append_abort",
970
- errorCode: "CLEANUP_TIMEOUT",
971
- error: "append queue was aborted and detached at the aggregate cleanup deadline",
972
- remainingMs: 0,
973
- });
974
- }
975
- const residual = this.cleanupStatus.hooks !== 0 ||
976
- this.cleanupStatus.scripts !== 0 ||
977
- this.cleanupStatus.interceptors !== 0 ||
978
- (this.cleanupStatus.residualErrors?.length ?? 0) > 0;
979
- this.cleanupCoreFailed = residual || errors.length > 0;
980
- if (this.cleanupCoreFailed)
981
- this.markFailure("capture_fatal");
982
- else
983
- this.state = terminal;
984
- this.cleanupStreamPrefixes =
985
- (await run("stream_prefixes", () => this.options.eventSink.prefixes?.() ?? [])) ?? [];
986
- this.cleanupFragment = (await run("write_manifest", () => this.writeManifest({
987
- streamPrefixes: this.cleanupStreamPrefixes,
988
- cleanupDeadlineMs: effectiveCleanupBudgetMs,
989
- cleanupSteps: steps,
990
- cleanupDetachedOperations: this.cleanupDetachedOperations,
991
- cleanupLateCompletions: this.cleanupLateCompletions,
992
- }))) ?? {
993
- ...this.manifest,
994
- streamPrefixes: this.cleanupStreamPrefixes,
995
- cleanupDeadlineMs: effectiveCleanupBudgetMs,
996
- cleanupSteps: steps,
997
- cleanupDetachedOperations: this.cleanupDetachedOperations,
998
- cleanupLateCompletions: this.cleanupLateCompletions,
999
- };
1000
- this.cleanupCoreComplete = true;
1001
- }
1002
- const streamPrefixes = this.cleanupStreamPrefixes;
1003
- const fragment = this.cleanupFragment ?? { ...this.manifest, streamPrefixes };
1004
- let detachSucceeded = this.options.detachOnCleanup === false;
1005
- if (this.options.detachOnCleanup === false) {
1006
- if (!steps.some(entry => entry.step === "detach_deferred_to_broker"))
1007
- steps.push({
1008
- step: "detach_deferred_to_broker",
1009
- status: "deferred",
1010
- ownership: "broker",
1011
- remainingMs: remainingCleanupMs(),
1012
- });
1013
- }
1014
- else {
1015
- const beforeDetachErrors = errors.length;
1016
- await run("detach", () => this.options.transport.detach?.());
1017
- detachSucceeded = errors.length === beforeDetachErrors;
1018
- }
1019
- if (!detachSucceeded) {
1020
- const detachResidual = {
1021
- step: "detach",
1022
- errorCode: "CLEANUP_STEP_FAILED",
1023
- residual: true,
1024
- };
1025
- this.cleanupStatus = {
1026
- ...this.cleanupStatus,
1027
- residualErrors: [
1028
- ...(this.cleanupStatus.residualErrors ?? []),
1029
- detachResidual,
1030
- ],
1031
- };
1032
- if (!steps.some(entry => entry.step === "detach" && entry.status === "failed")) {
1033
- steps.push({
1034
- step: "detach",
1035
- status: "failed",
1036
- errorCode: "CLEANUP_STEP_FAILED",
1037
- residual: true,
1038
- remainingMs: remainingCleanupMs(),
1039
- });
1040
- }
1041
- this.cleanupDetachRetryable = !this.cleanupCoreFailed;
1042
- this.markFailure("capture_fatal");
1043
- }
1044
- else {
1045
- this.cleanupDetachRetryable = false;
1046
- if (this.cleanupStatus.residualErrors)
1047
- this.cleanupStatus = {
1048
- ...this.cleanupStatus,
1049
- residualErrors: this.cleanupStatus.residualErrors.filter(entry => !isRecord(entry) || entry.step !== "detach"),
1050
- };
1051
- if (!this.cleanupCoreFailed) {
1052
- this.failureClass = undefined;
1053
- this.errorCode = undefined;
1054
- this.state = terminal;
1055
- }
1056
- }
1057
- let freezeSucceeded = false;
1058
- let freezeFailure;
1059
- const freezeController = new AbortController();
1060
- let finalPrefixes = streamPrefixes;
1061
- if (detachSucceeded && !this.cleanupCoreFailed && errors.length === 0 && this.appendInFlight === 0) {
1062
- finalPrefixes =
1063
- (await run("final_stream_prefixes", () => this.options.eventSink.prefixes?.() ?? streamPrefixes)) ?? streamPrefixes;
1064
- }
1065
- const freezeRemaining = remainingCleanupMs();
1066
- // Preserve part of the same aggregate deadline for publishing canonical
1067
- // failure evidence if freeze hangs. Detach remains best-effort even if
1068
- // both publication steps consume their bounded waits.
1069
- // Reserve explicit time in the same absolute deadline for terminal
1070
- // failure commit and publication quiescence. A freeze must not consume
1071
- // the entire cleanup window.
1072
- const FAILURE_COMMIT_RESERVE_MS = 500;
1073
- const QUIESCENCE_RESERVE_MS = 100;
1074
- const freezeWaitMs = Math.max(0, freezeRemaining - FAILURE_COMMIT_RESERVE_MS - QUIESCENCE_RESERVE_MS);
1075
- if (detachSucceeded && !this.cleanupCoreFailed && errors.length === 0 && this.appendInFlight === 0 && freezeWaitMs > 0) {
1076
- let timer;
1077
- let freezeTimedOut = false;
1078
- try {
1079
- const frozenFragment = {
1080
- ...fragment,
1081
- ...this.manifest,
1082
- streamPrefixes: finalPrefixes,
1083
- };
1084
- const pending = this.options.manifestSink?.freeze
1085
- ? Promise.resolve(this.options.manifestSink.freeze(frozenFragment, freezeController.signal))
1086
- : Promise.resolve(this.options.manifestSink?.write(frozenFragment)).then(() => ({
1087
- committed: true,
1088
- terminalStatus: "frozen",
1089
- }));
1090
- void pending.then(() => { if (freezeTimedOut)
1091
- this.cleanupLateCompletions++; }, () => { if (freezeTimedOut)
1092
- this.cleanupLateCompletions++; });
1093
- const timeout = this.clock.sleep
1094
- ? this.clock.sleep(freezeWaitMs).then(() => { throw new Error("FREEZE_TIMEOUT"); })
1095
- : new Promise((_, reject) => {
1096
- timer = setTimeout(() => reject(new Error("FREEZE_TIMEOUT")), freezeWaitMs);
1097
- });
1098
- const receipt = await Promise.race([pending, timeout]);
1099
- if (!receipt?.committed || receipt.terminalStatus !== "frozen")
1100
- throw new Error(`TERMINAL_FREEZE_NOT_COMMITTED:${receipt?.terminalStatus ?? "unknown"}`);
1101
- freezeSucceeded = true;
1102
- this.terminalStatus = "frozen";
1103
- this.frozen = true;
1104
- steps.push({ step: "freeze_artifacts", status: "passed", remainingMs: remainingCleanupMs() });
1105
- }
1106
- catch (error) {
1107
- freezeFailure = error;
1108
- if (String(error).includes("FREEZE_TIMEOUT")) {
1109
- freezeTimedOut = true;
1110
- this.cleanupDetachedOperations.push({
1111
- step: "freeze_artifacts",
1112
- status: "detached",
1113
- detachedAt: this.clock.now(),
1114
- });
1115
- }
1116
- freezeController.abort(error);
1117
- steps.push({ step: "freeze_artifacts", status: "failed", errorCode: String(error).includes("FREEZE_TIMEOUT") ? "CLEANUP_TIMEOUT" : "CLEANUP_STEP_FAILED", error: String(error), remainingMs: remainingCleanupMs() });
1118
- }
1119
- finally {
1120
- if (timer)
1121
- clearTimeout(timer);
1122
- }
1123
- }
1124
- else {
1125
- freezeFailure = new Error("FREEZE_PRECONDITION_FAILED");
1126
- freezeController.abort(freezeFailure);
1127
- steps.push({ step: "freeze_artifacts", status: "failed", errorCode: "FREEZE_PRECONDITION_FAILED", remainingMs: freezeRemaining });
1128
- }
1129
- if (!freezeSucceeded) {
1130
- freezeFailure ??= new Error("CAPTURE_TERMINAL_FREEZE_FAILED");
1131
- errors.push({ step: "freeze_artifacts", errorCode: String(freezeFailure).includes("FREEZE_TIMEOUT") ? "CLEANUP_TIMEOUT" : "CAPTURE_TERMINAL_FREEZE_FAILED", error: String(freezeFailure), remainingMs: remainingCleanupMs() });
1132
- this.markFailure("capture_fatal");
1133
- const terminalFailure = {
1134
- ...fragment,
1135
- ...this.manifest,
1136
- frozen: false,
1137
- terminal: true,
1138
- terminalStatus: "failed",
1139
- terminalFailure: {
1140
- errorCode: "CAPTURE_TERMINAL_FREEZE_FAILED",
1141
- error: String(freezeFailure),
1142
- recoverable: true,
1143
- nextAction: "retry artifact freeze from durable stream prefixes",
1144
- },
1145
- cleanupSteps: steps,
1146
- cleanupDetachedOperations: this.cleanupDetachedOperations,
1147
- cleanupLateCompletions: this.cleanupLateCompletions,
1148
- };
1149
- // Publishing failure evidence shares the aggregate cleanup deadline.
1150
- // A stuck sink is detached so transport detach is still issued; the
1151
- // frozen session state remains authoritative if that sink settles late.
1152
- const failureController = new AbortController();
1153
- const FAILURE_QUIESCENCE_RESERVE_MS = 100;
1154
- const failureRemaining = remainingCleanupMs();
1155
- const failurePublicationWaitMs = Math.max(0, failureRemaining - FAILURE_QUIESCENCE_RESERVE_MS);
1156
- let failureTimer;
1157
- let failureWaitActive = true;
1158
- await run("publish_terminal_failure", async () => {
1159
- const publication = this.options.manifestSink?.terminalFailure
1160
- ? Promise.resolve(this.options.manifestSink.terminalFailure(terminalFailure, failureController.signal))
1161
- : Promise.resolve(this.options.manifestSink?.write(terminalFailure)).then(() => ({
1162
- committed: true,
1163
- terminalStatus: "failed",
1164
- }));
1165
- // Start the guarded writer before its abort timer. This gives the
1166
- // publication a real commit opportunity while still reserving time
1167
- // for its abort-aware unlink path before the aggregate deadline.
1168
- if (failurePublicationWaitMs === 0) {
1169
- failureController.abort(new Error("CLEANUP_TIMEOUT"));
1170
- }
1171
- else if (this.clock.sleep) {
1172
- void this.clock.sleep(failurePublicationWaitMs).then(() => {
1173
- if (failureWaitActive)
1174
- failureController.abort(new Error("CLEANUP_TIMEOUT"));
1175
- });
1176
- }
1177
- else {
1178
- failureTimer = setTimeout(() => {
1179
- if (failureWaitActive)
1180
- failureController.abort(new Error("CLEANUP_TIMEOUT"));
1181
- }, failurePublicationWaitMs);
1182
- }
1183
- const receipt = await publication;
1184
- if (!receipt.committed || receipt.terminalStatus !== "failed")
1185
- throw new Error(`TERMINAL_FAILURE_NOT_COMMITTED:${receipt.terminalStatus}`);
1186
- this.terminalStatus = "failed";
1187
- return receipt;
1188
- });
1189
- failureWaitActive = false;
1190
- if (failureTimer)
1191
- clearTimeout(failureTimer);
1192
- }
1193
- await run("quiesce_manifest_publications", () => this.options.manifestSink?.quiesce?.());
1194
- this.draining = false;
1195
- if (!this.cleanupDetachRetryable)
1196
- this.cleanupTerminalComplete = true;
1197
- if ((errors.length || this.cleanupCoreFailed || !freezeSucceeded) && !freezeSucceeded)
1198
- throw new Error("CAPTURE_CLEANUP_INCOMPLETE");
1199
- })();
1200
- try {
1201
- await this.cleanupPromise;
1202
- }
1203
- finally {
1204
- if (this.cleanupDetachRetryable && !this.cleanupTerminalComplete)
1205
- this.cleanupPromise = undefined;
1206
- }
1207
- }
1208
- enqueue(operation) {
1209
- this.appendInFlight++;
1210
- const result = this.appendTail.then(async () => {
1211
- try {
1212
- return await operation();
1213
- }
1214
- finally {
1215
- this.appendInFlight--;
1216
- }
1217
- });
1218
- this.appendTail = result.then(() => undefined, () => undefined);
1219
- // The session owns the canonical append promise; detached callers may
1220
- // observe the operation rejection, but it must never become an
1221
- // unhandled rejection that escapes terminal cleanup.
1222
- void result.catch(() => undefined);
1223
- return result;
1224
- }
1225
- abortPendingAppends() {
1226
- this.appendTransactionsAborted = true;
1227
- for (const controller of this.appendControllers)
1228
- controller.abort();
1229
- }
1230
- makeEvent(kind, input, ingressOrdinal, seq) {
1231
- const target = isRecord(input.target) ? input.target : {};
1232
- const trigger = isRecord(input.trigger) ? input.trigger : {};
1233
- const rawEvent = isRecord(input.event) ? input.event : {};
1234
- return {
1235
- seq,
1236
- ingressOrdinal,
1237
- sessionId: this.options.sessionId,
1238
- buildKey: this.options.buildKey,
1239
- pid: this.options.pid,
1240
- moduleBase: this.options.moduleBase,
1241
- target: {
1242
- kind: TargetKindSchema.parse(target.kind ?? this.selector.kind),
1243
- name: typeof target.name === "string" ? target.name : null,
1244
- rva: typeof target.rva === "string" ? target.rva : null,
1245
- runtimeAddress: typeof target.runtimeAddress === "string"
1246
- ? target.runtimeAddress
1247
- : null,
1248
- },
1249
- environment: this.environmentInfo.value,
1250
- trigger: {
1251
- workloadId: typeof trigger.workloadId === "string"
1252
- ? trigger.workloadId
1253
- : this.plan.workloadId,
1254
- phase: trigger.phase ?? this.phase,
1255
- userAction: trigger.userAction ?? null,
1256
- ...trigger,
1257
- },
1258
- event: {
1259
- kind,
1260
- timestamp: new Date(this.clock.now()).toISOString(),
1261
- threadId: typeof rawEvent.threadId === "number" ? rawEvent.threadId : null,
1262
- depth: typeof rawEvent.depth === "number" ? rawEvent.depth : 0,
1263
- invocationId: typeof rawEvent.invocationId === "string"
1264
- ? rawEvent.invocationId
1265
- : null,
1266
- parentInvocationId: typeof rawEvent.parentInvocationId === "string"
1267
- ? rawEvent.parentInvocationId
1268
- : null,
1269
- recursive: rawEvent.recursive === true,
1270
- monotonicTimestamp: typeof rawEvent.monotonicTimestamp === "number"
1271
- ? rawEvent.monotonicTimestamp
1272
- : this.clock.monotonicNow?.() ?? this.clock.now(),
1273
- },
1274
- arguments: input.arguments ?? null,
1275
- returnValue: input.returnValue ?? null,
1276
- before: input.before ?? null,
1277
- after: input.after ?? null,
1278
- diff: input.diff ?? null,
1279
- callStack: Array.isArray(input.callStack) ? input.callStack : [],
1280
- evidence: Array.isArray(input.evidence) ? input.evidence : [],
1281
- status: input.status === "confirmed" ||
1282
- input.status === "dynamic" ||
1283
- input.status === "unresolved"
1284
- ? input.status
1285
- : "partial",
1286
- };
1287
- }
1288
- async writeManifest(extra = {}) {
1289
- const fragment = { ...this.manifest, ...extra };
1290
- if (this.options.manifestSink)
1291
- await this.options.manifestSink.write(fragment);
1292
- return fragment;
1293
- }
1294
- }
1295
- function phaseOf(trigger) {
1296
- const phase = trigger.phase ?? "during";
1297
- if (phase !== "before" && phase !== "during" && phase !== "after")
1298
- throw new Error("TRIGGER_PHASE_INVALID");
1299
- return phase;
1300
- }
1301
- function safeNonNegativeInteger(value) {
1302
- return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : 0;
1303
- }
1304
- function isAbortError(value) {
1305
- return !!value && typeof value === "object" &&
1306
- ("name" in value && value.name === "AbortError");
1307
- }
1308
- function isRecord(value) {
1309
- return value !== null && typeof value === "object" && !Array.isArray(value);
1310
- }
1311
- function stripSelectorMetadata(selector) {
1312
- return {
1313
- combine: selector.combine,
1314
- ...(selector.apis?.length ? { apis: selector.apis } : {}),
1315
- ...(selector.namespaces?.length || selector.namespace
1316
- ? {
1317
- namespaces: [
1318
- ...(selector.namespaces ?? []),
1319
- ...(selector.namespace ? [selector.namespace] : []),
1320
- ],
1321
- }
1322
- : {}),
1323
- ...(selector.globs?.length || selector.glob
1324
- ? {
1325
- globs: [
1326
- ...(selector.globs ?? []),
1327
- ...(selector.glob ? [selector.glob] : []),
1328
- ],
1329
- }
1330
- : {}),
1331
- ...(selector.rvas?.length || selector.rva
1332
- ? {
1333
- rvas: [
1334
- ...(selector.rvas ?? []),
1335
- ...(selector.rva ? [selector.rva] : []),
1336
- ],
1337
- }
1338
- : {}),
1339
- ...(selector.dataSourceIds?.length
1340
- ? { dataSourceIds: selector.dataSourceIds }
1341
- : {}),
1342
- ...(selector.objectIds?.length ? { objectIds: selector.objectIds } : {}),
1343
- ...(selector.ranges?.length ? { ranges: selector.ranges } : {}),
1344
- maxTargets: selector.maxTargets,
1345
- };
1346
- }
1347
- function selectorTargets(selector) {
1348
- return [
1349
- ...(selector.apis ?? []).map((value) => ({ api: value })),
1350
- ...(selector.namespaces ?? []).map((value) => ({ namespace: value })),
1351
- ...(selector.globs ?? []).map((value) => ({ glob: value })),
1352
- ...(selector.rvas ?? []).map((value) => ({ rva: value })),
1353
- ...(selector.dataSourceIds ?? []).map((value) => ({ dataSourceId: value })),
1354
- ...(selector.objectIds ?? []).map((value) => ({ objectId: value })),
1355
- ...(selector.ranges ?? []).map((value) => ({ ...value })),
1356
- ];
1357
- }