rea-agents 2.2.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 (179) hide show
  1. package/README.md +57 -10
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/ArtifactExtraction.js +5 -1
  4. package/dist/application/ArtifactGraphConstruction.js +3 -15
  5. package/dist/application/ArtifactInventory/classify.js +5 -1
  6. package/dist/application/ArtifactInventory/reader.js +2 -0
  7. package/dist/application/BinarySession.js +9 -2
  8. package/dist/application/BinarySessionStatus.js +40 -1
  9. package/dist/application/CapabilityInventory.js +26 -3
  10. package/dist/application/CommandShimReplay.js +6 -3
  11. package/dist/application/CompositeProvider.js +5 -2
  12. package/dist/application/DirectAnalysis.js +1 -20
  13. package/dist/application/DirectAnalysisStatus.js +83 -0
  14. package/dist/application/Doctor.js +34 -25
  15. package/dist/application/DoctorDiagnostics.js +1 -4
  16. package/dist/application/DoctorProjection.js +43 -0
  17. package/dist/application/DoctorScope.js +102 -0
  18. package/dist/application/EvidenceReferenceResolver.js +26 -0
  19. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  20. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  21. package/dist/application/JavaScriptApplicationService.js +3 -2
  22. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  23. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  24. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  25. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  26. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  27. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  28. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  29. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  30. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  31. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  32. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  33. package/dist/application/LoopbackReplay.js +277 -133
  34. package/dist/application/LoopbackReplayRecorder.js +101 -0
  35. package/dist/application/PermissionAuthority.js +7 -3
  36. package/dist/application/ProcessCaptureEnvironment.js +26 -0
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +25 -13
  39. package/dist/application/ProcessCheckpoints.js +35 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +64 -83
  42. package/dist/application/ProcessNormalization.js +9 -2
  43. package/dist/application/ProcessReactiveCoordinator.js +225 -0
  44. package/dist/application/ProcessReactiveEffects.js +141 -0
  45. package/dist/application/ProcessReactiveHarness.js +85 -0
  46. package/dist/application/ProcessReactiveObservations.js +105 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +11 -10
  50. package/dist/application/SetupPlan.js +1 -1
  51. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  52. package/dist/application/TerminalRenderer.js +3 -1
  53. package/dist/browser/CdpEndpoint.js +37 -4
  54. package/dist/catalogIdentity.js +16 -7
  55. package/dist/cli/coreAnalysisCommands.js +34 -2
  56. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  57. package/dist/cli/setupCommands.js +81 -32
  58. package/dist/cli/utilityCommands.js +10 -2
  59. package/dist/cliApplicationCommands.js +79 -16
  60. package/dist/cliCommandNames.js +3 -0
  61. package/dist/cliElectronCommands.js +23 -33
  62. package/dist/cliObservationOptions.js +1 -1
  63. package/dist/cliOutput.js +56 -0
  64. package/dist/cliProcessCommands.js +32 -1
  65. package/dist/cliSetup.js +30 -21
  66. package/dist/contracts/applicationToolContracts.js +54 -7
  67. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  68. package/dist/contracts/electronToolContracts.js +53 -32
  69. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  70. package/dist/contracts/processCaptureExample.js +1 -0
  71. package/dist/contracts/replayMachineExample.js +37 -0
  72. package/dist/contracts/toolContractExamples.js +2 -0
  73. package/dist/contracts/toolContracts.js +5 -1
  74. package/dist/contracts/toolEffects.js +3 -0
  75. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  76. package/dist/contracts/toolOutputSchemaPrimitives.js +43 -0
  77. package/dist/doctorRuntime.js +41 -0
  78. package/dist/domain/analysisErrorPresentation.js +5 -3
  79. package/dist/domain/analysisErrorProjection.js +7 -1
  80. package/dist/domain/analysisSnapshot.js +2 -0
  81. package/dist/domain/artifactGraph.js +2 -0
  82. package/dist/domain/binaryTarget.js +3 -6
  83. package/dist/domain/changedBehavior.js +5 -4
  84. package/dist/domain/completionLedgerGeneration.js +190 -0
  85. package/dist/domain/errors.js +4 -2
  86. package/dist/domain/evidence.js +2 -0
  87. package/dist/domain/evidenceCompletionLedger.js +113 -0
  88. package/dist/domain/hopperStartupFailure.js +9 -0
  89. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  90. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  91. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  92. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  93. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  94. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  95. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  96. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  97. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  98. package/dist/domain/javascriptSemanticCalls.js +269 -0
  99. package/dist/domain/javascriptSemanticGraph.js +356 -0
  100. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  101. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  102. package/dist/domain/javascriptSemanticIr.js +14 -1
  103. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  104. package/dist/domain/javascriptSemanticProjection.js +36 -4
  105. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  106. package/dist/domain/javascriptSemanticQuery.js +294 -0
  107. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  108. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  109. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  110. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  111. package/dist/domain/javascriptSemanticReturns.js +187 -0
  112. package/dist/domain/javascriptSemanticState.js +32 -0
  113. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  114. package/dist/domain/javascriptSemanticValues.js +54 -70
  115. package/dist/domain/processCapture.js +100 -0
  116. package/dist/domain/processCaptureReactiveValidation.js +175 -0
  117. package/dist/domain/processCaptureValidation.js +103 -12
  118. package/dist/domain/processComparison.js +78 -25
  119. package/dist/domain/processObservation.js +162 -0
  120. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  121. package/dist/domain/processReactiveMatching.js +210 -0
  122. package/dist/domain/processReactiveRuntime.js +120 -0
  123. package/dist/domain/processReactiveScenario.js +361 -0
  124. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  125. package/dist/domain/processReactiveTransition.js +161 -0
  126. package/dist/domain/processScenario.js +33 -0
  127. package/dist/domain/processTraceComparison.js +43 -0
  128. package/dist/domain/processTraceDimensionProjection.js +50 -0
  129. package/dist/domain/processTraceEvaluation.js +312 -0
  130. package/dist/domain/processTraceSpecification.js +373 -0
  131. package/dist/domain/reconstructionVerification.js +5 -4
  132. package/dist/domain/replayMachine.js +18 -2
  133. package/dist/domain/replayMachineRun.js +216 -0
  134. package/dist/domain/replayMachineRuntime.js +88 -9
  135. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  136. package/dist/domain/zipPackageFormat.js +20 -0
  137. package/dist/evaluation/CodexAgentEval.js +24 -0
  138. package/dist/generatedPackageMetadata.js +1 -1
  139. package/dist/ghidra/GhidraClient.js +13 -2
  140. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  141. package/dist/ghidra/GhidraLauncher.js +7 -3
  142. package/dist/ghidra/GhidraProvider.js +8 -1
  143. package/dist/hopper/BridgeLauncher.js +56 -14
  144. package/dist/hopper/HopperClient.js +23 -10
  145. package/dist/hopper/HopperDiagnostics.js +14 -0
  146. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  147. package/dist/hopper/HopperProvider.js +8 -1
  148. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  149. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  150. package/dist/main/transport.js +2 -0
  151. package/dist/mcpDoctor.js +310 -0
  152. package/dist/native/NativeMacOSProvider.js +1 -0
  153. package/dist/process/ProcessOwnership.js +200 -53
  154. package/dist/process/ProviderRunLineage.js +21 -0
  155. package/dist/server/createServer.js +4 -1
  156. package/dist/server/javascriptApplicationResult.js +22 -1
  157. package/dist/server/registerApplicationTools/characterization.js +3 -3
  158. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  159. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  160. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  161. package/dist/server/registerApplicationTools/coverage.js +3 -3
  162. package/dist/server/registerApplicationTools/helpers.js +37 -25
  163. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  164. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  165. package/dist/server/registerApplicationTools.js +4 -0
  166. package/dist/server/registerEvidenceResources.js +5 -1
  167. package/dist/server/registerProcessComparisonTool.js +17 -7
  168. package/dist/server/registerReplayMachineTool.js +20 -0
  169. package/dist/server/registerSessionTools.js +12 -7
  170. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  171. package/dist/serverIdentity.js +19 -0
  172. package/package.json +62 -30
  173. package/scripts/hopper-demo-x11.py +459 -61
  174. package/scripts/prepack.mjs +55 -0
  175. package/scripts/prepare.mjs +28 -0
  176. package/scripts/rea.mjs +26 -5
  177. package/skills/reverse-engineer-anything/SKILL.md +4 -3
  178. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
  179. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
@@ -8,79 +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
+ import { createProcessCaptureJournal, scheduleScenarioInteractions, } from "./ProcessCaptureJournal.js";
16
+ import { assertSupportedReactiveScenario, projectProcessReactiveRun, startProcessReactiveHarness, } from "./ProcessReactiveHarness.js";
17
+ import { makeProcessCaptureEnvironment } from "./ProcessCaptureEnvironment.js";
15
18
  export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js";
16
- const makeEnvironment = (options) => {
17
- const { scenario, home, replay, shimReplay, runId } = options;
18
- const environment = {
19
- ...scenario.environment,
20
- HOME: home,
21
- TERM: "xterm-256color",
22
- REA_PROCESS_RUN_ID: runId,
23
- };
24
- for (const name of scenario.inherit_environment) {
25
- const value = process.env[name];
26
- if (value !== undefined)
27
- environment[name] = value;
28
- }
29
- environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
30
- environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
31
- environment.REA_SHIM_LEDGER_URL = shimReplay.url;
32
- // PATH is evidence-producing input. Inheriting the host PATH here would let
33
- // the authority and reconstruction probe different tools without recording
34
- // that difference. Scenarios must opt in through environment or
35
- // inherit_environment; deterministic shims always take precedence.
36
- environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
37
- .filter((part) => part.length > 0)
38
- .join(":");
39
- return environment;
40
- };
41
- const scheduleScenarioEvents = (options) => {
42
- const { scenario, getTerminal, timers, interactions, renderer, started } = options;
43
- for (const [eventIndex, event] of scenario.events.entries()) {
44
- const timer = setTimeout(() => {
45
- options.dispatchedEventIndexes.add(eventIndex);
46
- const terminal = getTerminal();
47
- const dispatchedAt = Math.max(0, Date.now() - started);
48
- let outcome = "dispatched";
49
- if (terminal === undefined)
50
- outcome = "target_exited";
51
- else {
52
- try {
53
- if (event.type === "input")
54
- terminal.write(event.data);
55
- else if (event.type === "resize") {
56
- terminal.resize(event.columns, event.rows);
57
- renderer.resize(event.columns, event.rows, dispatchedAt);
58
- }
59
- else
60
- terminal.kill(event.signal);
61
- }
62
- catch {
63
- outcome = "failed";
64
- }
65
- }
66
- interactions.push({
67
- sequence: interactions.length,
68
- scheduled_at_ms: event.at_ms,
69
- dispatched_at_ms: dispatchedAt,
70
- type: event.type,
71
- data: event.type === "input"
72
- ? event.sensitive
73
- ? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
74
- : normalizeProcessText(event.data, scenario, "<no-temporary-root>", -1)
75
- : event.type === "resize"
76
- ? `${String(event.columns)}x${String(event.rows)}`
77
- : event.signal,
78
- outcome,
79
- });
80
- }, event.at_ms);
81
- timers.add(timer);
82
- }
83
- };
84
19
  const cleanupFailedStartup = async (options) => {
85
20
  options.terminal?.kill("SIGKILL");
86
21
  const cleanupFailure = await releaseProcessResources({
@@ -94,13 +29,19 @@ const cleanupFailedStartup = async (options) => {
94
29
  });
95
30
  throw options.cause;
96
31
  };
97
- const createTerminalRenderer = (scenario, temporaryRoot, terminalPid) => new TerminalRenderer({
32
+ const createTerminalRenderer = (scenario, temporaryRoot, terminalPid, recordEvent) => new TerminalRenderer({
98
33
  columns: scenario.terminal.columns,
99
34
  rows: scenario.terminal.rows,
100
35
  scrollback: scenario.terminal.scrollback,
101
36
  maxFrames: scenario.limits.frames,
102
37
  maxBytes: scenario.limits.output_bytes,
103
38
  normalize: (value) => normalizeProcessText(value, scenario, temporaryRoot, terminalPid()),
39
+ recordEvent,
40
+ });
41
+ const reactiveCapture = (options, protocolEvents) => ({
42
+ ...options,
43
+ processSamples: options.samples,
44
+ protocolEvents,
104
45
  });
105
46
  const startCaptureRuntime = async (options) => {
106
47
  const { scenario } = options;
@@ -109,18 +50,22 @@ const startCaptureRuntime = async (options) => {
109
50
  let checkpoints;
110
51
  let shimReplay;
111
52
  let terminal;
53
+ let reactive;
112
54
  try {
113
55
  const { spawn } = await import("@lydell/node-pty");
114
- replay = await startLoopbackReplay(scenario);
56
+ replay = await startLoopbackReplay(scenario, options.recordEvent);
115
57
  const startedAt = new Date();
116
58
  const started = Date.now();
117
59
  let lastOutput = started;
118
- renderer = createTerminalRenderer(scenario, options.temporaryRoot, () => terminal?.pid ?? -1);
119
- checkpoints = new ProcessCheckpoints(scenario, started, options.before, options.signal);
120
- shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started);
60
+ renderer = createTerminalRenderer(scenario, options.temporaryRoot, () => terminal?.pid ?? -1, options.recordEvent);
61
+ checkpoints = new ProcessCheckpoints(scenario, started, options.before, {
62
+ signal: options.signal,
63
+ recordEvent: options.recordEvent,
64
+ });
65
+ shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started, options.recordEvent);
121
66
  terminal = spawn(scenario.executable, [...scenario.arguments], {
122
67
  cwd: scenario.working_directory,
123
- env: makeEnvironment({ ...options, replay, shimReplay }),
68
+ env: makeProcessCaptureEnvironment({ ...options, replay, shimReplay }),
124
69
  cols: scenario.terminal.columns,
125
70
  rows: scenario.terminal.rows,
126
71
  name: "xterm-256color",
@@ -133,7 +78,16 @@ const startCaptureRuntime = async (options) => {
133
78
  renderer,
134
79
  checkpoints,
135
80
  });
136
- scheduleScenarioEvents({
81
+ reactive = startProcessReactiveHarness({
82
+ capture: reactiveCapture(options, () => replay?.events ?? []),
83
+ scenario,
84
+ terminal: () => terminal,
85
+ renderer,
86
+ checkpoints,
87
+ shimReplay,
88
+ started,
89
+ });
90
+ scheduleScenarioInteractions({
137
91
  ...options,
138
92
  getTerminal: () => terminal,
139
93
  renderer,
@@ -145,6 +99,7 @@ const startCaptureRuntime = async (options) => {
145
99
  started,
146
100
  limit: scenario.limits.processes,
147
101
  samples: options.samples,
102
+ recordEvent: options.recordEvent,
148
103
  });
149
104
  return {
150
105
  replay,
@@ -157,9 +112,12 @@ const startCaptureRuntime = async (options) => {
157
112
  lastOutput: () => lastOutput,
158
113
  framesTruncated,
159
114
  stopSampler,
115
+ reactive,
160
116
  };
161
117
  }
162
118
  catch (cause) {
119
+ reactive?.unsubscribe();
120
+ await reactive?.coordinator.close();
163
121
  return cleanupFailedStartup({
164
122
  cause,
165
123
  timers: options.timers,
@@ -173,12 +131,9 @@ const startCaptureRuntime = async (options) => {
173
131
  });
174
132
  }
175
133
  };
176
- const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => ({
177
- ...event,
178
- arguments: event.arguments.map((argument) => normalizeProcessText(argument, scenario, temporaryRoot, runtime.terminal.pid)),
179
- working_directory: normalizeProcessText(event.working_directory, scenario, temporaryRoot, runtime.terminal.pid),
180
- }));
134
+ const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => normalizeProcessShimEvent(event, scenario, temporaryRoot, runtime.terminal.pid));
181
135
  const finishProcessRun = async (options) => {
136
+ options.runtime?.reactive?.unsubscribe();
182
137
  await options.stopSampler();
183
138
  const cleanupFailure = await releaseProcessResources({
184
139
  timers: options.timers,
@@ -193,10 +148,16 @@ const finishProcessRun = async (options) => {
193
148
  ? []
194
149
  : selectCapturedProcessGroupIds(options.runtime.terminal.pid, options.samples),
195
150
  });
151
+ if (cleanupFailure !== undefined)
152
+ await options.runtime?.reactive?.coordinator.submit({
153
+ kind: "cleanup_failed",
154
+ });
155
+ await options.runtime?.reactive?.coordinator.close();
196
156
  let { capture, executionFailure } = options;
197
157
  if (capture !== undefined) {
198
158
  capture = {
199
159
  ...capture,
160
+ reactive_run: projectProcessReactiveRun(options.runtime?.reactive),
200
161
  settlement: {
201
162
  ...capture.settlement,
202
163
  cleanup_outcome: capture.settlement.state === "quiesced"
@@ -216,15 +177,25 @@ const completeCapture = async (options) => {
216
177
  const { runtime, scenario } = options;
217
178
  runtime.checkpoints.trigger("root_exit");
218
179
  const { reason } = options.exit;
219
- 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
+ }
220
185
  throw processCaptureCancelled();
221
- const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms);
186
+ }
187
+ if (runtime.reactive !== undefined) {
188
+ await runtime.reactive.coordinator.submit({ kind: "target_lost" });
189
+ runtime.reactive.unsubscribe();
190
+ }
191
+ const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms, options.recordEvent);
222
192
  runtime.checkpoints.trigger("settled");
223
193
  const samplingPartial = (await runtime.stopSampler()).partial;
224
194
  assertNotCancelled(options.signal);
225
195
  const after = await snapshotRoots(scenario, options.signal);
226
196
  const renderedFrames = await runtime.renderer.frames();
227
197
  const checkpoints = await runtime.checkpoints.finish(after);
198
+ await runtime.replay.close();
228
199
  const manifest = await createRunManifest(scenario, runtime.startedAt, new Date());
229
200
  const truncated = options.initiallyTruncated ||
230
201
  after.truncated ||
@@ -239,6 +210,7 @@ const completeCapture = async (options) => {
239
210
  exit: { ...options.exit, reason },
240
211
  samples: options.samples,
241
212
  replay: runtime.replay,
213
+ reactiveRun: projectProcessReactiveRun(runtime.reactive),
242
214
  before: options.before,
243
215
  after,
244
216
  truncated,
@@ -251,13 +223,17 @@ const completeCapture = async (options) => {
251
223
  shimEvents: normalizedShimEvents(runtime, scenario, options.temporaryRoot),
252
224
  settlement: { ...settlement, cleanup_outcome: "not_required" },
253
225
  manifest,
226
+ eventJournal: options.eventJournal,
254
227
  });
255
228
  };
256
229
  /** Execute one authorized scenario and return bounded observations. */
257
230
  const runProcessScenario = async (scenario, policy, signal) => {
231
+ assertSupportedReactiveScenario(scenario);
258
232
  const { temporaryRoot, runId, home, before } = await prepareProcessCapture(scenario, policy, signal);
259
233
  const frames = [];
260
234
  const samples = [];
235
+ const journal = createProcessCaptureJournal();
236
+ const { entries: eventJournal, recordEvent } = journal;
261
237
  let runtime;
262
238
  const timers = new Set();
263
239
  let capture;
@@ -277,6 +253,8 @@ const runProcessScenario = async (scenario, policy, signal) => {
277
253
  interactions,
278
254
  timers,
279
255
  dispatchedEventIndexes,
256
+ recordEvent,
257
+ journal,
280
258
  ...(signal === undefined ? {} : { signal }),
281
259
  });
282
260
  stopSampler = runtime.stopSampler;
@@ -289,6 +267,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
289
267
  timers,
290
268
  interactions,
291
269
  dispatchedEventIndexes,
270
+ recordEvent,
292
271
  });
293
272
  capture = await completeCapture({
294
273
  scenario,
@@ -301,6 +280,8 @@ const runProcessScenario = async (scenario, policy, signal) => {
301
280
  interactions,
302
281
  exit,
303
282
  initiallyTruncated: before.truncated,
283
+ eventJournal,
284
+ recordEvent,
304
285
  ...(signal === undefined ? {} : { signal }),
305
286
  });
306
287
  }
@@ -1,3 +1,5 @@
1
+ /** Bucket one elapsed process-capture timestamp under scenario normalization. */
2
+ export const normalizeProcessElapsedTime = (elapsedMs, timeBucketMs) => Math.floor(elapsedMs / timeBucketMs) * timeBucketMs;
1
3
  /** Normalize and redact one terminal/protocol payload under scenario rules. */
2
4
  export const normalizeProcessText = (value, scenario, temporaryRoot, pid) => {
3
5
  let normalized = value;
@@ -42,8 +44,7 @@ export const normalizeProcessSamples = (samples, scenario, rootPid) => {
42
44
  : normalized;
43
45
  };
44
46
  return samples.map((sample) => ({
45
- at_ms: Math.floor(sample.at_ms / scenario.normalization.time_bucket_ms) *
46
- scenario.normalization.time_bucket_ms,
47
+ at_ms: normalizeProcessElapsedTime(sample.at_ms, scenario.normalization.time_bucket_ms),
47
48
  pid: mapping.get(sample.pid) ?? 1,
48
49
  parent_pid: mapping.get(sample.parent_pid) ?? 0,
49
50
  process_group_id: sample.process_group_id === null
@@ -53,6 +54,12 @@ export const normalizeProcessSamples = (samples, scenario, rootPid) => {
53
54
  command: normalizeCommand(sample.command),
54
55
  }));
55
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
+ });
56
63
  const normalizePidTokens = (value, identifiers) => {
57
64
  const tokens = [...new Set(identifiers)]
58
65
  .filter((identifier) => Number.isSafeInteger(identifier) && identifier > 0)
@@ -0,0 +1,225 @@
1
+ import { commitProcessReactiveProposal, createProcessReactiveSnapshot, reduceProcessReactiveScenario, } from "../domain/processReactiveRuntime.js";
2
+ const systemTimerHost = {
3
+ schedule: (callback, delayMs) => {
4
+ const timer = setTimeout(callback, delayMs);
5
+ return { cancel: () => clearTimeout(timer) };
6
+ },
7
+ };
8
+ /**
9
+ * Serializes live observations, deadlines, and effect commits for one run.
10
+ * Producer callbacks enqueue work without awaiting it, preventing reentrancy.
11
+ */
12
+ export class ProcessReactiveCoordinator {
13
+ #scenario;
14
+ #executor;
15
+ #timerHost;
16
+ #now;
17
+ #scenarioDeadlineAt;
18
+ #onDecision;
19
+ #snapshot;
20
+ #pendingInputs = [];
21
+ #tail = Promise.resolve();
22
+ #running = false;
23
+ #failure;
24
+ #stateTimer;
25
+ #scenarioTimer;
26
+ #stateDeadlineAt = 0;
27
+ #effectAbort;
28
+ #accepting = true;
29
+ #closed = false;
30
+ constructor(options) {
31
+ this.#scenario = options.scenario;
32
+ this.#executor = options.executor;
33
+ this.#timerHost = options.timerHost ?? systemTimerHost;
34
+ this.#now = options.now ?? Date.now;
35
+ this.#scenarioDeadlineAt = this.#now() + options.scenario.deadline_ms;
36
+ this.#onDecision = options.onDecision;
37
+ this.#snapshot = createProcessReactiveSnapshot(options.scenario);
38
+ this.#scheduleScenarioDeadline();
39
+ this.#scheduleStateDeadline();
40
+ }
41
+ /** Current immutable reducer snapshot after all completed turns. */
42
+ get snapshot() {
43
+ return this.#snapshot;
44
+ }
45
+ /** Enqueue producer work without creating a floating promise. */
46
+ enqueue(input) {
47
+ if (!this.#accepting)
48
+ return;
49
+ if (this.#interruptsEffects(input))
50
+ this.#effectAbort?.abort();
51
+ this.#enqueueOwned(input);
52
+ }
53
+ /** Submit one input and wait until every turn queued before it completes. */
54
+ async submit(input) {
55
+ this.enqueue(input);
56
+ await this.drain();
57
+ return this.#snapshot;
58
+ }
59
+ /** Wait for currently queued reducer/effect work and surface host failures. */
60
+ async drain() {
61
+ for (;;) {
62
+ const observedTail = this.#tail;
63
+ await observedTail;
64
+ if (observedTail === this.#tail)
65
+ break;
66
+ }
67
+ if (this.#failure !== undefined)
68
+ throw this.#failure;
69
+ }
70
+ /** Stop owned deadlines after queued work settles. */
71
+ async close() {
72
+ this.#accepting = false;
73
+ this.#clearTimers();
74
+ await this.drain();
75
+ this.#closed = true;
76
+ }
77
+ #enqueueOwned(input) {
78
+ if (this.#closed || this.#failure !== undefined)
79
+ return;
80
+ this.#pendingInputs.push(input);
81
+ this.#startWorker();
82
+ }
83
+ #startWorker() {
84
+ if (this.#running || this.#pendingInputs.length === 0)
85
+ return;
86
+ this.#running = true;
87
+ this.#tail = this.#runQueue()
88
+ .catch((cause) => this.#fail(cause))
89
+ .finally(() => {
90
+ this.#running = false;
91
+ if (this.#pendingInputs.length > 0)
92
+ this.#startWorker();
93
+ });
94
+ }
95
+ async #runQueue() {
96
+ for (;;) {
97
+ const input = this.#nextInput();
98
+ if (input === undefined)
99
+ return;
100
+ await this.#advance(input);
101
+ }
102
+ }
103
+ #nextInput() {
104
+ const terminalIndex = this.#pendingInputs.findIndex(({ kind }) => kind === "cancelled" || kind === "cleanup_failed");
105
+ if (terminalIndex >= 0)
106
+ return this.#pendingInputs.splice(terminalIndex, 1)[0];
107
+ let observationIndex = -1;
108
+ let captureOrder = Number.POSITIVE_INFINITY;
109
+ for (const [index, input] of this.#pendingInputs.entries())
110
+ if (input.kind === "observation" &&
111
+ input.observation.capture_order < captureOrder) {
112
+ observationIndex = index;
113
+ captureOrder = input.observation.capture_order;
114
+ }
115
+ return observationIndex >= 0
116
+ ? this.#pendingInputs.splice(observationIndex, 1)[0]
117
+ : this.#pendingInputs.shift();
118
+ }
119
+ #fail(cause) {
120
+ this.#failure ??= cause;
121
+ this.#accepting = false;
122
+ this.#closed = true;
123
+ this.#effectAbort?.abort();
124
+ this.#pendingInputs.length = 0;
125
+ this.#clearTimers();
126
+ }
127
+ async #advance(input) {
128
+ const before = this.#snapshot;
129
+ let decision = reduceProcessReactiveScenario(this.#scenario, before, input);
130
+ let effectResults = [];
131
+ if (decision.kind === "proposal") {
132
+ const effectAbort = new AbortController();
133
+ this.#effectAbort = effectAbort;
134
+ try {
135
+ effectResults = await this.#executor.execute(decision.effects, effectAbort.signal);
136
+ }
137
+ catch (cause) {
138
+ if (effectAbort.signal.aborted)
139
+ return;
140
+ throw cause;
141
+ }
142
+ finally {
143
+ if (this.#effectAbort === effectAbort)
144
+ this.#effectAbort = undefined;
145
+ }
146
+ if (effectAbort.signal.aborted)
147
+ return;
148
+ const expired = this.#expiredDeadlineInput();
149
+ if (expired !== undefined) {
150
+ this.#enqueueOwned(expired);
151
+ return;
152
+ }
153
+ decision = commitProcessReactiveProposal(this.#scenario, decision, effectResults);
154
+ }
155
+ this.#snapshot = decision.snapshot;
156
+ this.#onDecision?.(decision, input);
157
+ if (this.#snapshot.status === "finished")
158
+ this.#clearTimers();
159
+ else if (this.#accepting &&
160
+ (before.active_state !== this.#snapshot.active_state ||
161
+ before.state_entry_capture_order !==
162
+ this.#snapshot.state_entry_capture_order))
163
+ this.#scheduleStateDeadline();
164
+ for (const result of effectResults)
165
+ if (result.status === "succeeded" &&
166
+ !this.#hasObservation(result.observation.event_id))
167
+ this.#enqueueOwned({
168
+ kind: "observation",
169
+ observation: result.observation,
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
+ }
177
+ #interruptsEffects(input) {
178
+ if (input.kind === "cancelled" ||
179
+ input.kind === "cleanup_failed" ||
180
+ input.kind === "scenario_deadline" ||
181
+ input.kind === "target_lost")
182
+ return true;
183
+ return (input.kind === "state_deadline" &&
184
+ input.state_id === this.#snapshot.active_state &&
185
+ input.state_entry_capture_order ===
186
+ this.#snapshot.state_entry_capture_order);
187
+ }
188
+ #expiredDeadlineInput() {
189
+ const now = this.#now();
190
+ if (now >= this.#scenarioDeadlineAt)
191
+ return { kind: "scenario_deadline" };
192
+ if (now < this.#stateDeadlineAt)
193
+ return undefined;
194
+ return {
195
+ kind: "state_deadline",
196
+ state_id: this.#snapshot.active_state,
197
+ state_entry_capture_order: this.#snapshot.state_entry_capture_order,
198
+ };
199
+ }
200
+ #scheduleScenarioDeadline() {
201
+ if (!this.#accepting)
202
+ return;
203
+ this.#scenarioTimer = this.#timerHost.schedule(() => this.enqueue({ kind: "scenario_deadline" }), this.#scenario.deadline_ms);
204
+ }
205
+ #scheduleStateDeadline() {
206
+ if (!this.#accepting)
207
+ return;
208
+ this.#stateTimer?.cancel();
209
+ const state = this.#scenario.states.find(({ id }) => id === this.#snapshot.active_state);
210
+ if (state === undefined)
211
+ return;
212
+ this.#stateDeadlineAt = this.#now() + state.deadline_ms;
213
+ const identity = {
214
+ state_id: state.id,
215
+ state_entry_capture_order: this.#snapshot.state_entry_capture_order,
216
+ };
217
+ this.#stateTimer = this.#timerHost.schedule(() => this.enqueue({ kind: "state_deadline", ...identity }), state.deadline_ms);
218
+ }
219
+ #clearTimers() {
220
+ this.#stateTimer?.cancel();
221
+ this.#scenarioTimer?.cancel();
222
+ this.#stateTimer = undefined;
223
+ this.#scenarioTimer = undefined;
224
+ }
225
+ }
@@ -0,0 +1,141 @@
1
+ import { createProcessObservation } from "../domain/processObservation.js";
2
+ const elapsed = (host) => Math.max(0, host.now() - host.startedAtMs);
3
+ const interactionData = (action) => {
4
+ if (action.type === "send_input")
5
+ return action.sensitive
6
+ ? `<redacted-input:${String(Buffer.byteLength(action.data))}-bytes>`
7
+ : action.data;
8
+ if (action.type === "resize")
9
+ return `${String(action.columns)}x${String(action.rows)}`;
10
+ return action.signal;
11
+ };
12
+ const interactionType = (action) => {
13
+ if (action.type === "send_input")
14
+ return "input";
15
+ if (action.type === "resize")
16
+ return "resize";
17
+ return "signal";
18
+ };
19
+ const performTerminalAction = (terminal, renderer, action, atMs) => {
20
+ if (action.type === "send_input")
21
+ terminal.write(action.data);
22
+ else if (action.type === "resize") {
23
+ terminal.resize(action.columns, action.rows);
24
+ renderer.resize(action.columns, action.rows, atMs);
25
+ }
26
+ else
27
+ terminal.kill(action.signal);
28
+ };
29
+ const recordInteraction = (host, action, atMs, outcome) => {
30
+ const index = host.interactions.length;
31
+ const event = {
32
+ sequence: index,
33
+ scheduled_at_ms: atMs,
34
+ dispatched_at_ms: atMs,
35
+ type: interactionType(action),
36
+ data: interactionData(action),
37
+ outcome,
38
+ };
39
+ host.interactions.push(event);
40
+ const location = host.journal.record("interaction_events", index);
41
+ if (outcome !== "dispatched")
42
+ return { status: "rejected" };
43
+ return {
44
+ status: "succeeded",
45
+ observation: createProcessObservation({
46
+ source: "interaction",
47
+ source_sequence: index,
48
+ captured_at_ms: atMs,
49
+ subject_id: null,
50
+ location,
51
+ payload: event,
52
+ }),
53
+ };
54
+ };
55
+ const executeTerminalAction = (host, action) => {
56
+ if (action.type === "send_signal" && action.target.kind !== "root")
57
+ return { status: "rejected" };
58
+ const terminal = host.terminal();
59
+ if (terminal === undefined)
60
+ return { status: "target_lost" };
61
+ const atMs = elapsed(host);
62
+ let outcome = "dispatched";
63
+ try {
64
+ performTerminalAction(terminal, host.renderer, action, atMs);
65
+ }
66
+ catch {
67
+ outcome = "failed";
68
+ }
69
+ return recordInteraction(host, action, atMs, outcome);
70
+ };
71
+ const executeCheckpoint = async (host, action, signal) => {
72
+ try {
73
+ const { index, checkpoint } = await host.checkpoints.captureAndRead(action.name, signal);
74
+ const location = host.journal.entries.findLast((entry) => entry.collection === "filesystem_checkpoints" && entry.index === index);
75
+ if (location === undefined)
76
+ return { status: "rejected" };
77
+ return {
78
+ status: "succeeded",
79
+ observation: createProcessObservation({
80
+ source: "filesystem",
81
+ source_sequence: index,
82
+ captured_at_ms: checkpoint.at_ms,
83
+ subject_id: `checkpoint:${checkpoint.name}`,
84
+ location,
85
+ payload: checkpoint,
86
+ }),
87
+ };
88
+ }
89
+ catch {
90
+ return { status: "rejected" };
91
+ }
92
+ };
93
+ /** Execute proposed effects in order and stop after the first failure. */
94
+ export const executeProcessReactiveEffects = async (host, actions, signal) => {
95
+ const results = [];
96
+ for (const action of actions) {
97
+ if (signal.aborted)
98
+ return results;
99
+ const result = action.type === "checkpoint"
100
+ ? await executeCheckpoint(host, action, signal)
101
+ : executeTerminalAction(host, action);
102
+ results.push(result);
103
+ if (result.status !== "succeeded")
104
+ break;
105
+ }
106
+ return results;
107
+ };
108
+ const triggerSources = (trigger) => {
109
+ if (trigger.kind === "terminal_text")
110
+ return [];
111
+ if (trigger.kind === "event")
112
+ return [
113
+ "terminal_raw",
114
+ "interaction",
115
+ "process",
116
+ "filesystem",
117
+ "http",
118
+ "websocket",
119
+ "shim",
120
+ ].includes(trigger.source)
121
+ ? []
122
+ : [trigger.source];
123
+ return trigger.kind === "repeat"
124
+ ? triggerSources(trigger.trigger)
125
+ : trigger.triggers.flatMap(triggerSources);
126
+ };
127
+ /** Return unsupported sources, selectors, and checkpoint collisions pre-launch. */
128
+ export const unsupportedProcessReactiveFeatures = (scenario, reservedCheckpointNames = new Set([
129
+ "before",
130
+ "after_settlement",
131
+ ])) => [
132
+ ...scenario.states.flatMap((state) => state.on.flatMap((transition) => triggerSources(transition.when).map((source) => `${transition.id}:source:${source}`))),
133
+ ...scenario.states.flatMap((state) => state.on.flatMap((transition) => transition.actions.flatMap((action) => {
134
+ if (action.type === "send_signal" && action.target.kind !== "root")
135
+ return [`${transition.id}:target:${action.target.kind}`];
136
+ if (action.type === "checkpoint" &&
137
+ reservedCheckpointNames.has(action.name))
138
+ return [`${transition.id}:checkpoint:${action.name}`];
139
+ return [];
140
+ }))),
141
+ ];