engineering-behavior-observatory 0.2.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 (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +151 -0
  3. package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
  4. package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
  5. package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
  6. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
  7. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  8. package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
  9. package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
  10. package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
  11. package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
  12. package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
  13. package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
  14. package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
  15. package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
  16. package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
  17. package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
  18. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
  19. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
  20. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  21. package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
  22. package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
  23. package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
  24. package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
  25. package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
  26. package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
  27. package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
  28. package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
  29. package/contracts/openhands-agent-server-v1.44.1.json +65 -0
  30. package/contracts/openhands-agent-server-v1.46.0.json +46 -0
  31. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
  32. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
  33. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
  34. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
  35. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
  36. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
  37. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
  38. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
  39. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
  40. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
  41. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
  42. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
  43. package/dist/src/agent-sdk-normalizer.d.ts +22 -0
  44. package/dist/src/agent-sdk-normalizer.js +995 -0
  45. package/dist/src/agent-sdk-run.d.ts +31 -0
  46. package/dist/src/agent-sdk-run.js +242 -0
  47. package/dist/src/agent-sdk-runner.d.ts +96 -0
  48. package/dist/src/agent-sdk-runner.js +407 -0
  49. package/dist/src/agent-sdk.d.ts +294 -0
  50. package/dist/src/agent-sdk.js +644 -0
  51. package/dist/src/aggregation.d.ts +183 -0
  52. package/dist/src/aggregation.js +513 -0
  53. package/dist/src/artifacts.d.ts +36 -0
  54. package/dist/src/artifacts.js +1372 -0
  55. package/dist/src/atlas-grafana.d.ts +377 -0
  56. package/dist/src/atlas-grafana.js +47 -0
  57. package/dist/src/atlas-html.d.ts +2 -0
  58. package/dist/src/atlas-html.js +34 -0
  59. package/dist/src/atlas.d.ts +130 -0
  60. package/dist/src/atlas.js +235 -0
  61. package/dist/src/behavior-assertions.d.ts +95 -0
  62. package/dist/src/behavior-assertions.js +154 -0
  63. package/dist/src/cli.d.ts +5 -0
  64. package/dist/src/cli.js +957 -0
  65. package/dist/src/codex-judge.d.ts +44 -0
  66. package/dist/src/codex-judge.js +214 -0
  67. package/dist/src/codex-run.d.ts +114 -0
  68. package/dist/src/codex-run.js +519 -0
  69. package/dist/src/codex.d.ts +187 -0
  70. package/dist/src/codex.js +1028 -0
  71. package/dist/src/contracts.d.ts +155 -0
  72. package/dist/src/contracts.js +866 -0
  73. package/dist/src/corpus.d.ts +67 -0
  74. package/dist/src/corpus.js +540 -0
  75. package/dist/src/cursor-sdk-runner.d.ts +81 -0
  76. package/dist/src/cursor-sdk-runner.js +362 -0
  77. package/dist/src/cursor-sdk.d.ts +81 -0
  78. package/dist/src/cursor-sdk.js +1078 -0
  79. package/dist/src/deepseek-adapter.d.ts +152 -0
  80. package/dist/src/deepseek-adapter.js +777 -0
  81. package/dist/src/exports.d.ts +69 -0
  82. package/dist/src/exports.js +966 -0
  83. package/dist/src/human-calibration.d.ts +167 -0
  84. package/dist/src/human-calibration.js +618 -0
  85. package/dist/src/index.d.ts +71 -0
  86. package/dist/src/index.js +38 -0
  87. package/dist/src/lifecycle.d.ts +196 -0
  88. package/dist/src/lifecycle.js +1889 -0
  89. package/dist/src/normalization-integrity.d.ts +128 -0
  90. package/dist/src/normalization-integrity.js +429 -0
  91. package/dist/src/openhands-run.d.ts +36 -0
  92. package/dist/src/openhands-run.js +415 -0
  93. package/dist/src/openhands.d.ts +130 -0
  94. package/dist/src/openhands.js +859 -0
  95. package/dist/src/pi.d.ts +203 -0
  96. package/dist/src/pi.js +1345 -0
  97. package/dist/src/process-protocol.d.ts +245 -0
  98. package/dist/src/process-protocol.js +1261 -0
  99. package/dist/src/retained-evidence.d.ts +12 -0
  100. package/dist/src/retained-evidence.js +201 -0
  101. package/dist/src/run-bundles.d.ts +193 -0
  102. package/dist/src/run-bundles.js +993 -0
  103. package/dist/src/scheduler.d.ts +109 -0
  104. package/dist/src/scheduler.js +1080 -0
  105. package/dist/src/semantic-judge.d.ts +194 -0
  106. package/dist/src/semantic-judge.js +875 -0
  107. package/dist/src/structural-observations.d.ts +124 -0
  108. package/dist/src/structural-observations.js +671 -0
  109. package/dist/src/task-packets.d.ts +115 -0
  110. package/dist/src/task-packets.js +683 -0
  111. package/dist/src/uniform-events.d.ts +120 -0
  112. package/dist/src/uniform-events.js +158 -0
  113. package/dist/src/verifiers.d.ts +116 -0
  114. package/dist/src/verifiers.js +819 -0
  115. package/dist/src/workspaces.d.ts +56 -0
  116. package/dist/src/workspaces.js +1314 -0
  117. package/docs/README.md +60 -0
  118. package/docs/development/README.md +71 -0
  119. package/docs/development/documentation-sync.md +58 -0
  120. package/docs/development/extension-contracts.md +307 -0
  121. package/docs/evaluation/README.md +48 -0
  122. package/docs/evaluation/aggregation.md +112 -0
  123. package/docs/evaluation/behavior-assertions.md +45 -0
  124. package/docs/evaluation/human-calibration.md +170 -0
  125. package/docs/evaluation/normalization-integrity.md +80 -0
  126. package/docs/evaluation/semantic-judge.md +188 -0
  127. package/docs/evaluation/structural-observations.md +84 -0
  128. package/docs/evaluation/uniform-events.md +131 -0
  129. package/docs/guides/atlas.md +144 -0
  130. package/docs/guides/evidence-and-sharing.md +100 -0
  131. package/docs/guides/operator-guide.md +424 -0
  132. package/docs/guides/quickstart.md +152 -0
  133. package/docs/guides/telemetry.md +66 -0
  134. package/docs/harnesses/README.md +31 -0
  135. package/docs/harnesses/claude-agent-sdk.md +61 -0
  136. package/docs/harnesses/codex-harness.md +174 -0
  137. package/docs/harnesses/cursor-sdk.md +149 -0
  138. package/docs/harnesses/deepseek-harness.md +134 -0
  139. package/docs/harnesses/openhands-agent-server.md +153 -0
  140. package/docs/harnesses/pi-sdk.md +66 -0
  141. package/docs/reference/README.md +19 -0
  142. package/docs/reference/agent-sdk-operational-runner.md +118 -0
  143. package/docs/reference/cli.md +114 -0
  144. package/docs/reference/contracts.md +222 -0
  145. package/docs/reference/run-bundle-contract.md +354 -0
  146. package/docs/reference/run-lifecycle.md +54 -0
  147. package/examples/cursor-sdk/README.md +11 -0
  148. package/examples/cursor-sdk/capture-profile.json +1 -0
  149. package/examples/cursor-sdk/harness.json +1 -0
  150. package/examples/cursor-sdk/model.json +1 -0
  151. package/examples/cursor-sdk/native-limits.json +1 -0
  152. package/examples/cursor-sdk/native-tool-policy.json +1 -0
  153. package/ontology/behavior-categories.v1.json +46 -0
  154. package/package.json +66 -0
  155. package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
  156. package/release/0.1.0/README.md +46 -0
  157. package/release/0.1.0/reproducibility.json +61 -0
  158. package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
  159. package/release/0.2.0/README.md +42 -0
  160. package/release/0.2.0/reproducibility.json +73 -0
  161. package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
  162. package/release/0.2.1/README.md +63 -0
  163. package/release/0.2.1/reproducibility.json +73 -0
  164. package/release/README.md +13 -0
  165. package/schemas/aggregation.v1.json +293 -0
  166. package/schemas/behavior-assertions.v1.json +220 -0
  167. package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
  168. package/schemas/experiment.v1.schema.json +188 -0
  169. package/schemas/human-calibration.v1.json +336 -0
  170. package/schemas/normalization-integrity.v1.json +302 -0
  171. package/schemas/run-bundles/v1.json +1040 -0
  172. package/schemas/run-queue.v1.schema.json +206 -0
  173. package/schemas/semantic-judge.v1.json +380 -0
  174. package/schemas/structural-observations.v1.json +131 -0
  175. package/schemas/task-packet-freeze.v1.schema.json +106 -0
  176. package/schemas/task-packet.v1.schema.json +234 -0
  177. package/schemas/uniform-events/v1.json +289 -0
  178. package/scripts/atlas-grafana.sh +18 -0
@@ -0,0 +1,1261 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { closeSync, constants, fsyncSync, mkdirSync, openSync, readSync, realpathSync, statSync, unlinkSync, writeSync } from "node:fs";
4
+ import { mkdir, open } from "node:fs/promises";
5
+ import { basename, dirname, resolve } from "node:path";
6
+ const DEFAULT_MAX_LINE_BYTES = 4 * 1024 * 1024;
7
+ const DEFAULT_MAX_RECORD_BYTES = DEFAULT_MAX_LINE_BYTES * 8 + 4096;
8
+ const DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
9
+ const DEFAULT_SHUTDOWN_GRACE_MS = 250;
10
+ const DEFAULT_KILL_GRACE_MS = 250;
11
+ const DEFAULT_MAX_IN_MEMORY_OBSERVATIONS = 1024;
12
+ // A frame observation retains both the original line and parsed payload. Keep
13
+ // the same conservative envelope sizing used for internally owned writers
14
+ // when a caller supplies a writer with a smaller record bound.
15
+ const PROTOCOL_RECORD_ENVELOPE_MULTIPLIER = 8;
16
+ const PROTOCOL_RECORD_ENVELOPE_OVERHEAD = 4096;
17
+ const MAX_TIMER_MS = 2_147_483_647;
18
+ const INTERNAL_RECORDER_TOKEN = Symbol("internal recorder event");
19
+ const CLAIMED_WRITER_PATHS = new Set();
20
+ /**
21
+ * Appends JSON records one line at a time and optionally fsyncs each append.
22
+ * A queued writer keeps records ordered even when protocol callbacks overlap.
23
+ */
24
+ export class JsonlEvidenceWriter {
25
+ path;
26
+ maxLineBytes;
27
+ shouldFsync;
28
+ exclusive;
29
+ handle;
30
+ queue = Promise.resolve();
31
+ closed = false;
32
+ poisoned = false;
33
+ claimed = false;
34
+ preparedExclusive = false;
35
+ hasWrites = false;
36
+ claimLockFd;
37
+ claimLockPath;
38
+ claimCanonicalPath;
39
+ _count = 0;
40
+ constructor(path, options = {}) {
41
+ if (path.trim() === "")
42
+ throw new Error("JSONL evidence path is required.");
43
+ this.path = resolve(path);
44
+ this.maxLineBytes = positiveInteger(options.maxLineBytes ?? DEFAULT_MAX_RECORD_BYTES, "JSONL line limit");
45
+ this.shouldFsync = options.fsync ?? true;
46
+ this.exclusive = options.exclusive ?? false;
47
+ }
48
+ get count() {
49
+ return this._count;
50
+ }
51
+ get maxRecordBytes() {
52
+ return this.maxLineBytes;
53
+ }
54
+ append(record, token) {
55
+ if (this.poisoned)
56
+ return Promise.reject(new Error("JSONL evidence writer is unusable after an ambiguous append."));
57
+ if (this.claimed && token !== INTERNAL_RECORDER_TOKEN) {
58
+ return Promise.reject(new Error("JSONL evidence writer is claimed by a protocol recorder."));
59
+ }
60
+ if (!isRecord(record))
61
+ return Promise.reject(new Error("JSONL evidence records must be objects."));
62
+ let line;
63
+ try {
64
+ assertJsonValue(record, "JSONL evidence record", new Set());
65
+ line = `${JSON.stringify(record)}\n`;
66
+ }
67
+ catch (error) {
68
+ return Promise.reject(error);
69
+ }
70
+ if (Buffer.byteLength(line) > this.maxLineBytes) {
71
+ return Promise.reject(new Error(`JSONL evidence record exceeds ${this.maxLineBytes} bytes.`));
72
+ }
73
+ this.hasWrites = true;
74
+ const operation = this.queue.then(async () => {
75
+ let directLockFd;
76
+ let directLockPath;
77
+ try {
78
+ if (token !== INTERNAL_RECORDER_TOKEN) {
79
+ const canonicalPath = canonicalWriterPath(this.path);
80
+ directLockPath = `${canonicalPath}.protocol.lock`;
81
+ mkdirSync(dirname(canonicalPath), { recursive: true, mode: 0o700 });
82
+ try {
83
+ directLockFd = openSync(directLockPath, "wx", 0o600);
84
+ }
85
+ catch (error) {
86
+ throw new Error(`JSONL evidence path is reserved by a protocol recorder: ${errorMessage(error)}`);
87
+ }
88
+ }
89
+ if (this.closed)
90
+ throw new Error("JSONL evidence writer is closed.");
91
+ const handle = await this.openHandle();
92
+ let wrote = false;
93
+ try {
94
+ const bytes = Buffer.from(line, "utf8");
95
+ let offset = 0;
96
+ while (offset < bytes.length) {
97
+ const { bytesWritten } = await handle.write(bytes.subarray(offset));
98
+ if (bytesWritten <= 0)
99
+ throw new Error("JSONL evidence write made no progress.");
100
+ wrote = true;
101
+ offset += bytesWritten;
102
+ }
103
+ if (this.shouldFsync)
104
+ await handle.sync();
105
+ this._count += 1;
106
+ }
107
+ catch (error) {
108
+ if (wrote)
109
+ this.poisoned = true;
110
+ throw error;
111
+ }
112
+ }
113
+ finally {
114
+ if (directLockFd !== undefined && directLockPath !== undefined) {
115
+ closeSync(directLockFd);
116
+ try {
117
+ unlinkSync(directLockPath);
118
+ }
119
+ catch {
120
+ // Preserve the append result; a missing lock is already released.
121
+ }
122
+ }
123
+ }
124
+ });
125
+ this.queue = operation.catch(() => undefined);
126
+ return operation;
127
+ }
128
+ flush() {
129
+ const operation = this.queue.then(async () => {
130
+ if (this.handle !== undefined && this.shouldFsync)
131
+ await this.handle.sync();
132
+ });
133
+ this.queue = operation.catch(() => undefined);
134
+ return operation;
135
+ }
136
+ close() {
137
+ const operation = this.queue.then(async () => {
138
+ if (this.closed) {
139
+ this.releaseClaimNow();
140
+ return;
141
+ }
142
+ this.closed = true;
143
+ try {
144
+ if (this.handle !== undefined) {
145
+ await this.handle.close();
146
+ this.handle = undefined;
147
+ }
148
+ }
149
+ finally {
150
+ this.releaseClaimNow();
151
+ }
152
+ });
153
+ this.queue = operation.catch(() => undefined);
154
+ return operation;
155
+ }
156
+ ensureOpen() {
157
+ const operation = this.queue.then(async () => {
158
+ if (this.closed)
159
+ throw new Error("JSONL evidence writer is closed.");
160
+ await this.openHandle();
161
+ });
162
+ this.queue = operation.catch(() => undefined);
163
+ return operation;
164
+ }
165
+ claim(token) {
166
+ if (this.closed)
167
+ throw new Error("JSONL evidence writer is closed.");
168
+ if (this.claimed)
169
+ throw new Error("JSONL evidence writer is already claimed by a protocol recorder.");
170
+ const canonicalPath = canonicalWriterPath(this.path);
171
+ if (CLAIMED_WRITER_PATHS.has(canonicalPath))
172
+ throw new Error("JSONL evidence path is already claimed by a protocol recorder.");
173
+ rejectHardLinkedPath(canonicalPath);
174
+ if (this.hasWrites)
175
+ throw new Error("JSONL evidence writer cannot be claimed after direct writes.");
176
+ if (token !== INTERNAL_RECORDER_TOKEN)
177
+ throw new Error("JSONL evidence writer claim is internal.");
178
+ mkdirSync(dirname(canonicalPath), { recursive: true, mode: 0o700 });
179
+ const lockPath = `${canonicalPath}.protocol.lock`;
180
+ let lockFd;
181
+ try {
182
+ lockFd = openSync(lockPath, "wx", 0o600);
183
+ writeSync(lockFd, `${process.pid}\n`, undefined, "utf8");
184
+ fsyncSync(lockFd);
185
+ syncDirectory(dirname(lockPath));
186
+ this.prepareForClaim();
187
+ const sequence = recoverWriterSequence(this.path, this.maxLineBytes);
188
+ this.claimed = true;
189
+ this.claimLockFd = lockFd;
190
+ this.claimLockPath = lockPath;
191
+ this.claimCanonicalPath = canonicalPath;
192
+ CLAIMED_WRITER_PATHS.add(canonicalPath);
193
+ return sequence;
194
+ }
195
+ catch (error) {
196
+ if (lockFd !== undefined) {
197
+ closeSync(lockFd);
198
+ try {
199
+ unlinkSync(lockPath);
200
+ syncDirectory(dirname(lockPath));
201
+ }
202
+ catch {
203
+ // Preserve the original claim/recovery error.
204
+ }
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+ releaseClaim(token) {
210
+ if (token !== INTERNAL_RECORDER_TOKEN)
211
+ throw new Error("JSONL evidence writer release is internal.");
212
+ this.releaseClaimNow();
213
+ }
214
+ releaseClaimNow() {
215
+ this.claimed = false;
216
+ if (this.claimLockFd === undefined || this.claimLockPath === undefined)
217
+ return;
218
+ const lockFd = this.claimLockFd;
219
+ const lockPath = this.claimLockPath;
220
+ const canonicalPath = this.claimCanonicalPath;
221
+ this.claimLockFd = undefined;
222
+ this.claimLockPath = undefined;
223
+ this.claimCanonicalPath = undefined;
224
+ if (canonicalPath !== undefined)
225
+ CLAIMED_WRITER_PATHS.delete(canonicalPath);
226
+ closeSync(lockFd);
227
+ try {
228
+ unlinkSync(lockPath);
229
+ syncDirectory(dirname(lockPath));
230
+ }
231
+ catch {
232
+ // A missing lock is already released; other errors do not change the closed writer state.
233
+ }
234
+ }
235
+ async openHandle() {
236
+ if (this.handle !== undefined)
237
+ return this.handle;
238
+ await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
239
+ const mode = this.preparedExclusive ? "a" : this.exclusive ? "wx" : "a";
240
+ const handle = await open(this.path, mode, 0o600);
241
+ try {
242
+ syncDirectory(dirname(this.path));
243
+ this.preparedExclusive = false;
244
+ this.handle = handle;
245
+ return handle;
246
+ }
247
+ catch (error) {
248
+ await handle.close().catch(() => undefined);
249
+ throw error;
250
+ }
251
+ }
252
+ prepareForClaim() {
253
+ if (this.handle !== undefined)
254
+ return;
255
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
256
+ const descriptor = openSync(this.path, this.exclusive ? "wx" : "a", 0o600);
257
+ try {
258
+ fsyncSync(descriptor);
259
+ }
260
+ finally {
261
+ closeSync(descriptor);
262
+ }
263
+ syncDirectory(dirname(this.path));
264
+ this.preparedExclusive = this.exclusive;
265
+ }
266
+ }
267
+ export async function openJsonlEvidenceWriter(path, options = {}) {
268
+ const writer = new JsonlEvidenceWriter(path, options);
269
+ await writer.ensureOpen();
270
+ return writer;
271
+ }
272
+ /** Keeps a bounded diagnostic prefix while continuing to drain the stream. */
273
+ export class BoundedDiagnosticCapture {
274
+ maxBytes;
275
+ chunks = [];
276
+ _sizeBytes = 0;
277
+ _truncated = false;
278
+ keptBytes = 0;
279
+ constructor(maxBytes = DEFAULT_MAX_STDERR_BYTES) {
280
+ this.maxBytes = maxBytes;
281
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
282
+ throw new Error("Diagnostic byte limit must be a nonnegative safe integer.");
283
+ }
284
+ }
285
+ write(chunk) {
286
+ const bytes = Buffer.from(chunk);
287
+ this._sizeBytes += bytes.length;
288
+ const kept = Math.max(0, Math.min(bytes.length, this.maxBytes - this.keptBytes));
289
+ if (kept > 0) {
290
+ this.chunks.push(bytes.subarray(0, kept));
291
+ this.keptBytes += kept;
292
+ }
293
+ if (kept < bytes.length)
294
+ this._truncated = true;
295
+ }
296
+ get sizeBytes() {
297
+ return this._sizeBytes;
298
+ }
299
+ get truncated() {
300
+ return this._truncated;
301
+ }
302
+ result() {
303
+ const bytes = Buffer.concat(this.chunks);
304
+ return {
305
+ bytes,
306
+ text: bytes.toString("utf8"),
307
+ sizeBytes: this._sizeBytes,
308
+ truncated: this._truncated,
309
+ maxBytes: this.maxBytes,
310
+ };
311
+ }
312
+ }
313
+ export class ProtocolEvidenceRecorder {
314
+ writer;
315
+ source;
316
+ now;
317
+ sequence = 0;
318
+ records = [];
319
+ maxInMemoryObservations;
320
+ queue = Promise.resolve();
321
+ fenced = false;
322
+ _dropped = 0;
323
+ constructor(writer, source, now = () => new Date().toISOString(), maxInMemoryObservations = DEFAULT_MAX_IN_MEMORY_OBSERVATIONS) {
324
+ this.writer = writer;
325
+ this.source = source;
326
+ this.now = now;
327
+ assertNonEmpty(source, "Protocol source");
328
+ this.maxInMemoryObservations = positiveInteger(maxInMemoryObservations, "In-memory observation limit");
329
+ this.sequence = writer.claim(INTERNAL_RECORDER_TOKEN);
330
+ }
331
+ get observations() {
332
+ return this.records.map((record) => structuredClone(record));
333
+ }
334
+ get droppedObservations() {
335
+ return this._dropped;
336
+ }
337
+ /** Prevents late observer callbacks from appending after process finalization. */
338
+ fence() {
339
+ this.fenced = true;
340
+ }
341
+ recordFrame(payload, observedAt = this.now(), raw, parseError) {
342
+ if (payload === undefined && parseError === undefined)
343
+ return Promise.reject(new Error("Frame payload is required."));
344
+ if (raw !== undefined)
345
+ assertNonEmpty(raw, "Raw frame text");
346
+ if (parseError !== undefined)
347
+ assertNonEmpty(parseError, "Frame parse error");
348
+ return this.record({
349
+ kind: "frame",
350
+ source: this.source,
351
+ stream: "stdout",
352
+ ...(raw === undefined ? {} : { raw }),
353
+ ...(parseError === undefined ? { payload } : { parseError }),
354
+ observedAt,
355
+ });
356
+ }
357
+ recordRequest(input) {
358
+ return this.record({ ...input, kind: "request", observedAt: input.observedAt ?? this.now() });
359
+ }
360
+ recordResponse(input) {
361
+ return this.record({ ...input, kind: "response", observedAt: input.observedAt ?? this.now() });
362
+ }
363
+ recordNotification(input) {
364
+ return this.record({ ...input, kind: "notification", observedAt: input.observedAt ?? this.now() });
365
+ }
366
+ recordCompletion(input) {
367
+ assertNonEmpty(input.source, "Completion source");
368
+ assertNonEmpty(input.status, "Completion status");
369
+ if (input.evidence === undefined)
370
+ return Promise.reject(new Error("Completion evidence is required."));
371
+ return this.record({
372
+ kind: "completion",
373
+ source: input.source,
374
+ ...(input.method === undefined ? {} : { method: input.method }),
375
+ ...(input.sourceIdentity === undefined ? {} : { sourceIdentity: input.sourceIdentity }),
376
+ evidence: input.evidence,
377
+ status: input.status,
378
+ observedAt: input.observedAt ?? this.now(),
379
+ });
380
+ }
381
+ recordCapability(input) {
382
+ assertNonEmpty(input.source, "Capability source");
383
+ assertNonEmpty(input.name, "Capability name");
384
+ if (!["available", "unsupported", "missing", "not-checked"].includes(String(input.status))) {
385
+ return Promise.reject(new Error("Capability status is invalid."));
386
+ }
387
+ return this.record({
388
+ kind: "capability",
389
+ source: input.source,
390
+ capability: input.name,
391
+ status: input.status,
392
+ ...(input.details === undefined ? {} : { evidence: input.details }),
393
+ observedAt: input.observedAt ?? this.now(),
394
+ });
395
+ }
396
+ recordError(message, evidence, token) {
397
+ assertNonEmpty(message, "Protocol error");
398
+ return this.record({
399
+ kind: "error",
400
+ source: this.source,
401
+ status: message,
402
+ ...(evidence === undefined ? {} : { evidence }),
403
+ observedAt: this.now(),
404
+ }, token === INTERNAL_RECORDER_TOKEN);
405
+ }
406
+ recordProcess(status, evidence, token) {
407
+ assertNonEmpty(status, "Process status");
408
+ return this.record({
409
+ kind: "process",
410
+ source: this.source,
411
+ status,
412
+ ...(evidence === undefined ? {} : { evidence }),
413
+ observedAt: this.now(),
414
+ }, token === INTERNAL_RECORDER_TOKEN);
415
+ }
416
+ flush() {
417
+ return this.queue.then(() => this.writer.flush());
418
+ }
419
+ close() {
420
+ return this.queue.then(() => this.writer.close());
421
+ }
422
+ record(input, allowFenced = false) {
423
+ if (this.fenced && !allowFenced)
424
+ return Promise.reject(new Error("Protocol evidence recorder is fenced."));
425
+ assertNonEmpty(input.source, "Protocol source");
426
+ assertNonEmpty(input.observedAt, "Observation timestamp");
427
+ if (input.method !== undefined)
428
+ assertNonEmpty(input.method, "Protocol method");
429
+ if (input.sourceIdentity !== undefined)
430
+ assertNonEmpty(input.sourceIdentity, "Protocol source identity");
431
+ if (input.id !== undefined && (typeof input.id === "number" && !Number.isFinite(input.id)
432
+ || typeof input.id !== "string" && typeof input.id !== "number" && input.id !== null)) {
433
+ return Promise.reject(new Error("Protocol identity must be a finite JSON string, number, or null."));
434
+ }
435
+ let payload;
436
+ let evidence;
437
+ try {
438
+ if (input.payload !== undefined)
439
+ payload = cloneJsonValue(input.payload, "payload");
440
+ if (input.evidence !== undefined)
441
+ evidence = cloneJsonValue(input.evidence, "evidence");
442
+ }
443
+ catch (error) {
444
+ return Promise.reject(error);
445
+ }
446
+ const operation = this.queue.then(async () => {
447
+ const observation = {
448
+ schemaVersion: "ebo.protocol-observation/v1",
449
+ sequence: this.sequence + 1,
450
+ observedAt: input.observedAt,
451
+ kind: input.kind,
452
+ source: input.source,
453
+ ...(input.stream === undefined ? {} : { stream: input.stream }),
454
+ ...(input.method === undefined ? {} : { method: input.method }),
455
+ ...(input.id === undefined ? {} : { id: input.id }),
456
+ ...(input.sourceIdentity === undefined ? {} : { sourceIdentity: input.sourceIdentity }),
457
+ ...(input.raw === undefined ? {} : { raw: input.raw }),
458
+ ...(input.parseError === undefined ? {} : { parseError: input.parseError }),
459
+ ...(payload === undefined ? {} : { payload }),
460
+ ...(evidence === undefined ? {} : { evidence }),
461
+ ...(input.status === undefined ? {} : { status: input.status }),
462
+ ...(input.capability === undefined ? {} : { capability: input.capability }),
463
+ };
464
+ await this.writer.append(observation, INTERNAL_RECORDER_TOKEN);
465
+ this.sequence = observation.sequence;
466
+ if (this.records.length >= this.maxInMemoryObservations) {
467
+ this.records.shift();
468
+ this._dropped += 1;
469
+ }
470
+ this.records.push(observation);
471
+ return structuredClone(observation);
472
+ });
473
+ this.queue = operation.then(() => undefined, () => undefined);
474
+ return operation;
475
+ }
476
+ }
477
+ export class ProtocolProcess {
478
+ options;
479
+ child;
480
+ recorder;
481
+ stderrCapture;
482
+ stdoutLineLimit;
483
+ shutdownGraceMs;
484
+ killGraceMs;
485
+ maxInMemoryObservations;
486
+ writer;
487
+ now;
488
+ command;
489
+ detached;
490
+ ownsWriter;
491
+ evidencePath;
492
+ stderrPath;
493
+ stderrPersisted = false;
494
+ startedAt;
495
+ cwd;
496
+ args;
497
+ onFrame;
498
+ nextLineNumber = 1;
499
+ frameCount = 0;
500
+ lineQueue = Promise.resolve();
501
+ pendingLineParts = [];
502
+ pendingLineBytes = 0;
503
+ protocolFailureQueued = false;
504
+ waitPromise;
505
+ termination = "natural";
506
+ protocolError;
507
+ recorderError;
508
+ childError;
509
+ signalQueue = Promise.resolve();
510
+ abortListener;
511
+ closeObserved = false;
512
+ finishStarted = false;
513
+ resolveResult;
514
+ constructor(options) {
515
+ this.options = options;
516
+ assertNonEmpty(options.command, "Protocol command");
517
+ assertNonEmpty(options.source, "Protocol source");
518
+ const requestedLineLimit = positiveInteger(options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES, "Protocol line limit");
519
+ this.stderrCapture = new BoundedDiagnosticCapture(options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES);
520
+ this.shutdownGraceMs = nonnegativeInteger(options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS, "Shutdown grace period");
521
+ this.killGraceMs = nonnegativeInteger(options.killGraceMs ?? DEFAULT_KILL_GRACE_MS, "Kill grace period");
522
+ if (this.shutdownGraceMs > MAX_TIMER_MS)
523
+ throw new Error("Shutdown grace period must not exceed the Node timer maximum.");
524
+ if (this.killGraceMs > MAX_TIMER_MS)
525
+ throw new Error("Kill grace period must not exceed the Node timer maximum.");
526
+ this.maxInMemoryObservations = positiveInteger(options.maxInMemoryObservations ?? DEFAULT_MAX_IN_MEMORY_OBSERVATIONS, "In-memory observation limit");
527
+ this.now = options.now ?? (() => new Date().toISOString());
528
+ this.command = options.command;
529
+ this.detached = options.spawnOptions?.detached ?? process.platform !== "win32";
530
+ this.startedAt = this.now();
531
+ assertTimestamp(this.startedAt, "Process start timestamp");
532
+ this.cwd = resolve(options.cwd ?? process.cwd());
533
+ this.args = [...(options.args ?? [])];
534
+ this.onFrame = options.onFrame;
535
+ let writer;
536
+ if (options.writer !== undefined) {
537
+ writer = options.writer;
538
+ }
539
+ else {
540
+ const evidencePath = options.evidencePath ?? resolve(process.cwd(), `.ebo-protocol-${randomUUID()}.jsonl`);
541
+ mkdirSync(dirname(evidencePath), { recursive: true, mode: 0o700 });
542
+ const descriptor = openSync(evidencePath, "wx", 0o600);
543
+ closeSync(descriptor);
544
+ syncDirectory(dirname(resolve(evidencePath)));
545
+ writer = new JsonlEvidenceWriter(evidencePath, {
546
+ maxLineBytes: Math.min(Number.MAX_SAFE_INTEGER, requestedLineLimit * 8 + 4096),
547
+ });
548
+ }
549
+ const configuredEvidencePath = options.evidencePath === undefined ? undefined : resolve(options.evidencePath);
550
+ if (options.writer !== undefined && configuredEvidencePath !== undefined && configuredEvidencePath !== writer.path) {
551
+ throw new Error("Protocol writer path conflicts with evidencePath.");
552
+ }
553
+ this.stdoutLineLimit = protocolLineLimitForWriter(requestedLineLimit, writer.maxRecordBytes, options.writer !== undefined);
554
+ this.ownsWriter = options.writer === undefined;
555
+ this.evidencePath = writer.path;
556
+ this.stderrPath = options.stderrPath === undefined ? undefined : resolve(options.stderrPath);
557
+ this.writer = writer;
558
+ this.recorder = new ProtocolEvidenceRecorder(writer, options.source, this.now, this.maxInMemoryObservations);
559
+ const callerSpawnOptions = { ...options.spawnOptions };
560
+ delete callerSpawnOptions.signal;
561
+ delete callerSpawnOptions.timeout;
562
+ delete callerSpawnOptions.killSignal;
563
+ const spawnOptions = {
564
+ ...callerSpawnOptions,
565
+ cwd: this.cwd,
566
+ env: options.env,
567
+ stdio: ["pipe", "pipe", "pipe"],
568
+ detached: this.detached,
569
+ };
570
+ try {
571
+ this.child = spawn(options.command, this.args, spawnOptions);
572
+ }
573
+ catch (error) {
574
+ this.writer.releaseClaim(INTERNAL_RECORDER_TOKEN);
575
+ throw error;
576
+ }
577
+ this.child.on("error", (error) => {
578
+ this.childError ??= errorMessage(error);
579
+ void this.recorder.recordError(this.childError, undefined, INTERNAL_RECORDER_TOKEN).catch(() => undefined);
580
+ });
581
+ this.child.stdin?.on("error", (error) => {
582
+ this.childError ??= `Protocol stdin failed: ${errorMessage(error)}`;
583
+ void this.recorder.recordError(this.childError, undefined, INTERNAL_RECORDER_TOKEN).catch(() => undefined);
584
+ });
585
+ this.attachStreams();
586
+ this.waitPromise = new Promise((resolveResult) => {
587
+ this.resolveResult = resolveResult;
588
+ this.child.once("close", (exitCode, signal) => {
589
+ this.beginFinish(exitCode, signal);
590
+ });
591
+ });
592
+ const abort = () => {
593
+ void this.interrupt();
594
+ };
595
+ this.abortListener = abort;
596
+ if (this.options.signal?.aborted)
597
+ abort();
598
+ else
599
+ this.options.signal?.addEventListener("abort", abort, { once: true });
600
+ if (this.childError !== undefined)
601
+ void this.recorder.recordError(this.childError, undefined, INTERNAL_RECORDER_TOKEN);
602
+ }
603
+ get pid() {
604
+ return this.child.pid ?? undefined;
605
+ }
606
+ get stdin() {
607
+ if (this.child.stdin === null)
608
+ throw new Error("Protocol process stdin is unavailable.");
609
+ return this.child.stdin;
610
+ }
611
+ get evidence() {
612
+ return this.recorder;
613
+ }
614
+ wait() {
615
+ return this.waitPromise;
616
+ }
617
+ async shutdown() {
618
+ if (this.closeObserved)
619
+ return this.wait();
620
+ this.setTermination("shutdown");
621
+ await this.sendSignal("SIGTERM", this.shutdownGraceMs);
622
+ return this.wait();
623
+ }
624
+ async interrupt(graceMs = this.shutdownGraceMs, killGraceMs = this.killGraceMs) {
625
+ if (this.closeObserved)
626
+ return this.wait();
627
+ const boundedGraceMs = nonnegativeInteger(graceMs, "Interrupt grace period");
628
+ const boundedKillGraceMs = nonnegativeInteger(killGraceMs, "Interrupt kill grace period");
629
+ if (boundedGraceMs > MAX_TIMER_MS || boundedKillGraceMs > MAX_TIMER_MS) {
630
+ throw new Error("Interrupt grace periods must not exceed the Node timer maximum.");
631
+ }
632
+ this.setTermination("interrupted");
633
+ await this.sendSignal("SIGINT", boundedGraceMs, boundedKillGraceMs);
634
+ return this.wait();
635
+ }
636
+ attachStreams() {
637
+ if (this.child.stderr !== null) {
638
+ this.child.stderr.on("data", (chunk) => {
639
+ this.stderrCapture.write(chunk);
640
+ if (this.options.onStderr !== undefined) {
641
+ this.child.stderr?.pause();
642
+ this.lineQueue = this.lineQueue
643
+ .then(() => this.options.onStderr(chunk, false, this.recorder))
644
+ .then(() => undefined)
645
+ .catch((error) => this.failRecorder(`Protocol stderr recording failed: ${errorMessage(error)}`));
646
+ void this.lineQueue.finally(() => this.child.stderr?.resume()).catch(() => undefined);
647
+ }
648
+ });
649
+ this.child.stderr.on("end", () => {
650
+ if (this.options.onStderr !== undefined) {
651
+ this.lineQueue = this.lineQueue
652
+ .then(() => this.options.onStderr(Buffer.alloc(0), true, this.recorder))
653
+ .then(() => undefined)
654
+ .catch((error) => this.failRecorder(`Protocol stderr recording failed: ${errorMessage(error)}`));
655
+ }
656
+ });
657
+ }
658
+ if (this.child.stdout === null)
659
+ return;
660
+ this.child.stdout.on("data", (chunk) => {
661
+ this.child.stdout?.pause();
662
+ try {
663
+ this.consumeStdout(chunk);
664
+ }
665
+ catch (error) {
666
+ void this.failRecorder(`Protocol output processing failed: ${errorMessage(error)}`);
667
+ }
668
+ void this.lineQueue.finally(() => this.child.stdout?.resume()).catch(() => undefined);
669
+ });
670
+ this.child.stdout.on("end", () => this.flushPendingLine());
671
+ }
672
+ consumeStdout(chunk) {
673
+ if (this.protocolError !== undefined || this.recorderError !== undefined)
674
+ return;
675
+ let offset = 0;
676
+ while (offset < chunk.length) {
677
+ const newline = chunk.indexOf(0x0a, offset);
678
+ if (newline === -1) {
679
+ this.appendPendingLine(chunk.subarray(offset));
680
+ return;
681
+ }
682
+ if (!this.appendPendingLine(chunk.subarray(offset, newline)))
683
+ return;
684
+ this.queuePendingLine(this.nextLineNumber);
685
+ this.nextLineNumber += 1;
686
+ offset = newline + 1;
687
+ }
688
+ }
689
+ appendPendingLine(part) {
690
+ if (part.length === 0)
691
+ return true;
692
+ this.pendingLineBytes += part.length;
693
+ if (this.pendingLineBytes > this.stdoutLineLimit) {
694
+ this.pendingLineParts = [];
695
+ this.pendingLineBytes = 0;
696
+ if (!this.protocolFailureQueued) {
697
+ this.protocolFailureQueued = true;
698
+ const line = this.nextLineNumber;
699
+ this.lineQueue = this.lineQueue
700
+ .then(() => this.failProtocol(`Protocol line ${line} exceeds the configured byte limit.`, undefined, line))
701
+ .catch((error) => this.failRecorder(`Protocol evidence recording failed: ${errorMessage(error)}`));
702
+ }
703
+ return false;
704
+ }
705
+ this.pendingLineParts.push(part);
706
+ return true;
707
+ }
708
+ queuePendingLine(lineNumber) {
709
+ const bytes = Buffer.concat(this.pendingLineParts, this.pendingLineBytes);
710
+ this.pendingLineParts = [];
711
+ this.pendingLineBytes = 0;
712
+ this.lineQueue = this.lineQueue
713
+ .then(() => this.processLine(bytes, lineNumber))
714
+ .catch((error) => this.failRecorder(`Protocol evidence recording failed: ${errorMessage(error)}`));
715
+ }
716
+ flushPendingLine() {
717
+ if (this.pendingLineBytes > 0 && this.protocolError === undefined && this.recorderError === undefined) {
718
+ const bytes = Buffer.concat(this.pendingLineParts, this.pendingLineBytes);
719
+ this.pendingLineParts = [];
720
+ this.pendingLineBytes = 0;
721
+ const line = this.nextLineNumber;
722
+ this.protocolFailureQueued = true;
723
+ this.lineQueue = this.lineQueue
724
+ .then(() => this.failProtocol("Unterminated JSONL protocol output.", bytes.toString("base64"), line))
725
+ .catch((error) => this.failRecorder(`Protocol evidence recording failed: ${errorMessage(error)}`));
726
+ this.nextLineNumber += 1;
727
+ }
728
+ }
729
+ async processLine(bytes, lineNumber) {
730
+ if (this.recorderError !== undefined || (this.protocolError !== undefined && lineNumber >= this.protocolError.line))
731
+ return;
732
+ let line;
733
+ try {
734
+ line = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
735
+ }
736
+ catch (error) {
737
+ await this.failProtocol(`Malformed UTF-8 protocol output: ${errorMessage(error)}`, bytes.toString("base64"), lineNumber);
738
+ return;
739
+ }
740
+ let payload;
741
+ try {
742
+ payload = JSON.parse(line);
743
+ }
744
+ catch (error) {
745
+ await this.failProtocol(`Malformed JSONL protocol output: ${errorMessage(error)}`, line, lineNumber);
746
+ return;
747
+ }
748
+ this.frameCount += 1;
749
+ const nonFiniteNumberPath = findNonFiniteJsonNumber(payload);
750
+ const parseError = nonFiniteNumberPath === undefined
751
+ ? undefined
752
+ : `Parsed JSON contains a non-finite number at ${nonFiniteNumberPath}; raw frame retained without a JSON-safe payload.`;
753
+ await this.recorder.recordFrame(nonFiniteNumberPath === undefined ? payload : undefined, this.now(), line, parseError);
754
+ if (this.onFrame !== undefined) {
755
+ const observer = Promise.resolve(this.onFrame(payload, this.recorder));
756
+ const outcome = await settleObserver(observer, this.shutdownGraceMs);
757
+ if (outcome.status === "failed")
758
+ throw outcome.error;
759
+ if (outcome.status === "timed-out") {
760
+ this.recorder.fence();
761
+ throw new Error("Protocol frame observer exceeded its grace period.");
762
+ }
763
+ }
764
+ }
765
+ async failProtocol(message, raw, line = this.nextLineNumber) {
766
+ if (this.protocolError !== undefined)
767
+ return;
768
+ this.protocolError = { line, message, ...(raw === undefined ? {} : { raw }) };
769
+ this.setTermination("malformed");
770
+ try {
771
+ await this.recorder.recordError(message, raw === undefined ? undefined : { raw }, INTERNAL_RECORDER_TOKEN);
772
+ }
773
+ catch (error) {
774
+ this.childError ??= `Protocol error evidence could not be persisted: ${errorMessage(error)}`;
775
+ }
776
+ void this.sendSignal("SIGTERM", this.shutdownGraceMs);
777
+ }
778
+ async failRecorder(message) {
779
+ if (this.recorderError !== undefined || this.protocolError !== undefined)
780
+ return;
781
+ this.recorderError = message;
782
+ this.setTermination("recorder-error");
783
+ try {
784
+ await this.recorder.recordError(message, undefined, INTERNAL_RECORDER_TOKEN);
785
+ }
786
+ catch (error) {
787
+ this.childError ??= `Protocol error evidence could not be persisted: ${errorMessage(error)}`;
788
+ }
789
+ void this.sendSignal("SIGTERM", this.shutdownGraceMs);
790
+ }
791
+ setTermination(termination) {
792
+ if (this.termination === "natural")
793
+ this.termination = termination;
794
+ }
795
+ sendSignal(signal, graceMs, killGraceMs = this.killGraceMs) {
796
+ const operation = this.signalQueue.then(() => this.sendSignalNow(signal, graceMs, killGraceMs));
797
+ this.signalQueue = operation.catch(() => undefined);
798
+ return operation;
799
+ }
800
+ async sendSignalNow(signal, graceMs, killGraceMs) {
801
+ if (this.closeObserved)
802
+ return;
803
+ const pid = this.child.pid;
804
+ if (pid === undefined) {
805
+ this.forceFinish();
806
+ return;
807
+ }
808
+ try {
809
+ if (process.platform !== "win32" && this.detached) {
810
+ process.kill(-pid, signal);
811
+ }
812
+ else {
813
+ this.child.kill(signal);
814
+ }
815
+ }
816
+ catch (error) {
817
+ if (error.code !== "ESRCH")
818
+ this.childError ??= errorMessage(error);
819
+ if (!this.closeObserved)
820
+ this.forceFinish();
821
+ return;
822
+ }
823
+ if (graceMs > 0)
824
+ await this.waitForClose(graceMs);
825
+ if (!this.closeObserved) {
826
+ try {
827
+ if (process.platform !== "win32" && this.detached) {
828
+ process.kill(-pid, "SIGKILL");
829
+ }
830
+ else {
831
+ this.child.kill("SIGKILL");
832
+ }
833
+ }
834
+ catch (error) {
835
+ if (error.code !== "ESRCH")
836
+ this.childError ??= errorMessage(error);
837
+ }
838
+ if (killGraceMs > 0)
839
+ await this.waitForClose(killGraceMs);
840
+ }
841
+ if (!this.closeObserved && (this.detached || this.child.exitCode !== null || this.child.signalCode !== null)) {
842
+ this.forceFinish();
843
+ }
844
+ }
845
+ async waitForClose(milliseconds) {
846
+ if (this.closeObserved)
847
+ return;
848
+ await new Promise((resolvePromise) => {
849
+ const timer = setTimeout(resolvePromise, milliseconds);
850
+ this.child.once("close", () => {
851
+ clearTimeout(timer);
852
+ resolvePromise();
853
+ });
854
+ });
855
+ }
856
+ beginFinish(exitCode, signal) {
857
+ if (this.finishStarted || this.resolveResult === undefined)
858
+ return;
859
+ this.finishStarted = true;
860
+ this.closeObserved = true;
861
+ void this.finish(exitCode, signal, this.resolveResult);
862
+ }
863
+ forceFinish() {
864
+ if (this.closeObserved)
865
+ return;
866
+ this.child.stdout?.destroy();
867
+ this.child.stderr?.destroy();
868
+ this.beginFinish(this.child.exitCode, this.child.signalCode);
869
+ }
870
+ async finish(exitCode, signal, resolveResult) {
871
+ try {
872
+ await this.lineQueue;
873
+ }
874
+ catch (error) {
875
+ this.childError ??= `Protocol output processing failed: ${errorMessage(error)}`;
876
+ }
877
+ this.recorder.fence();
878
+ try {
879
+ await this.recorder.flush();
880
+ await this.recorder.recordProcess(signal === null && exitCode === 0 ? "exited" : "terminated", { exitCode, signal, pid: this.child.pid ?? null }, INTERNAL_RECORDER_TOKEN);
881
+ await this.recorder.flush();
882
+ }
883
+ catch (error) {
884
+ this.childError ??= `Protocol evidence could not be persisted: ${errorMessage(error)}`;
885
+ }
886
+ if (this.options.signal !== undefined && this.abortListener !== undefined) {
887
+ this.options.signal.removeEventListener("abort", this.abortListener);
888
+ }
889
+ const stderr = this.stderrCapture.result();
890
+ if (this.stderrPath !== undefined) {
891
+ let diagnosticFile;
892
+ try {
893
+ await mkdir(dirname(this.stderrPath), { recursive: true, mode: 0o700 });
894
+ diagnosticFile = await open(this.stderrPath, "wx", 0o600);
895
+ await writeFileHandleFully(diagnosticFile, stderr.bytes, "stderr evidence");
896
+ await diagnosticFile.sync();
897
+ await diagnosticFile.close();
898
+ diagnosticFile = undefined;
899
+ syncDirectory(dirname(this.stderrPath));
900
+ this.stderrPersisted = true;
901
+ }
902
+ catch (error) {
903
+ await diagnosticFile?.close().catch(() => undefined);
904
+ this.childError ??= `stderr evidence could not be persisted: ${errorMessage(error)}`;
905
+ }
906
+ }
907
+ if (this.ownsWriter) {
908
+ // The process record above is durable before the writer is closed.
909
+ try {
910
+ await this.recorder.close();
911
+ }
912
+ catch (error) {
913
+ this.childError ??= `Protocol evidence could not be closed: ${errorMessage(error)}`;
914
+ }
915
+ }
916
+ let endedAt;
917
+ try {
918
+ endedAt = this.now();
919
+ assertTimestamp(endedAt, "Process completion timestamp");
920
+ }
921
+ catch (error) {
922
+ this.childError ??= `Process completion timestamp could not be recorded: ${errorMessage(error)}`;
923
+ endedAt = this.startedAt;
924
+ }
925
+ const termination = this.termination;
926
+ const status = this.protocolError !== undefined
927
+ ? "malformed"
928
+ : this.recorderError !== undefined || this.childError !== undefined
929
+ ? "failed"
930
+ : this.termination === "interrupted"
931
+ ? "interrupted"
932
+ : this.termination === "shutdown"
933
+ ? "shutdown"
934
+ : exitCode === 0 && signal === null
935
+ ? "completed"
936
+ : "failed";
937
+ const result = {
938
+ status,
939
+ partial: status === "interrupted" || status === "malformed" || status === "failed",
940
+ protocolOnly: true,
941
+ launch: {
942
+ command: this.command,
943
+ args: this.args,
944
+ cwd: this.cwd,
945
+ pid: this.child.pid ?? null,
946
+ startedAt: this.startedAt,
947
+ endedAt,
948
+ exitCode,
949
+ signal,
950
+ },
951
+ termination,
952
+ stdoutFrames: this.frameCount,
953
+ stderr,
954
+ ...(this.protocolError === undefined ? {} : { protocolError: this.protocolError }),
955
+ ...(this.childError === undefined && this.recorderError === undefined ? {} : { error: this.childError ?? this.recorderError }),
956
+ ...(this.recorderError === undefined ? {} : { recorderError: this.recorderError }),
957
+ ...(this.evidencePath === undefined || this.writer.count === 0 ? {} : { evidencePath: this.evidencePath }),
958
+ ...(this.stderrPersisted && this.stderrPath !== undefined ? { stderrPath: this.stderrPath } : {}),
959
+ droppedObservations: this.recorder.droppedObservations,
960
+ observations: [...this.recorder.observations],
961
+ };
962
+ resolveResult(result);
963
+ }
964
+ }
965
+ export function spawnProtocolProcess(options) {
966
+ return new ProtocolProcess(options);
967
+ }
968
+ export async function runProtocolProcess(options) {
969
+ return spawnProtocolProcess(options).wait();
970
+ }
971
+ function syncDirectory(path) {
972
+ const descriptor = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY);
973
+ try {
974
+ fsyncSync(descriptor);
975
+ }
976
+ finally {
977
+ closeSync(descriptor);
978
+ }
979
+ }
980
+ async function writeFileHandleFully(handle, bytes, label) {
981
+ let offset = 0;
982
+ while (offset < bytes.length) {
983
+ const { bytesWritten } = await handle.write(bytes.subarray(offset));
984
+ if (bytesWritten <= 0)
985
+ throw new Error(`${label} write made no progress.`);
986
+ offset += bytesWritten;
987
+ }
988
+ }
989
+ function protocolLineLimitForWriter(requestedLineLimit, writerMaxRecordBytes, callerSuppliedWriter) {
990
+ if (!callerSuppliedWriter)
991
+ return requestedLineLimit;
992
+ if (writerMaxRecordBytes <= PROTOCOL_RECORD_ENVELOPE_OVERHEAD) {
993
+ throw new Error("JSONL evidence writer is too small for a protocol observation envelope.");
994
+ }
995
+ const available = writerMaxRecordBytes - PROTOCOL_RECORD_ENVELOPE_OVERHEAD;
996
+ const envelopeSafeLimit = Math.max(1, Math.floor(available / PROTOCOL_RECORD_ENVELOPE_MULTIPLIER));
997
+ return Math.min(requestedLineLimit, envelopeSafeLimit);
998
+ }
999
+ function canonicalWriterPath(path) {
1000
+ try {
1001
+ return realpathSync.native(path);
1002
+ }
1003
+ catch (error) {
1004
+ if (error.code !== "ENOENT")
1005
+ throw error;
1006
+ let parent;
1007
+ try {
1008
+ parent = realpathSync.native(dirname(path));
1009
+ }
1010
+ catch (parentError) {
1011
+ if (parentError.code !== "ENOENT")
1012
+ throw parentError;
1013
+ parent = resolve(dirname(path));
1014
+ }
1015
+ return resolve(parent, basename(path));
1016
+ }
1017
+ }
1018
+ function isHardLinkedPath(path) {
1019
+ try {
1020
+ return statSync(path).nlink > 1;
1021
+ }
1022
+ catch (error) {
1023
+ if (error.code === "ENOENT")
1024
+ return false;
1025
+ throw error;
1026
+ }
1027
+ }
1028
+ function rejectHardLinkedPath(path) {
1029
+ if (isHardLinkedPath(path))
1030
+ throw new Error("JSONL evidence path has multiple hard links and cannot be claimed safely.");
1031
+ }
1032
+ function recoverWriterSequence(path, maxLineBytes) {
1033
+ let descriptor;
1034
+ try {
1035
+ descriptor = openSync(path, "r");
1036
+ }
1037
+ catch (error) {
1038
+ if (error.code === "ENOENT")
1039
+ return 0;
1040
+ throw error;
1041
+ }
1042
+ let sequence = 0;
1043
+ let lineNumber = 1;
1044
+ let lineParts = [];
1045
+ let lineBytes = 0;
1046
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1047
+ try {
1048
+ while (true) {
1049
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
1050
+ if (bytesRead === 0)
1051
+ break;
1052
+ const chunk = buffer.subarray(0, bytesRead);
1053
+ let offset = 0;
1054
+ while (offset < chunk.length) {
1055
+ const newline = chunk.indexOf(0x0a, offset);
1056
+ const end = newline === -1 ? chunk.length : newline;
1057
+ const part = chunk.subarray(offset, end);
1058
+ if (part.length > 0) {
1059
+ lineBytes += part.length;
1060
+ if (lineBytes > maxLineBytes) {
1061
+ throw new Error(`Existing JSONL evidence line ${lineNumber} exceeds ${maxLineBytes} bytes.`);
1062
+ }
1063
+ lineParts.push(Buffer.from(part));
1064
+ }
1065
+ if (newline === -1)
1066
+ break;
1067
+ const line = decodeRecoveredLine(lineParts, lineBytes, lineNumber);
1068
+ const value = parseRecoveredObservation(line, lineNumber);
1069
+ if (value.sequence !== sequence + 1) {
1070
+ throw new Error(`Existing JSONL evidence line ${lineNumber} is not a contiguous protocol observation.`);
1071
+ }
1072
+ sequence = value.sequence;
1073
+ lineNumber += 1;
1074
+ lineParts = [];
1075
+ lineBytes = 0;
1076
+ offset = newline + 1;
1077
+ }
1078
+ }
1079
+ if (lineBytes > 0) {
1080
+ throw new Error("Existing JSONL evidence stream has an unterminated final record.");
1081
+ }
1082
+ return sequence;
1083
+ }
1084
+ finally {
1085
+ closeSync(descriptor);
1086
+ }
1087
+ }
1088
+ function decodeRecoveredLine(parts, bytes, line) {
1089
+ try {
1090
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(parts, bytes));
1091
+ }
1092
+ catch (error) {
1093
+ throw new Error(`Existing JSONL evidence line ${line} is malformed: ${errorMessage(error)}`);
1094
+ }
1095
+ }
1096
+ function parseRecoveredObservation(line, lineNumber) {
1097
+ if (line.trim() === "")
1098
+ throw new Error(`Existing JSONL evidence line ${lineNumber} is blank.`);
1099
+ let value;
1100
+ try {
1101
+ value = JSON.parse(line);
1102
+ }
1103
+ catch (error) {
1104
+ throw new Error(`Existing JSONL evidence line ${lineNumber} is malformed: ${errorMessage(error)}`);
1105
+ }
1106
+ assertProtocolObservation(value, lineNumber);
1107
+ return value;
1108
+ }
1109
+ /** Validate a retained protocol envelope against its physical JSONL line. */
1110
+ export function assertProtocolObservation(value, lineNumber) {
1111
+ const candidate = isRecord(value) ? value.sequence : undefined;
1112
+ if (!isRecord(value) || value.schemaVersion !== "ebo.protocol-observation/v1"
1113
+ || !Number.isSafeInteger(lineNumber) || lineNumber < 1
1114
+ || typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate !== lineNumber) {
1115
+ throw new Error(`Existing JSONL evidence line ${lineNumber} is not a contiguous protocol observation.`);
1116
+ }
1117
+ assertRecoveredObservation(value, lineNumber);
1118
+ }
1119
+ function assertRecoveredObservation(value, line) {
1120
+ if (typeof value.observedAt !== "string" || value.observedAt.trim() === "") {
1121
+ throw new Error(`Existing JSONL evidence line ${line} has no valid observation timestamp.`);
1122
+ }
1123
+ const kind = value.kind;
1124
+ if (!["frame", "request", "response", "notification", "completion", "capability", "error", "process"].includes(String(kind))) {
1125
+ throw new Error(`Existing JSONL evidence line ${line} has an invalid observation kind.`);
1126
+ }
1127
+ if (typeof value.source !== "string" || value.source.trim() === "") {
1128
+ throw new Error(`Existing JSONL evidence line ${line} has no valid observation source.`);
1129
+ }
1130
+ if (value.stream !== undefined && value.stream !== "stdout" && value.stream !== "stderr") {
1131
+ throw new Error(`Existing JSONL evidence line ${line} has an invalid observation stream.`);
1132
+ }
1133
+ for (const field of ["method", "sourceIdentity", "raw", "parseError", "status", "capability"]) {
1134
+ if (value[field] !== undefined && (typeof value[field] !== "string" || value[field].trim() === "")) {
1135
+ throw new Error(`Existing JSONL evidence line ${line} has an invalid ${field}.`);
1136
+ }
1137
+ }
1138
+ if (value.id !== undefined && (typeof value.id === "number" && !Number.isFinite(value.id)
1139
+ || typeof value.id !== "string" && typeof value.id !== "number" && value.id !== null)) {
1140
+ throw new Error(`Existing JSONL evidence line ${line} has an invalid protocol identity.`);
1141
+ }
1142
+ if (kind === "frame" && !Object.hasOwn(value, "payload")) {
1143
+ if (typeof value.parseError !== "string" || value.parseError.trim() === "") {
1144
+ throw new Error(`Existing JSONL evidence line ${line} is missing frame payload.`);
1145
+ }
1146
+ }
1147
+ else if (kind === "frame" && value.parseError !== undefined) {
1148
+ throw new Error(`Existing JSONL evidence line ${line} has contradictory frame payload evidence.`);
1149
+ }
1150
+ else if (kind !== "frame" && value.parseError !== undefined) {
1151
+ throw new Error(`Existing JSONL evidence line ${line} has an unexpected frame parse error.`);
1152
+ }
1153
+ if ((kind === "request" || kind === "response" || kind === "notification")
1154
+ && (typeof value.method !== "string" || value.method.trim() === "")) {
1155
+ throw new Error(`Existing JSONL evidence line ${line} is missing protocol method.`);
1156
+ }
1157
+ if (kind === "completion" && (!Object.hasOwn(value, "evidence") || typeof value.status !== "string" || value.status.trim() === "")) {
1158
+ throw new Error(`Existing JSONL evidence line ${line} is missing completion evidence.`);
1159
+ }
1160
+ if (kind === "capability" && (typeof value.capability !== "string" || value.capability.trim() === ""
1161
+ || !["available", "unsupported", "missing", "not-checked"].includes(String(value.status)))) {
1162
+ throw new Error(`Existing JSONL evidence line ${line} has invalid capability evidence.`);
1163
+ }
1164
+ if ((kind === "error" || kind === "process") && (typeof value.status !== "string" || value.status.trim() === "")) {
1165
+ throw new Error(`Existing JSONL evidence line ${line} is missing status evidence.`);
1166
+ }
1167
+ }
1168
+ function isRecord(value) {
1169
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1170
+ }
1171
+ function assertNonEmpty(value, label) {
1172
+ if (value.trim() === "")
1173
+ throw new Error(`${label} is required.`);
1174
+ }
1175
+ function assertTimestamp(value, label) {
1176
+ if (typeof value !== "string" || value.trim() === "")
1177
+ throw new Error(`${label} is invalid.`);
1178
+ }
1179
+ function positiveInteger(value, label) {
1180
+ if (!Number.isSafeInteger(value) || value < 1)
1181
+ throw new Error(`${label} must be a positive safe integer.`);
1182
+ return value;
1183
+ }
1184
+ function nonnegativeInteger(value, label) {
1185
+ if (!Number.isSafeInteger(value) || value < 0)
1186
+ throw new Error(`${label} must be a nonnegative safe integer.`);
1187
+ return value;
1188
+ }
1189
+ function errorMessage(error) {
1190
+ return error instanceof Error ? error.message : String(error);
1191
+ }
1192
+ function cloneJsonValue(value, label) {
1193
+ assertJsonValue(value, label, new Set());
1194
+ return structuredClone(value);
1195
+ }
1196
+ function findNonFiniteJsonNumber(value, path = "$", seen = new Set()) {
1197
+ if (typeof value === "number")
1198
+ return Number.isFinite(value) ? undefined : path;
1199
+ if (value === null || typeof value !== "object")
1200
+ return undefined;
1201
+ if (seen.has(value))
1202
+ return undefined;
1203
+ seen.add(value);
1204
+ if (Array.isArray(value)) {
1205
+ for (const [index, item] of value.entries()) {
1206
+ const result = findNonFiniteJsonNumber(item, `${path}[${index}]`, seen);
1207
+ if (result !== undefined)
1208
+ return result;
1209
+ }
1210
+ }
1211
+ else {
1212
+ for (const [key, item] of Object.entries(value)) {
1213
+ const result = findNonFiniteJsonNumber(item, `${path}.${key}`, seen);
1214
+ if (result !== undefined)
1215
+ return result;
1216
+ }
1217
+ }
1218
+ seen.delete(value);
1219
+ return undefined;
1220
+ }
1221
+ function assertJsonValue(value, path, seen) {
1222
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1223
+ return;
1224
+ if (typeof value === "number") {
1225
+ if (!Number.isFinite(value))
1226
+ throw new Error(`${path} must be a finite JSON number.`);
1227
+ return;
1228
+ }
1229
+ if (typeof value !== "object")
1230
+ throw new Error(`${path} contains a non-JSON value.`);
1231
+ if (seen.has(value))
1232
+ throw new Error(`${path} contains a cycle.`);
1233
+ seen.add(value);
1234
+ if (Array.isArray(value)) {
1235
+ value.forEach((item, index) => assertJsonValue(item, `${path}[${index}]`, seen));
1236
+ }
1237
+ else {
1238
+ const prototype = Object.getPrototypeOf(value);
1239
+ if (prototype !== Object.prototype && prototype !== null)
1240
+ throw new Error(`${path} must be a JSON object or array.`);
1241
+ const object = value;
1242
+ for (const key of Object.keys(object))
1243
+ assertJsonValue(object[key], `${path}.${key}`, seen);
1244
+ }
1245
+ seen.delete(value);
1246
+ }
1247
+ async function settleObserver(promise, milliseconds) {
1248
+ let timer;
1249
+ try {
1250
+ return await Promise.race([
1251
+ promise.then((value) => ({ status: "completed", value }), (error) => ({ status: "failed", error })),
1252
+ new Promise((resolvePromise) => {
1253
+ timer = setTimeout(() => resolvePromise({ status: "timed-out" }), milliseconds);
1254
+ }),
1255
+ ]);
1256
+ }
1257
+ finally {
1258
+ if (timer !== undefined)
1259
+ clearTimeout(timer);
1260
+ }
1261
+ }