@agentuity/runtime 0.0.99 → 0.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/_metadata.d.ts +107 -0
  2. package/dist/_metadata.d.ts.map +1 -0
  3. package/dist/_metadata.js +179 -0
  4. package/dist/_metadata.js.map +1 -0
  5. package/dist/_process-protection.d.ts.map +1 -1
  6. package/dist/_process-protection.js +4 -0
  7. package/dist/_process-protection.js.map +1 -1
  8. package/dist/_services.d.ts.map +1 -1
  9. package/dist/_services.js +18 -17
  10. package/dist/_services.js.map +1 -1
  11. package/dist/_standalone.d.ts.map +1 -1
  12. package/dist/_standalone.js +17 -0
  13. package/dist/_standalone.js.map +1 -1
  14. package/dist/agent.d.ts +3 -3
  15. package/dist/agent.d.ts.map +1 -1
  16. package/dist/agent.js +53 -12
  17. package/dist/agent.js.map +1 -1
  18. package/dist/app.d.ts +0 -10
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js.map +1 -1
  21. package/dist/devmode.d.ts.map +1 -1
  22. package/dist/devmode.js +13 -5
  23. package/dist/devmode.js.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/middleware.d.ts +2 -2
  29. package/dist/middleware.d.ts.map +1 -1
  30. package/dist/middleware.js +59 -1
  31. package/dist/middleware.js.map +1 -1
  32. package/dist/services/evalrun/http.d.ts.map +1 -1
  33. package/dist/services/evalrun/http.js +14 -4
  34. package/dist/services/evalrun/http.js.map +1 -1
  35. package/dist/services/session/http.d.ts.map +1 -1
  36. package/dist/services/session/http.js +7 -0
  37. package/dist/services/session/http.js.map +1 -1
  38. package/dist/services/session/local.d.ts +2 -2
  39. package/dist/services/session/local.d.ts.map +1 -1
  40. package/dist/services/session/local.js +5 -4
  41. package/dist/services/session/local.js.map +1 -1
  42. package/dist/session.d.ts +30 -4
  43. package/dist/session.d.ts.map +1 -1
  44. package/dist/session.js +90 -13
  45. package/dist/session.js.map +1 -1
  46. package/dist/workbench.d.ts.map +1 -1
  47. package/dist/workbench.js +13 -20
  48. package/dist/workbench.js.map +1 -1
  49. package/package.json +5 -5
  50. package/src/_metadata.ts +307 -0
  51. package/src/_process-protection.ts +6 -0
  52. package/src/_services.ts +23 -21
  53. package/src/_standalone.ts +22 -0
  54. package/src/agent.ts +66 -14
  55. package/src/app.ts +0 -9
  56. package/src/devmode.ts +16 -5
  57. package/src/index.ts +5 -1
  58. package/src/middleware.ts +75 -4
  59. package/src/services/evalrun/http.ts +15 -4
  60. package/src/services/session/http.ts +11 -0
  61. package/src/services/session/local.ts +9 -4
  62. package/src/session.ts +142 -13
  63. package/src/workbench.ts +13 -26
package/src/app.ts CHANGED
@@ -15,11 +15,6 @@ import type { Email } from './io/email';
15
15
  import type { ThreadProvider, SessionProvider, Session, Thread } from './session';
16
16
  import type WaitUntilHandler from './_waituntil';
17
17
 
18
- // TODO: This should be imported from workbench package, but causes circular dependency
19
- export interface WorkbenchInstance {
20
- config: { route?: string; headers?: Record<string, string> };
21
- }
22
-
23
18
  type CorsOptions = Parameters<typeof cors>[0];
24
19
 
25
20
  export interface AppConfig<TAppState = Record<string, never>> {
@@ -63,10 +58,6 @@ export interface AppConfig<TAppState = Record<string, never>> {
63
58
  * the EvalRunEventProvider to override instead of the default
64
59
  */
65
60
  evalRunEvent?: EvalRunEventProvider;
66
- /**
67
- * the Workbench to override instead of the default
68
- */
69
- workbench?: WorkbenchInstance;
70
61
  };
71
62
  /**
72
63
  * Optional setup function called before server starts
package/src/devmode.ts CHANGED
@@ -78,12 +78,23 @@ const overlay = `
78
78
  </style>
79
79
  `;
80
80
 
81
+ // Global controller to avoid registering multiple SIGINT listeners
82
+ let globalController: AbortController | undefined;
83
+ let globalSigintHandler: (() => void) | undefined;
84
+
81
85
  export function registerDevModeRoutes(router: Hono) {
82
- const controller = new AbortController();
83
- const signal = controller.signal;
84
- process.on('SIGINT', () => {
85
- controller.abort();
86
- });
86
+ // Reuse existing controller or create new one
87
+ if (!globalController) {
88
+ globalController = new AbortController();
89
+
90
+ // Only register SIGINT handler once
91
+ globalSigintHandler = () => {
92
+ globalController?.abort();
93
+ };
94
+ process.on('SIGINT', globalSigintHandler);
95
+ }
96
+
97
+ const signal = globalController.signal;
87
98
  router.get('/__dev__/reload', () => {
88
99
  const stream = new ReadableStream({
89
100
  start(controller): void {
package/src/index.ts CHANGED
@@ -27,7 +27,6 @@ export {
27
27
 
28
28
  // app.ts exports (all app-related functionality)
29
29
  export {
30
- type WorkbenchInstance,
31
30
  type AppConfig,
32
31
  type Variables,
33
32
  type TriggerType,
@@ -193,3 +192,8 @@ export type { RouteSchema, GetRouteSchema } from './_validation';
193
192
  */
194
193
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
195
194
  export interface AppState {}
195
+
196
+ // Re-export bootstrapRuntimeEnv from @agentuity/server for convenience
197
+ // This allows generated code to import from @agentuity/runtime instead of having
198
+ // a direct dependency on @agentuity/server
199
+ export { bootstrapRuntimeEnv, type RuntimeBootstrapOptions } from '@agentuity/server';
package/src/middleware.ts CHANGED
@@ -22,6 +22,8 @@ import {
22
22
  } from '@opentelemetry/api';
23
23
  import { TraceState } from '@opentelemetry/core';
24
24
  import * as runtimeConfig from './_config';
25
+ import { getSessionEventProvider } from './_services';
26
+ import { internal } from './logger/internal';
25
27
 
26
28
  const SESSION_HEADER = 'x-session-id';
27
29
  const THREAD_HEADER = 'x-thread-id';
@@ -74,7 +76,8 @@ export interface MiddlewareConfig {
74
76
  * Create base middleware that sets up context variables
75
77
  */
76
78
  export function createBaseMiddleware(config: MiddlewareConfig) {
77
- return createMiddleware<Env>(async (c, next) => {
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ return createMiddleware<Env<any>>(async (c, next) => {
78
81
  c.set('logger', config.logger);
79
82
  c.set('tracer', config.tracer);
80
83
  c.set('meter', config.meter);
@@ -154,7 +157,8 @@ export function createCorsMiddleware(corsOptions?: Parameters<typeof cors>[0]) {
154
157
  * This is the critical middleware that creates AgentContext
155
158
  */
156
159
  export function createOtelMiddleware() {
157
- return createMiddleware<Env>(async (c, next) => {
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ return createMiddleware<Env<any>>(async (c, next) => {
158
162
  // Import providers dynamically to avoid circular deps
159
163
  const { getThreadProvider, getSessionProvider } = await import('./_services');
160
164
  const WaitUntilHandler = (await import('./_waituntil')).default;
@@ -188,6 +192,14 @@ export function createOtelMiddleware() {
188
192
  const deploymentId = runtimeConfig.getDeploymentId();
189
193
  const isDevMode = runtimeConfig.isDevMode();
190
194
 
195
+ internal.info(
196
+ '[session] config: orgId=%s, projectId=%s, deploymentId=%s, isDevMode=%s',
197
+ orgId ?? 'NOT SET (AGENTUITY_CLOUD_ORG_ID)',
198
+ projectId ?? 'NOT SET (AGENTUITY_CLOUD_PROJECT_ID)',
199
+ deploymentId ?? 'none',
200
+ isDevMode
201
+ );
202
+
191
203
  if (projectId) traceState = traceState.set('pid', projectId);
192
204
  if (orgId) traceState = traceState.set('oid', orgId);
193
205
  if (isDevMode) traceState = traceState.set('d', '1');
@@ -211,17 +223,45 @@ export function createOtelMiddleware() {
211
223
  c.set('session', session);
212
224
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
225
  (c as any).set('waitUntilHandler', handler);
226
+ const agentIds = new Set<string>();
214
227
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- (c as any).set('agentIds', new Set<string>());
228
+ (c as any).set('agentIds', agentIds);
216
229
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
230
  (c as any).set('trigger', 'api');
218
231
 
232
+ // Send session start event (so evalruns can reference this session)
233
+ const sessionEventProvider = getSessionEventProvider();
234
+ const shouldSendSession = !!(orgId && projectId);
235
+ if (shouldSendSession && sessionEventProvider) {
236
+ try {
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ const routeId = (c as any).var?.routeId || '';
239
+ await sessionEventProvider.start({
240
+ id: sessionId,
241
+ threadId: thread.id,
242
+ orgId,
243
+ projectId,
244
+ deploymentId: deploymentId || undefined,
245
+ devmode: isDevMode,
246
+ trigger: 'api',
247
+ routeId,
248
+ environment: runtimeConfig.getEnvironment(),
249
+ url: c.req.path,
250
+ method: c.req.method,
251
+ });
252
+ } catch (_ex) {
253
+ // Silently ignore session start errors - don't block request
254
+ }
255
+ }
256
+
219
257
  try {
220
258
  await next();
221
-
222
259
  // Save session/thread and send events
260
+ internal.info('[session] saving session %s (thread: %s)', sessionId, thread.id);
223
261
  await sessionProvider.save(session);
262
+ internal.info('[session] session saved, now saving thread');
224
263
  await threadProvider.save(thread);
264
+ internal.info('[session] thread saved');
225
265
  span.setStatus({ code: SpanStatusCode.OK });
226
266
  } catch (ex) {
227
267
  if (ex instanceof Error) {
@@ -233,6 +273,37 @@ export function createOtelMiddleware() {
233
273
  });
234
274
  throw ex;
235
275
  } finally {
276
+ // Send session complete event
277
+ internal.info(
278
+ '[session] shouldSendSession: %s, hasSessionEventProvider: %s',
279
+ shouldSendSession,
280
+ !!sessionEventProvider
281
+ );
282
+ if (shouldSendSession && sessionEventProvider) {
283
+ try {
284
+ const userData = session.serializeUserData();
285
+ internal.info(
286
+ '[session] sending session complete event, userData: %s',
287
+ userData ? `${userData.length} bytes` : 'none'
288
+ );
289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
+ const agentIdsSet = (c as any).get('agentIds') as Set<string> | undefined;
291
+ const agentIds = agentIdsSet ? [...agentIdsSet].filter(Boolean) : undefined;
292
+ internal.info('[session] agentIds: %o', agentIds);
293
+ await sessionEventProvider.complete({
294
+ id: sessionId,
295
+ threadId: thread.empty() ? null : thread.id,
296
+ statusCode: c.res?.status ?? 200,
297
+ agentIds: agentIds?.length ? agentIds : undefined,
298
+ userData,
299
+ });
300
+ internal.info('[session] session complete event sent');
301
+ } catch (ex) {
302
+ internal.info('[session] session complete event failed: %s', ex);
303
+ // Silently ignore session complete errors - don't block response
304
+ }
305
+ }
306
+
236
307
  const headers: Record<string, string> = {};
237
308
  propagation.inject(context.active(), headers);
238
309
  for (const key of Object.keys(headers)) {
@@ -13,6 +13,7 @@ import {
13
13
  type Logger,
14
14
  StructuredError,
15
15
  } from '@agentuity/core';
16
+ import { internal } from '../../logger/internal';
16
17
 
17
18
  const EvalRunResponseError = StructuredError('EvalRunResponseError');
18
19
 
@@ -38,12 +39,22 @@ export class HTTPEvalRunEventProvider implements EvalRunEventProvider {
38
39
  async start(event: EvalRunStartEvent): Promise<void> {
39
40
  const endpoint = '/evalrun/2025-03-17';
40
41
  const fullUrl = `${this.baseUrl}${endpoint}`;
41
- this.logger.debug('[EVALRUN HTTP] Sending eval run start event: %s', event.id);
42
- this.logger.debug('[EVALRUN HTTP] URL: %s %s', 'POST', fullUrl);
43
- this.logger.debug('[EVALRUN HTTP] Base URL: %s', this.baseUrl);
44
42
 
45
43
  const payload = { ...event, timestamp: Date.now() };
46
- this.logger.debug('[EVALRUN HTTP] Start event payload: %s', JSON.stringify(payload, null, 2));
44
+
45
+ // Log full payload using internal logger
46
+ internal.info('[EVALRUN HTTP] ========== START PAYLOAD ==========');
47
+ internal.info('[EVALRUN HTTP] id: %s', payload.id);
48
+ internal.info('[EVALRUN HTTP] evalId: %s', payload.evalId);
49
+ internal.info('[EVALRUN HTTP] evalIdentifier: %s', payload.evalIdentifier);
50
+ internal.info('[EVALRUN HTTP] sessionId: %s', payload.sessionId);
51
+ internal.info('[EVALRUN HTTP] orgId: %s', payload.orgId);
52
+ internal.info('[EVALRUN HTTP] projectId: %s', payload.projectId);
53
+ internal.info('[EVALRUN HTTP] devmode: %s', payload.devmode);
54
+ internal.info('[EVALRUN HTTP] deploymentId: %s', payload.deploymentId);
55
+ internal.info('[EVALRUN HTTP] spanId: %s', payload.spanId);
56
+ internal.info('[EVALRUN HTTP] URL: POST %s', fullUrl);
57
+ internal.info('[EVALRUN HTTP] ============================================');
47
58
 
48
59
  try {
49
60
  const resp = await this.apiClient.post(
@@ -8,6 +8,7 @@ import {
8
8
  type Logger,
9
9
  StructuredError,
10
10
  } from '@agentuity/core';
11
+ import { internal } from '../../logger/internal';
11
12
 
12
13
  const SessionResponseError = StructuredError('SessionResponseError');
13
14
 
@@ -29,6 +30,7 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
29
30
  * @param event SessionStartEvent
30
31
  */
31
32
  async start(event: SessionStartEvent): Promise<void> {
33
+ internal.info('[session-http] sending start event: %s', event.id);
32
34
  this.logger.debug('Sending session start event: %s', event.id);
33
35
  const resp = await this.apiClient.post(
34
36
  '/session/2025-03-17',
@@ -37,9 +39,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
37
39
  SessionStartEventDelayedSchema
38
40
  );
39
41
  if (resp.success) {
42
+ internal.info('[session-http] start event sent successfully: %s', event.id);
40
43
  this.logger.debug('Session start event sent successfully: %s', event.id);
41
44
  return;
42
45
  }
46
+ internal.info('[session-http] start event failed: %s - %s', event.id, resp.message);
43
47
  throw new SessionResponseError({ message: resp.message });
44
48
  }
45
49
 
@@ -49,6 +53,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
49
53
  * @param event SessionCompleteEvent
50
54
  */
51
55
  async complete(event: SessionCompleteEvent): Promise<void> {
56
+ internal.info(
57
+ '[session-http] sending complete event: %s, userData: %s',
58
+ event.id,
59
+ event.userData ? `${event.userData.length} bytes` : 'none'
60
+ );
52
61
  this.logger.debug('Sending session complete event: %s', event.id);
53
62
  const resp = await this.apiClient.put(
54
63
  '/session/2025-03-17',
@@ -57,9 +66,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
57
66
  SessionCompleteEventDelayedSchema
58
67
  );
59
68
  if (resp.success) {
69
+ internal.info('[session-http] complete event sent successfully: %s', event.id);
60
70
  this.logger.debug('Session complete event sent successfully: %s', event.id);
61
71
  return;
62
72
  }
73
+ internal.info('[session-http] complete event failed: %s - %s', event.id, resp.message);
63
74
  throw new SessionResponseError({ message: resp.message });
64
75
  }
65
76
  }
@@ -3,6 +3,7 @@ import {
3
3
  type SessionStartEvent,
4
4
  type SessionCompleteEvent,
5
5
  } from '@agentuity/core';
6
+ import { internal } from '../../logger/internal';
6
7
 
7
8
  /**
8
9
  * An implementation of the SessionEventProvider which is no-op
@@ -13,8 +14,8 @@ export class LocalSessionEventProvider implements SessionEventProvider {
13
14
  *
14
15
  * @param event SessionStartEvent
15
16
  */
16
- async start(_event: SessionStartEvent): Promise<void> {
17
- // no op
17
+ async start(event: SessionStartEvent): Promise<void> {
18
+ internal.info('[session-local] start event (no-op): %s', event.id);
18
19
  }
19
20
 
20
21
  /**
@@ -22,7 +23,11 @@ export class LocalSessionEventProvider implements SessionEventProvider {
22
23
  *
23
24
  * @param event SessionCompleteEvent
24
25
  */
25
- async complete(_event: SessionCompleteEvent): Promise<void> {
26
- // no op
26
+ async complete(event: SessionCompleteEvent): Promise<void> {
27
+ internal.info(
28
+ '[session-local] complete event (no-op): %s, userData: %s',
29
+ event.id,
30
+ event.userData ? `${event.userData.length} bytes` : 'none'
31
+ );
27
32
  }
28
33
  }
package/src/session.ts CHANGED
@@ -72,6 +72,19 @@ export interface Thread {
72
72
  */
73
73
  state: Map<string, unknown>;
74
74
 
75
+ /**
76
+ * Unencrypted metadata for filtering and querying threads.
77
+ * Unlike state, metadata is stored as-is in the database with GIN indexes
78
+ * for efficient filtering. Initialized to empty object, only persisted if non-empty.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * ctx.thread.metadata.userId = 'user123';
83
+ * ctx.thread.metadata.department = 'sales';
84
+ * ```
85
+ */
86
+ metadata: Record<string, unknown>;
87
+
75
88
  /**
76
89
  * Register an event listener for when the thread is destroyed.
77
90
  * Thread is destroyed when it expires or is manually destroyed.
@@ -182,6 +195,19 @@ export interface Session {
182
195
  */
183
196
  state: Map<string, unknown>;
184
197
 
198
+ /**
199
+ * Unencrypted metadata for filtering and querying sessions.
200
+ * Unlike state, metadata is stored as-is in the database with GIN indexes
201
+ * for efficient filtering. Initialized to empty object, only persisted if non-empty.
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * ctx.session.metadata.userId = 'user123';
206
+ * ctx.session.metadata.requestType = 'chat';
207
+ * ```
208
+ */
209
+ metadata: Record<string, unknown>;
210
+
185
211
  /**
186
212
  * Register an event listener for when the session completes.
187
213
  * Fired after the agent handler returns and response is sent.
@@ -653,13 +679,20 @@ export class DefaultThread implements Thread {
653
679
  #initialStateJson: string | undefined;
654
680
  readonly id: string;
655
681
  readonly state: Map<string, unknown>;
682
+ metadata: Record<string, unknown>;
656
683
  private provider: ThreadProvider;
657
684
 
658
- constructor(provider: ThreadProvider, id: string, initialStateJson?: string) {
685
+ constructor(
686
+ provider: ThreadProvider,
687
+ id: string,
688
+ initialStateJson?: string,
689
+ metadata?: Record<string, unknown>
690
+ ) {
659
691
  this.provider = provider;
660
692
  this.id = id;
661
693
  this.state = new Map();
662
694
  this.#initialStateJson = initialStateJson;
695
+ this.metadata = metadata || {};
663
696
  }
664
697
 
665
698
  addEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void {
@@ -707,10 +740,10 @@ export class DefaultThread implements Thread {
707
740
  }
708
741
 
709
742
  /**
710
- * Check if thread has any data
743
+ * Check if thread has any data (state or metadata)
711
744
  */
712
745
  empty(): boolean {
713
- return this.state.size === 0;
746
+ return this.state.size === 0 && Object.keys(this.metadata).length === 0;
714
747
  }
715
748
 
716
749
  /**
@@ -718,7 +751,24 @@ export class DefaultThread implements Thread {
718
751
  * @internal
719
752
  */
720
753
  getSerializedState(): string {
721
- return JSON.stringify(Object.fromEntries(this.state));
754
+ const hasState = this.state.size > 0;
755
+ const hasMetadata = Object.keys(this.metadata).length > 0;
756
+
757
+ if (!hasState && !hasMetadata) {
758
+ return '';
759
+ }
760
+
761
+ const data: { state?: Record<string, unknown>; metadata?: Record<string, unknown> } = {};
762
+
763
+ if (hasState) {
764
+ data.state = Object.fromEntries(this.state);
765
+ }
766
+
767
+ if (hasMetadata) {
768
+ data.metadata = this.metadata;
769
+ }
770
+
771
+ return JSON.stringify(data);
722
772
  }
723
773
  }
724
774
 
@@ -726,11 +776,13 @@ export class DefaultSession implements Session {
726
776
  readonly id: string;
727
777
  readonly thread: Thread;
728
778
  readonly state: Map<string, unknown>;
779
+ metadata: Record<string, unknown>;
729
780
 
730
- constructor(thread: Thread, id: string) {
781
+ constructor(thread: Thread, id: string, metadata?: Record<string, unknown>) {
731
782
  this.id = id;
732
783
  this.thread = thread;
733
784
  this.state = new Map();
785
+ this.metadata = metadata || {};
734
786
  }
735
787
 
736
788
  addEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void {
@@ -1032,7 +1084,11 @@ export class ThreadWebSocketClient {
1032
1084
  });
1033
1085
  }
1034
1086
 
1035
- async save(threadId: string, userData: string): Promise<void> {
1087
+ async save(
1088
+ threadId: string,
1089
+ userData: string,
1090
+ threadMetadata?: Record<string, unknown>
1091
+ ): Promise<void> {
1036
1092
  // Wait for connection/reconnection if in progress
1037
1093
  if (this.wsConnecting) {
1038
1094
  await this.wsConnecting;
@@ -1058,10 +1114,20 @@ export class ThreadWebSocketClient {
1058
1114
  reject,
1059
1115
  });
1060
1116
 
1117
+ const data: { thread_id: string; user_data: string; metadata?: Record<string, unknown> } =
1118
+ {
1119
+ thread_id: threadId,
1120
+ user_data: userData,
1121
+ };
1122
+
1123
+ if (threadMetadata && Object.keys(threadMetadata).length > 0) {
1124
+ data.metadata = threadMetadata;
1125
+ }
1126
+
1061
1127
  const message = {
1062
1128
  id: requestId,
1063
1129
  action: 'save',
1064
- data: { thread_id: threadId, user_data: userData },
1130
+ data,
1065
1131
  };
1066
1132
 
1067
1133
  this.ws!.send(JSON.stringify(message));
@@ -1174,26 +1240,59 @@ export class DefaultThreadProvider implements ThreadProvider {
1174
1240
  async restore(ctx: Context<Env>): Promise<Thread> {
1175
1241
  const threadId = await this.threadIDProvider!.getThreadId(this.appState!, ctx);
1176
1242
  validateThreadIdOrThrow(threadId);
1243
+ internal.info('[thread] restoring thread %s', threadId);
1177
1244
 
1178
1245
  // Wait for WebSocket connection if still connecting
1179
1246
  if (this.wsConnecting) {
1247
+ internal.info('[thread] waiting for WebSocket connection');
1180
1248
  await this.wsConnecting;
1181
1249
  }
1182
1250
 
1183
- // Restore thread state from WebSocket if available
1251
+ // Restore thread state and metadata from WebSocket if available
1184
1252
  let initialStateJson: string | undefined;
1253
+ let restoredMetadata: Record<string, unknown> | undefined;
1185
1254
  if (this.wsClient) {
1186
1255
  try {
1256
+ internal.info('[thread] restoring state from WebSocket');
1187
1257
  const restoredData = await this.wsClient.restore(threadId);
1188
1258
  if (restoredData) {
1189
1259
  initialStateJson = restoredData;
1260
+ internal.info('[thread] restored state: %d bytes', restoredData.length);
1261
+ // Parse to check if it includes metadata
1262
+ try {
1263
+ const parsed = JSON.parse(restoredData);
1264
+ // New format: { state?: {...}, metadata?: {...} }
1265
+ if (
1266
+ parsed &&
1267
+ typeof parsed === 'object' &&
1268
+ ('state' in parsed || 'metadata' in parsed)
1269
+ ) {
1270
+ if (parsed.metadata) {
1271
+ restoredMetadata = parsed.metadata;
1272
+ }
1273
+ // Update initialStateJson to be just the state part for backwards compatibility
1274
+ if (parsed.state) {
1275
+ initialStateJson = JSON.stringify(parsed.state);
1276
+ } else {
1277
+ initialStateJson = undefined;
1278
+ }
1279
+ }
1280
+ // else: Old format (just state object), keep as-is
1281
+ } catch {
1282
+ // Keep original if parse fails
1283
+ }
1284
+ } else {
1285
+ internal.info('[thread] no existing state found');
1190
1286
  }
1191
- } catch {
1287
+ } catch (err) {
1288
+ internal.info('[thread] WebSocket restore failed: %s', err);
1192
1289
  // Continue with empty state rather than failing
1193
1290
  }
1291
+ } else {
1292
+ internal.info('[thread] no WebSocket client available');
1194
1293
  }
1195
1294
 
1196
- const thread = new DefaultThread(this, threadId, initialStateJson);
1295
+ const thread = new DefaultThread(this, threadId, initialStateJson, restoredMetadata);
1197
1296
 
1198
1297
  // Populate thread state from restored data
1199
1298
  if (initialStateJson) {
@@ -1202,7 +1301,9 @@ export class DefaultThreadProvider implements ThreadProvider {
1202
1301
  for (const [key, value] of Object.entries(data)) {
1203
1302
  thread.state.set(key, value);
1204
1303
  }
1205
- } catch {
1304
+ internal.info('[thread] populated state with %d keys', thread.state.size);
1305
+ } catch (err) {
1306
+ internal.info('[thread] failed to parse state JSON: %s', err);
1206
1307
  // Continue with empty state if parsing fails
1207
1308
  }
1208
1309
  }
@@ -1213,8 +1314,16 @@ export class DefaultThreadProvider implements ThreadProvider {
1213
1314
 
1214
1315
  async save(thread: Thread): Promise<void> {
1215
1316
  if (thread instanceof DefaultThread) {
1317
+ internal.info(
1318
+ '[thread] DefaultThreadProvider.save() - thread %s, isDirty: %s, hasWsClient: %s',
1319
+ thread.id,
1320
+ thread.isDirty(),
1321
+ !!this.wsClient
1322
+ );
1323
+
1216
1324
  // Wait for WebSocket connection if still connecting
1217
1325
  if (this.wsConnecting) {
1326
+ internal.info('[thread] waiting for WebSocket connection');
1218
1327
  await this.wsConnecting;
1219
1328
  }
1220
1329
 
@@ -1222,10 +1331,20 @@ export class DefaultThreadProvider implements ThreadProvider {
1222
1331
  if (this.wsClient && thread.isDirty()) {
1223
1332
  try {
1224
1333
  const serialized = thread.getSerializedState();
1225
- await this.wsClient.save(thread.id, serialized);
1226
- } catch {
1334
+ internal.info(
1335
+ '[thread] saving to WebSocket, serialized length: %d',
1336
+ serialized.length
1337
+ );
1338
+ const metadata =
1339
+ Object.keys(thread.metadata).length > 0 ? thread.metadata : undefined;
1340
+ await this.wsClient.save(thread.id, serialized, metadata);
1341
+ internal.info('[thread] WebSocket save completed');
1342
+ } catch (err) {
1343
+ internal.info('[thread] WebSocket save failed: %s', err);
1227
1344
  // Don't throw - allow request to complete even if save fails
1228
1345
  }
1346
+ } else {
1347
+ internal.info('[thread] skipping save - no wsClient or thread not dirty');
1229
1348
  }
1230
1349
  }
1231
1350
  }
@@ -1269,20 +1388,30 @@ export class DefaultSessionProvider implements SessionProvider {
1269
1388
  }
1270
1389
 
1271
1390
  async restore(thread: Thread, sessionId: string): Promise<Session> {
1391
+ internal.info('[session] restoring session %s for thread %s', sessionId, thread.id);
1272
1392
  let session = this.sessions.get(sessionId);
1273
1393
  if (!session) {
1274
1394
  session = new DefaultSession(thread, sessionId);
1275
1395
  this.sessions.set(sessionId, session);
1396
+ internal.info('[session] created new session, firing session.started');
1276
1397
  await fireEvent('session.started', session);
1398
+ } else {
1399
+ internal.info('[session] found existing session');
1277
1400
  }
1278
1401
  return session;
1279
1402
  }
1280
1403
 
1281
1404
  async save(session: Session): Promise<void> {
1282
1405
  if (session instanceof DefaultSession) {
1406
+ internal.info(
1407
+ '[session] DefaultSessionProvider.save() - firing completed event for session %s',
1408
+ session.id
1409
+ );
1283
1410
  try {
1284
1411
  await session.fireEvent('completed');
1412
+ internal.info('[session] session.fireEvent completed, firing app event');
1285
1413
  await fireEvent('session.completed', session);
1414
+ internal.info('[session] session.completed app event fired');
1286
1415
  } finally {
1287
1416
  this.sessions.delete(session.id);
1288
1417
  sessionEventListeners.delete(session);