@starcite/sdk 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,25 +17,141 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
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
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ InMemoryCursorStore: () => InMemoryCursorStore,
34
+ LocalStorageCursorStore: () => LocalStorageCursorStore,
35
+ MemoryStore: () => MemoryStore,
36
+ SessionLogConflictError: () => SessionLogConflictError,
37
+ SessionLogGapError: () => SessionLogGapError,
38
+ Starcite: () => Starcite,
23
39
  StarciteApiError: () => StarciteApiError,
24
- StarciteClient: () => StarciteClient,
40
+ StarciteBackpressureError: () => StarciteBackpressureError,
25
41
  StarciteConnectionError: () => StarciteConnectionError,
26
42
  StarciteError: () => StarciteError,
43
+ StarciteIdentity: () => StarciteIdentity,
44
+ StarciteRetryLimitError: () => StarciteRetryLimitError,
27
45
  StarciteSession: () => StarciteSession,
28
- createStarciteClient: () => createStarciteClient,
29
- normalizeBaseUrl: () => normalizeBaseUrl,
30
- starcite: () => starcite
46
+ StarciteTailError: () => StarciteTailError,
47
+ StarciteTokenExpiredError: () => StarciteTokenExpiredError,
48
+ WebStorageCursorStore: () => WebStorageCursorStore
31
49
  });
32
50
  module.exports = __toCommonJS(index_exports);
33
51
 
34
- // src/client.ts
52
+ // src/auth.ts
53
+ var import_jose = require("jose");
35
54
  var import_zod2 = require("zod");
36
55
 
56
+ // src/identity.ts
57
+ var import_zod = require("zod");
58
+ var AGENT_PREFIX = "agent:";
59
+ var USER_PREFIX = "user:";
60
+ var PrincipalTypeSchema = import_zod.z.enum(["user", "agent"]);
61
+ var SessionCreatorPrincipalSchema = import_zod.z.object({
62
+ tenant_id: import_zod.z.string().min(1),
63
+ id: import_zod.z.string().min(1),
64
+ type: PrincipalTypeSchema
65
+ });
66
+ var SessionTokenPrincipalSchema = import_zod.z.object({
67
+ type: PrincipalTypeSchema,
68
+ id: import_zod.z.string().min(1)
69
+ });
70
+ var StarciteIdentity = class {
71
+ tenantId;
72
+ id;
73
+ type;
74
+ constructor(options) {
75
+ this.tenantId = options.tenantId;
76
+ this.id = options.id;
77
+ this.type = options.type;
78
+ }
79
+ /**
80
+ * Serializes to the `creator_principal` wire format expected by the API.
81
+ */
82
+ toCreatorPrincipal() {
83
+ return { tenant_id: this.tenantId, id: this.id, type: this.type };
84
+ }
85
+ /**
86
+ * Serializes to the `principal` wire format used in session token requests.
87
+ */
88
+ toTokenPrincipal() {
89
+ return { id: this.id, type: this.type };
90
+ }
91
+ /**
92
+ * Returns the actor string derived from this identity (e.g. `agent:planner`, `user:alice`).
93
+ */
94
+ toActor() {
95
+ if (this.id.startsWith(AGENT_PREFIX) || this.id.startsWith(USER_PREFIX)) {
96
+ return this.id;
97
+ }
98
+ return `${this.type}:${this.id}`;
99
+ }
100
+ };
101
+ function agentFromActor(actor) {
102
+ return actor.startsWith(AGENT_PREFIX) ? actor.slice(AGENT_PREFIX.length) : void 0;
103
+ }
104
+
105
+ // src/auth.ts
106
+ var ApiKeyClaimsSchema = import_zod2.z.object({
107
+ iss: import_zod2.z.string().min(1).optional(),
108
+ sub: import_zod2.z.string().min(1).optional(),
109
+ tenant_id: import_zod2.z.string().min(1).optional(),
110
+ principal_id: import_zod2.z.string().min(1).optional(),
111
+ principal_type: PrincipalTypeSchema.optional()
112
+ });
113
+ var SessionTokenClaimsSchema = import_zod2.z.object({
114
+ session_id: import_zod2.z.string().min(1).optional(),
115
+ sub: import_zod2.z.string().min(1).optional(),
116
+ tenant_id: import_zod2.z.string().min(1),
117
+ principal_id: import_zod2.z.string().min(1).optional(),
118
+ principal_type: PrincipalTypeSchema.optional()
119
+ });
120
+ function inferIssuerAuthorityFromApiKey(apiKey) {
121
+ const claims = ApiKeyClaimsSchema.parse((0, import_jose.decodeJwt)(apiKey));
122
+ if (!claims.iss) {
123
+ return void 0;
124
+ }
125
+ const url = new URL(claims.iss);
126
+ return url.origin;
127
+ }
128
+ function inferIdentityFromApiKey(apiKey) {
129
+ const claims = ApiKeyClaimsSchema.parse((0, import_jose.decodeJwt)(apiKey));
130
+ const id = claims.principal_id ?? claims.sub;
131
+ const tenantId = claims.tenant_id;
132
+ if (!(tenantId && id && claims.principal_type)) {
133
+ return void 0;
134
+ }
135
+ return new StarciteIdentity({
136
+ tenantId,
137
+ id,
138
+ type: claims.principal_type
139
+ });
140
+ }
141
+ function decodeSessionToken(token) {
142
+ const claims = SessionTokenClaimsSchema.parse((0, import_jose.decodeJwt)(token));
143
+ const principalId = claims.principal_id ?? claims.sub ?? "session-user";
144
+ const principalType = claims.principal_type ?? "user";
145
+ return {
146
+ sessionId: claims.session_id,
147
+ identity: new StarciteIdentity({
148
+ tenantId: claims.tenant_id,
149
+ id: principalId,
150
+ type: principalType
151
+ })
152
+ };
153
+ }
154
+
37
155
  // src/errors.ts
38
156
  var StarciteError = class extends Error {
39
157
  constructor(message) {
@@ -62,766 +180,1675 @@ var StarciteConnectionError = class extends StarciteError {
62
180
  this.name = "StarciteConnectionError";
63
181
  }
64
182
  };
183
+ var StarciteTailError = class extends StarciteConnectionError {
184
+ /** Session id tied to this tail stream. */
185
+ sessionId;
186
+ /** Failure stage in the tail lifecycle. */
187
+ stage;
188
+ /** Reconnect attempts observed before failing. */
189
+ attempts;
190
+ /** WebSocket close code when available. */
191
+ closeCode;
192
+ /** WebSocket close reason when available. */
193
+ closeReason;
194
+ constructor(message, options) {
195
+ super(message);
196
+ this.name = "StarciteTailError";
197
+ this.sessionId = options.sessionId;
198
+ this.stage = options.stage;
199
+ this.attempts = options.attempts ?? 0;
200
+ this.closeCode = options.closeCode;
201
+ this.closeReason = options.closeReason;
202
+ }
203
+ };
204
+ var StarciteTokenExpiredError = class extends StarciteTailError {
205
+ constructor(message, options) {
206
+ super(message, { ...options, stage: "stream" });
207
+ this.name = "StarciteTokenExpiredError";
208
+ }
209
+ };
210
+ var StarciteBackpressureError = class extends StarciteTailError {
211
+ constructor(message, options) {
212
+ super(message, { ...options, stage: "consumer_backpressure" });
213
+ this.name = "StarciteBackpressureError";
214
+ }
215
+ };
216
+ var StarciteRetryLimitError = class extends StarciteTailError {
217
+ constructor(message, options) {
218
+ super(message, { ...options, stage: "retry_limit" });
219
+ this.name = "StarciteRetryLimitError";
220
+ }
221
+ };
222
+
223
+ // src/session.ts
224
+ var import_eventemitter33 = __toESM(require("eventemitter3"), 1);
225
+
226
+ // src/session-log.ts
227
+ var import_eventemitter3 = __toESM(require("eventemitter3"), 1);
228
+ var SessionLogGapError = class extends StarciteError {
229
+ constructor(message) {
230
+ super(message);
231
+ this.name = "SessionLogGapError";
232
+ }
233
+ };
234
+ var SessionLogConflictError = class extends StarciteError {
235
+ constructor(message) {
236
+ super(message);
237
+ this.name = "SessionLogConflictError";
238
+ }
239
+ };
240
+ var SessionLog = class {
241
+ history = [];
242
+ emitter = new import_eventemitter3.default();
243
+ canonicalBySeq = /* @__PURE__ */ new Map();
244
+ maxEvents;
245
+ appliedSeq = 0;
246
+ constructor(options = {}) {
247
+ this.setMaxEvents(options.maxEvents);
248
+ }
249
+ setMaxEvents(maxEvents) {
250
+ if (maxEvents !== void 0 && (!Number.isInteger(maxEvents) || maxEvents <= 0)) {
251
+ throw new StarciteError(
252
+ "Session log maxEvents must be a positive integer"
253
+ );
254
+ }
255
+ this.maxEvents = maxEvents;
256
+ this.enforceRetention();
257
+ }
258
+ applyBatch(batch) {
259
+ const applied = [];
260
+ for (const event of batch) {
261
+ if (this.apply(event)) {
262
+ applied.push(event);
263
+ this.emitter.emit("event", event);
264
+ }
265
+ }
266
+ return applied;
267
+ }
268
+ hydrate(state) {
269
+ if (!Number.isInteger(state.cursor) || state.cursor < 0) {
270
+ throw new StarciteError(
271
+ "Session store cursor must be a non-negative integer"
272
+ );
273
+ }
274
+ this.history.length = 0;
275
+ this.canonicalBySeq.clear();
276
+ this.appliedSeq = state.cursor;
277
+ let previousSeq;
278
+ for (const event of state.events) {
279
+ if (event.seq > state.cursor) {
280
+ throw new StarciteError(
281
+ `Session store contains event seq ${event.seq} above cursor ${state.cursor}`
282
+ );
283
+ }
284
+ if (previousSeq !== void 0 && event.seq !== previousSeq + 1) {
285
+ throw new StarciteError(
286
+ `Session store events must be contiguous; saw seq ${event.seq} after ${previousSeq}`
287
+ );
288
+ }
289
+ this.history.push(event);
290
+ this.canonicalBySeq.set(event.seq, JSON.stringify(event));
291
+ previousSeq = event.seq;
292
+ }
293
+ this.enforceRetention();
294
+ }
295
+ subscribe(listener, options = {}) {
296
+ const shouldReplay = options.replay ?? true;
297
+ if (shouldReplay) {
298
+ for (const event of this.history) {
299
+ listener(event);
300
+ }
301
+ }
302
+ this.emitter.on("event", listener);
303
+ return () => {
304
+ this.emitter.off("event", listener);
305
+ };
306
+ }
307
+ apply(event) {
308
+ const existingCanonical = this.canonicalBySeq.get(event.seq);
309
+ if (event.seq <= this.appliedSeq) {
310
+ const incomingCanonical = JSON.stringify(event);
311
+ if (!existingCanonical) {
312
+ const oldestRetainedSeq = this.history[0]?.seq;
313
+ if (oldestRetainedSeq === void 0 || event.seq < oldestRetainedSeq) {
314
+ return false;
315
+ }
316
+ throw new SessionLogConflictError(
317
+ `Session log has no canonical payload for retained seq ${event.seq}`
318
+ );
319
+ }
320
+ if (incomingCanonical !== existingCanonical) {
321
+ throw new SessionLogConflictError(
322
+ `Session log conflict for seq ${event.seq}: received different payload for an already-applied event`
323
+ );
324
+ }
325
+ return false;
326
+ }
327
+ const expectedSeq = this.appliedSeq + 1;
328
+ if (event.seq !== expectedSeq) {
329
+ throw new SessionLogGapError(
330
+ `Session log gap detected: expected seq ${expectedSeq} but received ${event.seq}`
331
+ );
332
+ }
333
+ this.history.push(event);
334
+ this.canonicalBySeq.set(event.seq, JSON.stringify(event));
335
+ this.appliedSeq = event.seq;
336
+ this.enforceRetention();
337
+ return true;
338
+ }
339
+ getSnapshot(syncing) {
340
+ return {
341
+ events: this.history.slice(),
342
+ lastSeq: this.appliedSeq,
343
+ syncing
344
+ };
345
+ }
346
+ get events() {
347
+ return this.history.slice();
348
+ }
349
+ get cursor() {
350
+ return this.appliedSeq;
351
+ }
352
+ get lastSeq() {
353
+ return this.appliedSeq;
354
+ }
355
+ enforceRetention() {
356
+ if (this.maxEvents === void 0) {
357
+ return;
358
+ }
359
+ while (this.history.length > this.maxEvents) {
360
+ const removed = this.history.shift();
361
+ if (!removed) {
362
+ return;
363
+ }
364
+ this.canonicalBySeq.delete(removed.seq);
365
+ }
366
+ }
367
+ };
368
+
369
+ // src/tail/frame.ts
370
+ var import_zod4 = require("zod");
65
371
 
66
372
  // src/types.ts
67
- var import_zod = require("zod");
68
- var ArbitraryObjectSchema = import_zod.z.record(import_zod.z.unknown());
69
- var CreatorTypeSchema = import_zod.z.union([import_zod.z.literal("user"), import_zod.z.literal("agent")]);
70
- var SessionCreatorPrincipalSchema = import_zod.z.object({
71
- tenant_id: import_zod.z.string().min(1),
72
- id: import_zod.z.string().min(1),
73
- type: CreatorTypeSchema
373
+ var import_zod3 = require("zod");
374
+ var SessionCreatorPrincipalSchema2 = SessionCreatorPrincipalSchema;
375
+ var SessionTokenPrincipalSchema2 = SessionTokenPrincipalSchema;
376
+ var ArbitraryObjectSchema = import_zod3.z.record(import_zod3.z.unknown());
377
+ var SessionTokenScopeSchema = import_zod3.z.enum(["session:read", "session:append"]);
378
+ var IssueSessionTokenInputSchema = import_zod3.z.object({
379
+ session_id: import_zod3.z.string().min(1),
380
+ principal: SessionTokenPrincipalSchema2,
381
+ scopes: import_zod3.z.array(SessionTokenScopeSchema).min(1),
382
+ ttl_seconds: import_zod3.z.number().int().positive().max(24 * 60 * 60).optional()
74
383
  });
75
- var CreateSessionInputSchema = import_zod.z.object({
76
- id: import_zod.z.string().optional(),
77
- title: import_zod.z.string().optional(),
384
+ var IssueSessionTokenResponseSchema = import_zod3.z.object({
385
+ token: import_zod3.z.string().min(1),
386
+ expires_in: import_zod3.z.number().int().positive()
387
+ });
388
+ var CreateSessionInputSchema = import_zod3.z.object({
389
+ id: import_zod3.z.string().optional(),
390
+ title: import_zod3.z.string().optional(),
78
391
  metadata: ArbitraryObjectSchema.optional(),
79
- creator_principal: SessionCreatorPrincipalSchema.optional()
392
+ creator_principal: SessionCreatorPrincipalSchema2.optional()
80
393
  });
81
- var SessionRecordSchema = import_zod.z.object({
82
- id: import_zod.z.string(),
83
- title: import_zod.z.string().nullable().optional(),
394
+ var SessionRecordSchema = import_zod3.z.object({
395
+ id: import_zod3.z.string(),
396
+ title: import_zod3.z.string().nullable().optional(),
84
397
  metadata: ArbitraryObjectSchema,
85
- last_seq: import_zod.z.number().int().nonnegative(),
86
- created_at: import_zod.z.string(),
87
- updated_at: import_zod.z.string()
398
+ last_seq: import_zod3.z.number().int().nonnegative(),
399
+ created_at: import_zod3.z.string(),
400
+ updated_at: import_zod3.z.string()
88
401
  });
89
- var SessionListItemSchema = import_zod.z.object({
90
- id: import_zod.z.string(),
91
- title: import_zod.z.string().nullable().optional(),
402
+ var SessionListItemSchema = import_zod3.z.object({
403
+ id: import_zod3.z.string(),
404
+ title: import_zod3.z.string().nullable().optional(),
92
405
  metadata: ArbitraryObjectSchema,
93
- created_at: import_zod.z.string()
406
+ created_at: import_zod3.z.string()
94
407
  });
95
- var SessionListPageSchema = import_zod.z.object({
96
- sessions: import_zod.z.array(SessionListItemSchema),
97
- next_cursor: import_zod.z.string().nullable()
408
+ var SessionListPageSchema = import_zod3.z.object({
409
+ sessions: import_zod3.z.array(SessionListItemSchema),
410
+ next_cursor: import_zod3.z.string().nullable()
98
411
  });
99
- var AppendEventRequestSchema = import_zod.z.object({
100
- type: import_zod.z.string().min(1),
412
+ var AppendEventRequestSchema = import_zod3.z.object({
413
+ type: import_zod3.z.string().min(1),
101
414
  payload: ArbitraryObjectSchema,
102
- actor: import_zod.z.string().min(1),
103
- producer_id: import_zod.z.string().min(1),
104
- producer_seq: import_zod.z.number().int().positive(),
105
- source: import_zod.z.string().optional(),
415
+ actor: import_zod3.z.string().min(1).optional(),
416
+ producer_id: import_zod3.z.string().min(1),
417
+ producer_seq: import_zod3.z.number().int().positive(),
418
+ source: import_zod3.z.string().optional(),
106
419
  metadata: ArbitraryObjectSchema.optional(),
107
420
  refs: ArbitraryObjectSchema.optional(),
108
- idempotency_key: import_zod.z.string().optional(),
109
- expected_seq: import_zod.z.number().int().nonnegative().optional()
421
+ idempotency_key: import_zod3.z.string().optional(),
422
+ expected_seq: import_zod3.z.number().int().nonnegative().optional()
110
423
  });
111
- var AppendEventResponseSchema = import_zod.z.object({
112
- seq: import_zod.z.number().int().nonnegative(),
113
- last_seq: import_zod.z.number().int().nonnegative(),
114
- deduped: import_zod.z.boolean()
424
+ var AppendEventResponseSchema = import_zod3.z.object({
425
+ seq: import_zod3.z.number().int().nonnegative(),
426
+ last_seq: import_zod3.z.number().int().nonnegative(),
427
+ deduped: import_zod3.z.boolean()
115
428
  });
116
- var TailEventSchema = import_zod.z.object({
117
- seq: import_zod.z.number().int().nonnegative(),
118
- type: import_zod.z.string().min(1),
429
+ var TailEventSchema = import_zod3.z.object({
430
+ seq: import_zod3.z.number().int().nonnegative(),
431
+ type: import_zod3.z.string().min(1),
119
432
  payload: ArbitraryObjectSchema,
120
- actor: import_zod.z.string().min(1),
121
- producer_id: import_zod.z.string().min(1),
122
- producer_seq: import_zod.z.number().int().positive(),
123
- source: import_zod.z.string().optional(),
433
+ actor: import_zod3.z.string().min(1),
434
+ producer_id: import_zod3.z.string().min(1),
435
+ producer_seq: import_zod3.z.number().int().positive(),
436
+ source: import_zod3.z.string().optional(),
124
437
  metadata: ArbitraryObjectSchema.optional(),
125
438
  refs: ArbitraryObjectSchema.optional(),
126
- idempotency_key: import_zod.z.string().nullable().optional(),
127
- inserted_at: import_zod.z.string().optional()
128
- });
129
- var SessionEventInternalSchema = TailEventSchema.extend({
130
- agent: import_zod.z.string().optional(),
131
- text: import_zod.z.string().optional()
439
+ idempotency_key: import_zod3.z.string().nullable().optional(),
440
+ inserted_at: import_zod3.z.string().optional()
132
441
  });
133
- var SessionAppendInputSchema = import_zod.z.object({
134
- agent: import_zod.z.string().trim().min(1),
135
- producerId: import_zod.z.string().trim().min(1),
136
- producerSeq: import_zod.z.number().int().positive(),
137
- text: import_zod.z.string().optional(),
442
+ var SessionAppendInputSchema = import_zod3.z.object({
443
+ text: import_zod3.z.string().optional(),
138
444
  payload: ArbitraryObjectSchema.optional(),
139
- type: import_zod.z.string().optional(),
140
- source: import_zod.z.string().optional(),
445
+ type: import_zod3.z.string().optional(),
446
+ actor: import_zod3.z.string().trim().min(1).optional(),
447
+ source: import_zod3.z.string().optional(),
141
448
  metadata: ArbitraryObjectSchema.optional(),
142
449
  refs: ArbitraryObjectSchema.optional(),
143
- idempotencyKey: import_zod.z.string().optional(),
144
- expectedSeq: import_zod.z.number().int().nonnegative().optional()
450
+ idempotencyKey: import_zod3.z.string().optional(),
451
+ expectedSeq: import_zod3.z.number().int().nonnegative().optional()
145
452
  }).refine((value) => !!(value.text || value.payload), {
146
453
  message: "append() requires either 'text' or an object 'payload'"
147
454
  });
148
- var StarciteErrorPayloadSchema = import_zod.z.object({
149
- error: import_zod.z.string().optional(),
150
- message: import_zod.z.string().optional()
151
- }).catchall(import_zod.z.unknown());
455
+ var SessionListOptionsSchema = import_zod3.z.object({
456
+ limit: import_zod3.z.number().int().positive().optional(),
457
+ cursor: import_zod3.z.string().trim().min(1).optional(),
458
+ metadata: import_zod3.z.record(import_zod3.z.string().trim().min(1), import_zod3.z.string().trim().min(1)).optional()
459
+ });
152
460
 
153
- // src/client.ts
154
- var DEFAULT_BASE_URL = typeof process !== "undefined" && process.env.STARCITE_BASE_URL ? process.env.STARCITE_BASE_URL : "http://localhost:4000";
155
- var TRAILING_SLASHES_REGEX = /\/+$/;
156
- var BEARER_PREFIX_REGEX = /^bearer\s+/i;
157
- var DEFAULT_TAIL_RECONNECT_DELAY_MS = 3e3;
158
- var NORMAL_WEBSOCKET_CLOSE_CODE = 1e3;
159
- var SERVICE_TOKEN_SUB_ORG_PREFIX = "org:";
160
- var SERVICE_TOKEN_SUB_AGENT_PREFIX = "agent:";
161
- var SERVICE_TOKEN_SUB_USER_PREFIX = "user:";
162
- var TailFrameSchema = import_zod2.z.string().transform((frame, context) => {
461
+ // src/tail/frame.ts
462
+ var MIN_TAIL_BATCH_SIZE = 1;
463
+ var TailFramePayloadSchema = import_zod4.z.union([
464
+ TailEventSchema,
465
+ import_zod4.z.array(TailEventSchema).min(MIN_TAIL_BATCH_SIZE)
466
+ ]);
467
+ function toFrameText(data) {
468
+ if (typeof data === "string") {
469
+ return data;
470
+ }
471
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
472
+ return new TextDecoder().decode(data);
473
+ }
474
+ return void 0;
475
+ }
476
+ function parseTailFrame(data) {
477
+ const frameText = toFrameText(data);
478
+ if (!frameText) {
479
+ throw new StarciteConnectionError(
480
+ "Tail frame payload must be a UTF-8 string or binary buffer"
481
+ );
482
+ }
483
+ let framePayload;
163
484
  try {
164
- return JSON.parse(frame);
485
+ framePayload = JSON.parse(frameText);
165
486
  } catch {
166
- context.addIssue({
167
- code: import_zod2.z.ZodIssueCode.custom,
168
- message: "Tail frame was not valid JSON"
487
+ throw new StarciteConnectionError("Tail frame was not valid JSON");
488
+ }
489
+ const result = TailFramePayloadSchema.safeParse(framePayload);
490
+ if (!result.success) {
491
+ const reason = result.error.issues[0]?.message ?? "Tail frame did not match schema";
492
+ throw new StarciteConnectionError(reason);
493
+ }
494
+ return Array.isArray(result.data) ? result.data : [result.data];
495
+ }
496
+
497
+ // src/tail/managed-websocket.ts
498
+ var import_eventemitter32 = __toESM(require("eventemitter3"), 1);
499
+ var NORMAL_CLOSE_CODE = 1e3;
500
+ var INACTIVITY_CLOSE_CODE = 4e3;
501
+ var CONNECTION_TIMEOUT_CLOSE_CODE = 4100;
502
+ var ManagedWebSocket = class extends import_eventemitter32.default {
503
+ options;
504
+ socket;
505
+ cancelReconnectWait;
506
+ // Set while a socket run is active so `close()` can synchronously settle it.
507
+ forceCloseSocket;
508
+ started = false;
509
+ closed = false;
510
+ // Tracks reconnect attempts for backoff and retry-limit accounting.
511
+ reconnectAttempts = 0;
512
+ // Shared deferred completion for all callers of waitForClose(), even late ones.
513
+ donePromise;
514
+ // Set to `undefined` after resolution so finish() remains idempotent.
515
+ resolveDone;
516
+ constructor(options) {
517
+ super();
518
+ this.options = options;
519
+ this.donePromise = new Promise((resolve) => {
520
+ this.resolveDone = resolve;
169
521
  });
170
- return import_zod2.z.NEVER;
171
- }
172
- }).pipe(TailEventSchema);
173
- var AsyncQueue = class {
174
- items = [];
175
- waiters = [];
176
- settled = false;
177
- push(value) {
178
- if (this.settled) {
522
+ }
523
+ close(code = NORMAL_CLOSE_CODE, reason = "closed") {
524
+ if (this.closed) {
179
525
  return;
180
526
  }
181
- this.enqueue({ type: "value", value });
182
- }
183
- close() {
184
- if (this.settled) {
527
+ this.closed = true;
528
+ this.cancelReconnectWait?.();
529
+ this.cancelReconnectWait = void 0;
530
+ if (!this.started) {
531
+ this.finish({
532
+ closeCode: code,
533
+ closeReason: reason,
534
+ aborted: this.options.signal?.aborted ?? false,
535
+ graceful: code === NORMAL_CLOSE_CODE
536
+ });
185
537
  return;
186
538
  }
187
- this.settled = true;
188
- this.enqueue({ type: "done" });
189
- }
190
- fail(error) {
191
- if (this.settled) {
539
+ if (this.forceCloseSocket) {
540
+ this.forceCloseSocket(code, reason);
192
541
  return;
193
542
  }
194
- this.settled = true;
195
- this.enqueue({ type: "error", error: toError(error) });
543
+ this.socket?.close(code, reason);
196
544
  }
197
- async next() {
198
- const item = this.items.shift() ?? await new Promise((resolve) => {
199
- this.waiters.push(resolve);
200
- });
201
- if (item.type === "value") {
202
- return { value: item.value, done: false };
203
- }
204
- if (item.type === "done") {
205
- return { value: void 0, done: true };
206
- }
207
- throw item.error;
545
+ resetReconnectAttempts() {
546
+ this.reconnectAttempts = 0;
208
547
  }
209
- enqueue(item) {
210
- const waiter = this.waiters.shift();
211
- if (waiter) {
212
- waiter(item);
548
+ waitForClose() {
549
+ this.start();
550
+ return this.donePromise;
551
+ }
552
+ start() {
553
+ if (this.started || this.resolveDone === void 0) {
213
554
  return;
214
555
  }
215
- this.items.push(item);
216
- }
217
- };
218
- function normalizeBaseUrl(baseUrl) {
219
- const trimmed = baseUrl.trim().replace(TRAILING_SLASHES_REGEX, "");
220
- return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
221
- }
222
- function toWebSocketBaseUrl(apiBaseUrl) {
223
- if (apiBaseUrl.startsWith("https://")) {
224
- return `wss://${apiBaseUrl.slice("https://".length)}`;
225
- }
226
- if (apiBaseUrl.startsWith("http://")) {
227
- return `ws://${apiBaseUrl.slice("http://".length)}`;
556
+ this.started = true;
557
+ this.run().catch((error) => {
558
+ this.emitSafe("fatal", error);
559
+ this.finish({
560
+ closeCode: void 0,
561
+ closeReason: "run failed",
562
+ aborted: this.options.signal?.aborted ?? false,
563
+ graceful: false
564
+ });
565
+ });
228
566
  }
229
- throw new StarciteError(
230
- `Invalid Starcite base URL '${apiBaseUrl}'. Use http:// or https://.`
231
- );
232
- }
233
- function defaultWebSocketFactory(url, options = {}) {
234
- if (typeof WebSocket === "undefined") {
235
- throw new StarciteError(
236
- "WebSocket is not available in this runtime. Provide websocketFactory in StarciteClientOptions."
237
- );
567
+ async run() {
568
+ const finalState = {
569
+ closeCode: void 0,
570
+ closeReason: void 0,
571
+ aborted: this.options.signal?.aborted ?? false,
572
+ graceful: false
573
+ };
574
+ while (!this.closed) {
575
+ if (this.options.signal?.aborted) {
576
+ this.closed = true;
577
+ finalState.aborted = true;
578
+ break;
579
+ }
580
+ const attempt = this.reconnectAttempts + 1;
581
+ if (!this.emitSafe("connect_attempt", { attempt })) {
582
+ this.closed = true;
583
+ break;
584
+ }
585
+ if (this.closed) {
586
+ break;
587
+ }
588
+ let socket;
589
+ try {
590
+ const url = this.options.url();
591
+ socket = this.options.websocketFactory(url);
592
+ } catch (error) {
593
+ const shouldContinue2 = await this.handleConnectFailure(attempt, error);
594
+ if (shouldContinue2) {
595
+ continue;
596
+ }
597
+ break;
598
+ }
599
+ if (this.closed) {
600
+ closeQuietly(socket, NORMAL_CLOSE_CODE, "closed");
601
+ break;
602
+ }
603
+ const result = await this.runSocket(socket);
604
+ const shouldContinue = await this.handleSocketResult(
605
+ attempt,
606
+ result,
607
+ finalState
608
+ );
609
+ if (shouldContinue) {
610
+ continue;
611
+ }
612
+ break;
613
+ }
614
+ this.finish(finalState);
238
615
  }
239
- const headers = new Headers(options.headers);
240
- if (!hasAnyHeaders(headers)) {
241
- return new WebSocket(url);
616
+ async handleConnectFailure(attempt, error) {
617
+ const rootCause = toErrorMessage(error);
618
+ if (!this.emitSafe("connect_failed", { attempt, rootCause })) {
619
+ this.closed = true;
620
+ return false;
621
+ }
622
+ return await this.scheduleReconnect({
623
+ attempt,
624
+ trigger: "connect_failed",
625
+ rootCause
626
+ });
242
627
  }
243
- const headerObject = Object.fromEntries(headers.entries());
244
- try {
245
- return new WebSocket(url, { headers: headerObject });
246
- } catch {
247
- throw new StarciteError(
248
- "This runtime cannot set WebSocket upgrade headers with the default factory. Provide websocketFactory in StarciteClientOptions."
249
- );
628
+ async handleSocketResult(attempt, result, finalState) {
629
+ finalState.closeCode = result.closeCode;
630
+ finalState.closeReason = result.closeReason;
631
+ finalState.aborted = result.aborted || (this.options.signal?.aborted ?? false);
632
+ finalState.graceful = !result.sawTransportError && result.closeCode === NORMAL_CLOSE_CODE;
633
+ if (result.listenerError !== void 0) {
634
+ this.emitSafe("fatal", result.listenerError);
635
+ this.closed = true;
636
+ return false;
637
+ }
638
+ if (this.closed || finalState.aborted || finalState.graceful) {
639
+ return false;
640
+ }
641
+ if (!this.emitSafe("dropped", {
642
+ attempt,
643
+ closeCode: result.closeCode,
644
+ closeReason: result.closeReason
645
+ })) {
646
+ this.closed = true;
647
+ return false;
648
+ }
649
+ return await this.scheduleReconnect({
650
+ attempt,
651
+ trigger: "dropped",
652
+ closeCode: result.closeCode,
653
+ closeReason: result.closeReason
654
+ });
250
655
  }
251
- }
252
- function defaultFetch(input, init) {
253
- if (typeof fetch === "undefined") {
254
- throw new StarciteError(
255
- "fetch is not available in this runtime. Provide fetch in StarciteClientOptions."
656
+ async scheduleReconnect(input) {
657
+ if (!this.options.shouldReconnect || this.closed || this.options.signal?.aborted) {
658
+ this.closed = true;
659
+ return false;
660
+ }
661
+ if (input.attempt > this.options.reconnectPolicy.maxAttempts) {
662
+ this.emitSafe("retry_limit", {
663
+ attempt: input.attempt,
664
+ trigger: input.trigger,
665
+ closeCode: input.closeCode,
666
+ closeReason: input.closeReason,
667
+ rootCause: input.rootCause
668
+ });
669
+ this.closed = true;
670
+ return false;
671
+ }
672
+ const delayMs = reconnectDelayForAttempt(
673
+ input.attempt,
674
+ this.options.reconnectPolicy
256
675
  );
676
+ if (!this.emitSafe("reconnect_scheduled", {
677
+ attempt: input.attempt,
678
+ delayMs,
679
+ trigger: input.trigger,
680
+ closeCode: input.closeCode,
681
+ closeReason: input.closeReason,
682
+ rootCause: input.rootCause
683
+ })) {
684
+ this.closed = true;
685
+ return false;
686
+ }
687
+ this.reconnectAttempts = input.attempt;
688
+ const reconnectWait = waitForDelay(delayMs, this.options.signal);
689
+ this.cancelReconnectWait = reconnectWait.cancel;
690
+ await reconnectWait.promise;
691
+ if (this.cancelReconnectWait === reconnectWait.cancel) {
692
+ this.cancelReconnectWait = void 0;
693
+ }
694
+ return !(this.closed || (this.options.signal?.aborted ?? false));
257
695
  }
258
- return fetch(input, init);
259
- }
260
- function toError(error) {
261
- if (error instanceof Error) {
262
- return error;
696
+ runSocket(socket) {
697
+ return new Promise((resolve) => {
698
+ this.socket = socket;
699
+ let settled = false;
700
+ let socketOpen = false;
701
+ let sawTransportError = false;
702
+ let aborted = false;
703
+ let listenerError;
704
+ let closeCode;
705
+ let closeReason;
706
+ let connectionTimeout;
707
+ let inactivityTimeout;
708
+ const clearTimers = () => {
709
+ if (connectionTimeout) {
710
+ clearTimeout(connectionTimeout);
711
+ connectionTimeout = void 0;
712
+ }
713
+ if (inactivityTimeout) {
714
+ clearTimeout(inactivityTimeout);
715
+ inactivityTimeout = void 0;
716
+ }
717
+ };
718
+ const armConnectionTimeout = () => {
719
+ clearTimeout(connectionTimeout);
720
+ if (this.options.connectionTimeoutMs <= 0) {
721
+ return;
722
+ }
723
+ connectionTimeout = setTimeout(() => {
724
+ if (settled || socketOpen || this.closed) {
725
+ return;
726
+ }
727
+ closeAndSettle(CONNECTION_TIMEOUT_CLOSE_CODE, "connection timeout");
728
+ }, this.options.connectionTimeoutMs);
729
+ };
730
+ const armInactivityTimeout = () => {
731
+ clearTimeout(inactivityTimeout);
732
+ const timeoutMs = this.options.inactivityTimeoutMs;
733
+ if (!timeoutMs || timeoutMs <= 0 || this.closed) {
734
+ return;
735
+ }
736
+ inactivityTimeout = setTimeout(() => {
737
+ if (settled || this.closed) {
738
+ return;
739
+ }
740
+ closeAndSettle(INACTIVITY_CLOSE_CODE, "inactivity timeout");
741
+ }, timeoutMs);
742
+ };
743
+ const closeAndSettle = (code, reason, markAborted = false) => {
744
+ if (settled) {
745
+ return;
746
+ }
747
+ aborted = aborted || markAborted;
748
+ closeCode = code;
749
+ closeReason = reason;
750
+ try {
751
+ socket.close(code, reason);
752
+ } catch {
753
+ }
754
+ settle();
755
+ };
756
+ const settle = () => {
757
+ if (settled) {
758
+ return;
759
+ }
760
+ settled = true;
761
+ clearTimers();
762
+ socket.removeEventListener("open", onOpen);
763
+ socket.removeEventListener("message", onMessage);
764
+ socket.removeEventListener("error", onError);
765
+ socket.removeEventListener("close", onClose);
766
+ this.options.signal?.removeEventListener("abort", onAbort);
767
+ if (this.socket === socket) {
768
+ this.socket = void 0;
769
+ }
770
+ this.forceCloseSocket = void 0;
771
+ resolve({
772
+ closeCode,
773
+ closeReason,
774
+ sawTransportError,
775
+ aborted,
776
+ listenerError
777
+ });
778
+ };
779
+ const onOpen = () => {
780
+ socketOpen = true;
781
+ clearTimeout(connectionTimeout);
782
+ armInactivityTimeout();
783
+ if (!this.emitSafe("open")) {
784
+ listenerError = new Error("Managed websocket open listener failed");
785
+ closeAndSettle(NORMAL_CLOSE_CODE, "listener failed");
786
+ }
787
+ };
788
+ const onMessage = (event) => {
789
+ armInactivityTimeout();
790
+ if (!this.emitSafe("message", event.data)) {
791
+ listenerError = new Error(
792
+ "Managed websocket message listener failed"
793
+ );
794
+ closeAndSettle(NORMAL_CLOSE_CODE, "listener failed");
795
+ }
796
+ };
797
+ const onError = (_event) => {
798
+ sawTransportError = true;
799
+ };
800
+ const onClose = (event) => {
801
+ closeCode = event.code;
802
+ closeReason = event.reason;
803
+ settle();
804
+ };
805
+ const onAbort = () => {
806
+ this.closed = true;
807
+ closeAndSettle(NORMAL_CLOSE_CODE, "aborted", true);
808
+ };
809
+ this.forceCloseSocket = (code, reason, markAborted) => {
810
+ closeAndSettle(code, reason, markAborted ?? false);
811
+ };
812
+ socket.addEventListener("open", onOpen);
813
+ socket.addEventListener("message", onMessage);
814
+ socket.addEventListener("error", onError);
815
+ socket.addEventListener("close", onClose);
816
+ this.options.signal?.addEventListener("abort", onAbort, { once: true });
817
+ armConnectionTimeout();
818
+ });
263
819
  }
264
- return new Error(typeof error === "string" ? error : "Unknown error");
265
- }
266
- function hasAnyHeaders(headers) {
267
- for (const _ of headers.keys()) {
268
- return true;
820
+ emitSafe(eventName, payload) {
821
+ const emitter = this;
822
+ try {
823
+ if (payload === void 0) {
824
+ emitter.emit(eventName);
825
+ } else {
826
+ emitter.emit(eventName, payload);
827
+ }
828
+ return true;
829
+ } catch (error) {
830
+ if (eventName !== "fatal") {
831
+ emitter.emit("fatal", error);
832
+ }
833
+ return false;
834
+ }
269
835
  }
270
- return false;
271
- }
272
- function formatAuthorizationHeader(apiKey) {
273
- const normalized = apiKey.trim();
274
- if (normalized.length === 0) {
275
- throw new StarciteError("apiKey cannot be empty");
836
+ finish(event) {
837
+ if (this.resolveDone === void 0) {
838
+ return;
839
+ }
840
+ this.emit("closed", event);
841
+ this.removeAllListeners();
842
+ const resolve = this.resolveDone;
843
+ this.resolveDone = void 0;
844
+ resolve();
276
845
  }
277
- if (BEARER_PREFIX_REGEX.test(normalized)) {
278
- return normalized;
846
+ };
847
+ function reconnectDelayForAttempt(attempt, policy) {
848
+ const exponent = Math.max(0, attempt - 1);
849
+ const baseDelay = policy.initialDelayMs * policy.multiplier ** exponent;
850
+ const clamped = Math.min(policy.maxDelayMs, baseDelay);
851
+ if (policy.jitterRatio <= 0) {
852
+ return clamped;
279
853
  }
280
- return `Bearer ${normalized}`;
854
+ const spread = clamped * policy.jitterRatio;
855
+ const min = Math.max(0, clamped - spread);
856
+ const max = clamped + spread;
857
+ return min + Math.random() * (max - min);
281
858
  }
282
- function firstNonEmptyString(value) {
283
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
284
- }
285
- function parseJwtSegment(segment) {
286
- const base64 = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(segment.length + (4 - segment.length % 4) % 4, "=");
287
- try {
288
- if (typeof atob === "function") {
289
- return atob(base64);
859
+ function waitForDelay(ms, signal) {
860
+ if (ms <= 0 || signal?.aborted) {
861
+ return {
862
+ promise: Promise.resolve(),
863
+ cancel: () => void 0
864
+ };
865
+ }
866
+ let timer;
867
+ let settled = false;
868
+ let resolvePromise;
869
+ const onAbort = () => {
870
+ finish();
871
+ };
872
+ const finish = () => {
873
+ if (settled) {
874
+ return;
290
875
  }
291
- if (typeof Buffer !== "undefined") {
292
- return Buffer.from(base64, "base64").toString("utf8");
876
+ settled = true;
877
+ if (timer) {
878
+ clearTimeout(timer);
879
+ timer = void 0;
293
880
  }
294
- } catch {
295
- return void 0;
296
- }
297
- return void 0;
881
+ signal?.removeEventListener("abort", onAbort);
882
+ resolvePromise?.();
883
+ resolvePromise = void 0;
884
+ };
885
+ const promise = new Promise((resolve) => {
886
+ resolvePromise = resolve;
887
+ timer = setTimeout(() => {
888
+ finish();
889
+ }, ms);
890
+ signal?.addEventListener("abort", onAbort, { once: true });
891
+ });
892
+ return {
893
+ promise,
894
+ cancel: finish
895
+ };
298
896
  }
299
- function parseJwtClaims(apiKey) {
300
- const token = apiKey.replace(BEARER_PREFIX_REGEX, "").trim();
301
- const parts = token.split(".");
302
- if (parts.length !== 3) {
303
- return void 0;
304
- }
305
- const [, payloadSegment] = parts;
306
- if (payloadSegment === void 0) {
307
- return void 0;
308
- }
309
- const payload = parseJwtSegment(payloadSegment);
310
- if (payload === void 0) {
311
- return void 0;
312
- }
897
+ function closeQuietly(socket, code, reason) {
313
898
  try {
314
- const decoded = JSON.parse(payload);
315
- return decoded !== null && typeof decoded === "object" ? decoded : void 0;
899
+ socket.close(code, reason);
316
900
  } catch {
317
- return void 0;
318
901
  }
319
902
  }
320
- function parseClaimStrings(source, keys) {
321
- for (const key of keys) {
322
- const value = firstNonEmptyString(source[key]);
323
- if (value) {
324
- return value;
325
- }
326
- }
327
- return void 0;
903
+ function toErrorMessage(error) {
904
+ return error instanceof Error ? error.message : String(error);
328
905
  }
329
- function parseActorIdentityFromSubject(subject) {
330
- if (subject.startsWith(SERVICE_TOKEN_SUB_AGENT_PREFIX)) {
331
- return { id: subject, type: "agent" };
332
- }
333
- if (subject.startsWith(SERVICE_TOKEN_SUB_USER_PREFIX)) {
334
- return { id: subject, type: "user" };
335
- }
336
- return void 0;
337
- }
338
- function parseTenantIdFromSubject(subject) {
339
- const actorIdentity = parseActorIdentityFromSubject(subject);
340
- if (actorIdentity !== void 0) {
341
- return "";
342
- }
343
- if (subject.startsWith(SERVICE_TOKEN_SUB_ORG_PREFIX)) {
344
- return subject.slice(SERVICE_TOKEN_SUB_ORG_PREFIX.length).trim();
345
- }
346
- return subject;
347
- }
348
- function parseCreatorPrincipalFromClaims(claims) {
349
- const subject = firstNonEmptyString(claims.sub);
350
- const explicitPrincipal = claims.principal && typeof claims.principal === "object" ? claims.principal : void 0;
351
- const mergedClaims = explicitPrincipal ? { ...claims, ...explicitPrincipal } : claims;
352
- const actorFromSubject = subject ? parseActorIdentityFromSubject(subject) : void 0;
353
- const principalTypeFromClaims = parseClaimStrings(mergedClaims, [
354
- "principal_type",
355
- "principalType",
356
- "type"
357
- ]);
358
- const tenantId = parseClaimStrings(mergedClaims, ["tenant_id", "tenantId"]);
359
- const rawPrincipalId = parseClaimStrings(mergedClaims, [
360
- "principal_id",
361
- "principalId",
362
- "id",
363
- "sub"
364
- ]);
365
- const actorFromRawId = rawPrincipalId ? parseActorIdentityFromSubject(rawPrincipalId) : void 0;
366
- const principal = {
367
- tenant_id: tenantId ?? (subject ? parseTenantIdFromSubject(subject) : ""),
368
- id: rawPrincipalId ?? actorFromSubject?.id ?? "",
369
- type: principalTypeFromClaims === "agent" || principalTypeFromClaims === "user" ? principalTypeFromClaims : actorFromSubject?.type ?? actorFromRawId?.type ?? "user"
370
- };
371
- if (principal.tenant_id.length === 0 || principal.id.length === 0 || principal.type.length === 0) {
372
- return void 0;
373
- }
374
- const result = SessionCreatorPrincipalSchema.safeParse(principal);
375
- return result.success ? result.data : void 0;
376
- }
377
- function parseCreatorPrincipalFromClaimsSafe(apiKey) {
378
- const claims = parseJwtClaims(apiKey);
379
- return claims ? parseCreatorPrincipalFromClaims(claims) : void 0;
380
- }
381
- function parseEventFrame(data) {
382
- const result = TailFrameSchema.safeParse(data);
383
- if (!result.success) {
384
- const reason = result.error.issues[0]?.message ?? "Tail frame did not match schema";
385
- throw new StarciteConnectionError(reason);
386
- }
387
- return result.data;
388
- }
389
- function getEventData(event) {
390
- if (event && typeof event === "object" && "data" in event) {
391
- return event.data;
392
- }
393
- return void 0;
394
- }
395
- function getCloseCode(event) {
396
- if (event && typeof event === "object" && "code" in event) {
397
- const code = event.code;
398
- return typeof code === "number" ? code : void 0;
906
+
907
+ // src/tail/stream.ts
908
+ var NORMAL_CLOSE_CODE2 = 1e3;
909
+ var TailStream = class {
910
+ sessionId;
911
+ token;
912
+ websocketBaseUrl;
913
+ websocketFactory;
914
+ batchSize;
915
+ agent;
916
+ follow;
917
+ shouldReconnect;
918
+ reconnectPolicy;
919
+ catchUpIdleMs;
920
+ connectionTimeoutMs;
921
+ inactivityTimeoutMs;
922
+ maxBufferedBatches;
923
+ signal;
924
+ onLifecycleEvent;
925
+ cursor;
926
+ constructor(input) {
927
+ const opts = input.options;
928
+ const follow = opts.follow ?? true;
929
+ const policy = opts.reconnectPolicy;
930
+ const mode = policy?.mode === "fixed" ? "fixed" : "exponential";
931
+ const initialDelayMs = policy?.initialDelayMs ?? 500;
932
+ this.sessionId = input.sessionId;
933
+ this.token = input.token;
934
+ this.websocketBaseUrl = input.websocketBaseUrl;
935
+ this.websocketFactory = input.websocketFactory;
936
+ this.cursor = opts.cursor ?? 0;
937
+ this.batchSize = opts.batchSize;
938
+ this.agent = opts.agent?.trim();
939
+ this.follow = follow;
940
+ this.shouldReconnect = follow ? opts.reconnect ?? true : false;
941
+ this.catchUpIdleMs = opts.catchUpIdleMs ?? 1e3;
942
+ this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 4e3;
943
+ this.inactivityTimeoutMs = opts.inactivityTimeoutMs;
944
+ this.maxBufferedBatches = opts.maxBufferedBatches ?? 1024;
945
+ this.signal = opts.signal;
946
+ this.onLifecycleEvent = opts.onLifecycleEvent;
947
+ this.reconnectPolicy = {
948
+ mode,
949
+ initialDelayMs,
950
+ maxDelayMs: policy?.maxDelayMs ?? (mode === "fixed" ? initialDelayMs : Math.max(initialDelayMs, 15e3)),
951
+ multiplier: mode === "fixed" ? 1 : policy?.multiplier ?? 2,
952
+ jitterRatio: policy?.jitterRatio ?? 0.2,
953
+ maxAttempts: policy?.maxAttempts ?? Number.POSITIVE_INFINITY
954
+ };
399
955
  }
400
- return void 0;
401
- }
402
- function getCloseReason(event) {
403
- if (event && typeof event === "object" && "reason" in event) {
404
- const reason = event.reason;
405
- return typeof reason === "string" && reason.length > 0 ? reason : void 0;
956
+ /**
957
+ * Pushes batches to a callback, enabling emitter-style consumers.
958
+ */
959
+ async subscribe(onBatch) {
960
+ await this.subscribeWithSignal(onBatch, this.signal);
406
961
  }
407
- return void 0;
408
- }
409
- function describeClose(code, reason) {
410
- const codeText = `code ${typeof code === "number" ? code : "unknown"}`;
411
- return reason ? `${codeText}, reason '${reason}'` : codeText;
412
- }
413
- async function waitForDelay(ms, signal) {
414
- if (ms <= 0) {
415
- return;
416
- }
417
- await new Promise((resolve) => {
418
- let settled = false;
419
- const timeout = setTimeout(() => {
420
- if (settled) {
962
+ async subscribeWithSignal(onBatch, signal) {
963
+ let streamError;
964
+ let streamReason = this.follow ? "graceful" : "caught_up";
965
+ let queuedBatches = 0;
966
+ let catchUpTimer;
967
+ let dispatchChain = Promise.resolve();
968
+ const stream = new ManagedWebSocket({
969
+ // URL is re-read per attempt by `ManagedWebSocket` and reflects current cursor.
970
+ url: () => this.buildTailUrl(),
971
+ websocketFactory: this.websocketFactory,
972
+ signal,
973
+ shouldReconnect: this.shouldReconnect,
974
+ reconnectPolicy: {
975
+ initialDelayMs: this.reconnectPolicy.initialDelayMs,
976
+ maxDelayMs: this.reconnectPolicy.maxDelayMs,
977
+ multiplier: this.reconnectPolicy.multiplier,
978
+ jitterRatio: this.reconnectPolicy.jitterRatio,
979
+ maxAttempts: this.reconnectPolicy.maxAttempts
980
+ },
981
+ connectionTimeoutMs: this.connectionTimeoutMs,
982
+ inactivityTimeoutMs: this.inactivityTimeoutMs
983
+ });
984
+ const fail = (error) => {
985
+ if (streamError !== void 0) {
421
986
  return;
422
987
  }
423
- settled = true;
424
- if (signal) {
425
- signal.removeEventListener("abort", onAbort);
988
+ streamError = error;
989
+ stream.close(NORMAL_CLOSE_CODE2, "stream failed");
990
+ };
991
+ const emitLifecycle = (event) => {
992
+ if (!this.onLifecycleEvent) {
993
+ return;
426
994
  }
427
- resolve();
428
- }, ms);
429
- const onAbort = () => {
430
- if (settled) {
995
+ try {
996
+ this.onLifecycleEvent(event);
997
+ } catch (error) {
998
+ fail(error);
999
+ }
1000
+ };
1001
+ const clearCatchUpTimer = () => {
1002
+ if (catchUpTimer) {
1003
+ clearTimeout(catchUpTimer);
1004
+ catchUpTimer = void 0;
1005
+ }
1006
+ };
1007
+ const scheduleCatchUpClose = () => {
1008
+ if (this.follow) {
1009
+ return;
1010
+ }
1011
+ clearCatchUpTimer();
1012
+ catchUpTimer = setTimeout(() => {
1013
+ streamReason = "caught_up";
1014
+ stream.close(NORMAL_CLOSE_CODE2, "caught up");
1015
+ }, this.catchUpIdleMs);
1016
+ };
1017
+ const dispatchBatch = (batch) => {
1018
+ if (streamError !== void 0) {
1019
+ return;
1020
+ }
1021
+ if (this.maxBufferedBatches > 0 && queuedBatches > this.maxBufferedBatches) {
1022
+ fail(
1023
+ new StarciteBackpressureError(
1024
+ `Tail consumer for session '${this.sessionId}' fell behind after buffering ${this.maxBufferedBatches} batch(es)`,
1025
+ { sessionId: this.sessionId, attempts: 0 }
1026
+ )
1027
+ );
431
1028
  return;
432
1029
  }
433
- settled = true;
434
- clearTimeout(timeout);
435
- signal?.removeEventListener("abort", onAbort);
436
- resolve();
1030
+ queuedBatches += 1;
1031
+ dispatchChain = dispatchChain.then(async () => {
1032
+ try {
1033
+ await onBatch(batch);
1034
+ } finally {
1035
+ queuedBatches -= 1;
1036
+ }
1037
+ }).catch((error) => {
1038
+ fail(error);
1039
+ });
1040
+ };
1041
+ const onConnectAttempt = (event) => {
1042
+ emitLifecycle({
1043
+ type: "connect_attempt",
1044
+ sessionId: this.sessionId,
1045
+ attempt: event.attempt,
1046
+ cursor: this.cursor
1047
+ });
1048
+ };
1049
+ const onConnectFailed = (event) => {
1050
+ if (!this.shouldReconnect) {
1051
+ fail(
1052
+ new StarciteTailError(
1053
+ `Tail connection failed for session '${this.sessionId}': ${event.rootCause}`,
1054
+ {
1055
+ sessionId: this.sessionId,
1056
+ stage: "connect",
1057
+ attempts: event.attempt - 1
1058
+ }
1059
+ )
1060
+ );
1061
+ }
1062
+ };
1063
+ const onReconnectScheduled = (event) => {
1064
+ emitLifecycle({
1065
+ type: "reconnect_scheduled",
1066
+ sessionId: this.sessionId,
1067
+ attempt: event.attempt,
1068
+ delayMs: event.delayMs,
1069
+ trigger: event.trigger,
1070
+ closeCode: event.closeCode,
1071
+ closeReason: event.closeReason
1072
+ });
1073
+ };
1074
+ const onDropped = (event) => {
1075
+ if (event.closeCode === 4001 || event.closeReason === "token_expired") {
1076
+ fail(
1077
+ new StarciteTokenExpiredError(
1078
+ `Tail token expired for session '${this.sessionId}'. Re-issue a session token and reconnect from the last processed cursor.`,
1079
+ {
1080
+ sessionId: this.sessionId,
1081
+ attempts: event.attempt,
1082
+ closeCode: event.closeCode,
1083
+ closeReason: event.closeReason
1084
+ }
1085
+ )
1086
+ );
1087
+ return;
1088
+ }
1089
+ emitLifecycle({
1090
+ type: "stream_dropped",
1091
+ sessionId: this.sessionId,
1092
+ attempt: event.attempt,
1093
+ closeCode: event.closeCode,
1094
+ closeReason: event.closeReason
1095
+ });
1096
+ if (!this.shouldReconnect) {
1097
+ fail(
1098
+ new StarciteTailError(
1099
+ `Tail connection dropped for session '${this.sessionId}' (${describeClose(event.closeCode, event.closeReason)})`,
1100
+ {
1101
+ sessionId: this.sessionId,
1102
+ stage: "stream",
1103
+ attempts: event.attempt - 1,
1104
+ closeCode: event.closeCode,
1105
+ closeReason: event.closeReason
1106
+ }
1107
+ )
1108
+ );
1109
+ }
1110
+ };
1111
+ const onRetryLimit = (event) => {
1112
+ const message = event.trigger === "connect_failed" ? `Tail connection failed for session '${this.sessionId}' after ${this.reconnectPolicy.maxAttempts} reconnect attempt(s): ${event.rootCause ?? "Unknown error"}` : `Tail connection dropped for session '${this.sessionId}' after ${this.reconnectPolicy.maxAttempts} reconnect attempt(s) (${describeClose(event.closeCode, event.closeReason)})`;
1113
+ fail(
1114
+ new StarciteRetryLimitError(message, {
1115
+ sessionId: this.sessionId,
1116
+ attempts: event.attempt,
1117
+ closeCode: event.closeCode,
1118
+ closeReason: event.closeReason
1119
+ })
1120
+ );
1121
+ };
1122
+ const onOpen = () => {
1123
+ scheduleCatchUpClose();
1124
+ };
1125
+ const onMessage = (data) => {
1126
+ try {
1127
+ const parsedEvents = parseTailFrame(data);
1128
+ const matchingEvents = [];
1129
+ for (const parsedEvent of parsedEvents) {
1130
+ this.cursor = Math.max(this.cursor, parsedEvent.seq);
1131
+ if (this.agent && agentFromActor(parsedEvent.actor) !== this.agent) {
1132
+ continue;
1133
+ }
1134
+ matchingEvents.push(parsedEvent);
1135
+ }
1136
+ if (matchingEvents.length > 0) {
1137
+ stream.resetReconnectAttempts();
1138
+ dispatchBatch(matchingEvents);
1139
+ }
1140
+ scheduleCatchUpClose();
1141
+ } catch (error) {
1142
+ fail(error);
1143
+ }
437
1144
  };
438
- if (signal) {
439
- if (signal.aborted) {
440
- onAbort();
1145
+ const onFatal = (error) => {
1146
+ fail(error);
1147
+ };
1148
+ const onClosed = (event) => {
1149
+ clearCatchUpTimer();
1150
+ if (event.aborted) {
1151
+ streamReason = "aborted";
1152
+ return;
1153
+ }
1154
+ if (!this.follow) {
1155
+ streamReason = "caught_up";
441
1156
  return;
442
1157
  }
443
- signal.addEventListener("abort", onAbort, { once: true });
1158
+ if (event.graceful) {
1159
+ streamReason = "graceful";
1160
+ }
1161
+ };
1162
+ stream.on("connect_attempt", onConnectAttempt);
1163
+ stream.on("connect_failed", onConnectFailed);
1164
+ stream.on("reconnect_scheduled", onReconnectScheduled);
1165
+ stream.on("dropped", onDropped);
1166
+ stream.on("retry_limit", onRetryLimit);
1167
+ stream.on("open", onOpen);
1168
+ stream.on("message", onMessage);
1169
+ stream.on("fatal", onFatal);
1170
+ stream.on("closed", onClosed);
1171
+ try {
1172
+ await stream.waitForClose();
1173
+ clearCatchUpTimer();
1174
+ await dispatchChain;
1175
+ if (streamError !== void 0) {
1176
+ throw streamError;
1177
+ }
1178
+ emitLifecycle({
1179
+ type: "stream_ended",
1180
+ sessionId: this.sessionId,
1181
+ reason: streamReason
1182
+ });
1183
+ if (streamError !== void 0) {
1184
+ throw streamError;
1185
+ }
1186
+ } finally {
1187
+ clearCatchUpTimer();
1188
+ stream.off("connect_attempt", onConnectAttempt);
1189
+ stream.off("connect_failed", onConnectFailed);
1190
+ stream.off("reconnect_scheduled", onReconnectScheduled);
1191
+ stream.off("dropped", onDropped);
1192
+ stream.off("retry_limit", onRetryLimit);
1193
+ stream.off("open", onOpen);
1194
+ stream.off("message", onMessage);
1195
+ stream.off("fatal", onFatal);
1196
+ stream.off("closed", onClosed);
1197
+ stream.close(NORMAL_CLOSE_CODE2, "finished");
444
1198
  }
445
- });
1199
+ }
1200
+ buildTailUrl() {
1201
+ const query = new URLSearchParams({ cursor: `${this.cursor}` });
1202
+ if (this.batchSize !== void 0) {
1203
+ query.set("batch_size", `${this.batchSize}`);
1204
+ }
1205
+ if (this.token) {
1206
+ query.set("access_token", this.token);
1207
+ }
1208
+ return `${this.websocketBaseUrl}/sessions/${encodeURIComponent(
1209
+ this.sessionId
1210
+ )}/tail?${query.toString()}`;
1211
+ }
1212
+ };
1213
+ function describeClose(code, reason) {
1214
+ const codeText = `code ${code ?? "unknown"}`;
1215
+ return reason ? `${codeText}, reason '${reason}'` : codeText;
446
1216
  }
447
- function agentFromActor(actor) {
448
- if (actor.startsWith("agent:")) {
449
- return actor.slice("agent:".length);
1217
+
1218
+ // src/transport.ts
1219
+ var TRAILING_SLASHES_REGEX = /\/+$/;
1220
+ function normalizeAbsoluteHttpUrl(value, context) {
1221
+ const parsed = new URL(value);
1222
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1223
+ throw new StarciteError(`${context} must use http:// or https://`);
450
1224
  }
451
- return void 0;
1225
+ return parsed.toString().replace(TRAILING_SLASHES_REGEX, "");
452
1226
  }
453
- function toSessionEvent(event) {
454
- const agent = agentFromActor(event.actor);
455
- const text = typeof event.payload.text === "string" ? event.payload.text : void 0;
456
- return {
457
- ...event,
458
- agent,
459
- text
460
- };
1227
+ function toApiBaseUrl(baseUrl) {
1228
+ const normalized = normalizeAbsoluteHttpUrl(baseUrl, "baseUrl");
1229
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
1230
+ }
1231
+ function toWebSocketBaseUrl(apiBaseUrl) {
1232
+ const url = new URL(apiBaseUrl);
1233
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1234
+ return url.toString().replace(TRAILING_SLASHES_REGEX, "");
1235
+ }
1236
+ function defaultWebSocketFactory(url) {
1237
+ if (typeof WebSocket === "undefined") {
1238
+ throw new StarciteError(
1239
+ "WebSocket is not available in this runtime. Provide websocketFactory in StarciteOptions."
1240
+ );
1241
+ }
1242
+ return new WebSocket(url);
1243
+ }
1244
+ function request(transport, path, init, schema) {
1245
+ return requestWithBaseUrl(transport, transport.baseUrl, path, init, schema);
461
1246
  }
1247
+ async function requestWithBaseUrl(transport, baseUrl, path, init, schema) {
1248
+ const headers = new Headers(transport.headers);
1249
+ if (transport.authorization) {
1250
+ headers.set("authorization", transport.authorization);
1251
+ }
1252
+ if (init.body !== void 0 && !headers.has("content-type")) {
1253
+ headers.set("content-type", "application/json");
1254
+ }
1255
+ if (init.headers) {
1256
+ const perRequestHeaders = new Headers(init.headers);
1257
+ for (const [key, value] of perRequestHeaders.entries()) {
1258
+ headers.set(key, value);
1259
+ }
1260
+ }
1261
+ let response;
1262
+ try {
1263
+ response = await transport.fetchFn(`${baseUrl}${path}`, {
1264
+ ...init,
1265
+ headers
1266
+ });
1267
+ } catch (error) {
1268
+ throw new StarciteConnectionError(
1269
+ `Failed to connect to Starcite at ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`
1270
+ );
1271
+ }
1272
+ if (!response.ok) {
1273
+ let payload = null;
1274
+ try {
1275
+ payload = await response.json();
1276
+ } catch {
1277
+ }
1278
+ const code = typeof payload?.error === "string" ? payload.error : `http_${response.status}`;
1279
+ const message = typeof payload?.message === "string" ? payload.message : response.statusText;
1280
+ throw new StarciteApiError(message, response.status, code, payload);
1281
+ }
1282
+ if (response.status === 204) {
1283
+ return schema.parse(void 0);
1284
+ }
1285
+ let body;
1286
+ try {
1287
+ body = await response.json();
1288
+ } catch (error) {
1289
+ throw new StarciteConnectionError(
1290
+ `Received invalid JSON from Starcite: ${error instanceof Error ? error.message : String(error)}`
1291
+ );
1292
+ }
1293
+ return schema.parse(body);
1294
+ }
1295
+
1296
+ // src/session.ts
462
1297
  var StarciteSession = class {
463
1298
  /** Session identifier. */
464
1299
  id;
1300
+ /** The session JWT used for auth. Extract this for frontend handoff. */
1301
+ token;
1302
+ /** Identity bound to this session. */
1303
+ identity;
465
1304
  /** Optional session record captured at creation time. */
466
1305
  record;
467
- client;
468
- constructor(client, id, record) {
469
- this.client = client;
470
- this.id = id;
471
- this.record = record;
1306
+ transport;
1307
+ producerId;
1308
+ producerSeq = 0;
1309
+ log;
1310
+ store;
1311
+ lifecycle = new import_eventemitter33.default();
1312
+ eventSubscriptions = /* @__PURE__ */ new Map();
1313
+ liveSyncController;
1314
+ liveSyncTask;
1315
+ constructor(options) {
1316
+ this.id = options.id;
1317
+ this.token = options.token;
1318
+ this.identity = options.identity;
1319
+ this.transport = options.transport;
1320
+ this.record = options.record;
1321
+ this.producerId = crypto.randomUUID();
1322
+ this.store = options.store;
1323
+ this.log = new SessionLog(options.logOptions);
1324
+ const storedState = this.store.load(this.id);
1325
+ if (storedState) {
1326
+ this.log.hydrate(storedState);
1327
+ }
472
1328
  }
473
1329
  /**
474
- * Appends a high-level agent event to this session.
1330
+ * Appends an event to this session.
475
1331
  *
476
- * Automatically prefixes `agent` as `agent:<name>` when needed.
1332
+ * The SDK manages `actor`, `producer_id`, and `producer_seq` automatically.
477
1333
  */
478
- append(input) {
1334
+ async append(input, options) {
479
1335
  const parsed = SessionAppendInputSchema.parse(input);
480
- const actor = parsed.agent.startsWith("agent:") ? parsed.agent : `agent:${parsed.agent}`;
481
- return this.client.appendEvent(this.id, {
482
- type: parsed.type ?? "content",
483
- payload: parsed.payload ?? { text: parsed.text },
484
- actor,
485
- producer_id: parsed.producerId,
486
- producer_seq: parsed.producerSeq,
487
- source: parsed.source ?? "agent",
488
- metadata: parsed.metadata,
489
- refs: parsed.refs,
490
- idempotency_key: parsed.idempotencyKey,
491
- expected_seq: parsed.expectedSeq
492
- });
1336
+ this.producerSeq += 1;
1337
+ const result = await this.appendRaw(
1338
+ {
1339
+ type: parsed.type ?? "content",
1340
+ payload: parsed.payload ?? { text: parsed.text },
1341
+ actor: parsed.actor ?? this.identity.toActor(),
1342
+ producer_id: this.producerId,
1343
+ producer_seq: this.producerSeq,
1344
+ source: parsed.source ?? "agent",
1345
+ metadata: parsed.metadata,
1346
+ refs: parsed.refs,
1347
+ idempotency_key: parsed.idempotencyKey,
1348
+ expected_seq: parsed.expectedSeq
1349
+ },
1350
+ options
1351
+ );
1352
+ return {
1353
+ seq: result.seq,
1354
+ deduped: result.deduped
1355
+ };
493
1356
  }
494
1357
  /**
495
- * Appends a raw event payload as-is.
1358
+ * Appends a raw event payload as-is. Caller manages all fields.
496
1359
  */
497
- appendRaw(input) {
498
- return this.client.appendEvent(this.id, input);
1360
+ appendRaw(input, options) {
1361
+ return request(
1362
+ this.transport,
1363
+ `/sessions/${encodeURIComponent(this.id)}/append`,
1364
+ {
1365
+ method: "POST",
1366
+ body: JSON.stringify(input),
1367
+ signal: options?.signal
1368
+ },
1369
+ AppendEventResponseSchema
1370
+ );
1371
+ }
1372
+ on(eventName, listener) {
1373
+ if (eventName === "event") {
1374
+ const eventListener = listener;
1375
+ if (!this.eventSubscriptions.has(eventListener)) {
1376
+ const unsubscribe = this.log.subscribe(eventListener, { replay: true });
1377
+ this.eventSubscriptions.set(eventListener, unsubscribe);
1378
+ }
1379
+ this.ensureLiveSync();
1380
+ return () => {
1381
+ this.off("event", eventListener);
1382
+ };
1383
+ }
1384
+ if (eventName === "error") {
1385
+ const errorListener = listener;
1386
+ this.lifecycle.on("error", errorListener);
1387
+ return () => {
1388
+ this.off("error", errorListener);
1389
+ };
1390
+ }
1391
+ throw new StarciteError(`Unsupported event name '${eventName}'`);
1392
+ }
1393
+ off(eventName, listener) {
1394
+ if (eventName === "event") {
1395
+ const eventListener = listener;
1396
+ const unsubscribe = this.eventSubscriptions.get(eventListener);
1397
+ if (!unsubscribe) {
1398
+ return;
1399
+ }
1400
+ this.eventSubscriptions.delete(eventListener);
1401
+ unsubscribe();
1402
+ if (this.eventSubscriptions.size === 0) {
1403
+ this.liveSyncController?.abort();
1404
+ }
1405
+ return;
1406
+ }
1407
+ if (eventName === "error") {
1408
+ this.lifecycle.off("error", listener);
1409
+ return;
1410
+ }
1411
+ throw new StarciteError(`Unsupported event name '${eventName}'`);
499
1412
  }
500
1413
  /**
501
- * Streams transformed session events with SDK convenience fields (`agent`, `text`).
1414
+ * Stops live syncing and removes listeners registered via `on()`.
502
1415
  */
503
- tail(options = {}) {
504
- return this.client.tailEvents(this.id, options);
1416
+ disconnect() {
1417
+ this.liveSyncController?.abort();
1418
+ for (const unsubscribe of this.eventSubscriptions.values()) {
1419
+ unsubscribe();
1420
+ }
1421
+ this.eventSubscriptions.clear();
1422
+ this.lifecycle.removeAllListeners();
505
1423
  }
506
1424
  /**
507
- * Streams raw tail events returned by the API.
1425
+ * Backwards-compatible alias for `disconnect()`.
508
1426
  */
509
- tailRaw(options = {}) {
510
- return this.client.tailRawEvents(this.id, options);
1427
+ close() {
1428
+ this.disconnect();
511
1429
  }
512
- };
513
- var StarciteClient = class {
514
- /** Normalized API base URL ending with `/v1`. */
515
- baseUrl;
516
- inferredCreatorPrincipal;
517
- websocketBaseUrl;
518
- fetchFn;
519
- headers;
520
- websocketFactory;
521
1430
  /**
522
- * Creates a new client instance.
1431
+ * Updates in-memory session log retention.
523
1432
  */
524
- constructor(options = {}) {
525
- this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
526
- this.websocketBaseUrl = toWebSocketBaseUrl(this.baseUrl);
527
- this.fetchFn = options.fetch ?? defaultFetch;
528
- this.headers = new Headers(options.headers);
529
- if (options.apiKey !== void 0) {
530
- const authorization = formatAuthorizationHeader(options.apiKey);
531
- this.headers.set("authorization", authorization);
532
- this.inferredCreatorPrincipal = parseCreatorPrincipalFromClaimsSafe(authorization);
533
- }
534
- this.websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
1433
+ setLogOptions(options) {
1434
+ this.log.setMaxEvents(options.maxEvents);
1435
+ this.persistLogState();
535
1436
  }
536
1437
  /**
537
- * Returns a session helper bound to an existing session id.
1438
+ * Returns a stable snapshot of the current canonical in-memory log.
538
1439
  */
539
- session(sessionId, record) {
540
- return new StarciteSession(this, sessionId, record);
1440
+ getSnapshot() {
1441
+ return this.log.getSnapshot(this.liveSyncTask !== void 0);
541
1442
  }
542
1443
  /**
543
- * Creates a new session and returns a bound `StarciteSession` helper.
1444
+ * Streams tail events one at a time via callback.
544
1445
  */
545
- async create(input = {}) {
546
- const record = await this.createSession(input);
547
- return this.session(record.id, record);
1446
+ async tail(onEvent, options = {}) {
1447
+ await this.tailBatches(async (batch) => {
1448
+ for (const event of batch) {
1449
+ await onEvent(event);
1450
+ }
1451
+ }, options);
548
1452
  }
549
1453
  /**
550
- * Creates a new session and returns the raw session record.
1454
+ * Streams tail event batches grouped by incoming frame via callback.
551
1455
  */
552
- createSession(input = {}) {
553
- const payload = CreateSessionInputSchema.parse({
554
- ...input,
555
- creator_principal: input.creator_principal ?? this.inferredCreatorPrincipal
556
- });
557
- return this.request(
558
- "/sessions",
559
- {
560
- method: "POST",
561
- body: JSON.stringify(payload)
562
- },
563
- SessionRecordSchema
564
- );
1456
+ async tailBatches(onBatch, options = {}) {
1457
+ await new TailStream({
1458
+ sessionId: this.id,
1459
+ token: this.token,
1460
+ websocketBaseUrl: this.transport.websocketBaseUrl,
1461
+ websocketFactory: this.transport.websocketFactory,
1462
+ options
1463
+ }).subscribe(onBatch);
565
1464
  }
566
1465
  /**
567
- * Lists sessions from the archive-backed catalog.
1466
+ * Durably consumes events and checkpoints `event.seq` after each successful handler invocation.
568
1467
  */
569
- listSessions(options = {}) {
570
- const query = new URLSearchParams();
571
- if (options.limit !== void 0) {
572
- if (!Number.isInteger(options.limit) || options.limit <= 0) {
1468
+ async consume(options) {
1469
+ const {
1470
+ cursorStore,
1471
+ handler,
1472
+ cursor: requestedCursor,
1473
+ ...tailOptions
1474
+ } = options;
1475
+ let cursor;
1476
+ if (requestedCursor !== void 0) {
1477
+ cursor = requestedCursor;
1478
+ } else {
1479
+ try {
1480
+ cursor = await cursorStore.load(this.id) ?? 0;
1481
+ } catch (error) {
573
1482
  throw new StarciteError(
574
- "listSessions() limit must be a positive integer"
1483
+ `consume() failed to load cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
575
1484
  );
576
1485
  }
577
- query.set("limit", `${options.limit}`);
578
1486
  }
579
- if (options.cursor !== void 0) {
580
- if (options.cursor.trim().length === 0) {
581
- throw new StarciteError("listSessions() cursor cannot be empty");
1487
+ const stream = new TailStream({
1488
+ sessionId: this.id,
1489
+ token: this.token,
1490
+ websocketBaseUrl: this.transport.websocketBaseUrl,
1491
+ websocketFactory: this.transport.websocketFactory,
1492
+ options: {
1493
+ ...tailOptions,
1494
+ cursor
582
1495
  }
583
- query.set("cursor", options.cursor);
584
- }
585
- if (options.metadata !== void 0) {
586
- for (const [key, value] of Object.entries(options.metadata)) {
587
- if (key.trim().length === 0 || value.trim().length === 0) {
1496
+ });
1497
+ await stream.subscribe(async (batch) => {
1498
+ for (const event of batch) {
1499
+ await handler(event);
1500
+ try {
1501
+ await cursorStore.save(this.id, event.seq);
1502
+ } catch (error) {
588
1503
  throw new StarciteError(
589
- "listSessions() metadata filters must be non-empty strings"
1504
+ `consume() failed to save cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
590
1505
  );
591
1506
  }
592
- query.set(`metadata.${key}`, value);
593
1507
  }
594
- }
595
- const suffix = query.size > 0 ? `?${query.toString()}` : "";
596
- return this.request(
597
- `/sessions${suffix}`,
598
- {
599
- method: "GET"
600
- },
601
- SessionListPageSchema
602
- );
1508
+ });
603
1509
  }
604
- /**
605
- * Appends a raw event payload to a specific session.
606
- */
607
- appendEvent(sessionId, input) {
608
- const payload = AppendEventRequestSchema.parse(input);
609
- return this.request(
610
- `/sessions/${encodeURIComponent(sessionId)}/append`,
611
- {
612
- method: "POST",
613
- body: JSON.stringify(payload)
614
- },
615
- AppendEventResponseSchema
616
- );
1510
+ emitStreamError(error) {
1511
+ const streamError = error instanceof Error ? error : new StarciteError(`Session stream failed: ${String(error)}`);
1512
+ if (this.lifecycle.listenerCount("error") > 0) {
1513
+ this.lifecycle.emit("error", streamError);
1514
+ return;
1515
+ }
1516
+ queueMicrotask(() => {
1517
+ throw streamError;
1518
+ });
617
1519
  }
618
- /**
619
- * Opens a WebSocket tail stream and yields raw events.
620
- */
621
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: single-loop reconnect state machine is intentionally explicit for stream correctness.
622
- async *tailRawEvents(sessionId, options = {}) {
623
- const initialCursor = options.cursor ?? 0;
624
- const reconnectEnabled = options.reconnect ?? true;
625
- const reconnectDelayMs = options.reconnectDelayMs ?? DEFAULT_TAIL_RECONNECT_DELAY_MS;
626
- if (!Number.isInteger(initialCursor) || initialCursor < 0) {
627
- throw new StarciteError("tail() cursor must be a non-negative integer");
628
- }
629
- if (!Number.isFinite(reconnectDelayMs) || reconnectDelayMs < 0) {
630
- throw new StarciteError(
631
- "tail() reconnectDelayMs must be a non-negative number"
632
- );
1520
+ ensureLiveSync() {
1521
+ if (this.liveSyncTask || this.eventSubscriptions.size === 0) {
1522
+ return;
633
1523
  }
634
- let cursor = initialCursor;
635
- while (true) {
636
- if (options.signal?.aborted) {
637
- return;
638
- }
639
- const wsUrl = `${this.websocketBaseUrl}/sessions/${encodeURIComponent(
640
- sessionId
641
- )}/tail?cursor=${cursor}`;
642
- const websocketHeaders = new Headers();
643
- const authorization = this.headers.get("authorization");
644
- if (authorization) {
645
- websocketHeaders.set("authorization", authorization);
1524
+ const controller = new AbortController();
1525
+ this.liveSyncController = controller;
1526
+ this.liveSyncTask = this.runLiveSync(controller.signal).catch((error) => {
1527
+ if (!controller.signal.aborted) {
1528
+ this.emitStreamError(error);
646
1529
  }
647
- let socket;
648
- try {
649
- socket = this.websocketFactory(
650
- wsUrl,
651
- hasAnyHeaders(websocketHeaders) ? {
652
- headers: websocketHeaders
653
- } : void 0
654
- );
655
- } catch (error) {
656
- const rootCause = toError(error).message;
657
- if (!reconnectEnabled || options.signal?.aborted) {
658
- throw new StarciteConnectionError(
659
- `Tail connection failed for session '${sessionId}': ${rootCause}`
660
- );
661
- }
662
- await waitForDelay(reconnectDelayMs, options.signal);
663
- continue;
664
- }
665
- const queue = new AsyncQueue();
666
- let sawTransportError = false;
667
- let closeCode;
668
- let closeReason;
669
- let abortRequested = false;
670
- const onMessage = (event) => {
671
- try {
672
- const parsed = parseEventFrame(getEventData(event));
673
- cursor = Math.max(cursor, parsed.seq);
674
- if (options.agent && agentFromActor(parsed.actor) !== options.agent) {
675
- return;
676
- }
677
- queue.push(parsed);
678
- } catch (error) {
679
- queue.fail(error);
680
- }
681
- };
682
- const onError = () => {
683
- sawTransportError = true;
684
- queue.close();
685
- };
686
- const onClose = (event) => {
687
- closeCode = getCloseCode(event);
688
- closeReason = getCloseReason(event);
689
- queue.close();
690
- };
691
- const onAbort = () => {
692
- abortRequested = true;
693
- queue.close();
694
- socket.close(NORMAL_WEBSOCKET_CLOSE_CODE, "aborted");
695
- };
696
- socket.addEventListener("message", onMessage);
697
- socket.addEventListener("error", onError);
698
- socket.addEventListener("close", onClose);
699
- if (options.signal) {
700
- if (options.signal.aborted) {
701
- onAbort();
702
- } else {
703
- options.signal.addEventListener("abort", onAbort, { once: true });
1530
+ }).finally(() => {
1531
+ this.liveSyncTask = void 0;
1532
+ this.liveSyncController = void 0;
1533
+ });
1534
+ }
1535
+ async runLiveSync(signal) {
1536
+ while (!signal.aborted && this.eventSubscriptions.size > 0) {
1537
+ const stream = new TailStream({
1538
+ sessionId: this.id,
1539
+ token: this.token,
1540
+ websocketBaseUrl: this.transport.websocketBaseUrl,
1541
+ websocketFactory: this.transport.websocketFactory,
1542
+ options: {
1543
+ cursor: this.log.lastSeq,
1544
+ signal
704
1545
  }
705
- }
706
- let iterationError = null;
1546
+ });
707
1547
  try {
708
- while (true) {
709
- const next = await queue.next();
710
- if (next.done) {
711
- break;
1548
+ await stream.subscribe((batch) => {
1549
+ const appliedEvents = this.log.applyBatch(batch);
1550
+ if (appliedEvents.length > 0) {
1551
+ this.persistLogState();
712
1552
  }
713
- yield next.value;
714
- }
1553
+ });
715
1554
  } catch (error) {
716
- iterationError = toError(error);
717
- } finally {
718
- socket.removeEventListener("message", onMessage);
719
- socket.removeEventListener("error", onError);
720
- socket.removeEventListener("close", onClose);
721
- if (options.signal) {
722
- options.signal.removeEventListener("abort", onAbort);
1555
+ if (signal.aborted) {
1556
+ return;
723
1557
  }
724
- socket.close(NORMAL_WEBSOCKET_CLOSE_CODE, "finished");
725
- }
726
- if (iterationError) {
727
- throw iterationError;
728
- }
729
- if (abortRequested || options.signal?.aborted) {
730
- return;
731
- }
732
- const gracefullyClosed = !sawTransportError && closeCode === NORMAL_WEBSOCKET_CLOSE_CODE;
733
- if (gracefullyClosed) {
734
- return;
735
- }
736
- if (!reconnectEnabled) {
737
- throw new StarciteConnectionError(
738
- `Tail connection dropped for session '${sessionId}' (${describeClose(
739
- closeCode,
740
- closeReason
741
- )})`
742
- );
1558
+ if (error instanceof SessionLogGapError) {
1559
+ continue;
1560
+ }
1561
+ throw error;
743
1562
  }
744
- await waitForDelay(reconnectDelayMs, options.signal);
745
1563
  }
746
1564
  }
1565
+ persistLogState() {
1566
+ this.store.save(this.id, {
1567
+ cursor: this.log.cursor,
1568
+ events: [...this.log.events]
1569
+ });
1570
+ }
1571
+ };
1572
+
1573
+ // src/session-store.ts
1574
+ function cloneEvents(events) {
1575
+ return events.map((event) => structuredClone(event));
1576
+ }
1577
+ function cloneState(state) {
1578
+ return {
1579
+ cursor: state.cursor,
1580
+ events: cloneEvents(state.events)
1581
+ };
1582
+ }
1583
+ var MemoryStore = class {
1584
+ sessions = /* @__PURE__ */ new Map();
1585
+ load(sessionId) {
1586
+ const stored = this.sessions.get(sessionId);
1587
+ return stored ? cloneState(stored) : void 0;
1588
+ }
1589
+ save(sessionId, state) {
1590
+ this.sessions.set(sessionId, cloneState(state));
1591
+ }
1592
+ clear(sessionId) {
1593
+ this.sessions.delete(sessionId);
1594
+ }
1595
+ };
1596
+
1597
+ // src/client.ts
1598
+ var DEFAULT_BASE_URL = globalThis.process?.env?.STARCITE_BASE_URL ?? "http://localhost:4000";
1599
+ var DEFAULT_AUTH_URL = globalThis.process?.env?.STARCITE_AUTH_URL;
1600
+ function resolveAuthBaseUrl(explicitAuthUrl, apiKey) {
1601
+ if (explicitAuthUrl) {
1602
+ return normalizeAbsoluteHttpUrl(explicitAuthUrl, "authUrl");
1603
+ }
1604
+ if (DEFAULT_AUTH_URL) {
1605
+ return normalizeAbsoluteHttpUrl(DEFAULT_AUTH_URL, "STARCITE_AUTH_URL");
1606
+ }
1607
+ if (apiKey) {
1608
+ return inferIssuerAuthorityFromApiKey(apiKey);
1609
+ }
1610
+ return void 0;
1611
+ }
1612
+ var Starcite = class {
1613
+ /** Normalized API base URL ending with `/v1`. */
1614
+ baseUrl;
1615
+ transport;
1616
+ authBaseUrl;
1617
+ inferredIdentity;
1618
+ store;
1619
+ constructor(options = {}) {
1620
+ const baseUrl = toApiBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
1621
+ this.baseUrl = baseUrl;
1622
+ const userFetch = options.fetch;
1623
+ const fetchFn = userFetch ? (input, init) => userFetch(input, init) : (input, init) => fetch(input, init);
1624
+ const headers = new Headers(options.headers);
1625
+ const apiKey = options.apiKey?.trim();
1626
+ let authorization;
1627
+ if (apiKey) {
1628
+ authorization = `Bearer ${apiKey}`;
1629
+ this.inferredIdentity = inferIdentityFromApiKey(apiKey);
1630
+ }
1631
+ this.authBaseUrl = resolveAuthBaseUrl(options.authUrl, apiKey);
1632
+ const websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
1633
+ this.store = options.store ?? new MemoryStore();
1634
+ this.transport = {
1635
+ baseUrl,
1636
+ websocketBaseUrl: toWebSocketBaseUrl(baseUrl),
1637
+ authorization: authorization ?? null,
1638
+ fetchFn,
1639
+ headers,
1640
+ websocketFactory
1641
+ };
1642
+ }
1643
+ /**
1644
+ * Creates a user identity bound to this client's tenant.
1645
+ */
1646
+ user(options) {
1647
+ return new StarciteIdentity({
1648
+ tenantId: this.requireTenantId("user()"),
1649
+ id: options.id,
1650
+ type: "user"
1651
+ });
1652
+ }
747
1653
  /**
748
- * Opens a WebSocket tail stream and yields transformed session events.
1654
+ * Creates an agent identity bound to this client's tenant.
749
1655
  */
750
- async *tailEvents(sessionId, options = {}) {
751
- for await (const rawEvent of this.tailRawEvents(sessionId, options)) {
752
- yield toSessionEvent(rawEvent);
1656
+ agent(options) {
1657
+ return new StarciteIdentity({
1658
+ tenantId: this.requireTenantId("agent()"),
1659
+ id: options.id,
1660
+ type: "agent"
1661
+ });
1662
+ }
1663
+ session(input) {
1664
+ if ("token" in input) {
1665
+ return this.sessionFromToken(input.token, input.logOptions);
753
1666
  }
1667
+ return this.sessionFromIdentity(input);
754
1668
  }
755
- async request(path, init, schema) {
756
- const headers = new Headers(this.headers);
757
- if (!headers.has("content-type")) {
758
- headers.set("content-type", "application/json");
1669
+ /**
1670
+ * Lists sessions from the archive-backed catalog.
1671
+ */
1672
+ listSessions(options = {}, requestOptions) {
1673
+ const parsed = SessionListOptionsSchema.parse(options);
1674
+ const query = new URLSearchParams();
1675
+ if (parsed.limit !== void 0) {
1676
+ query.set("limit", `${parsed.limit}`);
759
1677
  }
760
- if (init.headers) {
761
- const perRequestHeaders = new Headers(init.headers);
762
- for (const [key, value] of perRequestHeaders.entries()) {
763
- headers.set(key, value);
1678
+ if (parsed.cursor !== void 0) {
1679
+ query.set("cursor", parsed.cursor);
1680
+ }
1681
+ if (parsed.metadata !== void 0) {
1682
+ for (const [key, value] of Object.entries(parsed.metadata)) {
1683
+ query.set(`metadata.${key}`, value);
764
1684
  }
765
1685
  }
766
- let response;
767
- try {
768
- response = await this.fetchFn(`${this.baseUrl}${path}`, {
769
- ...init,
770
- headers
1686
+ const suffix = query.size > 0 ? `?${query.toString()}` : "";
1687
+ return request(
1688
+ this.transport,
1689
+ `/sessions${suffix}`,
1690
+ {
1691
+ method: "GET",
1692
+ signal: requestOptions?.signal
1693
+ },
1694
+ SessionListPageSchema
1695
+ );
1696
+ }
1697
+ async sessionFromIdentity(input) {
1698
+ let sessionId = input.id;
1699
+ let record;
1700
+ if (!sessionId) {
1701
+ record = await this.createSession({
1702
+ creator_principal: input.identity.toCreatorPrincipal(),
1703
+ title: input.title,
1704
+ metadata: input.metadata
771
1705
  });
772
- } catch (error) {
773
- const rootCause = toError(error).message;
774
- throw new StarciteConnectionError(
775
- `Failed to connect to Starcite at ${this.baseUrl}: ${rootCause}`
1706
+ sessionId = record.id;
1707
+ }
1708
+ const tokenResponse = await this.issueSessionToken({
1709
+ session_id: sessionId,
1710
+ principal: input.identity.toTokenPrincipal(),
1711
+ scopes: ["session:read", "session:append"]
1712
+ });
1713
+ return new StarciteSession({
1714
+ id: sessionId,
1715
+ token: tokenResponse.token,
1716
+ identity: input.identity,
1717
+ transport: this.buildSessionTransport(tokenResponse.token),
1718
+ store: this.store,
1719
+ record,
1720
+ logOptions: input.logOptions
1721
+ });
1722
+ }
1723
+ sessionFromToken(token, logOptions) {
1724
+ const decoded = decodeSessionToken(token);
1725
+ const sessionId = decoded.sessionId;
1726
+ if (!sessionId) {
1727
+ throw new StarciteError(
1728
+ "session({ token }) requires a token with a session_id claim."
776
1729
  );
777
1730
  }
778
- if (!response.ok) {
779
- const payload = await tryParseJson(response);
780
- const code = typeof payload?.error === "string" ? payload.error : `http_${response.status}`;
781
- const message = typeof payload?.message === "string" ? payload.message : response.statusText;
782
- throw new StarciteApiError(message, response.status, code, payload);
1731
+ return new StarciteSession({
1732
+ id: sessionId,
1733
+ token,
1734
+ identity: decoded.identity,
1735
+ transport: this.buildSessionTransport(token),
1736
+ store: this.store,
1737
+ logOptions
1738
+ });
1739
+ }
1740
+ buildSessionTransport(token) {
1741
+ return {
1742
+ ...this.transport,
1743
+ authorization: `Bearer ${token}`
1744
+ };
1745
+ }
1746
+ createSession(input) {
1747
+ return request(
1748
+ this.transport,
1749
+ "/sessions",
1750
+ {
1751
+ method: "POST",
1752
+ body: JSON.stringify(input)
1753
+ },
1754
+ SessionRecordSchema
1755
+ );
1756
+ }
1757
+ issueSessionToken(input) {
1758
+ if (!this.transport.authorization) {
1759
+ throw new StarciteError(
1760
+ "session() with identity requires apiKey. Set StarciteOptions.apiKey."
1761
+ );
783
1762
  }
784
- if (response.status === 204) {
785
- return void 0;
1763
+ if (!this.authBaseUrl) {
1764
+ throw new StarciteError(
1765
+ "session() could not resolve auth issuer URL. Set StarciteOptions.authUrl, STARCITE_AUTH_URL, or use an API key JWT with an 'iss' claim."
1766
+ );
786
1767
  }
787
- const responseBody = await response.json();
788
- if (!schema) {
789
- return responseBody;
1768
+ return requestWithBaseUrl(
1769
+ this.transport,
1770
+ this.authBaseUrl,
1771
+ "/api/v1/session-tokens",
1772
+ {
1773
+ method: "POST",
1774
+ headers: {
1775
+ "cache-control": "no-store"
1776
+ },
1777
+ body: JSON.stringify(input)
1778
+ },
1779
+ IssueSessionTokenResponseSchema
1780
+ );
1781
+ }
1782
+ requireTenantId(method) {
1783
+ const tenantId = this.inferredIdentity?.tenantId;
1784
+ if (!tenantId) {
1785
+ throw new StarciteError(`${method} requires apiKey to determine tenant.`);
790
1786
  }
791
- const parsed = schema.safeParse(responseBody);
792
- if (!parsed.success) {
793
- const issue = parsed.error.issues[0]?.message ?? "invalid response";
794
- throw new StarciteConnectionError(
795
- `Received unexpected response payload from Starcite: ${issue}`
796
- );
1787
+ return tenantId;
1788
+ }
1789
+ };
1790
+
1791
+ // src/cursor-store.ts
1792
+ var DEFAULT_KEY_PREFIX = "starcite";
1793
+ var InMemoryCursorStore = class {
1794
+ cursors;
1795
+ constructor(initial = {}) {
1796
+ this.cursors = new Map(Object.entries(initial));
1797
+ }
1798
+ load(sessionId) {
1799
+ return this.cursors.get(sessionId);
1800
+ }
1801
+ save(sessionId, cursor) {
1802
+ this.cursors.set(sessionId, cursor);
1803
+ }
1804
+ };
1805
+ var WebStorageCursorStore = class {
1806
+ storage;
1807
+ keyForSession;
1808
+ constructor(storage, options = {}) {
1809
+ this.storage = storage;
1810
+ const prefix = options.keyPrefix?.trim() || DEFAULT_KEY_PREFIX;
1811
+ this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:lastSeq`);
1812
+ }
1813
+ load(sessionId) {
1814
+ const raw = this.storage.getItem(this.keyForSession(sessionId));
1815
+ if (raw === null) {
1816
+ return void 0;
797
1817
  }
798
- return parsed.data;
1818
+ const parsed = Number.parseInt(raw, 10);
1819
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
1820
+ }
1821
+ save(sessionId, cursor) {
1822
+ this.storage.setItem(this.keyForSession(sessionId), `${cursor}`);
799
1823
  }
800
1824
  };
801
- async function tryParseJson(response) {
802
- try {
803
- const parsed = await response.json();
804
- const result = StarciteErrorPayloadSchema.safeParse(parsed);
805
- return result.success ? result.data : null;
806
- } catch {
807
- return null;
1825
+ var LocalStorageCursorStore = class extends WebStorageCursorStore {
1826
+ constructor(options = {}) {
1827
+ if (typeof localStorage === "undefined") {
1828
+ throw new StarciteError(
1829
+ "localStorage is not available in this runtime. Use WebStorageCursorStore with a custom storage adapter."
1830
+ );
1831
+ }
1832
+ super(localStorage, options);
808
1833
  }
809
- }
810
-
811
- // src/index.ts
812
- function createStarciteClient(options = {}) {
813
- return new StarciteClient(options);
814
- }
815
- var starcite = createStarciteClient();
1834
+ };
816
1835
  // Annotate the CommonJS export names for ESM import in node:
817
1836
  0 && (module.exports = {
1837
+ InMemoryCursorStore,
1838
+ LocalStorageCursorStore,
1839
+ MemoryStore,
1840
+ SessionLogConflictError,
1841
+ SessionLogGapError,
1842
+ Starcite,
818
1843
  StarciteApiError,
819
- StarciteClient,
1844
+ StarciteBackpressureError,
820
1845
  StarciteConnectionError,
821
1846
  StarciteError,
1847
+ StarciteIdentity,
1848
+ StarciteRetryLimitError,
822
1849
  StarciteSession,
823
- createStarciteClient,
824
- normalizeBaseUrl,
825
- starcite
1850
+ StarciteTailError,
1851
+ StarciteTokenExpiredError,
1852
+ WebStorageCursorStore
826
1853
  });
827
1854
  //# sourceMappingURL=index.cjs.map