rea-agents 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/dist/application/ArtifactExtraction.js +5 -1
  3. package/dist/application/ArtifactGraphConstruction.js +3 -15
  4. package/dist/application/ArtifactInventory/classify.js +5 -1
  5. package/dist/application/ArtifactInventory/reader.js +2 -0
  6. package/dist/application/BinarySessionStatus.js +1 -0
  7. package/dist/application/CapabilityInventory.js +11 -3
  8. package/dist/application/ProcessCaptureEnvironment.js +26 -0
  9. package/dist/application/ProcessCaptureLifecycle.js +1 -0
  10. package/dist/application/ProcessCheckpoints.js +5 -0
  11. package/dist/application/ProcessHarness.js +45 -34
  12. package/dist/application/ProcessNormalization.js +6 -0
  13. package/dist/application/ProcessReactiveCoordinator.js +10 -3
  14. package/dist/application/ProcessReactiveEffects.js +9 -1
  15. package/dist/application/ProcessReactiveHarness.js +85 -0
  16. package/dist/application/ProcessReactiveObservations.js +101 -16
  17. package/dist/contracts/toolOutputSchemaPrimitives.js +3 -0
  18. package/dist/domain/analysisSnapshot.js +2 -0
  19. package/dist/domain/artifactGraph.js +2 -0
  20. package/dist/domain/binaryTarget.js +3 -6
  21. package/dist/domain/evidence.js +2 -0
  22. package/dist/domain/processCapture.js +64 -0
  23. package/dist/domain/processCaptureReactiveValidation.js +175 -0
  24. package/dist/domain/processCaptureValidation.js +2 -0
  25. package/dist/domain/processObservation.js +15 -6
  26. package/dist/domain/processReactiveRuntime.js +21 -10
  27. package/dist/domain/processReactiveTransition.js +8 -6
  28. package/dist/domain/processScenario.js +21 -0
  29. package/dist/domain/zipPackageFormat.js +20 -0
  30. package/dist/generatedPackageMetadata.js +1 -1
  31. package/dist/native/NativeMacOSProvider.js +1 -0
  32. package/package.json +1 -1
  33. package/skills/reverse-engineer-anything/SKILL.md +3 -2
  34. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
package/README.md CHANGED
@@ -376,7 +376,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
376
376
  | Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations |
377
377
  | Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery |
378
378
  | Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
379
- | Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree |
379
+ | Artifact graph | 2 | deterministic directory, ZIP/APK/IPA/MSIX/AppX, and ASAR inventory; explicitly selected extraction into an absent owned tree |
380
380
  | Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
381
381
  | Browser observation | 8 | exact-origin CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, and visual evidence |
382
382
  | Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
@@ -479,7 +479,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
479
479
  "mcpServers": {
480
480
  "rea": {
481
481
  "command": "npx",
482
- "args": ["-y", "rea-agents@2.3.0", "mcp"]
482
+ "args": ["-y", "rea-agents@2.4.0", "mcp"]
483
483
  }
484
484
  }
485
485
  }
@@ -140,7 +140,11 @@ const createReader = async (path, format) => {
140
140
  return new DirectoryArtifactReader(path);
141
141
  if (format === "asar")
142
142
  return new AsarArtifactReader(path);
143
- if (format === "ipa" || format === "apk" || format === "zip")
143
+ if (format === "ipa" ||
144
+ format === "apk" ||
145
+ format === "msix" ||
146
+ format === "appx" ||
147
+ format === "zip")
144
148
  return new ZipArtifactReader(path, format);
145
149
  if (format === "mach-o")
146
150
  return new MachOSliceArtifactReader(path);
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { extname } from "node:path";
3
2
  import canonicalize from "canonicalize";
3
+ import { zipPackageFormatForPath } from "../domain/zipPackageFormat.js";
4
4
  /** Index the first occurrence for each canonical logical path. */
5
5
  export const indexOccurrencesByPath = (occurrences) => {
6
6
  const byPath = new Map();
@@ -74,7 +74,7 @@ export const createRootNode = (input) => createArtifactNode({
74
74
  }),
75
75
  size: input.digest?.bytes ?? 0,
76
76
  kind: input.directory ||
77
- ["zip", "ipa", "apk", "asar", "dmg", "pkg"].includes(input.format)
77
+ ["zip", "ipa", "apk", "msix", "appx", "asar", "dmg", "pkg"].includes(input.format)
78
78
  ? "container"
79
79
  : classifyArtifactPath(input.path).kind,
80
80
  format: input.format,
@@ -174,7 +174,7 @@ export const classifyArtifactPath = (path) => {
174
174
  return { kind: "javascript", format: "javascript-bundle" };
175
175
  if (lower.endsWith(".asar"))
176
176
  return { kind: "container", format: "asar" };
177
- const archiveFormat = archiveFormatForExtension(extname(lower));
177
+ const archiveFormat = zipPackageFormatForPath(lower);
178
178
  if (archiveFormat !== undefined)
179
179
  return { kind: "container", format: archiveFormat };
180
180
  if (/\.framework(?:\/|$)/u.test(lower))
@@ -251,16 +251,4 @@ const relationFor = (path) => {
251
251
  return "embeds";
252
252
  return "contains";
253
253
  };
254
- const archiveFormatForExtension = (extension) => {
255
- switch (extension) {
256
- case ".zip":
257
- return "zip";
258
- case ".ipa":
259
- return "ipa";
260
- case ".apk":
261
- return "apk";
262
- default:
263
- return undefined;
264
- }
265
- };
266
254
  const depth = (path) => path.split("/").length;
@@ -1,8 +1,12 @@
1
1
  import { open } from "node:fs/promises";
2
2
  import { classifyArtifactPath } from "../ArtifactGraphConstruction.js";
3
+ import { zipPackageFormatForPath } from "../../domain/zipPackageFormat.js";
3
4
  const classifyContainerExtension = (path) => {
4
5
  const lower = path.toLowerCase();
5
- for (const format of ["asar", "ipa", "apk", "zip", "dmg", "pkg"])
6
+ const zipPackage = zipPackageFormatForPath(lower);
7
+ if (zipPackage !== undefined)
8
+ return zipPackage;
9
+ for (const format of ["asar", "dmg", "pkg"])
6
10
  if (lower.endsWith(`.${format}`))
7
11
  return format;
8
12
  return undefined;
@@ -11,6 +11,8 @@ export const createReader = async (path, format, nativeMount, signal) => {
11
11
  case "zip":
12
12
  case "ipa":
13
13
  case "apk":
14
+ case "msix":
15
+ case "appx":
14
16
  return new ZipArtifactReader(path, format);
15
17
  case "asar":
16
18
  return new AsarArtifactReader(path);
@@ -91,6 +91,7 @@ const capabilityStatus = (descriptor) => ({
91
91
  operation: descriptor.operation,
92
92
  available: descriptor.available,
93
93
  reason: descriptor.reason,
94
+ availability_code: descriptor.availabilityCode ?? null,
94
95
  input_contract_version: descriptor.inputContractVersion,
95
96
  output_contract_version: descriptor.outputContractVersion,
96
97
  pagination: descriptor.pagination,
@@ -8,6 +8,7 @@ const statusSchema = z.object({
8
8
  operation: z.string(),
9
9
  available: z.boolean(),
10
10
  reason: z.string().nullable(),
11
+ availability_code: z.string().nullable().optional(),
11
12
  })),
12
13
  });
13
14
  const ENHANCED_REQUIREMENTS = {
@@ -35,7 +36,14 @@ const ENHANCED_REQUIREMENTS = {
35
36
  /** Build stable per-operation availability without hiding familiar tools. */
36
37
  export const buildCapabilityInventory = (sessionStatus, policy) => {
37
38
  const status = statusSchema.parse(sessionStatus);
38
- const descriptors = new Map(status.capabilities.map((descriptor) => [descriptor.operation, descriptor]));
39
+ const descriptors = new Map(status.capabilities.map((descriptor) => [
40
+ descriptor.operation,
41
+ {
42
+ available: descriptor.available,
43
+ reason: descriptor.reason,
44
+ availability_code: descriptor.availability_code ?? null,
45
+ },
46
+ ]));
39
47
  return TOOL_CONTRACTS.map((contract) => {
40
48
  const availability = availabilityFor({
41
49
  name: contract.name,
@@ -157,7 +165,7 @@ const providerAvailability = ({ descriptors, kind, name, }) => {
157
165
  remediation: "Install or configure a provider that declares this operation.",
158
166
  };
159
167
  if (!descriptor.available &&
160
- descriptor.reason?.toLowerCase().includes("require macos") === true)
168
+ descriptor.availability_code === "unsupported_host")
161
169
  return {
162
170
  reason: "unsupported_host",
163
171
  remediation: descriptor.reason,
@@ -187,7 +195,7 @@ const composedAvailability = (name, descriptors) => {
187
195
  .find((descriptor) => descriptor?.available === false);
188
196
  if (unavailable !== undefined)
189
197
  return {
190
- reason: unavailable.reason?.toLowerCase().includes("require macos") === true
198
+ reason: unavailable.availability_code === "unsupported_host"
191
199
  ? "unsupported_host"
192
200
  : "provider_unavailable",
193
201
  remediation: unavailable.reason ?? "Choose another target or configured provider.",
@@ -0,0 +1,26 @@
1
+ /** Build the explicitly admitted environment for a captured process. */
2
+ export const makeProcessCaptureEnvironment = (options) => {
3
+ const { scenario, home, replay, shimReplay, runId } = options;
4
+ const environment = {
5
+ ...scenario.environment,
6
+ HOME: home,
7
+ TERM: "xterm-256color",
8
+ REA_PROCESS_RUN_ID: runId,
9
+ };
10
+ for (const name of scenario.inherit_environment) {
11
+ const value = process.env[name];
12
+ if (value !== undefined)
13
+ environment[name] = value;
14
+ }
15
+ environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
16
+ environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
17
+ environment.REA_SHIM_LEDGER_URL = shimReplay.url;
18
+ // PATH is evidence-producing input. Inheriting the host PATH here would let
19
+ // the authority and reconstruction probe different tools without recording
20
+ // that difference. Scenarios must opt in through environment or
21
+ // inherit_environment; deterministic shims always take precedence.
22
+ environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
23
+ .filter((part) => part.length > 0)
24
+ .join(":");
25
+ return environment;
26
+ };
@@ -33,6 +33,7 @@ export const buildCaptureResult = (options) => ({
33
33
  shim_events: options.shimEvents,
34
34
  protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
35
35
  replay_transitions: options.replay.transitions,
36
+ reactive_run: options.reactiveRun,
36
37
  event_journal: options.eventJournal,
37
38
  files_before: options.before.files,
38
39
  files_after: options.after.files,
@@ -79,6 +79,11 @@ export class ProcessCheckpoints {
79
79
  capture(name) {
80
80
  this.#scheduleCapture(name, this.#signal);
81
81
  }
82
+ /** Read one committed checkpoint for synchronous journal projection. */
83
+ at(index) {
84
+ const checkpoint = this.#captures[index];
85
+ return checkpoint === undefined ? undefined : structuredClone(checkpoint);
86
+ }
82
87
  #scheduleCapture(name, signal) {
83
88
  if (this.#captured.has(name))
84
89
  return;
@@ -8,37 +8,14 @@ import { snapshotRoots } from "./FilesystemSnapshot.js";
8
8
  import { ProcessCheckpoints } from "./ProcessCheckpoints.js";
9
9
  import { TerminalRenderer } from "./TerminalRenderer.js";
10
10
  import { startCommandShimReplay, } from "./CommandShimReplay.js";
11
- import { normalizeProcessText } from "./ProcessNormalization.js";
11
+ import { normalizeProcessShimEvent, normalizeProcessText, } from "./ProcessNormalization.js";
12
12
  import { selectCapturedProcessGroupIds } from "../process/ProcessOwnership.js";
13
13
  import { awaitTerminalExit, buildCaptureResult, captureTerminalFrames, createRunManifest, observeSettlement, prepareProcessCapture, releaseProcessResources, resolveProcessResult, } from "./ProcessCaptureLifecycle.js";
14
14
  import { assertNotCancelled } from "./ProcessCaptureAuthority.js";
15
15
  import { createProcessCaptureJournal, scheduleScenarioInteractions, } from "./ProcessCaptureJournal.js";
16
+ import { assertSupportedReactiveScenario, projectProcessReactiveRun, startProcessReactiveHarness, } from "./ProcessReactiveHarness.js";
17
+ import { makeProcessCaptureEnvironment } from "./ProcessCaptureEnvironment.js";
16
18
  export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js";
17
- const makeEnvironment = (options) => {
18
- const { scenario, home, replay, shimReplay, runId } = options;
19
- const environment = {
20
- ...scenario.environment,
21
- HOME: home,
22
- TERM: "xterm-256color",
23
- REA_PROCESS_RUN_ID: runId,
24
- };
25
- for (const name of scenario.inherit_environment) {
26
- const value = process.env[name];
27
- if (value !== undefined)
28
- environment[name] = value;
29
- }
30
- environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
31
- environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
32
- environment.REA_SHIM_LEDGER_URL = shimReplay.url;
33
- // PATH is evidence-producing input. Inheriting the host PATH here would let
34
- // the authority and reconstruction probe different tools without recording
35
- // that difference. Scenarios must opt in through environment or
36
- // inherit_environment; deterministic shims always take precedence.
37
- environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
38
- .filter((part) => part.length > 0)
39
- .join(":");
40
- return environment;
41
- };
42
19
  const cleanupFailedStartup = async (options) => {
43
20
  options.terminal?.kill("SIGKILL");
44
21
  const cleanupFailure = await releaseProcessResources({
@@ -61,6 +38,11 @@ const createTerminalRenderer = (scenario, temporaryRoot, terminalPid, recordEven
61
38
  normalize: (value) => normalizeProcessText(value, scenario, temporaryRoot, terminalPid()),
62
39
  recordEvent,
63
40
  });
41
+ const reactiveCapture = (options, protocolEvents) => ({
42
+ ...options,
43
+ processSamples: options.samples,
44
+ protocolEvents,
45
+ });
64
46
  const startCaptureRuntime = async (options) => {
65
47
  const { scenario } = options;
66
48
  let replay;
@@ -68,6 +50,7 @@ const startCaptureRuntime = async (options) => {
68
50
  let checkpoints;
69
51
  let shimReplay;
70
52
  let terminal;
53
+ let reactive;
71
54
  try {
72
55
  const { spawn } = await import("@lydell/node-pty");
73
56
  replay = await startLoopbackReplay(scenario, options.recordEvent);
@@ -82,7 +65,7 @@ const startCaptureRuntime = async (options) => {
82
65
  shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started, options.recordEvent);
83
66
  terminal = spawn(scenario.executable, [...scenario.arguments], {
84
67
  cwd: scenario.working_directory,
85
- env: makeEnvironment({ ...options, replay, shimReplay }),
68
+ env: makeProcessCaptureEnvironment({ ...options, replay, shimReplay }),
86
69
  cols: scenario.terminal.columns,
87
70
  rows: scenario.terminal.rows,
88
71
  name: "xterm-256color",
@@ -95,6 +78,15 @@ const startCaptureRuntime = async (options) => {
95
78
  renderer,
96
79
  checkpoints,
97
80
  });
81
+ reactive = startProcessReactiveHarness({
82
+ capture: reactiveCapture(options, () => replay?.events ?? []),
83
+ scenario,
84
+ terminal: () => terminal,
85
+ renderer,
86
+ checkpoints,
87
+ shimReplay,
88
+ started,
89
+ });
98
90
  scheduleScenarioInteractions({
99
91
  ...options,
100
92
  getTerminal: () => terminal,
@@ -120,9 +112,12 @@ const startCaptureRuntime = async (options) => {
120
112
  lastOutput: () => lastOutput,
121
113
  framesTruncated,
122
114
  stopSampler,
115
+ reactive,
123
116
  };
124
117
  }
125
118
  catch (cause) {
119
+ reactive?.unsubscribe();
120
+ await reactive?.coordinator.close();
126
121
  return cleanupFailedStartup({
127
122
  cause,
128
123
  timers: options.timers,
@@ -136,12 +131,9 @@ const startCaptureRuntime = async (options) => {
136
131
  });
137
132
  }
138
133
  };
139
- const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => ({
140
- ...event,
141
- arguments: event.arguments.map((argument) => normalizeProcessText(argument, scenario, temporaryRoot, runtime.terminal.pid)),
142
- working_directory: normalizeProcessText(event.working_directory, scenario, temporaryRoot, runtime.terminal.pid),
143
- }));
134
+ const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => normalizeProcessShimEvent(event, scenario, temporaryRoot, runtime.terminal.pid));
144
135
  const finishProcessRun = async (options) => {
136
+ options.runtime?.reactive?.unsubscribe();
145
137
  await options.stopSampler();
146
138
  const cleanupFailure = await releaseProcessResources({
147
139
  timers: options.timers,
@@ -156,10 +148,16 @@ const finishProcessRun = async (options) => {
156
148
  ? []
157
149
  : selectCapturedProcessGroupIds(options.runtime.terminal.pid, options.samples),
158
150
  });
151
+ if (cleanupFailure !== undefined)
152
+ await options.runtime?.reactive?.coordinator.submit({
153
+ kind: "cleanup_failed",
154
+ });
155
+ await options.runtime?.reactive?.coordinator.close();
159
156
  let { capture, executionFailure } = options;
160
157
  if (capture !== undefined) {
161
158
  capture = {
162
159
  ...capture,
160
+ reactive_run: projectProcessReactiveRun(options.runtime?.reactive),
163
161
  settlement: {
164
162
  ...capture.settlement,
165
163
  cleanup_outcome: capture.settlement.state === "quiesced"
@@ -179,8 +177,17 @@ const completeCapture = async (options) => {
179
177
  const { runtime, scenario } = options;
180
178
  runtime.checkpoints.trigger("root_exit");
181
179
  const { reason } = options.exit;
182
- if (reason === "cancelled")
180
+ if (reason === "cancelled") {
181
+ if (runtime.reactive !== undefined) {
182
+ await runtime.reactive.coordinator.submit({ kind: "cancelled" });
183
+ runtime.reactive.unsubscribe();
184
+ }
183
185
  throw processCaptureCancelled();
186
+ }
187
+ if (runtime.reactive !== undefined) {
188
+ await runtime.reactive.coordinator.submit({ kind: "target_lost" });
189
+ runtime.reactive.unsubscribe();
190
+ }
184
191
  const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms, options.recordEvent);
185
192
  runtime.checkpoints.trigger("settled");
186
193
  const samplingPartial = (await runtime.stopSampler()).partial;
@@ -203,6 +210,7 @@ const completeCapture = async (options) => {
203
210
  exit: { ...options.exit, reason },
204
211
  samples: options.samples,
205
212
  replay: runtime.replay,
213
+ reactiveRun: projectProcessReactiveRun(runtime.reactive),
206
214
  before: options.before,
207
215
  after,
208
216
  truncated,
@@ -220,10 +228,12 @@ const completeCapture = async (options) => {
220
228
  };
221
229
  /** Execute one authorized scenario and return bounded observations. */
222
230
  const runProcessScenario = async (scenario, policy, signal) => {
231
+ assertSupportedReactiveScenario(scenario);
223
232
  const { temporaryRoot, runId, home, before } = await prepareProcessCapture(scenario, policy, signal);
224
233
  const frames = [];
225
234
  const samples = [];
226
- const { entries: eventJournal, recordEvent } = createProcessCaptureJournal();
235
+ const journal = createProcessCaptureJournal();
236
+ const { entries: eventJournal, recordEvent } = journal;
227
237
  let runtime;
228
238
  const timers = new Set();
229
239
  let capture;
@@ -244,6 +254,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
244
254
  timers,
245
255
  dispatchedEventIndexes,
246
256
  recordEvent,
257
+ journal,
247
258
  ...(signal === undefined ? {} : { signal }),
248
259
  });
249
260
  stopSampler = runtime.stopSampler;
@@ -54,6 +54,12 @@ export const normalizeProcessSamples = (samples, scenario, rootPid) => {
54
54
  command: normalizeCommand(sample.command),
55
55
  }));
56
56
  };
57
+ /** Normalize one shim observation identically for live matching and capture. */
58
+ export const normalizeProcessShimEvent = (event, scenario, temporaryRoot, rootPid) => ({
59
+ ...event,
60
+ arguments: event.arguments.map((argument) => normalizeProcessText(argument, scenario, temporaryRoot, rootPid)),
61
+ working_directory: normalizeProcessText(event.working_directory, scenario, temporaryRoot, rootPid),
62
+ });
57
63
  const normalizePidTokens = (value, identifiers) => {
58
64
  const tokens = [...new Set(identifiers)]
59
65
  .filter((identifier) => Number.isSafeInteger(identifier) && identifier > 0)
@@ -153,7 +153,7 @@ export class ProcessReactiveCoordinator {
153
153
  decision = commitProcessReactiveProposal(this.#scenario, decision, effectResults);
154
154
  }
155
155
  this.#snapshot = decision.snapshot;
156
- this.#onDecision?.(decision);
156
+ this.#onDecision?.(decision, input);
157
157
  if (this.#snapshot.status === "finished")
158
158
  this.#clearTimers();
159
159
  else if (this.#accepting &&
@@ -162,16 +162,23 @@ export class ProcessReactiveCoordinator {
162
162
  this.#snapshot.state_entry_capture_order))
163
163
  this.#scheduleStateDeadline();
164
164
  for (const result of effectResults)
165
- if (result.status === "succeeded")
165
+ if (result.status === "succeeded" &&
166
+ !this.#hasObservation(result.observation.event_id))
166
167
  this.#enqueueOwned({
167
168
  kind: "observation",
168
169
  observation: result.observation,
169
170
  });
170
171
  }
172
+ #hasObservation(eventId) {
173
+ return (this.#snapshot.observations.some((observation) => observation.event_id === eventId) ||
174
+ this.#pendingInputs.some((input) => input.kind === "observation" &&
175
+ input.observation.event_id === eventId));
176
+ }
171
177
  #interruptsEffects(input) {
172
178
  if (input.kind === "cancelled" ||
173
179
  input.kind === "cleanup_failed" ||
174
- input.kind === "scenario_deadline")
180
+ input.kind === "scenario_deadline" ||
181
+ input.kind === "target_lost")
175
182
  return true;
176
183
  return (input.kind === "state_deadline" &&
177
184
  input.state_id === this.#snapshot.active_state &&
@@ -109,7 +109,15 @@ const triggerSources = (trigger) => {
109
109
  if (trigger.kind === "terminal_text")
110
110
  return [];
111
111
  if (trigger.kind === "event")
112
- return ["terminal_raw", "interaction", "filesystem"].includes(trigger.source)
112
+ return [
113
+ "terminal_raw",
114
+ "interaction",
115
+ "process",
116
+ "filesystem",
117
+ "http",
118
+ "websocket",
119
+ "shim",
120
+ ].includes(trigger.source)
113
121
  ? []
114
122
  : [trigger.source];
115
123
  return trigger.kind === "repeat"
@@ -0,0 +1,85 @@
1
+ import { ProcessCaptureError } from "./ProcessCaptureError.js";
2
+ import { ProcessReactiveCoordinator } from "./ProcessReactiveCoordinator.js";
3
+ import { executeProcessReactiveEffects, unsupportedProcessReactiveFeatures, } from "./ProcessReactiveEffects.js";
4
+ import { subscribeProcessReactiveObservations } from "./ProcessReactiveObservations.js";
5
+ import { normalizeProcessSamples, normalizeProcessShimEvent, redactProtocolEvents, } from "./ProcessNormalization.js";
6
+ const controlKind = (input) => input.kind === "observation" ? undefined : input.kind;
7
+ /** Reject reactive declarations the current process adapter cannot execute. */
8
+ export const assertSupportedReactiveScenario = (scenario) => {
9
+ if (scenario.reactive === null)
10
+ return;
11
+ const unsupported = unsupportedProcessReactiveFeatures(scenario.reactive);
12
+ if (unsupported.length === 0)
13
+ return;
14
+ throw new ProcessCaptureError(`reactive scenario requests unsupported features: ${unsupported.join(", ")}`);
15
+ };
16
+ /**
17
+ * Attach the serialized reactive runtime once the target PID is available.
18
+ *
19
+ * Subscription replays journal entries admitted during launcher startup before
20
+ * observing new records, so PID-dependent normalization cannot drop them.
21
+ */
22
+ export const startProcessReactiveHarness = (options) => {
23
+ if (options.scenario.reactive === null)
24
+ return undefined;
25
+ const controls = [];
26
+ const coordinator = new ProcessReactiveCoordinator({
27
+ scenario: options.scenario.reactive,
28
+ executor: {
29
+ execute: (actions, signal) => executeProcessReactiveEffects({
30
+ terminal: options.terminal,
31
+ renderer: options.renderer,
32
+ checkpoints: options.checkpoints,
33
+ interactions: options.capture.interactions,
34
+ journal: options.capture.journal,
35
+ startedAtMs: options.started,
36
+ now: Date.now,
37
+ }, actions, signal),
38
+ },
39
+ onDecision: (decision, input) => {
40
+ const kind = controlKind(input);
41
+ if (kind === undefined || decision.kind !== "finished")
42
+ return;
43
+ controls.push({
44
+ sequence: controls.length,
45
+ kind,
46
+ after_capture_order: decision.snapshot.observations.at(-1)?.capture_order ?? -1,
47
+ });
48
+ },
49
+ });
50
+ return {
51
+ coordinator,
52
+ controls,
53
+ unsubscribe: subscribeProcessReactiveObservations({
54
+ journal: options.capture.journal,
55
+ frames: options.capture.frames,
56
+ interactions: options.capture.interactions,
57
+ checkpointAt: (index) => options.checkpoints.at(index),
58
+ processSampleAt: (index) => {
59
+ const rootPid = options.terminal()?.pid;
60
+ return rootPid === undefined
61
+ ? undefined
62
+ : normalizeProcessSamples(options.capture.processSamples, options.scenario, rootPid)[index];
63
+ },
64
+ protocolEventAt: (index) => redactProtocolEvents(options.capture.protocolEvents().slice(index, index + 1), options.scenario)[0],
65
+ shimEventAt: (index) => {
66
+ const event = options.shimReplay.events[index];
67
+ const rootPid = options.terminal()?.pid;
68
+ return event === undefined || rootPid === undefined
69
+ ? undefined
70
+ : normalizeProcessShimEvent(event, options.scenario, options.capture.temporaryRoot, rootPid);
71
+ },
72
+ sink: coordinator,
73
+ }),
74
+ };
75
+ };
76
+ /** Project internal reducer state into the stable Process Capture shape. */
77
+ export const projectProcessReactiveRun = (harness) => harness === undefined
78
+ ? null
79
+ : {
80
+ status: harness.coordinator.snapshot.status,
81
+ outcome: harness.coordinator.snapshot.outcome,
82
+ active_state: harness.coordinator.snapshot.active_state,
83
+ transitions: harness.coordinator.snapshot.transitions,
84
+ controls: harness.controls,
85
+ };
@@ -1,20 +1,105 @@
1
- import { createProcessObservation } from "../domain/processObservation.js";
2
- /** Subscribe a reactive coordinator to normalized raw PTY journal records. */
3
- export const subscribeProcessReactiveTerminalObservations = (options) => options.journal.subscribe((entry) => {
4
- if (entry.collection !== "frames")
5
- return;
6
- const frame = options.frames[entry.index];
7
- if (frame === undefined)
8
- return;
9
- options.sink.enqueue({
10
- kind: "observation",
11
- observation: createProcessObservation({
1
+ import { createProcessObservation, processObservationSubject, } from "../domain/processObservation.js";
2
+ const terminalObservation = (options, entry) => {
3
+ const value = options.frames[entry.index];
4
+ return value === undefined
5
+ ? undefined
6
+ : createProcessObservation({
12
7
  source: "terminal_raw",
13
- source_sequence: frame.sequence,
14
- captured_at_ms: frame.at_ms,
8
+ source_sequence: value.sequence,
9
+ captured_at_ms: value.at_ms,
15
10
  subject_id: null,
16
11
  location: entry,
17
- payload: frame,
18
- }),
12
+ payload: value,
13
+ });
14
+ };
15
+ const interactionObservation = (options, entry) => {
16
+ const value = options.interactions[entry.index];
17
+ return value === undefined
18
+ ? undefined
19
+ : createProcessObservation({
20
+ source: "interaction",
21
+ source_sequence: value.sequence,
22
+ captured_at_ms: value.dispatched_at_ms,
23
+ subject_id: null,
24
+ location: entry,
25
+ payload: value,
26
+ });
27
+ };
28
+ const filesystemObservation = (options, entry) => {
29
+ const value = options.checkpointAt(entry.index);
30
+ return value === undefined
31
+ ? undefined
32
+ : createProcessObservation({
33
+ source: "filesystem",
34
+ source_sequence: entry.index,
35
+ captured_at_ms: value.at_ms,
36
+ subject_id: processObservationSubject("filesystem", value),
37
+ location: entry,
38
+ payload: value,
39
+ });
40
+ };
41
+ const processObservation = (options, entry) => {
42
+ const value = options.processSampleAt(entry.index);
43
+ return value === undefined
44
+ ? undefined
45
+ : createProcessObservation({
46
+ source: "process",
47
+ source_sequence: entry.index,
48
+ captured_at_ms: value.at_ms,
49
+ subject_id: processObservationSubject("process", value),
50
+ location: entry,
51
+ payload: value,
52
+ });
53
+ };
54
+ const protocolObservation = (options, entry) => {
55
+ const value = options.protocolEventAt(entry.index);
56
+ return value === undefined
57
+ ? undefined
58
+ : createProcessObservation({
59
+ source: value.protocol,
60
+ source_sequence: value.sequence,
61
+ captured_at_ms: value.at_ms,
62
+ subject_id: processObservationSubject(value.protocol, value),
63
+ location: entry,
64
+ payload: value,
65
+ });
66
+ };
67
+ const shimObservation = (options, entry) => {
68
+ const normalized = options.shimEventAt(entry.index);
69
+ if (normalized === undefined)
70
+ return undefined;
71
+ return createProcessObservation({
72
+ source: "shim",
73
+ source_sequence: normalized.sequence,
74
+ captured_at_ms: normalized.at_ms,
75
+ subject_id: processObservationSubject("shim", normalized),
76
+ location: entry,
77
+ payload: normalized,
19
78
  });
20
- });
79
+ };
80
+ const observationFor = (options, entry) => {
81
+ if (entry.collection === "frames")
82
+ return terminalObservation(options, entry);
83
+ if (entry.collection === "interaction_events")
84
+ return interactionObservation(options, entry);
85
+ if (entry.collection === "filesystem_checkpoints")
86
+ return filesystemObservation(options, entry);
87
+ if (entry.collection === "process_samples")
88
+ return processObservation(options, entry);
89
+ if (entry.collection === "protocol_events")
90
+ return protocolObservation(options, entry);
91
+ if (entry.collection === "shim_events")
92
+ return shimObservation(options, entry);
93
+ return undefined;
94
+ };
95
+ /** Subscribe the reactive coordinator to admitted normalized journal records. */
96
+ export const subscribeProcessReactiveObservations = (options) => {
97
+ const enqueue = (entry) => {
98
+ const observation = observationFor(options, entry);
99
+ if (observation !== undefined)
100
+ options.sink.enqueue({ kind: "observation", observation });
101
+ };
102
+ for (const entry of options.journal.entries)
103
+ enqueue(entry);
104
+ return options.journal.subscribe(enqueue);
105
+ };