wowdump 0.2.1 → 0.3.1

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +56 -118
  3. package/dist/agent.js +5 -8
  4. package/dist/cli.js +497 -0
  5. package/dist/discovery.js +1 -12
  6. package/dist/dry-run.js +0 -2
  7. package/dist/focused-session.js +61 -1329
  8. package/dist/frida-runtime.js +51 -73
  9. package/dist/frida-worker.js +100 -0
  10. package/dist/ghidra.js +769 -0
  11. package/dist/main.js +66 -0
  12. package/dist/processes.js +2 -5
  13. package/dist/reader-broker.js +460 -0
  14. package/dist/reader-client.js +1 -0
  15. package/dist/reader-main.js +67 -0
  16. package/dist/session.js +17 -120
  17. package/dist/toolchain.js +594 -0
  18. package/dist/windows-launcher.js +207 -0
  19. package/dist/windows-reader.js +102 -0
  20. package/dist/wow-analysis.js +211 -236
  21. package/package.json +18 -35
  22. package/skills/wowdump/SKILL.md +15 -0
  23. package/skills/wowdump/commands.md +44 -0
  24. package/dist/analysis-path.js +0 -38
  25. package/dist/analysis-process-log.js +0 -146
  26. package/dist/broker-client.js +0 -411
  27. package/dist/broker-codec.js +0 -148
  28. package/dist/broker-core.js +0 -1045
  29. package/dist/broker-gateway.js +0 -447
  30. package/dist/broker-ledger.js +0 -196
  31. package/dist/broker-main.js +0 -291
  32. package/dist/broker-protocol.js +0 -119
  33. package/dist/broker-runtime.js +0 -1283
  34. package/dist/broker-server.js +0 -466
  35. package/dist/build-bundle-loader.js +0 -183
  36. package/dist/build-bundle.js +0 -11
  37. package/dist/focus-errors.js +0 -63
  38. package/dist/focus-service.js +0 -1855
  39. package/dist/mcp-main.js +0 -51
  40. package/dist/mcp.js +0 -924
  41. package/dist/process-log-lock.js +0 -195
  42. package/dist/runtime-config.js +0 -399
  43. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  44. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  45. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  46. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  47. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
@@ -1,1357 +1,89 @@
1
1
  import { createHash } from "node:crypto";
2
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({
3
+ export const TargetKindSchema = z.enum(["lua_wrapper", "cpp_function", "data_source", "object", "address_range"]);
4
+ export const FocusSelectorSchema = z.object({
16
5
  schema: z.literal("selector-schema.v1").default("selector-schema.v1"),
17
6
  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.
7
+ apis: z.array(z.string().min(1)).optional(),
28
8
  namespace: z.string().min(1).optional(),
29
9
  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
- }
10
+ rva: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
11
+ address: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
12
+ start: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
13
+ end: z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
14
+ }).superRefine((v, ctx) => {
15
+ if (v.kind === "address_range" && (!v.start || !v.end))
16
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "address_range requires start and end" });
17
+ if (v.kind !== "address_range" && v.kind !== "object" && !v.apis?.length && !v.namespace && !v.glob && !v.rva && !v.address)
18
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "selector must identify a target" });
19
+ });
20
+ export const validateSelector = (input) => FocusSelectorSchema.parse(input);
134
21
  /** Canonical byte representation used for selector identity and replay checks. */
135
22
  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
- }
23
+ const value = validateSelector(input);
24
+ const ordered = {};
25
+ for (const key of ["schema", "kind", "apis", "namespace", "glob", "rva", "address", "start", "end"]) {
26
+ if (value[key] !== undefined)
27
+ ordered[key] = Array.isArray(value[key]) ? [...value[key]].sort() : value[key];
196
28
  }
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(),
29
+ return JSON.stringify(ordered);
290
30
  };
31
+ const systemClock = { now: () => Date.now() };
291
32
  export class FocusedSession {
292
33
  state = "created";
293
34
  seq = 0;
294
- ingressOrdinal = 0;
295
35
  bytes = 0;
296
36
  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 = {};
37
+ depth = new Map();
38
+ dropped = 0;
307
39
  startedAt = 0;
308
40
  stoppedAt;
309
- pausedAt;
310
- pauseGaps = [];
311
41
  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
42
  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;
43
+ constructor(options) { validateSelector(options.target); if (!Number.isInteger(options.pid) || options.pid <= 0)
44
+ throw new Error("pid must be positive"); this.options = options; }
45
+ get status() { return this.state; }
46
+ get manifest() { return { schema: "wow.focus-session.v1", sessionId: this.options.sessionId, pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase, target: this.options.target, environment: this.options.environment ?? {}, triggerPlan: this.options.triggerPlan ?? {}, startTime: this.startedAt ? new Date(this.startedAt).toISOString() : null, stopTime: this.stoppedAt ? new Date(this.stoppedAt).toISOString() : null, hookIds: this.hooks, droppedEventCount: this.dropped, cleanupStatus: this.cleanupStatus, state: this.state }; }
47
+ async start() { if (this.state !== "created")
48
+ throw new Error("session already started"); await this.options.transport.revalidateIdentity(this.identity()); this.hooks = await this.options.transport.installHooks(this.options.target); this.startedAt = this.clock.now(); this.state = "running"; await this.writeManifest(); return this.manifest; }
49
+ async pause() { await this.revalidate(); if (this.state !== "running")
50
+ throw new Error("session is not running"); this.state = "paused"; await this.writeManifest(); }
51
+ async resume() { await this.revalidate(); if (this.state !== "paused")
52
+ throw new Error("session is not paused"); this.state = "running"; await this.writeManifest(); }
53
+ async checkpoint(label = "checkpoint") { await this.revalidate(); await this.writeManifest({ checkpoint: label, seq: this.seq }); return this.manifest; }
54
+ async emit(input) {
55
+ if (this.state !== "running") {
56
+ this.dropped++;
57
+ return false;
469
58
  }
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;
59
+ if (this.options.sampling !== undefined && this.options.sampling < 1 && Math.random() > this.options.sampling) {
60
+ this.dropped++;
61
+ return false;
546
62
  }
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);
63
+ const event = { ...input, seq: this.seq + 1, sessionId: this.options.sessionId, buildKey: this.options.buildKey, pid: this.options.pid, moduleBase: this.options.moduleBase, environment: this.options.environment ?? {}, event: { ...input.event, timestamp: new Date(this.clock.now()).toISOString(), depth: input.event.depth ?? 0 } };
589
64
  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);
65
+ if ((this.options.maxEvents && this.seq >= this.options.maxEvents) || (this.options.maxBytes && this.bytes + size > this.options.maxBytes)) {
66
+ this.dropped++;
67
+ return false;
68
+ }
594
69
  this.seq++;
595
70
  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
- }
71
+ await this.options.eventSink.append(event);
607
72
  return true;
608
73
  }
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];
74
+ async enter(threadId, input) { const d = (this.depth.get(threadId) ?? 0) + 1; this.depth.set(threadId, d); return this.emit({ ...input, event: { kind: "enter", threadId, depth: d } }); }
75
+ async leave(threadId, input) { const d = this.depth.get(threadId) ?? 1; const ok = await this.emit({ ...input, event: { kind: "leave", threadId, depth: d } }); if (d <= 1)
76
+ this.depth.delete(threadId);
77
+ else
78
+ this.depth.set(threadId, d - 1); return ok; }
79
+ async stop() { if (this.state === "stopped")
80
+ return this.manifest; this.state = "stopped"; this.stoppedAt = this.clock.now(); this.cleanupStatus = await this.options.transport.cleanup(this.hooks); await this.options.transport.detach?.(); await this.writeManifest(); return this.manifest; }
81
+ get clock() { return this.options.clock ?? systemClock; }
82
+ identity() { return { pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase }; }
83
+ async revalidate() { const actual = await this.options.transport.revalidateIdentity(this.identity()); if (actual.pid !== this.options.pid || actual.buildKey !== this.options.buildKey || actual.moduleBase.toLowerCase() !== this.options.moduleBase.toLowerCase()) {
802
84
  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
- ];
85
+ throw new Error("TARGET_IDENTITY_CHANGED");
86
+ } }
87
+ async writeManifest(extra = {}) { if (this.options.manifestSink)
88
+ await this.options.manifestSink.write({ ...this.manifest, ...extra, manifestHash: createHash("sha256").update(JSON.stringify(this.manifest)).digest("hex") }); }
1357
89
  }