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