@ash-cloud/ash-ai 0.1.16 → 0.1.18

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.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var zod = require('zod');
4
4
  var child_process = require('child_process');
5
+ var events = require('events');
5
6
  var nanoid = require('nanoid');
6
7
  var fs = require('fs/promises');
7
8
  var path4 = require('path');
@@ -107,7 +108,7 @@ var init_normalized = __esm({
107
108
  "src/types/normalized.ts"() {
108
109
  }
109
110
  });
110
- exports.SessionStatus = void 0; exports.AgentStatus = void 0; exports.MessageRole = void 0; exports.messageContentSchema = void 0; exports.messageSchema = void 0; exports.sessionSchema = void 0; exports.attachmentSchema = void 0; exports.DEFAULT_SANDBOX_PROVIDER_CONFIG = void 0; exports.StreamEventType = void 0; exports.QueueItemStatus = void 0; exports.EventCategory = void 0;
111
+ exports.SessionStatus = void 0; exports.AgentStatus = void 0; exports.MessageRole = void 0; exports.messageContentSchema = void 0; exports.messageSchema = void 0; exports.sessionSchema = void 0; exports.attachmentSchema = void 0; exports.DEFAULT_SANDBOX_PROVIDER_CONFIG = void 0; exports.StreamEventType = void 0; exports.QueueItemStatus = void 0; exports.EventCategory = void 0; exports.EventSource = void 0;
111
112
  var init_types = __esm({
112
113
  "src/types/index.ts"() {
113
114
  init_normalized();
@@ -232,6 +233,14 @@ var init_types = __esm({
232
233
  WEBHOOK: "webhook"
233
234
  // webhook_delivery, webhook_failure (outbound webhook events)
234
235
  };
236
+ exports.EventSource = {
237
+ AGENT: "agent",
238
+ // Directly from the agent SDK
239
+ SYSTEM: "system",
240
+ // Generated by execution layer (lifecycle, user input)
241
+ DERIVED: "derived"
242
+ // Transformed/aggregated from agent events
243
+ };
235
244
  }
236
245
  });
237
246
 
@@ -384,6 +393,85 @@ var init_errors = __esm({
384
393
  }
385
394
  });
386
395
 
396
+ // src/relay/stream-event-writer.ts
397
+ exports.StreamEventWriter = void 0;
398
+ var init_stream_event_writer = __esm({
399
+ "src/relay/stream-event-writer.ts"() {
400
+ exports.StreamEventWriter = class {
401
+ storage;
402
+ sessionId;
403
+ emitter;
404
+ flushIntervalMs;
405
+ maxBatchChars;
406
+ // Text delta batching state
407
+ pendingDelta = "";
408
+ pendingDeltaCount = 0;
409
+ flushTimer = null;
410
+ closed = false;
411
+ constructor(options) {
412
+ this.storage = options.storage;
413
+ this.sessionId = options.sessionId;
414
+ this.emitter = options.emitter;
415
+ this.flushIntervalMs = options.flushIntervalMs ?? 100;
416
+ this.maxBatchChars = options.maxBatchChars ?? 500;
417
+ }
418
+ /**
419
+ * Write a stream event. Text deltas are batched; all other events are written immediately.
420
+ */
421
+ async write(event) {
422
+ if (this.closed) return;
423
+ if (this.emitter) {
424
+ this.emitter.emit("event", event);
425
+ }
426
+ if (event.type === "text_delta") {
427
+ this.pendingDelta += event.delta;
428
+ this.pendingDeltaCount++;
429
+ if (!this.flushTimer) {
430
+ this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
431
+ }
432
+ if (this.pendingDelta.length >= this.maxBatchChars) {
433
+ await this.flush();
434
+ }
435
+ } else {
436
+ await this.flush();
437
+ await this.writeToStorage([{ eventType: event.type, payload: event, batchCount: 1 }]);
438
+ }
439
+ }
440
+ /**
441
+ * Flush any pending text_delta events to storage as a single batched event.
442
+ */
443
+ async flush() {
444
+ if (this.flushTimer) {
445
+ clearTimeout(this.flushTimer);
446
+ this.flushTimer = null;
447
+ }
448
+ if (this.pendingDelta.length === 0) return;
449
+ const batchedEvent = {
450
+ type: "text_delta",
451
+ delta: this.pendingDelta
452
+ };
453
+ const batchCount = this.pendingDeltaCount;
454
+ this.pendingDelta = "";
455
+ this.pendingDeltaCount = 0;
456
+ await this.writeToStorage([{ eventType: "text_delta", payload: batchedEvent, batchCount }]);
457
+ }
458
+ /**
459
+ * Close the writer, flushing any remaining events.
460
+ */
461
+ async close() {
462
+ await this.flush();
463
+ this.closed = true;
464
+ if (this.emitter) {
465
+ this.emitter.emit("done");
466
+ }
467
+ }
468
+ async writeToStorage(events) {
469
+ return this.storage.appendEvents(this.sessionId, events);
470
+ }
471
+ };
472
+ }
473
+ });
474
+
387
475
  // src/session/manager.ts
388
476
  exports.SessionManager = void 0;
389
477
  var init_manager = __esm({
@@ -1788,11 +1876,10 @@ var init_sandbox_logger = __esm({
1788
1876
  };
1789
1877
  }
1790
1878
  });
1791
-
1792
- // src/agent/harness.ts
1793
1879
  exports.AgentHarness = void 0;
1794
1880
  var init_harness = __esm({
1795
1881
  "src/agent/harness.ts"() {
1882
+ init_stream_event_writer();
1796
1883
  init_manager();
1797
1884
  init_claude_sdk();
1798
1885
  init_backend();
@@ -1809,6 +1896,8 @@ var init_harness = __esm({
1809
1896
  sessionSkillDirs = /* @__PURE__ */ new Map();
1810
1897
  /** Tracks active executions by session ID for stop/interrupt capability */
1811
1898
  activeExecutions = /* @__PURE__ */ new Map();
1899
+ /** EventEmitters for same-process SSE relay fast-path, keyed by session ID */
1900
+ sessionEmitters = /* @__PURE__ */ new Map();
1812
1901
  constructor(config) {
1813
1902
  this.name = config.name;
1814
1903
  this.config = config;
@@ -1956,6 +2045,20 @@ var init_harness = __esm({
1956
2045
  getSessionManager() {
1957
2046
  return this.sessionManager;
1958
2047
  }
2048
+ /**
2049
+ * Get the stream event storage (if configured)
2050
+ */
2051
+ getStreamEventStorage() {
2052
+ return this.config.streamEventStorage;
2053
+ }
2054
+ /**
2055
+ * Get the EventEmitter for a session's stream events.
2056
+ * Used by the relay endpoint for same-process fast-path.
2057
+ * Returns undefined if no active execution or no streamEventStorage configured.
2058
+ */
2059
+ getSessionEmitter(sessionId) {
2060
+ return this.sessionEmitters.get(sessionId);
2061
+ }
1959
2062
  async ensureInitialized() {
1960
2063
  if (!this.initialized) {
1961
2064
  await this.initialize();
@@ -1970,9 +2073,24 @@ var init_harness = __esm({
1970
2073
  async *send(prompt, options = {}) {
1971
2074
  const controller = new AbortController();
1972
2075
  self.activeExecutions.set(session.id, controller);
2076
+ let writer;
2077
+ let emitter;
2078
+ if (self.config.streamEventStorage) {
2079
+ emitter = new events.EventEmitter();
2080
+ self.sessionEmitters.set(session.id, emitter);
2081
+ writer = new exports.StreamEventWriter({
2082
+ storage: self.config.streamEventStorage,
2083
+ sessionId: session.id,
2084
+ emitter
2085
+ });
2086
+ }
1973
2087
  if (options.signal) {
1974
2088
  options.signal.addEventListener("abort", () => controller.abort(), { once: true });
1975
2089
  }
2090
+ const writeEvent = writer ? (event) => {
2091
+ writer.write(event).catch(() => {
2092
+ });
2093
+ } : void 0;
1976
2094
  let partialTextContent = "";
1977
2095
  const sandboxLogs = [];
1978
2096
  const logQueue = [];
@@ -1986,7 +2104,9 @@ var init_harness = __esm({
1986
2104
  const yieldQueuedLogs = async function* () {
1987
2105
  while (logQueue.length > 0) {
1988
2106
  const entry = logQueue.shift();
1989
- yield { type: "sandbox_log", entry };
2107
+ const event = { type: "sandbox_log", entry };
2108
+ writeEvent?.(event);
2109
+ yield event;
1990
2110
  }
1991
2111
  };
1992
2112
  logger3.info("execution", `Starting execution for session ${session.id}`);
@@ -2003,11 +2123,13 @@ var init_harness = __esm({
2003
2123
  if (self.hooks.onMessage && savedUserMessage) {
2004
2124
  await self.hooks.onMessage(savedUserMessage);
2005
2125
  }
2006
- yield {
2126
+ const sessionStartEvent = {
2007
2127
  type: "session_start",
2008
2128
  sessionId: session.id,
2009
2129
  sdkSessionId: session.sdkSessionId ?? ""
2010
2130
  };
2131
+ writeEvent?.(sessionStartEvent);
2132
+ yield sessionStartEvent;
2011
2133
  yield* yieldQueuedLogs();
2012
2134
  const assistantContent = [];
2013
2135
  let wasAborted = false;
@@ -2028,22 +2150,26 @@ var init_harness = __esm({
2028
2150
  }
2029
2151
  if (event.type === "text_delta" && event.delta) {
2030
2152
  partialTextContent += event.delta;
2031
- yield {
2153
+ const textDeltaEvent = {
2032
2154
  type: "text_delta",
2033
2155
  delta: event.delta
2034
2156
  };
2157
+ writeEvent?.(textDeltaEvent);
2158
+ yield textDeltaEvent;
2035
2159
  } else if (event.type === "thinking_delta" && event.delta) {
2036
- yield {
2160
+ const thinkingEvent = {
2037
2161
  type: "thinking_delta",
2038
2162
  delta: event.delta
2039
2163
  };
2164
+ writeEvent?.(thinkingEvent);
2165
+ yield thinkingEvent;
2040
2166
  } else if (event.type === "text") {
2041
2167
  const textContent = {
2042
2168
  type: "text",
2043
2169
  text: event.text
2044
2170
  };
2045
2171
  assistantContent.push(textContent);
2046
- yield {
2172
+ const messageEvent = {
2047
2173
  type: "message",
2048
2174
  message: {
2049
2175
  id: "",
@@ -2053,6 +2179,8 @@ var init_harness = __esm({
2053
2179
  createdAt: /* @__PURE__ */ new Date()
2054
2180
  }
2055
2181
  };
2182
+ writeEvent?.(messageEvent);
2183
+ yield messageEvent;
2056
2184
  } else if (event.type === "tool_use") {
2057
2185
  logger3.debug("execution", `Tool use: ${event.name}`, { toolId: event.id });
2058
2186
  const toolContent = {
@@ -2062,12 +2190,14 @@ var init_harness = __esm({
2062
2190
  input: event.input
2063
2191
  };
2064
2192
  assistantContent.push(toolContent);
2065
- yield {
2193
+ const toolUseEvent = {
2066
2194
  type: "tool_use",
2067
2195
  id: event.id,
2068
2196
  name: event.name,
2069
2197
  input: event.input
2070
2198
  };
2199
+ writeEvent?.(toolUseEvent);
2200
+ yield toolUseEvent;
2071
2201
  yield* yieldQueuedLogs();
2072
2202
  if (self.hooks.onToolUse) {
2073
2203
  await self.hooks.onToolUse(event.name, event.input, null);
@@ -2084,12 +2214,14 @@ var init_harness = __esm({
2084
2214
  isError: event.isError
2085
2215
  };
2086
2216
  assistantContent.push(resultContent);
2087
- yield {
2217
+ const toolResultEvent = {
2088
2218
  type: "tool_result",
2089
2219
  toolUseId: event.toolUseId,
2090
2220
  content: event.content,
2091
2221
  isError: event.isError
2092
2222
  };
2223
+ writeEvent?.(toolResultEvent);
2224
+ yield toolResultEvent;
2093
2225
  }
2094
2226
  }
2095
2227
  if (wasAborted || controller.signal.aborted) {
@@ -2118,19 +2250,23 @@ var init_harness = __esm({
2118
2250
  }
2119
2251
  });
2120
2252
  yield* yieldQueuedLogs();
2121
- yield {
2253
+ const stoppedEvent = {
2122
2254
  type: "session_stopped",
2123
2255
  sessionId: session.id,
2124
2256
  reason: "user_requested",
2125
2257
  partialContent: partialTextContent || void 0
2126
2258
  };
2259
+ writeEvent?.(stoppedEvent);
2260
+ yield stoppedEvent;
2127
2261
  return;
2128
2262
  }
2129
2263
  logger3.info("execution", "Execution completed successfully");
2130
- yield {
2264
+ const turnCompleteEvent = {
2131
2265
  type: "turn_complete",
2132
2266
  sessionId: session.id
2133
2267
  };
2268
+ writeEvent?.(turnCompleteEvent);
2269
+ yield turnCompleteEvent;
2134
2270
  if (assistantContent.length > 0) {
2135
2271
  const savedAssistantMessages = await self.sessionManager.saveMessages(
2136
2272
  session.id,
@@ -2154,11 +2290,13 @@ var init_harness = __esm({
2154
2290
  }
2155
2291
  });
2156
2292
  yield* yieldQueuedLogs();
2157
- yield {
2293
+ const sessionEndEvent = {
2158
2294
  type: "session_end",
2159
2295
  sessionId: session.id,
2160
2296
  status: "completed"
2161
2297
  };
2298
+ writeEvent?.(sessionEndEvent);
2299
+ yield sessionEndEvent;
2162
2300
  if (self.hooks.onSessionEnd) {
2163
2301
  const updatedSession = await self.sessionManager.getSession(session.id);
2164
2302
  if (updatedSession) {
@@ -2189,12 +2327,14 @@ var init_harness = __esm({
2189
2327
  }
2190
2328
  });
2191
2329
  yield* yieldQueuedLogs();
2192
- yield {
2330
+ const catchStoppedEvent = {
2193
2331
  type: "session_stopped",
2194
2332
  sessionId: session.id,
2195
2333
  reason: "user_requested",
2196
2334
  partialContent: partialTextContent || void 0
2197
2335
  };
2336
+ writeEvent?.(catchStoppedEvent);
2337
+ yield catchStoppedEvent;
2198
2338
  } else {
2199
2339
  logger3.error("execution", `Execution error: ${errorMessage}`);
2200
2340
  await self.sessionManager.updateSession(session.id, {
@@ -2212,13 +2352,19 @@ var init_harness = __esm({
2212
2352
  await self.hooks.onError(error, updatedSession);
2213
2353
  }
2214
2354
  }
2215
- yield {
2355
+ const errorEvent = {
2216
2356
  type: "error",
2217
2357
  error: errorMessage
2218
2358
  };
2359
+ writeEvent?.(errorEvent);
2360
+ yield errorEvent;
2219
2361
  }
2220
2362
  } finally {
2221
2363
  self.activeExecutions.delete(session.id);
2364
+ if (writer) {
2365
+ await writer.close();
2366
+ }
2367
+ self.sessionEmitters.delete(session.id);
2222
2368
  }
2223
2369
  },
2224
2370
  async getSession() {
@@ -2457,6 +2603,11 @@ var init_memory = __esm({
2457
2603
  if (options.status) {
2458
2604
  sessions2 = sessions2.filter((s) => s.status === options.status);
2459
2605
  }
2606
+ if (options.resourceId) {
2607
+ sessions2 = sessions2.filter(
2608
+ (s) => s.metadata && s.metadata["resourceId"] === options.resourceId
2609
+ );
2610
+ }
2460
2611
  const orderBy = options.orderBy ?? "createdAt";
2461
2612
  const order = options.order ?? "desc";
2462
2613
  sessions2.sort((a, b) => {
@@ -2722,12 +2873,88 @@ var init_queue_memory = __esm({
2722
2873
  };
2723
2874
  }
2724
2875
  });
2876
+ exports.MemoryStreamEventStorage = void 0;
2877
+ var init_stream_event_memory = __esm({
2878
+ "src/storage/stream-event-memory.ts"() {
2879
+ exports.MemoryStreamEventStorage = class {
2880
+ events = /* @__PURE__ */ new Map();
2881
+ nextSequence = /* @__PURE__ */ new Map();
2882
+ async initialize() {
2883
+ }
2884
+ async close() {
2885
+ this.events.clear();
2886
+ this.nextSequence.clear();
2887
+ }
2888
+ async appendEvents(sessionId, events) {
2889
+ if (!this.events.has(sessionId)) {
2890
+ this.events.set(sessionId, []);
2891
+ }
2892
+ const sessionEvents2 = this.events.get(sessionId);
2893
+ let seq = this.nextSequence.get(sessionId) ?? 0;
2894
+ const stored = [];
2895
+ for (const event of events) {
2896
+ seq += 1;
2897
+ const storedEvent = {
2898
+ id: nanoid.nanoid(),
2899
+ sessionId,
2900
+ sequence: seq,
2901
+ eventType: event.eventType,
2902
+ payload: event.payload,
2903
+ batchCount: event.batchCount ?? 1,
2904
+ createdAt: /* @__PURE__ */ new Date()
2905
+ };
2906
+ sessionEvents2.push(storedEvent);
2907
+ stored.push(storedEvent);
2908
+ }
2909
+ this.nextSequence.set(sessionId, seq);
2910
+ return stored;
2911
+ }
2912
+ async readEvents(sessionId, options) {
2913
+ const sessionEvents2 = this.events.get(sessionId) ?? [];
2914
+ let result = sessionEvents2;
2915
+ if (options?.afterSequence !== void 0) {
2916
+ result = result.filter((e) => e.sequence > options.afterSequence);
2917
+ }
2918
+ if (options?.limit !== void 0) {
2919
+ result = result.slice(0, options.limit);
2920
+ }
2921
+ return result;
2922
+ }
2923
+ async getLatestSequence(sessionId) {
2924
+ return this.nextSequence.get(sessionId) ?? 0;
2925
+ }
2926
+ async deleteSessionEvents(sessionId) {
2927
+ this.events.delete(sessionId);
2928
+ this.nextSequence.delete(sessionId);
2929
+ }
2930
+ async cleanupOldEvents(maxAgeMs) {
2931
+ const cutoff = Date.now() - maxAgeMs;
2932
+ let deleted = 0;
2933
+ for (const [sessionId, sessionEvents2] of this.events) {
2934
+ const before = sessionEvents2.length;
2935
+ const remaining = sessionEvents2.filter(
2936
+ (e) => e.createdAt.getTime() >= cutoff
2937
+ );
2938
+ deleted += before - remaining.length;
2939
+ if (remaining.length === 0) {
2940
+ this.events.delete(sessionId);
2941
+ this.nextSequence.delete(sessionId);
2942
+ } else {
2943
+ this.events.set(sessionId, remaining);
2944
+ }
2945
+ }
2946
+ return deleted;
2947
+ }
2948
+ };
2949
+ }
2950
+ });
2725
2951
 
2726
2952
  // src/storage/index.ts
2727
2953
  var init_storage = __esm({
2728
2954
  "src/storage/index.ts"() {
2729
2955
  init_memory();
2730
2956
  init_queue_memory();
2957
+ init_stream_event_memory();
2731
2958
  }
2732
2959
  });
2733
2960
 
@@ -4312,13 +4539,13 @@ function getSandboxPool() {
4312
4539
  return globalPool;
4313
4540
  }
4314
4541
  function isPoolEnabled() {
4315
- if (!process.env.VERCEL) {
4316
- return false;
4317
- }
4318
4542
  if (process.env.SANDBOX_POOL_ENABLED === "false") {
4319
4543
  return false;
4320
4544
  }
4321
- return true;
4545
+ if (process.env.SANDBOX_POOL_ENABLED === "true") {
4546
+ return true;
4547
+ }
4548
+ return !!process.env.VERCEL;
4322
4549
  }
4323
4550
  async function ensureSandboxPoolInitialized() {
4324
4551
  if (globalPool && globalPool.isRunning()) {
@@ -4403,7 +4630,7 @@ node -e "require('@anthropic-ai/claude-agent-sdk'); console.log('[warmup] SDK ve
4403
4630
 
4404
4631
  echo "[warmup] Warmup complete!"
4405
4632
  `;
4406
- exports.SandboxPool = class {
4633
+ exports.SandboxPool = class _SandboxPool {
4407
4634
  config;
4408
4635
  pool = /* @__PURE__ */ new Map();
4409
4636
  warmingInProgress = /* @__PURE__ */ new Set();
@@ -4412,6 +4639,14 @@ echo "[warmup] Warmup complete!"
4412
4639
  lastMaintenanceAt = null;
4413
4640
  metricsCallback;
4414
4641
  startPromise = null;
4642
+ /** Consecutive warmup failure count (reset on success) */
4643
+ consecutiveFailures = 0;
4644
+ /** Timestamp of last warmup attempt — used for backoff */
4645
+ lastWarmupAttemptAt = 0;
4646
+ /** Max consecutive failures before stopping attempts entirely until manual restart */
4647
+ static MAX_CONSECUTIVE_FAILURES = 10;
4648
+ /** Base backoff delay in ms (doubles each failure: 30s, 60s, 120s, 240s…) */
4649
+ static BACKOFF_BASE_MS = 3e4;
4415
4650
  constructor(config = {}) {
4416
4651
  this.config = {
4417
4652
  minPoolSize: config.minPoolSize ?? parseInt(process.env.SANDBOX_POOL_MIN_SIZE ?? "2"),
@@ -4587,6 +4822,8 @@ echo "[warmup] Warmup complete!"
4587
4822
  warming: this.warmingInProgress.size,
4588
4823
  running: this.running,
4589
4824
  lastMaintenanceAt: this.lastMaintenanceAt,
4825
+ consecutiveFailures: this.consecutiveFailures,
4826
+ warmupSuspended: this.consecutiveFailures >= _SandboxPool.MAX_CONSECUTIVE_FAILURES,
4590
4827
  config: {
4591
4828
  minPoolSize: this.config.minPoolSize,
4592
4829
  maxPoolSize: this.config.maxPoolSize,
@@ -4612,6 +4849,7 @@ echo "[warmup] Warmup complete!"
4612
4849
  this.warmingInProgress.add(warmupId);
4613
4850
  this.emitMetric("warmup_started", { warmupId });
4614
4851
  const startTime = Date.now();
4852
+ this.lastWarmupAttemptAt = startTime;
4615
4853
  const useTarball = !!this.config.baseTarballUrl;
4616
4854
  try {
4617
4855
  const { Sandbox } = await import('@vercel/sandbox');
@@ -4712,6 +4950,7 @@ echo "[warmup] Warmup complete!"
4712
4950
  lastHeartbeat: now
4713
4951
  };
4714
4952
  console.log(`[POOL] Warmup completed for ${sandbox.sandboxId} (took ${warmupTime}ms)${useTarball ? " [tarball]" : ""}`);
4953
+ this.consecutiveFailures = 0;
4715
4954
  this.emitMetric("warmup_completed", {
4716
4955
  sandboxId: pooled.sandboxId,
4717
4956
  warmupTimeMs: warmupTime,
@@ -4721,10 +4960,20 @@ echo "[warmup] Warmup complete!"
4721
4960
  return pooled;
4722
4961
  } catch (error) {
4723
4962
  const warmupTime = Date.now() - startTime;
4963
+ this.consecutiveFailures++;
4964
+ const errorMessage = error instanceof Error ? error.message : "Unknown";
4965
+ if (this.consecutiveFailures === 1) {
4966
+ console.error(`[POOL] Warmup failed: ${errorMessage}`);
4967
+ } else {
4968
+ console.error(
4969
+ `[POOL] Warmup failed (${this.consecutiveFailures} consecutive). Next attempt in ${Math.min(_SandboxPool.BACKOFF_BASE_MS * Math.pow(2, this.consecutiveFailures - 1), 3e5) / 1e3}s. Error: ${errorMessage}`
4970
+ );
4971
+ }
4724
4972
  this.emitMetric("warmup_failed", {
4725
- error: error instanceof Error ? error.message : "Unknown",
4973
+ error: errorMessage,
4726
4974
  warmupTimeMs: warmupTime,
4727
- usedTarball: useTarball
4975
+ usedTarball: useTarball,
4976
+ consecutiveFailures: this.consecutiveFailures
4728
4977
  });
4729
4978
  throw error;
4730
4979
  } finally {
@@ -4832,9 +5081,30 @@ echo "[warmup] Warmup complete!"
4832
5081
  });
4833
5082
  }
4834
5083
  /**
4835
- * Replenish pool if below min size
5084
+ * Replenish pool if below min size.
5085
+ * Applies exponential backoff when warmups keep failing to avoid
5086
+ * a tight failure loop that wastes resources and floods metrics.
4836
5087
  */
4837
5088
  async replenishPool() {
5089
+ if (this.consecutiveFailures >= _SandboxPool.MAX_CONSECUTIVE_FAILURES) {
5090
+ if (Math.random() < 0.2) {
5091
+ console.warn(
5092
+ `[POOL] Warmup suspended after ${this.consecutiveFailures} consecutive failures. Check SANDBOX_BASE_TARBALL_URL or Vercel API status. Pool will retry on next acquire().`
5093
+ );
5094
+ }
5095
+ return;
5096
+ }
5097
+ if (this.consecutiveFailures > 0) {
5098
+ const backoffMs = Math.min(
5099
+ _SandboxPool.BACKOFF_BASE_MS * Math.pow(2, this.consecutiveFailures - 1),
5100
+ 5 * 6e4
5101
+ // Cap at 5 minutes
5102
+ );
5103
+ const timeSinceLastAttempt = Date.now() - this.lastWarmupAttemptAt;
5104
+ if (timeSinceLastAttempt < backoffMs) {
5105
+ return;
5106
+ }
5107
+ }
4838
5108
  const eligibleCount = this.getEligibleCount();
4839
5109
  const warmingCount = this.warmingInProgress.size;
4840
5110
  const totalProjected = eligibleCount + warmingCount;
@@ -5182,10 +5452,11 @@ async function getOrCreateSandbox(options) {
5182
5452
  startupScriptRan: true,
5183
5453
  // Assume ran for reconnected sandboxes
5184
5454
  startupScriptHash: void 0,
5455
+ // Unknown — caller should not re-run based on hash mismatch alone
5185
5456
  isNew: false,
5186
5457
  configFileUrl: void 0,
5187
- configInstalledAt: void 0
5188
- // We don't know when config was installed on reconnected sandboxes
5458
+ configInstalledAt: now2
5459
+ // Assume config was installed — prevents unnecessary re-install on reconnection
5189
5460
  };
5190
5461
  } else {
5191
5462
  console.log("[SANDBOX] Reconnected sandbox failed health check, will create new");
@@ -5571,7 +5842,8 @@ async function* executeInSandbox(prompt, apiKey, options) {
5571
5842
  markSdkInstalled(sessionId);
5572
5843
  }
5573
5844
  const currentScriptHash = hashStartupScript(options.startupScript);
5574
- const needsStartupScript = options.startupScript && (!startupScriptRan || currentScriptHash !== cachedScriptHash);
5845
+ const scriptHashChanged = cachedScriptHash !== void 0 && currentScriptHash !== cachedScriptHash;
5846
+ const needsStartupScript = options.startupScript && (!startupScriptRan || scriptHashChanged);
5575
5847
  if (needsStartupScript) {
5576
5848
  console.log("[SANDBOX] Running startup script...");
5577
5849
  const startupStartTime = Date.now();
@@ -5705,11 +5977,18 @@ const { query } = require('@anthropic-ai/claude-agent-sdk');
5705
5977
  const prompt = ${JSON.stringify(prompt)};
5706
5978
  const options = ${JSON.stringify(sdkOptions)};
5707
5979
 
5980
+ let queryCompleted = false;
5981
+
5708
5982
  async function run() {
5709
5983
  try {
5710
5984
  for await (const message of query({ prompt, options })) {
5711
5985
  console.log(JSON.stringify(message));
5712
5986
  }
5987
+ queryCompleted = true;
5988
+ // Exit cleanly immediately after query completes.
5989
+ // MCP server cleanup (e.g. Playwright browser shutdown) can trigger
5990
+ // unhandled rejections after the query is done, causing spurious exit code 1.
5991
+ process.exit(0);
5713
5992
  } catch (error) {
5714
5993
  const errorInfo = {
5715
5994
  type: 'error',
@@ -5725,6 +6004,11 @@ async function run() {
5725
6004
  }
5726
6005
 
5727
6006
  process.on('unhandledRejection', (reason) => {
6007
+ // Ignore rejections from MCP server cleanup after query has completed
6008
+ if (queryCompleted) {
6009
+ process.exit(0);
6010
+ return;
6011
+ }
5728
6012
  const errorInfo = {
5729
6013
  type: 'error',
5730
6014
  error: reason instanceof Error ? reason.message : String(reason),
@@ -5735,6 +6019,11 @@ process.on('unhandledRejection', (reason) => {
5735
6019
  });
5736
6020
 
5737
6021
  process.on('uncaughtException', (error) => {
6022
+ // Ignore exceptions from MCP server cleanup after query has completed
6023
+ if (queryCompleted) {
6024
+ process.exit(0);
6025
+ return;
6026
+ }
5738
6027
  const errorInfo = {
5739
6028
  type: 'error',
5740
6029
  error: error.message || 'Unknown error',
@@ -5921,6 +6210,85 @@ SCRIPT_EOF`]
5921
6210
  }
5922
6211
  }
5923
6212
  }
6213
+ async function warmSandboxForSession(options) {
6214
+ const {
6215
+ sessionId,
6216
+ runtime = "node22",
6217
+ timeout = 600,
6218
+ vcpus = 4,
6219
+ startupScript,
6220
+ configFileUrl,
6221
+ envVars = {}
6222
+ } = options;
6223
+ console.log(`[WARM] Pre-warming sandbox for session: ${sessionId}`);
6224
+ const startTime = Date.now();
6225
+ const result = await getOrCreateSandbox({
6226
+ sessionId,
6227
+ runtime,
6228
+ timeout,
6229
+ vcpus
6230
+ });
6231
+ const { sandbox, sdkInstalled } = result;
6232
+ if (!sdkInstalled) {
6233
+ console.log("[WARM] Installing SDK...");
6234
+ await sandbox.runCommand({
6235
+ cmd: "bash",
6236
+ args: ["-c", "which jq || (sudo dnf install -y jq 2>/dev/null || sudo apt-get update && sudo apt-get install -y jq 2>/dev/null || sudo apk add jq 2>/dev/null) || true"]
6237
+ });
6238
+ await sandbox.runCommand({
6239
+ cmd: "bash",
6240
+ args: ["-c", "which ffmpeg || (sudo dnf install -y ffmpeg 2>/dev/null || sudo apt-get update && sudo apt-get install -y ffmpeg 2>/dev/null || sudo apk add ffmpeg 2>/dev/null) || true"]
6241
+ });
6242
+ const sdkResult = await sandbox.runCommand({
6243
+ cmd: "bash",
6244
+ args: ["-c", "npm init -y && npm install @anthropic-ai/claude-agent-sdk"],
6245
+ env: envVars
6246
+ });
6247
+ if (sdkResult.exitCode !== 0) {
6248
+ const stderr = await sdkResult.stderr();
6249
+ throw new Error(`Failed to install Claude Agent SDK during warmup: ${stderr}`);
6250
+ }
6251
+ markSdkInstalled(sessionId);
6252
+ }
6253
+ if (startupScript) {
6254
+ console.log("[WARM] Running startup script...");
6255
+ const scriptResult = await sandbox.runCommand({
6256
+ cmd: "bash",
6257
+ args: ["-c", startupScript],
6258
+ env: envVars
6259
+ });
6260
+ if (scriptResult.exitCode !== 0) {
6261
+ const stderr = await scriptResult.stderr();
6262
+ throw new Error(`Startup script failed during warmup: ${stderr}`);
6263
+ }
6264
+ markStartupScriptRan(sessionId, hashStartupScript(startupScript));
6265
+ }
6266
+ if (configFileUrl) {
6267
+ console.log("[WARM] Installing config from:", configFileUrl);
6268
+ const configScript = `
6269
+ set -e
6270
+ curl -sSL --fail "${configFileUrl}" -o /tmp/config.zip
6271
+ unzip -o /tmp/config.zip -d .
6272
+ rm -f /tmp/config.zip
6273
+ `;
6274
+ const configResult = await sandbox.runCommand({
6275
+ cmd: "bash",
6276
+ args: ["-c", configScript],
6277
+ env: envVars
6278
+ });
6279
+ if (configResult.exitCode !== 0) {
6280
+ const stderr = await configResult.stderr();
6281
+ throw new Error(`Config installation failed during warmup: ${stderr}`);
6282
+ }
6283
+ markConfigInstalled(sessionId, configFileUrl);
6284
+ }
6285
+ const duration = Date.now() - startTime;
6286
+ console.log(`[WARM] Sandbox pre-warmed for session ${sessionId} in ${duration}ms (sandbox: ${result.sandboxId})`);
6287
+ return {
6288
+ sandboxId: result.sandboxId,
6289
+ ready: true
6290
+ };
6291
+ }
5924
6292
  var sandboxCache, DEFAULT_IDLE_TTL, CLEANUP_INTERVAL, HEARTBEAT_INTERVAL, cleanupIntervalId, heartbeatIntervalId, GLOBAL_HEARTBEAT_KEY, heartbeatListeners;
5925
6293
  var init_vercel_sandbox_executor = __esm({
5926
6294
  "src/runtime/vercel-sandbox-executor.ts"() {
@@ -6575,6 +6943,31 @@ WATCHER_EOF`;
6575
6943
  globalInSandboxManager = null;
6576
6944
  }
6577
6945
  });
6946
+ function matchGlob(pattern, filePath) {
6947
+ const normalizedPath = filePath.replace(/\\/g, "/");
6948
+ const normalizedPattern = pattern.replace(/\\/g, "/");
6949
+ let regexStr = "";
6950
+ let i = 0;
6951
+ while (i < normalizedPattern.length) {
6952
+ if (normalizedPattern[i] === "*" && normalizedPattern[i + 1] === "*") {
6953
+ if (normalizedPattern[i + 2] === "/") {
6954
+ regexStr += "(?:.*/)?";
6955
+ i += 3;
6956
+ } else {
6957
+ regexStr += ".*";
6958
+ i += 2;
6959
+ }
6960
+ } else if (normalizedPattern[i] === "*") {
6961
+ regexStr += "[^/]*";
6962
+ i += 1;
6963
+ } else {
6964
+ const ch = normalizedPattern[i];
6965
+ regexStr += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6966
+ i += 1;
6967
+ }
6968
+ }
6969
+ return new RegExp(`^${regexStr}$`).test(normalizedPath);
6970
+ }
6578
6971
  function extractErrorMessage(error) {
6579
6972
  if (error === null || error === void 0) {
6580
6973
  return "Unknown error (null/undefined)";
@@ -6617,12 +7010,18 @@ function extractErrorMessage(error) {
6617
7010
  function createSandboxFileSync(options) {
6618
7011
  return new exports.SandboxFileSync(options);
6619
7012
  }
6620
- exports.SandboxFileSync = void 0;
7013
+ var DEFAULT_WEBHOOK_IGNORE; exports.SandboxFileSync = void 0;
6621
7014
  var init_sandbox_file_sync = __esm({
6622
7015
  "src/runtime/sandbox-file-sync.ts"() {
6623
7016
  init_sandbox_file_watcher();
6624
7017
  init_in_sandbox_watcher();
6625
7018
  init_types();
7019
+ DEFAULT_WEBHOOK_IGNORE = [
7020
+ "**/agent-runner.js",
7021
+ "**/agent-runner.cjs",
7022
+ "**/.file-watcher.js",
7023
+ "**/.file-watcher-output.log"
7024
+ ];
6626
7025
  exports.SandboxFileSync = class {
6627
7026
  fileStore;
6628
7027
  sandboxBasePath;
@@ -6637,6 +7036,10 @@ var init_sandbox_file_sync = __esm({
6637
7036
  fileChangeSubscribers = /* @__PURE__ */ new Set();
6638
7037
  // Sequence number cache per session (for event storage)
6639
7038
  sequenceNumbers = /* @__PURE__ */ new Map();
7039
+ // Webhook batching state
7040
+ batchTimer = null;
7041
+ batchQueue = /* @__PURE__ */ new Map();
7042
+ // keyed by file path for dedup
6640
7043
  constructor(options) {
6641
7044
  this.fileStore = options.fileStore;
6642
7045
  this.sandboxBasePath = options.sandboxBasePath ?? ".claude/files";
@@ -6671,6 +7074,72 @@ var init_sandbox_file_sync = __esm({
6671
7074
  */
6672
7075
  removeWebhook() {
6673
7076
  this.webhookConfig = void 0;
7077
+ if (this.batchTimer) {
7078
+ clearTimeout(this.batchTimer);
7079
+ this.batchTimer = null;
7080
+ }
7081
+ this.batchQueue.clear();
7082
+ }
7083
+ /**
7084
+ * Check if a file path should be excluded from webhook notifications.
7085
+ * Tests against DEFAULT_WEBHOOK_IGNORE patterns and any user-configured ignorePaths.
7086
+ */
7087
+ shouldIgnorePath(filePath) {
7088
+ if (!filePath) return false;
7089
+ const userPatterns = this.webhookConfig?.ignorePaths ?? [];
7090
+ const allPatterns = [...DEFAULT_WEBHOOK_IGNORE, ...userPatterns];
7091
+ return allPatterns.some((pattern) => matchGlob(pattern, filePath));
7092
+ }
7093
+ /**
7094
+ * Route a webhook payload through batching (if configured) or send immediately.
7095
+ * When batchWindowMs is set, payloads are queued and deduplicated by file path.
7096
+ */
7097
+ enqueueOrSendWebhook(payload, sessionId) {
7098
+ const batchWindowMs = this.webhookConfig?.batchWindowMs;
7099
+ if (!batchWindowMs || batchWindowMs <= 0) {
7100
+ this.sendWebhook(payload, sessionId);
7101
+ return;
7102
+ }
7103
+ const filePath = payload.fileSyncEvent?.canonicalPath ?? payload.fileChangeEvent?.relativePath ?? `_unknown_${Date.now()}`;
7104
+ this.batchQueue.set(filePath, payload);
7105
+ if (this.batchTimer) {
7106
+ clearTimeout(this.batchTimer);
7107
+ }
7108
+ this.batchTimer = setTimeout(() => {
7109
+ this.flushWebhookBatch(sessionId);
7110
+ }, batchWindowMs);
7111
+ }
7112
+ /**
7113
+ * Flush all queued webhook payloads as a single batch request.
7114
+ */
7115
+ async flushWebhookBatch(sessionId) {
7116
+ this.batchTimer = null;
7117
+ if (this.batchQueue.size === 0) return;
7118
+ const payloads = Array.from(this.batchQueue.values());
7119
+ this.batchQueue.clear();
7120
+ const fileSyncEvents = [];
7121
+ const fileChangeEvents = [];
7122
+ let batchSessionId = sessionId ?? "";
7123
+ for (const p of payloads) {
7124
+ if (!batchSessionId) {
7125
+ batchSessionId = p.sessionId;
7126
+ }
7127
+ if (p.fileSyncEvent) {
7128
+ fileSyncEvents.push(p.fileSyncEvent);
7129
+ }
7130
+ if (p.fileChangeEvent) {
7131
+ fileChangeEvents.push(p.fileChangeEvent);
7132
+ }
7133
+ }
7134
+ const batchPayload = {
7135
+ event: "file_sync_batch",
7136
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7137
+ sessionId: batchSessionId,
7138
+ metadata: this.webhookConfig?.metadata,
7139
+ fileSyncEvents,
7140
+ fileChangeEvents
7141
+ };
7142
+ await this.sendWebhook(batchPayload, sessionId);
6674
7143
  }
6675
7144
  /**
6676
7145
  * Extract hostname from URL for display (security - don't expose full URL)
@@ -6807,22 +7276,48 @@ var init_sandbox_file_sync = __esm({
6807
7276
  }
6808
7277
  }
6809
7278
  /**
6810
- * Send a file sync event webhook
7279
+ * Send a file sync event webhook.
7280
+ * Strips raw Buffer content (previousContent/newContent) and replaces with a
7281
+ * pre-signed S3 download URL when the file exists in storage.
6811
7282
  */
6812
7283
  async sendFileSyncWebhook(sessionId, event) {
7284
+ if (this.shouldIgnorePath(event.canonicalPath)) {
7285
+ return;
7286
+ }
7287
+ let downloadUrl;
7288
+ if (event.success && event.canonicalPath && !event.operation.startsWith("deleted")) {
7289
+ try {
7290
+ downloadUrl = await this.fileStore.getSignedUrl(sessionId, event.canonicalPath);
7291
+ } catch {
7292
+ }
7293
+ }
7294
+ const webhookEvent = {
7295
+ operation: event.operation,
7296
+ source: event.source,
7297
+ canonicalPath: event.canonicalPath,
7298
+ basePath: event.basePath,
7299
+ sandboxPath: event.sandboxPath,
7300
+ fileSize: event.fileSize,
7301
+ success: event.success,
7302
+ error: event.error,
7303
+ downloadUrl
7304
+ };
6813
7305
  const payload = {
6814
7306
  event: "file_sync",
6815
7307
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6816
7308
  sessionId,
6817
7309
  metadata: this.webhookConfig?.metadata,
6818
- fileSyncEvent: event
7310
+ fileSyncEvent: webhookEvent
6819
7311
  };
6820
- await this.sendWebhook(payload, sessionId);
7312
+ this.enqueueOrSendWebhook(payload, sessionId);
6821
7313
  }
6822
7314
  /**
6823
7315
  * Send a file change event webhook (from watcher)
6824
7316
  */
6825
7317
  async sendFileChangeWebhook(event) {
7318
+ if (this.shouldIgnorePath(event.relativePath)) {
7319
+ return;
7320
+ }
6826
7321
  const payload = {
6827
7322
  event: "file_change",
6828
7323
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6830,7 +7325,7 @@ var init_sandbox_file_sync = __esm({
6830
7325
  metadata: this.webhookConfig?.metadata,
6831
7326
  fileChangeEvent: event
6832
7327
  };
6833
- await this.sendWebhook(payload, event.sessionId);
7328
+ this.enqueueOrSendWebhook(payload, event.sessionId);
6834
7329
  }
6835
7330
  /**
6836
7331
  * Get the next sequence number for a session
@@ -7494,6 +7989,11 @@ var init_sandbox_file_sync = __esm({
7494
7989
  await Promise.all([...inSandboxPromises, ...localPromises]);
7495
7990
  this.inSandboxWatchers.clear();
7496
7991
  this.localWatchers.clear();
7992
+ if (this.batchTimer) {
7993
+ clearTimeout(this.batchTimer);
7994
+ this.batchTimer = null;
7995
+ await this.flushWebhookBatch();
7996
+ }
7497
7997
  }
7498
7998
  /**
7499
7999
  * Get watching status for all sessions
@@ -9103,6 +9603,135 @@ var init_utils = __esm({
9103
9603
  init_sandbox_logger();
9104
9604
  }
9105
9605
  });
9606
+
9607
+ // src/relay/stream-event-relay.ts
9608
+ async function* streamEventRelay(options) {
9609
+ const {
9610
+ storage,
9611
+ sessionId,
9612
+ afterSequence = 0,
9613
+ emitter,
9614
+ pollIntervalMs = 100,
9615
+ heartbeatIntervalMs = 15e3,
9616
+ signal
9617
+ } = options;
9618
+ let lastSequence = afterSequence;
9619
+ let done = false;
9620
+ const catchUpEvents = await storage.readEvents(sessionId, {
9621
+ afterSequence: lastSequence,
9622
+ limit: 1e4
9623
+ });
9624
+ for (const stored of catchUpEvents) {
9625
+ if (signal?.aborted || done) return;
9626
+ lastSequence = stored.sequence;
9627
+ yield stored.payload;
9628
+ if (TERMINAL_EVENT_TYPES.has(stored.eventType)) {
9629
+ done = true;
9630
+ return;
9631
+ }
9632
+ }
9633
+ if (signal?.aborted) return;
9634
+ if (emitter) {
9635
+ yield* relayFromEmitter(emitter, signal, heartbeatIntervalMs);
9636
+ } else {
9637
+ yield* relayByPolling(storage, sessionId, lastSequence, pollIntervalMs, heartbeatIntervalMs, signal);
9638
+ }
9639
+ }
9640
+ async function* relayFromEmitter(emitter, signal, heartbeatIntervalMs = 15e3) {
9641
+ const queue = [];
9642
+ let resolve3 = null;
9643
+ const onEvent = (event) => {
9644
+ queue.push(event);
9645
+ if (resolve3) {
9646
+ resolve3();
9647
+ resolve3 = null;
9648
+ }
9649
+ };
9650
+ const onDone = () => {
9651
+ queue.push("done");
9652
+ if (resolve3) {
9653
+ resolve3();
9654
+ resolve3 = null;
9655
+ }
9656
+ };
9657
+ emitter.on("event", onEvent);
9658
+ emitter.on("done", onDone);
9659
+ const heartbeatTimer = setInterval(() => {
9660
+ if (resolve3) {
9661
+ resolve3();
9662
+ resolve3 = null;
9663
+ }
9664
+ }, heartbeatIntervalMs);
9665
+ try {
9666
+ while (!signal?.aborted) {
9667
+ while (queue.length > 0) {
9668
+ const item = queue.shift();
9669
+ if (item === "done") return;
9670
+ yield item;
9671
+ if (TERMINAL_EVENT_TYPES.has(item.type)) return;
9672
+ }
9673
+ await new Promise((r) => {
9674
+ resolve3 = r;
9675
+ if (signal) {
9676
+ signal.addEventListener("abort", () => {
9677
+ resolve3 = null;
9678
+ r();
9679
+ }, { once: true });
9680
+ }
9681
+ });
9682
+ }
9683
+ } finally {
9684
+ clearInterval(heartbeatTimer);
9685
+ emitter.off("event", onEvent);
9686
+ emitter.off("done", onDone);
9687
+ }
9688
+ }
9689
+ async function* relayByPolling(storage, sessionId, startSequence, pollIntervalMs, heartbeatIntervalMs, signal) {
9690
+ let lastSequence = startSequence;
9691
+ while (!signal?.aborted) {
9692
+ const events = await storage.readEvents(sessionId, {
9693
+ afterSequence: lastSequence,
9694
+ limit: 100
9695
+ });
9696
+ for (const stored of events) {
9697
+ if (signal?.aborted) return;
9698
+ lastSequence = stored.sequence;
9699
+ yield stored.payload;
9700
+ if (TERMINAL_EVENT_TYPES.has(stored.eventType)) {
9701
+ return;
9702
+ }
9703
+ }
9704
+ if (events.length === 0) {
9705
+ await new Promise((r) => {
9706
+ const timer = setTimeout(r, pollIntervalMs);
9707
+ if (signal) {
9708
+ signal.addEventListener("abort", () => {
9709
+ clearTimeout(timer);
9710
+ r();
9711
+ }, { once: true });
9712
+ }
9713
+ });
9714
+ }
9715
+ }
9716
+ }
9717
+ var TERMINAL_EVENT_TYPES;
9718
+ var init_stream_event_relay = __esm({
9719
+ "src/relay/stream-event-relay.ts"() {
9720
+ TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
9721
+ "session_end",
9722
+ "error",
9723
+ "session_stopped"
9724
+ ]);
9725
+ }
9726
+ });
9727
+
9728
+ // src/relay/index.ts
9729
+ var init_relay = __esm({
9730
+ "src/relay/index.ts"() {
9731
+ init_stream_event_writer();
9732
+ init_stream_event_relay();
9733
+ }
9734
+ });
9106
9735
  function broadcastToSession(sessionId, event) {
9107
9736
  const subscribers = sessionSubscribers.get(sessionId);
9108
9737
  if (subscribers && subscribers.size > 0) {
@@ -9235,6 +9864,8 @@ function createSessionsRouter(options) {
9235
9864
  agentName: session.agentName,
9236
9865
  startedAt: /* @__PURE__ */ new Date()
9237
9866
  });
9867
+ const hasStreamEventStorage = !!agent.getStreamEventStorage();
9868
+ let sendSequenceCounter = 0;
9238
9869
  return streaming.streamSSE(c, async (stream) => {
9239
9870
  try {
9240
9871
  for await (const event of activeSession.send(data.prompt, {
@@ -9246,7 +9877,9 @@ function createSessionsRouter(options) {
9246
9877
  if (controller.signal.aborted) {
9247
9878
  break;
9248
9879
  }
9880
+ sendSequenceCounter++;
9249
9881
  await stream.writeSSE({
9882
+ ...hasStreamEventStorage ? { id: String(sendSequenceCounter) } : {},
9250
9883
  event: event.type,
9251
9884
  data: JSON.stringify(event)
9252
9885
  });
@@ -9429,11 +10062,129 @@ function createSessionsRouter(options) {
9429
10062
  }
9430
10063
  });
9431
10064
  });
10065
+ router.get("/:sessionId/stream-events", async (c) => {
10066
+ const sessionId = c.req.param("sessionId");
10067
+ const afterSequence = Math.max(0, parseInt(c.req.query("afterSequence") || "0", 10) || 0);
10068
+ const limit = Math.min(Math.max(1, parseInt(c.req.query("limit") || "100", 10) || 100), 1e3);
10069
+ const waitMs = Math.min(Math.max(0, parseInt(c.req.query("waitMs") || "0", 10) || 0), 3e4);
10070
+ const session = await sessionManager.getSession(sessionId);
10071
+ if (!session) {
10072
+ return c.json({ error: "Session not found" }, 404);
10073
+ }
10074
+ const agent = agents2.get(session.agentName);
10075
+ if (!agent) {
10076
+ return c.json({ error: `Agent not found: ${session.agentName}` }, 404);
10077
+ }
10078
+ const streamEventStorage = agent.getStreamEventStorage();
10079
+ if (!streamEventStorage) {
10080
+ return c.json({ error: "Stream event storage not configured" }, 501);
10081
+ }
10082
+ let events = await streamEventStorage.readEvents(sessionId, {
10083
+ afterSequence,
10084
+ limit
10085
+ });
10086
+ if (events.length === 0 && waitMs > 0) {
10087
+ const deadline = Date.now() + waitMs;
10088
+ const pollInterval = 200;
10089
+ while (Date.now() < deadline) {
10090
+ events = await streamEventStorage.readEvents(sessionId, {
10091
+ afterSequence,
10092
+ limit
10093
+ });
10094
+ if (events.length > 0) break;
10095
+ const currentSession = await sessionManager.getSession(sessionId);
10096
+ if (currentSession && ["completed", "error", "stopped"].includes(currentSession.status)) {
10097
+ break;
10098
+ }
10099
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
10100
+ }
10101
+ }
10102
+ const nextCursor = events.length > 0 ? Math.max(...events.map((e) => e.sequence)) : afterSequence;
10103
+ return c.json({
10104
+ events: events.map((e) => ({
10105
+ id: e.id,
10106
+ sequence: e.sequence,
10107
+ eventType: e.eventType,
10108
+ payload: e.payload,
10109
+ batchCount: e.batchCount,
10110
+ createdAt: e.createdAt.toISOString()
10111
+ })),
10112
+ nextCursor,
10113
+ hasMore: events.length === limit,
10114
+ sessionStatus: session.status
10115
+ });
10116
+ });
10117
+ router.get("/:sessionId/stream", async (c) => {
10118
+ const sessionId = c.req.param("sessionId");
10119
+ const afterParam = c.req.query("after");
10120
+ const afterSequence = afterParam ? parseInt(afterParam, 10) : 0;
10121
+ const session = await sessionManager.getSession(sessionId);
10122
+ if (!session) {
10123
+ return c.json({ error: "Session not found" }, 404);
10124
+ }
10125
+ const agent = agents2.get(session.agentName);
10126
+ if (!agent) {
10127
+ return c.json({ error: `Agent not found: ${session.agentName}` }, 404);
10128
+ }
10129
+ const streamEventStorage = agent.getStreamEventStorage();
10130
+ if (!streamEventStorage) {
10131
+ return c.json({ error: "Stream event storage not configured" }, 501);
10132
+ }
10133
+ return streaming.streamSSE(c, async (stream) => {
10134
+ const abortController = new AbortController();
10135
+ c.req.raw.signal.addEventListener("abort", () => {
10136
+ abortController.abort();
10137
+ });
10138
+ const heartbeatInterval = setInterval(async () => {
10139
+ try {
10140
+ await stream.writeSSE({
10141
+ event: "heartbeat",
10142
+ data: JSON.stringify({ type: "heartbeat", timestamp: Date.now() })
10143
+ });
10144
+ } catch {
10145
+ clearInterval(heartbeatInterval);
10146
+ }
10147
+ }, 15e3);
10148
+ let sequenceCounter = afterSequence;
10149
+ try {
10150
+ const sessionEmitter = agent.getSessionEmitter(sessionId);
10151
+ const relay = streamEventRelay({
10152
+ storage: streamEventStorage,
10153
+ sessionId,
10154
+ afterSequence,
10155
+ emitter: sessionEmitter,
10156
+ signal: abortController.signal
10157
+ });
10158
+ for await (const event of relay) {
10159
+ if (abortController.signal.aborted) break;
10160
+ sequenceCounter++;
10161
+ await stream.writeSSE({
10162
+ id: String(sequenceCounter),
10163
+ event: event.type,
10164
+ data: JSON.stringify(event)
10165
+ });
10166
+ }
10167
+ } catch (error) {
10168
+ if (!abortController.signal.aborted) {
10169
+ await stream.writeSSE({
10170
+ event: "error",
10171
+ data: JSON.stringify({
10172
+ type: "error",
10173
+ error: error instanceof Error ? error.message : "Unknown error"
10174
+ })
10175
+ });
10176
+ }
10177
+ } finally {
10178
+ clearInterval(heartbeatInterval);
10179
+ }
10180
+ });
10181
+ });
9432
10182
  return router;
9433
10183
  }
9434
10184
  var createSessionSchema, fileAttachmentSchema, sendMessageSchema, updateEnvSchema, listSessionsSchema, activeStreams, sessionSubscribers;
9435
10185
  var init_sessions = __esm({
9436
10186
  "src/server/routes/sessions.ts"() {
10187
+ init_stream_event_relay();
9437
10188
  createSessionSchema = zod.z.object({
9438
10189
  agentName: zod.z.string(),
9439
10190
  metadata: zod.z.record(zod.z.unknown()).optional(),
@@ -10047,7 +10798,8 @@ function createHarnessServer(config) {
10047
10798
  basePath = "/api",
10048
10799
  middleware = [],
10049
10800
  hooks = {},
10050
- queue: queueConfig = {}
10801
+ queue: queueConfig = {},
10802
+ sandbox: sandboxConfig
10051
10803
  } = config;
10052
10804
  let agentsNeedReload = false;
10053
10805
  const sessionManager = new exports.SessionManager(storage);
@@ -10198,7 +10950,7 @@ function createHarnessServer(config) {
10198
10950
  return agentStorage.getActiveAgents();
10199
10951
  },
10200
10952
  /**
10201
- * Initialize storage, agents, and start queue processor
10953
+ * Initialize storage, agents, start queue processor, and optionally init sandbox pool
10202
10954
  */
10203
10955
  async initialize() {
10204
10956
  await storage.initialize();
@@ -10212,12 +10964,20 @@ function createHarnessServer(config) {
10212
10964
  await agent.initialize();
10213
10965
  }
10214
10966
  queueProcessor?.start();
10967
+ if (sandboxConfig?.pool && sandboxConfig.autoInitPool !== false) {
10968
+ try {
10969
+ await initializeSandboxPool(sandboxConfig.pool);
10970
+ } catch (error) {
10971
+ console.error("[SERVER] Failed to initialize sandbox pool:", error instanceof Error ? error.message : error);
10972
+ }
10973
+ }
10215
10974
  },
10216
10975
  /**
10217
- * Close storage, agents, and stop queue processor
10976
+ * Close storage, agents, stop queue processor, and shutdown sandbox pool
10218
10977
  */
10219
10978
  async close() {
10220
10979
  queueProcessor?.stop();
10980
+ await shutdownSandboxPool();
10221
10981
  for (const agent of agents2.values()) {
10222
10982
  await agent.close();
10223
10983
  }
@@ -10239,6 +10999,7 @@ var init_server = __esm({
10239
10999
  init_agents();
10240
11000
  init_skills2();
10241
11001
  init_queue2();
11002
+ init_sandbox_pool();
10242
11003
  }
10243
11004
  });
10244
11005
 
@@ -13160,7 +13921,8 @@ function createOpenAPIServer(config) {
13160
13921
  basePath = "/api",
13161
13922
  middleware = [],
13162
13923
  hooks = {},
13163
- docs = {}
13924
+ docs = {},
13925
+ sandbox: sandboxConfig
13164
13926
  } = config;
13165
13927
  const {
13166
13928
  enabled: docsEnabled = true,
@@ -13335,7 +14097,7 @@ Authentication is handled by the hosting application. This API does not enforce
13335
14097
  return agentStorage.getActiveAgents();
13336
14098
  },
13337
14099
  /**
13338
- * Initialize storage and agents
14100
+ * Initialize storage, agents, and optionally init sandbox pool
13339
14101
  */
13340
14102
  async initialize() {
13341
14103
  await storage.initialize();
@@ -13345,11 +14107,19 @@ Authentication is handled by the hosting application. This API does not enforce
13345
14107
  for (const agent of agents2.values()) {
13346
14108
  await agent.initialize();
13347
14109
  }
14110
+ if (sandboxConfig?.pool && sandboxConfig.autoInitPool !== false) {
14111
+ try {
14112
+ await initializeSandboxPool(sandboxConfig.pool);
14113
+ } catch (error) {
14114
+ console.error("[SERVER] Failed to initialize sandbox pool:", error instanceof Error ? error.message : error);
14115
+ }
14116
+ }
13348
14117
  },
13349
14118
  /**
13350
- * Close storage and agents
14119
+ * Close storage, agents, and shutdown sandbox pool
13351
14120
  */
13352
14121
  async close() {
14122
+ await shutdownSandboxPool();
13353
14123
  for (const agent of agents2.values()) {
13354
14124
  await agent.close();
13355
14125
  }
@@ -13381,6 +14151,7 @@ var init_server2 = __esm({
13381
14151
  init_sessions2();
13382
14152
  init_agents2();
13383
14153
  init_skills3();
14154
+ init_sandbox_pool();
13384
14155
  }
13385
14156
  });
13386
14157
 
@@ -16145,6 +16916,7 @@ __export(schema_exports, {
16145
16916
  configDeployments: () => configDeployments,
16146
16917
  configDeploymentsRelations: () => configDeploymentsRelations,
16147
16918
  eventCategoryEnum: () => eventCategoryEnum,
16919
+ eventSourceEnum: () => eventSourceEnum,
16148
16920
  messageRoleEnum: () => messageRoleEnum,
16149
16921
  messages: () => messages,
16150
16922
  messagesRelations: () => messagesRelations,
@@ -16154,9 +16926,11 @@ __export(schema_exports, {
16154
16926
  sessionEventsRelations: () => sessionEventsRelations,
16155
16927
  sessionStatusEnum: () => sessionStatusEnum,
16156
16928
  sessions: () => sessions,
16157
- sessionsRelations: () => sessionsRelations
16929
+ sessionsRelations: () => sessionsRelations,
16930
+ streamEvents: () => streamEvents,
16931
+ streamEventsRelations: () => streamEventsRelations
16158
16932
  });
16159
- var sessionStatusEnum, messageRoleEnum, agentStatusEnum, agentBackendEnum, queueItemStatusEnum, configDeploymentStatusEnum, configDeploymentTriggerEnum, eventCategoryEnum, sessions, messages, attachments, agents, queueItems, configDeployments, sessionEvents, sessionsRelations, sessionEventsRelations, messagesRelations, attachmentsRelations, configDeploymentsRelations, agentsRelations;
16933
+ var sessionStatusEnum, messageRoleEnum, agentStatusEnum, agentBackendEnum, queueItemStatusEnum, configDeploymentStatusEnum, configDeploymentTriggerEnum, eventSourceEnum, eventCategoryEnum, sessions, messages, attachments, agents, queueItems, configDeployments, sessionEvents, streamEvents, sessionsRelations, sessionEventsRelations, messagesRelations, attachmentsRelations, configDeploymentsRelations, streamEventsRelations, agentsRelations;
16160
16934
  var init_schema = __esm({
16161
16935
  "src/storage-postgres/schema.ts"() {
16162
16936
  sessionStatusEnum = pgCore.pgEnum("session_status", [
@@ -16198,6 +16972,14 @@ var init_schema = __esm({
16198
16972
  "initial",
16199
16973
  "rollback"
16200
16974
  ]);
16975
+ eventSourceEnum = pgCore.pgEnum("event_source", [
16976
+ "agent",
16977
+ // Directly from the agent SDK
16978
+ "system",
16979
+ // Generated by execution layer (lifecycle, user input)
16980
+ "derived"
16981
+ // Transformed/aggregated from agent events
16982
+ ]);
16201
16983
  eventCategoryEnum = pgCore.pgEnum("event_category", [
16202
16984
  "lifecycle",
16203
16985
  // session_start, session_end, turn_complete
@@ -16388,6 +17170,8 @@ var init_schema = __esm({
16388
17170
  // Aggregation fields (for text_stream events)
16389
17171
  isAggregated: pgCore.boolean("is_aggregated").default(false),
16390
17172
  aggregatedCount: pgCore.integer("aggregated_count"),
17173
+ // Event source tagging
17174
+ eventSource: eventSourceEnum("event_source").notNull().default("agent"),
16391
17175
  // Ordering
16392
17176
  sequenceNumber: pgCore.integer("sequence_number").notNull(),
16393
17177
  createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
@@ -16397,6 +17181,21 @@ var init_schema = __esm({
16397
17181
  pgCore.index("idx_session_events_category").on(table.sessionId, table.category)
16398
17182
  ]
16399
17183
  );
17184
+ streamEvents = pgCore.pgTable(
17185
+ "stream_events",
17186
+ {
17187
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
17188
+ sessionId: pgCore.uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
17189
+ sequence: pgCore.integer("sequence").notNull(),
17190
+ eventType: pgCore.text("event_type").notNull(),
17191
+ payload: pgCore.jsonb("payload").notNull().$type(),
17192
+ batchCount: pgCore.integer("batch_count").default(1).notNull(),
17193
+ createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
17194
+ },
17195
+ (table) => [
17196
+ pgCore.index("idx_stream_events_session_seq").on(table.sessionId, table.sequence)
17197
+ ]
17198
+ );
16400
17199
  sessionsRelations = drizzleOrm.relations(sessions, ({ one, many }) => ({
16401
17200
  parentSession: one(sessions, {
16402
17201
  fields: [sessions.parentSessionId],
@@ -16407,7 +17206,8 @@ var init_schema = __esm({
16407
17206
  relationName: "parentSession"
16408
17207
  }),
16409
17208
  messages: many(messages),
16410
- events: many(sessionEvents)
17209
+ events: many(sessionEvents),
17210
+ streamEvents: many(streamEvents)
16411
17211
  }));
16412
17212
  sessionEventsRelations = drizzleOrm.relations(sessionEvents, ({ one }) => ({
16413
17213
  session: one(sessions, {
@@ -16434,6 +17234,12 @@ var init_schema = __esm({
16434
17234
  references: [agents.id]
16435
17235
  })
16436
17236
  }));
17237
+ streamEventsRelations = drizzleOrm.relations(streamEvents, ({ one }) => ({
17238
+ session: one(sessions, {
17239
+ fields: [streamEvents.sessionId],
17240
+ references: [sessions.id]
17241
+ })
17242
+ }));
16437
17243
  agentsRelations = drizzleOrm.relations(agents, ({ many }) => ({
16438
17244
  configDeployments: many(configDeployments)
16439
17245
  }));
@@ -16621,6 +17427,20 @@ var init_storage2 = __esm({
16621
17427
  `);
16622
17428
  await this.db.execute(drizzleOrm.sql`
16623
17429
  ALTER TABLE agents ADD COLUMN IF NOT EXISTS backend agent_backend DEFAULT 'claude' NOT NULL;
17430
+ `);
17431
+ await this.db.execute(drizzleOrm.sql`
17432
+ CREATE TABLE IF NOT EXISTS stream_events (
17433
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17434
+ session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
17435
+ sequence INTEGER NOT NULL,
17436
+ event_type TEXT NOT NULL,
17437
+ payload JSONB NOT NULL,
17438
+ batch_count INTEGER NOT NULL DEFAULT 1,
17439
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17440
+ );
17441
+ `);
17442
+ await this.db.execute(drizzleOrm.sql`
17443
+ CREATE INDEX IF NOT EXISTS idx_stream_events_session_seq ON stream_events(session_id, sequence);
16624
17444
  `);
16625
17445
  }
16626
17446
  /**
@@ -16813,6 +17633,20 @@ var init_storage2 = __esm({
16813
17633
  `);
16814
17634
  await this.db.execute(drizzleOrm.sql`
16815
17635
  CREATE INDEX IF NOT EXISTS idx_queue_items_pending ON queue_items(agent_name, session_id, status, priority, created_at);
17636
+ `);
17637
+ await this.db.execute(drizzleOrm.sql`
17638
+ CREATE TABLE IF NOT EXISTS stream_events (
17639
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17640
+ session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
17641
+ sequence INTEGER NOT NULL,
17642
+ event_type TEXT NOT NULL,
17643
+ payload JSONB NOT NULL,
17644
+ batch_count INTEGER NOT NULL DEFAULT 1,
17645
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17646
+ );
17647
+ `);
17648
+ await this.db.execute(drizzleOrm.sql`
17649
+ CREATE INDEX IF NOT EXISTS idx_stream_events_session_seq ON stream_events(session_id, sequence);
16816
17650
  `);
16817
17651
  await this.db.execute(drizzleOrm.sql`
16818
17652
  DO $$ BEGIN
@@ -16930,6 +17764,11 @@ var init_storage2 = __esm({
16930
17764
  drizzleOrm.eq(sessions.status, options.status)
16931
17765
  );
16932
17766
  }
17767
+ if (options.resourceId) {
17768
+ conditions.push(
17769
+ drizzleOrm.sql`${sessions.metadata}->>'resourceId' = ${options.resourceId}`
17770
+ );
17771
+ }
16933
17772
  const orderColumn = options.orderBy === "updatedAt" ? sessions.updatedAt : sessions.createdAt;
16934
17773
  const orderFn = options.order === "asc" ? drizzleOrm.asc : drizzleOrm.desc;
16935
17774
  const limit = options.limit ?? 50;
@@ -17203,6 +18042,56 @@ var init_storage2 = __esm({
17203
18042
  return rows.map((row) => this.rowToAgent(row));
17204
18043
  }
17205
18044
  // ============================================================================
18045
+ // Stream Events (SSE Relay)
18046
+ // ============================================================================
18047
+ async appendEvents(sessionId, events) {
18048
+ if (events.length === 0) return [];
18049
+ const [maxSeqResult] = await this.db.select({ maxSeq: drizzleOrm.sql`COALESCE(MAX(${streamEvents.sequence}), 0)` }).from(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
18050
+ let nextSeq = (maxSeqResult?.maxSeq ?? 0) + 1;
18051
+ const rows = await this.db.insert(streamEvents).values(
18052
+ events.map((event) => ({
18053
+ sessionId,
18054
+ sequence: nextSeq++,
18055
+ eventType: event.eventType,
18056
+ payload: event.payload,
18057
+ batchCount: event.batchCount ?? 1
18058
+ }))
18059
+ ).returning();
18060
+ return rows.map((row) => this.rowToStoredStreamEvent(row));
18061
+ }
18062
+ async readEvents(sessionId, options) {
18063
+ const conditions = [drizzleOrm.eq(streamEvents.sessionId, sessionId)];
18064
+ if (options?.afterSequence !== void 0) {
18065
+ conditions.push(drizzleOrm.sql`${streamEvents.sequence} > ${options.afterSequence}`);
18066
+ }
18067
+ const limit = options?.limit ?? 1e3;
18068
+ const rows = await this.db.select().from(streamEvents).where(drizzleOrm.and(...conditions)).orderBy(drizzleOrm.asc(streamEvents.sequence)).limit(limit);
18069
+ return rows.map((row) => this.rowToStoredStreamEvent(row));
18070
+ }
18071
+ async getLatestSequence(sessionId) {
18072
+ const [result] = await this.db.select({ maxSeq: drizzleOrm.sql`COALESCE(MAX(${streamEvents.sequence}), 0)` }).from(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
18073
+ return result?.maxSeq ?? 0;
18074
+ }
18075
+ async deleteSessionEvents(sessionId) {
18076
+ await this.db.delete(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
18077
+ }
18078
+ async cleanupOldEvents(maxAgeMs) {
18079
+ const cutoff = new Date(Date.now() - maxAgeMs);
18080
+ const result = await this.db.delete(streamEvents).where(drizzleOrm.sql`${streamEvents.createdAt} < ${cutoff.toISOString()}`);
18081
+ return result.rowCount ?? 0;
18082
+ }
18083
+ rowToStoredStreamEvent(row) {
18084
+ return {
18085
+ id: row.id,
18086
+ sessionId: row.sessionId,
18087
+ sequence: row.sequence,
18088
+ eventType: row.eventType,
18089
+ payload: row.payload,
18090
+ batchCount: row.batchCount,
18091
+ createdAt: row.createdAt
18092
+ };
18093
+ }
18094
+ // ============================================================================
17206
18095
  // Database Access (for extensions)
17207
18096
  // ============================================================================
17208
18097
  /**
@@ -17601,6 +18490,9 @@ var init_storage3 = __esm({
17601
18490
  if (options.status) {
17602
18491
  query = query.eq("status", options.status);
17603
18492
  }
18493
+ if (options.resourceId) {
18494
+ query = query.eq("metadata->>resourceId", options.resourceId);
18495
+ }
17604
18496
  const orderColumn = options.orderBy === "updatedAt" ? "updated_at" : "created_at";
17605
18497
  const ascending = options.order === "asc";
17606
18498
  query = query.order(orderColumn, { ascending }).range(offset, offset + limit - 1);
@@ -17958,6 +18850,7 @@ var init_storage3 = __esm({
17958
18850
  sessionId: row.session_id,
17959
18851
  eventType: row.event_type,
17960
18852
  category: row.category,
18853
+ eventSource: row.event_source,
17961
18854
  startedAt: new Date(row.started_at),
17962
18855
  endedAt: row.ended_at ? new Date(row.ended_at) : void 0,
17963
18856
  durationMs: row.duration_ms ?? void 0,
@@ -17980,6 +18873,7 @@ var init_storage3 = __esm({
17980
18873
  session_id: sessionId,
17981
18874
  event_type: event.eventType,
17982
18875
  category: event.category,
18876
+ event_source: event.eventSource || "agent",
17983
18877
  started_at: event.startedAt.toISOString(),
17984
18878
  ended_at: event.endedAt?.toISOString() ?? null,
17985
18879
  duration_ms: event.durationMs ?? null,
@@ -18006,6 +18900,9 @@ var init_storage3 = __esm({
18006
18900
  if (options.eventType) {
18007
18901
  query = query.eq("event_type", options.eventType);
18008
18902
  }
18903
+ if (options.eventSource) {
18904
+ query = query.eq("event_source", options.eventSource);
18905
+ }
18009
18906
  if (options.filePath) {
18010
18907
  query = query.or(
18011
18908
  `event_data->>filePath.eq.${options.filePath},event_data->>canonicalPath.eq.${options.filePath},and(category.eq.tool,tool_name.in.(Read,Write,Edit),event_data->input->>file_path.eq.${options.filePath})`
@@ -18038,6 +18935,72 @@ var init_storage3 = __esm({
18038
18935
  }
18039
18936
  return data ? data.sequence_number + 1 : 1;
18040
18937
  }
18938
+ // ============================================================================
18939
+ // Stream Events (SSE Relay)
18940
+ // ============================================================================
18941
+ async appendEvents(sessionId, events) {
18942
+ if (events.length === 0) return [];
18943
+ const { data: maxSeqData } = await this.client.from("stream_events").select("sequence").eq("session_id", sessionId).order("sequence", { ascending: false }).limit(1).single();
18944
+ let nextSeq = maxSeqData ? maxSeqData.sequence + 1 : 1;
18945
+ const { data, error } = await this.client.from("stream_events").insert(
18946
+ events.map((event) => ({
18947
+ session_id: sessionId,
18948
+ sequence: nextSeq++,
18949
+ event_type: event.eventType,
18950
+ payload: event.payload,
18951
+ batch_count: event.batchCount ?? 1
18952
+ }))
18953
+ ).select();
18954
+ if (error) {
18955
+ throw new Error(`Failed to append stream events: ${error.message}`);
18956
+ }
18957
+ return data.map((row) => this.rowToStoredStreamEvent(row));
18958
+ }
18959
+ async readEvents(sessionId, options) {
18960
+ let query = this.client.from("stream_events").select("*").eq("session_id", sessionId);
18961
+ if (options?.afterSequence !== void 0) {
18962
+ query = query.gt("sequence", options.afterSequence);
18963
+ }
18964
+ const limit = options?.limit ?? 1e3;
18965
+ query = query.order("sequence", { ascending: true }).limit(limit);
18966
+ const { data, error } = await query;
18967
+ if (error) {
18968
+ throw new Error(`Failed to read stream events: ${error.message}`);
18969
+ }
18970
+ return data.map((row) => this.rowToStoredStreamEvent(row));
18971
+ }
18972
+ async getLatestSequence(sessionId) {
18973
+ const { data, error } = await this.client.from("stream_events").select("sequence").eq("session_id", sessionId).order("sequence", { ascending: false }).limit(1).single();
18974
+ if (error && error.code !== "PGRST116") {
18975
+ throw new Error(`Failed to get latest sequence: ${error.message}`);
18976
+ }
18977
+ return data ? data.sequence : 0;
18978
+ }
18979
+ async deleteStreamEvents(sessionId) {
18980
+ const { error } = await this.client.from("stream_events").delete().eq("session_id", sessionId);
18981
+ if (error) {
18982
+ throw new Error(`Failed to delete stream events: ${error.message}`);
18983
+ }
18984
+ }
18985
+ async cleanupOldEvents(maxAgeMs) {
18986
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
18987
+ const { data, error } = await this.client.from("stream_events").delete().lt("created_at", cutoff).select("id");
18988
+ if (error) {
18989
+ throw new Error(`Failed to cleanup old stream events: ${error.message}`);
18990
+ }
18991
+ return data?.length ?? 0;
18992
+ }
18993
+ rowToStoredStreamEvent(row) {
18994
+ return {
18995
+ id: row.id,
18996
+ sessionId: row.session_id,
18997
+ sequence: row.sequence,
18998
+ eventType: row.event_type,
18999
+ payload: row.payload,
19000
+ batchCount: row.batch_count,
19001
+ createdAt: new Date(row.created_at)
19002
+ };
19003
+ }
18041
19004
  };
18042
19005
  }
18043
19006
  });
@@ -18276,7 +19239,8 @@ var init_storage4 = __esm({
18276
19239
  limit: options?.limit,
18277
19240
  offset: options?.offset,
18278
19241
  agentName: options?.agentName,
18279
- status: options?.status
19242
+ status: options?.status,
19243
+ resourceId: options?.resourceId
18280
19244
  }
18281
19245
  }
18282
19246
  );
@@ -18870,6 +19834,7 @@ var init_src = __esm({
18870
19834
  init_credentials();
18871
19835
  init_skills();
18872
19836
  init_utils();
19837
+ init_relay();
18873
19838
  init_server();
18874
19839
  init_server2();
18875
19840
  init_schemas();
@@ -19046,7 +20011,9 @@ exports.shouldUseSandbox = shouldUseSandbox;
19046
20011
  exports.shutdownSandboxPool = shutdownSandboxPool;
19047
20012
  exports.sseMcpWithAuth = sseMcpWithAuth;
19048
20013
  exports.startServer = startServer;
20014
+ exports.streamEventRelay = streamEventRelay;
19049
20015
  exports.updateToolCallWithResult = updateToolCallWithResult;
20016
+ exports.warmSandboxForSession = warmSandboxForSession;
19050
20017
  exports.writeFileToSandbox = writeFileToSandbox;
19051
20018
  //# sourceMappingURL=index.cjs.map
19052
20019
  //# sourceMappingURL=index.cjs.map