@raisindb/client 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +313 -70
  2. package/dist/chunk-ASOWGNQZ.mjs +532 -0
  3. package/dist/chunk-ASOWGNQZ.mjs.map +1 -0
  4. package/dist/chunk-FYWYSNQG.mjs +190 -0
  5. package/dist/chunk-FYWYSNQG.mjs.map +1 -0
  6. package/dist/chunk-NOPZAMIV.mjs +1427 -0
  7. package/dist/chunk-NOPZAMIV.mjs.map +1 -0
  8. package/dist/chunk-T2ZSNYFG.mjs +248 -0
  9. package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
  10. package/dist/chunk-TPZNJNZR.mjs +866 -0
  11. package/dist/chunk-TPZNJNZR.mjs.map +1 -0
  12. package/dist/chunk-ZCRYQCXS.mjs +170 -0
  13. package/dist/chunk-ZCRYQCXS.mjs.map +1 -0
  14. package/dist/conversation-list-store-D-W6_nsv.d.mts +8447 -0
  15. package/dist/conversation-list-store-D-W6_nsv.d.ts +8447 -0
  16. package/dist/index-B4CqE3Cb.d.ts +301 -0
  17. package/dist/index-CeBRaSop.d.mts +301 -0
  18. package/dist/index.d.mts +72 -6109
  19. package/dist/index.d.ts +72 -6109
  20. package/dist/index.js +3123 -257
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +2745 -3185
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/react.d.mts +4 -0
  25. package/dist/react.d.ts +4 -0
  26. package/dist/react.js +2569 -0
  27. package/dist/react.js.map +1 -0
  28. package/dist/react.mjs +9 -0
  29. package/dist/react.mjs.map +1 -0
  30. package/dist/svelte-sql-CQoB4FEm.d.mts +52 -0
  31. package/dist/svelte-sql-CTXjkz0R.d.ts +52 -0
  32. package/dist/svelte.d.mts +288 -0
  33. package/dist/svelte.d.ts +288 -0
  34. package/dist/svelte.js +2482 -0
  35. package/dist/svelte.js.map +1 -0
  36. package/dist/svelte.mjs +25 -0
  37. package/dist/svelte.mjs.map +1 -0
  38. package/dist/vue.d.mts +197 -0
  39. package/dist/vue.d.ts +197 -0
  40. package/dist/vue.js +1510 -0
  41. package/dist/vue.js.map +1 -0
  42. package/dist/vue.mjs +9 -0
  43. package/dist/vue.mjs.map +1 -0
  44. package/package.json +18 -3
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __typeError = (msg) => {
7
+ throw TypeError(msg);
8
+ };
8
9
  var __export = (target, all) => {
9
10
  for (var name in all)
10
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,19 +18,17 @@ var __copyProps = (to, from, except, desc) => {
17
18
  }
18
19
  return to;
19
20
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
23
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
24
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
25
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
29
26
 
30
27
  // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
28
+ var src_exports = {};
29
+ __export(src_exports, {
30
+ AgentRunsApi: () => AgentRunsApi,
31
+ AgentRunsWsApi: () => AgentRunsWsApi,
33
32
  AllNodeEventTypes: () => AllNodeEventTypes,
34
33
  Archetypes: () => Archetypes,
35
34
  AuthManager: () => AuthManager,
@@ -49,6 +48,7 @@ __export(index_exports, {
49
48
  DEFAULT_RETRY_BASE_DELAY: () => DEFAULT_RETRY_BASE_DELAY,
50
49
  DEFAULT_RETRY_MAX_DELAY: () => DEFAULT_RETRY_MAX_DELAY,
51
50
  DEFAULT_RETRY_OPTIONS: () => DEFAULT_RETRY_OPTIONS,
51
+ DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS: () => DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS,
52
52
  Database: () => Database,
53
53
  ElementTypes: () => ElementTypes,
54
54
  EventHandler: () => EventHandler,
@@ -56,19 +56,27 @@ __export(index_exports, {
56
56
  FlowClient: () => FlowClient,
57
57
  FlowsApi: () => FlowsApi,
58
58
  FunctionsApi: () => FunctionsApi,
59
+ HttpArchetypes: () => HttpArchetypes,
59
60
  HttpDatabase: () => HttpDatabase,
61
+ HttpElementTypes: () => HttpElementTypes,
60
62
  HttpFunctionsApi: () => HttpFunctionsApi,
63
+ HttpNodeTypes: () => HttpNodeTypes,
61
64
  HttpWorkspaceClient: () => HttpWorkspaceClient,
65
+ IdentityAuthApi: () => IdentityAuthApi,
66
+ InboxApi: () => InboxApi,
62
67
  LocalStorageTokenStorage: () => LocalStorageTokenStorage,
63
68
  LogLevel: () => LogLevel,
69
+ McpClient: () => McpClient,
64
70
  MemoryTokenStorage: () => MemoryTokenStorage,
65
71
  NodeBuilder: () => NodeBuilder,
72
+ NodeDevApi: () => NodeDevApi,
66
73
  NodeEventType: () => NodeEventType,
67
74
  NodeFileSource: () => NodeFileSource,
68
75
  NodeHelpers: () => NodeHelpers,
69
76
  NodeOperations: () => NodeOperations,
70
77
  NodeTypes: () => NodeTypes,
71
78
  RAISIN_CONTEXT_KEY: () => RAISIN_CONTEXT_KEY,
79
+ RETRY_STRATEGIES: () => RETRY_STRATEGIES,
72
80
  RaisinAbortError: () => RaisinAbortError,
73
81
  RaisinAuthError: () => RaisinAuthError,
74
82
  RaisinClient: () => RaisinClient,
@@ -83,6 +91,7 @@ __export(index_exports, {
83
91
  ResponseStatus: () => ResponseStatus,
84
92
  RetryBuilder: () => RetryBuilder,
85
93
  SSEClient: () => SSEClient,
94
+ SchedulerApi: () => SchedulerApi,
86
95
  SqlQuery: () => SqlQuery,
87
96
  Tags: () => Tags,
88
97
  Transaction: () => Transaction,
@@ -90,6 +99,7 @@ __export(index_exports, {
90
99
  UploadErrorCode: () => UploadErrorCode,
91
100
  UploadManager: () => UploadManager,
92
101
  Uploader: () => Uploader,
102
+ VisitorChatError: () => VisitorChatError,
93
103
  WorkspaceClient: () => WorkspaceClient,
94
104
  WorkspaceManager: () => WorkspaceManager,
95
105
  calculateChunkCount: () => calculateChunkCount,
@@ -104,35 +114,45 @@ __export(index_exports, {
104
114
  createFlowAdapter: () => createFlowAdapter,
105
115
  createLoader: () => createLoader,
106
116
  createRaisinReact: () => createRaisinReact,
117
+ createRaisinVue: () => createRaisinVue,
107
118
  createRequestScopedClient: () => createRequestScopedClient,
108
119
  createSSRClient: () => createSSRClient,
109
120
  createSqlAdapter: () => createSqlAdapter,
110
121
  createSqlHandler: () => createSqlHandler,
111
122
  createSubscriptionAdapter: () => createSubscriptionAdapter,
112
123
  decodeMessage: () => decodeMessage,
124
+ defaultVisitorSessionStore: () => defaultVisitorSessionStore,
113
125
  encodeMessage: () => encodeMessage,
114
126
  getChunkRange: () => getChunkRange,
115
127
  getContentType: () => getContentType,
116
128
  getErrorMessage: () => getErrorMessage,
117
129
  getLogLevel: () => getLogLevel,
118
130
  getLoggerConfig: () => getLoggerConfig,
131
+ getRefDisplayName: () => getRefDisplayName,
132
+ getRefPath: () => getRefPath,
119
133
  isEventMessage: () => isEventMessage,
134
+ isFlowDefinitionContainer: () => isFlowDefinitionContainer,
135
+ isFlowDefinitionStep: () => isFlowDefinitionStep,
120
136
  isHttpClient: () => isHttpClient,
121
137
  isResponseEnvelope: () => isResponseEnvelope,
122
138
  isRetryableError: () => isRetryableError,
123
139
  isTerminalEvent: () => isTerminalEvent,
124
140
  isWebSocketClient: () => isWebSocketClient,
125
141
  logger: () => logger,
142
+ newControlId: () => newControlId,
143
+ normalizeHomePath: () => normalizeHomePath,
126
144
  projectPlansFromMessages: () => projectPlansFromMessages,
145
+ readTokensFromFragment: () => readTokensFromFragment,
127
146
  rowsToObjects: () => rowsToObjects,
128
147
  setLogLevel: () => setLogLevel,
129
148
  sleep: () => sleep,
149
+ subjectOf: () => subjectOf,
130
150
  useConversation: () => useConversation,
131
151
  useConversationList: () => useConversationList,
132
152
  useFlow: () => useFlow,
133
153
  withRetry: () => withRetry
134
154
  });
135
- module.exports = __toCommonJS(index_exports);
155
+ module.exports = __toCommonJS(src_exports);
136
156
 
137
157
  // src/client.ts
138
158
  var import_events4 = require("events");
@@ -326,7 +346,8 @@ var Connection = class extends import_events.EventEmitter {
326
346
  autoReconnect: options.autoReconnect ?? true,
327
347
  heartbeatInterval: options.heartbeatInterval ?? 3e4,
328
348
  heartbeatTimeout: options.heartbeatTimeout ?? 5e3,
329
- protocols: options.protocols
349
+ protocols: options.protocols,
350
+ headers: options.headers
330
351
  };
331
352
  this.reconnectManager = new ReconnectManager(options.reconnectOptions);
332
353
  this.WebSocketImpl = getWebSocketImpl();
@@ -342,10 +363,10 @@ var Connection = class extends import_events.EventEmitter {
342
363
  this.setState("connecting" /* Connecting */);
343
364
  return new Promise((resolve, reject) => {
344
365
  try {
345
- this.ws = new this.WebSocketImpl(
346
- this.url,
347
- this.options.protocols
348
- );
366
+ const isBrowserWebSocket = typeof WebSocket !== "undefined" && this.WebSocketImpl === WebSocket;
367
+ this.ws = isBrowserWebSocket || !this.options.headers ? new this.WebSocketImpl(this.url, this.options.protocols) : new this.WebSocketImpl(this.url, this.options.protocols, {
368
+ headers: this.options.headers
369
+ });
349
370
  this.ws.binaryType = "arraybuffer";
350
371
  this.ws.onopen = () => {
351
372
  this.setState("connected" /* Connected */);
@@ -434,6 +455,16 @@ var Connection = class extends import_events.EventEmitter {
434
455
  isConnected() {
435
456
  return this.state === "connected" /* Connected */;
436
457
  }
458
+ /**
459
+ * Whether auto-reconnect is enabled for this connection.
460
+ *
461
+ * Used by the client to decide whether requests issued while the
462
+ * connection is down can be queued (the connection will come back) or
463
+ * should fail immediately.
464
+ */
465
+ isAutoReconnectEnabled() {
466
+ return this.options.autoReconnect;
467
+ }
437
468
  /**
438
469
  * Reconnect to the server
439
470
  */
@@ -677,7 +708,7 @@ var LocalStorageTokenStorage = class {
677
708
  function isJwtCredentials(credentials) {
678
709
  return "type" in credentials && credentials.type === "jwt";
679
710
  }
680
- var AuthManager = class {
711
+ var AuthManager = class _AuthManager {
681
712
  constructor(storage) {
682
713
  this.expiresAt = null;
683
714
  this._storage = storage ?? new MemoryTokenStorage();
@@ -700,6 +731,31 @@ var AuthManager = class {
700
731
  this.expiresAt = Date.now() + expiresInMs;
701
732
  this.scheduleTokenRefresh(expiresInMs);
702
733
  }
734
+ /**
735
+ * Learn a bare token's expiry FROM THE TOKEN.
736
+ *
737
+ * `setTokens` is the only other writer of `expiresAt`, and it needs a whole
738
+ * `AuthenticateResponse`. Several paths hold nothing but a JWT string — a
739
+ * session restored from storage, `authenticate({type:'jwt'})`,
740
+ * `setIdentityTokens(token)` — and left `expiresAt` null. `isTokenExpired()`
741
+ * reads null as EXPIRED, so `isAuthenticated()` answered false for the whole
742
+ * life of every restored session, i.e. every page load after the first.
743
+ *
744
+ * It failed silently, and it had already been routed around three times
745
+ * rather than fixed: both HTTP send paths were made to stop consulting
746
+ * `isAuthenticated()` (see `request()` and `mcpStream()` in http-client),
747
+ * and Studio grew its own token parse to tell a 404 apart from a logged-out
748
+ * read. This is the missing writer those workarounds existed for.
749
+ *
750
+ * Same 60s safety margin as `setTokens`, so both writers agree on when a
751
+ * token stops counting as good.
752
+ *
753
+ * @param token - A JWT held in storage or handed to us
754
+ */
755
+ adoptToken(token) {
756
+ const exp = _AuthManager.parseToken(token)?.exp;
757
+ this.expiresAt = typeof exp === "number" ? exp * 1e3 - 6e4 : null;
758
+ }
703
759
  /**
704
760
  * Get current access token
705
761
  */
@@ -818,6 +874,7 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
818
874
  RequestType2["NodeUpdate"] = "node_update";
819
875
  RequestType2["NodeDelete"] = "node_delete";
820
876
  RequestType2["NodeGet"] = "node_get";
877
+ RequestType2["NodeGetByLocalizedPath"] = "node_get_by_localized_path";
821
878
  RequestType2["NodeQuery"] = "node_query";
822
879
  RequestType2["NodeQueryByPath"] = "node_query_by_path";
823
880
  RequestType2["NodeQueryByProperty"] = "node_query_by_property";
@@ -831,6 +888,7 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
831
888
  RequestType2["NodeReorder"] = "node_reorder";
832
889
  RequestType2["NodeMoveChildBefore"] = "node_move_child_before";
833
890
  RequestType2["NodeMoveChildAfter"] = "node_move_child_after";
891
+ RequestType2["NodeApplyChildOrder"] = "node_apply_child_order";
834
892
  RequestType2["PropertyGet"] = "property_get";
835
893
  RequestType2["PropertyUpdate"] = "property_update";
836
894
  RequestType2["RelationAdd"] = "relation_add";
@@ -850,6 +908,8 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
850
908
  RequestType2["BranchUpdateHead"] = "branch_update_head";
851
909
  RequestType2["BranchMerge"] = "branch_merge";
852
910
  RequestType2["BranchCompare"] = "branch_compare";
911
+ RequestType2["BranchDiff"] = "branch_diff";
912
+ RequestType2["BranchCopyNodes"] = "branch_copy_nodes";
853
913
  RequestType2["TagCreate"] = "tag_create";
854
914
  RequestType2["TagGet"] = "tag_get";
855
915
  RequestType2["TagList"] = "tag_list";
@@ -870,6 +930,7 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
870
930
  RequestType2["ArchetypeDelete"] = "archetype_delete";
871
931
  RequestType2["ArchetypePublish"] = "archetype_publish";
872
932
  RequestType2["ArchetypeUnpublish"] = "archetype_unpublish";
933
+ RequestType2["ArchetypeGetResolved"] = "archetype_get_resolved";
873
934
  RequestType2["ElementTypeCreate"] = "element_type_create";
874
935
  RequestType2["ElementTypeGet"] = "element_type_get";
875
936
  RequestType2["ElementTypeList"] = "element_type_list";
@@ -877,6 +938,7 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
877
938
  RequestType2["ElementTypeDelete"] = "element_type_delete";
878
939
  RequestType2["ElementTypePublish"] = "element_type_publish";
879
940
  RequestType2["ElementTypeUnpublish"] = "element_type_unpublish";
941
+ RequestType2["ElementTypeGetResolved"] = "element_type_get_resolved";
880
942
  RequestType2["FullTextSearch"] = "full_text_search";
881
943
  RequestType2["VectorSearch"] = "vector_search";
882
944
  RequestType2["Subscribe"] = "subscribe";
@@ -899,8 +961,23 @@ var RequestType = /* @__PURE__ */ ((RequestType2) => {
899
961
  RequestType2["FlowCancel"] = "flow_cancel";
900
962
  RequestType2["FlowSubscribeEvents"] = "flow_subscribe_events";
901
963
  RequestType2["FlowUnsubscribeEvents"] = "flow_unsubscribe_events";
964
+ RequestType2["AgentRunCreate"] = "agent_run_create";
965
+ RequestType2["AgentRunGet"] = "agent_run_get";
966
+ RequestType2["AgentRunBySubject"] = "agent_run_by_subject";
967
+ RequestType2["AgentRunList"] = "agent_run_list";
968
+ RequestType2["AgentRunEvents"] = "agent_run_events";
969
+ RequestType2["AgentRunSubscribe"] = "agent_run_subscribe";
970
+ RequestType2["AgentRunUnsubscribe"] = "agent_run_unsubscribe";
971
+ RequestType2["AgentRunControl"] = "agent_run_control";
972
+ RequestType2["AgentRunChildren"] = "agent_run_children";
902
973
  RequestType2["FunctionInvoke"] = "function_invoke";
903
974
  RequestType2["FunctionInvokeSync"] = "function_invoke_sync";
975
+ RequestType2["ScheduledInvocationCreate"] = "scheduled_invocation_create";
976
+ RequestType2["ScheduledInvocationCancel"] = "scheduled_invocation_cancel";
977
+ RequestType2["ScheduledInvocationList"] = "scheduled_invocation_list";
978
+ RequestType2["ScheduledInvocationGet"] = "scheduled_invocation_get";
979
+ RequestType2["VisitorChatStart"] = "visitor_chat_start";
980
+ RequestType2["VisitorChatSend"] = "visitor_chat_send";
904
981
  return RequestType2;
905
982
  })(RequestType || {});
906
983
  var ResponseStatus = /* @__PURE__ */ ((ResponseStatus2) => {
@@ -1010,6 +1087,14 @@ var EventHandler = class {
1010
1087
  * If a subscription with identical filters already exists, the callback
1011
1088
  * is added to the existing subscription instead of creating a new one.
1012
1089
  *
1090
+ * Path filter semantics (matched server-side):
1091
+ * - A plain path matches **only that exact node** — there is no implicit
1092
+ * prefix matching.
1093
+ * - `*` matches exactly one path segment (e.g. `/inbox/*` matches
1094
+ * `/inbox/a` but not `/inbox/a/b`).
1095
+ * - `**` matches recursively (e.g. `/inbox/**` matches all descendants
1096
+ * of `/inbox`, but not `/inbox` itself).
1097
+ *
1013
1098
  * @param filters - Event filters
1014
1099
  * @param callback - Callback function for events
1015
1100
  * @returns Subscription object
@@ -1176,7 +1261,8 @@ var EventHandler = class {
1176
1261
  this.filterSubscriptions.set(hash, {
1177
1262
  serverId: subscriptionId,
1178
1263
  filters: {},
1179
- callbacks: /* @__PURE__ */ new Set([callback])
1264
+ callbacks: /* @__PURE__ */ new Set([callback]),
1265
+ synthetic: true
1180
1266
  });
1181
1267
  this.subscriptions.set(subscriptionId, hash);
1182
1268
  this.callbackToHash.set(callback, hash);
@@ -1200,14 +1286,30 @@ var EventHandler = class {
1200
1286
  * Restore subscriptions after reconnect
1201
1287
  *
1202
1288
  * Re-subscribes to the server with the original filters for all active
1203
- * subscription entries.
1289
+ * subscription entries. Failed entries are kept (with their callbacks)
1290
+ * and marked pending so a retry call can re-attempt them without
1291
+ * duplicating already-restored subscriptions.
1292
+ *
1293
+ * @param options.retry - When true, only re-attempts entries that failed
1294
+ * in a previous call (instead of re-subscribing everything)
1295
+ * @throws Error if any subscription could not be restored
1204
1296
  */
1205
- async restoreSubscriptions() {
1206
- const entriesToRestore = Array.from(this.filterSubscriptions.entries());
1207
- this.subscriptions.clear();
1208
- this.filterSubscriptions.clear();
1297
+ async restoreSubscriptions(options) {
1298
+ if (!options?.retry) {
1299
+ for (const entry of this.filterSubscriptions.values()) {
1300
+ if (entry.synthetic) continue;
1301
+ this.subscriptions.delete(entry.serverId);
1302
+ entry.restorePending = true;
1303
+ }
1304
+ }
1305
+ const entriesToRestore = Array.from(this.filterSubscriptions.entries()).filter(([, entry]) => entry.restorePending && !entry.synthetic);
1306
+ let failureCount = 0;
1307
+ let lastError;
1209
1308
  for (const [hash, entry] of entriesToRestore) {
1210
- if (entry.callbacks.size === 0) continue;
1309
+ if (entry.callbacks.size === 0) {
1310
+ entry.restorePending = false;
1311
+ continue;
1312
+ }
1211
1313
  try {
1212
1314
  const payload = { filters: entry.filters };
1213
1315
  const response = await this.sendRequest(
@@ -1215,22 +1317,24 @@ var EventHandler = class {
1215
1317
  "subscribe" /* Subscribe */
1216
1318
  );
1217
1319
  const newServerId = response.subscription_id;
1218
- this.filterSubscriptions.set(hash, {
1219
- serverId: newServerId,
1220
- filters: entry.filters,
1221
- callbacks: entry.callbacks
1222
- });
1320
+ entry.serverId = newServerId;
1321
+ entry.restorePending = false;
1223
1322
  this.subscriptions.set(newServerId, hash);
1224
1323
  for (const callback of entry.callbacks) {
1225
1324
  this.callbackToHash.set(callback, hash);
1226
1325
  }
1227
1326
  } catch (error) {
1228
1327
  logger.error("Failed to restore subscription:", error);
1229
- for (const callback of entry.callbacks) {
1230
- this.callbackToHash.delete(callback);
1231
- }
1328
+ failureCount++;
1329
+ lastError = error;
1232
1330
  }
1233
1331
  }
1332
+ if (failureCount > 0) {
1333
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
1334
+ throw new Error(
1335
+ `Failed to restore ${failureCount} subscription(s) after reconnect: ${message}`
1336
+ );
1337
+ }
1234
1338
  }
1235
1339
  };
1236
1340
  var EventSubscriptions = class {
@@ -1265,7 +1369,15 @@ var EventSubscriptions = class {
1265
1369
  /**
1266
1370
  * Subscribe to events for a specific path
1267
1371
  *
1268
- * @param path - Path pattern (supports wildcards like "/folder/*")
1372
+ * Path pattern semantics (matched server-side):
1373
+ * - A plain path matches **only that exact node** — there is no implicit
1374
+ * prefix matching, so `/folder` does NOT match `/folder/child`.
1375
+ * - `*` matches exactly one path segment: `/folder/*` matches
1376
+ * `/folder/child` but not `/folder/child/grandchild`.
1377
+ * - `**` matches recursively: `/folder/**` matches every descendant of
1378
+ * `/folder` (but not `/folder` itself).
1379
+ *
1380
+ * @param path - Path pattern (e.g. "/folder", "/folder/*", "/folder/**")
1269
1381
  * @param callback - Callback function for events
1270
1382
  * @param options - Optional subscription options
1271
1383
  * @param options.includeNode - Include full node data in event payload (default: false)
@@ -1312,6 +1424,40 @@ var NodeOperations = class {
1312
1424
  const result = await this.sendRequest(payload);
1313
1425
  return result;
1314
1426
  }
1427
+ /**
1428
+ * Create a node, auto-creating any missing ancestor folders.
1429
+ *
1430
+ * @param options - Node creation options (with optional `parentNodeType`)
1431
+ * @returns Created node
1432
+ */
1433
+ async createDeep(options) {
1434
+ const payload = {
1435
+ node_type: options.type,
1436
+ path: options.path,
1437
+ properties: options.properties ?? {},
1438
+ content: options.content,
1439
+ parent_node_type: options.parentNodeType
1440
+ };
1441
+ const result = await this.sendRequest(payload, "node_create_deep");
1442
+ return result;
1443
+ }
1444
+ /**
1445
+ * Upsert a node by path (create-or-update), auto-creating any missing ancestor folders.
1446
+ *
1447
+ * @param options - Node creation options (with optional `parentNodeType`)
1448
+ * @returns Upserted node
1449
+ */
1450
+ async upsertDeep(options) {
1451
+ const payload = {
1452
+ node_type: options.type,
1453
+ path: options.path,
1454
+ properties: options.properties ?? {},
1455
+ content: options.content,
1456
+ parent_node_type: options.parentNodeType
1457
+ };
1458
+ const result = await this.sendRequest(payload, "node_upsert_deep");
1459
+ return result;
1460
+ }
1315
1461
  /**
1316
1462
  * Update an existing node
1317
1463
  *
@@ -1361,6 +1507,96 @@ var NodeOperations = class {
1361
1507
  throw error;
1362
1508
  }
1363
1509
  }
1510
+ /**
1511
+ * List a node's revision history (git-style "file history"), newest first.
1512
+ *
1513
+ * Each entry carries the `revision` (usable with `atRevision()` reads to
1514
+ * fetch the full snapshot), plus when/who and whether the node was deleted
1515
+ * at that revision. Always available regardless of the NodeType `auditable`
1516
+ * flag — it reflects the structural MVCC version history.
1517
+ *
1518
+ * @param id - Node ID
1519
+ * @param options - Optional `{ limit }` to cap the number of revisions
1520
+ * @returns Array of revision entries (newest first)
1521
+ *
1522
+ * @example
1523
+ * ```typescript
1524
+ * const history = await ws.nodes().history(nodeId, { limit: 50 });
1525
+ * for (const rev of history) {
1526
+ * const snapshot = await ws.atRevision(rev.revision).nodes().get(nodeId);
1527
+ * }
1528
+ * ```
1529
+ */
1530
+ async history(id, options) {
1531
+ const payload = {
1532
+ node_id: id,
1533
+ limit: options?.limit
1534
+ };
1535
+ const result = await this.sendRequest(payload, "node_history");
1536
+ return result ?? [];
1537
+ }
1538
+ /**
1539
+ * Get a node by its localized URL path (plan Phase 12).
1540
+ *
1541
+ * @param locale - Locale code (`fr`, `fr-CA`)
1542
+ * @param path - The node's path in that locale (`/produits/chaise`)
1543
+ * @returns The translated node with `canonical_path`,
1544
+ * `canonical_localized_path`, hreflang `alternates` and a `redirect`
1545
+ * hint, or `null` when nothing resolves (missing, hidden in the locale
1546
+ * or not readable — the same answer)
1547
+ *
1548
+ * @example
1549
+ * ```typescript
1550
+ * const hit = await ws.nodes().getByLocalizedPath('fr', '/produits/chaise');
1551
+ * if (hit?.redirect) redirect301(`/fr${hit.canonical_localized_path}`);
1552
+ * ```
1553
+ */
1554
+ async getByLocalizedPath(locale, path) {
1555
+ const payload = { locale, path };
1556
+ const result = await this.sendRequest(payload, "node_get_by_localized_path");
1557
+ return result ?? null;
1558
+ }
1559
+ /**
1560
+ * List a node's revision history by path (newest first).
1561
+ *
1562
+ * @param path - Node path
1563
+ * @param options - Optional `{ limit }` to cap the number of revisions
1564
+ * @returns Array of revision entries (newest first)
1565
+ */
1566
+ async historyByPath(path, options) {
1567
+ const payload = {
1568
+ path,
1569
+ limit: options?.limit
1570
+ };
1571
+ const result = await this.sendRequest(payload, "node_history");
1572
+ return result ?? [];
1573
+ }
1574
+ /**
1575
+ * Query a node's audit-log entries by ID.
1576
+ *
1577
+ * Audit logs are only produced for NodeTypes marked `auditable`. This is
1578
+ * distinct from {@link history}, which is the always-on structural revision
1579
+ * history available for every node.
1580
+ *
1581
+ * @param id - Node ID
1582
+ * @returns Array of audit-log entries
1583
+ */
1584
+ async auditLog(id) {
1585
+ const payload = { node_id: id };
1586
+ const result = await this.sendRequest(payload, "audit_query");
1587
+ return result ?? [];
1588
+ }
1589
+ /**
1590
+ * Query a node's audit-log entries by path.
1591
+ *
1592
+ * @param path - Node path
1593
+ * @returns Array of audit-log entries
1594
+ */
1595
+ async auditLogByPath(path) {
1596
+ const payload = { path };
1597
+ const result = await this.sendRequest(payload, "audit_query");
1598
+ return result ?? [];
1599
+ }
1364
1600
  /**
1365
1601
  * Query nodes
1366
1602
  *
@@ -1383,15 +1619,8 @@ var NodeOperations = class {
1383
1619
  * @returns Node or null if not found
1384
1620
  */
1385
1621
  async getByPath(path) {
1386
- try {
1387
- const result = await this.query({
1388
- query: { path },
1389
- limit: 1
1390
- });
1391
- return result.length > 0 ? result[0] : null;
1392
- } catch (error) {
1393
- return null;
1394
- }
1622
+ const result = await this.sendRequest({ path }, "node_query_by_path");
1623
+ return result ?? null;
1395
1624
  }
1396
1625
  /**
1397
1626
  * Query nodes by property
@@ -1402,14 +1631,12 @@ var NodeOperations = class {
1402
1631
  * @returns Array of nodes
1403
1632
  */
1404
1633
  async queryByProperty(propertyName, propertyValue, limit) {
1405
- return this.query({
1406
- query: {
1407
- properties: {
1408
- [propertyName]: propertyValue
1409
- }
1410
- },
1634
+ const payload = {
1635
+ query: { [propertyName]: propertyValue },
1411
1636
  limit
1412
- });
1637
+ };
1638
+ const result = await this.sendRequest(payload, "node_query_by_property");
1639
+ return result;
1413
1640
  }
1414
1641
  /**
1415
1642
  * Query nodes by type
@@ -1420,7 +1647,7 @@ var NodeOperations = class {
1420
1647
  */
1421
1648
  async queryByType(nodeType, limit) {
1422
1649
  return this.query({
1423
- query: { node_type: nodeType },
1650
+ query: { type: nodeType },
1424
1651
  limit
1425
1652
  });
1426
1653
  }
@@ -1455,17 +1682,40 @@ var NodeOperations = class {
1455
1682
  // Tree Operations
1456
1683
  // ========================================================================
1457
1684
  /**
1458
- * List children of a parent node
1685
+ * List children of a parent node, in editorial (drag-and-drop) order.
1459
1686
  *
1460
1687
  * @param parentPath - Path of the parent node
1461
1688
  * @returns Array of child nodes
1462
1689
  */
1463
1690
  async listChildren(parentPath) {
1691
+ const { items } = await this.listChildrenPage(parentPath);
1692
+ return items;
1693
+ }
1694
+ /**
1695
+ * List one page of a parent's children, in editorial order.
1696
+ *
1697
+ * Pagination is keyset-based: pass the previous response's `nextCursor` back as
1698
+ * `cursor` to get the following page. `nextCursor` is `null` on the last page.
1699
+ * Treat the cursor as opaque — do not parse or construct it.
1700
+ *
1701
+ * @param parentPath - Path of the parent node
1702
+ * @param options - `cursor` from a previous page, and page `limit`
1703
+ */
1704
+ async listChildrenPage(parentPath, options = {}) {
1464
1705
  const payload = {
1465
1706
  parent_path: parentPath
1466
1707
  };
1708
+ if (options.cursor !== void 0) payload.cursor = options.cursor;
1709
+ if (options.limit !== void 0) payload.limit = options.limit;
1467
1710
  const result = await this.sendRequest(payload, "node_list_children");
1468
- return result;
1711
+ if (Array.isArray(result)) {
1712
+ return { items: result, nextCursor: null };
1713
+ }
1714
+ const page = result;
1715
+ return {
1716
+ items: page.nodes ?? [],
1717
+ nextCursor: page.next_cursor == null ? null : JSON.stringify(page.next_cursor)
1718
+ };
1469
1719
  }
1470
1720
  /**
1471
1721
  * Get a node tree starting from a root node
@@ -1476,7 +1726,7 @@ var NodeOperations = class {
1476
1726
  */
1477
1727
  async getTree(rootPath, maxDepth) {
1478
1728
  const payload = {
1479
- root_path: rootPath
1729
+ parent_path: rootPath
1480
1730
  };
1481
1731
  if (maxDepth !== void 0) {
1482
1732
  payload.max_depth = maxDepth;
@@ -1493,7 +1743,7 @@ var NodeOperations = class {
1493
1743
  */
1494
1744
  async getTreeFlat(rootPath, maxDepth) {
1495
1745
  const payload = {
1496
- root_path: rootPath
1746
+ parent_path: rootPath
1497
1747
  };
1498
1748
  if (maxDepth !== void 0) {
1499
1749
  payload.max_depth = maxDepth;
@@ -1528,7 +1778,7 @@ var NodeOperations = class {
1528
1778
  */
1529
1779
  async rename(nodePath, newName) {
1530
1780
  const payload = {
1531
- node_path: nodePath,
1781
+ old_path: nodePath,
1532
1782
  new_name: newName
1533
1783
  };
1534
1784
  const result = await this.sendRequest(payload, "node_rename");
@@ -1544,9 +1794,8 @@ var NodeOperations = class {
1544
1794
  */
1545
1795
  async copy(fromPath, toParentPath, newName) {
1546
1796
  const payload = {
1547
- from_path: fromPath,
1548
- to_parent_path: toParentPath,
1549
- deep: false
1797
+ source_path: fromPath,
1798
+ target_parent: toParentPath
1550
1799
  };
1551
1800
  if (newName !== void 0) {
1552
1801
  payload.new_name = newName;
@@ -1564,9 +1813,8 @@ var NodeOperations = class {
1564
1813
  */
1565
1814
  async copyTree(fromPath, toParentPath, newName) {
1566
1815
  const payload = {
1567
- from_path: fromPath,
1568
- to_parent_path: toParentPath,
1569
- deep: true
1816
+ source_path: fromPath,
1817
+ target_parent: toParentPath
1570
1818
  };
1571
1819
  if (newName !== void 0) {
1572
1820
  payload.new_name = newName;
@@ -1575,53 +1823,72 @@ var NodeOperations = class {
1575
1823
  return result;
1576
1824
  }
1577
1825
  /**
1578
- * Reorder a node by setting a new order key
1826
+ * Move a child to a specific position among its siblings.
1579
1827
  *
1580
- * @param nodePath - Node path
1581
- * @param orderKey - New order key (base62-encoded fractional index)
1582
- * @returns Reordered node
1828
+ * Positions are 0-based; a position past the end appends. The server owns the
1829
+ * fractional order key — you name a position, it mints the key.
1830
+ *
1831
+ * @param parentPath - Parent node path
1832
+ * @param childName - Name of the child to move (not a full path)
1833
+ * @param position - Target 0-based position among the parent's children
1834
+ * @returns The reordered node, carrying its newly assigned `order_key`
1583
1835
  */
1584
- async reorder(nodePath, orderKey) {
1836
+ async reorder(parentPath, childName, position) {
1585
1837
  const payload = {
1586
- node_path: nodePath,
1587
- order_key: orderKey
1838
+ parent_path: parentPath,
1839
+ child_name: childName,
1840
+ position
1588
1841
  };
1589
1842
  const result = await this.sendRequest(payload, "node_reorder");
1590
1843
  return result;
1591
1844
  }
1592
1845
  /**
1593
- * Move a child node before a reference sibling
1846
+ * Move a child so it sits immediately before one of its siblings.
1594
1847
  *
1595
1848
  * @param parentPath - Parent node path
1596
- * @param childPath - Child node path to move
1597
- * @param referencePath - Reference sibling path to position before
1598
- * @returns Moved node
1849
+ * @param childName - Name of the child to move (not a full path)
1850
+ * @param beforeChildName - Name of the sibling to sit before
1599
1851
  */
1600
- async moveChildBefore(parentPath, childPath, referencePath) {
1852
+ async moveChildBefore(parentPath, childName, beforeChildName) {
1601
1853
  const payload = {
1602
1854
  parent_path: parentPath,
1603
- child_path: childPath,
1604
- reference_path: referencePath
1855
+ child_name: childName,
1856
+ before_child_name: beforeChildName
1605
1857
  };
1606
- const result = await this.sendRequest(payload, "node_move_child_before");
1607
- return result;
1858
+ await this.sendRequest(payload, "node_move_child_before");
1608
1859
  }
1609
1860
  /**
1610
- * Move a child node after a reference sibling
1861
+ * Move a child so it sits immediately after one of its siblings.
1611
1862
  *
1612
1863
  * @param parentPath - Parent node path
1613
- * @param childPath - Child node path to move
1614
- * @param referencePath - Reference sibling path to position after
1615
- * @returns Moved node
1864
+ * @param childName - Name of the child to move (not a full path)
1865
+ * @param afterChildName - Name of the sibling to sit after
1616
1866
  */
1617
- async moveChildAfter(parentPath, childPath, referencePath) {
1867
+ async moveChildAfter(parentPath, childName, afterChildName) {
1618
1868
  const payload = {
1619
1869
  parent_path: parentPath,
1620
- child_path: childPath,
1621
- reference_path: referencePath
1870
+ child_name: childName,
1871
+ after_child_name: afterChildName
1622
1872
  };
1623
- const result = await this.sendRequest(payload, "node_move_child_after");
1624
- return result;
1873
+ await this.sendRequest(payload, "node_move_child_after");
1874
+ }
1875
+ /**
1876
+ * Replay a parent's child order from another branch onto the current branch.
1877
+ *
1878
+ * Carries node sibling-order across branches — useful for selective publish
1879
+ * flows, where copying node content (e.g. via SQL) does not move the ordering
1880
+ * index. Only children that exist under the parent on both branches are
1881
+ * reordered. The target branch is the connection's current branch.
1882
+ *
1883
+ * @param parentPath - Parent node path
1884
+ * @param sourceBranch - Branch to copy the child order FROM
1885
+ */
1886
+ async applyChildOrder(parentPath, sourceBranch) {
1887
+ const payload = {
1888
+ parent_path: parentPath,
1889
+ source_branch: sourceBranch
1890
+ };
1891
+ await this.sendRequest(payload, "node_apply_child_order");
1625
1892
  }
1626
1893
  // ========================================================================
1627
1894
  // Relationship Operations
@@ -1897,7 +2164,17 @@ var NodeFileSource = class {
1897
2164
  return;
1898
2165
  }
1899
2166
  if (!this._fsPromises) {
1900
- this._fsPromises = await import("fs/promises");
2167
+ if (!isNodeEnvironment()) {
2168
+ throw new Error(
2169
+ "NodeFileSource requires Node.js (fs/promises is not available in browsers). In the browser, use a File or Blob object instead."
2170
+ );
2171
+ }
2172
+ const specifier = "fs/promises";
2173
+ this._fsPromises = await import(
2174
+ /* @vite-ignore */
2175
+ /* webpackIgnore: true */
2176
+ specifier
2177
+ );
1901
2178
  }
1902
2179
  const stats = await this._fsPromises.stat(this._filePath);
1903
2180
  this._size = stats.size;
@@ -2506,7 +2783,8 @@ function createSqlHandler(sendRequest) {
2506
2783
 
2507
2784
  // src/node-types.ts
2508
2785
  var NodeTypes = class {
2509
- constructor(_context, sendRequest) {
2786
+ constructor(context, sendRequest) {
2787
+ this.context = context;
2510
2788
  this.sendRequest = sendRequest;
2511
2789
  }
2512
2790
  /**
@@ -2590,14 +2868,25 @@ var NodeTypes = class {
2590
2868
  );
2591
2869
  }
2592
2870
  /**
2593
- * Validate a node against its NodeType
2871
+ * Validate a node against its NodeType.
2872
+ *
2873
+ * The server validates in the context of a workspace (workspace-level
2874
+ * allow-lists and unique checks), so one is required. Pass it here, or
2875
+ * create the database with a workspace already in its context.
2594
2876
  */
2595
- async validate(node) {
2877
+ async validate(node, workspace) {
2878
+ const ws = workspace ?? this.context.workspace;
2879
+ if (!ws) {
2880
+ throw new Error(
2881
+ "nodeTypes().validate() requires a workspace: pass it as the second argument"
2882
+ );
2883
+ }
2596
2884
  return this.sendRequest(
2597
2885
  {
2598
2886
  node
2599
2887
  },
2600
- "node_type_validate" /* NodeTypeValidate */
2888
+ "node_type_validate" /* NodeTypeValidate */,
2889
+ { ...this.context, workspace: ws }
2601
2890
  );
2602
2891
  }
2603
2892
  /**
@@ -2698,6 +2987,17 @@ var Archetypes = class {
2698
2987
  "archetype_unpublish" /* ArchetypeUnpublish */
2699
2988
  );
2700
2989
  }
2990
+ /**
2991
+ * Get resolved Archetype with full inheritance applied
2992
+ */
2993
+ async getResolved(name) {
2994
+ return this.sendRequest(
2995
+ {
2996
+ name
2997
+ },
2998
+ "archetype_get_resolved" /* ArchetypeGetResolved */
2999
+ );
3000
+ }
2701
3001
  };
2702
3002
 
2703
3003
  // src/element-types.ts
@@ -2785,6 +3085,17 @@ var ElementTypes = class {
2785
3085
  "element_type_unpublish" /* ElementTypeUnpublish */
2786
3086
  );
2787
3087
  }
3088
+ /**
3089
+ * Get resolved ElementType with full inheritance applied
3090
+ */
3091
+ async getResolved(name) {
3092
+ return this.sendRequest(
3093
+ {
3094
+ name
3095
+ },
3096
+ "element_type_get_resolved" /* ElementTypeGetResolved */
3097
+ );
3098
+ }
2788
3099
  };
2789
3100
 
2790
3101
  // src/branches.ts
@@ -2886,6 +3197,110 @@ var Branches = class {
2886
3197
  "branch_compare" /* BranchCompare */
2887
3198
  );
2888
3199
  }
3200
+ /**
3201
+ * Per-node diff of `branch` relative to `baseBranch` (added/modified/deleted).
3202
+ *
3203
+ * Cheaper than reading both branch listings: cost scales with the number of
3204
+ * changed nodes since the branches diverged, not the total content.
3205
+ */
3206
+ async diff(branch, baseBranch) {
3207
+ return this.sendRequest(
3208
+ {
3209
+ branch,
3210
+ base_branch: baseBranch
3211
+ },
3212
+ "branch_diff" /* BranchDiff */
3213
+ );
3214
+ }
3215
+ /**
3216
+ * Copy a node set from `sourceBranch` onto `targetBranch` (branch
3217
+ * promotion): node ids are preserved, so repeated copies update the same
3218
+ * target nodes; all changes land in ONE atomic commit on the target branch.
3219
+ *
3220
+ * Each root's parent path must already exist on the target branch. With
3221
+ * `deleteMissing`, target nodes under the roots that are absent from the
3222
+ * copied source set are pruned (one-way sync).
3223
+ *
3224
+ * `sourceRevision` pins the read to a point in time. A promotion of a large
3225
+ * set is not instantaneous, so without it the copy reads each node when its
3226
+ * turn comes and a writer working in parallel lands partly inside the result
3227
+ * — a torn snapshot. Pass the revision captured when the operation was
3228
+ * decided (see `getHead`) to copy exactly that state. The target branch is
3229
+ * always resolved at its head: the pin says WHAT to copy, never where.
3230
+ */
3231
+ async copyNodes(sourceBranch, targetBranch, opts) {
3232
+ return this.sendRequest(
3233
+ {
3234
+ source_branch: sourceBranch,
3235
+ target_branch: targetBranch,
3236
+ workspace: opts.workspace,
3237
+ roots: opts.roots,
3238
+ recursive: opts.recursive ?? true,
3239
+ delete_missing: opts.deleteMissing ?? false,
3240
+ source_revision: opts.sourceRevision ?? null
3241
+ },
3242
+ "branch_copy_nodes" /* BranchCopyNodes */
3243
+ );
3244
+ }
3245
+ };
3246
+
3247
+ // src/scheduler.ts
3248
+ var SchedulerApi = class {
3249
+ constructor(_context, sendRequest) {
3250
+ this.sendRequest = sendRequest;
3251
+ }
3252
+ /**
3253
+ * Schedule a one-shot invocation of a function or flow at a fixed time.
3254
+ */
3255
+ async schedule(options) {
3256
+ const runAt = options.runAt instanceof Date ? options.runAt.toISOString() : options.runAt;
3257
+ return this.sendRequest(
3258
+ {
3259
+ target_kind: options.targetKind,
3260
+ target_path: options.targetPath,
3261
+ input: options.input ?? {},
3262
+ run_at: runAt,
3263
+ external_key: options.externalKey,
3264
+ branch: options.branch,
3265
+ workspace: options.workspace,
3266
+ max_retries: options.maxRetries
3267
+ },
3268
+ "scheduled_invocation_create" /* ScheduledInvocationCreate */
3269
+ );
3270
+ }
3271
+ /**
3272
+ * Cancel a pending invocation before it fires, by job id or external key.
3273
+ */
3274
+ async cancel(ref) {
3275
+ const payload = typeof ref === "string" ? { job_id: ref } : { job_id: ref.jobId, external_key: ref.externalKey };
3276
+ return this.sendRequest(
3277
+ payload,
3278
+ "scheduled_invocation_cancel" /* ScheduledInvocationCancel */
3279
+ );
3280
+ }
3281
+ /**
3282
+ * List this repository's scheduled invocations.
3283
+ */
3284
+ async list(filter) {
3285
+ const result = await this.sendRequest(
3286
+ {
3287
+ external_key: filter?.externalKey,
3288
+ status: filter?.status
3289
+ },
3290
+ "scheduled_invocation_list" /* ScheduledInvocationList */
3291
+ );
3292
+ return result?.invocations ?? [];
3293
+ }
3294
+ /**
3295
+ * Fetch a single scheduled invocation, by job id or external key.
3296
+ */
3297
+ async get(ref) {
3298
+ const payload = typeof ref === "string" ? { job_id: ref } : { job_id: ref.jobId, external_key: ref.externalKey };
3299
+ return this.sendRequest(
3300
+ payload,
3301
+ "scheduled_invocation_get" /* ScheduledInvocationGet */
3302
+ );
3303
+ }
2889
3304
  };
2890
3305
 
2891
3306
  // src/tags.ts
@@ -3064,29 +3479,440 @@ function createAsyncQueue() {
3064
3479
  };
3065
3480
  }
3066
3481
 
3067
- // src/functions-api.ts
3068
- var FunctionsApi = class {
3069
- constructor(repository, context, sendRequest) {
3070
- this.context = { ...context, repository };
3071
- this.sendRequest = sendRequest;
3482
+ // src/agent-run-children.ts
3483
+ function spawnBody(o) {
3484
+ return {
3485
+ objective: o.objective,
3486
+ budgets: o.budgets,
3487
+ on_exceeded: o.onExceeded,
3488
+ spawn_key: o.spawnKey,
3489
+ subject: o.subject,
3490
+ as_agent: o.asAgent,
3491
+ agent_ref: o.agentRef,
3492
+ input: o.input ?? null,
3493
+ executor_config: o.executorConfig,
3494
+ reducer: o.reducer,
3495
+ inherit_reducer: o.inheritReducer ?? false
3496
+ };
3497
+ }
3498
+ function checkpointBody(o) {
3499
+ return {
3500
+ fence: o.fence,
3501
+ operation_id: o.operationId,
3502
+ reason: o.reason,
3503
+ summary: o.summary,
3504
+ transcript_cutoff: o.transcriptCutoff,
3505
+ state: o.state,
3506
+ large_refs: o.largeRefs ?? []
3507
+ };
3508
+ }
3509
+
3510
+ // src/agent-runs.ts
3511
+ function createRunBody(options) {
3512
+ return {
3513
+ subject: options.subject,
3514
+ branch: options.branch,
3515
+ agent_ref: options.agentRef,
3516
+ create_key: options.createKey,
3517
+ budgets: options.budgets,
3518
+ input: options.input ?? null,
3519
+ reducer: options.reducer,
3520
+ executor_config: options.executorConfig,
3521
+ control_capability: options.controlCapability,
3522
+ as_agent: options.asAgent
3523
+ };
3524
+ }
3525
+ function subjectOf(subject) {
3526
+ if (typeof subject !== "string") return subject;
3527
+ const cut = subject.indexOf(":/");
3528
+ if (cut <= 0) throw new Error(`subject must be "workspace:/path", got "${subject}"`);
3529
+ return { workspace: subject.slice(0, cut), path: subject.slice(cut + 1) };
3530
+ }
3531
+ var controlCounter = 0;
3532
+ function newControlId(prefix = "ctl") {
3533
+ controlCounter += 1;
3534
+ const rand = Math.random().toString(36).slice(2, 10);
3535
+ return `${prefix}-${Date.now().toString(36)}-${controlCounter}-${rand}`;
3536
+ }
3537
+ var AgentRunsApi = class {
3538
+ constructor(repository, transport) {
3539
+ this.repository = repository;
3540
+ this.transport = transport;
3541
+ }
3542
+ base(runId, rest = "") {
3543
+ const repo = encodeURIComponent(this.repository);
3544
+ const run = runId ? `/${encodeURIComponent(runId)}` : "";
3545
+ return `/api/agent-runs/${repo}${run}${rest}`;
3546
+ }
3547
+ /** Create a run, or get back the subject's live run. */
3548
+ create(options) {
3549
+ return this.transport.request("POST", this.base(), createRunBody(options));
3550
+ }
3551
+ /** Every run about `subject` you may see, newest (the live one) first. */
3552
+ bySubject(subject, limit = 20) {
3553
+ const s = subjectOf(subject);
3554
+ const q = new URLSearchParams({ subject: `${s.workspace}:${s.path ?? ""}`, limit: String(limit) });
3555
+ if (s.node_id) q.set("subject_node_id", s.node_id);
3556
+ return this.transport.request("GET", `${this.base()}?${q.toString()}`);
3557
+ }
3558
+ /** Read a run (record + effective projection). */
3559
+ get(runId) {
3560
+ return this.transport.request("GET", this.base(runId));
3561
+ }
3562
+ /** Your runs in one status (default "running"). */
3563
+ list(status = "running", limit = 50) {
3564
+ return this.transport.request("GET", `${this.base()}?status=${status}&limit=${limit}`);
3565
+ }
3566
+ /** Durable events after `afterSeq` (gap-free; page with the last seq). */
3567
+ events(runId, afterSeq = 0, limit = 500) {
3568
+ return this.transport.request("GET", this.base(runId, `/events?after_seq=${afterSeq}&limit=${limit}`));
3569
+ }
3570
+ /** Any control. Retrying with the same `controlId` is a no-op. */
3571
+ control(runId, command, controlId = newControlId(command.command), capability) {
3572
+ return this.transport.request("POST", this.base(runId, "/control"), {
3573
+ control_id: controlId,
3574
+ command,
3575
+ capability
3576
+ });
3577
+ }
3578
+ stop(runId, reason, controlId) {
3579
+ return this.control(runId, { command: "stop", reason }, controlId);
3580
+ }
3581
+ pause(runId, controlId) {
3582
+ return this.control(runId, { command: "pause" }, controlId);
3583
+ }
3584
+ resume(runId, controlId) {
3585
+ return this.control(runId, { command: "resume" }, controlId);
3586
+ }
3587
+ /** Queue input for the run's next safe boundary. */
3588
+ steer(runId, input, controlId) {
3589
+ return this.control(runId, { command: "steer", input }, controlId);
3590
+ }
3591
+ /** Decide an approval request; the digest ties it to one changeset. */
3592
+ approve(runId, requestId, subjectDigest, reject, controlId) {
3593
+ const decision = reject ? { decision: "reject", reason: reject.reason } : { decision: "approve" };
3594
+ return this.control(
3595
+ runId,
3596
+ { command: "approve", request_id: requestId, decision, subject_digest: subjectDigest },
3597
+ controlId
3598
+ );
3599
+ }
3600
+ /** Answer an input request. */
3601
+ answer(runId, requestId, value, controlId) {
3602
+ return this.control(runId, { command: "provide_input", request_id: requestId, value }, controlId);
3072
3603
  }
3073
3604
  /**
3074
- * Invoke a server-side function by name (async).
3075
- *
3076
- * Queues a `FunctionExecution` job and returns the execution and job IDs
3077
- * immediately. The function runs asynchronously on the server.
3078
- *
3079
- * @param functionName - Name of the function to invoke
3080
- * @param input - Input data passed to the function
3081
- * @returns Execution ID and job ID for tracking
3605
+ * Stream the run's events: replay after `afterSeq`, then live. Ends after
3606
+ * the run's terminal event. Resume after a disconnect with the last `seq`.
3082
3607
  */
3083
- async invoke(functionName, input, options) {
3084
- const effectiveRequestTimeoutMs = options?.requestTimeoutMs;
3085
- let effectiveWaitTimeoutMs = options?.waitTimeoutMs;
3086
- if (options?.waitForResult) {
3087
- const transportTimeoutBudget = effectiveRequestTimeoutMs ?? 3e4;
3088
- effectiveWaitTimeoutMs = Math.min(
3089
- effectiveWaitTimeoutMs ?? transportTimeoutBudget,
3608
+ async *stream(runId, afterSeq = 0, signal) {
3609
+ const response = await this.transport.fetch(
3610
+ this.transport.url(this.base(runId, `/stream?after_seq=${afterSeq}`)),
3611
+ { headers: { ...this.transport.headers(), Accept: "text/event-stream" }, signal }
3612
+ );
3613
+ if (!response.ok || !response.body) {
3614
+ throw new Error(`agent run stream failed: ${response.status} ${await response.text()}`);
3615
+ }
3616
+ const reader = response.body.getReader();
3617
+ const decoder = new TextDecoder();
3618
+ let buffer = "";
3619
+ for (; ; ) {
3620
+ const { value, done } = await reader.read();
3621
+ if (done) return;
3622
+ buffer += decoder.decode(value, { stream: true });
3623
+ let cut;
3624
+ while ((cut = buffer.indexOf("\n\n")) >= 0) {
3625
+ const block = buffer.slice(0, cut);
3626
+ buffer = buffer.slice(cut + 2);
3627
+ let event = "message";
3628
+ let data = "";
3629
+ for (const line of block.split("\n")) {
3630
+ if (line.startsWith("event:")) event = line.slice(6).trim();
3631
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
3632
+ }
3633
+ if (event === "end") return;
3634
+ if (event === "error") throw new Error(`agent run stream error: ${data}`);
3635
+ if (event === "run-event" && data) yield JSON.parse(data);
3636
+ }
3637
+ }
3638
+ }
3639
+ // ---- client-driven runs -------------------------------------------------
3640
+ /** Take the lease of a queued client-driven run. */
3641
+ acquire(runId) {
3642
+ return this.transport.request("POST", this.base(runId, "/lease/acquire"), {});
3643
+ }
3644
+ renew(runId, fence) {
3645
+ return this.transport.request("POST", this.base(runId, "/lease/renew"), { fence });
3646
+ }
3647
+ release(runId, fence) {
3648
+ return this.transport.request("POST", this.base(runId, "/lease/release"), { fence });
3649
+ }
3650
+ /** Begin an operation; returns the active operation (its `op_id`). */
3651
+ begin(runId, fence, op) {
3652
+ return this.transport.request("POST", this.base(runId, "/operations"), { fence, ...op });
3653
+ }
3654
+ /** Record an operation's result. */
3655
+ finish(runId, fence, opId, result) {
3656
+ return this.transport.request("POST", this.base(runId, "/operations/finish"), { fence, op_id: opId, ...result });
3657
+ }
3658
+ /** Open approval/input requests and wait (`kind: "approval" | "input"`). */
3659
+ wait(runId, fence, requests) {
3660
+ return this.transport.request("POST", this.base(runId, "/wait"), { fence, requests });
3661
+ }
3662
+ /** End a client-driven run. */
3663
+ complete(runId, fence, status, outcome) {
3664
+ return this.transport.request("POST", this.base(runId, "/complete"), { fence, status, outcome });
3665
+ }
3666
+ // ── Child runs, mailbox, checkpoints, usage ──────────────────────────────
3667
+ /** Spawn a child with a typed objective; its budgets come out of this run's. */
3668
+ spawnChild(runId, options) {
3669
+ const q = options.branch ? `?branch=${encodeURIComponent(options.branch)}` : "";
3670
+ return this.transport.request("POST", this.base(runId, `/children${q}`), spawnBody(options));
3671
+ }
3672
+ /** Every child of a run: the parent's link plus the child's live status. */
3673
+ children(runId) {
3674
+ return this.transport.request("GET", this.base(runId, "/children"));
3675
+ }
3676
+ /** Inspect one child: record, events after `afterSeq`, usage, checkpoint. */
3677
+ inspectChild(runId, childId, afterSeq = 0, limit = 200) {
3678
+ const rest = `/children/${encodeURIComponent(childId)}?after_seq=${afterSeq}&limit=${limit}`;
3679
+ return this.transport.request("GET", this.base(runId, rest));
3680
+ }
3681
+ /** Message, steer, interrupt or resume a child (idempotent per `controlId`). */
3682
+ controlChild(runId, childId, action, controlId = newControlId(action.action)) {
3683
+ const rest = `/children/${encodeURIComponent(childId)}/control`;
3684
+ return this.transport.request("POST", this.base(runId, rest), { control_id: controlId, ...action });
3685
+ }
3686
+ messageChild(runId, childId, message, controlId) {
3687
+ return this.controlChild(runId, childId, { action: "message", message }, controlId);
3688
+ }
3689
+ steerChild(runId, childId, input, controlId) {
3690
+ return this.controlChild(runId, childId, { action: "steer", input }, controlId);
3691
+ }
3692
+ interruptChild(runId, childId, reason, mode = "stop", controlId) {
3693
+ return this.controlChild(runId, childId, { action: "interrupt", mode, reason }, controlId);
3694
+ }
3695
+ /** A client-driven parent waits (under its lease) for a child's hand-back. */
3696
+ waitChild(runId, childId, fence, expiresAtMs) {
3697
+ const rest = `/children/${encodeURIComponent(childId)}/wait`;
3698
+ return this.transport.request("POST", this.base(runId, rest), { fence, expires_at_ms: expiresAtMs });
3699
+ }
3700
+ /** Unacknowledged mailbox items (hand-backs and child messages). */
3701
+ mailbox(runId) {
3702
+ return this.transport.request("GET", this.base(runId, "/mailbox"));
3703
+ }
3704
+ /** Acknowledge mailbox items up to `upTo`. */
3705
+ ackMailbox(runId, upTo) {
3706
+ return this.transport.request("POST", this.base(runId, "/mailbox/ack"), { up_to: upTo });
3707
+ }
3708
+ /** A child posts a message to its parent's mailbox (idempotent per id). */
3709
+ postToParent(runId, messageId, message) {
3710
+ return this.transport.request("POST", this.base(runId, "/post-to-parent"), { message_id: messageId, message });
3711
+ }
3712
+ /** Write a structured checkpoint (compaction keeps state, not prose). */
3713
+ checkpoint(runId, options) {
3714
+ return this.transport.request("POST", this.base(runId, "/checkpoints"), checkpointBody(options));
3715
+ }
3716
+ /** Read a checkpoint (default: latest) with its structured state. */
3717
+ readCheckpoint(runId, checkpointNo) {
3718
+ return this.transport.request("GET", this.base(runId, `/checkpoints/${checkpointNo ?? "latest"}`));
3719
+ }
3720
+ /** Usage accounting: own, children, reserved, spare. */
3721
+ usage(runId) {
3722
+ return this.transport.request("GET", this.base(runId, "/usage"));
3723
+ }
3724
+ };
3725
+
3726
+ // src/agent-runs-ws.ts
3727
+ var TERMINAL = ["completed", "failed", "stopped"];
3728
+ var AgentRunsWsApi = class {
3729
+ constructor(context, sendRequest, eventHandler) {
3730
+ this.context = context;
3731
+ this.sendRequest = sendRequest;
3732
+ this.eventHandler = eventHandler;
3733
+ }
3734
+ call(type, payload) {
3735
+ return this.sendRequest(payload, type, this.context);
3736
+ }
3737
+ /** Create a run, or get back the subject's live run. */
3738
+ create(options) {
3739
+ return this.call("agent_run_create" /* AgentRunCreate */, createRunBody(options));
3740
+ }
3741
+ /** Read a run (record + effective projection). */
3742
+ get(runId) {
3743
+ return this.call("agent_run_get" /* AgentRunGet */, { run_id: runId });
3744
+ }
3745
+ /**
3746
+ * Every run about `subject` (a conversation, a document) you may see,
3747
+ * newest first — the live run, if there is one, comes first.
3748
+ */
3749
+ bySubject(subject, limit = 20) {
3750
+ return this.call("agent_run_by_subject" /* AgentRunBySubject */, { ...subjectOf(subject), limit });
3751
+ }
3752
+ /** The newest run of `subject`, or null. */
3753
+ async latest(subject) {
3754
+ const [first] = await this.bySubject(subject, 1);
3755
+ return first ?? null;
3756
+ }
3757
+ /** Your runs in one status (default "running"). */
3758
+ list(status = "running", limit = 50) {
3759
+ return this.call("agent_run_list" /* AgentRunList */, { status, limit });
3760
+ }
3761
+ /** Durable events after `afterSeq`. */
3762
+ events(runId, afterSeq = 0, limit = 500) {
3763
+ return this.call("agent_run_events" /* AgentRunEvents */, { run_id: runId, after_seq: afterSeq, limit });
3764
+ }
3765
+ /** The children a run spawned, with their current status. */
3766
+ children(runId) {
3767
+ return this.call("agent_run_children" /* AgentRunChildren */, { run_id: runId });
3768
+ }
3769
+ /** Any control. Retrying with the same `controlId` is a no-op. */
3770
+ control(runId, command, controlId = newControlId(command.command), capability) {
3771
+ return this.call("agent_run_control" /* AgentRunControl */, { run_id: runId, control_id: controlId, command, capability });
3772
+ }
3773
+ stop(runId, reason, controlId) {
3774
+ return this.control(runId, { command: "stop", reason }, controlId);
3775
+ }
3776
+ pause(runId, controlId) {
3777
+ return this.control(runId, { command: "pause" }, controlId);
3778
+ }
3779
+ resume(runId, controlId) {
3780
+ return this.control(runId, { command: "resume" }, controlId);
3781
+ }
3782
+ /** Queue input for the run's next safe boundary. */
3783
+ steer(runId, input, controlId) {
3784
+ return this.control(runId, { command: "steer", input }, controlId);
3785
+ }
3786
+ /** Decide an approval request; the digest ties it to one changeset. */
3787
+ approve(runId, requestId, subjectDigest, reject, controlId) {
3788
+ const decision = reject ? { decision: "reject", reason: reject.reason } : { decision: "approve" };
3789
+ return this.control(runId, { command: "approve", request_id: requestId, decision, subject_digest: subjectDigest }, controlId);
3790
+ }
3791
+ /** Answer an input request. */
3792
+ answer(runId, requestId, value, controlId) {
3793
+ return this.control(runId, { command: "provide_input", request_id: requestId, value }, controlId);
3794
+ }
3795
+ /**
3796
+ * Follow a run: replay after `afterSeq`, then live, until the run ends.
3797
+ *
3798
+ * Events that reach the socket before the subscription is known here are
3799
+ * not lost: the durable log is read once the listener is in place, live
3800
+ * events are held until that read is merged, and every seq is delivered
3801
+ * exactly once, in order.
3802
+ */
3803
+ async subscribe(runId, options = {}) {
3804
+ let last = options.afterSeq ?? 0;
3805
+ let ready = false;
3806
+ let closed = false;
3807
+ const held = [];
3808
+ const buffer = [];
3809
+ const waiters = [];
3810
+ let resolveDone;
3811
+ const done = new Promise((r) => resolveDone = r);
3812
+ const close = (ended) => {
3813
+ if (closed) return;
3814
+ closed = true;
3815
+ if (ended) options.onEnd?.(last);
3816
+ for (const w of waiters.splice(0)) w({ value: void 0, done: true });
3817
+ resolveDone();
3818
+ };
3819
+ const deliver = (ev) => {
3820
+ if (closed || ev.seq <= last) return;
3821
+ last = ev.seq;
3822
+ options.onEvent?.(ev);
3823
+ const w = waiters.shift();
3824
+ if (w) w({ value: ev, done: false });
3825
+ else buffer.push(ev);
3826
+ };
3827
+ const listener = (message) => {
3828
+ if (message.event_type === "agent_run_end") {
3829
+ if (ready) close(true);
3830
+ else held.push({ seq: Number.MAX_SAFE_INTEGER });
3831
+ return;
3832
+ }
3833
+ const ev = message.payload;
3834
+ if (ready) deliver(ev);
3835
+ else held.push(ev);
3836
+ };
3837
+ const response = await this.call("agent_run_subscribe" /* AgentRunSubscribe */, {
3838
+ run_id: runId,
3839
+ after_seq: last
3840
+ });
3841
+ const subscriptionId = response.subscription_id;
3842
+ this.eventHandler.addFlowEventListener(subscriptionId, listener);
3843
+ const unsubscribe = async () => {
3844
+ this.eventHandler.removeFlowEventListener(subscriptionId);
3845
+ close(false);
3846
+ await this.call("agent_run_unsubscribe" /* AgentRunUnsubscribe */, { subscription_id: subscriptionId }).catch(() => void 0);
3847
+ };
3848
+ options.signal?.addEventListener("abort", () => void unsubscribe(), { once: true });
3849
+ for (; ; ) {
3850
+ const page = await this.events(runId, last, 500);
3851
+ page.forEach(deliver);
3852
+ if (page.length < 500) break;
3853
+ }
3854
+ ready = true;
3855
+ const endSeen = held.some((e) => e.seq === Number.MAX_SAFE_INTEGER);
3856
+ held.filter((e) => e.seq !== Number.MAX_SAFE_INTEGER).sort((a, b) => a.seq - b.seq).forEach(deliver);
3857
+ if (endSeen) close(true);
3858
+ else {
3859
+ const view = await this.get(runId).catch(() => null);
3860
+ const finalized = !view?.run?.domain || view.run.domain.finalized !== false;
3861
+ if (view && TERMINAL.includes(view.status) && finalized) {
3862
+ const tail = await this.events(runId, last, 500).catch(() => []);
3863
+ tail.forEach(deliver);
3864
+ this.eventHandler.removeFlowEventListener(subscriptionId);
3865
+ close(true);
3866
+ }
3867
+ }
3868
+ void done.then(() => this.eventHandler.removeFlowEventListener(subscriptionId));
3869
+ return {
3870
+ get lastSeq() {
3871
+ return last;
3872
+ },
3873
+ done,
3874
+ unsubscribe,
3875
+ [Symbol.asyncIterator]() {
3876
+ return {
3877
+ next: () => {
3878
+ const ev = buffer.shift();
3879
+ if (ev) return Promise.resolve({ value: ev, done: false });
3880
+ if (closed) return Promise.resolve({ value: void 0, done: true });
3881
+ return new Promise((r) => waiters.push(r));
3882
+ },
3883
+ return: async () => {
3884
+ await unsubscribe();
3885
+ return { value: void 0, done: true };
3886
+ }
3887
+ };
3888
+ }
3889
+ };
3890
+ }
3891
+ };
3892
+
3893
+ // src/functions-api.ts
3894
+ var FunctionsApi = class {
3895
+ constructor(repository, context, sendRequest) {
3896
+ this.context = { ...context, repository };
3897
+ this.sendRequest = sendRequest;
3898
+ }
3899
+ /**
3900
+ * Invoke a server-side function by name (async).
3901
+ *
3902
+ * Queues a `FunctionExecution` job and returns the execution and job IDs
3903
+ * immediately. The function runs asynchronously on the server.
3904
+ *
3905
+ * @param functionName - Name of the function to invoke
3906
+ * @param input - Input data passed to the function
3907
+ * @returns Execution ID and job ID for tracking
3908
+ */
3909
+ async invoke(functionName, input, options) {
3910
+ const effectiveRequestTimeoutMs = options?.requestTimeoutMs;
3911
+ let effectiveWaitTimeoutMs = options?.waitTimeoutMs;
3912
+ if (options?.waitForResult) {
3913
+ const transportTimeoutBudget = effectiveRequestTimeoutMs ?? 3e4;
3914
+ effectiveWaitTimeoutMs = Math.min(
3915
+ effectiveWaitTimeoutMs ?? transportTimeoutBudget,
3090
3916
  transportTimeoutBudget
3091
3917
  );
3092
3918
  }
@@ -3153,6 +3979,104 @@ var HttpFunctionsApi = class {
3153
3979
  }
3154
3980
  };
3155
3981
 
3982
+ // src/search-api.ts
3983
+ var ASK_FUNCTION = "ask";
3984
+ var DEFAULT_LIMIT = 8;
3985
+ var MAX_LIMIT = 50;
3986
+ var SearchApi = class {
3987
+ constructor(runSql, runFunction) {
3988
+ this.runSql = runSql;
3989
+ this.runFunction = runFunction;
3990
+ }
3991
+ /**
3992
+ * Find the passages that match a query, by meaning and by keyword at once.
3993
+ *
3994
+ * No model is involved, so this is the call for a search box, a "related
3995
+ * content" list, or anything assembling its own context.
3996
+ */
3997
+ async search(query, options) {
3998
+ if (!query || !query.trim()) {
3999
+ throw new Error("search(query) requires a non-empty query");
4000
+ }
4001
+ if (!options || !options.workspaces || !options.workspaces.trim()) {
4002
+ throw new Error(
4003
+ "search() requires `workspaces` \u2014 name a workspace, a list, a glob, or 'ALL READABLE'. An unscoped search on a public site is how draft content reaches a visitor."
4004
+ );
4005
+ }
4006
+ const limit = Math.min(
4007
+ Math.max(Math.floor(options.limit ?? DEFAULT_LIMIT), 1),
4008
+ MAX_LIMIT
4009
+ );
4010
+ const distance = Number.isFinite(options.maxDistance) ? `, max_distance => ${Number(options.maxDistance)}` : "";
4011
+ const sql = `
4012
+ SELECT node_id, path, name, score, chunk_index, chunk_text, chunk_text_source
4013
+ FROM HYBRID_SEARCH($1, ${limit},
4014
+ workspaces => $2,
4015
+ granularity => 'chunk'${distance})
4016
+ `;
4017
+ const result = await this.runSql(sql, [query.trim(), options.workspaces.trim()]);
4018
+ const rows = result?.rows ?? [];
4019
+ return rows.map((row) => ({
4020
+ path: String(row.path ?? ""),
4021
+ nodeId: String(row.node_id ?? ""),
4022
+ name: String(row.name ?? ""),
4023
+ // `0` is a real chunk index — a document that was never split — so it
4024
+ // must not be collapsed into "missing" by a falsy check.
4025
+ chunkIndex: row.chunk_index == null ? 0 : Number(row.chunk_index),
4026
+ text: String(row.chunk_text ?? ""),
4027
+ textIsExact: row.chunk_text_source === "exact",
4028
+ score: Number(row.score ?? 0)
4029
+ })).filter((passage) => passage.text.length > 0);
4030
+ }
4031
+ /**
4032
+ * Answer a question from the stored content, with citations.
4033
+ *
4034
+ * Retrieves, judges whether what came back answers the question, rewrites the
4035
+ * query once if it does not, and answers from those passages only.
4036
+ */
4037
+ async ask(question, options) {
4038
+ if (!question || !question.trim()) {
4039
+ throw new Error("ask(question) requires a non-empty question");
4040
+ }
4041
+ if (!options || !options.workspaces || !options.workspaces.trim()) {
4042
+ throw new Error(
4043
+ "ask() requires `workspaces` \u2014 name a workspace, a list, a glob, or 'ALL READABLE'. An answer is quoted back to whoever asked, so the corpus it may be drawn from is stated, never assumed."
4044
+ );
4045
+ }
4046
+ const raw = await this.runFunction(ASK_FUNCTION, {
4047
+ question: question.trim(),
4048
+ workspaces: options.workspaces.trim(),
4049
+ limit: options.limit,
4050
+ model: options.model,
4051
+ use_graph: options.useGraph
4052
+ });
4053
+ if (raw && typeof raw === "object" && "error" in raw && raw.error) {
4054
+ throw new Error(`ask failed: ${String(raw.error)}`);
4055
+ }
4056
+ const citations = Array.isArray(raw?.citations) ? raw.citations : [];
4057
+ return {
4058
+ answer: String(raw?.answer ?? ""),
4059
+ // Absent counts as NOT grounded. This flag is the one a caller branches
4060
+ // on before showing an answer, so an unexpected response shape must fail
4061
+ // towards "we found nothing", never towards "trust this".
4062
+ grounded: raw?.grounded === true,
4063
+ citations: citations.map((c) => ({
4064
+ marker: Number(c.marker ?? 0),
4065
+ path: String(c.path ?? ""),
4066
+ nodeId: String(c.node_id ?? ""),
4067
+ name: String(c.title ?? ""),
4068
+ chunkIndex: c.chunk_index == null ? 0 : Number(c.chunk_index),
4069
+ textIsExact: c.text_is_exact === true
4070
+ })),
4071
+ attempts: Array.isArray(raw?.attempts) ? raw.attempts.map((a) => ({
4072
+ query: String(a.query ?? ""),
4073
+ passages: Number(a.passages ?? 0)
4074
+ })) : [],
4075
+ model: String(raw?.model ?? "")
4076
+ };
4077
+ }
4078
+ };
4079
+
3156
4080
  // src/errors.ts
3157
4081
  var RaisinError = class extends Error {
3158
4082
  constructor(message, code, details) {
@@ -3241,6 +4165,7 @@ var SSEClient = class {
3241
4165
  this._lastEventId = options.lastEventId;
3242
4166
  this.autoReconnect = options.reconnect?.enabled ?? true;
3243
4167
  this.externalSignal = options.signal;
4168
+ this.inactivityTimeoutMs = options.inactivityTimeoutMs;
3244
4169
  this.eventTypeSet = options.eventTypes?.length ? new Set(options.eventTypes) : null;
3245
4170
  this.reconnectManager = new ReconnectManager({
3246
4171
  initialDelay: options.reconnect?.initialDelay ?? 1e3,
@@ -3384,6 +4309,23 @@ var SSEClient = class {
3384
4309
  if (err.name === "AbortError" || err instanceof RaisinAbortError) {
3385
4310
  return;
3386
4311
  }
4312
+ if (err instanceof RaisinTimeoutError && err.code === "SSE_INACTIVITY_TIMEOUT") {
4313
+ logger.warn("SSE inactivity timeout:", err.message);
4314
+ if (!this.autoReconnect) {
4315
+ this.setState("disconnected");
4316
+ onError?.(err);
4317
+ throw err;
4318
+ }
4319
+ this.setState("reconnecting");
4320
+ const scheduled2 = await this.waitForReconnect();
4321
+ if (!scheduled2) {
4322
+ logger.warn("SSE max reconnection attempts reached");
4323
+ this.setState("disconnected");
4324
+ onError?.(err);
4325
+ throw err;
4326
+ }
4327
+ continue;
4328
+ }
3387
4329
  logger.error("SSE connection error:", err.message);
3388
4330
  onError?.(err);
3389
4331
  if (!this.autoReconnect) {
@@ -3432,7 +4374,9 @@ var SSEClient = class {
3432
4374
  fetchOptions.headers["Content-Type"] = "application/json";
3433
4375
  fetchOptions.body = JSON.stringify(this.options.body);
3434
4376
  }
3435
- const response = await this.fetchImpl(this.url, fetchOptions);
4377
+ const response = await this.withInactivityTimeout(
4378
+ this.fetchImpl(this.url, fetchOptions)
4379
+ );
3436
4380
  if (!response.ok) {
3437
4381
  const text = await response.text().catch(() => "");
3438
4382
  const msg = `SSE connection failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`;
@@ -3455,7 +4399,7 @@ var SSEClient = class {
3455
4399
  let buffer = "";
3456
4400
  try {
3457
4401
  while (true) {
3458
- const { done, value } = await reader.read();
4402
+ const { done, value } = await this.withInactivityTimeout(reader.read());
3459
4403
  if (done) break;
3460
4404
  buffer += decoder.decode(value, { stream: true });
3461
4405
  const blocks = buffer.split("\n\n");
@@ -3486,9 +4430,48 @@ var SSEClient = class {
3486
4430
  }
3487
4431
  }
3488
4432
  } finally {
3489
- reader.releaseLock();
4433
+ try {
4434
+ reader.releaseLock();
4435
+ } catch {
4436
+ }
3490
4437
  }
3491
4438
  }
4439
+ /**
4440
+ * Race a stream operation against the inactivity timeout.
4441
+ *
4442
+ * If no bytes (or response) arrive within `inactivityTimeoutMs`, the
4443
+ * internal connection is aborted and the operation rejects with a
4444
+ * `RaisinTimeoutError` (code `SSE_INACTIVITY_TIMEOUT`). The timer is
4445
+ * effectively reset on every chunk because each `reader.read()` is
4446
+ * wrapped individually.
4447
+ */
4448
+ withInactivityTimeout(promise) {
4449
+ const timeoutMs = this.inactivityTimeoutMs;
4450
+ if (!timeoutMs || timeoutMs <= 0) return promise;
4451
+ return new Promise((resolve, reject) => {
4452
+ const timer = setTimeout(() => {
4453
+ promise.catch(() => void 0);
4454
+ this.abortController?.abort();
4455
+ reject(
4456
+ new RaisinTimeoutError(
4457
+ `SSE stream inactive for ${timeoutMs}ms`,
4458
+ "SSE_INACTIVITY_TIMEOUT",
4459
+ timeoutMs
4460
+ )
4461
+ );
4462
+ }, timeoutMs);
4463
+ promise.then(
4464
+ (value) => {
4465
+ clearTimeout(timer);
4466
+ resolve(value);
4467
+ },
4468
+ (error) => {
4469
+ clearTimeout(timer);
4470
+ reject(error);
4471
+ }
4472
+ );
4473
+ });
4474
+ }
3492
4475
  /**
3493
4476
  * Wait for the reconnect manager to schedule a reconnection attempt.
3494
4477
  * Returns true if reconnection was scheduled, false if max attempts reached.
@@ -3513,9 +4496,27 @@ function isRecoveredDoneEvent(event) {
3513
4496
  return typeof done.content !== "string" || done.content.trim().length === 0;
3514
4497
  }
3515
4498
 
4499
+ // src/utils/home-path.ts
4500
+ var ACCESS_CONTROL_PREFIX = "/raisin:access_control";
4501
+ function normalizeHomePath(home) {
4502
+ if (home === null || home === void 0) return null;
4503
+ let path = home.trim();
4504
+ if (path.length === 0) return null;
4505
+ if (path === ACCESS_CONTROL_PREFIX) return "/";
4506
+ if (path.startsWith(`${ACCESS_CONTROL_PREFIX}/`)) {
4507
+ path = path.slice(ACCESS_CONTROL_PREFIX.length);
4508
+ }
4509
+ while (path.length > 1 && path.endsWith("/")) {
4510
+ path = path.slice(0, -1);
4511
+ }
4512
+ return path;
4513
+ }
4514
+
3516
4515
  // src/conversations.ts
4516
+ var DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS = 12e4;
3517
4517
  var ConversationManager = class {
3518
- constructor(baseUrl, repository, authManager, options = {}, executeSql) {
4518
+ constructor(baseUrl, repository, authManager, options = {}, executeSql, visitor) {
4519
+ this.visitor = visitor;
3519
4520
  this.baseUrl = baseUrl.replace(/\/$/, "");
3520
4521
  this.repository = repository;
3521
4522
  this.authManager = authManager;
@@ -3535,10 +4536,11 @@ var ConversationManager = class {
3535
4536
  );
3536
4537
  if (!result?.rows?.[0]) throw new Error("User not authenticated");
3537
4538
  const row = result.rows[0];
3538
- this.cachedUserHome = row.home;
3539
- this.cachedUserId = row.user_id || this.cachedUserHome;
3540
- if (!this.cachedUserHome) throw new Error("User home path not found");
3541
- return { userId: this.cachedUserId, userHome: this.cachedUserHome };
4539
+ const userHome = normalizeHomePath(row.home);
4540
+ if (!userHome) throw new Error("User home path not found");
4541
+ this.cachedUserHome = userHome;
4542
+ this.cachedUserId = row.user_id || userHome;
4543
+ return { userId: this.cachedUserId, userHome };
3542
4544
  }
3543
4545
  // ==========================================================================
3544
4546
  // Conversation listing
@@ -3635,6 +4637,45 @@ var ConversationManager = class {
3635
4637
  ]
3636
4638
  };
3637
4639
  }
4640
+ /**
4641
+ * Start (or resume) an ANONYMOUS conversation with an agent — a public
4642
+ * website's chat, without login.
4643
+ *
4644
+ * Works on an anonymous connection only, and only with an agent that allows
4645
+ * anonymous visitors (`anonymous.enabled: true` on its raisin:AIAgent). The
4646
+ * server keeps the conversation in an ephemeral visitor session that only
4647
+ * this browser session can read; the session secret is kept in
4648
+ * sessionStorage (override with `options.store`), so a reload continues the
4649
+ * same conversation. Then use `sendMessage`, `getMessages`, `subscribe` and
4650
+ * `ConversationStore` / `useConversation` exactly as for a signed-in user.
4651
+ *
4652
+ * @example
4653
+ * ```typescript
4654
+ * const convo = await db.conversations.startAnonymous('/agents/website-assistant');
4655
+ * for await (const ev of db.conversations.sendMessage(convo.conversationPath, 'Hello')) {
4656
+ * if (ev.type === 'text_chunk') append(ev.text);
4657
+ * if (ev.type === 'failed') showLimit(ev.code); // e.g. RATE_LIMITED, LIMIT_REACHED
4658
+ * }
4659
+ * ```
4660
+ */
4661
+ async startAnonymous(agent, options) {
4662
+ if (!this.visitor) {
4663
+ throw new Error("Anonymous chat needs a WebSocket database: use client.database(repo).");
4664
+ }
4665
+ return this.visitor.start(this.normalizeAgentPath(agent), options);
4666
+ }
4667
+ /** Forget the stored anonymous session for `agent` (the next start begins a new one). */
4668
+ forgetAnonymous(agent, options) {
4669
+ this.visitor?.forget(this.normalizeAgentPath(agent), options);
4670
+ }
4671
+ /** Whether this tab remembers an anonymous conversation with `agent`. */
4672
+ hasStoredAnonymous(agent, options) {
4673
+ return this.visitor?.remembers(this.normalizeAgentPath(agent), options) ?? false;
4674
+ }
4675
+ /** Whether `conversationPath` is an anonymous conversation of this client. */
4676
+ isAnonymous(conversationPath) {
4677
+ return this.visitor?.isAnonymous(conversationPath) ?? false;
4678
+ }
3638
4679
  /**
3639
4680
  * Open an existing conversation by path.
3640
4681
  * Returns null if not found.
@@ -3680,6 +4721,7 @@ var ConversationManager = class {
3680
4721
  * Mark a conversation as read by setting unread_count to 0.
3681
4722
  */
3682
4723
  async markAsRead(conversationPath) {
4724
+ if (this.isAnonymous(conversationPath)) return;
3683
4725
  const workspace = "raisin:access_control";
3684
4726
  await this.sqlQuery(
3685
4727
  `UPDATE '${workspace}' SET properties = jsonb_set(properties, '{unread_count}', '0'::jsonb) WHERE path = $1 AND node_type = 'raisin:Conversation'`,
@@ -3690,6 +4732,7 @@ var ConversationManager = class {
3690
4732
  * Mark a single message as read by the current user.
3691
4733
  */
3692
4734
  async markMessageAsRead(messagePath) {
4735
+ if (messagePath.startsWith("/visitors/")) return;
3693
4736
  const workspace = "raisin:access_control";
3694
4737
  const now = (/* @__PURE__ */ new Date()).toISOString();
3695
4738
  await this.sqlQuery(
@@ -3714,6 +4757,18 @@ var ConversationManager = class {
3714
4757
  */
3715
4758
  async *sendMessage(conversationPath, content, options) {
3716
4759
  const shouldStream = options?.stream !== false;
4760
+ if (this.visitor?.isAnonymous(conversationPath)) {
4761
+ if (!shouldStream) {
4762
+ await this.visitor.post(conversationPath, content);
4763
+ yield { type: "waiting", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
4764
+ return;
4765
+ }
4766
+ yield* this.visitor.stream(conversationPath, content, {
4767
+ signal: options?.signal,
4768
+ inactivityTimeoutMs: options?.inactivityTimeoutMs ?? DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS
4769
+ });
4770
+ return;
4771
+ }
3717
4772
  logger.debug("[ConversationManager] Starting SSE stream for message send");
3718
4773
  if (!shouldStream) {
3719
4774
  await this.createUserMessage(conversationPath, content);
@@ -3733,13 +4788,15 @@ var ConversationManager = class {
3733
4788
  fetch: this.fetchImpl,
3734
4789
  signal: options?.signal,
3735
4790
  method: "POST",
3736
- body: { channel: streamTarget.channel, path: streamTarget.path }
4791
+ body: { channel: streamTarget.channel, path: streamTarget.path },
4792
+ inactivityTimeoutMs: options?.inactivityTimeoutMs ?? DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS
3737
4793
  });
3738
4794
  const iterator = sse[Symbol.asyncIterator]();
3739
4795
  let pendingFirst = iterator.next();
3740
4796
  try {
3741
4797
  await sse.waitUntilConnected();
3742
4798
  } catch {
4799
+ pendingFirst?.catch(() => void 0);
3743
4800
  pendingFirst = null;
3744
4801
  sse.close();
3745
4802
  logger.debug("SSE connection failed, falling back to create-only");
@@ -3776,6 +4833,15 @@ var ConversationManager = class {
3776
4833
  return;
3777
4834
  }
3778
4835
  }
4836
+ } catch (error) {
4837
+ if (error instanceof RaisinTimeoutError && error.code === "SSE_INACTIVITY_TIMEOUT") {
4838
+ logger.warn(
4839
+ `[ConversationManager] SSE stream inactive for ${error.timeoutMs}ms, ending turn with synthetic waiting event`
4840
+ );
4841
+ yield { type: "waiting", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
4842
+ return;
4843
+ }
4844
+ throw error;
3779
4845
  } finally {
3780
4846
  sse.close();
3781
4847
  }
@@ -3790,10 +4856,23 @@ var ConversationManager = class {
3790
4856
  */
3791
4857
  async createUserMessage(conversationPath, content) {
3792
4858
  logger.debug("[ConversationManager] Creating user message", { path: conversationPath });
4859
+ if (this.visitor?.isAnonymous(conversationPath)) {
4860
+ await this.visitor.post(conversationPath, content);
4861
+ return;
4862
+ }
3793
4863
  const workspace = "raisin:access_control";
4864
+ const properties = await this.loadConversationProperties(conversationPath, workspace);
4865
+ const flowInstanceId = typeof properties?.flow_instance_id === "string" ? properties.flow_instance_id.trim() : "";
4866
+ if (flowInstanceId) {
4867
+ await this.httpRequest({
4868
+ method: "POST",
4869
+ path: `/api/flows/${this.repository}/instances/${encodeURIComponent(flowInstanceId)}/resume`,
4870
+ body: { resume_data: { message: content } }
4871
+ });
4872
+ return;
4873
+ }
3794
4874
  const { userId, userHome } = await this.ensureCurrentUser();
3795
4875
  const messageId = crypto.randomUUID();
3796
- const properties = await this.loadConversationProperties(conversationPath, workspace);
3797
4876
  const participants = Array.isArray(properties?.participants) ? properties.participants : [];
3798
4877
  let recipientId = participants.find(
3799
4878
  (p) => typeof p === "string" && p.length > 0 && p !== userId && p !== userHome
@@ -3832,6 +4911,12 @@ var ConversationManager = class {
3832
4911
  * initiated messages, etc.). Auto-reconnects on disconnect.
3833
4912
  */
3834
4913
  subscribe(conversationPath, onEvent, options) {
4914
+ if (this.visitor?.isAnonymous(conversationPath)) {
4915
+ const stop = this.visitor.listen(conversationPath, onEvent);
4916
+ options?.signal?.addEventListener("abort", stop, { once: true });
4917
+ return { unsubscribe: stop, waitUntilConnected: async () => {
4918
+ } };
4919
+ }
3835
4920
  logger.info("[ConversationManager] Subscribing to SSE", { path: conversationPath });
3836
4921
  const sseUrl = `${this.baseUrl}/api/conversations/${this.repository}/events`;
3837
4922
  const headers = {};
@@ -3998,7 +5083,17 @@ var ConversationManager = class {
3998
5083
  tasks.push({
3999
5084
  id: row.id,
4000
5085
  title: row.properties?.title ?? row.name,
4001
- status: row.properties?.status ?? "pending"
5086
+ status: row.properties?.status ?? "pending",
5087
+ delegation: row.properties?.delegation_id ? {
5088
+ id: row.properties.delegation_id,
5089
+ status: row.properties.delegation_status ?? "unknown",
5090
+ agentRef: row.properties.delegated_agent_ref?.["raisin:path"] ?? row.properties.delegated_agent_ref?.["raisin:ref"] ?? (typeof row.properties.delegated_agent_ref === "string" ? row.properties.delegated_agent_ref : void 0),
5091
+ flowInstanceId: row.properties.delegation_flow_instance_id ?? void 0,
5092
+ branch: row.properties.delegation_branch ?? void 0,
5093
+ baseBranch: row.properties.delegation_base_branch ?? void 0,
5094
+ result: row.properties.delegation_result,
5095
+ error: row.properties.delegation_error ?? void 0
5096
+ } : void 0
4002
5097
  });
4003
5098
  tasksByPlan.set(planPath, tasks);
4004
5099
  continue;
@@ -4255,12 +5350,278 @@ var ConversationManager = class {
4255
5350
  }
4256
5351
  async ensureFolderNode(workspace, path, title) {
4257
5352
  await this.sqlQuery(
4258
- `UPSERT INTO '${workspace}' (path, node_type, properties) VALUES ($1, 'raisin:Folder', $2::jsonb)`,
5353
+ `UPSERT INTO '${workspace}' (path, node_type, properties) VALUES ($1, 'raisin:AclFolder', $2::jsonb)`,
4259
5354
  [path, JSON.stringify({ title })]
4260
5355
  );
4261
5356
  }
4262
5357
  };
4263
5358
 
5359
+ // src/visitor-chat.ts
5360
+ function defaultVisitorSessionStore() {
5361
+ const memory = /* @__PURE__ */ new Map();
5362
+ const storage = () => {
5363
+ try {
5364
+ return typeof sessionStorage !== "undefined" ? sessionStorage : null;
5365
+ } catch {
5366
+ return null;
5367
+ }
5368
+ };
5369
+ return {
5370
+ get(key) {
5371
+ try {
5372
+ return storage()?.getItem(key) ?? memory.get(key) ?? null;
5373
+ } catch {
5374
+ return memory.get(key) ?? null;
5375
+ }
5376
+ },
5377
+ set(key, value) {
5378
+ memory.set(key, value);
5379
+ try {
5380
+ storage()?.setItem(key, value);
5381
+ } catch {
5382
+ }
5383
+ },
5384
+ remove(key) {
5385
+ memory.delete(key);
5386
+ try {
5387
+ storage()?.removeItem(key);
5388
+ } catch {
5389
+ }
5390
+ }
5391
+ };
5392
+ }
5393
+ var VisitorChatError = class extends Error {
5394
+ constructor(message, code) {
5395
+ super(message);
5396
+ this.name = "VisitorChatError";
5397
+ this.code = code;
5398
+ }
5399
+ };
5400
+ var REBIND_CODES = /* @__PURE__ */ new Set(["NO_SESSION", "UNKNOWN_CONVERSATION"]);
5401
+ function asError(err) {
5402
+ const e = err;
5403
+ return new VisitorChatError(e?.message ?? String(err), e?.code ?? "UNAVAILABLE");
5404
+ }
5405
+ var VisitorChat = class {
5406
+ constructor(repository, send, events, onReconnected) {
5407
+ this.repository = repository;
5408
+ this.send = send;
5409
+ this.events = events;
5410
+ this.byPath = /* @__PURE__ */ new Map();
5411
+ this.token = null;
5412
+ onReconnected?.(() => {
5413
+ void this.rebindAll();
5414
+ });
5415
+ }
5416
+ /** Whether `conversationPath` is a visitor conversation started here. */
5417
+ isAnonymous(conversationPath) {
5418
+ return this.byPath.has(conversationPath);
5419
+ }
5420
+ storeOf(options) {
5421
+ if (options.store === false) return null;
5422
+ return options.store ?? defaultVisitorSessionStore();
5423
+ }
5424
+ keyOf(agent, options) {
5425
+ return options.storageKey ?? `raisindb:visitor:${this.repository}:${agent}`;
5426
+ }
5427
+ /** Start (or resume) a visitor conversation with `agent` (`/agents/<name>`). */
5428
+ async start(agent, options = {}) {
5429
+ const store = this.storeOf(options);
5430
+ const key = this.keyOf(agent, options);
5431
+ const sessionToken = options.sessionToken ?? this.token ?? store?.get(`${key}:session`) ?? void 0;
5432
+ const conversationId = options.newConversation ? void 0 : options.conversationId ?? store?.get(`${key}:conversation`) ?? void 0;
5433
+ let res;
5434
+ try {
5435
+ res = await this.send(
5436
+ { agent, session_token: sessionToken, conversation_id: conversationId },
5437
+ "visitor_chat_start"
5438
+ );
5439
+ } catch (err) {
5440
+ const e = asError(err);
5441
+ if ((conversationId || sessionToken) && !options.conversationId && !options.sessionToken && ["UNKNOWN_CONVERSATION", "INVALID_CONVERSATION", "SESSION_EXPIRED", "SESSION_MISMATCH"].includes(e.code)) {
5442
+ store?.remove(`${key}:conversation`);
5443
+ if (e.code !== "UNKNOWN_CONVERSATION" && e.code !== "INVALID_CONVERSATION") {
5444
+ store?.remove(`${key}:session`);
5445
+ this.token = null;
5446
+ }
5447
+ return this.start(agent, { ...options, newConversation: true });
5448
+ }
5449
+ throw e;
5450
+ }
5451
+ const token = res.session_token ?? sessionToken ?? this.token;
5452
+ if (!token) throw new VisitorChatError("the server returned no session", "UNAVAILABLE");
5453
+ this.token = token;
5454
+ store?.set(`${key}:session`, token);
5455
+ store?.set(`${key}:conversation`, res.conversation_id);
5456
+ const path = res.conversation_path;
5457
+ let entry = this.byPath.get(path);
5458
+ if (!entry) {
5459
+ entry = {
5460
+ agent: res.agent,
5461
+ conversationId: res.conversation_id,
5462
+ subscriptionId: res.subscription_id,
5463
+ listeners: /* @__PURE__ */ new Set(),
5464
+ options
5465
+ };
5466
+ this.byPath.set(path, entry);
5467
+ }
5468
+ this.route(entry);
5469
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5470
+ return {
5471
+ id: res.conversation_id,
5472
+ type: "ai_chat",
5473
+ agentRef: res.agent,
5474
+ conversationPath: path,
5475
+ conversationWorkspace: res.conversation_workspace,
5476
+ anonymous: true,
5477
+ conversationId: res.conversation_id,
5478
+ sessionToken: token,
5479
+ limits: res.limits,
5480
+ initialEvents: [{ type: "waiting", timestamp: now }]
5481
+ };
5482
+ }
5483
+ /** Route the server's forwarded events for this conversation to its listeners. */
5484
+ route(entry) {
5485
+ this.events.removeFlowEventListener(entry.subscriptionId);
5486
+ this.events.addFlowEventListener(entry.subscriptionId, (msg) => {
5487
+ const event = msg.payload;
5488
+ if (!event || typeof event.type !== "string") return;
5489
+ for (const listener of entry.listeners) {
5490
+ try {
5491
+ listener(event);
5492
+ } catch (err) {
5493
+ logger.error("[VisitorChat] listener failed", err);
5494
+ }
5495
+ }
5496
+ });
5497
+ }
5498
+ /** Re-prove the session on a new connection, for every conversation started here. */
5499
+ async rebindAll() {
5500
+ for (const entry of this.byPath.values()) {
5501
+ try {
5502
+ await this.send(
5503
+ { agent: entry.agent, session_token: this.token, conversation_id: entry.conversationId },
5504
+ "visitor_chat_start"
5505
+ );
5506
+ this.route(entry);
5507
+ } catch (err) {
5508
+ logger.warn("[VisitorChat] could not resume a conversation after reconnect", err);
5509
+ }
5510
+ }
5511
+ }
5512
+ /** Listen to a visitor conversation's events. Returns an unsubscribe function. */
5513
+ listen(conversationPath, listener) {
5514
+ const entry = this.require(conversationPath);
5515
+ entry.listeners.add(listener);
5516
+ return () => {
5517
+ entry.listeners.delete(listener);
5518
+ };
5519
+ }
5520
+ require(conversationPath) {
5521
+ const entry = this.byPath.get(conversationPath);
5522
+ if (!entry) {
5523
+ throw new VisitorChatError(
5524
+ "not a visitor conversation of this client; call startAnonymous first",
5525
+ "UNKNOWN_CONVERSATION"
5526
+ );
5527
+ }
5528
+ return entry;
5529
+ }
5530
+ /** Make sure this connection is bound (after a reconnect nothing else may read the home). */
5531
+ async ensureBound(conversationPath) {
5532
+ const entry = this.require(conversationPath);
5533
+ await this.send(
5534
+ { agent: entry.agent, session_token: this.token, conversation_id: entry.conversationId },
5535
+ "visitor_chat_start"
5536
+ ).catch((err) => {
5537
+ throw asError(err);
5538
+ });
5539
+ }
5540
+ /** Send one visitor message. Events arrive through {@link listen}. */
5541
+ async post(conversationPath, text) {
5542
+ const entry = this.require(conversationPath);
5543
+ const once = async () => await this.send(
5544
+ { conversation_id: entry.conversationId, text },
5545
+ "visitor_chat_send"
5546
+ );
5547
+ try {
5548
+ const res = await once();
5549
+ return { messageId: res.message_id, messagesLeft: res.messages_left };
5550
+ } catch (err) {
5551
+ const e = asError(err);
5552
+ if (!REBIND_CODES.has(e.code)) throw e;
5553
+ await this.ensureBound(conversationPath);
5554
+ this.route(entry);
5555
+ const res = await once().catch((again) => {
5556
+ throw asError(again);
5557
+ });
5558
+ return { messageId: res.message_id, messagesLeft: res.messages_left };
5559
+ }
5560
+ }
5561
+ /**
5562
+ * Send one visitor message and stream the turn: `text_chunk`,
5563
+ * `tool_call_started`, `tool_call_completed`, … until `done`. A refusal
5564
+ * ends the stream with `failed` (its `code` says why). No event for
5565
+ * `inactivityTimeoutMs` ends it with a synthetic `waiting`.
5566
+ */
5567
+ async *stream(conversationPath, text, options = {}) {
5568
+ const queue = [];
5569
+ let wake = null;
5570
+ const push = (event) => {
5571
+ queue.push(event);
5572
+ wake?.();
5573
+ };
5574
+ const stop = this.listen(conversationPath, push);
5575
+ const onAbort = () => push({ type: "failed", error: "aborted", code: "ABORTED", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
5576
+ options.signal?.addEventListener("abort", onAbort, { once: true });
5577
+ const timeout = options.inactivityTimeoutMs ?? 12e4;
5578
+ try {
5579
+ try {
5580
+ await this.post(conversationPath, text);
5581
+ } catch (err) {
5582
+ const e = asError(err);
5583
+ yield { type: "failed", error: e.message, code: e.code, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5584
+ return;
5585
+ }
5586
+ for (; ; ) {
5587
+ if (!queue.length) {
5588
+ const idle = await new Promise((resolve) => {
5589
+ const timer = timeout > 0 ? setTimeout(() => resolve(true), timeout) : null;
5590
+ wake = () => {
5591
+ if (timer) clearTimeout(timer);
5592
+ resolve(false);
5593
+ };
5594
+ });
5595
+ wake = null;
5596
+ if (idle && !queue.length) {
5597
+ yield { type: "waiting", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5598
+ return;
5599
+ }
5600
+ }
5601
+ const event = queue.shift();
5602
+ yield event;
5603
+ if (event.type === "done" || event.type === "failed" || event.type === "waiting") return;
5604
+ }
5605
+ } finally {
5606
+ stop();
5607
+ options.signal?.removeEventListener("abort", onAbort);
5608
+ }
5609
+ }
5610
+ /** Whether the store remembers a conversation with `agent`. */
5611
+ remembers(agent, options = {}) {
5612
+ const store = this.storeOf(options);
5613
+ return !!store?.get(`${this.keyOf(agent, options)}:conversation`);
5614
+ }
5615
+ /** Forget the stored session (e.g. a "new chat" button that should start a new session). */
5616
+ forget(agent, options = {}) {
5617
+ const store = this.storeOf(options);
5618
+ const key = this.keyOf(agent, options);
5619
+ store?.remove(`${key}:session`);
5620
+ store?.remove(`${key}:conversation`);
5621
+ this.token = null;
5622
+ }
5623
+ };
5624
+
4264
5625
  // src/flow-client.ts
4265
5626
  var FlowClient = class _FlowClient {
4266
5627
  /**
@@ -4538,31 +5899,29 @@ var FlowClient = class _FlowClient {
4538
5899
  * Respond to a human task within a flow.
4539
5900
  *
4540
5901
  * When a flow is waiting on a human_task step (approval, input, review),
4541
- * use this method to submit the response and resume the flow.
5902
+ * use this method to submit the response. Completion is validated
5903
+ * (caller must be the task's assignee or an admin) and resumes the flow.
4542
5904
  *
4543
- * @param instanceId - Flow instance ID
4544
- * @param taskId - The human task node ID
4545
- * @param response - The human's response data
5905
+ * Note: prefer `InboxApi` for full inbox functionality (listing tasks,
5906
+ * task details); this is a convenience passthrough to the same endpoint.
5907
+ *
5908
+ * @param instanceId - Flow instance ID (unused; kept for API stability)
5909
+ * @param taskId - The inbox task node ID (or path)
5910
+ * @param response - The response data
4546
5911
  *
4547
5912
  * @example
4548
5913
  * ```typescript
4549
5914
  * // Approve a task
4550
- * await flows.respondToHumanTask('instance-123', 'step-5', {
5915
+ * await flows.respondToHumanTask('instance-123', 'task-abc', {
4551
5916
  * action: 'approve',
4552
5917
  * comment: 'Looks good!',
4553
5918
  * });
4554
- *
4555
- * // Provide input for an input task
4556
- * await flows.respondToHumanTask('instance-123', 'step-3', {
4557
- * name: 'John Doe',
4558
- * email: 'john@example.com',
4559
- * });
4560
5919
  * ```
4561
5920
  */
4562
- async respondToHumanTask(instanceId, taskId, response, options) {
5921
+ async respondToHumanTask(_instanceId, taskId, response, options) {
4563
5922
  await this.request({
4564
5923
  method: "POST",
4565
- path: `/api/flows/${this.repository}/instances/${instanceId}/tasks/${taskId}/respond`,
5924
+ path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}/complete`,
4566
5925
  body: { response },
4567
5926
  signal: options?.signal
4568
5927
  });
@@ -4701,6 +6060,134 @@ var FlowClient = class _FlowClient {
4701
6060
  }
4702
6061
  };
4703
6062
 
6063
+ // src/inbox.ts
6064
+ var InboxApi = class {
6065
+ constructor(baseUrl, repository, authManager, options = {}) {
6066
+ this.baseUrl = baseUrl.replace(/\/$/, "");
6067
+ this.repository = repository;
6068
+ this.authManager = authManager;
6069
+ this.fetchImpl = options.fetch ?? fetch;
6070
+ this.requestTimeout = options.requestTimeout ?? 3e4;
6071
+ }
6072
+ /**
6073
+ * List the caller's inbox tasks.
6074
+ *
6075
+ * Tasks are sorted pending-first, then by priority (highest first) and
6076
+ * due time (earliest first).
6077
+ */
6078
+ async listTasks(options = {}) {
6079
+ const params = new URLSearchParams();
6080
+ if (options.status) params.set("status", options.status);
6081
+ if (options.assignee) params.set("assignee", options.assignee);
6082
+ const query = params.size > 0 ? `?${params.toString()}` : "";
6083
+ return this.request({
6084
+ method: "GET",
6085
+ path: `/api/inbox/${this.repository}${query}`,
6086
+ signal: options.signal
6087
+ });
6088
+ }
6089
+ /**
6090
+ * Get a single inbox task by ID (or path).
6091
+ */
6092
+ async getTask(taskId, options) {
6093
+ return this.request({
6094
+ method: "GET",
6095
+ path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}`,
6096
+ signal: options?.signal
6097
+ });
6098
+ }
6099
+ /**
6100
+ * Complete an inbox task.
6101
+ *
6102
+ * The caller must be the task's assignee (or an admin). Completion marks
6103
+ * the task done, records who decided, and resumes the owning flow.
6104
+ *
6105
+ * @param taskId - Task node ID (or path)
6106
+ * @param response - The response payload:
6107
+ * - approval tasks: `{ action: '<option value>', comment?: string }`
6108
+ * - input tasks: the input value(s) matching the task's input_schema
6109
+ * - review/action tasks: any acknowledgement payload
6110
+ *
6111
+ * @example
6112
+ * ```typescript
6113
+ * await inbox.completeTask('task-123', { action: 'approve' });
6114
+ * ```
6115
+ */
6116
+ async completeTask(taskId, response, options) {
6117
+ return this.request({
6118
+ method: "POST",
6119
+ path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}/complete`,
6120
+ body: { response },
6121
+ signal: options?.signal
6122
+ });
6123
+ }
6124
+ // ==========================================================================
6125
+ // Internal HTTP request helper
6126
+ // ==========================================================================
6127
+ async request(options) {
6128
+ const url = `${this.baseUrl}${options.path}`;
6129
+ const headers = {
6130
+ "Content-Type": "application/json"
6131
+ };
6132
+ const token = this.authManager.getAccessToken();
6133
+ if (token) {
6134
+ headers["Authorization"] = `Bearer ${token}`;
6135
+ }
6136
+ const controller = new AbortController();
6137
+ const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
6138
+ if (options.signal) {
6139
+ if (options.signal.aborted) {
6140
+ clearTimeout(timeoutId);
6141
+ throw new RaisinAbortError();
6142
+ }
6143
+ options.signal.addEventListener("abort", () => controller.abort(), {
6144
+ once: true
6145
+ });
6146
+ }
6147
+ try {
6148
+ const response = await this.fetchImpl(url, {
6149
+ method: options.method,
6150
+ headers,
6151
+ body: options.body ? JSON.stringify(options.body) : void 0,
6152
+ signal: controller.signal
6153
+ });
6154
+ clearTimeout(timeoutId);
6155
+ if (!response.ok) {
6156
+ const errorText = await response.text();
6157
+ let errorMessage;
6158
+ try {
6159
+ const errorJson = JSON.parse(errorText);
6160
+ errorMessage = errorJson.message || errorJson.error || errorText;
6161
+ } catch {
6162
+ errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
6163
+ }
6164
+ throw classifyHttpError(response.status, errorMessage);
6165
+ }
6166
+ const contentType = response.headers.get("content-type");
6167
+ if (contentType?.includes("application/json")) {
6168
+ return await response.json();
6169
+ }
6170
+ return void 0;
6171
+ } catch (error) {
6172
+ clearTimeout(timeoutId);
6173
+ if (error instanceof RaisinAbortError || error instanceof RaisinTimeoutError) {
6174
+ throw error;
6175
+ }
6176
+ if (error instanceof Error && error.name === "AbortError") {
6177
+ if (options.signal?.aborted) {
6178
+ throw new RaisinAbortError();
6179
+ }
6180
+ throw new RaisinTimeoutError(
6181
+ `Request timeout after ${this.requestTimeout}ms`,
6182
+ "REQUEST_TIMEOUT",
6183
+ this.requestTimeout
6184
+ );
6185
+ }
6186
+ throw error;
6187
+ }
6188
+ }
6189
+ };
6190
+
4704
6191
  // src/database.ts
4705
6192
  var Database = class _Database {
4706
6193
  constructor(repository, context, sendRequest, eventHandler, branch, revision, getUploadManager, getSignAssetUrl, httpOptions) {
@@ -4890,6 +6377,13 @@ var Database = class _Database {
4890
6377
  tags() {
4891
6378
  return new Tags(this._context, this.sendRequest);
4892
6379
  }
6380
+ /**
6381
+ * Get SchedulerApi for one-shot scheduled invocations (time-based
6382
+ * function/flow runs).
6383
+ */
6384
+ scheduler() {
6385
+ return new SchedulerApi(this._context, this.sendRequest);
6386
+ }
4893
6387
  /**
4894
6388
  * Get FlowsApi for running flows via WebSocket
4895
6389
  */
@@ -4904,6 +6398,16 @@ var Database = class _Database {
4904
6398
  }
4905
6399
  return this._flowsApi;
4906
6400
  }
6401
+ /**
6402
+ * Durable agent runs over this WebSocket connection: find a subject's run,
6403
+ * follow it live (gap-free, resumable), control it, list its children.
6404
+ */
6405
+ runs() {
6406
+ if (!this._runsApi) {
6407
+ this._runsApi = new AgentRunsWsApi(this._context, this.sendRequest, this.eventHandler);
6408
+ }
6409
+ return this._runsApi;
6410
+ }
4907
6411
  /**
4908
6412
  * Get FunctionsApi for invoking server-side functions via WebSocket
4909
6413
  */
@@ -4917,6 +6421,73 @@ var Database = class _Database {
4917
6421
  }
4918
6422
  return this._functionsApi;
4919
6423
  }
6424
+ /**
6425
+ * The retrieval implementation behind {@link search} and {@link ask}.
6426
+ *
6427
+ * Private: callers get the two verbs, not the object, so how retrieval is
6428
+ * served stays ours to change.
6429
+ */
6430
+ searchApi() {
6431
+ if (!this._searchApi) {
6432
+ this._searchApi = new SearchApi(
6433
+ (sql, params) => this.executeSql(sql, params),
6434
+ async (name, input) => {
6435
+ const run = await this.functions().invoke(name, input, {
6436
+ waitForResult: true
6437
+ });
6438
+ return run?.result ?? run;
6439
+ }
6440
+ );
6441
+ }
6442
+ return this._searchApi;
6443
+ }
6444
+ /**
6445
+ * Find the passages matching a query, by meaning and by keyword at once.
6446
+ *
6447
+ * No model is involved — this is the call for a search box or for assembling
6448
+ * your own context. Returns one entry per PASSAGE, so several may come from
6449
+ * one document.
6450
+ *
6451
+ * @example
6452
+ * ```typescript
6453
+ * const passages = await db.search('how much notice to terminate', {
6454
+ * workspaces: 'stories',
6455
+ * });
6456
+ * passages[0].text; // the passage itself
6457
+ * passages[0].chunkIndex; // where in the document it came from
6458
+ * ```
6459
+ */
6460
+ async search(query, options) {
6461
+ return this.searchApi().search(query, options);
6462
+ }
6463
+ /**
6464
+ * Answer a question from the stored content, with citations.
6465
+ *
6466
+ * Retrieves, judges whether what came back answers the question, rewrites the
6467
+ * query once if it does not, and answers from those passages only. Check
6468
+ * `grounded` before showing the answer: when it is false nothing relevant was
6469
+ * found and the model was never asked.
6470
+ *
6471
+ * `workspaces` is required, the same as on {@link search}: an answer is
6472
+ * quoted back to whoever asked, so the corpus it may be drawn from is stated
6473
+ * rather than assumed. One name, a comma-separated list, a glob, or
6474
+ * `'ALL READABLE'`.
6475
+ *
6476
+ * A method rather than `functions().invoke('ask', ...)` on purpose — see the
6477
+ * note at the top of `search-api.ts`. `invoke` is the escape hatch for
6478
+ * functions YOU wrote.
6479
+ *
6480
+ * @example
6481
+ * ```typescript
6482
+ * const { answer, citations, grounded } = await db.ask(
6483
+ * 'How much notice do we have to give?',
6484
+ * { workspaces: 'stories, handbook' }, // one name, a list, a glob, or 'ALL READABLE'
6485
+ * );
6486
+ * ```
6487
+ */
6488
+ async ask(question, options) {
6489
+ return this.searchApi().ask(question, options);
6490
+ }
4920
6491
  /**
4921
6492
  * Get a pre-configured ConversationManager for managing conversations.
4922
6493
  *
@@ -4943,7 +6514,13 @@ var Database = class _Database {
4943
6514
  this.repository,
4944
6515
  this.httpOptions.authManager,
4945
6516
  {},
4946
- (query, params) => this.executeSql(query, params)
6517
+ (query, params) => this.executeSql(query, params),
6518
+ new VisitorChat(
6519
+ this.repository,
6520
+ (payload, requestType) => this.sendRequest(payload, requestType),
6521
+ this.eventHandler,
6522
+ this.httpOptions.onReconnected
6523
+ )
4947
6524
  );
4948
6525
  }
4949
6526
  return this._conversationManager;
@@ -4977,8 +6554,44 @@ var Database = class _Database {
4977
6554
  }
4978
6555
  return this._flowClient;
4979
6556
  }
6557
+ /**
6558
+ * Get a pre-configured InboxApi for human-in-the-loop tasks.
6559
+ *
6560
+ * Requires the Database to have been created via `RaisinClient.database()`
6561
+ * (which automatically provides HTTP context).
6562
+ *
6563
+ * @example
6564
+ * ```typescript
6565
+ * const db = client.database('my-repo');
6566
+ * const { tasks } = await db.inbox.listTasks({ status: 'pending' });
6567
+ * await db.inbox.completeTask(tasks[0].id, { action: 'approve' });
6568
+ * ```
6569
+ */
6570
+ get inbox() {
6571
+ if (!this._inboxApi) {
6572
+ if (!this.httpOptions?.httpBaseUrl || !this.httpOptions?.authManager) {
6573
+ throw new Error(
6574
+ "db.inbox requires HTTP context. Use client.database() to get a Database with inbox support."
6575
+ );
6576
+ }
6577
+ this._inboxApi = new InboxApi(
6578
+ this.httpOptions.httpBaseUrl,
6579
+ this.repository,
6580
+ this.httpOptions.authManager
6581
+ );
6582
+ }
6583
+ return this._inboxApi;
6584
+ }
4980
6585
  /**
4981
6586
  * Get EventHandler for subscribing to real-time events
6587
+ *
6588
+ * Path filter semantics for subscriptions (matched server-side):
6589
+ * - A plain path matches **only that exact node** — there is no implicit
6590
+ * prefix matching.
6591
+ * - `*` matches exactly one path segment (`/inbox/*` matches `/inbox/a`,
6592
+ * not `/inbox/a/b`).
6593
+ * - `**` matches recursively (`/inbox/**` matches all descendants of
6594
+ * `/inbox`, but not `/inbox` itself).
4982
6595
  */
4983
6596
  events() {
4984
6597
  return this.eventHandler;
@@ -6257,10 +7870,375 @@ var UploadManager = class {
6257
7870
  }
6258
7871
  };
6259
7872
 
7873
+ // src/http-schema.ts
7874
+ var HttpSchemaBase = class {
7875
+ constructor(repository, branch, client) {
7876
+ this.repository = repository;
7877
+ this.branch = branch;
7878
+ this.client = client;
7879
+ }
7880
+ /** Base management URL for this schema kind. */
7881
+ base() {
7882
+ return `/api/management/${this.repository}/${this.branch}/${this.kind}`;
7883
+ }
7884
+ /** List all schemas (or only published ones). */
7885
+ async list(publishedOnly = false) {
7886
+ const path = publishedOnly ? `${this.base()}/published` : this.base();
7887
+ const result = await this.client.managementRequest("GET", path);
7888
+ return Array.isArray(result) ? result : [];
7889
+ }
7890
+ /** Get a single schema by name. */
7891
+ async get(name) {
7892
+ return this.client.managementRequest("GET", `${this.base()}/${encodeURIComponent(name)}`);
7893
+ }
7894
+ /** Get the schema with full inheritance (`extends`) applied. */
7895
+ async getResolved(name) {
7896
+ return this.client.managementRequest(
7897
+ "GET",
7898
+ `${this.base()}/${encodeURIComponent(name)}/resolved`
7899
+ );
7900
+ }
7901
+ /** Create a schema. `name` is folded into the definition body if absent. */
7902
+ async create(name, definition, commit) {
7903
+ return this.client.managementRequest("POST", this.base(), {
7904
+ [this.defField]: { name, ...definition },
7905
+ commit
7906
+ });
7907
+ }
7908
+ /** Update an existing schema. */
7909
+ async update(name, definition, commit) {
7910
+ return this.client.managementRequest(
7911
+ "PUT",
7912
+ `${this.base()}/${encodeURIComponent(name)}`,
7913
+ { [this.defField]: { name, ...definition }, commit }
7914
+ );
7915
+ }
7916
+ /** Delete a schema. */
7917
+ async delete(name, commit) {
7918
+ return this.client.managementRequest(
7919
+ "DELETE",
7920
+ `${this.base()}/${encodeURIComponent(name)}`,
7921
+ commit
7922
+ );
7923
+ }
7924
+ /** Publish a schema. */
7925
+ async publish(name, commit) {
7926
+ return this.client.managementRequest(
7927
+ "POST",
7928
+ `${this.base()}/${encodeURIComponent(name)}/publish`,
7929
+ commit
7930
+ );
7931
+ }
7932
+ /** Unpublish a schema. */
7933
+ async unpublish(name, commit) {
7934
+ return this.client.managementRequest(
7935
+ "POST",
7936
+ `${this.base()}/${encodeURIComponent(name)}/unpublish`,
7937
+ commit
7938
+ );
7939
+ }
7940
+ };
7941
+ var HttpNodeTypes = class extends HttpSchemaBase {
7942
+ constructor() {
7943
+ super(...arguments);
7944
+ this.kind = "nodetypes";
7945
+ this.defField = "node_type";
7946
+ }
7947
+ /**
7948
+ * Get the resolved NodeType. Unlike archetypes/element types, NodeType
7949
+ * resolution can be workspace-aware (honors workspace NodeType pins).
7950
+ */
7951
+ async getResolved(name, opts) {
7952
+ const query = opts?.workspace ? `?workspace=${encodeURIComponent(opts.workspace)}` : "";
7953
+ return this.client.managementRequest(
7954
+ "GET",
7955
+ `${this.base()}/${encodeURIComponent(name)}/resolved${query}`
7956
+ );
7957
+ }
7958
+ };
7959
+ var HttpArchetypes = class extends HttpSchemaBase {
7960
+ constructor() {
7961
+ super(...arguments);
7962
+ this.kind = "archetypes";
7963
+ this.defField = "archetype";
7964
+ }
7965
+ };
7966
+ var HttpElementTypes = class extends HttpSchemaBase {
7967
+ constructor() {
7968
+ super(...arguments);
7969
+ this.kind = "elementtypes";
7970
+ this.defField = "element_type";
7971
+ }
7972
+ };
7973
+
7974
+ // src/mcp.ts
7975
+ var McpClient = class {
7976
+ constructor(transport) {
7977
+ this.transport = transport;
7978
+ }
7979
+ /**
7980
+ * List the tools this MCP server advertises to the calling identity.
7981
+ *
7982
+ * Wraps the `tools/list` JSON-RPC method. The visible set is RLS-scoped to
7983
+ * the authenticated caller (or the anonymous role).
7984
+ *
7985
+ * @returns The advertised tool descriptors.
7986
+ */
7987
+ async listTools() {
7988
+ const result = await this.transport.rpc("tools/list");
7989
+ return { tools: result?.tools ?? [] };
7990
+ }
7991
+ /**
7992
+ * Invoke a tool by name.
7993
+ *
7994
+ * Wraps the `tools/call` JSON-RPC method. The underlying `raisin:Function`
7995
+ * runs RLS-scoped to the calling identity. A domain-level tool failure is
7996
+ * reported via {@link McpCallToolResult.isError} (not thrown); transport and
7997
+ * protocol errors are thrown.
7998
+ *
7999
+ * @param name - Tool name from {@link listTools}.
8000
+ * @param args - Arguments object matching the tool's `inputSchema`.
8001
+ * @returns The tool's content blocks and optional structured content.
8002
+ */
8003
+ async callTool(name, args) {
8004
+ const result = await this.transport.rpc("tools/call", {
8005
+ name,
8006
+ arguments: args ?? {}
8007
+ });
8008
+ return result;
8009
+ }
8010
+ /**
8011
+ * Read a resource by its `raisin://` URI.
8012
+ *
8013
+ * Wraps the `resources/read` JSON-RPC method. JSON-only resources return
8014
+ * their properties as a `text` entry; byte-serving resources return base64
8015
+ * in `blob`.
8016
+ *
8017
+ * @param uri - `raisin://{workspace}/{path}` URI to read.
8018
+ * @returns One content entry per URI read.
8019
+ */
8020
+ async readResource(uri) {
8021
+ const result = await this.transport.rpc("resources/read", {
8022
+ uri
8023
+ });
8024
+ return { contents: result?.contents ?? [] };
8025
+ }
8026
+ /**
8027
+ * Subscribe to change notifications for a resource URI (or URI prefix).
8028
+ *
8029
+ * Opens a `subscriptions/listen` stream (SSE) with a `resourceSubscriptions`
8030
+ * filter naming the URI. The returned {@link McpResourceSubscription} is an
8031
+ * async iterable of {@link McpResourceUpdate}; the server's acknowledgement
8032
+ * frame and the closing response are consumed internally and not yielded.
8033
+ * Call `close()` to end the stream.
8034
+ *
8035
+ * @param uri - `raisin://` URI (or prefix) to watch.
8036
+ * @returns A live, closeable subscription.
8037
+ */
8038
+ subscribeResource(uri) {
8039
+ const stream = this.transport.subscribe("subscriptions/listen", {
8040
+ notifications: { resourceSubscriptions: [uri] }
8041
+ });
8042
+ return {
8043
+ close: () => stream.close(),
8044
+ async *[Symbol.asyncIterator]() {
8045
+ for await (const frame of stream) {
8046
+ if (frame.method === "notifications/resources/updated") {
8047
+ const params = frame.params ?? {};
8048
+ if (typeof params.uri === "string") {
8049
+ yield { uri: params.uri };
8050
+ }
8051
+ }
8052
+ }
8053
+ }
8054
+ };
8055
+ }
8056
+ };
8057
+
8058
+ // src/identity-auth.ts
8059
+ var IdentityAuthApi = class {
8060
+ constructor(repository, transport) {
8061
+ this.repository = repository;
8062
+ this.transport = transport;
8063
+ }
8064
+ get base() {
8065
+ return `/auth/${encodeURIComponent(this.repository)}`;
8066
+ }
8067
+ /**
8068
+ * Ask RaisinDB to email a sign-in link.
8069
+ *
8070
+ * The reply is an acknowledgement, never a token — the token only exists in
8071
+ * the email. It also resolves for an address that has no account, because a
8072
+ * response that differed would turn this endpoint into an account-existence
8073
+ * oracle.
8074
+ */
8075
+ async sendMagicLink(email, options) {
8076
+ return this.transport({
8077
+ method: "POST",
8078
+ path: `${this.base}/magic-link`,
8079
+ body: { email, redirect_url: options?.redirectUrl },
8080
+ skipAuth: true
8081
+ });
8082
+ }
8083
+ /**
8084
+ * Exchange a magic-link token for a session.
8085
+ *
8086
+ * Most applications never call this: following the emailed link hits
8087
+ * RaisinDB directly, which verifies and 302s to the redirect URL with the
8088
+ * tokens in the URL **fragment** (so they stay out of server logs and out
8089
+ * of the Referer header) — see {@link readTokensFromFragment}. Call this
8090
+ * only when handling the token yourself, and note it is single-use.
8091
+ */
8092
+ async verifyMagicLink(token) {
8093
+ return this.transport({
8094
+ method: "GET",
8095
+ path: `${this.base}/magic-link/verify?token=${encodeURIComponent(token)}`,
8096
+ skipAuth: true
8097
+ });
8098
+ }
8099
+ /**
8100
+ * Sign in with email and password, where the repository enables it.
8101
+ *
8102
+ * `rememberMe` chooses the lifetime of the sign-in: the session AND its refresh
8103
+ * token last 30 days with it and 24 hours without (both configurable per tenant:
8104
+ * `refresh_token_duration_days`, `refresh_token_session_hours`). Refreshing never
8105
+ * extends a session past that.
8106
+ */
8107
+ async login(email, password, options) {
8108
+ return this.transport({
8109
+ method: "POST",
8110
+ path: `${this.base}/login`,
8111
+ body: { email, password, remember_me: options?.rememberMe ?? false },
8112
+ skipAuth: true
8113
+ });
8114
+ }
8115
+ /** Create an account with email and password. */
8116
+ async register(email, password, options) {
8117
+ return this.transport({
8118
+ method: "POST",
8119
+ path: `${this.base}/register`,
8120
+ body: { email, password, display_name: options?.displayName },
8121
+ skipAuth: true
8122
+ });
8123
+ }
8124
+ /** Trade a refresh token for a fresh pair. */
8125
+ async refresh(refreshToken) {
8126
+ return this.transport({
8127
+ method: "POST",
8128
+ path: `${this.base}/refresh`,
8129
+ body: { refresh_token: refreshToken },
8130
+ skipAuth: true
8131
+ });
8132
+ }
8133
+ /**
8134
+ * Which sign-in methods to render.
8135
+ *
8136
+ * Ask rather than hard-code: magic link and password are per-repository
8137
+ * configuration, and a button for a disabled method is a dead end.
8138
+ */
8139
+ async providers() {
8140
+ return this.transport({
8141
+ method: "GET",
8142
+ path: `${this.base}/providers`,
8143
+ skipAuth: true
8144
+ });
8145
+ }
8146
+ /** Who the current token belongs to in this repository. */
8147
+ async me() {
8148
+ return this.transport({ method: "GET", path: `${this.base}/me` });
8149
+ }
8150
+ };
8151
+ function readTokensFromFragment(hashOrUrl) {
8152
+ const hash = hashOrUrl.includes("#") ? hashOrUrl.slice(hashOrUrl.indexOf("#") + 1) : hashOrUrl;
8153
+ if (!hash) return null;
8154
+ const params = new URLSearchParams(hash);
8155
+ const accessToken = params.get("access_token");
8156
+ if (!accessToken) return null;
8157
+ return { accessToken, refreshToken: params.get("refresh_token") };
8158
+ }
8159
+
8160
+ // src/node-dev.ts
8161
+ var NodeDevApi = class {
8162
+ constructor(repository, transport) {
8163
+ this.repository = repository;
8164
+ this.transport = transport;
8165
+ }
8166
+ /** Call any method by name (the typed helpers below all use this). */
8167
+ call(method, request) {
8168
+ const branch = typeof request.branch === "string" ? `?branch=${encodeURIComponent(request.branch)}` : "";
8169
+ return this.transport.request(
8170
+ "POST",
8171
+ `/api/node-dev/${encodeURIComponent(this.repository)}/${method}${branch}`,
8172
+ request
8173
+ );
8174
+ }
8175
+ changeset(o) {
8176
+ const { idempotencyKey, ...rest } = o;
8177
+ return { ...rest, idempotency_key: idempotencyKey };
8178
+ }
8179
+ stat(target, o = {}) {
8180
+ return this.call("stat", { ...o, target });
8181
+ }
8182
+ read(target, o = {}) {
8183
+ return this.call("read", { ...o, target });
8184
+ }
8185
+ list(target, o = {}) {
8186
+ return this.call("list", { ...o, target });
8187
+ }
8188
+ diff(target, from, to, o = {}) {
8189
+ return this.call("diff", { ...o, target, from, to });
8190
+ }
8191
+ /** Changes inside the roots after `since` (a cursor from a previous call). */
8192
+ watch(since, o = {}) {
8193
+ return this.call("watch", { ...o, since });
8194
+ }
8195
+ dryRun(o) {
8196
+ return this.call("dry_run", this.changeset(o));
8197
+ }
8198
+ propose(o) {
8199
+ return this.call(
8200
+ "propose",
8201
+ this.changeset(o)
8202
+ );
8203
+ }
8204
+ getChangeset(changesetId, o = {}) {
8205
+ return this.call("get_changeset", { ...o, changeset_id: changesetId });
8206
+ }
8207
+ listChangesets(o = {}) {
8208
+ return this.call("list_changesets", { ...o });
8209
+ }
8210
+ /** Commit; pass the reviewed digest to bind the commit to that approval. */
8211
+ commit(changesetId, expectedDigest, o = {}) {
8212
+ return this.call("commit", { ...o, changeset_id: changesetId, expected_digest: expectedDigest });
8213
+ }
8214
+ discard(changesetId, o = {}) {
8215
+ return this.call("discard", { ...o, changeset_id: changesetId });
8216
+ }
8217
+ /** Propose and commit in one call. */
8218
+ apply(o) {
8219
+ return this.call("apply", this.changeset(o));
8220
+ }
8221
+ forkBranch(name, o = {}) {
8222
+ return this.call("fork_branch", { ...o, name });
8223
+ }
8224
+ diffBranch(base = "main", o = {}) {
8225
+ return this.call("diff_branch", { ...o, base });
8226
+ }
8227
+ mergeBranch(target, o = {}) {
8228
+ const { dryRun, ...rest } = o;
8229
+ return this.call("merge_branch", { ...rest, target, dry_run: dryRun });
8230
+ }
8231
+ discardBranch(o = {}) {
8232
+ return this.call("discard_branch", { ...o });
8233
+ }
8234
+ };
8235
+
6260
8236
  // src/http-client.ts
6261
8237
  var RaisinHttpClient = class extends import_events3.EventEmitter {
6262
8238
  constructor(baseUrl, options = {}) {
6263
8239
  super();
8240
+ /** Monotonic JSON-RPC id counter for MCP requests. */
8241
+ this._mcpRequestId = 0;
6264
8242
  this._currentUser = null;
6265
8243
  this._uploadManager = null;
6266
8244
  this.baseUrl = baseUrl.replace(/\/$/, "");
@@ -6288,6 +8266,7 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6288
8266
  async authenticate(credentials, repository) {
6289
8267
  if (isJwtCredentials(credentials)) {
6290
8268
  this.authManager.storage.setAccessToken(credentials.token);
8269
+ this.authManager.adoptToken(credentials.token);
6291
8270
  if (repository) {
6292
8271
  try {
6293
8272
  const userInfo = await this.fetchCurrentUser(repository);
@@ -6306,9 +8285,13 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6306
8285
  body: payload,
6307
8286
  skipAuth: true
6308
8287
  });
6309
- this.authManager.setTokens(response.data);
8288
+ if (response.data.token) {
8289
+ this.authManager.storage.setAccessToken(response.data.token);
8290
+ } else {
8291
+ this.authManager.setTokens(response.data);
8292
+ }
6310
8293
  this._currentUser = {
6311
- userId: "admin",
8294
+ userId: response.data.user_id ?? "admin",
6312
8295
  anonymous: false
6313
8296
  };
6314
8297
  this.emit("authenticated", response.data);
@@ -6649,6 +8632,66 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6649
8632
  return this.authManager;
6650
8633
  }
6651
8634
  // ============================================================================
8635
+ // Identity Authentication
8636
+ // ============================================================================
8637
+ /**
8638
+ * Identity authentication (magic link, password, providers) for one
8639
+ * repository.
8640
+ *
8641
+ * Identities are per repository — their home nodes and roles live there — so
8642
+ * the repository is required rather than inferred from client state, which
8643
+ * would silently sign somebody into whichever one happened to be current.
8644
+ *
8645
+ * ```ts
8646
+ * const auth = client.auth('studio');
8647
+ * await auth.sendMagicLink('a@example.com', {
8648
+ * redirectUrl: 'https://app.example.com/account/callback',
8649
+ * });
8650
+ * // …on the callback route:
8651
+ * const tokens = readTokensFromFragment(window.location.hash);
8652
+ * if (tokens) client.setIdentityTokens(tokens.accessToken, tokens.refreshToken);
8653
+ * ```
8654
+ */
8655
+ auth(repository) {
8656
+ return new IdentityAuthApi(repository, async (options) => {
8657
+ const response = await this.request({
8658
+ method: options.method,
8659
+ path: options.path,
8660
+ body: options.body,
8661
+ skipAuth: options.skipAuth
8662
+ });
8663
+ return response.data;
8664
+ });
8665
+ }
8666
+ /**
8667
+ * Adopt an identity's tokens for every subsequent call on this client.
8668
+ *
8669
+ * Takes the loose form so it serves both callers: the whole result of a
8670
+ * sign-in, and the pair recovered from a redirect fragment (which carries no
8671
+ * identity object and may carry no refresh token).
8672
+ */
8673
+ setIdentityTokens(accessTokenOrResult, refreshToken) {
8674
+ if (typeof accessTokenOrResult === "string") {
8675
+ this.authManager.storage.setAccessToken(accessTokenOrResult);
8676
+ this.authManager.adoptToken(accessTokenOrResult);
8677
+ if (refreshToken) this.authManager.storage.setRefreshToken(refreshToken);
8678
+ return;
8679
+ }
8680
+ const result = accessTokenOrResult;
8681
+ this.authManager.setTokens({
8682
+ access_token: result.access_token,
8683
+ refresh_token: result.refresh_token,
8684
+ // The server reports an absolute expiry; AuthManager schedules from a
8685
+ // duration. Deriving it here keeps the refresh timer honest instead of
8686
+ // leaving it unset, which reads as "never expires".
8687
+ expires_in: Math.max(0, result.expires_at - Math.floor(Date.now() / 1e3))
8688
+ });
8689
+ }
8690
+ /** Forget the current identity's tokens. */
8691
+ clearIdentityTokens() {
8692
+ this.authManager.clear();
8693
+ }
8694
+ // ============================================================================
6652
8695
  // Upload Methods
6653
8696
  // ============================================================================
6654
8697
  /**
@@ -6850,7 +8893,7 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6850
8893
  "Content-Type": "application/json",
6851
8894
  ...options.headers
6852
8895
  };
6853
- if (!options.skipAuth && this.authManager.isAuthenticated()) {
8896
+ if (!options.skipAuth) {
6854
8897
  const token = this.authManager.getAccessToken();
6855
8898
  if (token) {
6856
8899
  headers["Authorization"] = `Bearer ${token}`;
@@ -6915,6 +8958,33 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6915
8958
  * @param input - Input data passed to the function
6916
8959
  * @returns Execution ID and job ID for tracking
6917
8960
  */
8961
+ /**
8962
+ * Durable agent runs of `repository`: create, read, control, stream, and
8963
+ * drive them directly (see {@link AgentRunsApi}).
8964
+ */
8965
+ agentRuns(repository) {
8966
+ return new AgentRunsApi(repository, {
8967
+ request: async (method, path, body) => (await this.request({ method, path, body })).data,
8968
+ url: (path) => `${this.baseUrl}${path}`,
8969
+ headers: () => {
8970
+ const token = this.authManager.getAccessToken();
8971
+ const headers = {};
8972
+ if (token) headers["Authorization"] = `Bearer ${token}`;
8973
+ return headers;
8974
+ },
8975
+ fetch: this.fetchImpl
8976
+ });
8977
+ }
8978
+ /**
8979
+ * The node-development surface of `repository`: typed reads, atomic
8980
+ * changesets with review and idempotency, branch worktrees (see
8981
+ * {@link NodeDevApi}).
8982
+ */
8983
+ nodeDev(repository) {
8984
+ return new NodeDevApi(repository, {
8985
+ request: async (method, path, body) => (await this.request({ method, path, body })).data
8986
+ });
8987
+ }
6918
8988
  async invokeFunction(repository, functionName, input, options) {
6919
8989
  const requestTimeoutMs = options?.requestTimeoutMs ?? this.options.requestTimeout;
6920
8990
  let waitTimeoutMs = options?.waitTimeoutMs;
@@ -6952,6 +9022,105 @@ var RaisinHttpClient = class extends import_events3.EventEmitter {
6952
9022
  });
6953
9023
  return response.data;
6954
9024
  }
9025
+ /**
9026
+ * Send one JSON-RPC request to an MCP server endpoint and return its
9027
+ * unwrapped `result`.
9028
+ *
9029
+ * Calls `POST /mcp/{repo}/{branch}/{slug}` carrying a single JSON-RPC 2.0
9030
+ * message. MCP errors are returned in the JSON-RPC envelope (HTTP `200`), so
9031
+ * this method inspects the body's `error` member and throws on failure.
9032
+ *
9033
+ * @internal Used by {@link McpClient} via the transport built in `mcp()`.
9034
+ */
9035
+ async mcpRpc(repository, branch, slug, method, params) {
9036
+ const id = ++this._mcpRequestId;
9037
+ const response = await this.request({
9038
+ method: "POST",
9039
+ path: `/mcp/${repository}/${branch}/${slug}`,
9040
+ body: {
9041
+ jsonrpc: "2.0",
9042
+ id,
9043
+ method,
9044
+ ...params !== void 0 ? { params } : {}
9045
+ }
9046
+ });
9047
+ const frame = response.data;
9048
+ if (frame?.error) {
9049
+ const err = new Error(frame.error.message || `MCP error ${frame.error.code}`);
9050
+ err.code = frame.error.code;
9051
+ err.data = frame.error.data;
9052
+ throw err;
9053
+ }
9054
+ return frame?.result;
9055
+ }
9056
+ /**
9057
+ * Open the `subscriptions/listen` SSE stream for an MCP server endpoint.
9058
+ *
9059
+ * Streams JSON-RPC frames (the subscription ack followed by
9060
+ * `notifications/resources/updated` frames) from
9061
+ * `POST /mcp/{repo}/{branch}/{slug}`.
9062
+ *
9063
+ * @internal Used by {@link McpClient} via the transport built in `mcp()`.
9064
+ */
9065
+ mcpSubscribe(repository, branch, slug, method, params) {
9066
+ const headers = {};
9067
+ const token = this.authManager.getAccessToken();
9068
+ if (token) {
9069
+ headers["Authorization"] = `Bearer ${token}`;
9070
+ }
9071
+ const sse = new SSEClient(
9072
+ `${this.baseUrl}/mcp/${repository}/${branch}/${slug}`,
9073
+ {
9074
+ method: "POST",
9075
+ body: {
9076
+ jsonrpc: "2.0",
9077
+ id: ++this._mcpRequestId,
9078
+ method,
9079
+ params
9080
+ },
9081
+ headers,
9082
+ fetch: this.fetchImpl,
9083
+ reconnect: { enabled: false }
9084
+ }
9085
+ );
9086
+ return {
9087
+ close: () => sse.close(),
9088
+ async *[Symbol.asyncIterator]() {
9089
+ for await (const event of sse) {
9090
+ yield event.data;
9091
+ }
9092
+ }
9093
+ };
9094
+ }
9095
+ /**
9096
+ * Get an {@link McpClient} for a content-declared MCP server.
9097
+ *
9098
+ * @param slug - The `raisin:McpServer` slug (resolved in the `mcp` workspace).
9099
+ * @param branch - Branch to address; defaults to the client's current branch.
9100
+ */
9101
+ mcp(slug, branch) {
9102
+ const repository = this._context.repository;
9103
+ if (!repository) {
9104
+ throw new Error("mcp() requires a repository; call database(name) first");
9105
+ }
9106
+ const effectiveBranch = branch ?? this.getBranch();
9107
+ const transport = {
9108
+ rpc: (m, p) => this.mcpRpc(repository, effectiveBranch, slug, m, p),
9109
+ subscribe: (m, p) => this.mcpSubscribe(repository, effectiveBranch, slug, m, p)
9110
+ };
9111
+ return new McpClient(transport);
9112
+ }
9113
+ /**
9114
+ * Make a management REST request and return the parsed body.
9115
+ *
9116
+ * @internal Used by the HTTP schema-management helpers
9117
+ * ({@link HttpNodeTypes}/{@link HttpArchetypes}/{@link HttpElementTypes}) to
9118
+ * reuse the client's centralized auth, timeout, and base-URL handling.
9119
+ */
9120
+ async managementRequest(method, path, body) {
9121
+ const response = await this.request({ method, path, body });
9122
+ return response.data;
9123
+ }
6955
9124
  /**
6956
9125
  * Get a simplified database interface for SSR
6957
9126
  * This returns an HTTP-based database client
@@ -6979,19 +9148,6 @@ var HttpDatabase = class _HttpDatabase {
6979
9148
  getBranch() {
6980
9149
  return this.branch;
6981
9150
  }
6982
- /**
6983
- * Create a new HttpDatabase instance scoped to a specific branch.
6984
- *
6985
- * @param branch - Branch name
6986
- * @returns New HttpDatabase instance with branch context
6987
- *
6988
- * @example
6989
- * ```typescript
6990
- * const db = client.database('my-repo');
6991
- * const staging = db.onBranch('staging');
6992
- * const result = await staging.executeSql('SELECT * FROM content');
6993
- * ```
6994
- */
6995
9151
  onBranch(branch) {
6996
9152
  return new _HttpDatabase(this.repository, this.client, branch);
6997
9153
  }
@@ -7032,12 +9188,107 @@ var HttpDatabase = class _HttpDatabase {
7032
9188
  /**
7033
9189
  * Get FunctionsApi for invoking server-side functions via HTTP
7034
9190
  */
7035
- functions() {
7036
- return new HttpFunctionsApi(
7037
- this.repository,
7038
- (repo, name, input, options) => this.client.invokeFunction(repo, name, input, options),
7039
- (repo, name, input) => this.client.invokeFunctionSync(repo, name, input)
7040
- );
9191
+ /** Durable agent runs of this repository. */
9192
+ agentRuns() {
9193
+ return this.client.agentRuns(this.repository);
9194
+ }
9195
+ /** The node-development surface of this repository. */
9196
+ nodeDev() {
9197
+ return this.client.nodeDev(this.repository);
9198
+ }
9199
+ functions() {
9200
+ return new HttpFunctionsApi(
9201
+ this.repository,
9202
+ (repo, name, input, options) => this.client.invokeFunction(repo, name, input, options),
9203
+ (repo, name, input) => this.client.invokeFunctionSync(repo, name, input)
9204
+ );
9205
+ }
9206
+ /**
9207
+ * The retrieval implementation behind {@link search} and {@link ask}.
9208
+ *
9209
+ * The SAME `SearchApi` the WebSocket client uses, handed this transport's SQL
9210
+ * and function runners. Retrieval is one behaviour — the query shape, the
9211
+ * limit clamp, the required scope, the `grounded` fail-closed mapping — and a
9212
+ * second copy written against HTTP would drift from the first the moment
9213
+ * either is touched. Only the two runners differ.
9214
+ */
9215
+ searchApi() {
9216
+ if (!this._searchApi) {
9217
+ this._searchApi = new SearchApi(
9218
+ (sql, params) => this.executeSql(sql, params),
9219
+ async (name, input) => {
9220
+ const run = await this.functions().invoke(name, input, {
9221
+ waitForResult: true
9222
+ });
9223
+ return run?.result ?? run;
9224
+ }
9225
+ );
9226
+ }
9227
+ return this._searchApi;
9228
+ }
9229
+ /**
9230
+ * Find the passages matching a query, by meaning and by keyword at once.
9231
+ *
9232
+ * Available on this HTTP client as well as the WebSocket one, because this is
9233
+ * the client a server-side route handler reaches for — and a route handler is
9234
+ * exactly where retrieval belongs when the page is public: the browser must
9235
+ * not hold a credential that can read the repository.
9236
+ *
9237
+ * @example
9238
+ * ```typescript
9239
+ * const passages = await db.search('how much notice to terminate', {
9240
+ * workspaces: 'stories',
9241
+ * });
9242
+ * ```
9243
+ */
9244
+ async search(query, options) {
9245
+ return this.searchApi().search(query, options);
9246
+ }
9247
+ /**
9248
+ * Answer a question from the stored content, with citations.
9249
+ *
9250
+ * Check `grounded` before showing the answer: when it is false nothing
9251
+ * relevant was retrieved and the model was never asked.
9252
+ *
9253
+ * @example
9254
+ * ```typescript
9255
+ * const { answer, citations, grounded } = await db.ask(question, {
9256
+ * workspaces: 'stories, handbook',
9257
+ * });
9258
+ * ```
9259
+ */
9260
+ async ask(question, options) {
9261
+ return this.searchApi().ask(question, options);
9262
+ }
9263
+ /**
9264
+ * Get an {@link McpClient} for a content-declared MCP server on this database.
9265
+ *
9266
+ * @param slug - The `raisin:McpServer` slug (resolved in the `mcp` workspace).
9267
+ * @returns A client scoped to `{repo}/{branch}/{slug}`, using this database's
9268
+ * branch (falling back to the HTTP client's current branch).
9269
+ */
9270
+ mcp(slug) {
9271
+ const transport = {
9272
+ rpc: (m, p) => this.client.mcpRpc(this.repository, this.schemaBranch(), slug, m, p),
9273
+ subscribe: (m, p) => this.client.mcpSubscribe(this.repository, this.schemaBranch(), slug, m, p)
9274
+ };
9275
+ return new McpClient(transport);
9276
+ }
9277
+ /** Effective branch for management calls. */
9278
+ schemaBranch() {
9279
+ return this.branch ?? this.client.getBranch();
9280
+ }
9281
+ /** NodeType management (get/list/getResolved/create/update/delete/publish/unpublish). */
9282
+ nodeTypes() {
9283
+ return new HttpNodeTypes(this.repository, this.schemaBranch(), this.client);
9284
+ }
9285
+ /** Archetype management (get/list/getResolved/create/update/delete/publish/unpublish). */
9286
+ archetypes() {
9287
+ return new HttpArchetypes(this.repository, this.schemaBranch(), this.client);
9288
+ }
9289
+ /** ElementType management (get/list/getResolved/create/update/delete/publish/unpublish). */
9290
+ elementTypes() {
9291
+ return new HttpElementTypes(this.repository, this.schemaBranch(), this.client);
7041
9292
  }
7042
9293
  };
7043
9294
  var HttpWorkspaceClient = class {
@@ -7196,10 +9447,13 @@ var HttpWorkspaceClient = class {
7196
9447
  };
7197
9448
 
7198
9449
  // src/client.ts
7199
- var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
9450
+ var _refreshingUser;
9451
+ var _RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7200
9452
  constructor(url, options = {}) {
7201
9453
  super();
7202
9454
  this._currentUser = null;
9455
+ /** In-flight role re-resolve, used to coalesce concurrent refreshCurrentUser() calls. */
9456
+ __privateAdd(this, _refreshingUser, null);
7203
9457
  // Auth state change listeners (Firebase/Supabase pattern)
7204
9458
  this._authListeners = /* @__PURE__ */ new Set();
7205
9459
  this._userChangeListeners = /* @__PURE__ */ new Set();
@@ -7210,23 +9464,44 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7210
9464
  // Reconnection tracking
7211
9465
  this._previousConnectionState = "disconnected" /* Disconnected */;
7212
9466
  this._reconnectedListeners = /* @__PURE__ */ new Set();
9467
+ this._everConnected = false;
9468
+ // Requests queued while the connection is re-establishing.
9469
+ // Flushed after reconnect + re-auth; bounded by MAX_QUEUED_REQUESTS.
9470
+ this._queuedRequests = [];
9471
+ /**
9472
+ * Backoff delays (ms) between subscription-restore retries after reconnect.
9473
+ * @internal - overridable in tests
9474
+ */
9475
+ this._restoreRetryDelaysMs = [1e3, 3e3, 9e3];
7213
9476
  // Upload manager (uses HTTP under the hood)
7214
9477
  this._uploadManager = null;
9478
+ /** In-flight refresh shared by concurrent refreshToken() callers. */
9479
+ this._refreshInFlight = null;
7215
9480
  if (options.logLevel !== void 0) {
7216
9481
  setLogLevel(options.logLevel);
7217
9482
  }
9483
+ const connectionUrl = this.buildConnectionUrl(url, options.repository);
7218
9484
  this.options = {
7219
9485
  requestTimeout: options.requestTimeout ?? 3e4,
7220
- tenantId: options.tenantId ?? this.extractTenantFromUrl(url),
9486
+ tenantId: options.tenantId ?? this.extractTenantFromUrl(connectionUrl),
7221
9487
  defaultBranch: options.defaultBranch ?? "main"
7222
9488
  };
7223
- this._httpBaseUrl = options.httpBaseUrl ?? this.deriveHttpUrl(url);
7224
- this._repository = this.extractRepositoryFromUrl(url);
9489
+ this._tokenRefresher = options.tokenRefresher;
9490
+ this._httpBaseUrl = options.httpBaseUrl ?? this.deriveHttpUrl(connectionUrl);
9491
+ this._repository = options.repository ?? this.extractRepositoryFromUrl(connectionUrl);
7225
9492
  this._context = {
7226
9493
  tenant_id: this.options.tenantId,
7227
9494
  branch: this.options.defaultBranch
7228
9495
  };
7229
- this.connection = new Connection(url, options.connection);
9496
+ const explicitTenant = options.tenantId ?? (this.extractTenantFromUrl(url) !== "default" ? this.options.tenantId : void 0);
9497
+ const connectionOptions = {
9498
+ ...options.connection,
9499
+ headers: {
9500
+ ...explicitTenant ? { "x-tenant-id": explicitTenant } : {},
9501
+ ...options.connection?.headers
9502
+ }
9503
+ };
9504
+ this.connection = this.createConnection(connectionUrl, connectionOptions);
7230
9505
  this.requestTracker = new RequestTracker({
7231
9506
  defaultTimeout: this.options.requestTimeout
7232
9507
  });
@@ -7236,6 +9511,14 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7236
9511
  );
7237
9512
  this.setupConnectionHandlers();
7238
9513
  }
9514
+ /**
9515
+ * Create the underlying WebSocket connection.
9516
+ * Seam for tests to inject a fake connection.
9517
+ * @internal
9518
+ */
9519
+ createConnection(url, options) {
9520
+ return new Connection(url, options);
9521
+ }
7239
9522
  /**
7240
9523
  * Derive HTTP URL from WebSocket URL
7241
9524
  * ws://host:port/path -> http://host:port
@@ -7251,15 +9534,45 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7251
9534
  return "http://localhost:8081";
7252
9535
  }
7253
9536
  }
9537
+ /**
9538
+ * Build the WebSocket connection URL.
9539
+ *
9540
+ * When a repository option is provided and the URL carries no path
9541
+ * (e.g. `ws://host:8081`), build the tenant-less `/ws/{repository}`
9542
+ * route internally. URLs that already carry a path are used as-is.
9543
+ */
9544
+ buildConnectionUrl(url, repository) {
9545
+ if (!repository) {
9546
+ return url;
9547
+ }
9548
+ try {
9549
+ const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
9550
+ const parsed = new URL(wsUrl);
9551
+ const hasPath = parsed.pathname.split("/").some((p) => p.length > 0);
9552
+ if (hasPath) {
9553
+ return url;
9554
+ }
9555
+ return `${url.replace(/\/+$/, "")}/ws/${repository}`;
9556
+ } catch {
9557
+ return url;
9558
+ }
9559
+ }
7254
9560
  /**
7255
9561
  * Extract repository from URL
7256
- * URL format: raisin://host:port/tenant/repository or ws://host:port/tenant/repository
9562
+ * URL forms: /ws/{repo} (tenant-less, preferred), /sys/{tenant}/{repo}
9563
+ * (operator/multi-tenant), /{tenant}/{repo} (legacy).
7257
9564
  */
7258
9565
  extractRepositoryFromUrl(url) {
7259
9566
  try {
7260
9567
  const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
7261
9568
  const parsed = new URL(wsUrl);
7262
9569
  const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
9570
+ if (parts[0] === "ws") {
9571
+ return parts.length > 1 ? parts[1] : "";
9572
+ }
9573
+ if (parts[0] === "sys") {
9574
+ return parts.length > 2 ? parts[2] : "";
9575
+ }
7263
9576
  return parts.length > 1 ? parts[1] : "";
7264
9577
  } catch {
7265
9578
  return "";
@@ -7267,14 +9580,24 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7267
9580
  }
7268
9581
  /**
7269
9582
  * Extract tenant ID from URL
7270
- * URL format: raisin://host:port/tenant/repository or ws://host:port/tenant/repository
9583
+ * URL forms: /ws/{repo} (tenant-less — no tenant in URL),
9584
+ * /sys/{tenant}/{repo} (canonical), /{tenant}/{repo} (legacy).
9585
+ * When the URL doesn't carry a tenant, fall back to 'default' — the
9586
+ * server's tenant middleware resolves it anyway (production derives the
9587
+ * tenant from the request itself), so an explicit tenantId is optional.
7271
9588
  */
7272
9589
  extractTenantFromUrl(url) {
7273
9590
  try {
7274
9591
  const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
7275
9592
  const parsed = new URL(wsUrl);
7276
9593
  const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
7277
- return parts.length > 0 ? parts[0] : "default";
9594
+ if (parts[0] === "ws") {
9595
+ return "default";
9596
+ }
9597
+ if (parts[0] === "sys") {
9598
+ return parts.length > 1 ? parts[1] : "default";
9599
+ }
9600
+ return parts.length >= 2 ? parts[0] : "default";
7278
9601
  } catch (error) {
7279
9602
  return "default";
7280
9603
  }
@@ -7288,7 +9611,11 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7288
9611
  });
7289
9612
  this.connection.on("stateChange", (state) => {
7290
9613
  const wasDisconnected = this._previousConnectionState === "disconnected" /* Disconnected */ || this._previousConnectionState === "reconnecting" /* Reconnecting */;
9614
+ if (state === "connected" /* Connected */) {
9615
+ this._everConnected = true;
9616
+ }
7291
9617
  if (state === "disconnected" /* Disconnected */ || state === "closed" /* Closed */) {
9618
+ this._queuedRequests = [];
7292
9619
  this.requestTracker.cancelAll();
7293
9620
  }
7294
9621
  if (state === "connected" /* Connected */ && wasDisconnected && this._previousConnectionState !== "disconnected" /* Disconnected */) {
@@ -7319,8 +9646,13 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7319
9646
  if (isConnectedMessage(message)) {
7320
9647
  this.handleConnectedMessage(message);
7321
9648
  } else if (isEventMessage(message)) {
7322
- logger.debug(`Routing to event handler`);
7323
- this.eventHandler.handleEvent(message);
9649
+ if (message.event_type === "permissions:changed") {
9650
+ logger.info("[permissions:changed] server signaled a role/permission change; re-resolving");
9651
+ void this.refreshCurrentUser();
9652
+ } else {
9653
+ logger.debug(`Routing to event handler`);
9654
+ this.eventHandler.handleEvent(message);
9655
+ }
7324
9656
  } else if (isResponseEnvelope(message)) {
7325
9657
  logger.debug(`Routing to response handler - request_id: ${message.request_id}, status: ${message.status}`);
7326
9658
  this.handleResponse(message);
@@ -7353,19 +9685,36 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7353
9685
  };
7354
9686
  logger.debug(`Anonymous user info stored: ${message.user_id}`);
7355
9687
  this._updateReadyState();
9688
+ this._flushQueuedRequests();
7356
9689
  if (hasActiveSubscriptions) {
7357
9690
  this._restoreSubscriptionsAndNotify();
7358
9691
  }
7359
9692
  } else {
7360
9693
  logger.info(`[handleConnectedMessage] Stored token found, auto-authenticating...`);
7361
- this.autoReauthenticate().then(async () => {
9694
+ this.autoReauthenticate().then(async (authenticated) => {
9695
+ if (authenticated) {
9696
+ this._flushQueuedRequests();
9697
+ } else {
9698
+ this._rejectQueuedRequests(
9699
+ new RaisinAuthError(
9700
+ "Re-authentication failed after reconnect",
9701
+ "AUTH_TOKEN_EXPIRED",
9702
+ 401
9703
+ )
9704
+ );
9705
+ }
7362
9706
  if (hasActiveSubscriptions) {
7363
9707
  await this._restoreSubscriptionsAndNotify();
7364
9708
  }
7365
9709
  }).catch((err) => {
7366
9710
  logger.error(`[handleConnectedMessage] Auto-authenticate failed:`, err);
9711
+ this._rejectQueuedRequests(
9712
+ err instanceof Error ? err : new Error("Re-authentication failed after reconnect")
9713
+ );
7367
9714
  });
7368
9715
  }
9716
+ } else {
9717
+ this._flushQueuedRequests();
7369
9718
  }
7370
9719
  this.emit("connected", {
7371
9720
  connectionId: message.connection_id,
@@ -7374,18 +9723,41 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7374
9723
  });
7375
9724
  }
7376
9725
  /**
7377
- * Restore subscriptions after reconnection and notify listeners
9726
+ * Restore subscriptions after reconnection and notify listeners.
9727
+ *
9728
+ * Retries with backoff (see `_restoreRetryDelaysMs`); on final failure a
9729
+ * `subscription_restore_failed` event is emitted so applications can
9730
+ * resync their state (e.g. reload lists and re-subscribe manually).
7378
9731
  * @internal
7379
9732
  */
7380
9733
  async _restoreSubscriptionsAndNotify() {
7381
- try {
7382
- logger.info("[_restoreSubscriptionsAndNotify] Restoring subscriptions...");
7383
- await this.eventHandler.restoreSubscriptions();
7384
- logger.info("[_restoreSubscriptionsAndNotify] Subscriptions restored successfully");
7385
- this._emitReconnected();
7386
- } catch (error) {
7387
- logger.error("[_restoreSubscriptionsAndNotify] Failed to restore subscriptions:", error);
9734
+ const delays = this._restoreRetryDelaysMs;
9735
+ let lastError;
9736
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
9737
+ try {
9738
+ logger.info(
9739
+ `[_restoreSubscriptionsAndNotify] Restoring subscriptions (attempt ${attempt + 1}/${delays.length + 1})...`
9740
+ );
9741
+ await this.eventHandler.restoreSubscriptions({ retry: attempt > 0 });
9742
+ logger.info("[_restoreSubscriptionsAndNotify] Subscriptions restored successfully");
9743
+ this._emitReconnected();
9744
+ return;
9745
+ } catch (error) {
9746
+ lastError = error;
9747
+ logger.error(
9748
+ `[_restoreSubscriptionsAndNotify] Restore attempt ${attempt + 1} failed:`,
9749
+ error
9750
+ );
9751
+ if (attempt < delays.length) {
9752
+ await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
9753
+ }
9754
+ }
7388
9755
  }
9756
+ logger.error("[_restoreSubscriptionsAndNotify] Giving up after retries; events may be stale");
9757
+ this.emit(
9758
+ "subscription_restore_failed",
9759
+ lastError instanceof Error ? lastError : new Error(String(lastError))
9760
+ );
7389
9761
  }
7390
9762
  /**
7391
9763
  * Emit reconnected event to all listeners
@@ -7404,13 +9776,15 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7404
9776
  }
7405
9777
  /**
7406
9778
  * Auto-reauthenticate with stored JWT on reconnection
9779
+ *
9780
+ * @returns true if the session was re-authenticated, false otherwise
7407
9781
  * @internal
7408
9782
  */
7409
9783
  async autoReauthenticate() {
7410
9784
  const token = this.authManager.storage.getAccessToken();
7411
9785
  if (!token) {
7412
9786
  logger.warn("[autoReauthenticate] No stored token");
7413
- return;
9787
+ return false;
7414
9788
  }
7415
9789
  const payload = AuthManager.parseToken(token);
7416
9790
  if (payload?.exp && typeof payload.exp === "number") {
@@ -7422,18 +9796,20 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7422
9796
  this.authManager.clear();
7423
9797
  this._currentUser = null;
7424
9798
  this._emitAuthEvent("SESSION_EXPIRED");
7425
- return;
9799
+ return false;
7426
9800
  }
7427
9801
  const newToken = this.authManager.storage.getAccessToken();
7428
9802
  if (newToken) {
7429
9803
  await this.authenticate({ type: "jwt", token: newToken });
9804
+ return true;
7430
9805
  }
7431
- return;
9806
+ return false;
7432
9807
  }
7433
9808
  }
7434
9809
  try {
7435
9810
  await this.authenticate({ type: "jwt", token });
7436
9811
  logger.info("[autoReauthenticate] Successfully re-authenticated");
9812
+ return true;
7437
9813
  } catch (err) {
7438
9814
  logger.error("[autoReauthenticate] Failed to authenticate:", err);
7439
9815
  const refreshed = await this.refreshToken();
@@ -7441,11 +9817,14 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7441
9817
  const newToken = this.authManager.storage.getAccessToken();
7442
9818
  if (newToken) {
7443
9819
  await this.authenticate({ type: "jwt", token: newToken });
9820
+ return true;
7444
9821
  }
9822
+ return false;
7445
9823
  } else {
7446
9824
  this.authManager.clear();
7447
9825
  this._currentUser = null;
7448
9826
  this._emitAuthEvent("SESSION_EXPIRED");
9827
+ return false;
7449
9828
  }
7450
9829
  }
7451
9830
  }
@@ -7495,6 +9874,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7495
9874
  async authenticate(credentials) {
7496
9875
  if (isJwtCredentials(credentials)) {
7497
9876
  logger.info("[authenticate] Starting JWT authentication...");
9877
+ this.authManager.adoptToken(credentials.token);
7498
9878
  const payload = { token: credentials.token };
7499
9879
  const response = await this.sendRequestInternal(
7500
9880
  payload,
@@ -7561,18 +9941,18 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7561
9941
  }
7562
9942
  const url = `${this._httpBaseUrl}/auth/${repository}/login`;
7563
9943
  logger.debug("[loginWithEmail] POST", url);
7564
- const response = await fetch(url, {
9944
+ const response = await this._fetchWithTimeout(url, {
7565
9945
  method: "POST",
7566
9946
  headers: { "Content-Type": "application/json" },
7567
9947
  body: JSON.stringify({ email, password })
7568
9948
  });
7569
9949
  if (!response.ok) {
7570
9950
  const error = await response.json().catch(() => ({ message: response.statusText }));
7571
- const authError = {
7572
- code: error.code || "LOGIN_FAILED",
7573
- message: error.message || "Login failed"
7574
- };
7575
- throw authError;
9951
+ throw new RaisinAuthError(
9952
+ error.message || "Login failed",
9953
+ error.code || "LOGIN_FAILED",
9954
+ response.status
9955
+ );
7576
9956
  }
7577
9957
  const tokens = await response.json();
7578
9958
  logger.info("[loginWithEmail] HTTP login successful, user_id:", tokens.identity.id);
@@ -7601,11 +9981,11 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7601
9981
  }
7602
9982
  this._currentUser = {
7603
9983
  userId: user.id,
7604
- roles: [],
9984
+ roles: this._currentUser?.roles ?? [],
7605
9985
  anonymous: false,
7606
9986
  node: userNode ?? void 0
7607
9987
  };
7608
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
9988
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7609
9989
  if (expiresInMs > 0) {
7610
9990
  this.scheduleAutoRefresh(expiresInMs);
7611
9991
  }
@@ -7629,18 +10009,18 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7629
10009
  async registerWithEmail(email, password, repository, displayName) {
7630
10010
  logger.info("[registerWithEmail] Starting registration...");
7631
10011
  const url = `${this._httpBaseUrl}/auth/${repository}/register`;
7632
- const response = await fetch(url, {
10012
+ const response = await this._fetchWithTimeout(url, {
7633
10013
  method: "POST",
7634
10014
  headers: { "Content-Type": "application/json" },
7635
10015
  body: JSON.stringify({ email, password, display_name: displayName })
7636
10016
  });
7637
10017
  if (!response.ok) {
7638
10018
  const error = await response.json().catch(() => ({ message: response.statusText }));
7639
- const authError = {
7640
- code: error.code || "REGISTRATION_FAILED",
7641
- message: error.message || "Registration failed"
7642
- };
7643
- throw authError;
10019
+ throw new RaisinAuthError(
10020
+ error.message || "Registration failed",
10021
+ error.code || "REGISTRATION_FAILED",
10022
+ response.status
10023
+ );
7644
10024
  }
7645
10025
  const tokens = await response.json();
7646
10026
  logger.info("[registerWithEmail] Registration successful, user_id:", tokens.identity.id);
@@ -7667,11 +10047,11 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7667
10047
  }
7668
10048
  this._currentUser = {
7669
10049
  userId: user.id,
7670
- roles: [],
10050
+ roles: this._currentUser?.roles ?? [],
7671
10051
  anonymous: false,
7672
10052
  node: userNode ?? void 0
7673
10053
  };
7674
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
10054
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7675
10055
  if (expiresInMs > 0) {
7676
10056
  this.scheduleAutoRefresh(expiresInMs);
7677
10057
  }
@@ -7763,39 +10143,18 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7763
10143
  if (payload && payload.exp && typeof payload.exp === "number") {
7764
10144
  if (payload.exp * 1e3 < Date.now()) {
7765
10145
  logger.info("[initSession] Token expired, attempting refresh...");
7766
- const refreshToken = this.authManager.storage.getRefreshToken();
7767
- if (refreshToken) {
7768
- try {
7769
- const url = this._repository ? `${this._httpBaseUrl}/auth/${this._repository}/refresh` : `${this._httpBaseUrl}/auth/refresh`;
7770
- const response = await fetch(url, {
7771
- method: "POST",
7772
- headers: { "Content-Type": "application/json" },
7773
- body: JSON.stringify({ refresh_token: refreshToken })
7774
- });
7775
- if (response.ok) {
7776
- const tokens = await response.json();
7777
- this.authManager.storage.setAccessToken(tokens.access_token);
7778
- this.authManager.storage.setRefreshToken(tokens.refresh_token);
7779
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
7780
- if (expiresInMs > 0) {
7781
- this.scheduleAutoRefresh(expiresInMs);
7782
- }
7783
- logger.info("[initSession] Token refreshed successfully");
7784
- } else {
7785
- logger.warn("[initSession] Token refresh failed, clearing session");
7786
- this.authManager.clear();
7787
- return null;
7788
- }
7789
- } catch (err) {
7790
- logger.warn("[initSession] Token refresh error:", err);
7791
- this.authManager.clear();
7792
- return null;
7793
- }
7794
- } else {
10146
+ if (!this.authManager.storage.getRefreshToken()) {
7795
10147
  logger.warn("[initSession] No refresh token, clearing session");
7796
10148
  this.authManager.clear();
7797
10149
  return null;
7798
10150
  }
10151
+ const refreshed = await this.refreshToken();
10152
+ if (!refreshed) {
10153
+ logger.warn("[initSession] Token refresh failed, clearing session");
10154
+ this.authManager.clear();
10155
+ return null;
10156
+ }
10157
+ logger.info("[initSession] Token refreshed successfully");
7799
10158
  }
7800
10159
  }
7801
10160
  try {
@@ -7829,7 +10188,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7829
10188
  }
7830
10189
  this._currentUser = {
7831
10190
  userId: user.id,
7832
- roles: [],
10191
+ roles: this._currentUser?.roles ?? [],
7833
10192
  anonymous: false,
7834
10193
  node: userNode ?? void 0
7835
10194
  };
@@ -7885,6 +10244,17 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7885
10244
  * @throws Error if no refresh token is stored or refresh fails
7886
10245
  */
7887
10246
  async refreshToken() {
10247
+ if (this._refreshInFlight) {
10248
+ logger.debug("[refreshToken] Refresh already in flight, awaiting it");
10249
+ return this._refreshInFlight;
10250
+ }
10251
+ this._refreshInFlight = this._refreshToken().finally(() => {
10252
+ this._refreshInFlight = null;
10253
+ });
10254
+ return this._refreshInFlight;
10255
+ }
10256
+ async _refreshToken() {
10257
+ if (this._tokenRefresher) return this._refreshViaHook(this._tokenRefresher);
7888
10258
  const refreshToken = this.authManager.storage.getRefreshToken();
7889
10259
  if (!refreshToken) {
7890
10260
  logger.warn("[refreshToken] No refresh token available");
@@ -7893,7 +10263,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7893
10263
  try {
7894
10264
  logger.info("[refreshToken] Refreshing access token...");
7895
10265
  const url = this._repository ? `${this._httpBaseUrl}/auth/${this._repository}/refresh` : `${this._httpBaseUrl}/auth/refresh`;
7896
- const response = await fetch(url, {
10266
+ const response = await this._fetchWithTimeout(url, {
7897
10267
  method: "POST",
7898
10268
  headers: { "Content-Type": "application/json" },
7899
10269
  body: JSON.stringify({ refresh_token: refreshToken })
@@ -7901,6 +10271,11 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7901
10271
  if (!response.ok) {
7902
10272
  const errorText = await response.text();
7903
10273
  logger.error("[refreshToken] Refresh failed:", response.status, errorText);
10274
+ if (/TOKEN_REUSE_DETECTED|TOKEN_FAMILY_MISMATCH/.test(errorText)) {
10275
+ logger.error(
10276
+ "[refreshToken] Session revoked by the server as a suspected token replay. If this happened on a normal page load, two refreshes raced on the same token."
10277
+ );
10278
+ }
7904
10279
  if (response.status === 401 || response.status === 403) {
7905
10280
  this.authManager.clear();
7906
10281
  this._currentUser = null;
@@ -7923,7 +10298,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7923
10298
  if (this.authManager.storage instanceof LocalStorageTokenStorage) {
7924
10299
  this.authManager.storage.setUser(user);
7925
10300
  }
7926
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
10301
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7927
10302
  if (expiresInMs > 0) {
7928
10303
  this.scheduleAutoRefresh(expiresInMs);
7929
10304
  }
@@ -7936,6 +10311,78 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7936
10311
  return null;
7937
10312
  }
7938
10313
  }
10314
+ /** `refreshToken()` through `ClientOptions.tokenRefresher` (the session is not the page's to renew). */
10315
+ async _refreshViaHook(hook) {
10316
+ try {
10317
+ const res = await hook();
10318
+ const accessToken = typeof res === "string" ? res : res?.accessToken;
10319
+ if (!accessToken) {
10320
+ this.authManager.clear();
10321
+ this._currentUser = null;
10322
+ this.emit("auth_error", new Error("Token refresh failed - session expired"));
10323
+ return null;
10324
+ }
10325
+ this.authManager.storage.setAccessToken(accessToken);
10326
+ this.authManager.adoptToken(accessToken);
10327
+ const exp = typeof res === "object" && res?.expiresAt ? _RaisinClient.expiresAtToMs(res.expiresAt) : (AuthManager.parseToken(accessToken)?.exp ?? 0) * 1e3;
10328
+ const inMs = exp - Date.now() - 6e4;
10329
+ if (exp && inMs > 0) this.scheduleAutoRefresh(inMs);
10330
+ const user = this._currentUser ? { id: this._currentUser.userId, email: "", displayName: null, avatarUrl: null, emailVerified: false, home: null } : { id: "", email: "", displayName: null, avatarUrl: null, emailVerified: false, home: null };
10331
+ this._emitAuthEvent("TOKEN_REFRESHED");
10332
+ this.emit("token_refreshed", user);
10333
+ return user;
10334
+ } catch (err) {
10335
+ logger.error("[refreshToken] tokenRefresher failed:", err);
10336
+ return null;
10337
+ }
10338
+ }
10339
+ /**
10340
+ * Fetch with a timeout derived from `requestTimeout`.
10341
+ *
10342
+ * Aborts the request after `requestTimeout` ms and rejects with a
10343
+ * `RaisinTimeoutError`. If the caller provides its own AbortSignal in
10344
+ * `init.signal`, it is respected: an external abort rejects with the
10345
+ * original AbortError instead of a timeout error.
10346
+ * @internal
10347
+ */
10348
+ async _fetchWithTimeout(url, init = {}) {
10349
+ const timeoutMs = this.options.requestTimeout;
10350
+ const controller = new AbortController();
10351
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10352
+ const externalSignal = init.signal ?? void 0;
10353
+ if (externalSignal) {
10354
+ if (externalSignal.aborted) {
10355
+ controller.abort();
10356
+ } else {
10357
+ externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
10358
+ }
10359
+ }
10360
+ try {
10361
+ return await fetch(url, { ...init, signal: controller.signal });
10362
+ } catch (error) {
10363
+ if (error instanceof Error && error.name === "AbortError" && !externalSignal?.aborted) {
10364
+ throw new RaisinTimeoutError(
10365
+ `Request to ${url} timed out after ${timeoutMs}ms`,
10366
+ "REQUEST_TIMEOUT",
10367
+ timeoutMs
10368
+ );
10369
+ }
10370
+ throw error;
10371
+ } finally {
10372
+ clearTimeout(timer);
10373
+ }
10374
+ }
10375
+ /**
10376
+ * Normalize a token `expires_at` value to epoch milliseconds.
10377
+ *
10378
+ * Server endpoints are inconsistent: some return seconds, others
10379
+ * milliseconds. Anything above 1e11 can't be a seconds-epoch (year 5138),
10380
+ * so treat it as milliseconds.
10381
+ * @internal
10382
+ */
10383
+ static expiresAtToMs(expiresAt) {
10384
+ return expiresAt > 1e11 ? expiresAt : expiresAt * 1e3;
10385
+ }
7939
10386
  /**
7940
10387
  * Schedule automatic token refresh
7941
10388
  * @internal
@@ -7944,11 +10391,18 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7944
10391
  if (this._refreshTimer) {
7945
10392
  clearTimeout(this._refreshTimer);
7946
10393
  }
7947
- logger.debug(`[scheduleAutoRefresh] Scheduling refresh in ${Math.round(delayMs / 1e3 / 60)} minutes`);
10394
+ const clamped = Math.min(Math.max(delayMs, 3e4), 2147483647);
10395
+ logger.debug(`[scheduleAutoRefresh] Scheduling refresh in ${Math.round(clamped / 1e3 / 60)} minutes`);
7948
10396
  this._refreshTimer = setTimeout(async () => {
7949
10397
  logger.info("[autoRefresh] Auto-refreshing token...");
7950
- await this.refreshToken();
7951
- }, delayMs);
10398
+ const user = await this.refreshToken();
10399
+ if (!user) {
10400
+ logger.error("[autoRefresh] Refresh failed, clearing session");
10401
+ this.authManager.clear();
10402
+ this._currentUser = null;
10403
+ this._emitAuthEvent("SESSION_EXPIRED");
10404
+ }
10405
+ }, clamped);
7952
10406
  }
7953
10407
  /**
7954
10408
  * Get a database/repository interface
@@ -7967,15 +10421,35 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7967
10421
  void 0,
7968
10422
  () => this.getUploadManager(),
7969
10423
  (options) => this.signAssetUrl(options),
7970
- { httpBaseUrl: this._httpBaseUrl, authManager: this.authManager }
10424
+ {
10425
+ httpBaseUrl: this._httpBaseUrl,
10426
+ authManager: this.authManager,
10427
+ onReconnected: (callback) => this.onReconnected(callback)
10428
+ }
7971
10429
  );
7972
10430
  }
7973
10431
  /**
7974
10432
  * Send a request to the server
10433
+ *
10434
+ * If the connection is temporarily down (connecting/reconnecting, or
10435
+ * disconnected with auto-reconnect enabled), the request is queued and
10436
+ * flushed once the connection is re-established and re-authenticated.
10437
+ * Queued requests are still subject to their own request timeout, and the
10438
+ * queue is bounded (overflow rejects immediately).
7975
10439
  */
7976
10440
  async sendRequestInternal(payload, requestType, contextOverride, requestOptions) {
7977
- if (!this.connection.isConnected()) {
7978
- throw new Error("Not connected to server");
10441
+ const connected = this.connection.isConnected();
10442
+ if (!connected) {
10443
+ const state = this.connection.getState();
10444
+ const willReconnect = state === "connecting" /* Connecting */ || state === "reconnecting" /* Reconnecting */ || state === "disconnected" /* Disconnected */ && this._everConnected && this.connection.isAutoReconnectEnabled();
10445
+ if (!willReconnect) {
10446
+ throw new Error("Not connected to server");
10447
+ }
10448
+ if (this._queuedRequests.length >= _RaisinClient.MAX_QUEUED_REQUESTS) {
10449
+ throw new Error(
10450
+ `Request queue is full (${_RaisinClient.MAX_QUEUED_REQUESTS} requests pending while reconnecting)`
10451
+ );
10452
+ }
7979
10453
  }
7980
10454
  const requestId = this.requestTracker.generateRequestId();
7981
10455
  const context = contextOverride || this._context;
@@ -7994,8 +10468,15 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
7994
10468
  try {
7995
10469
  const encoded = encodeMessage(request);
7996
10470
  logger.debug(`Encoded request - ID: ${requestId}, type: ${requestType}, size: ${encoded.length} bytes`);
7997
- this.connection.send(encoded);
7998
- logger.debug(`Request sent - ID: ${requestId}`);
10471
+ if (connected) {
10472
+ this.connection.send(encoded);
10473
+ logger.debug(`Request sent - ID: ${requestId}`);
10474
+ } else {
10475
+ this._queuedRequests.push({ requestId, encoded });
10476
+ logger.debug(
10477
+ `Request queued while reconnecting - ID: ${requestId}, queue size: ${this._queuedRequests.length}`
10478
+ );
10479
+ }
7999
10480
  } catch (error) {
8000
10481
  logger.error(`Failed to send request - ID: ${requestId}:`, error);
8001
10482
  this.requestTracker.rejectRequest(
@@ -8006,6 +10487,42 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8006
10487
  }
8007
10488
  return responsePromise;
8008
10489
  }
10490
+ /**
10491
+ * Flush requests queued while the connection was re-establishing.
10492
+ * Called after the connection is connected and (re-)authenticated.
10493
+ * @internal
10494
+ */
10495
+ _flushQueuedRequests() {
10496
+ if (this._queuedRequests.length === 0) return;
10497
+ const queued = this._queuedRequests;
10498
+ this._queuedRequests = [];
10499
+ logger.info(`[_flushQueuedRequests] Flushing ${queued.length} queued request(s)`);
10500
+ for (const { requestId, encoded } of queued) {
10501
+ if (!this.requestTracker.hasPendingRequest(requestId)) continue;
10502
+ try {
10503
+ this.connection.send(encoded);
10504
+ } catch (error) {
10505
+ this.requestTracker.rejectRequest(
10506
+ requestId,
10507
+ error instanceof Error ? error : new Error("Failed to send queued request")
10508
+ );
10509
+ }
10510
+ }
10511
+ }
10512
+ /**
10513
+ * Reject all requests queued while the connection was re-establishing
10514
+ * (e.g. re-authentication failed after reconnect).
10515
+ * @internal
10516
+ */
10517
+ _rejectQueuedRequests(error) {
10518
+ if (this._queuedRequests.length === 0) return;
10519
+ const queued = this._queuedRequests;
10520
+ this._queuedRequests = [];
10521
+ logger.warn(`[_rejectQueuedRequests] Rejecting ${queued.length} queued request(s): ${error.message}`);
10522
+ for (const { requestId } of queued) {
10523
+ this.requestTracker.rejectRequest(requestId, error);
10524
+ }
10525
+ }
8009
10526
  /**
8010
10527
  * Get the HTTP base URL derived from the WebSocket URL
8011
10528
  */
@@ -8083,6 +10600,44 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8083
10600
  getCurrentUser() {
8084
10601
  return this._currentUser;
8085
10602
  }
10603
+ /**
10604
+ * Re-resolve the current user's EFFECTIVE roles from the server and refresh
10605
+ * `getCurrentUser()`.
10606
+ *
10607
+ * Roles are deliberately not embedded in the JWT (they are resolved live
10608
+ * server-side), so when an admin changes a user's roles/groups the token does
10609
+ * not change — the client must re-ask. This re-runs the WS JWT auth handshake
10610
+ * (which returns freshly resolved effective roles), updates `_currentUser`, and
10611
+ * emits `USER_UPDATED` so role-gated UI can react without a reload.
10612
+ *
10613
+ * Called automatically when the server pushes `permissions:changed` after an
10614
+ * access-control write, and exposed for on-demand refresh. Safe no-op when not
10615
+ * authenticated.
10616
+ */
10617
+ async refreshCurrentUser() {
10618
+ if (__privateGet(this, _refreshingUser)) return __privateGet(this, _refreshingUser);
10619
+ const token = this.authManager.storage.getAccessToken();
10620
+ if (!token) return;
10621
+ __privateSet(this, _refreshingUser, (async () => {
10622
+ const before = JSON.stringify(this._currentUser?.roles ?? null);
10623
+ const prevNode = this._currentUser?.node;
10624
+ try {
10625
+ await this.authenticate({ type: "jwt", token });
10626
+ if (this._currentUser && prevNode && !this._currentUser.node) {
10627
+ this._currentUser.node = prevNode;
10628
+ }
10629
+ const after = JSON.stringify(this._currentUser?.roles ?? null);
10630
+ if (after !== before) {
10631
+ this._emitAuthEvent("USER_UPDATED");
10632
+ }
10633
+ } catch (err) {
10634
+ logger.warn("[refreshCurrentUser] role re-resolve failed:", err);
10635
+ } finally {
10636
+ __privateSet(this, _refreshingUser, null);
10637
+ }
10638
+ })());
10639
+ return __privateGet(this, _refreshingUser);
10640
+ }
8086
10641
  /**
8087
10642
  * Get current user ID
8088
10643
  *
@@ -8291,7 +10846,10 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8291
10846
  _emitAuthEvent(event) {
8292
10847
  const session = {
8293
10848
  user: this.getStoredUser(),
8294
- accessToken: this.authManager.storage.getAccessToken()
10849
+ accessToken: this.authManager.storage.getAccessToken(),
10850
+ // Surface effective roles so role-gated consumers can react to
10851
+ // SIGNED_IN / USER_UPDATED without calling getCurrentUser() themselves.
10852
+ roles: this._currentUser?.roles ?? null
8295
10853
  };
8296
10854
  const change = { event, session };
8297
10855
  for (const callback of this._authListeners) {
@@ -8331,6 +10889,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8331
10889
  */
8332
10890
  async _subscribeToUserHome(repository, userHome) {
8333
10891
  await this._unsubscribeFromUserHome();
10892
+ userHome = normalizeHomePath(userHome) ?? userHome;
8334
10893
  try {
8335
10894
  const db = this.database(repository);
8336
10895
  const pathParts = userHome.split("/").filter((p) => p.length > 0);
@@ -8630,7 +11189,7 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8630
11189
  const branch = options.branch ?? this._context.branch ?? "main";
8631
11190
  const pathWithProperty = options.propertyPath ? `${options.path}@${options.propertyPath}` : options.path;
8632
11191
  const endpoint = `/api/repository/${options.repository}/${branch}/head/${options.workspace}${pathWithProperty}/raisin:sign`;
8633
- const response = await fetch(`${this._httpBaseUrl}${endpoint}`, {
11192
+ const response = await this._fetchWithTimeout(`${this._httpBaseUrl}${endpoint}`, {
8634
11193
  method: "POST",
8635
11194
  headers: {
8636
11195
  "Content-Type": "application/json",
@@ -8685,6 +11244,10 @@ var RaisinClient = class _RaisinClient extends import_events4.EventEmitter {
8685
11244
  return _RaisinClient.forSSR(baseUrl, options);
8686
11245
  }
8687
11246
  };
11247
+ _refreshingUser = new WeakMap();
11248
+ /** Hard cap on requests held while reconnecting; overflow rejects immediately. */
11249
+ _RaisinClient.MAX_QUEUED_REQUESTS = 100;
11250
+ var RaisinClient = _RaisinClient;
8688
11251
 
8689
11252
  // src/node-builder.ts
8690
11253
  var NodeBuilder = class _NodeBuilder {
@@ -8772,7 +11335,12 @@ var NodeBuilder = class _NodeBuilder {
8772
11335
  return this;
8773
11336
  }
8774
11337
  /**
8775
- * Set the order key
11338
+ * Set the order key.
11339
+ *
11340
+ * @deprecated The server owns editorial ordering: the order key is assigned by
11341
+ * the ordering index on write, so any value set here is overwritten. To place a
11342
+ * node, create it (it appends to the end of its parent) then call
11343
+ * `nodes.reorder()`, `nodes.moveChildBefore()` or `nodes.moveChildAfter()`.
8776
11344
  */
8777
11345
  orderKey(orderKey) {
8778
11346
  this.node.order_key = orderKey;
@@ -8852,10 +11420,10 @@ var NodeBuilder = class _NodeBuilder {
8852
11420
  if (!this.node.node_type) {
8853
11421
  throw new Error("Node type is required");
8854
11422
  }
11423
+ if (this.node.order_key === void 0) this.node.order_key = "";
8855
11424
  if (!this.node.children) this.node.children = [];
8856
11425
  if (!this.node.relations) this.node.relations = [];
8857
11426
  if (!this.node.properties) this.node.properties = {};
8858
- if (!this.node.order_key) this.node.order_key = "a";
8859
11427
  if (typeof this.node.version !== "number") this.node.version = 1;
8860
11428
  return this.node;
8861
11429
  }
@@ -8937,8 +11505,8 @@ var NodeHelpers = class {
8937
11505
  node_type: "MockType",
8938
11506
  properties: {},
8939
11507
  children: [],
11508
+ order_key: "",
8940
11509
  relations: [],
8941
- order_key: "a",
8942
11510
  version: 1,
8943
11511
  ...overrides
8944
11512
  };
@@ -9019,6 +11587,29 @@ function isTerminalEvent(event) {
9019
11587
  return event.type === "flow_completed" || event.type === "flow_failed";
9020
11588
  }
9021
11589
 
11590
+ // src/types/flow-definition.ts
11591
+ function getRefDisplayName(ref) {
11592
+ const path = ref["raisin:path"] || ref["raisin:ref"];
11593
+ const segments = path.split("/").filter(Boolean);
11594
+ return segments[segments.length - 1] || path;
11595
+ }
11596
+ function getRefPath(ref) {
11597
+ return ref["raisin:path"] || ref["raisin:ref"];
11598
+ }
11599
+ var RETRY_STRATEGIES = {
11600
+ none: { max_retries: 0, base_delay_ms: 0, max_delay_ms: 0 },
11601
+ quick: { max_retries: 3, base_delay_ms: 1e3, max_delay_ms: 1e4 },
11602
+ standard: { max_retries: 5, base_delay_ms: 2e3, max_delay_ms: 6e4 },
11603
+ aggressive: { max_retries: 10, base_delay_ms: 5e3, max_delay_ms: 12e4 },
11604
+ llm: { max_retries: 5, base_delay_ms: 1e4, max_delay_ms: 12e4 }
11605
+ };
11606
+ function isFlowDefinitionStep(node) {
11607
+ return node.node_type === "raisin:FlowStep";
11608
+ }
11609
+ function isFlowDefinitionContainer(node) {
11610
+ return node.node_type === "raisin:FlowContainer";
11611
+ }
11612
+
9022
11613
  // src/utils/plan-projection.ts
9023
11614
  function asRecord(value) {
9024
11615
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@@ -9038,15 +11629,35 @@ function parsePlanTasks(rawTasks) {
9038
11629
  }
9039
11630
  const task = asRecord(rawTask);
9040
11631
  if (!task) return null;
11632
+ const delegation = asRecord(task.delegation);
9041
11633
  return {
9042
11634
  taskId: asString(task.task_id) ?? asString(task.id),
9043
11635
  title: asString(task.title) ?? `Task ${index + 1}`,
9044
11636
  status: asString(task.status) ?? "pending",
9045
11637
  description: asString(task.description),
9046
- priority: asString(task.priority)
11638
+ priority: asString(task.priority),
11639
+ delegation: delegation ? {
11640
+ id: asString(delegation.id),
11641
+ status: asString(delegation.status) ?? "unknown",
11642
+ agentRef: asString(delegation.agent_ref) ?? asString(delegation.agentRef),
11643
+ flowInstanceId: asString(delegation.flow_instance_id) ?? asString(delegation.flowInstanceId),
11644
+ branch: asString(delegation.branch),
11645
+ baseBranch: asString(delegation.base_branch) ?? asString(delegation.baseBranch),
11646
+ result: delegation.result,
11647
+ error: asString(delegation.error)
11648
+ } : void 0
9047
11649
  };
9048
11650
  }).filter((task) => task !== null);
9049
11651
  }
11652
+ var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled"]);
11653
+ var TERMINAL_PLAN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
11654
+ function setPlanStatus(plan, nextStatus) {
11655
+ if (!nextStatus) return;
11656
+ if (TERMINAL_PLAN_STATUSES.has(plan.status) && !TERMINAL_PLAN_STATUSES.has(nextStatus)) {
11657
+ return;
11658
+ }
11659
+ plan.status = nextStatus;
11660
+ }
9050
11661
  function upsertTask(plan, nextTask) {
9051
11662
  const existingIndex = plan.tasks.findIndex(
9052
11663
  (task) => nextTask.taskId && task.taskId === nextTask.taskId || !nextTask.taskId && task.title === nextTask.title
@@ -9055,8 +11666,12 @@ function upsertTask(plan, nextTask) {
9055
11666
  plan.tasks.push(nextTask);
9056
11667
  return;
9057
11668
  }
11669
+ const existing = plan.tasks[existingIndex];
11670
+ if (TERMINAL_TASK_STATUSES.has(existing.status) && !TERMINAL_TASK_STATUSES.has(nextTask.status)) {
11671
+ return;
11672
+ }
9058
11673
  plan.tasks[existingIndex] = {
9059
- ...plan.tasks[existingIndex],
11674
+ ...existing,
9060
11675
  ...nextTask
9061
11676
  };
9062
11677
  }
@@ -9100,7 +11715,7 @@ function projectPlansFromMessages(messages) {
9100
11715
  plan.planPath = planPath ?? plan.planPath;
9101
11716
  plan.planId = planId ?? plan.planId;
9102
11717
  plan.title = asString(data.title) || plan.title;
9103
- plan.status = asString(data.status) || plan.status;
11718
+ setPlanStatus(plan, asString(data.status));
9104
11719
  plan.requiresApproval = Boolean(data.requires_approval) || plan.status === "pending_approval";
9105
11720
  plan.sourceMessagePath = message.path ?? plan.sourceMessagePath;
9106
11721
  const parsedTasks = parsePlanTasks(data.tasks);
@@ -9116,7 +11731,7 @@ function projectPlansFromMessages(messages) {
9116
11731
  const plan = ensurePlan(plans, key, "Plan", timestamp);
9117
11732
  plan.planPath = planPath ?? plan.planPath;
9118
11733
  plan.planId = planId ?? plan.planId;
9119
- plan.status = asString(data.plan_status) || plan.status;
11734
+ setPlanStatus(plan, asString(data.plan_status));
9120
11735
  plan.requiresApproval = plan.status === "pending_approval";
9121
11736
  const taskUpdate = {
9122
11737
  taskId: asString(data.task_id),
@@ -9137,12 +11752,28 @@ function projectPlansFromMessages(messages) {
9137
11752
  plan.tasks = child.tasks.map((task) => ({
9138
11753
  taskId: task.id,
9139
11754
  title: task.title,
9140
- status: task.status
11755
+ status: task.status,
11756
+ delegation: task.delegation ? {
11757
+ id: task.delegation.id,
11758
+ status: task.delegation.status,
11759
+ agentRef: task.delegation.agentRef,
11760
+ flowInstanceId: task.delegation.flowInstanceId,
11761
+ branch: task.delegation.branch,
11762
+ baseBranch: task.delegation.baseBranch,
11763
+ result: task.delegation.result,
11764
+ error: task.delegation.error
11765
+ } : void 0
9141
11766
  }));
9142
11767
  }
9143
11768
  }
9144
11769
  }
9145
11770
  }
11771
+ for (const plan of plans.values()) {
11772
+ if (!TERMINAL_PLAN_STATUSES.has(plan.status) && plan.tasks.length > 0 && plan.tasks.every((task) => TERMINAL_TASK_STATUSES.has(task.status))) {
11773
+ plan.status = "completed";
11774
+ plan.requiresApproval = false;
11775
+ }
11776
+ }
9146
11777
  return [...plans.values()].sort((a, b) => {
9147
11778
  const aTime = Date.parse(a.updatedAt || "");
9148
11779
  const bTime = Date.parse(b.updatedAt || "");
@@ -9174,6 +11805,34 @@ var ConversationStore = class {
9174
11805
  this._watchdogIntervalMs = options.watchdogIntervalMs ?? 3e4;
9175
11806
  if (this._conversationPath) {
9176
11807
  this.ensureSubscription();
11808
+ } else if (this.createOptions?.anonymous) {
11809
+ void this.resumeAnonymous();
11810
+ }
11811
+ }
11812
+ anonymousOptions() {
11813
+ const a = this.createOptions?.anonymous;
11814
+ if (!a) return null;
11815
+ return a === true ? {} : a;
11816
+ }
11817
+ /**
11818
+ * Resume the visitor conversation this tab already has (the session secret
11819
+ * and conversation id live in sessionStorage), and load its history. A tab
11820
+ * without one starts nothing until the first message.
11821
+ */
11822
+ async resumeAnonymous() {
11823
+ const opts = this.anonymousOptions();
11824
+ if (!opts || !this.createOptions || this._conversationPath) return;
11825
+ const agent = this.createOptions.participant;
11826
+ try {
11827
+ if (!this.manager.hasStoredAnonymous(agent, opts)) return;
11828
+ const convo = await this.manager.startAnonymous(agent, opts);
11829
+ if (this._conversationPath) return;
11830
+ this._conversationPath = convo.conversationPath;
11831
+ this._conversationType = convo.type;
11832
+ this.ensureSubscription();
11833
+ await this.loadMessages();
11834
+ } catch (err) {
11835
+ logger.debug("[ConversationStore] No visitor conversation to resume", err);
9177
11836
  }
9178
11837
  }
9179
11838
  // ==========================================================================
@@ -9228,7 +11887,8 @@ var ConversationStore = class {
9228
11887
  throw new Error("No conversationPath and no createOptions provided");
9229
11888
  }
9230
11889
  try {
9231
- const convo = await this.manager.create({
11890
+ const anonymous = this.anonymousOptions();
11891
+ const convo = anonymous ? await this.manager.startAnonymous(this.createOptions.participant, anonymous) : await this.manager.create({
9232
11892
  participant: this.createOptions.participant,
9233
11893
  input: this.createOptions.input
9234
11894
  });
@@ -9357,10 +12017,12 @@ var ConversationStore = class {
9357
12017
  }
9358
12018
  switch (event.type) {
9359
12019
  case "text_chunk":
12020
+ this.markStreamingActivity();
9360
12021
  this._streamingText += event.text;
9361
12022
  this.notify();
9362
12023
  break;
9363
12024
  case "thought_chunk":
12025
+ this.markStreamingActivity();
9364
12026
  this.notify();
9365
12027
  break;
9366
12028
  case "assistant_message":
@@ -9369,6 +12031,7 @@ var ConversationStore = class {
9369
12031
  this.notify();
9370
12032
  break;
9371
12033
  case "tool_call_started": {
12034
+ this.markStreamingActivity();
9372
12035
  logger.debug("[ConversationStore] Tool started:", event.functionName);
9373
12036
  const info = {
9374
12037
  id: event.toolCallId,
@@ -9381,6 +12044,7 @@ var ConversationStore = class {
9381
12044
  break;
9382
12045
  }
9383
12046
  case "tool_call_completed": {
12047
+ this.markStreamingActivity();
9384
12048
  logger.debug("[ConversationStore] Tool completed:", event.toolCallId);
9385
12049
  const idx = this._activeToolCalls.findIndex((tc) => tc.id === event.toolCallId);
9386
12050
  if (idx >= 0) {
@@ -9455,6 +12119,7 @@ var ConversationStore = class {
9455
12119
  logger.error("[ConversationStore] Turn failed", event.error);
9456
12120
  this._error = event.error;
9457
12121
  this._isStreaming = false;
12122
+ this._isWaiting = false;
9458
12123
  this._streamingText = "";
9459
12124
  this._activeToolCalls = [];
9460
12125
  this.clearStreamingTimer();
@@ -9492,6 +12157,26 @@ var ConversationStore = class {
9492
12157
  }
9493
12158
  }
9494
12159
  }
12160
+ /**
12161
+ * Agent activity observed while the store thought the turn was over.
12162
+ *
12163
+ * This happens when execution continues WITHOUT a user message — plan
12164
+ * approval in approve_then_auto / step_by_step (approvePlan() returns an
12165
+ * enqueue receipt and the agent starts executing), or agent-initiated
12166
+ * follow-ups. The events arrive over the persistent live subscription
12167
+ * (no history replay), so activity is always "now": re-enter streaming
12168
+ * state so UIs show progress and the end-of-turn handling (done/waiting
12169
+ * → deferred reload) runs again for this continuation.
12170
+ */
12171
+ markStreamingActivity() {
12172
+ if (this._isStreaming) return;
12173
+ logger.debug("[ConversationStore] Agent activity while idle \u2014 re-entering streaming state");
12174
+ this._isStreaming = true;
12175
+ this._isWaiting = false;
12176
+ this._streamingText = "";
12177
+ this.resetStreamingTimer();
12178
+ this.startWatchdog();
12179
+ }
9495
12180
  /**
9496
12181
  * If accumulated streaming text hasn't been captured in an assistant_message,
9497
12182
  * synthesize one.
@@ -9725,6 +12410,14 @@ var ConversationStore = class {
9725
12410
  };
9726
12411
 
9727
12412
  // src/stores/conversation-list-store.ts
12413
+ function buildChatsSubscriptionFilters(userHome) {
12414
+ const home = normalizeHomePath(userHome) ?? userHome;
12415
+ return {
12416
+ workspace: "raisin:access_control",
12417
+ path: `${home}/inbox/chats/**`,
12418
+ event_types: ["node:created", "node:updated"]
12419
+ };
12420
+ }
9728
12421
  var ConversationListStore = class {
9729
12422
  constructor(options) {
9730
12423
  this.subscribers = /* @__PURE__ */ new Set();
@@ -9870,13 +12563,8 @@ var ConversationListStore = class {
9870
12563
  const rows = result.rows;
9871
12564
  const userHome = rows?.[0]?.home;
9872
12565
  if (!userHome) return;
9873
- const chatsPath = `${userHome}/inbox/chats`;
9874
12566
  this._realtimeSubscription = await events.subscribe(
9875
- {
9876
- workspace: "raisin:access_control",
9877
- path: chatsPath,
9878
- event_types: ["node:created", "node:updated"]
9879
- },
12567
+ buildChatsSubscriptionFilters(userHome),
9880
12568
  async (event) => {
9881
12569
  const eventData = event;
9882
12570
  const nodeId = eventData.payload?.node_id;
@@ -9982,7 +12670,7 @@ function useConversation(react, options) {
9982
12670
  var INITIAL_LIST_SNAPSHOT = {
9983
12671
  conversations: [],
9984
12672
  totalUnreadCount: 0,
9985
- isLoading: false,
12673
+ isLoading: true,
9986
12674
  error: null
9987
12675
  };
9988
12676
  function useConversationList(react, options) {
@@ -10860,8 +13548,164 @@ function createRaisinReact(react) {
10860
13548
  useFlow: useFlow2
10861
13549
  };
10862
13550
  }
13551
+
13552
+ // src/integrations/vue/vue-auth.ts
13553
+ function createUseAuth2(vue) {
13554
+ return function useAuth(client) {
13555
+ const adapter = createAuthAdapter(client);
13556
+ const snapshot = vue.ref(adapter.getSnapshot());
13557
+ const unsubscribe = adapter.subscribe((s) => {
13558
+ snapshot.value = s;
13559
+ });
13560
+ vue.onUnmounted(() => {
13561
+ unsubscribe();
13562
+ adapter.destroy();
13563
+ });
13564
+ return {
13565
+ user: vue.computed(() => snapshot.value.user),
13566
+ isAuthenticated: vue.computed(() => snapshot.value.isAuthenticated),
13567
+ isLoading: vue.computed(() => snapshot.value.isLoading),
13568
+ login: adapter.login,
13569
+ register: adapter.register,
13570
+ logout: adapter.logout,
13571
+ initSession: adapter.initSession
13572
+ };
13573
+ };
13574
+ }
13575
+
13576
+ // src/integrations/vue/vue-connection.ts
13577
+ function createUseConnection2(vue) {
13578
+ return function useConnection(client) {
13579
+ const adapter = createConnectionAdapter(client);
13580
+ const snapshot = vue.ref(adapter.getSnapshot());
13581
+ const unsubscribe = adapter.subscribe((s) => {
13582
+ snapshot.value = s;
13583
+ });
13584
+ vue.onUnmounted(() => {
13585
+ unsubscribe();
13586
+ adapter.destroy();
13587
+ });
13588
+ return {
13589
+ state: vue.computed(() => snapshot.value.state),
13590
+ isConnected: vue.computed(() => snapshot.value.isConnected),
13591
+ isReady: vue.computed(() => snapshot.value.isReady),
13592
+ connect: adapter.connect,
13593
+ disconnect: adapter.disconnect
13594
+ };
13595
+ };
13596
+ }
13597
+
13598
+ // src/integrations/vue/vue-sql.ts
13599
+ function createUseSql2(vue) {
13600
+ return function useSql(database, sql, params, options) {
13601
+ const adapter = createSqlAdapter(database, sql, params, options);
13602
+ const snapshot = vue.ref(adapter.getSnapshot());
13603
+ const unsubscribe = adapter.subscribe((s) => {
13604
+ snapshot.value = s;
13605
+ });
13606
+ vue.onUnmounted(() => {
13607
+ unsubscribe();
13608
+ adapter.destroy();
13609
+ });
13610
+ return {
13611
+ data: vue.computed(() => snapshot.value.data),
13612
+ isLoading: vue.computed(() => snapshot.value.isLoading),
13613
+ error: vue.computed(() => snapshot.value.error),
13614
+ refetch: adapter.refetch
13615
+ };
13616
+ };
13617
+ }
13618
+
13619
+ // src/integrations/vue/vue-subscription.ts
13620
+ function createUseSubscription2(vue) {
13621
+ return function useSubscription(database, filters, callback) {
13622
+ const adapter = createSubscriptionAdapter(database, filters, callback);
13623
+ vue.onUnmounted(() => adapter.destroy());
13624
+ return {
13625
+ unsubscribe: () => adapter.destroy()
13626
+ };
13627
+ };
13628
+ }
13629
+
13630
+ // src/integrations/vue/vue-conversation.ts
13631
+ function createUseConversation(vue) {
13632
+ return function useConversation2(options) {
13633
+ const store = new ConversationStore(options);
13634
+ const snapshot = vue.ref(store.getSnapshot());
13635
+ const unsubscribe = store.subscribe((s) => {
13636
+ snapshot.value = s;
13637
+ });
13638
+ vue.onUnmounted(() => {
13639
+ unsubscribe();
13640
+ store.destroy();
13641
+ });
13642
+ return {
13643
+ conversation: vue.computed(() => snapshot.value.conversation),
13644
+ messages: vue.computed(() => snapshot.value.messages),
13645
+ isStreaming: vue.computed(() => snapshot.value.isStreaming),
13646
+ isWaiting: vue.computed(() => snapshot.value.isWaiting),
13647
+ streamingText: vue.computed(() => snapshot.value.streamingText),
13648
+ error: vue.computed(() => snapshot.value.error),
13649
+ activeToolCalls: vue.computed(() => snapshot.value.activeToolCalls),
13650
+ plans: vue.computed(() => snapshot.value.plans),
13651
+ isLoading: vue.computed(() => snapshot.value.isLoading),
13652
+ conversationPath: vue.computed(() => snapshot.value.conversationPath),
13653
+ sendMessage: (content) => store.sendMessage(content),
13654
+ approvePlan: async (planPath) => {
13655
+ await store.approvePlan(planPath);
13656
+ },
13657
+ rejectPlan: async (planPath, feedback) => {
13658
+ await store.rejectPlan(planPath, feedback);
13659
+ },
13660
+ stop: () => store.stop(),
13661
+ loadMessages: () => store.loadMessages(),
13662
+ store
13663
+ };
13664
+ };
13665
+ }
13666
+ function createUseConversationList(vue) {
13667
+ return function useConversationList2(options) {
13668
+ const store = new ConversationListStore(options);
13669
+ const snapshot = vue.ref(store.getSnapshot());
13670
+ const unsubscribe = store.subscribe((s) => {
13671
+ snapshot.value = s;
13672
+ });
13673
+ store.load().catch((err) => {
13674
+ logger.error("[useConversationList] Initial load failed:", err);
13675
+ });
13676
+ vue.onUnmounted(() => {
13677
+ unsubscribe();
13678
+ store.destroy();
13679
+ });
13680
+ return {
13681
+ conversations: vue.computed(() => snapshot.value.conversations),
13682
+ totalUnreadCount: vue.computed(() => snapshot.value.totalUnreadCount),
13683
+ isLoading: vue.computed(() => snapshot.value.isLoading),
13684
+ error: vue.computed(() => snapshot.value.error),
13685
+ createConversation: (opts) => store.createConversation(opts),
13686
+ deleteConversation: (conversationPath) => store.deleteConversation(conversationPath),
13687
+ markAsRead: (conversationPath) => store.markAsRead(conversationPath),
13688
+ refresh: () => store.load(),
13689
+ store
13690
+ };
13691
+ };
13692
+ }
13693
+
13694
+ // src/integrations/vue/index.ts
13695
+ function createRaisinVue(vue) {
13696
+ return {
13697
+ useAuth: createUseAuth2(vue),
13698
+ useConnection: createUseConnection2(vue),
13699
+ useSql: createUseSql2(vue),
13700
+ useSubscription: createUseSubscription2(vue),
13701
+ useConversation: createUseConversation(vue),
13702
+ useConversationList: createUseConversationList(vue)
13703
+ };
13704
+ }
10863
13705
  // Annotate the CommonJS export names for ESM import in node:
10864
13706
  0 && (module.exports = {
13707
+ AgentRunsApi,
13708
+ AgentRunsWsApi,
10865
13709
  AllNodeEventTypes,
10866
13710
  Archetypes,
10867
13711
  AuthManager,
@@ -10881,6 +13725,7 @@ function createRaisinReact(react) {
10881
13725
  DEFAULT_RETRY_BASE_DELAY,
10882
13726
  DEFAULT_RETRY_MAX_DELAY,
10883
13727
  DEFAULT_RETRY_OPTIONS,
13728
+ DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS,
10884
13729
  Database,
10885
13730
  ElementTypes,
10886
13731
  EventHandler,
@@ -10888,19 +13733,27 @@ function createRaisinReact(react) {
10888
13733
  FlowClient,
10889
13734
  FlowsApi,
10890
13735
  FunctionsApi,
13736
+ HttpArchetypes,
10891
13737
  HttpDatabase,
13738
+ HttpElementTypes,
10892
13739
  HttpFunctionsApi,
13740
+ HttpNodeTypes,
10893
13741
  HttpWorkspaceClient,
13742
+ IdentityAuthApi,
13743
+ InboxApi,
10894
13744
  LocalStorageTokenStorage,
10895
13745
  LogLevel,
13746
+ McpClient,
10896
13747
  MemoryTokenStorage,
10897
13748
  NodeBuilder,
13749
+ NodeDevApi,
10898
13750
  NodeEventType,
10899
13751
  NodeFileSource,
10900
13752
  NodeHelpers,
10901
13753
  NodeOperations,
10902
13754
  NodeTypes,
10903
13755
  RAISIN_CONTEXT_KEY,
13756
+ RETRY_STRATEGIES,
10904
13757
  RaisinAbortError,
10905
13758
  RaisinAuthError,
10906
13759
  RaisinClient,
@@ -10915,6 +13768,7 @@ function createRaisinReact(react) {
10915
13768
  ResponseStatus,
10916
13769
  RetryBuilder,
10917
13770
  SSEClient,
13771
+ SchedulerApi,
10918
13772
  SqlQuery,
10919
13773
  Tags,
10920
13774
  Transaction,
@@ -10922,6 +13776,7 @@ function createRaisinReact(react) {
10922
13776
  UploadErrorCode,
10923
13777
  UploadManager,
10924
13778
  Uploader,
13779
+ VisitorChatError,
10925
13780
  WorkspaceClient,
10926
13781
  WorkspaceManager,
10927
13782
  calculateChunkCount,
@@ -10936,31 +13791,42 @@ function createRaisinReact(react) {
10936
13791
  createFlowAdapter,
10937
13792
  createLoader,
10938
13793
  createRaisinReact,
13794
+ createRaisinVue,
10939
13795
  createRequestScopedClient,
10940
13796
  createSSRClient,
10941
13797
  createSqlAdapter,
10942
13798
  createSqlHandler,
10943
13799
  createSubscriptionAdapter,
10944
13800
  decodeMessage,
13801
+ defaultVisitorSessionStore,
10945
13802
  encodeMessage,
10946
13803
  getChunkRange,
10947
13804
  getContentType,
10948
13805
  getErrorMessage,
10949
13806
  getLogLevel,
10950
13807
  getLoggerConfig,
13808
+ getRefDisplayName,
13809
+ getRefPath,
10951
13810
  isEventMessage,
13811
+ isFlowDefinitionContainer,
13812
+ isFlowDefinitionStep,
10952
13813
  isHttpClient,
10953
13814
  isResponseEnvelope,
10954
13815
  isRetryableError,
10955
13816
  isTerminalEvent,
10956
13817
  isWebSocketClient,
10957
13818
  logger,
13819
+ newControlId,
13820
+ normalizeHomePath,
10958
13821
  projectPlansFromMessages,
13822
+ readTokensFromFragment,
10959
13823
  rowsToObjects,
10960
13824
  setLogLevel,
10961
13825
  sleep,
13826
+ subjectOf,
10962
13827
  useConversation,
10963
13828
  useConversationList,
10964
13829
  useFlow,
10965
13830
  withRetry
10966
13831
  });
13832
+ //# sourceMappingURL=index.js.map