@ash-cloud/ash-ai 0.1.16 → 0.1.17

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');
@@ -384,6 +385,85 @@ var init_errors = __esm({
384
385
  }
385
386
  });
386
387
 
388
+ // src/relay/stream-event-writer.ts
389
+ exports.StreamEventWriter = void 0;
390
+ var init_stream_event_writer = __esm({
391
+ "src/relay/stream-event-writer.ts"() {
392
+ exports.StreamEventWriter = class {
393
+ storage;
394
+ sessionId;
395
+ emitter;
396
+ flushIntervalMs;
397
+ maxBatchChars;
398
+ // Text delta batching state
399
+ pendingDelta = "";
400
+ pendingDeltaCount = 0;
401
+ flushTimer = null;
402
+ closed = false;
403
+ constructor(options) {
404
+ this.storage = options.storage;
405
+ this.sessionId = options.sessionId;
406
+ this.emitter = options.emitter;
407
+ this.flushIntervalMs = options.flushIntervalMs ?? 100;
408
+ this.maxBatchChars = options.maxBatchChars ?? 500;
409
+ }
410
+ /**
411
+ * Write a stream event. Text deltas are batched; all other events are written immediately.
412
+ */
413
+ async write(event) {
414
+ if (this.closed) return;
415
+ if (this.emitter) {
416
+ this.emitter.emit("event", event);
417
+ }
418
+ if (event.type === "text_delta") {
419
+ this.pendingDelta += event.delta;
420
+ this.pendingDeltaCount++;
421
+ if (!this.flushTimer) {
422
+ this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
423
+ }
424
+ if (this.pendingDelta.length >= this.maxBatchChars) {
425
+ await this.flush();
426
+ }
427
+ } else {
428
+ await this.flush();
429
+ await this.writeToStorage([{ eventType: event.type, payload: event, batchCount: 1 }]);
430
+ }
431
+ }
432
+ /**
433
+ * Flush any pending text_delta events to storage as a single batched event.
434
+ */
435
+ async flush() {
436
+ if (this.flushTimer) {
437
+ clearTimeout(this.flushTimer);
438
+ this.flushTimer = null;
439
+ }
440
+ if (this.pendingDelta.length === 0) return;
441
+ const batchedEvent = {
442
+ type: "text_delta",
443
+ delta: this.pendingDelta
444
+ };
445
+ const batchCount = this.pendingDeltaCount;
446
+ this.pendingDelta = "";
447
+ this.pendingDeltaCount = 0;
448
+ await this.writeToStorage([{ eventType: "text_delta", payload: batchedEvent, batchCount }]);
449
+ }
450
+ /**
451
+ * Close the writer, flushing any remaining events.
452
+ */
453
+ async close() {
454
+ await this.flush();
455
+ this.closed = true;
456
+ if (this.emitter) {
457
+ this.emitter.emit("done");
458
+ }
459
+ }
460
+ async writeToStorage(events) {
461
+ return this.storage.appendEvents(this.sessionId, events);
462
+ }
463
+ };
464
+ }
465
+ });
466
+
387
467
  // src/session/manager.ts
388
468
  exports.SessionManager = void 0;
389
469
  var init_manager = __esm({
@@ -1788,11 +1868,10 @@ var init_sandbox_logger = __esm({
1788
1868
  };
1789
1869
  }
1790
1870
  });
1791
-
1792
- // src/agent/harness.ts
1793
1871
  exports.AgentHarness = void 0;
1794
1872
  var init_harness = __esm({
1795
1873
  "src/agent/harness.ts"() {
1874
+ init_stream_event_writer();
1796
1875
  init_manager();
1797
1876
  init_claude_sdk();
1798
1877
  init_backend();
@@ -1809,6 +1888,8 @@ var init_harness = __esm({
1809
1888
  sessionSkillDirs = /* @__PURE__ */ new Map();
1810
1889
  /** Tracks active executions by session ID for stop/interrupt capability */
1811
1890
  activeExecutions = /* @__PURE__ */ new Map();
1891
+ /** EventEmitters for same-process SSE relay fast-path, keyed by session ID */
1892
+ sessionEmitters = /* @__PURE__ */ new Map();
1812
1893
  constructor(config) {
1813
1894
  this.name = config.name;
1814
1895
  this.config = config;
@@ -1956,6 +2037,20 @@ var init_harness = __esm({
1956
2037
  getSessionManager() {
1957
2038
  return this.sessionManager;
1958
2039
  }
2040
+ /**
2041
+ * Get the stream event storage (if configured)
2042
+ */
2043
+ getStreamEventStorage() {
2044
+ return this.config.streamEventStorage;
2045
+ }
2046
+ /**
2047
+ * Get the EventEmitter for a session's stream events.
2048
+ * Used by the relay endpoint for same-process fast-path.
2049
+ * Returns undefined if no active execution or no streamEventStorage configured.
2050
+ */
2051
+ getSessionEmitter(sessionId) {
2052
+ return this.sessionEmitters.get(sessionId);
2053
+ }
1959
2054
  async ensureInitialized() {
1960
2055
  if (!this.initialized) {
1961
2056
  await this.initialize();
@@ -1970,9 +2065,24 @@ var init_harness = __esm({
1970
2065
  async *send(prompt, options = {}) {
1971
2066
  const controller = new AbortController();
1972
2067
  self.activeExecutions.set(session.id, controller);
2068
+ let writer;
2069
+ let emitter;
2070
+ if (self.config.streamEventStorage) {
2071
+ emitter = new events.EventEmitter();
2072
+ self.sessionEmitters.set(session.id, emitter);
2073
+ writer = new exports.StreamEventWriter({
2074
+ storage: self.config.streamEventStorage,
2075
+ sessionId: session.id,
2076
+ emitter
2077
+ });
2078
+ }
1973
2079
  if (options.signal) {
1974
2080
  options.signal.addEventListener("abort", () => controller.abort(), { once: true });
1975
2081
  }
2082
+ const writeEvent = writer ? (event) => {
2083
+ writer.write(event).catch(() => {
2084
+ });
2085
+ } : void 0;
1976
2086
  let partialTextContent = "";
1977
2087
  const sandboxLogs = [];
1978
2088
  const logQueue = [];
@@ -1986,7 +2096,9 @@ var init_harness = __esm({
1986
2096
  const yieldQueuedLogs = async function* () {
1987
2097
  while (logQueue.length > 0) {
1988
2098
  const entry = logQueue.shift();
1989
- yield { type: "sandbox_log", entry };
2099
+ const event = { type: "sandbox_log", entry };
2100
+ writeEvent?.(event);
2101
+ yield event;
1990
2102
  }
1991
2103
  };
1992
2104
  logger3.info("execution", `Starting execution for session ${session.id}`);
@@ -2003,11 +2115,13 @@ var init_harness = __esm({
2003
2115
  if (self.hooks.onMessage && savedUserMessage) {
2004
2116
  await self.hooks.onMessage(savedUserMessage);
2005
2117
  }
2006
- yield {
2118
+ const sessionStartEvent = {
2007
2119
  type: "session_start",
2008
2120
  sessionId: session.id,
2009
2121
  sdkSessionId: session.sdkSessionId ?? ""
2010
2122
  };
2123
+ writeEvent?.(sessionStartEvent);
2124
+ yield sessionStartEvent;
2011
2125
  yield* yieldQueuedLogs();
2012
2126
  const assistantContent = [];
2013
2127
  let wasAborted = false;
@@ -2028,22 +2142,26 @@ var init_harness = __esm({
2028
2142
  }
2029
2143
  if (event.type === "text_delta" && event.delta) {
2030
2144
  partialTextContent += event.delta;
2031
- yield {
2145
+ const textDeltaEvent = {
2032
2146
  type: "text_delta",
2033
2147
  delta: event.delta
2034
2148
  };
2149
+ writeEvent?.(textDeltaEvent);
2150
+ yield textDeltaEvent;
2035
2151
  } else if (event.type === "thinking_delta" && event.delta) {
2036
- yield {
2152
+ const thinkingEvent = {
2037
2153
  type: "thinking_delta",
2038
2154
  delta: event.delta
2039
2155
  };
2156
+ writeEvent?.(thinkingEvent);
2157
+ yield thinkingEvent;
2040
2158
  } else if (event.type === "text") {
2041
2159
  const textContent = {
2042
2160
  type: "text",
2043
2161
  text: event.text
2044
2162
  };
2045
2163
  assistantContent.push(textContent);
2046
- yield {
2164
+ const messageEvent = {
2047
2165
  type: "message",
2048
2166
  message: {
2049
2167
  id: "",
@@ -2053,6 +2171,8 @@ var init_harness = __esm({
2053
2171
  createdAt: /* @__PURE__ */ new Date()
2054
2172
  }
2055
2173
  };
2174
+ writeEvent?.(messageEvent);
2175
+ yield messageEvent;
2056
2176
  } else if (event.type === "tool_use") {
2057
2177
  logger3.debug("execution", `Tool use: ${event.name}`, { toolId: event.id });
2058
2178
  const toolContent = {
@@ -2062,12 +2182,14 @@ var init_harness = __esm({
2062
2182
  input: event.input
2063
2183
  };
2064
2184
  assistantContent.push(toolContent);
2065
- yield {
2185
+ const toolUseEvent = {
2066
2186
  type: "tool_use",
2067
2187
  id: event.id,
2068
2188
  name: event.name,
2069
2189
  input: event.input
2070
2190
  };
2191
+ writeEvent?.(toolUseEvent);
2192
+ yield toolUseEvent;
2071
2193
  yield* yieldQueuedLogs();
2072
2194
  if (self.hooks.onToolUse) {
2073
2195
  await self.hooks.onToolUse(event.name, event.input, null);
@@ -2084,12 +2206,14 @@ var init_harness = __esm({
2084
2206
  isError: event.isError
2085
2207
  };
2086
2208
  assistantContent.push(resultContent);
2087
- yield {
2209
+ const toolResultEvent = {
2088
2210
  type: "tool_result",
2089
2211
  toolUseId: event.toolUseId,
2090
2212
  content: event.content,
2091
2213
  isError: event.isError
2092
2214
  };
2215
+ writeEvent?.(toolResultEvent);
2216
+ yield toolResultEvent;
2093
2217
  }
2094
2218
  }
2095
2219
  if (wasAborted || controller.signal.aborted) {
@@ -2118,19 +2242,23 @@ var init_harness = __esm({
2118
2242
  }
2119
2243
  });
2120
2244
  yield* yieldQueuedLogs();
2121
- yield {
2245
+ const stoppedEvent = {
2122
2246
  type: "session_stopped",
2123
2247
  sessionId: session.id,
2124
2248
  reason: "user_requested",
2125
2249
  partialContent: partialTextContent || void 0
2126
2250
  };
2251
+ writeEvent?.(stoppedEvent);
2252
+ yield stoppedEvent;
2127
2253
  return;
2128
2254
  }
2129
2255
  logger3.info("execution", "Execution completed successfully");
2130
- yield {
2256
+ const turnCompleteEvent = {
2131
2257
  type: "turn_complete",
2132
2258
  sessionId: session.id
2133
2259
  };
2260
+ writeEvent?.(turnCompleteEvent);
2261
+ yield turnCompleteEvent;
2134
2262
  if (assistantContent.length > 0) {
2135
2263
  const savedAssistantMessages = await self.sessionManager.saveMessages(
2136
2264
  session.id,
@@ -2154,11 +2282,13 @@ var init_harness = __esm({
2154
2282
  }
2155
2283
  });
2156
2284
  yield* yieldQueuedLogs();
2157
- yield {
2285
+ const sessionEndEvent = {
2158
2286
  type: "session_end",
2159
2287
  sessionId: session.id,
2160
2288
  status: "completed"
2161
2289
  };
2290
+ writeEvent?.(sessionEndEvent);
2291
+ yield sessionEndEvent;
2162
2292
  if (self.hooks.onSessionEnd) {
2163
2293
  const updatedSession = await self.sessionManager.getSession(session.id);
2164
2294
  if (updatedSession) {
@@ -2189,12 +2319,14 @@ var init_harness = __esm({
2189
2319
  }
2190
2320
  });
2191
2321
  yield* yieldQueuedLogs();
2192
- yield {
2322
+ const catchStoppedEvent = {
2193
2323
  type: "session_stopped",
2194
2324
  sessionId: session.id,
2195
2325
  reason: "user_requested",
2196
2326
  partialContent: partialTextContent || void 0
2197
2327
  };
2328
+ writeEvent?.(catchStoppedEvent);
2329
+ yield catchStoppedEvent;
2198
2330
  } else {
2199
2331
  logger3.error("execution", `Execution error: ${errorMessage}`);
2200
2332
  await self.sessionManager.updateSession(session.id, {
@@ -2212,13 +2344,19 @@ var init_harness = __esm({
2212
2344
  await self.hooks.onError(error, updatedSession);
2213
2345
  }
2214
2346
  }
2215
- yield {
2347
+ const errorEvent = {
2216
2348
  type: "error",
2217
2349
  error: errorMessage
2218
2350
  };
2351
+ writeEvent?.(errorEvent);
2352
+ yield errorEvent;
2219
2353
  }
2220
2354
  } finally {
2221
2355
  self.activeExecutions.delete(session.id);
2356
+ if (writer) {
2357
+ await writer.close();
2358
+ }
2359
+ self.sessionEmitters.delete(session.id);
2222
2360
  }
2223
2361
  },
2224
2362
  async getSession() {
@@ -2722,12 +2860,88 @@ var init_queue_memory = __esm({
2722
2860
  };
2723
2861
  }
2724
2862
  });
2863
+ exports.MemoryStreamEventStorage = void 0;
2864
+ var init_stream_event_memory = __esm({
2865
+ "src/storage/stream-event-memory.ts"() {
2866
+ exports.MemoryStreamEventStorage = class {
2867
+ events = /* @__PURE__ */ new Map();
2868
+ nextSequence = /* @__PURE__ */ new Map();
2869
+ async initialize() {
2870
+ }
2871
+ async close() {
2872
+ this.events.clear();
2873
+ this.nextSequence.clear();
2874
+ }
2875
+ async appendEvents(sessionId, events) {
2876
+ if (!this.events.has(sessionId)) {
2877
+ this.events.set(sessionId, []);
2878
+ }
2879
+ const sessionEvents2 = this.events.get(sessionId);
2880
+ let seq = this.nextSequence.get(sessionId) ?? 0;
2881
+ const stored = [];
2882
+ for (const event of events) {
2883
+ seq += 1;
2884
+ const storedEvent = {
2885
+ id: nanoid.nanoid(),
2886
+ sessionId,
2887
+ sequence: seq,
2888
+ eventType: event.eventType,
2889
+ payload: event.payload,
2890
+ batchCount: event.batchCount ?? 1,
2891
+ createdAt: /* @__PURE__ */ new Date()
2892
+ };
2893
+ sessionEvents2.push(storedEvent);
2894
+ stored.push(storedEvent);
2895
+ }
2896
+ this.nextSequence.set(sessionId, seq);
2897
+ return stored;
2898
+ }
2899
+ async readEvents(sessionId, options) {
2900
+ const sessionEvents2 = this.events.get(sessionId) ?? [];
2901
+ let result = sessionEvents2;
2902
+ if (options?.afterSequence !== void 0) {
2903
+ result = result.filter((e) => e.sequence > options.afterSequence);
2904
+ }
2905
+ if (options?.limit !== void 0) {
2906
+ result = result.slice(0, options.limit);
2907
+ }
2908
+ return result;
2909
+ }
2910
+ async getLatestSequence(sessionId) {
2911
+ return this.nextSequence.get(sessionId) ?? 0;
2912
+ }
2913
+ async deleteSessionEvents(sessionId) {
2914
+ this.events.delete(sessionId);
2915
+ this.nextSequence.delete(sessionId);
2916
+ }
2917
+ async cleanupOldEvents(maxAgeMs) {
2918
+ const cutoff = Date.now() - maxAgeMs;
2919
+ let deleted = 0;
2920
+ for (const [sessionId, sessionEvents2] of this.events) {
2921
+ const before = sessionEvents2.length;
2922
+ const remaining = sessionEvents2.filter(
2923
+ (e) => e.createdAt.getTime() >= cutoff
2924
+ );
2925
+ deleted += before - remaining.length;
2926
+ if (remaining.length === 0) {
2927
+ this.events.delete(sessionId);
2928
+ this.nextSequence.delete(sessionId);
2929
+ } else {
2930
+ this.events.set(sessionId, remaining);
2931
+ }
2932
+ }
2933
+ return deleted;
2934
+ }
2935
+ };
2936
+ }
2937
+ });
2725
2938
 
2726
2939
  // src/storage/index.ts
2727
2940
  var init_storage = __esm({
2728
2941
  "src/storage/index.ts"() {
2729
2942
  init_memory();
2730
2943
  init_queue_memory();
2944
+ init_stream_event_memory();
2731
2945
  }
2732
2946
  });
2733
2947
 
@@ -5182,10 +5396,11 @@ async function getOrCreateSandbox(options) {
5182
5396
  startupScriptRan: true,
5183
5397
  // Assume ran for reconnected sandboxes
5184
5398
  startupScriptHash: void 0,
5399
+ // Unknown — caller should not re-run based on hash mismatch alone
5185
5400
  isNew: false,
5186
5401
  configFileUrl: void 0,
5187
- configInstalledAt: void 0
5188
- // We don't know when config was installed on reconnected sandboxes
5402
+ configInstalledAt: now2
5403
+ // Assume config was installed — prevents unnecessary re-install on reconnection
5189
5404
  };
5190
5405
  } else {
5191
5406
  console.log("[SANDBOX] Reconnected sandbox failed health check, will create new");
@@ -5571,7 +5786,8 @@ async function* executeInSandbox(prompt, apiKey, options) {
5571
5786
  markSdkInstalled(sessionId);
5572
5787
  }
5573
5788
  const currentScriptHash = hashStartupScript(options.startupScript);
5574
- const needsStartupScript = options.startupScript && (!startupScriptRan || currentScriptHash !== cachedScriptHash);
5789
+ const scriptHashChanged = cachedScriptHash !== void 0 && currentScriptHash !== cachedScriptHash;
5790
+ const needsStartupScript = options.startupScript && (!startupScriptRan || scriptHashChanged);
5575
5791
  if (needsStartupScript) {
5576
5792
  console.log("[SANDBOX] Running startup script...");
5577
5793
  const startupStartTime = Date.now();
@@ -5705,11 +5921,18 @@ const { query } = require('@anthropic-ai/claude-agent-sdk');
5705
5921
  const prompt = ${JSON.stringify(prompt)};
5706
5922
  const options = ${JSON.stringify(sdkOptions)};
5707
5923
 
5924
+ let queryCompleted = false;
5925
+
5708
5926
  async function run() {
5709
5927
  try {
5710
5928
  for await (const message of query({ prompt, options })) {
5711
5929
  console.log(JSON.stringify(message));
5712
5930
  }
5931
+ queryCompleted = true;
5932
+ // Exit cleanly immediately after query completes.
5933
+ // MCP server cleanup (e.g. Playwright browser shutdown) can trigger
5934
+ // unhandled rejections after the query is done, causing spurious exit code 1.
5935
+ process.exit(0);
5713
5936
  } catch (error) {
5714
5937
  const errorInfo = {
5715
5938
  type: 'error',
@@ -5725,6 +5948,11 @@ async function run() {
5725
5948
  }
5726
5949
 
5727
5950
  process.on('unhandledRejection', (reason) => {
5951
+ // Ignore rejections from MCP server cleanup after query has completed
5952
+ if (queryCompleted) {
5953
+ process.exit(0);
5954
+ return;
5955
+ }
5728
5956
  const errorInfo = {
5729
5957
  type: 'error',
5730
5958
  error: reason instanceof Error ? reason.message : String(reason),
@@ -5735,6 +5963,11 @@ process.on('unhandledRejection', (reason) => {
5735
5963
  });
5736
5964
 
5737
5965
  process.on('uncaughtException', (error) => {
5966
+ // Ignore exceptions from MCP server cleanup after query has completed
5967
+ if (queryCompleted) {
5968
+ process.exit(0);
5969
+ return;
5970
+ }
5738
5971
  const errorInfo = {
5739
5972
  type: 'error',
5740
5973
  error: error.message || 'Unknown error',
@@ -6807,15 +7040,35 @@ var init_sandbox_file_sync = __esm({
6807
7040
  }
6808
7041
  }
6809
7042
  /**
6810
- * Send a file sync event webhook
7043
+ * Send a file sync event webhook.
7044
+ * Strips raw Buffer content (previousContent/newContent) and replaces with a
7045
+ * pre-signed S3 download URL when the file exists in storage.
6811
7046
  */
6812
7047
  async sendFileSyncWebhook(sessionId, event) {
7048
+ let downloadUrl;
7049
+ if (event.success && event.canonicalPath && !event.operation.startsWith("deleted")) {
7050
+ try {
7051
+ downloadUrl = await this.fileStore.getSignedUrl(sessionId, event.canonicalPath);
7052
+ } catch {
7053
+ }
7054
+ }
7055
+ const webhookEvent = {
7056
+ operation: event.operation,
7057
+ source: event.source,
7058
+ canonicalPath: event.canonicalPath,
7059
+ basePath: event.basePath,
7060
+ sandboxPath: event.sandboxPath,
7061
+ fileSize: event.fileSize,
7062
+ success: event.success,
7063
+ error: event.error,
7064
+ downloadUrl
7065
+ };
6813
7066
  const payload = {
6814
7067
  event: "file_sync",
6815
7068
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6816
7069
  sessionId,
6817
7070
  metadata: this.webhookConfig?.metadata,
6818
- fileSyncEvent: event
7071
+ fileSyncEvent: webhookEvent
6819
7072
  };
6820
7073
  await this.sendWebhook(payload, sessionId);
6821
7074
  }
@@ -9103,6 +9356,135 @@ var init_utils = __esm({
9103
9356
  init_sandbox_logger();
9104
9357
  }
9105
9358
  });
9359
+
9360
+ // src/relay/stream-event-relay.ts
9361
+ async function* streamEventRelay(options) {
9362
+ const {
9363
+ storage,
9364
+ sessionId,
9365
+ afterSequence = 0,
9366
+ emitter,
9367
+ pollIntervalMs = 100,
9368
+ heartbeatIntervalMs = 15e3,
9369
+ signal
9370
+ } = options;
9371
+ let lastSequence = afterSequence;
9372
+ let done = false;
9373
+ const catchUpEvents = await storage.readEvents(sessionId, {
9374
+ afterSequence: lastSequence,
9375
+ limit: 1e4
9376
+ });
9377
+ for (const stored of catchUpEvents) {
9378
+ if (signal?.aborted || done) return;
9379
+ lastSequence = stored.sequence;
9380
+ yield stored.payload;
9381
+ if (TERMINAL_EVENT_TYPES.has(stored.eventType)) {
9382
+ done = true;
9383
+ return;
9384
+ }
9385
+ }
9386
+ if (signal?.aborted) return;
9387
+ if (emitter) {
9388
+ yield* relayFromEmitter(emitter, signal, heartbeatIntervalMs);
9389
+ } else {
9390
+ yield* relayByPolling(storage, sessionId, lastSequence, pollIntervalMs, heartbeatIntervalMs, signal);
9391
+ }
9392
+ }
9393
+ async function* relayFromEmitter(emitter, signal, heartbeatIntervalMs = 15e3) {
9394
+ const queue = [];
9395
+ let resolve3 = null;
9396
+ const onEvent = (event) => {
9397
+ queue.push(event);
9398
+ if (resolve3) {
9399
+ resolve3();
9400
+ resolve3 = null;
9401
+ }
9402
+ };
9403
+ const onDone = () => {
9404
+ queue.push("done");
9405
+ if (resolve3) {
9406
+ resolve3();
9407
+ resolve3 = null;
9408
+ }
9409
+ };
9410
+ emitter.on("event", onEvent);
9411
+ emitter.on("done", onDone);
9412
+ const heartbeatTimer = setInterval(() => {
9413
+ if (resolve3) {
9414
+ resolve3();
9415
+ resolve3 = null;
9416
+ }
9417
+ }, heartbeatIntervalMs);
9418
+ try {
9419
+ while (!signal?.aborted) {
9420
+ while (queue.length > 0) {
9421
+ const item = queue.shift();
9422
+ if (item === "done") return;
9423
+ yield item;
9424
+ if (TERMINAL_EVENT_TYPES.has(item.type)) return;
9425
+ }
9426
+ await new Promise((r) => {
9427
+ resolve3 = r;
9428
+ if (signal) {
9429
+ signal.addEventListener("abort", () => {
9430
+ resolve3 = null;
9431
+ r();
9432
+ }, { once: true });
9433
+ }
9434
+ });
9435
+ }
9436
+ } finally {
9437
+ clearInterval(heartbeatTimer);
9438
+ emitter.off("event", onEvent);
9439
+ emitter.off("done", onDone);
9440
+ }
9441
+ }
9442
+ async function* relayByPolling(storage, sessionId, startSequence, pollIntervalMs, heartbeatIntervalMs, signal) {
9443
+ let lastSequence = startSequence;
9444
+ while (!signal?.aborted) {
9445
+ const events = await storage.readEvents(sessionId, {
9446
+ afterSequence: lastSequence,
9447
+ limit: 100
9448
+ });
9449
+ for (const stored of events) {
9450
+ if (signal?.aborted) return;
9451
+ lastSequence = stored.sequence;
9452
+ yield stored.payload;
9453
+ if (TERMINAL_EVENT_TYPES.has(stored.eventType)) {
9454
+ return;
9455
+ }
9456
+ }
9457
+ if (events.length === 0) {
9458
+ await new Promise((r) => {
9459
+ const timer = setTimeout(r, pollIntervalMs);
9460
+ if (signal) {
9461
+ signal.addEventListener("abort", () => {
9462
+ clearTimeout(timer);
9463
+ r();
9464
+ }, { once: true });
9465
+ }
9466
+ });
9467
+ }
9468
+ }
9469
+ }
9470
+ var TERMINAL_EVENT_TYPES;
9471
+ var init_stream_event_relay = __esm({
9472
+ "src/relay/stream-event-relay.ts"() {
9473
+ TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
9474
+ "session_end",
9475
+ "error",
9476
+ "session_stopped"
9477
+ ]);
9478
+ }
9479
+ });
9480
+
9481
+ // src/relay/index.ts
9482
+ var init_relay = __esm({
9483
+ "src/relay/index.ts"() {
9484
+ init_stream_event_writer();
9485
+ init_stream_event_relay();
9486
+ }
9487
+ });
9106
9488
  function broadcastToSession(sessionId, event) {
9107
9489
  const subscribers = sessionSubscribers.get(sessionId);
9108
9490
  if (subscribers && subscribers.size > 0) {
@@ -9235,6 +9617,8 @@ function createSessionsRouter(options) {
9235
9617
  agentName: session.agentName,
9236
9618
  startedAt: /* @__PURE__ */ new Date()
9237
9619
  });
9620
+ const hasStreamEventStorage = !!agent.getStreamEventStorage();
9621
+ let sendSequenceCounter = 0;
9238
9622
  return streaming.streamSSE(c, async (stream) => {
9239
9623
  try {
9240
9624
  for await (const event of activeSession.send(data.prompt, {
@@ -9246,7 +9630,9 @@ function createSessionsRouter(options) {
9246
9630
  if (controller.signal.aborted) {
9247
9631
  break;
9248
9632
  }
9633
+ sendSequenceCounter++;
9249
9634
  await stream.writeSSE({
9635
+ ...hasStreamEventStorage ? { id: String(sendSequenceCounter) } : {},
9250
9636
  event: event.type,
9251
9637
  data: JSON.stringify(event)
9252
9638
  });
@@ -9429,11 +9815,77 @@ function createSessionsRouter(options) {
9429
9815
  }
9430
9816
  });
9431
9817
  });
9818
+ router.get("/:sessionId/stream", async (c) => {
9819
+ const sessionId = c.req.param("sessionId");
9820
+ const afterParam = c.req.query("after");
9821
+ const afterSequence = afterParam ? parseInt(afterParam, 10) : 0;
9822
+ const session = await sessionManager.getSession(sessionId);
9823
+ if (!session) {
9824
+ return c.json({ error: "Session not found" }, 404);
9825
+ }
9826
+ const agent = agents2.get(session.agentName);
9827
+ if (!agent) {
9828
+ return c.json({ error: `Agent not found: ${session.agentName}` }, 404);
9829
+ }
9830
+ const streamEventStorage = agent.getStreamEventStorage();
9831
+ if (!streamEventStorage) {
9832
+ return c.json({ error: "Stream event storage not configured" }, 501);
9833
+ }
9834
+ return streaming.streamSSE(c, async (stream) => {
9835
+ const abortController = new AbortController();
9836
+ c.req.raw.signal.addEventListener("abort", () => {
9837
+ abortController.abort();
9838
+ });
9839
+ const heartbeatInterval = setInterval(async () => {
9840
+ try {
9841
+ await stream.writeSSE({
9842
+ event: "heartbeat",
9843
+ data: JSON.stringify({ type: "heartbeat", timestamp: Date.now() })
9844
+ });
9845
+ } catch {
9846
+ clearInterval(heartbeatInterval);
9847
+ }
9848
+ }, 15e3);
9849
+ let sequenceCounter = afterSequence;
9850
+ try {
9851
+ const sessionEmitter = agent.getSessionEmitter(sessionId);
9852
+ const relay = streamEventRelay({
9853
+ storage: streamEventStorage,
9854
+ sessionId,
9855
+ afterSequence,
9856
+ emitter: sessionEmitter,
9857
+ signal: abortController.signal
9858
+ });
9859
+ for await (const event of relay) {
9860
+ if (abortController.signal.aborted) break;
9861
+ sequenceCounter++;
9862
+ await stream.writeSSE({
9863
+ id: String(sequenceCounter),
9864
+ event: event.type,
9865
+ data: JSON.stringify(event)
9866
+ });
9867
+ }
9868
+ } catch (error) {
9869
+ if (!abortController.signal.aborted) {
9870
+ await stream.writeSSE({
9871
+ event: "error",
9872
+ data: JSON.stringify({
9873
+ type: "error",
9874
+ error: error instanceof Error ? error.message : "Unknown error"
9875
+ })
9876
+ });
9877
+ }
9878
+ } finally {
9879
+ clearInterval(heartbeatInterval);
9880
+ }
9881
+ });
9882
+ });
9432
9883
  return router;
9433
9884
  }
9434
9885
  var createSessionSchema, fileAttachmentSchema, sendMessageSchema, updateEnvSchema, listSessionsSchema, activeStreams, sessionSubscribers;
9435
9886
  var init_sessions = __esm({
9436
9887
  "src/server/routes/sessions.ts"() {
9888
+ init_stream_event_relay();
9437
9889
  createSessionSchema = zod.z.object({
9438
9890
  agentName: zod.z.string(),
9439
9891
  metadata: zod.z.record(zod.z.unknown()).optional(),
@@ -16154,9 +16606,11 @@ __export(schema_exports, {
16154
16606
  sessionEventsRelations: () => sessionEventsRelations,
16155
16607
  sessionStatusEnum: () => sessionStatusEnum,
16156
16608
  sessions: () => sessions,
16157
- sessionsRelations: () => sessionsRelations
16609
+ sessionsRelations: () => sessionsRelations,
16610
+ streamEvents: () => streamEvents,
16611
+ streamEventsRelations: () => streamEventsRelations
16158
16612
  });
16159
- var sessionStatusEnum, messageRoleEnum, agentStatusEnum, agentBackendEnum, queueItemStatusEnum, configDeploymentStatusEnum, configDeploymentTriggerEnum, eventCategoryEnum, sessions, messages, attachments, agents, queueItems, configDeployments, sessionEvents, sessionsRelations, sessionEventsRelations, messagesRelations, attachmentsRelations, configDeploymentsRelations, agentsRelations;
16613
+ var sessionStatusEnum, messageRoleEnum, agentStatusEnum, agentBackendEnum, queueItemStatusEnum, configDeploymentStatusEnum, configDeploymentTriggerEnum, eventCategoryEnum, sessions, messages, attachments, agents, queueItems, configDeployments, sessionEvents, streamEvents, sessionsRelations, sessionEventsRelations, messagesRelations, attachmentsRelations, configDeploymentsRelations, streamEventsRelations, agentsRelations;
16160
16614
  var init_schema = __esm({
16161
16615
  "src/storage-postgres/schema.ts"() {
16162
16616
  sessionStatusEnum = pgCore.pgEnum("session_status", [
@@ -16397,6 +16851,21 @@ var init_schema = __esm({
16397
16851
  pgCore.index("idx_session_events_category").on(table.sessionId, table.category)
16398
16852
  ]
16399
16853
  );
16854
+ streamEvents = pgCore.pgTable(
16855
+ "stream_events",
16856
+ {
16857
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
16858
+ sessionId: pgCore.uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
16859
+ sequence: pgCore.integer("sequence").notNull(),
16860
+ eventType: pgCore.text("event_type").notNull(),
16861
+ payload: pgCore.jsonb("payload").notNull().$type(),
16862
+ batchCount: pgCore.integer("batch_count").default(1).notNull(),
16863
+ createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
16864
+ },
16865
+ (table) => [
16866
+ pgCore.index("idx_stream_events_session_seq").on(table.sessionId, table.sequence)
16867
+ ]
16868
+ );
16400
16869
  sessionsRelations = drizzleOrm.relations(sessions, ({ one, many }) => ({
16401
16870
  parentSession: one(sessions, {
16402
16871
  fields: [sessions.parentSessionId],
@@ -16407,7 +16876,8 @@ var init_schema = __esm({
16407
16876
  relationName: "parentSession"
16408
16877
  }),
16409
16878
  messages: many(messages),
16410
- events: many(sessionEvents)
16879
+ events: many(sessionEvents),
16880
+ streamEvents: many(streamEvents)
16411
16881
  }));
16412
16882
  sessionEventsRelations = drizzleOrm.relations(sessionEvents, ({ one }) => ({
16413
16883
  session: one(sessions, {
@@ -16434,6 +16904,12 @@ var init_schema = __esm({
16434
16904
  references: [agents.id]
16435
16905
  })
16436
16906
  }));
16907
+ streamEventsRelations = drizzleOrm.relations(streamEvents, ({ one }) => ({
16908
+ session: one(sessions, {
16909
+ fields: [streamEvents.sessionId],
16910
+ references: [sessions.id]
16911
+ })
16912
+ }));
16437
16913
  agentsRelations = drizzleOrm.relations(agents, ({ many }) => ({
16438
16914
  configDeployments: many(configDeployments)
16439
16915
  }));
@@ -16621,6 +17097,20 @@ var init_storage2 = __esm({
16621
17097
  `);
16622
17098
  await this.db.execute(drizzleOrm.sql`
16623
17099
  ALTER TABLE agents ADD COLUMN IF NOT EXISTS backend agent_backend DEFAULT 'claude' NOT NULL;
17100
+ `);
17101
+ await this.db.execute(drizzleOrm.sql`
17102
+ CREATE TABLE IF NOT EXISTS stream_events (
17103
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17104
+ session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
17105
+ sequence INTEGER NOT NULL,
17106
+ event_type TEXT NOT NULL,
17107
+ payload JSONB NOT NULL,
17108
+ batch_count INTEGER NOT NULL DEFAULT 1,
17109
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17110
+ );
17111
+ `);
17112
+ await this.db.execute(drizzleOrm.sql`
17113
+ CREATE INDEX IF NOT EXISTS idx_stream_events_session_seq ON stream_events(session_id, sequence);
16624
17114
  `);
16625
17115
  }
16626
17116
  /**
@@ -16813,6 +17303,20 @@ var init_storage2 = __esm({
16813
17303
  `);
16814
17304
  await this.db.execute(drizzleOrm.sql`
16815
17305
  CREATE INDEX IF NOT EXISTS idx_queue_items_pending ON queue_items(agent_name, session_id, status, priority, created_at);
17306
+ `);
17307
+ await this.db.execute(drizzleOrm.sql`
17308
+ CREATE TABLE IF NOT EXISTS stream_events (
17309
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17310
+ session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
17311
+ sequence INTEGER NOT NULL,
17312
+ event_type TEXT NOT NULL,
17313
+ payload JSONB NOT NULL,
17314
+ batch_count INTEGER NOT NULL DEFAULT 1,
17315
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17316
+ );
17317
+ `);
17318
+ await this.db.execute(drizzleOrm.sql`
17319
+ CREATE INDEX IF NOT EXISTS idx_stream_events_session_seq ON stream_events(session_id, sequence);
16816
17320
  `);
16817
17321
  await this.db.execute(drizzleOrm.sql`
16818
17322
  DO $$ BEGIN
@@ -17203,6 +17707,56 @@ var init_storage2 = __esm({
17203
17707
  return rows.map((row) => this.rowToAgent(row));
17204
17708
  }
17205
17709
  // ============================================================================
17710
+ // Stream Events (SSE Relay)
17711
+ // ============================================================================
17712
+ async appendEvents(sessionId, events) {
17713
+ if (events.length === 0) return [];
17714
+ const [maxSeqResult] = await this.db.select({ maxSeq: drizzleOrm.sql`COALESCE(MAX(${streamEvents.sequence}), 0)` }).from(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
17715
+ let nextSeq = (maxSeqResult?.maxSeq ?? 0) + 1;
17716
+ const rows = await this.db.insert(streamEvents).values(
17717
+ events.map((event) => ({
17718
+ sessionId,
17719
+ sequence: nextSeq++,
17720
+ eventType: event.eventType,
17721
+ payload: event.payload,
17722
+ batchCount: event.batchCount ?? 1
17723
+ }))
17724
+ ).returning();
17725
+ return rows.map((row) => this.rowToStoredStreamEvent(row));
17726
+ }
17727
+ async readEvents(sessionId, options) {
17728
+ const conditions = [drizzleOrm.eq(streamEvents.sessionId, sessionId)];
17729
+ if (options?.afterSequence !== void 0) {
17730
+ conditions.push(drizzleOrm.sql`${streamEvents.sequence} > ${options.afterSequence}`);
17731
+ }
17732
+ const limit = options?.limit ?? 1e3;
17733
+ const rows = await this.db.select().from(streamEvents).where(drizzleOrm.and(...conditions)).orderBy(drizzleOrm.asc(streamEvents.sequence)).limit(limit);
17734
+ return rows.map((row) => this.rowToStoredStreamEvent(row));
17735
+ }
17736
+ async getLatestSequence(sessionId) {
17737
+ const [result] = await this.db.select({ maxSeq: drizzleOrm.sql`COALESCE(MAX(${streamEvents.sequence}), 0)` }).from(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
17738
+ return result?.maxSeq ?? 0;
17739
+ }
17740
+ async deleteSessionEvents(sessionId) {
17741
+ await this.db.delete(streamEvents).where(drizzleOrm.eq(streamEvents.sessionId, sessionId));
17742
+ }
17743
+ async cleanupOldEvents(maxAgeMs) {
17744
+ const cutoff = new Date(Date.now() - maxAgeMs);
17745
+ const result = await this.db.delete(streamEvents).where(drizzleOrm.sql`${streamEvents.createdAt} < ${cutoff.toISOString()}`);
17746
+ return result.rowCount ?? 0;
17747
+ }
17748
+ rowToStoredStreamEvent(row) {
17749
+ return {
17750
+ id: row.id,
17751
+ sessionId: row.sessionId,
17752
+ sequence: row.sequence,
17753
+ eventType: row.eventType,
17754
+ payload: row.payload,
17755
+ batchCount: row.batchCount,
17756
+ createdAt: row.createdAt
17757
+ };
17758
+ }
17759
+ // ============================================================================
17206
17760
  // Database Access (for extensions)
17207
17761
  // ============================================================================
17208
17762
  /**
@@ -18038,6 +18592,72 @@ var init_storage3 = __esm({
18038
18592
  }
18039
18593
  return data ? data.sequence_number + 1 : 1;
18040
18594
  }
18595
+ // ============================================================================
18596
+ // Stream Events (SSE Relay)
18597
+ // ============================================================================
18598
+ async appendEvents(sessionId, events) {
18599
+ if (events.length === 0) return [];
18600
+ const { data: maxSeqData } = await this.client.from("stream_events").select("sequence").eq("session_id", sessionId).order("sequence", { ascending: false }).limit(1).single();
18601
+ let nextSeq = maxSeqData ? maxSeqData.sequence + 1 : 1;
18602
+ const { data, error } = await this.client.from("stream_events").insert(
18603
+ events.map((event) => ({
18604
+ session_id: sessionId,
18605
+ sequence: nextSeq++,
18606
+ event_type: event.eventType,
18607
+ payload: event.payload,
18608
+ batch_count: event.batchCount ?? 1
18609
+ }))
18610
+ ).select();
18611
+ if (error) {
18612
+ throw new Error(`Failed to append stream events: ${error.message}`);
18613
+ }
18614
+ return data.map((row) => this.rowToStoredStreamEvent(row));
18615
+ }
18616
+ async readEvents(sessionId, options) {
18617
+ let query = this.client.from("stream_events").select("*").eq("session_id", sessionId);
18618
+ if (options?.afterSequence !== void 0) {
18619
+ query = query.gt("sequence", options.afterSequence);
18620
+ }
18621
+ const limit = options?.limit ?? 1e3;
18622
+ query = query.order("sequence", { ascending: true }).limit(limit);
18623
+ const { data, error } = await query;
18624
+ if (error) {
18625
+ throw new Error(`Failed to read stream events: ${error.message}`);
18626
+ }
18627
+ return data.map((row) => this.rowToStoredStreamEvent(row));
18628
+ }
18629
+ async getLatestSequence(sessionId) {
18630
+ const { data, error } = await this.client.from("stream_events").select("sequence").eq("session_id", sessionId).order("sequence", { ascending: false }).limit(1).single();
18631
+ if (error && error.code !== "PGRST116") {
18632
+ throw new Error(`Failed to get latest sequence: ${error.message}`);
18633
+ }
18634
+ return data ? data.sequence : 0;
18635
+ }
18636
+ async deleteStreamEvents(sessionId) {
18637
+ const { error } = await this.client.from("stream_events").delete().eq("session_id", sessionId);
18638
+ if (error) {
18639
+ throw new Error(`Failed to delete stream events: ${error.message}`);
18640
+ }
18641
+ }
18642
+ async cleanupOldEvents(maxAgeMs) {
18643
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
18644
+ const { data, error } = await this.client.from("stream_events").delete().lt("created_at", cutoff).select("id");
18645
+ if (error) {
18646
+ throw new Error(`Failed to cleanup old stream events: ${error.message}`);
18647
+ }
18648
+ return data?.length ?? 0;
18649
+ }
18650
+ rowToStoredStreamEvent(row) {
18651
+ return {
18652
+ id: row.id,
18653
+ sessionId: row.session_id,
18654
+ sequence: row.sequence,
18655
+ eventType: row.event_type,
18656
+ payload: row.payload,
18657
+ batchCount: row.batch_count,
18658
+ createdAt: new Date(row.created_at)
18659
+ };
18660
+ }
18041
18661
  };
18042
18662
  }
18043
18663
  });
@@ -18870,6 +19490,7 @@ var init_src = __esm({
18870
19490
  init_credentials();
18871
19491
  init_skills();
18872
19492
  init_utils();
19493
+ init_relay();
18873
19494
  init_server();
18874
19495
  init_server2();
18875
19496
  init_schemas();
@@ -19046,6 +19667,7 @@ exports.shouldUseSandbox = shouldUseSandbox;
19046
19667
  exports.shutdownSandboxPool = shutdownSandboxPool;
19047
19668
  exports.sseMcpWithAuth = sseMcpWithAuth;
19048
19669
  exports.startServer = startServer;
19670
+ exports.streamEventRelay = streamEventRelay;
19049
19671
  exports.updateToolCallWithResult = updateToolCallWithResult;
19050
19672
  exports.writeFileToSandbox = writeFileToSandbox;
19051
19673
  //# sourceMappingURL=index.cjs.map