@ekairos/thread 1.21.89-beta.0 → 1.21.90-beta.0

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/schema.js CHANGED
@@ -13,6 +13,7 @@ export const threadDomain = domain("thread")
13
13
  thread_contexts: i.entity({
14
14
  createdAt: i.date(),
15
15
  updatedAt: i.date().optional(),
16
+ // Required for prefix/history queries that use `$like` over context keys.
16
17
  key: i.string().optional().indexed().unique(),
17
18
  status: i.string().optional().indexed(), // open | streaming | closed
18
19
  content: i.any().optional(),
@@ -74,6 +74,39 @@ function inferDirection(item) {
74
74
  }
75
75
  return undefined;
76
76
  }
77
+ function shouldDebugThreadStoreSteps() {
78
+ return (process.env.EKAIROS_THREAD_DEBUG === "1" ||
79
+ process.env.PLAYWRIGHT_TEST === "1");
80
+ }
81
+ function summarizeStepError(error) {
82
+ const err = error;
83
+ return {
84
+ name: err?.name,
85
+ message: err?.message,
86
+ code: err?.code,
87
+ status: err?.status ?? err?.statusCode,
88
+ details: err?.details ?? err?.body ?? err?.response ?? err?.data,
89
+ stack: typeof err?.stack === "string"
90
+ ? err.stack.slice(0, 1500)
91
+ : undefined,
92
+ };
93
+ }
94
+ function summarizeContextIdentifierForLog(identifier) {
95
+ return {
96
+ id: identifier && typeof identifier.id === "string"
97
+ ? String(identifier.id)
98
+ : undefined,
99
+ key: identifier && typeof identifier.key === "string"
100
+ ? String(identifier.key)
101
+ : undefined,
102
+ };
103
+ }
104
+ function logStepDebug(message, payload) {
105
+ if (!shouldDebugThreadStoreSteps())
106
+ return;
107
+ // eslint-disable-next-line no-console
108
+ console.error(`[thread][store.steps] ${message}`, payload);
109
+ }
77
110
  /**
78
111
  * Initializes/ensures the story context exists and emits a single `data-context-id` chunk.
79
112
  *
@@ -174,13 +207,54 @@ export async function saveTriggerAndCreateExecution(params) {
174
207
  const { getThreadRuntime } = await import("@ekairos/thread/runtime");
175
208
  const runtime = await getThreadRuntime(params.env);
176
209
  const { store, db } = runtime;
177
- const saved = await store.saveItem(params.contextIdentifier, params.triggerEvent);
210
+ logStepDebug("saveTriggerAndCreateExecution:start", {
211
+ contextIdentifier: summarizeContextIdentifierForLog(params.contextIdentifier),
212
+ triggerEventId: params.triggerEvent?.id,
213
+ triggerEventType: params.triggerEvent?.type,
214
+ triggerEventChannel: params.triggerEvent?.channel,
215
+ triggerEventCreatedAt: params.triggerEvent?.createdAt,
216
+ });
217
+ let saved;
218
+ try {
219
+ saved = await store.saveItem(params.contextIdentifier, params.triggerEvent);
220
+ }
221
+ catch (error) {
222
+ logStepDebug("saveTriggerAndCreateExecution:saveItem:error", {
223
+ contextIdentifier: summarizeContextIdentifierForLog(params.contextIdentifier),
224
+ triggerEventId: params.triggerEvent?.id,
225
+ error: summarizeStepError(error),
226
+ });
227
+ throw error;
228
+ }
178
229
  const uuid = globalThis.crypto?.randomUUID?.();
179
230
  const reactionEventId = typeof uuid === "string"
180
231
  ? uuid
181
232
  : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
182
- await store.updateContextStatus(params.contextIdentifier, "streaming");
183
- const execution = await store.createExecution(params.contextIdentifier, saved.id, reactionEventId);
233
+ try {
234
+ await store.updateContextStatus(params.contextIdentifier, "streaming");
235
+ }
236
+ catch (error) {
237
+ logStepDebug("saveTriggerAndCreateExecution:updateContextStatus:error", {
238
+ contextIdentifier: summarizeContextIdentifierForLog(params.contextIdentifier),
239
+ triggerEventId: saved.id,
240
+ reactionEventId,
241
+ error: summarizeStepError(error),
242
+ });
243
+ throw error;
244
+ }
245
+ let execution;
246
+ try {
247
+ execution = await store.createExecution(params.contextIdentifier, saved.id, reactionEventId);
248
+ }
249
+ catch (error) {
250
+ logStepDebug("saveTriggerAndCreateExecution:createExecution:error", {
251
+ contextIdentifier: summarizeContextIdentifierForLog(params.contextIdentifier),
252
+ triggerEventId: saved.id,
253
+ reactionEventId,
254
+ error: summarizeStepError(error),
255
+ });
256
+ throw error;
257
+ }
184
258
  const { runId, meta } = await resolveWorkflowRunId({
185
259
  env: params.env,
186
260
  db,
@@ -14,6 +14,7 @@ export type InstantStoreDb = any;
14
14
  export declare class InstantStore implements ThreadStore {
15
15
  private db;
16
16
  constructor(db: InstantStoreDb);
17
+ private syncLegacyThreadRowBestEffort;
17
18
  private ensureContextThreadLink;
18
19
  private normalizeThread;
19
20
  private normalizeContext;
@@ -4,10 +4,92 @@ import { convertItemsToModelMessages } from "../thread.events.js";
4
4
  export { parseAndStoreDocument } from "./instant.document-parser.js";
5
5
  import { expandEventsWithInstantDocuments } from "./instant.documents.js";
6
6
  export { coerceDocumentTextPages, expandEventsWithInstantDocuments, } from "./instant.documents.js";
7
+ function shouldDebugInstantStore() {
8
+ return (process.env.EKAIROS_THREAD_DEBUG === "1" ||
9
+ process.env.PLAYWRIGHT_TEST === "1");
10
+ }
11
+ function clipText(value, max = 500) {
12
+ if (value.length <= max)
13
+ return value;
14
+ return `${value.slice(0, max)}...<truncated:${value.length - max}>`;
15
+ }
16
+ function simplifyForLog(value, depth = 0) {
17
+ if (value === null || value === undefined)
18
+ return value;
19
+ if (typeof value === "string")
20
+ return clipText(value);
21
+ if (typeof value === "number" || typeof value === "boolean")
22
+ return value;
23
+ if (value instanceof Date)
24
+ return value.toISOString();
25
+ if (depth >= 2)
26
+ return "[max-depth]";
27
+ if (Array.isArray(value)) {
28
+ const maxItems = 8;
29
+ const items = value
30
+ .slice(0, maxItems)
31
+ .map((item) => simplifyForLog(item, depth + 1));
32
+ if (value.length > maxItems) {
33
+ items.push(`...+${value.length - maxItems} more`);
34
+ }
35
+ return items;
36
+ }
37
+ if (typeof value === "object") {
38
+ const entries = Object.entries(value);
39
+ const maxEntries = 16;
40
+ const out = {};
41
+ for (const [key, entryValue] of entries.slice(0, maxEntries)) {
42
+ out[key] = simplifyForLog(entryValue, depth + 1);
43
+ }
44
+ if (entries.length > maxEntries) {
45
+ out.__truncatedKeys = entries.length - maxEntries;
46
+ }
47
+ return out;
48
+ }
49
+ return String(value);
50
+ }
51
+ function summarizeError(error) {
52
+ const err = error;
53
+ return {
54
+ name: err?.name,
55
+ message: err?.message,
56
+ code: err?.code,
57
+ status: err?.status ?? err?.statusCode,
58
+ details: simplifyForLog(err?.details ?? err?.body ?? err?.response ?? err?.data),
59
+ stack: typeof err?.stack === "string"
60
+ ? clipText(err.stack, 1500)
61
+ : undefined,
62
+ };
63
+ }
64
+ function logInstantTransactFailure(params) {
65
+ if (!shouldDebugInstantStore())
66
+ return;
67
+ const payload = {
68
+ action: params.action,
69
+ meta: simplifyForLog(params.meta ?? {}),
70
+ txs: simplifyForLog(params.txs),
71
+ error: summarizeError(params.error),
72
+ };
73
+ // eslint-disable-next-line no-console
74
+ console.error("[thread][instant.store] transact failed", payload);
75
+ }
7
76
  export class InstantStore {
8
77
  constructor(db) {
9
78
  this.db = db;
10
79
  }
80
+ async syncLegacyThreadRowBestEffort(threadId) {
81
+ if (!threadId)
82
+ return;
83
+ try {
84
+ await this.db.transact([
85
+ this.db.tx.thread[threadId].update({}),
86
+ ]);
87
+ }
88
+ catch {
89
+ // Legacy `thread` entity does not exist on all apps.
90
+ // This is a best-effort compatibility write.
91
+ }
92
+ }
11
93
  async ensureContextThreadLink(context, contextIdentifier) {
12
94
  if (context.threadId)
13
95
  return context;
@@ -15,6 +97,7 @@ export class InstantStore {
15
97
  context.key ??
16
98
  (context.id ? String(context.id) : null);
17
99
  const thread = await this.createThread(fallbackKey ? { key: String(fallbackKey) } : null, context.id ? String(context.id) : undefined);
100
+ await this.syncLegacyThreadRowBestEffort(thread.id);
18
101
  const txs = [this.db.tx.thread_contexts[context.id].link({ thread: thread.id })];
19
102
  if (!context.key && thread.key) {
20
103
  txs.push(this.db.tx.thread_contexts[context.id].update({
@@ -56,6 +139,7 @@ export class InstantStore {
56
139
  };
57
140
  }
58
141
  async getThreadByContextId(contextId) {
142
+ let contextKey = null;
59
143
  try {
60
144
  const res = await this.db.query({
61
145
  thread_contexts: {
@@ -64,13 +148,27 @@ export class InstantStore {
64
148
  },
65
149
  });
66
150
  const ctx = res?.thread_contexts?.[0];
151
+ contextKey = typeof ctx?.key === "string" ? ctx.key : null;
67
152
  const threadNode = Array.isArray(ctx?.thread) ? ctx.thread[0] : ctx?.thread;
68
- if (threadNode)
69
- return this.normalizeThread(threadNode);
153
+ if (threadNode) {
154
+ const normalized = this.normalizeThread(threadNode);
155
+ if (normalized.id)
156
+ return normalized;
157
+ }
70
158
  }
71
159
  catch {
72
160
  // fallback below
73
161
  }
162
+ if (contextKey) {
163
+ try {
164
+ const byKey = await this.getThread({ key: contextKey });
165
+ if (byKey?.id)
166
+ return byKey;
167
+ }
168
+ catch {
169
+ // continue fallback chain
170
+ }
171
+ }
74
172
  try {
75
173
  const res = await this.db.query({
76
174
  thread_threads: {
@@ -81,7 +179,23 @@ export class InstantStore {
81
179
  return row ? this.normalizeThread(row) : null;
82
180
  }
83
181
  catch {
84
- return null;
182
+ try {
183
+ const executionFallback = await this.db.query({
184
+ thread_executions: {
185
+ $: { where: { "context.id": contextId }, limit: 1 },
186
+ thread: {},
187
+ },
188
+ });
189
+ const executionRow = executionFallback?.thread_executions?.[0];
190
+ const executionThread = Array.isArray(executionRow?.thread)
191
+ ? executionRow.thread[0]
192
+ : executionRow?.thread;
193
+ const normalized = executionThread ? this.normalizeThread(executionThread) : null;
194
+ return normalized?.id ? normalized : null;
195
+ }
196
+ catch {
197
+ return null;
198
+ }
85
199
  }
86
200
  }
87
201
  async getContextByThreadId(threadId) {
@@ -142,6 +256,7 @@ export class InstantStore {
142
256
  key,
143
257
  }),
144
258
  ]);
259
+ await this.syncLegacyThreadRowBestEffort(threadId);
145
260
  const thread = await this.getThread({ id: threadId });
146
261
  if (!thread) {
147
262
  throw new Error("InstantStore: failed to create thread");
@@ -333,7 +448,26 @@ export class InstantStore {
333
448
  ];
334
449
  txs.push(this.db.tx.thread_items[event.id].link({ context: context.id }));
335
450
  txs.push(this.db.tx.thread_items[event.id].link({ thread: thread.id }));
336
- await this.db.transact(txs);
451
+ try {
452
+ await this.db.transact(txs);
453
+ }
454
+ catch (error) {
455
+ logInstantTransactFailure({
456
+ action: "saveItem",
457
+ meta: {
458
+ contextIdentifier: simplifyForLog(contextIdentifier),
459
+ contextId: context.id,
460
+ threadId: thread.id,
461
+ eventId: event?.id,
462
+ eventType: event?.type,
463
+ eventChannel: event?.channel,
464
+ eventCreatedAt: event?.createdAt,
465
+ },
466
+ txs,
467
+ error,
468
+ });
469
+ throw error;
470
+ }
337
471
  const persisted = await this.getItem(event.id);
338
472
  if (!persisted)
339
473
  throw new Error("InstantStore: failed to read event after save");
@@ -414,7 +548,25 @@ export class InstantStore {
414
548
  status: "streaming",
415
549
  updatedAt: new Date(),
416
550
  }));
417
- await this.db.transact(txs);
551
+ try {
552
+ await this.db.transact(txs);
553
+ }
554
+ catch (error) {
555
+ logInstantTransactFailure({
556
+ action: "createExecution",
557
+ meta: {
558
+ contextIdentifier: simplifyForLog(contextIdentifier),
559
+ contextId: context.id,
560
+ threadId: thread.id,
561
+ executionId,
562
+ triggerEventId,
563
+ reactionEventId,
564
+ },
565
+ txs,
566
+ error,
567
+ });
568
+ throw error;
569
+ }
418
570
  return { id: executionId };
419
571
  }
420
572
  async completeExecution(contextIdentifier, executionId, status) {
@@ -442,7 +594,23 @@ export class InstantStore {
442
594
  }),
443
595
  ];
444
596
  txs.push(this.db.tx.thread_steps[stepId].link({ execution: params.executionId }));
445
- await this.db.transact(txs);
597
+ try {
598
+ await this.db.transact(txs);
599
+ }
600
+ catch (error) {
601
+ logInstantTransactFailure({
602
+ action: "createStep",
603
+ meta: {
604
+ executionId: params.executionId,
605
+ iteration: params.iteration,
606
+ stepId,
607
+ eventId,
608
+ },
609
+ txs,
610
+ error,
611
+ });
612
+ throw error;
613
+ }
446
614
  return { id: stepId, eventId };
447
615
  }
448
616
  async updateStep(stepId, patch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekairos/thread",
3
- "version": "1.21.89-beta.0",
3
+ "version": "1.21.90-beta.0",
4
4
  "description": "Pulzar Thread - Workflow-based AI Threads",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -110,7 +110,7 @@
110
110
  },
111
111
  "dependencies": {
112
112
  "@ai-sdk/openai": "^2.0.52",
113
- "@ekairos/domain": "^1.21.89-beta.0",
113
+ "@ekairos/domain": "^1.21.90-beta.0",
114
114
  "@instantdb/admin": "0.22.126",
115
115
  "@instantdb/core": "0.22.126",
116
116
  "@vercel/mcp-adapter": "^1.0.0",
@@ -120,7 +120,7 @@
120
120
  "jose": "^6.1.3",
121
121
  "llamaindex": "^0.12.0",
122
122
  "react": "^19.2.0",
123
- "workflow": "4.1.0-beta.53",
123
+ "workflow": "4.1.0-beta.55",
124
124
  "xmlbuilder2": "^3.1.1",
125
125
  "zod": "^4.1.8"
126
126
  },