@replayci/replay 0.1.2 → 0.1.3

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.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var CaptureBuffer = class {
22
22
  apiKey;
23
23
  endpoint;
24
24
  diagnostics;
25
+ onStateChange;
25
26
  fetchImpl;
26
27
  now;
27
28
  queue = [];
@@ -31,6 +32,11 @@ var CaptureBuffer = class {
31
32
  circuitOpenUntil = 0;
32
33
  remoteDisabled = false;
33
34
  closed = false;
35
+ droppedOverflowTotal = 0;
36
+ lastFlushAttemptMs = 0;
37
+ lastFlushSuccessMs = 0;
38
+ lastFlushErrorMs = 0;
39
+ lastFlushErrorMsg = null;
34
40
  constructor(opts) {
35
41
  this.apiKey = opts.apiKey;
36
42
  this.endpoint = normalizeEndpoint(opts.endpoint);
@@ -41,6 +47,7 @@ var CaptureBuffer = class {
41
47
  MAX_SEND_TIMEOUT
42
48
  );
43
49
  this.diagnostics = opts.diagnostics;
50
+ this.onStateChange = opts.onStateChange;
44
51
  this.fetchImpl = opts.fetchImpl ?? fetch;
45
52
  this.now = opts.now ?? Date.now;
46
53
  this.scheduleNextDrain();
@@ -57,6 +64,21 @@ var CaptureBuffer = class {
57
64
  get isRemoteDisabled() {
58
65
  return this.remoteDisabled;
59
66
  }
67
+ get droppedOverflow() {
68
+ return this.droppedOverflowTotal;
69
+ }
70
+ get lastFlushAttemptAt() {
71
+ return this.lastFlushAttemptMs;
72
+ }
73
+ get lastFlushSuccessAt() {
74
+ return this.lastFlushSuccessMs;
75
+ }
76
+ get lastFlushErrorAt() {
77
+ return this.lastFlushErrorMs;
78
+ }
79
+ get lastFlushError() {
80
+ return this.lastFlushErrorMsg;
81
+ }
60
82
  push(item) {
61
83
  if (this.closed || this.remoteDisabled) {
62
84
  return;
@@ -66,10 +88,12 @@ var CaptureBuffer = class {
66
88
  }
67
89
  if (this.queue.length >= this.maxBuffer) {
68
90
  this.queue.shift();
91
+ this.droppedOverflowTotal += 1;
69
92
  emitDiagnostics(this.diagnostics, {
70
93
  type: "buffer_overflow",
71
94
  dropped: 1
72
95
  });
96
+ emitStateChange(this.onStateChange, { type: "buffer_overflow", dropped: 1 });
73
97
  }
74
98
  this.queue.push(item);
75
99
  }
@@ -127,11 +151,13 @@ var CaptureBuffer = class {
127
151
  if (batch.length === 0) {
128
152
  return;
129
153
  }
154
+ this.lastFlushAttemptMs = this.now();
155
+ emitStateChange(this.onStateChange, { type: "flush_attempt" });
130
156
  let payload = "";
131
157
  try {
132
158
  payload = JSON.stringify({ captures: batch });
133
159
  } catch {
134
- this.handleFailure();
160
+ this.handleFailure("JSON serialization failed");
135
161
  return;
136
162
  }
137
163
  const controller = new AbortController();
@@ -153,22 +179,35 @@ var CaptureBuffer = class {
153
179
  this.clearTimer();
154
180
  this.failureCount = 0;
155
181
  this.circuitOpenUntil = Number.MAX_SAFE_INTEGER;
182
+ emitDiagnostics(this.diagnostics, { type: "remote_disabled" });
183
+ emitStateChange(this.onStateChange, { type: "remote_disabled" });
156
184
  return;
157
185
  }
158
186
  if (!response.ok) {
159
- this.handleFailure();
187
+ this.handleFailure(`HTTP ${response.status}`);
160
188
  return;
161
189
  }
162
190
  this.failureCount = 0;
163
191
  this.circuitOpenUntil = 0;
164
- } catch {
165
- this.handleFailure();
192
+ this.lastFlushSuccessMs = this.now();
193
+ this.lastFlushErrorMsg = null;
194
+ emitStateChange(this.onStateChange, { type: "flush_success", batch_size: batch.length });
195
+ } catch (err) {
196
+ this.handleFailure(err instanceof Error ? err.message : String(err));
166
197
  } finally {
167
198
  clearTimeout(timeout);
168
199
  }
169
200
  }
170
- handleFailure() {
201
+ handleFailure(errorMsg) {
171
202
  this.failureCount += 1;
203
+ const errorStr = errorMsg ?? "unknown error";
204
+ this.lastFlushErrorMs = this.now();
205
+ this.lastFlushErrorMsg = errorStr;
206
+ emitDiagnostics(this.diagnostics, {
207
+ type: "flush_error",
208
+ error: errorStr
209
+ });
210
+ emitStateChange(this.onStateChange, { type: "flush_error", error: errorStr });
172
211
  if (this.failureCount >= CIRCUIT_BREAKER_FAILURE_LIMIT) {
173
212
  this.circuitOpenUntil = this.now() + CIRCUIT_BREAKER_MS;
174
213
  emitDiagnostics(this.diagnostics, {
@@ -176,6 +215,12 @@ var CaptureBuffer = class {
176
215
  failures: this.failureCount,
177
216
  backoffMs: CIRCUIT_BREAKER_MS
178
217
  });
218
+ emitStateChange(this.onStateChange, {
219
+ type: "circuit_open",
220
+ failures: this.failureCount,
221
+ backoffMs: CIRCUIT_BREAKER_MS,
222
+ openUntil: this.circuitOpenUntil
223
+ });
179
224
  try {
180
225
  console.warn(
181
226
  `[replayci] Capture buffer circuit breaker open after ${this.failureCount} consecutive failures. Captures will be dropped for ${CIRCUIT_BREAKER_MS / 6e4} minutes.`
@@ -255,6 +300,12 @@ function emitDiagnostics(diagnostics, event) {
255
300
  } catch {
256
301
  }
257
302
  }
303
+ function emitStateChange(listener, event) {
304
+ try {
305
+ listener?.(event);
306
+ } catch {
307
+ }
308
+ }
258
309
  function isRemoteDisable(response) {
259
310
  return response.headers.get("x-replayci-disable")?.toLowerCase() === "true";
260
311
  }
@@ -905,7 +956,8 @@ function parseNodeMajorVersion(nodeVersion) {
905
956
 
906
957
  // src/captureSchema.ts
907
958
  var CAPTURE_SCHEMA_VERSION_LEGACY = "2026-03-04";
908
- var CAPTURE_SCHEMA_VERSION_CURRENT = "2026-03-06";
959
+ var CAPTURE_SCHEMA_VERSION_V2 = "2026-03-06";
960
+ var CAPTURE_SCHEMA_VERSION_CURRENT = "2026-03-09";
909
961
  function isRecord(value) {
910
962
  return value !== null && typeof value === "object" && !Array.isArray(value);
911
963
  }
@@ -1069,9 +1121,19 @@ function validateUsage(value, path) {
1069
1121
  total_tokens: requireNonNegativeInt(usage.total_tokens, `${path}.total_tokens`)
1070
1122
  };
1071
1123
  }
1124
+ function optionalString(value, path) {
1125
+ if (value === void 0 || value === null) {
1126
+ return void 0;
1127
+ }
1128
+ if (typeof value !== "string") {
1129
+ throw new Error(`${path} must be a string when provided`);
1130
+ }
1131
+ return value;
1132
+ }
1072
1133
  function parseCommonCapture(capture, index, modelId, schemaVersion) {
1073
1134
  const toolNames = requireStringArray(capture.tool_names, `captures[${index}].tool_names`);
1074
1135
  const primaryToolName = capture.primary_tool_name === void 0 ? toolNames[0] ?? null : nullableString(capture.primary_tool_name, `captures[${index}].primary_tool_name`);
1136
+ const sdkSessionId = optionalString(capture.sdk_session_id, `captures[${index}].sdk_session_id`);
1075
1137
  return {
1076
1138
  schema_version: schemaVersion,
1077
1139
  agent: requireString(capture.agent, `captures[${index}].agent`),
@@ -1084,7 +1146,8 @@ function parseCommonCapture(capture, index, modelId, schemaVersion) {
1084
1146
  response: validateResponse(capture.response, `captures[${index}].response`),
1085
1147
  ...capture.validation !== void 0 ? { validation: validateValidation(capture.validation, `captures[${index}].validation`) } : {},
1086
1148
  ...capture.usage !== void 0 ? { usage: validateUsage(capture.usage, `captures[${index}].usage`) } : {},
1087
- latency_ms: requireNonNegativeInt(capture.latency_ms, `captures[${index}].latency_ms`)
1149
+ latency_ms: requireNonNegativeInt(capture.latency_ms, `captures[${index}].latency_ms`),
1150
+ ...sdkSessionId !== void 0 ? { sdk_session_id: sdkSessionId } : {}
1088
1151
  };
1089
1152
  }
1090
1153
  function parseLegacyCapturedCall(capture, index) {
@@ -1095,6 +1158,14 @@ function parseLegacyCapturedCall(capture, index) {
1095
1158
  CAPTURE_SCHEMA_VERSION_LEGACY
1096
1159
  );
1097
1160
  }
1161
+ function parseV2CapturedCall(capture, index) {
1162
+ return parseCommonCapture(
1163
+ capture,
1164
+ index,
1165
+ requireString(capture.model_id, `captures[${index}].model_id`),
1166
+ CAPTURE_SCHEMA_VERSION_V2
1167
+ );
1168
+ }
1098
1169
  function parseCurrentCapturedCall(capture, index) {
1099
1170
  return parseCommonCapture(
1100
1171
  capture,
@@ -1105,25 +1176,264 @@ function parseCurrentCapturedCall(capture, index) {
1105
1176
  }
1106
1177
  var CAPTURE_SCHEMA_PARSERS = {
1107
1178
  [CAPTURE_SCHEMA_VERSION_LEGACY]: parseLegacyCapturedCall,
1179
+ [CAPTURE_SCHEMA_VERSION_V2]: parseV2CapturedCall,
1108
1180
  [CAPTURE_SCHEMA_VERSION_CURRENT]: parseCurrentCapturedCall
1109
1181
  };
1110
1182
 
1183
+ // src/healthStore.ts
1184
+ import { randomBytes } from "crypto";
1185
+ import {
1186
+ closeSync,
1187
+ existsSync,
1188
+ fsyncSync,
1189
+ mkdirSync,
1190
+ openSync,
1191
+ readFileSync,
1192
+ readdirSync,
1193
+ renameSync,
1194
+ unlinkSync,
1195
+ writeFileSync
1196
+ } from "fs";
1197
+ import { readFile } from "fs/promises";
1198
+ import { join } from "path";
1199
+ var SESSIONS_DIR = "observe-sessions";
1200
+ var SCHEMA_VERSION = "1.0";
1201
+ var JANITOR_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1202
+ function generateSessionId() {
1203
+ return `obs_${randomBytes(12).toString("hex")}`;
1204
+ }
1205
+ function resolveStateDir(opts) {
1206
+ let explicit;
1207
+ let workDir;
1208
+ if (typeof opts === "string") {
1209
+ workDir = opts;
1210
+ } else if (opts != null) {
1211
+ explicit = opts.stateDir;
1212
+ workDir = opts.cwd;
1213
+ }
1214
+ if (typeof explicit === "string" && explicit.length > 0) {
1215
+ return explicit;
1216
+ }
1217
+ const envValue = typeof process !== "undefined" ? process.env.REPLAYCI_STATE_DIR : void 0;
1218
+ if (typeof envValue === "string" && envValue.length > 0) {
1219
+ return envValue;
1220
+ }
1221
+ const base = workDir ?? process.cwd();
1222
+ return join(base, ".replayci", "runtime");
1223
+ }
1224
+ function resolveSessionsDir(stateDir) {
1225
+ return join(stateDir, SESSIONS_DIR);
1226
+ }
1227
+ function sessionFilePath(sessionsDir, sessionId) {
1228
+ return join(sessionsDir, `${sessionId}.json`);
1229
+ }
1230
+ function writeHealthSnapshot(sessionsDir, sessionId, snapshot) {
1231
+ ensureDir(sessionsDir);
1232
+ const destPath = sessionFilePath(sessionsDir, sessionId);
1233
+ const tmpPath = join(sessionsDir, `.tmp_${sessionId}_${randomBytes(4).toString("hex")}.json`);
1234
+ const content = JSON.stringify(snapshot, null, 2);
1235
+ const fd = openSync(tmpPath, "w", 384);
1236
+ try {
1237
+ writeFileSync(fd, content);
1238
+ fsyncSync(fd);
1239
+ } finally {
1240
+ closeSync(fd);
1241
+ }
1242
+ renameSync(tmpPath, destPath);
1243
+ }
1244
+ function parseHealthFile(raw) {
1245
+ let parsed;
1246
+ try {
1247
+ parsed = JSON.parse(raw);
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ if (!isValidHealthSnapshot(parsed)) {
1252
+ return null;
1253
+ }
1254
+ return parsed;
1255
+ }
1256
+ function isValidHealthSnapshot(value) {
1257
+ if (value === null || typeof value !== "object") {
1258
+ return false;
1259
+ }
1260
+ const obj = value;
1261
+ if (obj.schema_version !== SCHEMA_VERSION) {
1262
+ return false;
1263
+ }
1264
+ if (typeof obj.session_id !== "string" || obj.session_id.length === 0) {
1265
+ return false;
1266
+ }
1267
+ if (typeof obj.agent !== "string") {
1268
+ return false;
1269
+ }
1270
+ const validProviders = ["openai", "anthropic", null];
1271
+ if (!validProviders.includes(obj.provider)) {
1272
+ return false;
1273
+ }
1274
+ const validStates = ["active", "inactive", "stopped", "stale"];
1275
+ if (typeof obj.state !== "string" || !validStates.includes(obj.state)) {
1276
+ return false;
1277
+ }
1278
+ if (!isValidActivation(obj.activation)) {
1279
+ return false;
1280
+ }
1281
+ if (!isValidProcess(obj.process)) {
1282
+ return false;
1283
+ }
1284
+ if (!isValidRuntime(obj.runtime)) {
1285
+ return false;
1286
+ }
1287
+ if (typeof obj.last_heartbeat_at !== "string") {
1288
+ return false;
1289
+ }
1290
+ return true;
1291
+ }
1292
+ function isValidActivation(value) {
1293
+ if (value === null || typeof value !== "object") {
1294
+ return false;
1295
+ }
1296
+ const act = value;
1297
+ if (typeof act.active !== "boolean") {
1298
+ return false;
1299
+ }
1300
+ const validReasons = [
1301
+ "active",
1302
+ "disabled",
1303
+ "missing_api_key",
1304
+ "unsupported_client",
1305
+ "double_wrap",
1306
+ "patch_target_unwritable",
1307
+ "internal_error"
1308
+ ];
1309
+ if (typeof act.reason_code !== "string" || !validReasons.includes(act.reason_code)) {
1310
+ return false;
1311
+ }
1312
+ if (typeof act.activated_at !== "string") {
1313
+ return false;
1314
+ }
1315
+ return true;
1316
+ }
1317
+ function isValidProcess(value) {
1318
+ if (value === null || typeof value !== "object") {
1319
+ return false;
1320
+ }
1321
+ const proc = value;
1322
+ if (typeof proc.cwd !== "string") {
1323
+ return false;
1324
+ }
1325
+ if (typeof proc.node_version !== "string") {
1326
+ return false;
1327
+ }
1328
+ if (typeof proc.sdk_version !== "string") {
1329
+ return false;
1330
+ }
1331
+ return true;
1332
+ }
1333
+ function isValidRuntime(value) {
1334
+ if (value === null || typeof value !== "object") {
1335
+ return false;
1336
+ }
1337
+ const rt = value;
1338
+ if (typeof rt.captures_seen !== "number") {
1339
+ return false;
1340
+ }
1341
+ if (typeof rt.dropped_overflow !== "number") {
1342
+ return false;
1343
+ }
1344
+ if (typeof rt.queue_size !== "number") {
1345
+ return false;
1346
+ }
1347
+ if (typeof rt.consecutive_failures !== "number") {
1348
+ return false;
1349
+ }
1350
+ if (typeof rt.remote_disabled !== "boolean") {
1351
+ return false;
1352
+ }
1353
+ return true;
1354
+ }
1355
+ function runJanitor(sessionsDir, currentSessionId, now) {
1356
+ try {
1357
+ if (!existsSync(sessionsDir)) {
1358
+ return;
1359
+ }
1360
+ const nowMs = now ?? Date.now();
1361
+ const entries = readdirSync(sessionsDir);
1362
+ for (const entry of entries) {
1363
+ if (!entry.endsWith(".json") || entry.startsWith(".tmp_")) {
1364
+ continue;
1365
+ }
1366
+ const sessionId = entry.replace(/\.json$/, "");
1367
+ if (sessionId === currentSessionId) {
1368
+ continue;
1369
+ }
1370
+ const filePath = join(sessionsDir, entry);
1371
+ let snapshot = null;
1372
+ try {
1373
+ const raw = readFileSync(filePath, "utf-8");
1374
+ snapshot = parseHealthFile(raw);
1375
+ } catch {
1376
+ continue;
1377
+ }
1378
+ if (!snapshot) {
1379
+ continue;
1380
+ }
1381
+ const isTerminal = snapshot.state === "stopped" || snapshot.state === "stale";
1382
+ if (!isTerminal) {
1383
+ continue;
1384
+ }
1385
+ const heartbeatAge = nowMs - new Date(snapshot.last_heartbeat_at).getTime();
1386
+ if (heartbeatAge < JANITOR_TTL_MS) {
1387
+ continue;
1388
+ }
1389
+ try {
1390
+ unlinkSync(filePath);
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ } catch {
1395
+ }
1396
+ }
1397
+ function getSdkVersion() {
1398
+ try {
1399
+ const url = new URL("../package.json", import.meta.url);
1400
+ const raw = readFileSync(url, "utf-8");
1401
+ const pkg = JSON.parse(raw);
1402
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
1403
+ } catch {
1404
+ return "0.0.0";
1405
+ }
1406
+ }
1407
+ function ensureDir(dir) {
1408
+ try {
1409
+ mkdirSync(dir, { recursive: true });
1410
+ } catch (err) {
1411
+ if (err.code !== "EEXIST") {
1412
+ throw err;
1413
+ }
1414
+ }
1415
+ }
1416
+
1111
1417
  // src/observe.ts
1112
1418
  var REPLAY_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
1113
1419
  var DEFAULT_AGENT = "default";
1420
+ var IDLE_HEARTBEAT_MS = 3e4;
1114
1421
  function observe(client, opts = {}) {
1115
1422
  assertSupportedNodeRuntime();
1423
+ const sessionId = generateSessionId();
1424
+ const agent = typeof opts.agent === "string" && opts.agent.length > 0 ? opts.agent : DEFAULT_AGENT;
1425
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1116
1426
  try {
1117
1427
  if (isDisabled(opts)) {
1118
- return createNoopHandle(client);
1428
+ return createInactiveHandle(client, sessionId, agent, "disabled", void 0, now, opts.diagnostics, opts.stateDir);
1119
1429
  }
1120
1430
  const apiKey = resolveApiKey(opts);
1121
1431
  if (!apiKey) {
1122
- return createNoopHandle(client);
1432
+ return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, opts.diagnostics, opts.stateDir);
1123
1433
  }
1124
1434
  const provider = detectProviderSafely(client, opts.diagnostics);
1125
1435
  if (!provider) {
1126
- return createNoopHandle(client);
1436
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, opts.diagnostics, opts.stateDir);
1127
1437
  }
1128
1438
  const patchTarget = resolvePatchTarget(client, provider);
1129
1439
  if (!patchTarget) {
@@ -1132,14 +1442,14 @@ function observe(client, opts = {}) {
1132
1442
  mode: "observe",
1133
1443
  detail: `Unsupported ${provider} client shape.`
1134
1444
  });
1135
- return createNoopHandle(client);
1445
+ return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, opts.diagnostics, opts.stateDir);
1136
1446
  }
1137
1447
  if (isWrapped(client, patchTarget.target)) {
1138
1448
  emitDiagnostic(opts.diagnostics, {
1139
1449
  type: "double_wrap",
1140
1450
  mode: "observe"
1141
1451
  });
1142
- return createNoopHandle(client);
1452
+ return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, opts.diagnostics, opts.stateDir);
1143
1453
  }
1144
1454
  const patchabilityError = getPatchabilityError(patchTarget.target, patchTarget.methodName);
1145
1455
  if (patchabilityError) {
@@ -1148,19 +1458,171 @@ function observe(client, opts = {}) {
1148
1458
  mode: "observe",
1149
1459
  detail: patchabilityError
1150
1460
  });
1151
- return createNoopHandle(client);
1461
+ return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, opts.diagnostics, opts.stateDir);
1152
1462
  }
1153
1463
  const captureLevel = normalizeCaptureLevel(opts.captureLevel);
1154
- const agent = typeof opts.agent === "string" && opts.agent.length > 0 ? opts.agent : DEFAULT_AGENT;
1464
+ const patchTargetName = `${provider}.${provider === "openai" ? "chat.completions.create" : "messages.create"}`;
1465
+ const runtimeState = {
1466
+ captures_seen: 0,
1467
+ dropped_overflow: 0,
1468
+ last_capture_at: null,
1469
+ last_flush_attempt_at: null,
1470
+ last_flush_success_at: null,
1471
+ last_flush_error_at: null,
1472
+ last_flush_error: null,
1473
+ queue_size: 0,
1474
+ consecutive_failures: 0,
1475
+ circuit_open_until: null,
1476
+ remote_disabled: false
1477
+ };
1478
+ let lastHealthStoreErrorAt = null;
1479
+ let lastHealthStoreError = null;
1480
+ const sessionsDir = resolveSessionsDir(
1481
+ resolveStateDir(opts.stateDir ? { stateDir: opts.stateDir } : void 0)
1482
+ );
1483
+ const buildCurrentSnapshot = () => ({
1484
+ schema_version: "1.0",
1485
+ session_id: sessionId,
1486
+ agent,
1487
+ provider,
1488
+ patch_target: patchTargetName,
1489
+ state: "active",
1490
+ activation: {
1491
+ active: true,
1492
+ reason_code: "active",
1493
+ activated_at: now
1494
+ },
1495
+ process: {
1496
+ pid: typeof process !== "undefined" ? process.pid : null,
1497
+ cwd: typeof process !== "undefined" ? process.cwd() : "",
1498
+ node_version: typeof process !== "undefined" ? process.versions?.node ?? "" : "",
1499
+ sdk_version: getSdkVersion()
1500
+ },
1501
+ runtime: { ...runtimeState },
1502
+ stopped_at: null,
1503
+ last_heartbeat_at: (/* @__PURE__ */ new Date()).toISOString(),
1504
+ last_health_store_error_at: lastHealthStoreErrorAt,
1505
+ last_health_store_error: lastHealthStoreError
1506
+ });
1507
+ const persistHealth = (snapshot) => {
1508
+ const s = snapshot ?? buildCurrentSnapshot();
1509
+ try {
1510
+ writeHealthSnapshot(sessionsDir, sessionId, s);
1511
+ if (lastHealthStoreErrorAt !== null) {
1512
+ lastHealthStoreErrorAt = null;
1513
+ lastHealthStoreError = null;
1514
+ }
1515
+ } catch (err) {
1516
+ const detail = err instanceof Error ? err.message : "Failed to write health snapshot";
1517
+ lastHealthStoreErrorAt = (/* @__PURE__ */ new Date()).toISOString();
1518
+ lastHealthStoreError = detail;
1519
+ emitDiagnostic(opts.diagnostics, {
1520
+ type: "health_store_error",
1521
+ detail
1522
+ });
1523
+ }
1524
+ };
1525
+ let lastEventWriteAt = Date.now();
1526
+ let heartbeatTimer;
1527
+ const persistHealthEvent = () => {
1528
+ lastEventWriteAt = Date.now();
1529
+ persistHealth();
1530
+ };
1531
+ const startHeartbeat = () => {
1532
+ heartbeatTimer = setInterval(() => {
1533
+ const sinceLast = Date.now() - lastEventWriteAt;
1534
+ if (sinceLast >= IDLE_HEARTBEAT_MS) {
1535
+ persistHealth();
1536
+ }
1537
+ }, IDLE_HEARTBEAT_MS);
1538
+ unrefTimerHandle(heartbeatTimer);
1539
+ };
1540
+ const stopHeartbeat = () => {
1541
+ if (heartbeatTimer !== void 0) {
1542
+ clearInterval(heartbeatTimer);
1543
+ heartbeatTimer = void 0;
1544
+ }
1545
+ };
1546
+ const onBufferStateChange = (event) => {
1547
+ if (restored) return;
1548
+ switch (event.type) {
1549
+ case "flush_attempt":
1550
+ runtimeState.last_flush_attempt_at = (/* @__PURE__ */ new Date()).toISOString();
1551
+ persistHealthEvent();
1552
+ break;
1553
+ case "flush_success":
1554
+ runtimeState.last_flush_success_at = (/* @__PURE__ */ new Date()).toISOString();
1555
+ runtimeState.last_flush_error = null;
1556
+ runtimeState.consecutive_failures = 0;
1557
+ runtimeState.circuit_open_until = null;
1558
+ runtimeState.queue_size = buffer.size;
1559
+ emitDiagnostic(opts.diagnostics, {
1560
+ type: "flush_succeeded",
1561
+ batch_size: event.batch_size
1562
+ });
1563
+ persistHealthEvent();
1564
+ break;
1565
+ case "flush_error":
1566
+ runtimeState.last_flush_error_at = (/* @__PURE__ */ new Date()).toISOString();
1567
+ runtimeState.last_flush_error = event.error;
1568
+ runtimeState.consecutive_failures = buffer.consecutiveFailures;
1569
+ runtimeState.queue_size = buffer.size;
1570
+ persistHealthEvent();
1571
+ break;
1572
+ case "buffer_overflow":
1573
+ runtimeState.dropped_overflow = buffer.droppedOverflow;
1574
+ runtimeState.queue_size = buffer.size;
1575
+ persistHealthEvent();
1576
+ break;
1577
+ case "circuit_open":
1578
+ runtimeState.circuit_open_until = new Date(event.openUntil).toISOString();
1579
+ runtimeState.consecutive_failures = event.failures;
1580
+ persistHealthEvent();
1581
+ break;
1582
+ case "remote_disabled":
1583
+ runtimeState.remote_disabled = true;
1584
+ runtimeState.queue_size = 0;
1585
+ persistHealthEvent();
1586
+ break;
1587
+ }
1588
+ };
1155
1589
  const buffer = new CaptureBuffer({
1156
1590
  apiKey,
1157
1591
  endpoint: opts.endpoint,
1158
1592
  maxBuffer: opts.maxBuffer,
1159
1593
  flushMs: opts.flushMs,
1160
1594
  timeoutMs: opts.timeoutMs,
1161
- diagnostics: opts.diagnostics
1595
+ diagnostics: opts.diagnostics,
1596
+ onStateChange: onBufferStateChange
1162
1597
  });
1163
1598
  registerBeforeExit(buffer);
1599
+ let exitSnapshotWritten = false;
1600
+ const beforeExitHandler = () => {
1601
+ if (exitSnapshotWritten || restored) return;
1602
+ exitSnapshotWritten = true;
1603
+ try {
1604
+ stopHeartbeat();
1605
+ const stoppedSnapshot = buildCurrentSnapshot();
1606
+ stoppedSnapshot.state = "stopped";
1607
+ stoppedSnapshot.stopped_at = (/* @__PURE__ */ new Date()).toISOString();
1608
+ stoppedSnapshot.last_heartbeat_at = (/* @__PURE__ */ new Date()).toISOString();
1609
+ persistHealth(stoppedSnapshot);
1610
+ } catch {
1611
+ }
1612
+ };
1613
+ if (typeof process !== "undefined" && typeof process.on === "function") {
1614
+ process.on("beforeExit", beforeExitHandler);
1615
+ }
1616
+ persistHealthEvent();
1617
+ safeRunJanitor(sessionsDir, sessionId);
1618
+ startHeartbeat();
1619
+ emitDiagnostic(opts.diagnostics, {
1620
+ type: "observe_activated",
1621
+ session_id: sessionId,
1622
+ provider,
1623
+ agent,
1624
+ patch_target: patchTargetName
1625
+ });
1164
1626
  const wrappedCreate = function observeWrappedCreate(...args) {
1165
1627
  const requestSnapshot = snapshotRequest(args[0], captureLevel);
1166
1628
  const startedAt = Date.now();
@@ -1173,7 +1635,11 @@ function observe(client, opts = {}) {
1173
1635
  provider,
1174
1636
  requestSnapshot,
1175
1637
  response,
1176
- startedAt
1638
+ startedAt,
1639
+ sessionId,
1640
+ runtimeState,
1641
+ persistHealthEvent,
1642
+ diagnostics: opts.diagnostics
1177
1643
  });
1178
1644
  return response;
1179
1645
  });
@@ -1183,11 +1649,18 @@ function observe(client, opts = {}) {
1183
1649
  let restored = false;
1184
1650
  return {
1185
1651
  client,
1652
+ flush() {
1653
+ return buffer.flush();
1654
+ },
1186
1655
  restore() {
1187
1656
  if (restored) {
1188
1657
  return;
1189
1658
  }
1190
1659
  restored = true;
1660
+ stopHeartbeat();
1661
+ if (typeof process !== "undefined" && typeof process.removeListener === "function") {
1662
+ process.removeListener("beforeExit", beforeExitHandler);
1663
+ }
1191
1664
  if (patchTarget.target[patchTarget.methodName] === wrappedCreate) {
1192
1665
  if (patchTarget.hadOwnMethod) {
1193
1666
  patchTarget.target[patchTarget.methodName] = patchTarget.originalCreate;
@@ -1196,11 +1669,124 @@ function observe(client, opts = {}) {
1196
1669
  }
1197
1670
  }
1198
1671
  clearWrapped(client, patchTarget.target);
1672
+ const stoppedSnapshot = buildCurrentSnapshot();
1673
+ stoppedSnapshot.state = "stopped";
1674
+ stoppedSnapshot.stopped_at = (/* @__PURE__ */ new Date()).toISOString();
1675
+ stoppedSnapshot.last_heartbeat_at = (/* @__PURE__ */ new Date()).toISOString();
1676
+ persistHealth(stoppedSnapshot);
1199
1677
  buffer.close();
1678
+ },
1679
+ getHealth() {
1680
+ if (restored) {
1681
+ const snapshot = buildCurrentSnapshot();
1682
+ snapshot.state = "stopped";
1683
+ snapshot.stopped_at = snapshot.stopped_at ?? (/* @__PURE__ */ new Date()).toISOString();
1684
+ return snapshot;
1685
+ }
1686
+ runtimeState.queue_size = buffer.size;
1687
+ return buildCurrentSnapshot();
1200
1688
  }
1201
1689
  };
1690
+ } catch (err) {
1691
+ const detail = err instanceof Error ? err.message : "Unknown internal error";
1692
+ return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics, opts.stateDir);
1693
+ }
1694
+ }
1695
+ function createInactiveHandle(client, sessionId, agent, reasonCode, detail, activatedAt, diagnostics, stateDir) {
1696
+ const healthSnapshot = buildHealthSnapshot({
1697
+ sessionId,
1698
+ agent,
1699
+ provider: null,
1700
+ patchTarget: null,
1701
+ state: "inactive",
1702
+ reasonCode,
1703
+ active: false,
1704
+ activatedAt,
1705
+ detail
1706
+ });
1707
+ const sessionsDir = resolveSessionsDir(
1708
+ resolveStateDir(stateDir ? { stateDir } : void 0)
1709
+ );
1710
+ safeWriteHealth(sessionsDir, sessionId, healthSnapshot, diagnostics);
1711
+ emitDiagnostic(diagnostics, {
1712
+ type: "observe_inactive",
1713
+ reason_code: reasonCode,
1714
+ ...detail ? { detail } : {}
1715
+ });
1716
+ return {
1717
+ client,
1718
+ flush() {
1719
+ return Promise.resolve();
1720
+ },
1721
+ restore() {
1722
+ },
1723
+ getHealth() {
1724
+ return { ...healthSnapshot };
1725
+ }
1726
+ };
1727
+ }
1728
+ function unrefTimerHandle(timer) {
1729
+ if (typeof timer === "object" && timer !== null && "unref" in timer) {
1730
+ try {
1731
+ timer.unref?.call(timer);
1732
+ } catch {
1733
+ }
1734
+ }
1735
+ }
1736
+ function buildHealthSnapshot(input) {
1737
+ const snapshot = {
1738
+ schema_version: "1.0",
1739
+ session_id: input.sessionId,
1740
+ agent: input.agent,
1741
+ provider: input.provider,
1742
+ patch_target: input.patchTarget,
1743
+ state: input.state,
1744
+ activation: {
1745
+ active: input.active,
1746
+ reason_code: input.reasonCode,
1747
+ activated_at: input.activatedAt,
1748
+ ...input.detail ? { detail: input.detail } : {}
1749
+ },
1750
+ process: {
1751
+ pid: typeof process !== "undefined" ? process.pid : null,
1752
+ cwd: typeof process !== "undefined" ? process.cwd() : "",
1753
+ node_version: typeof process !== "undefined" ? process.versions?.node ?? "" : "",
1754
+ sdk_version: getSdkVersion()
1755
+ },
1756
+ runtime: {
1757
+ captures_seen: 0,
1758
+ dropped_overflow: 0,
1759
+ last_capture_at: null,
1760
+ last_flush_attempt_at: null,
1761
+ last_flush_success_at: null,
1762
+ last_flush_error_at: null,
1763
+ last_flush_error: null,
1764
+ queue_size: 0,
1765
+ consecutive_failures: 0,
1766
+ circuit_open_until: null,
1767
+ remote_disabled: false
1768
+ },
1769
+ stopped_at: null,
1770
+ last_heartbeat_at: input.activatedAt,
1771
+ last_health_store_error_at: null,
1772
+ last_health_store_error: null
1773
+ };
1774
+ return snapshot;
1775
+ }
1776
+ function safeWriteHealth(sessionsDir, sessionId, snapshot, diagnostics) {
1777
+ try {
1778
+ writeHealthSnapshot(sessionsDir, sessionId, snapshot);
1779
+ } catch (err) {
1780
+ emitDiagnostic(diagnostics, {
1781
+ type: "health_store_error",
1782
+ detail: err instanceof Error ? err.message : "Failed to write health snapshot"
1783
+ });
1784
+ }
1785
+ }
1786
+ function safeRunJanitor(sessionsDir, currentSessionId) {
1787
+ try {
1788
+ runJanitor(sessionsDir, currentSessionId);
1202
1789
  } catch {
1203
- return createNoopHandle(client);
1204
1790
  }
1205
1791
  }
1206
1792
  function safelyCaptureResponse(input) {
@@ -1230,10 +1816,19 @@ function safelyCaptureResponse(input) {
1230
1816
  usage: extractUsage(input.response, input.provider)
1231
1817
  },
1232
1818
  endedAt: Date.now(),
1233
- startedAt: input.startedAt
1819
+ startedAt: input.startedAt,
1820
+ sessionId: input.sessionId
1234
1821
  });
1235
1822
  if (capture) {
1236
1823
  input.buffer.push(capture);
1824
+ input.runtimeState.captures_seen += 1;
1825
+ input.runtimeState.last_capture_at = (/* @__PURE__ */ new Date()).toISOString();
1826
+ input.runtimeState.queue_size = input.buffer.size;
1827
+ emitDiagnostic(input.diagnostics, {
1828
+ type: "capture_seen",
1829
+ session_id: input.sessionId
1830
+ });
1831
+ input.persistHealthEvent();
1237
1832
  }
1238
1833
  } catch {
1239
1834
  }
@@ -1250,10 +1845,19 @@ function safelyPushStreamCapture(input) {
1250
1845
  requestSnapshot: input.requestSnapshot,
1251
1846
  responseData: input.summary,
1252
1847
  startedAt: input.startedAt,
1253
- endedAt: Date.now()
1848
+ endedAt: Date.now(),
1849
+ sessionId: input.sessionId
1254
1850
  });
1255
1851
  if (capture) {
1256
1852
  input.buffer.push(capture);
1853
+ input.runtimeState.captures_seen += 1;
1854
+ input.runtimeState.last_capture_at = (/* @__PURE__ */ new Date()).toISOString();
1855
+ input.runtimeState.queue_size = input.buffer.size;
1856
+ emitDiagnostic(input.diagnostics, {
1857
+ type: "capture_seen",
1858
+ session_id: input.sessionId
1859
+ });
1860
+ input.persistHealthEvent();
1257
1861
  }
1258
1862
  } catch {
1259
1863
  }
@@ -1279,7 +1883,8 @@ function buildCapturedCall(input) {
1279
1883
  ...input.captureLevel === "full" && input.responseData.textBlocks && input.responseData.textBlocks.length > 0 ? { text_blocks: input.responseData.textBlocks } : {}
1280
1884
  },
1281
1885
  ...input.responseData.usage ? { usage: input.responseData.usage } : {},
1282
- latency_ms: Math.max(0, input.endedAt - input.startedAt)
1886
+ latency_ms: Math.max(0, input.endedAt - input.startedAt),
1887
+ ...input.sessionId ? { sdk_session_id: input.sessionId } : {}
1283
1888
  };
1284
1889
  } catch {
1285
1890
  return null;
@@ -1458,13 +2063,6 @@ function clearWrapped(client, target) {
1458
2063
  } catch {
1459
2064
  }
1460
2065
  }
1461
- function createNoopHandle(client) {
1462
- return {
1463
- client,
1464
- restore() {
1465
- }
1466
- };
1467
- }
1468
2066
  function resolveApiKey(opts) {
1469
2067
  return typeof opts.apiKey === "string" && opts.apiKey.length > 0 ? opts.apiKey : toNonEmptyString(process.env.REPLAYCI_API_KEY);
1470
2068
  }
@@ -1517,12 +2115,12 @@ import {
1517
2115
  normalizeToolArray as normalizeToolArray2
1518
2116
  } from "@replayci/contracts-core";
1519
2117
  import {
1520
- readdirSync,
2118
+ readdirSync as readdirSync2,
1521
2119
  statSync
1522
2120
  } from "fs";
1523
2121
  import {
1524
2122
  extname,
1525
- join,
2123
+ join as join2,
1526
2124
  relative,
1527
2125
  resolve
1528
2126
  } from "path";
@@ -1613,8 +2211,8 @@ function collectContractFiles(inputPath) {
1613
2211
  if (!stat.isDirectory()) {
1614
2212
  return [];
1615
2213
  }
1616
- return readdirSync(inputPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
1617
- const fullPath = join(inputPath, entry.name);
2214
+ return readdirSync2(inputPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
2215
+ const fullPath = join2(inputPath, entry.name);
1618
2216
  if (entry.isDirectory()) {
1619
2217
  return collectContractFiles(fullPath);
1620
2218
  }