@agent-native/core 0.14.5 → 0.14.6

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.
@@ -1,4 +1,5 @@
1
1
  import { getDbExec, intType } from "../db/client.js";
2
+ import { mergeThreadDataForClientSave, normalizeThreadRepository, } from "../agent/thread-data-builder.js";
2
3
  import { emitChatThreadChange } from "./emitter.js";
3
4
  let _initPromise;
4
5
  /**
@@ -11,10 +12,9 @@ let _initPromise;
11
12
  * while leaving straight reads and other thread-data-unrelated updates
12
13
  * untouched.
13
14
  *
14
- * Cross-process races (multiple Node replicas writing the same thread at
15
- * the same instant) are not fixed here acceptable for `thread_data`
16
- * today because writes come from either the user's own tab or an agent
17
- * run owned by that user, which run in one place at a time.
15
+ * Cross-process races are handled by `updateThreadData`, which performs a
16
+ * compare-and-swap on `updated_at`, rereads the latest row on conflict, and
17
+ * remerges message history before retrying.
18
18
  */
19
19
  const _threadDataLocks = new Map();
20
20
  export function withThreadDataLock(threadId, fn) {
@@ -56,6 +56,48 @@ async function ensureTable() {
56
56
  function generateId() {
57
57
  return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
58
58
  }
59
+ function deriveMessageCount(threadData, fallback) {
60
+ if (typeof threadData !== "string" || !threadData.trim())
61
+ return fallback;
62
+ try {
63
+ const repo = normalizeThreadRepository(JSON.parse(threadData));
64
+ if (Array.isArray(repo.messages))
65
+ return repo.messages.length;
66
+ }
67
+ catch {
68
+ // Keep the stored count if the JSON blob is malformed.
69
+ }
70
+ return fallback;
71
+ }
72
+ function rowToThread(r) {
73
+ const threadData = r.thread_data ?? "{}";
74
+ const storedCount = Number(r.message_count);
75
+ return {
76
+ id: r.id,
77
+ ownerEmail: r.owner_email,
78
+ title: r.title,
79
+ preview: r.preview,
80
+ threadData,
81
+ messageCount: deriveMessageCount(threadData, storedCount),
82
+ createdAt: Number(r.created_at),
83
+ updatedAt: Number(r.updated_at),
84
+ };
85
+ }
86
+ function rowToSummary(r) {
87
+ const threadData = r.thread_data;
88
+ const storedCount = Number(r.message_count);
89
+ const messageCount = deriveMessageCount(threadData, storedCount);
90
+ if (messageCount <= 0)
91
+ return null;
92
+ return {
93
+ id: r.id,
94
+ title: r.title,
95
+ preview: r.preview,
96
+ messageCount,
97
+ createdAt: Number(r.created_at),
98
+ updatedAt: Number(r.updated_at),
99
+ };
100
+ }
59
101
  export async function createThread(ownerEmail, opts) {
60
102
  await ensureTable();
61
103
  const client = getDbExec();
@@ -86,17 +128,7 @@ export async function getThread(id) {
86
128
  });
87
129
  if (rows.length === 0)
88
130
  return null;
89
- const r = rows[0];
90
- return {
91
- id: r.id,
92
- ownerEmail: r.owner_email,
93
- title: r.title,
94
- preview: r.preview,
95
- threadData: r.thread_data,
96
- messageCount: Number(r.message_count),
97
- createdAt: Number(r.created_at),
98
- updatedAt: Number(r.updated_at),
99
- };
131
+ return rowToThread(rows[0]);
100
132
  }
101
133
  export async function forkThread(sourceId, ownerEmail, opts) {
102
134
  const source = await getThread(sourceId);
@@ -134,17 +166,12 @@ export async function listThreads(ownerEmail, limit = 50, offset = 0) {
134
166
  await ensureTable();
135
167
  const client = getDbExec();
136
168
  const { rows } = await client.execute({
137
- sql: `SELECT id, title, preview, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?`,
169
+ sql: `SELECT id, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (message_count > 0 OR thread_data LIKE '%"messages"%') ORDER BY updated_at DESC LIMIT ? OFFSET ?`,
138
170
  args: [ownerEmail, limit, offset],
139
171
  });
140
- return rows.map((r) => ({
141
- id: r.id,
142
- title: r.title,
143
- preview: r.preview,
144
- messageCount: Number(r.message_count),
145
- createdAt: Number(r.created_at),
146
- updatedAt: Number(r.updated_at),
147
- }));
172
+ return rows
173
+ .map((r) => rowToSummary(r))
174
+ .filter((r) => r !== null);
148
175
  }
149
176
  function escapeLike(s) {
150
177
  return s.replace(/([\\%_])/g, "\\$1");
@@ -154,26 +181,68 @@ export async function searchThreads(ownerEmail, query, limit = 50) {
154
181
  const client = getDbExec();
155
182
  const pattern = `%${escapeLike(query)}%`;
156
183
  const { rows } = await client.execute({
157
- sql: `SELECT id, title, preview, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (title LIKE ? OR preview LIKE ? OR thread_data LIKE ?) ORDER BY updated_at DESC LIMIT ?`,
184
+ sql: `SELECT id, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (message_count > 0 OR thread_data LIKE '%"messages"%') AND (title LIKE ? OR preview LIKE ? OR thread_data LIKE ?) ORDER BY updated_at DESC LIMIT ?`,
158
185
  args: [ownerEmail, pattern, pattern, pattern, limit],
159
186
  });
160
- return rows.map((r) => ({
161
- id: r.id,
162
- title: r.title,
163
- preview: r.preview,
164
- messageCount: Number(r.message_count),
165
- createdAt: Number(r.created_at),
166
- updatedAt: Number(r.updated_at),
167
- }));
187
+ return rows
188
+ .map((r) => rowToSummary(r))
189
+ .filter((r) => r !== null);
168
190
  }
169
- export async function updateThreadData(id, threadData, title, preview, messageCount) {
191
+ function parseThreadData(value) {
192
+ try {
193
+ return JSON.parse(value || "{}");
194
+ }
195
+ catch {
196
+ return {};
197
+ }
198
+ }
199
+ export async function updateThreadData(id, threadData, title, preview, messageCount, options = {}) {
170
200
  await ensureTable();
171
201
  const client = getDbExec();
172
- await client.execute({
173
- sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ?`,
174
- args: [threadData, title, preview, messageCount, Date.now(), id],
175
- });
176
- emitChatThreadChange(id);
202
+ const maxAttempts = options.maxAttempts ?? 5;
203
+ let lastConflict = false;
204
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
205
+ const current = await getThread(id);
206
+ if (!current)
207
+ return;
208
+ let nextThreadData = threadData;
209
+ let nextMessageCount = messageCount;
210
+ try {
211
+ const merged = mergeThreadDataForClientSave(parseThreadData(current.threadData), parseThreadData(threadData), {
212
+ preserveExistingQueuedMessages: options.preserveExistingQueuedMessages ?? true,
213
+ preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
214
+ });
215
+ nextThreadData = JSON.stringify(merged);
216
+ if (Array.isArray(merged.messages)) {
217
+ nextMessageCount = merged.messages.length;
218
+ }
219
+ }
220
+ catch {
221
+ // Keep the caller's serialized value if either JSON blob is malformed.
222
+ }
223
+ const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
224
+ const result = await client.execute({
225
+ sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ? AND updated_at = ?`,
226
+ args: [
227
+ nextThreadData,
228
+ title,
229
+ preview,
230
+ nextMessageCount,
231
+ nextUpdatedAt,
232
+ id,
233
+ current.updatedAt,
234
+ ],
235
+ });
236
+ if (result.rowsAffected > 0) {
237
+ emitChatThreadChange(id);
238
+ return;
239
+ }
240
+ lastConflict = true;
241
+ await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)));
242
+ }
243
+ if (lastConflict) {
244
+ throw new Error(`Failed to update chat thread ${id} after concurrent write conflicts.`);
245
+ }
177
246
  }
178
247
  /**
179
248
  * Read the engine pinned to a thread (stored in thread_data JSON).
@@ -230,7 +299,7 @@ export async function setThreadQueuedMessages(threadId, queuedMessages) {
230
299
  else {
231
300
  data.queuedMessages = queuedMessages;
232
301
  }
233
- await updateThreadData(threadId, JSON.stringify(data), thread.title, thread.preview, thread.messageCount);
302
+ await updateThreadData(threadId, JSON.stringify(data), thread.title, thread.preview, thread.messageCount, { preserveExistingQueuedMessages: false });
234
303
  });
235
304
  }
236
305
  export async function deleteThread(id) {
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/chat-threads/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec, isPostgres, intType } from \"../db/client.js\";\nimport { emitChatThreadChange } from \"./emitter.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\n/**\n * Per-thread async mutex. Read-modify-write on the `thread_data` JSON blob\n * is not atomic at the DB level — two concurrent callers (e.g. the UI\n * persisting queued messages while `onRunComplete` appends agent output)\n * would both read the same row, each mutate it independently, and the\n * second write clobbers the first. Serializing on thread id inside this\n * process eliminates the race for the usual single-process deployment\n * while leaving straight reads and other thread-data-unrelated updates\n * untouched.\n *\n * Cross-process races (multiple Node replicas writing the same thread at\n * the same instant) are not fixed here — acceptable for `thread_data`\n * today because writes come from either the user's own tab or an agent\n * run owned by that user, which run in one place at a time.\n */\nconst _threadDataLocks = new Map<string, Promise<unknown>>();\n\nexport function withThreadDataLock<T>(\n threadId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const prev = _threadDataLocks.get(threadId) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n _threadDataLocks.set(threadId, next);\n // Use `.then(cleanup, cleanup)` (not `.finally`) so the rejection is\n // observed on this chained promise — otherwise any failure inside `fn`\n // triggers `unhandledRejection` on the discarded `finally()` return.\n // The caller still sees the rejection via `next`.\n const cleanup = () => {\n if (_threadDataLocks.get(threadId) === next) {\n _threadDataLocks.delete(threadId);\n }\n };\n next.then(cleanup, cleanup);\n return next as Promise<T>;\n}\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS chat_threads (\n id TEXT PRIMARY KEY,\n owner_email TEXT NOT NULL,\n title TEXT NOT NULL DEFAULT '',\n preview TEXT NOT NULL DEFAULT '',\n thread_data TEXT NOT NULL DEFAULT '{}',\n message_count ${intType()} NOT NULL DEFAULT 0,\n created_at ${intType()} NOT NULL,\n updated_at ${intType()} NOT NULL\n )\n `);\n })();\n }\n return _initPromise;\n}\n\nfunction generateId(): string {\n return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nexport interface ChatThread {\n id: string;\n ownerEmail: string;\n title: string;\n preview: string;\n threadData: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface ChatThreadSummary {\n id: string;\n title: string;\n preview: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport async function createThread(\n ownerEmail: string,\n opts?: { id?: string; title?: string },\n): Promise<ChatThread> {\n await ensureTable();\n const client = getDbExec();\n const id = opts?.id ?? generateId();\n const now = Date.now();\n const title = opts?.title ?? \"\";\n\n await client.execute({\n sql: `INSERT INTO chat_threads (id, owner_email, title, preview, thread_data, message_count, created_at, updated_at) VALUES (?, ?, ?, '', '{}', 0, ?, ?)`,\n args: [id, ownerEmail, title, now, now],\n });\n\n return {\n id,\n ownerEmail,\n title,\n preview: \"\",\n threadData: \"{}\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n}\n\nexport async function getThread(id: string): Promise<ChatThread | null> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE id = ?`,\n args: [id],\n });\n if (rows.length === 0) return null;\n const r = rows[0];\n return {\n id: r.id as string,\n ownerEmail: r.owner_email as string,\n title: r.title as string,\n preview: r.preview as string,\n threadData: r.thread_data as string,\n messageCount: Number(r.message_count),\n createdAt: Number(r.created_at),\n updatedAt: Number(r.updated_at),\n };\n}\n\nexport async function forkThread(\n sourceId: string,\n ownerEmail: string,\n opts?: { id?: string },\n): Promise<ChatThread | null> {\n const source = await getThread(sourceId);\n if (!source || source.ownerEmail !== ownerEmail) return null;\n const id = opts?.id ?? generateId();\n const now = Date.now();\n const title = source.title ? `${source.title} (fork)` : \"\";\n const client = getDbExec();\n await client.execute({\n sql: `INSERT INTO chat_threads (id, owner_email, title, preview, thread_data, message_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n title,\n source.preview,\n source.threadData,\n source.messageCount,\n now,\n now,\n ],\n });\n return {\n id,\n ownerEmail,\n title,\n preview: source.preview,\n threadData: source.threadData,\n messageCount: source.messageCount,\n createdAt: now,\n updatedAt: now,\n };\n}\n\nexport async function listThreads(\n ownerEmail: string,\n limit = 50,\n offset = 0,\n): Promise<ChatThreadSummary[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, title, preview, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?`,\n args: [ownerEmail, limit, offset],\n });\n return rows.map((r) => ({\n id: r.id as string,\n title: r.title as string,\n preview: r.preview as string,\n messageCount: Number(r.message_count),\n createdAt: Number(r.created_at),\n updatedAt: Number(r.updated_at),\n }));\n}\n\nfunction escapeLike(s: string): string {\n return s.replace(/([\\\\%_])/g, \"\\\\$1\");\n}\n\nexport async function searchThreads(\n ownerEmail: string,\n query: string,\n limit = 50,\n): Promise<ChatThreadSummary[]> {\n await ensureTable();\n const client = getDbExec();\n const pattern = `%${escapeLike(query)}%`;\n const { rows } = await client.execute({\n sql: `SELECT id, title, preview, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (title LIKE ? OR preview LIKE ? OR thread_data LIKE ?) ORDER BY updated_at DESC LIMIT ?`,\n args: [ownerEmail, pattern, pattern, pattern, limit],\n });\n return rows.map((r) => ({\n id: r.id as string,\n title: r.title as string,\n preview: r.preview as string,\n messageCount: Number(r.message_count),\n createdAt: Number(r.created_at),\n updatedAt: Number(r.updated_at),\n }));\n}\n\nexport async function updateThreadData(\n id: string,\n threadData: string,\n title: string,\n preview: string,\n messageCount: number,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n await client.execute({\n sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ?`,\n args: [threadData, title, preview, messageCount, Date.now(), id],\n });\n emitChatThreadChange(id);\n}\n\nexport interface ThreadEngineMeta {\n engineName: string;\n model: string;\n}\n\n/**\n * Read the engine pinned to a thread (stored in thread_data JSON).\n * Returns null if no engine is pinned.\n */\nexport async function getThreadEngineMeta(\n threadId: string,\n): Promise<ThreadEngineMeta | null> {\n const thread = await getThread(threadId);\n if (!thread?.threadData) return null;\n try {\n const data = JSON.parse(thread.threadData);\n if (data.engineMeta?.engineName) return data.engineMeta as ThreadEngineMeta;\n } catch {}\n return null;\n}\n\n/**\n * Pin an engine to a thread by storing engineMeta in thread_data JSON.\n * Does not change messages, title, or preview.\n */\nexport async function setThreadEngineMeta(\n threadId: string,\n meta: ThreadEngineMeta,\n): Promise<void> {\n return withThreadDataLock(threadId, async () => {\n const thread = await getThread(threadId);\n if (!thread) return;\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(thread.threadData);\n } catch {}\n data.engineMeta = meta;\n await updateThreadData(\n threadId,\n JSON.stringify(data),\n thread.title,\n thread.preview,\n thread.messageCount,\n );\n });\n}\n\nexport interface QueuedMessage {\n id: string;\n text: string;\n images?: string[];\n references?: unknown[];\n}\n\n/**\n * Persist the user's queued (not-yet-sent) messages onto the thread.\n * Stored in thread_data JSON so it survives reloads without a schema\n * change. Safe to call often — the frontend debounces writes.\n */\nexport async function setThreadQueuedMessages(\n threadId: string,\n queuedMessages: QueuedMessage[],\n): Promise<void> {\n return withThreadDataLock(threadId, async () => {\n const thread = await getThread(threadId);\n if (!thread) return;\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(thread.threadData);\n } catch {}\n if (queuedMessages.length === 0) {\n delete data.queuedMessages;\n } else {\n data.queuedMessages = queuedMessages;\n }\n await updateThreadData(\n threadId,\n JSON.stringify(data),\n thread.title,\n thread.preview,\n thread.messageCount,\n );\n });\n}\n\nexport async function deleteThread(id: string): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const result = await client.execute({\n sql: `DELETE FROM chat_threads WHERE id = ?`,\n args: [id],\n });\n if (result.rowsAffected > 0) {\n emitChatThreadChange(id);\n return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/chat-threads/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,IAAI,YAAuC,CAAC;AAE5C;;;;;;;;;;;;;GAaG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA4B,CAAC;AAE7D,MAAM,UAAU,kBAAkB,CAChC,QAAgB,EAChB,EAAoB;IAEpB,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAC/B,gBAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,uEAAuE;IACvE,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5C,gBAAgB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,IAAkB,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;YAC3B,MAAM,MAAM,CAAC,OAAO,CAAC;;;;;;;0BAOD,OAAO,EAAE;uBACZ,OAAO,EAAE;uBACT,OAAO,EAAE;;OAEzB,CAAC,CAAC;QACL,CAAC,CAAC,EAAE,CAAC;IACP,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,UAAU,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAC1E,CAAC;AAsBD,SAAS,kBAAkB,CAAC,UAAmB,EAAE,QAAgB;IAC/D,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE;QAAE,OAAO,QAAQ,CAAC;IAC1E,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QAC/D,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;IACzD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,WAAW,CAAC,CAA0B;IAC7C,MAAM,UAAU,GAAI,CAAC,CAAC,WAAsB,IAAI,IAAI,CAAC;IACrD,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC5C,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAY;QAClB,UAAU,EAAE,CAAC,CAAC,WAAqB;QACnC,KAAK,EAAE,CAAC,CAAC,KAAe;QACxB,OAAO,EAAE,CAAC,CAAC,OAAiB;QAC5B,UAAU;QACV,YAAY,EAAE,kBAAkB,CAAC,UAAU,EAAE,WAAW,CAAC;QACzD,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;KAChC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,CAA0B;IAC9C,MAAM,UAAU,GAAG,CAAC,CAAC,WAAiC,CAAC;IACvD,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAG,kBAAkB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACjE,IAAI,YAAY,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAY;QAClB,KAAK,EAAE,CAAC,CAAC,KAAe;QACxB,OAAO,EAAE,CAAC,CAAC,OAAiB;QAC5B,YAAY;QACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;KAChC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,UAAkB,EAClB,IAAsC;IAEtC,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,UAAU,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;IAEhC,MAAM,MAAM,CAAC,OAAO,CAAC;QACnB,GAAG,EAAE,oJAAoJ;QACzJ,IAAI,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;KACxC,CAAC,CAAC;IAEH,OAAO;QACL,EAAE;QACF,UAAU;QACV,KAAK;QACL,OAAO,EAAE,EAAE;QACX,UAAU,EAAE,IAAI;QAChB,YAAY,EAAE,CAAC;QACf,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG;KACf,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAAU;IACxC,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QACpC,GAAG,EAAE,2HAA2H;QAChI,IAAI,EAAE,CAAC,EAAE,CAAC;KACX,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgB,EAChB,UAAkB,EAClB,IAAsB;IAEtB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,UAAU,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,MAAM,CAAC,OAAO,CAAC;QACnB,GAAG,EAAE,gJAAgJ;QACrJ,IAAI,EAAE;YACJ,EAAE;YACF,UAAU;YACV,KAAK;YACL,MAAM,CAAC,OAAO;YACd,MAAM,CAAC,UAAU;YACjB,MAAM,CAAC,YAAY;YACnB,GAAG;YACH,GAAG;SACJ;KACF,CAAC,CAAC;IACH,OAAO;QACL,EAAE;QACF,UAAU;QACV,KAAK;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG;KACf,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAAkB,EAClB,KAAK,GAAG,EAAE,EACV,MAAM,GAAG,CAAC;IAEV,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QACpC,GAAG,EAAE,4NAA4N;QACjO,IAAI,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC;KAClC,CAAC,CAAC;IACH,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;SAC3B,MAAM,CAAC,CAAC,CAAC,EAA0B,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAkB,EAClB,KAAa,EACb,KAAK,GAAG,EAAE;IAEV,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;IACzC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QACpC,GAAG,EAAE,8QAA8Q;QACnR,IAAI,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC;KACrD,CAAC,CAAC;IACH,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;SAC3B,MAAM,CAAC,CAAC,CAAC,EAA0B,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACvD,CAAC;AAQD,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAAU,EACV,UAAkB,EAClB,KAAa,EACb,OAAe,EACf,YAAoB,EACpB,UAAmC,EAAE;IAErC,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;IAC7C,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,cAAc,GAAG,UAAU,CAAC;QAChC,IAAI,gBAAgB,GAAG,YAAY,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,4BAA4B,CACzC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,EACnC,eAAe,CAAC,UAAU,CAAC,EAC3B;gBACE,8BAA8B,EAC5B,OAAO,CAAC,8BAA8B,IAAI,IAAI;gBAChD,4BAA4B,EAC1B,OAAO,CAAC,4BAA4B,IAAI,IAAI;aAC/C,CACF,CAAC;YACF,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnC,gBAAgB,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC5C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;QACzE,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;YAClC,GAAG,EAAE,oIAAoI;YACzI,IAAI,EAAE;gBACJ,cAAc;gBACd,KAAK;gBACL,OAAO;gBACP,gBAAgB;gBAChB,aAAa;gBACb,EAAE;gBACF,OAAO,CAAC,SAAS;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YAC5B,oBAAoB,CAAC,EAAE,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QAED,YAAY,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,gCAAgC,EAAE,oCAAoC,CACvE,CAAC;IACJ,CAAC;AACH,CAAC;AAOD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB;IAEhB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC3C,IAAI,IAAI,CAAC,UAAU,EAAE,UAAU;YAAE,OAAO,IAAI,CAAC,UAA8B,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,IAAsB;IAEtB,OAAO,kBAAkB,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,IAAI,GAA4B,EAAE,CAAC;QACvC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,MAAM,gBAAgB,CACpB,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EACpB,MAAM,CAAC,KAAK,EACZ,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,YAAY,CACpB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AASD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,QAAgB,EAChB,cAA+B;IAE/B,OAAO,kBAAkB,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,IAAI,GAA4B,EAAE,CAAC;QACvC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,cAAc,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACvC,CAAC;QACD,MAAM,gBAAgB,CACpB,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EACpB,MAAM,CAAC,KAAK,EACZ,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,YAAY,EACnB,EAAE,8BAA8B,EAAE,KAAK,EAAE,CAC1C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU;IAC3C,MAAM,WAAW,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAClC,GAAG,EAAE,uCAAuC;QAC5C,IAAI,EAAE,CAAC,EAAE,CAAC;KACX,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5B,oBAAoB,CAAC,EAAE,CAAC,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { getDbExec, intType } from \"../db/client.js\";\nimport {\n mergeThreadDataForClientSave,\n normalizeThreadRepository,\n} from \"../agent/thread-data-builder.js\";\nimport { emitChatThreadChange } from \"./emitter.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\n/**\n * Per-thread async mutex. Read-modify-write on the `thread_data` JSON blob\n * is not atomic at the DB level — two concurrent callers (e.g. the UI\n * persisting queued messages while `onRunComplete` appends agent output)\n * would both read the same row, each mutate it independently, and the\n * second write clobbers the first. Serializing on thread id inside this\n * process eliminates the race for the usual single-process deployment\n * while leaving straight reads and other thread-data-unrelated updates\n * untouched.\n *\n * Cross-process races are handled by `updateThreadData`, which performs a\n * compare-and-swap on `updated_at`, rereads the latest row on conflict, and\n * remerges message history before retrying.\n */\nconst _threadDataLocks = new Map<string, Promise<unknown>>();\n\nexport function withThreadDataLock<T>(\n threadId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const prev = _threadDataLocks.get(threadId) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n _threadDataLocks.set(threadId, next);\n // Use `.then(cleanup, cleanup)` (not `.finally`) so the rejection is\n // observed on this chained promise — otherwise any failure inside `fn`\n // triggers `unhandledRejection` on the discarded `finally()` return.\n // The caller still sees the rejection via `next`.\n const cleanup = () => {\n if (_threadDataLocks.get(threadId) === next) {\n _threadDataLocks.delete(threadId);\n }\n };\n next.then(cleanup, cleanup);\n return next as Promise<T>;\n}\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS chat_threads (\n id TEXT PRIMARY KEY,\n owner_email TEXT NOT NULL,\n title TEXT NOT NULL DEFAULT '',\n preview TEXT NOT NULL DEFAULT '',\n thread_data TEXT NOT NULL DEFAULT '{}',\n message_count ${intType()} NOT NULL DEFAULT 0,\n created_at ${intType()} NOT NULL,\n updated_at ${intType()} NOT NULL\n )\n `);\n })();\n }\n return _initPromise;\n}\n\nfunction generateId(): string {\n return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nexport interface ChatThread {\n id: string;\n ownerEmail: string;\n title: string;\n preview: string;\n threadData: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface ChatThreadSummary {\n id: string;\n title: string;\n preview: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nfunction deriveMessageCount(threadData: unknown, fallback: number): number {\n if (typeof threadData !== \"string\" || !threadData.trim()) return fallback;\n try {\n const repo = normalizeThreadRepository(JSON.parse(threadData));\n if (Array.isArray(repo.messages)) return repo.messages.length;\n } catch {\n // Keep the stored count if the JSON blob is malformed.\n }\n return fallback;\n}\n\nfunction rowToThread(r: Record<string, unknown>): ChatThread {\n const threadData = (r.thread_data as string) ?? \"{}\";\n const storedCount = Number(r.message_count);\n return {\n id: r.id as string,\n ownerEmail: r.owner_email as string,\n title: r.title as string,\n preview: r.preview as string,\n threadData,\n messageCount: deriveMessageCount(threadData, storedCount),\n createdAt: Number(r.created_at),\n updatedAt: Number(r.updated_at),\n };\n}\n\nfunction rowToSummary(r: Record<string, unknown>): ChatThreadSummary | null {\n const threadData = r.thread_data as string | undefined;\n const storedCount = Number(r.message_count);\n const messageCount = deriveMessageCount(threadData, storedCount);\n if (messageCount <= 0) return null;\n return {\n id: r.id as string,\n title: r.title as string,\n preview: r.preview as string,\n messageCount,\n createdAt: Number(r.created_at),\n updatedAt: Number(r.updated_at),\n };\n}\n\nexport async function createThread(\n ownerEmail: string,\n opts?: { id?: string; title?: string },\n): Promise<ChatThread> {\n await ensureTable();\n const client = getDbExec();\n const id = opts?.id ?? generateId();\n const now = Date.now();\n const title = opts?.title ?? \"\";\n\n await client.execute({\n sql: `INSERT INTO chat_threads (id, owner_email, title, preview, thread_data, message_count, created_at, updated_at) VALUES (?, ?, ?, '', '{}', 0, ?, ?)`,\n args: [id, ownerEmail, title, now, now],\n });\n\n return {\n id,\n ownerEmail,\n title,\n preview: \"\",\n threadData: \"{}\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n}\n\nexport async function getThread(id: string): Promise<ChatThread | null> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE id = ?`,\n args: [id],\n });\n if (rows.length === 0) return null;\n return rowToThread(rows[0]);\n}\n\nexport async function forkThread(\n sourceId: string,\n ownerEmail: string,\n opts?: { id?: string },\n): Promise<ChatThread | null> {\n const source = await getThread(sourceId);\n if (!source || source.ownerEmail !== ownerEmail) return null;\n const id = opts?.id ?? generateId();\n const now = Date.now();\n const title = source.title ? `${source.title} (fork)` : \"\";\n const client = getDbExec();\n await client.execute({\n sql: `INSERT INTO chat_threads (id, owner_email, title, preview, thread_data, message_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n title,\n source.preview,\n source.threadData,\n source.messageCount,\n now,\n now,\n ],\n });\n return {\n id,\n ownerEmail,\n title,\n preview: source.preview,\n threadData: source.threadData,\n messageCount: source.messageCount,\n createdAt: now,\n updatedAt: now,\n };\n}\n\nexport async function listThreads(\n ownerEmail: string,\n limit = 50,\n offset = 0,\n): Promise<ChatThreadSummary[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT id, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (message_count > 0 OR thread_data LIKE '%\"messages\"%') ORDER BY updated_at DESC LIMIT ? OFFSET ?`,\n args: [ownerEmail, limit, offset],\n });\n return rows\n .map((r) => rowToSummary(r))\n .filter((r): r is ChatThreadSummary => r !== null);\n}\n\nfunction escapeLike(s: string): string {\n return s.replace(/([\\\\%_])/g, \"\\\\$1\");\n}\n\nexport async function searchThreads(\n ownerEmail: string,\n query: string,\n limit = 50,\n): Promise<ChatThreadSummary[]> {\n await ensureTable();\n const client = getDbExec();\n const pattern = `%${escapeLike(query)}%`;\n const { rows } = await client.execute({\n sql: `SELECT id, title, preview, thread_data, message_count, created_at, updated_at FROM chat_threads WHERE owner_email = ? AND (message_count > 0 OR thread_data LIKE '%\"messages\"%') AND (title LIKE ? OR preview LIKE ? OR thread_data LIKE ?) ORDER BY updated_at DESC LIMIT ?`,\n args: [ownerEmail, pattern, pattern, pattern, limit],\n });\n return rows\n .map((r) => rowToSummary(r))\n .filter((r): r is ChatThreadSummary => r !== null);\n}\n\nexport interface UpdateThreadDataOptions {\n preserveExistingQueuedMessages?: boolean;\n preserveExistingTopLevelKeys?: boolean;\n maxAttempts?: number;\n}\n\nfunction parseThreadData(value: string): any {\n try {\n return JSON.parse(value || \"{}\");\n } catch {\n return {};\n }\n}\n\nexport async function updateThreadData(\n id: string,\n threadData: string,\n title: string,\n preview: string,\n messageCount: number,\n options: UpdateThreadDataOptions = {},\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const maxAttempts = options.maxAttempts ?? 5;\n let lastConflict = false;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const current = await getThread(id);\n if (!current) return;\n\n let nextThreadData = threadData;\n let nextMessageCount = messageCount;\n try {\n const merged = mergeThreadDataForClientSave(\n parseThreadData(current.threadData),\n parseThreadData(threadData),\n {\n preserveExistingQueuedMessages:\n options.preserveExistingQueuedMessages ?? true,\n preserveExistingTopLevelKeys:\n options.preserveExistingTopLevelKeys ?? true,\n },\n );\n nextThreadData = JSON.stringify(merged);\n if (Array.isArray(merged.messages)) {\n nextMessageCount = merged.messages.length;\n }\n } catch {\n // Keep the caller's serialized value if either JSON blob is malformed.\n }\n\n const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);\n const result = await client.execute({\n sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ? AND updated_at = ?`,\n args: [\n nextThreadData,\n title,\n preview,\n nextMessageCount,\n nextUpdatedAt,\n id,\n current.updatedAt,\n ],\n });\n\n if (result.rowsAffected > 0) {\n emitChatThreadChange(id);\n return;\n }\n\n lastConflict = true;\n await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)));\n }\n\n if (lastConflict) {\n throw new Error(\n `Failed to update chat thread ${id} after concurrent write conflicts.`,\n );\n }\n}\n\nexport interface ThreadEngineMeta {\n engineName: string;\n model: string;\n}\n\n/**\n * Read the engine pinned to a thread (stored in thread_data JSON).\n * Returns null if no engine is pinned.\n */\nexport async function getThreadEngineMeta(\n threadId: string,\n): Promise<ThreadEngineMeta | null> {\n const thread = await getThread(threadId);\n if (!thread?.threadData) return null;\n try {\n const data = JSON.parse(thread.threadData);\n if (data.engineMeta?.engineName) return data.engineMeta as ThreadEngineMeta;\n } catch {}\n return null;\n}\n\n/**\n * Pin an engine to a thread by storing engineMeta in thread_data JSON.\n * Does not change messages, title, or preview.\n */\nexport async function setThreadEngineMeta(\n threadId: string,\n meta: ThreadEngineMeta,\n): Promise<void> {\n return withThreadDataLock(threadId, async () => {\n const thread = await getThread(threadId);\n if (!thread) return;\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(thread.threadData);\n } catch {}\n data.engineMeta = meta;\n await updateThreadData(\n threadId,\n JSON.stringify(data),\n thread.title,\n thread.preview,\n thread.messageCount,\n );\n });\n}\n\nexport interface QueuedMessage {\n id: string;\n text: string;\n images?: string[];\n references?: unknown[];\n}\n\n/**\n * Persist the user's queued (not-yet-sent) messages onto the thread.\n * Stored in thread_data JSON so it survives reloads without a schema\n * change. Safe to call often — the frontend debounces writes.\n */\nexport async function setThreadQueuedMessages(\n threadId: string,\n queuedMessages: QueuedMessage[],\n): Promise<void> {\n return withThreadDataLock(threadId, async () => {\n const thread = await getThread(threadId);\n if (!thread) return;\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(thread.threadData);\n } catch {}\n if (queuedMessages.length === 0) {\n delete data.queuedMessages;\n } else {\n data.queuedMessages = queuedMessages;\n }\n await updateThreadData(\n threadId,\n JSON.stringify(data),\n thread.title,\n thread.preview,\n thread.messageCount,\n { preserveExistingQueuedMessages: false },\n );\n });\n}\n\nexport async function deleteThread(id: string): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const result = await client.execute({\n sql: `DELETE FROM chat_threads WHERE id = ?`,\n args: [id],\n });\n if (result.rowsAffected > 0) {\n emitChatThreadChange(id);\n return true;\n }\n return false;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"AgentPanel.d.ts","sourceRoot":"","sources":["../../src/client/AgentPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KASN,MAAM,OAAO,CAAC;AAwCf,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AA6J7D,MAAM,WAAW,oBAAoB;IACnC,mGAAmG;IACnG,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,mEAAmE;IACnE,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,wEAAwE;IACxE,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACzC;AAuBD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,kBAAkB,EAClB,eAAe,CAChB;IACC,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6GAA6G;IAC7G,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,iFAAiF;IACjF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6HAA6H;IAC7H,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,oBAAoB,CAAC;CACnC;AAg1CD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,2CAgBhD;AAID,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,gDAAgD;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;yDACqD;IACrD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,cAAsC,EACtC,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,QAAkB,EAClB,WAAmB,EACnB,aAAqB,GACtB,EAAE,iBAAiB,2CAiWnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,SAgB7B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAuBtE"}
1
+ {"version":3,"file":"AgentPanel.d.ts","sourceRoot":"","sources":["../../src/client/AgentPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KASN,MAAM,OAAO,CAAC;AAwCf,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AA6J7D,MAAM,WAAW,oBAAoB;IACnC,mGAAmG;IACnG,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,mEAAmE;IACnE,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,wEAAwE;IACxE,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACzC;AAuBD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,kBAAkB,EAClB,eAAe,CAChB;IACC,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6GAA6G;IAC7G,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,iFAAiF;IACjF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6HAA6H;IAC7H,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,oBAAoB,CAAC;CACnC;AAs1CD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,2CAgBhD;AAID,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,gDAAgD;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;yDACqD;IACrD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,cAAsC,EACtC,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,QAAkB,EAClB,WAAmB,EACnB,aAAqB,GACtB,EAAE,iBAAiB,2CAiWnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,SAgB7B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAuBtE"}
@@ -309,7 +309,14 @@ function AgentPanelInner({ defaultMode = "chat", className, apiUrl, emptyStateTe
309
309
  const codeUnavailableSecondaryCtaLabel = codeAccess?.unavailableSecondaryCtaLabel ?? "Use Builder";
310
310
  const codeUnavailableSecondaryCtaHref = codeAccess?.unavailableSecondaryCtaHref;
311
311
  const canUseCodeTools = isDevMode && codeAccessEnabled;
312
- const showCliMode = isDevMode || !codeAccessEnabled;
312
+ // Hide the CLI tab when embedded in the Builder.io frame — code editing
313
+ // there happens via Builder, and the CLI panel only offers a Download
314
+ // Desktop CTA, which adds clutter without value.
315
+ const showCliMode = (isDevMode || !codeAccessEnabled) && !isInFrame();
316
+ useEffect(() => {
317
+ if (mode === "cli" && !showCliMode)
318
+ switchMode("chat");
319
+ }, [mode, showCliMode, switchMode]);
313
320
  // Notify frame when dev mode changes — use both a local CustomEvent (for
314
321
  // when AgentPanel is rendered directly in the frame) AND postMessage (for
315
322
  // when AgentPanel is inside the iframe and needs to cross the boundary).