twinny 0.0.0-dev.260525150705

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/README.zh-CN.md +252 -0
  4. package/configs/banner.png +0 -0
  5. package/configs/logo.png +0 -0
  6. package/dist/app/caffeinate.d.ts +28 -0
  7. package/dist/app/caffeinate.js +96 -0
  8. package/dist/app/caffeinate.js.map +1 -0
  9. package/dist/app/daemon.d.ts +1 -0
  10. package/dist/app/daemon.js +44 -0
  11. package/dist/app/daemon.js.map +1 -0
  12. package/dist/app/lark-assets.d.ts +25 -0
  13. package/dist/app/lark-assets.js +108 -0
  14. package/dist/app/lark-assets.js.map +1 -0
  15. package/dist/app/startup-probe.d.ts +17 -0
  16. package/dist/app/startup-probe.js +90 -0
  17. package/dist/app/startup-probe.js.map +1 -0
  18. package/dist/app/wiring.d.ts +122 -0
  19. package/dist/app/wiring.js +694 -0
  20. package/dist/app/wiring.js.map +1 -0
  21. package/dist/cli/commands.d.ts +1 -0
  22. package/dist/cli/commands.js +47 -0
  23. package/dist/cli/commands.js.map +1 -0
  24. package/dist/cli/install-wizard.d.ts +41 -0
  25. package/dist/cli/install-wizard.js +629 -0
  26. package/dist/cli/install-wizard.js.map +1 -0
  27. package/dist/codex/appserver.d.ts +109 -0
  28. package/dist/codex/appserver.js +308 -0
  29. package/dist/codex/appserver.js.map +1 -0
  30. package/dist/codex/goal.d.ts +64 -0
  31. package/dist/codex/goal.js +433 -0
  32. package/dist/codex/goal.js.map +1 -0
  33. package/dist/codex/index.d.ts +6 -0
  34. package/dist/codex/index.js +7 -0
  35. package/dist/codex/index.js.map +1 -0
  36. package/dist/codex/protocol.d.ts +95 -0
  37. package/dist/codex/protocol.js +205 -0
  38. package/dist/codex/protocol.js.map +1 -0
  39. package/dist/codex/thread-name.d.ts +3 -0
  40. package/dist/codex/thread-name.js +27 -0
  41. package/dist/codex/thread-name.js.map +1 -0
  42. package/dist/codex/thread.d.ts +76 -0
  43. package/dist/codex/thread.js +80 -0
  44. package/dist/codex/thread.js.map +1 -0
  45. package/dist/codex/turn.d.ts +166 -0
  46. package/dist/codex/turn.js +746 -0
  47. package/dist/codex/turn.js.map +1 -0
  48. package/dist/config/bootstrap.d.ts +14 -0
  49. package/dist/config/bootstrap.js +56 -0
  50. package/dist/config/bootstrap.js.map +1 -0
  51. package/dist/config/index.d.ts +4 -0
  52. package/dist/config/index.js +5 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/config/loader.d.ts +49 -0
  55. package/dist/config/loader.js +467 -0
  56. package/dist/config/loader.js.map +1 -0
  57. package/dist/config/paths.d.ts +11 -0
  58. package/dist/config/paths.js +43 -0
  59. package/dist/config/paths.js.map +1 -0
  60. package/dist/config/secrets.d.ts +33 -0
  61. package/dist/config/secrets.js +85 -0
  62. package/dist/config/secrets.js.map +1 -0
  63. package/dist/conversation/manager.d.ts +701 -0
  64. package/dist/conversation/manager.js +7673 -0
  65. package/dist/conversation/manager.js.map +1 -0
  66. package/dist/conversation/queue.d.ts +8 -0
  67. package/dist/conversation/queue.js +28 -0
  68. package/dist/conversation/queue.js.map +1 -0
  69. package/dist/conversation/routing.d.ts +11 -0
  70. package/dist/conversation/routing.js +55 -0
  71. package/dist/conversation/routing.js.map +1 -0
  72. package/dist/errors.d.ts +6 -0
  73. package/dist/errors.js +17 -0
  74. package/dist/errors.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.js +4 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/lark/auth.d.ts +41 -0
  79. package/dist/lark/auth.js +132 -0
  80. package/dist/lark/auth.js.map +1 -0
  81. package/dist/lark/browser-auth.d.ts +68 -0
  82. package/dist/lark/browser-auth.js +258 -0
  83. package/dist/lark/browser-auth.js.map +1 -0
  84. package/dist/lark/cards.d.ts +140 -0
  85. package/dist/lark/cards.js +1150 -0
  86. package/dist/lark/cards.js.map +1 -0
  87. package/dist/lark/contact.d.ts +41 -0
  88. package/dist/lark/contact.js +122 -0
  89. package/dist/lark/contact.js.map +1 -0
  90. package/dist/lark/events.d.ts +65 -0
  91. package/dist/lark/events.js +218 -0
  92. package/dist/lark/events.js.map +1 -0
  93. package/dist/lark/files.d.ts +36 -0
  94. package/dist/lark/files.js +191 -0
  95. package/dist/lark/files.js.map +1 -0
  96. package/dist/lark/filters.d.ts +73 -0
  97. package/dist/lark/filters.js +678 -0
  98. package/dist/lark/filters.js.map +1 -0
  99. package/dist/lark/index.d.ts +10 -0
  100. package/dist/lark/index.js +11 -0
  101. package/dist/lark/index.js.map +1 -0
  102. package/dist/lark/messages.d.ts +87 -0
  103. package/dist/lark/messages.js +428 -0
  104. package/dist/lark/messages.js.map +1 -0
  105. package/dist/lark/openapi.d.ts +58 -0
  106. package/dist/lark/openapi.js +206 -0
  107. package/dist/lark/openapi.js.map +1 -0
  108. package/dist/lark/redactor.d.ts +5 -0
  109. package/dist/lark/redactor.js +68 -0
  110. package/dist/lark/redactor.js.map +1 -0
  111. package/dist/lark/types.d.ts +49 -0
  112. package/dist/lark/types.js +18 -0
  113. package/dist/lark/types.js.map +1 -0
  114. package/dist/launchd/install.d.ts +22 -0
  115. package/dist/launchd/install.js +114 -0
  116. package/dist/launchd/install.js.map +1 -0
  117. package/dist/launchd/plist.d.ts +10 -0
  118. package/dist/launchd/plist.js +61 -0
  119. package/dist/launchd/plist.js.map +1 -0
  120. package/dist/lock/index.d.ts +20 -0
  121. package/dist/lock/index.js +74 -0
  122. package/dist/lock/index.js.map +1 -0
  123. package/dist/main.d.ts +2 -0
  124. package/dist/main.js +11 -0
  125. package/dist/main.js.map +1 -0
  126. package/dist/markdown.d.ts +12 -0
  127. package/dist/markdown.js +149 -0
  128. package/dist/markdown.js.map +1 -0
  129. package/dist/observability/health.d.ts +25 -0
  130. package/dist/observability/health.js +187 -0
  131. package/dist/observability/health.js.map +1 -0
  132. package/dist/observability/logs.d.ts +10 -0
  133. package/dist/observability/logs.js +34 -0
  134. package/dist/observability/logs.js.map +1 -0
  135. package/dist/observability/system-notifications.d.ts +25 -0
  136. package/dist/observability/system-notifications.js +33 -0
  137. package/dist/observability/system-notifications.js.map +1 -0
  138. package/dist/profiles/guest.d.ts +19 -0
  139. package/dist/profiles/guest.js +241 -0
  140. package/dist/profiles/guest.js.map +1 -0
  141. package/dist/profiles/index.d.ts +5 -0
  142. package/dist/profiles/index.js +14 -0
  143. package/dist/profiles/index.js.map +1 -0
  144. package/dist/profiles/owner.d.ts +1 -0
  145. package/dist/profiles/owner.js +6 -0
  146. package/dist/profiles/owner.js.map +1 -0
  147. package/dist/store/db.d.ts +10 -0
  148. package/dist/store/db.js +29 -0
  149. package/dist/store/db.js.map +1 -0
  150. package/dist/store/index.d.ts +3 -0
  151. package/dist/store/index.js +4 -0
  152. package/dist/store/index.js.map +1 -0
  153. package/dist/store/migrations.d.ts +13 -0
  154. package/dist/store/migrations.js +79 -0
  155. package/dist/store/migrations.js.map +1 -0
  156. package/dist/store/repositories.d.ts +227 -0
  157. package/dist/store/repositories.js +1384 -0
  158. package/dist/store/repositories.js.map +1 -0
  159. package/dist/telemetry/client.d.ts +60 -0
  160. package/dist/telemetry/client.js +204 -0
  161. package/dist/telemetry/client.js.map +1 -0
  162. package/dist/telemetry/hash.d.ts +1 -0
  163. package/dist/telemetry/hash.js +8 -0
  164. package/dist/telemetry/hash.js.map +1 -0
  165. package/dist/telemetry/index.d.ts +4 -0
  166. package/dist/telemetry/index.js +5 -0
  167. package/dist/telemetry/index.js.map +1 -0
  168. package/dist/telemetry/posthog.d.ts +27 -0
  169. package/dist/telemetry/posthog.js +45 -0
  170. package/dist/telemetry/posthog.js.map +1 -0
  171. package/dist/telemetry/reporter.d.ts +13 -0
  172. package/dist/telemetry/reporter.js +29 -0
  173. package/dist/telemetry/reporter.js.map +1 -0
  174. package/dist/types.d.ts +330 -0
  175. package/dist/types.js +9 -0
  176. package/dist/types.js.map +1 -0
  177. package/dist/version.d.ts +1 -0
  178. package/dist/version.js +1 -0
  179. package/dist/version.js.map +1 -0
  180. package/dist/version.json +3 -0
  181. package/dist/workspace/index.d.ts +2 -0
  182. package/dist/workspace/index.js +3 -0
  183. package/dist/workspace/index.js.map +1 -0
  184. package/dist/workspace/manager.d.ts +14 -0
  185. package/dist/workspace/manager.js +69 -0
  186. package/dist/workspace/manager.js.map +1 -0
  187. package/dist/workspace/slug.d.ts +8 -0
  188. package/dist/workspace/slug.js +59 -0
  189. package/dist/workspace/slug.js.map +1 -0
  190. package/migrations/0001_initial.sql +102 -0
  191. package/package.json +85 -0
@@ -0,0 +1,1384 @@
1
+ import path from "node:path";
2
+ import { TwinnyError } from "../errors.js";
3
+ import { assertValidConversationKey, createGroupConversationKey, createP2PConversationKey } from "../workspace/slug.js";
4
+ export class ConversationRepository {
5
+ db;
6
+ now;
7
+ insertConversation;
8
+ selectByConversationKey;
9
+ selectByTypeAndChatId;
10
+ selectByCodexThreadId;
11
+ selectAll;
12
+ updateSettings;
13
+ updateThread;
14
+ markCodexThreadRollout;
15
+ deleteByKey;
16
+ upsertCodexThreadStatement;
17
+ selectCodexThreadById;
18
+ selectCodexThreadByConversationAndLarkThread;
19
+ replaceCodexThreadForLarkThreadStatement;
20
+ updateCodexThreadUsageStatement;
21
+ updateCodexThreadCardStatement;
22
+ updateCodexThreadModelSettingsStatement;
23
+ updateCodexThreadNameStatement;
24
+ updateCodexThreadModeStatement;
25
+ updateCodexThreadStatusStatement;
26
+ updateCodexThreadGoalStatusStatement;
27
+ selectCodexThreadWorkStats;
28
+ selectCodexThreadStatusStats;
29
+ selectConversationStatusStats;
30
+ insertLarkMessageStatement;
31
+ selectLarkMessageById;
32
+ selectLarkMessageByEventId;
33
+ selectLarkMessageUsageTargetForTurn;
34
+ selectLatestSteeredLarkMessageForTurn;
35
+ selectUnfinishedLarkMessages;
36
+ updateLarkMessageUsageStatement;
37
+ updateLarkMessageProcessingStatement;
38
+ updateLarkMessageSteeredStatement;
39
+ updateLarkMessageQueuedStatement;
40
+ updateQueuedLarkMessageStatement;
41
+ updateLarkMessageSideMetadataStatement;
42
+ updateLarkMessageRecalledStatement;
43
+ updateLarkMessageCompletedStatement;
44
+ updateLarkMessageFailedStatement;
45
+ updateLarkMessageInterruptedStatement;
46
+ updateLarkMessageClearedStatement;
47
+ constructor(db, options = {}) {
48
+ this.db = db;
49
+ this.now = options.now ?? Date.now;
50
+ this.insertConversation = this.db.prepare(`
51
+ INSERT INTO conversations (
52
+ conversation_key,
53
+ type,
54
+ chat_id,
55
+ name,
56
+ response_mode,
57
+ profile,
58
+ thread_id,
59
+ workspace,
60
+ profile_codex_home,
61
+ created_at,
62
+ updated_at
63
+ ) VALUES (
64
+ @conversationKey,
65
+ @type,
66
+ @chatId,
67
+ @name,
68
+ @responseMode,
69
+ @profile,
70
+ @codexThreadId,
71
+ @workspace,
72
+ @profileCodexHome,
73
+ @createdAt,
74
+ @updatedAt
75
+ )
76
+ `);
77
+ this.selectByConversationKey = this.db.prepare(`
78
+ SELECT * FROM conversations WHERE conversation_key = ?
79
+ `);
80
+ this.selectByTypeAndChatId = this.db.prepare(`
81
+ SELECT * FROM conversations WHERE type = ? AND chat_id = ?
82
+ `);
83
+ this.selectByCodexThreadId = this.db.prepare(`
84
+ SELECT * FROM conversations WHERE thread_id = ?
85
+ `);
86
+ this.selectAll = this.db.prepare(`
87
+ SELECT * FROM conversations ORDER BY id ASC
88
+ `);
89
+ this.updateSettings = this.db.prepare(`
90
+ UPDATE conversations
91
+ SET type = ?,
92
+ name = ?,
93
+ response_mode = ?,
94
+ updated_at = ?
95
+ WHERE conversation_key = ?
96
+ `);
97
+ this.updateThread = this.db.prepare(`
98
+ UPDATE conversations
99
+ SET thread_id = ?,
100
+ profile = ?,
101
+ profile_codex_home = ?,
102
+ workspace = ?,
103
+ updated_at = ?
104
+ WHERE conversation_key = ?
105
+ `);
106
+ this.markCodexThreadRollout = this.db.prepare(`
107
+ UPDATE threads
108
+ SET thread_has_rollout = 1,
109
+ updated_at = ?
110
+ WHERE conversation_key = ?
111
+ AND thread_id = ?
112
+ `);
113
+ this.deleteByKey = this.db.prepare(`
114
+ DELETE FROM conversations WHERE conversation_key = ?
115
+ `);
116
+ this.upsertCodexThreadStatement = this.db.prepare(`
117
+ INSERT INTO threads (
118
+ thread_id,
119
+ conversation_key,
120
+ name,
121
+ lark_thread_id,
122
+ profile,
123
+ model,
124
+ effort,
125
+ forked_from_thread_id,
126
+ forked_at,
127
+ thread_has_rollout,
128
+ total_tokens,
129
+ token_usage_json,
130
+ created_at,
131
+ updated_at
132
+ ) VALUES (
133
+ @codexThreadId,
134
+ @conversationKey,
135
+ COALESCE(@name, '新会话'),
136
+ @larkThreadId,
137
+ @profile,
138
+ @model,
139
+ @effort,
140
+ @forkedFromCodexThreadId,
141
+ @forkedAt,
142
+ @codexThreadHasRollout,
143
+ @totalTokens,
144
+ @tokenUsageJson,
145
+ @createdAt,
146
+ @updatedAt
147
+ )
148
+ ON CONFLICT(thread_id) DO UPDATE SET
149
+ conversation_key = excluded.conversation_key,
150
+ name = COALESCE(@name, threads.name),
151
+ lark_thread_id = COALESCE(excluded.lark_thread_id, threads.lark_thread_id),
152
+ profile = excluded.profile,
153
+ model = COALESCE(excluded.model, threads.model),
154
+ effort = COALESCE(excluded.effort, threads.effort),
155
+ forked_from_thread_id = COALESCE(excluded.forked_from_thread_id, threads.forked_from_thread_id),
156
+ forked_at = COALESCE(excluded.forked_at, threads.forked_at),
157
+ thread_has_rollout = CASE
158
+ WHEN threads.thread_has_rollout = 1 OR excluded.thread_has_rollout = 1 THEN 1
159
+ ELSE 0
160
+ END,
161
+ updated_at = excluded.updated_at
162
+ `);
163
+ this.selectCodexThreadById = this.db.prepare(`
164
+ SELECT * FROM threads WHERE thread_id = ?
165
+ `);
166
+ this.selectCodexThreadByConversationAndLarkThread = this.db.prepare(`
167
+ SELECT * FROM threads
168
+ WHERE conversation_key = ?
169
+ AND lark_thread_id = ?
170
+ `);
171
+ this.replaceCodexThreadForLarkThreadStatement = this.db.prepare(`
172
+ UPDATE threads
173
+ SET thread_id = @codexThreadId,
174
+ profile = @profile,
175
+ model = @model,
176
+ effort = @effort,
177
+ input_tokens = 0,
178
+ output_tokens = 0,
179
+ cached_input_tokens = 0,
180
+ reasoning_output_tokens = 0,
181
+ total_tokens = 0,
182
+ context_tokens = 0,
183
+ context_window = 0,
184
+ thread_has_rollout = @codexThreadHasRollout,
185
+ token_usage_json = '{}',
186
+ goal_status = 'none',
187
+ goal_updated_at = NULL,
188
+ updated_at = @updatedAt
189
+ WHERE conversation_key = @conversationKey
190
+ AND lark_thread_id = @larkThreadId
191
+ `);
192
+ this.updateCodexThreadUsageStatement = this.db.prepare(`
193
+ INSERT INTO threads (
194
+ thread_id,
195
+ conversation_key,
196
+ name,
197
+ lark_thread_id,
198
+ profile,
199
+ model,
200
+ effort,
201
+ input_tokens,
202
+ output_tokens,
203
+ cached_input_tokens,
204
+ reasoning_output_tokens,
205
+ total_tokens,
206
+ context_tokens,
207
+ context_window,
208
+ thread_has_rollout,
209
+ token_usage_json,
210
+ created_at,
211
+ updated_at
212
+ ) VALUES (
213
+ @codexThreadId,
214
+ @conversationKey,
215
+ COALESCE(@name, '新会话'),
216
+ NULL,
217
+ @profile,
218
+ @model,
219
+ @effort,
220
+ @inputTokens,
221
+ @outputTokens,
222
+ @cachedInputTokens,
223
+ @reasoningOutputTokens,
224
+ @totalTokens,
225
+ @contextTokens,
226
+ @contextWindow,
227
+ 1,
228
+ @tokenUsageJson,
229
+ @createdAt,
230
+ @updatedAt
231
+ )
232
+ ON CONFLICT(thread_id) DO UPDATE SET
233
+ conversation_key = excluded.conversation_key,
234
+ name = COALESCE(@name, threads.name),
235
+ profile = excluded.profile,
236
+ model = COALESCE(excluded.model, threads.model),
237
+ effort = COALESCE(excluded.effort, threads.effort),
238
+ input_tokens = excluded.input_tokens,
239
+ output_tokens = excluded.output_tokens,
240
+ cached_input_tokens = excluded.cached_input_tokens,
241
+ reasoning_output_tokens = excluded.reasoning_output_tokens,
242
+ total_tokens = excluded.total_tokens,
243
+ context_tokens = excluded.context_tokens,
244
+ context_window = excluded.context_window,
245
+ thread_has_rollout = 1,
246
+ token_usage_json = excluded.token_usage_json,
247
+ updated_at = excluded.updated_at
248
+ `);
249
+ this.updateCodexThreadCardStatement = this.db.prepare(`
250
+ INSERT INTO threads (
251
+ thread_id,
252
+ conversation_key,
253
+ name,
254
+ lark_thread_id,
255
+ profile,
256
+ model,
257
+ effort,
258
+ creator_open_id,
259
+ card_message_id,
260
+ thread_has_rollout,
261
+ created_at,
262
+ updated_at
263
+ ) VALUES (
264
+ @codexThreadId,
265
+ @conversationKey,
266
+ COALESCE(@name, '新会话'),
267
+ @larkThreadId,
268
+ @profile,
269
+ @model,
270
+ @effort,
271
+ @creatorOpenId,
272
+ @cardMessageId,
273
+ 0,
274
+ @createdAt,
275
+ @updatedAt
276
+ )
277
+ ON CONFLICT(thread_id) DO UPDATE SET
278
+ conversation_key = excluded.conversation_key,
279
+ name = COALESCE(@name, threads.name),
280
+ lark_thread_id = COALESCE(excluded.lark_thread_id, threads.lark_thread_id),
281
+ profile = excluded.profile,
282
+ model = COALESCE(excluded.model, threads.model),
283
+ effort = COALESCE(excluded.effort, threads.effort),
284
+ creator_open_id = COALESCE(excluded.creator_open_id, threads.creator_open_id),
285
+ card_message_id = COALESCE(excluded.card_message_id, threads.card_message_id),
286
+ updated_at = excluded.updated_at
287
+ `);
288
+ this.updateCodexThreadModelSettingsStatement = this.db.prepare(`
289
+ UPDATE threads
290
+ SET model = ?,
291
+ effort = ?,
292
+ updated_at = ?
293
+ WHERE thread_id = ?
294
+ `);
295
+ this.updateCodexThreadNameStatement = this.db.prepare(`
296
+ UPDATE threads
297
+ SET name = ?,
298
+ updated_at = ?
299
+ WHERE thread_id = ?
300
+ `);
301
+ this.updateCodexThreadModeStatement = this.db.prepare(`
302
+ UPDATE threads
303
+ SET mode = ?,
304
+ updated_at = ?
305
+ WHERE conversation_key = ?
306
+ AND thread_id = ?
307
+ `);
308
+ this.updateCodexThreadStatusStatement = this.db.prepare(`
309
+ UPDATE threads
310
+ SET status = ?,
311
+ updated_at = ?
312
+ WHERE conversation_key = ?
313
+ AND thread_id = ?
314
+ `);
315
+ this.updateCodexThreadGoalStatusStatement = this.db.prepare(`
316
+ UPDATE threads
317
+ SET goal_status = ?,
318
+ goal_updated_at = ?,
319
+ updated_at = ?
320
+ WHERE thread_id = ?
321
+ `);
322
+ this.selectCodexThreadWorkStats = this.db.prepare(`
323
+ SELECT
324
+ COUNT(*) AS turn_count,
325
+ COALESCE(SUM(CASE
326
+ WHEN terminal_at IS NOT NULL AND started_at IS NOT NULL AND terminal_at > started_at
327
+ THEN terminal_at - started_at
328
+ ELSE 0
329
+ END), 0) AS total_work_duration_ms
330
+ FROM (
331
+ SELECT
332
+ codex_turn_id,
333
+ MIN(processing_started_at) AS started_at,
334
+ MAX(COALESCE(completed_at, failed_at, cleared_at)) AS terminal_at
335
+ FROM lark_messages
336
+ WHERE thread_id = ?
337
+ AND codex_turn_id IS NOT NULL
338
+ AND processing_started_at IS NOT NULL
339
+ AND route_kind <> 'side_message'
340
+ GROUP BY codex_turn_id
341
+ )
342
+ `);
343
+ this.selectCodexThreadStatusStats = this.db.prepare(`
344
+ SELECT
345
+ (
346
+ SELECT COUNT(*)
347
+ FROM lark_messages
348
+ WHERE thread_id = @codexThreadId
349
+ AND route_kind IN ('message', 'goal_message', 'steered_message', 'queued_message', 'side_message')
350
+ ) AS user_message_count,
351
+ COUNT(*) AS turn_count,
352
+ COALESCE(SUM(CASE
353
+ WHEN terminal_at IS NOT NULL AND started_at IS NOT NULL AND terminal_at > started_at
354
+ THEN terminal_at - started_at
355
+ ELSE 0
356
+ END), 0) AS total_work_duration_ms
357
+ FROM (
358
+ SELECT
359
+ codex_turn_id,
360
+ MIN(processing_started_at) AS started_at,
361
+ MAX(COALESCE(completed_at, failed_at, cleared_at)) AS terminal_at
362
+ FROM lark_messages
363
+ WHERE thread_id = @codexThreadId
364
+ AND codex_turn_id IS NOT NULL
365
+ AND processing_started_at IS NOT NULL
366
+ AND route_kind <> 'side_message'
367
+ GROUP BY codex_turn_id
368
+ )
369
+ `);
370
+ this.selectConversationStatusStats = this.db.prepare(`
371
+ SELECT
372
+ (
373
+ SELECT COUNT(*)
374
+ FROM threads
375
+ WHERE conversation_key = @conversationKey
376
+ ) AS topic_count,
377
+ (
378
+ SELECT COUNT(*)
379
+ FROM lark_messages
380
+ WHERE conversation_key = @conversationKey
381
+ AND route_kind IN ('message', 'goal_message', 'steered_message', 'queued_message', 'side_message')
382
+ ) AS user_message_count,
383
+ (
384
+ SELECT COALESCE(SUM(input_tokens), 0)
385
+ FROM threads
386
+ WHERE conversation_key = @conversationKey
387
+ ) AS input_tokens,
388
+ (
389
+ SELECT COALESCE(SUM(output_tokens), 0)
390
+ FROM threads
391
+ WHERE conversation_key = @conversationKey
392
+ ) AS output_tokens,
393
+ (
394
+ SELECT COALESCE(SUM(cached_input_tokens), 0)
395
+ FROM threads
396
+ WHERE conversation_key = @conversationKey
397
+ ) AS cached_input_tokens,
398
+ (
399
+ SELECT COALESCE(SUM(reasoning_output_tokens), 0)
400
+ FROM threads
401
+ WHERE conversation_key = @conversationKey
402
+ ) AS reasoning_output_tokens,
403
+ (
404
+ SELECT COALESCE(SUM(total_tokens), 0)
405
+ FROM threads
406
+ WHERE conversation_key = @conversationKey
407
+ ) AS total_tokens,
408
+ COALESCE(SUM(CASE
409
+ WHEN terminal_at IS NOT NULL AND started_at IS NOT NULL AND terminal_at > started_at
410
+ THEN terminal_at - started_at
411
+ ELSE 0
412
+ END), 0) AS total_work_duration_ms
413
+ FROM (
414
+ SELECT
415
+ thread_id,
416
+ codex_turn_id,
417
+ MIN(processing_started_at) AS started_at,
418
+ MAX(COALESCE(completed_at, failed_at, cleared_at)) AS terminal_at
419
+ FROM lark_messages
420
+ WHERE conversation_key = @conversationKey
421
+ AND thread_id IS NOT NULL
422
+ AND codex_turn_id IS NOT NULL
423
+ AND processing_started_at IS NOT NULL
424
+ AND route_kind <> 'side_message'
425
+ GROUP BY thread_id, codex_turn_id
426
+ )
427
+ `);
428
+ this.insertLarkMessageStatement = this.db.prepare(`
429
+ INSERT INTO lark_messages (
430
+ lark_message_id,
431
+ event_id,
432
+ lark_user_id,
433
+ lark_group_id,
434
+ lark_thread_id,
435
+ conversation_key,
436
+ thread_id,
437
+ codex_turn_id,
438
+ route_kind,
439
+ status,
440
+ text,
441
+ lark_create_time,
442
+ side_id,
443
+ agent_card_message_id,
444
+ received_at,
445
+ updated_at,
446
+ raw_event_json
447
+ ) VALUES (
448
+ @larkMessageId,
449
+ @eventId,
450
+ @larkUserId,
451
+ @larkGroupId,
452
+ @larkThreadId,
453
+ @conversationKey,
454
+ @codexThreadId,
455
+ @codexTurnId,
456
+ @routeKind,
457
+ @status,
458
+ @text,
459
+ @larkCreateTime,
460
+ @sideId,
461
+ @agentCardMessageId,
462
+ @receivedAt,
463
+ @updatedAt,
464
+ @rawEventJson
465
+ )
466
+ ON CONFLICT(event_id) DO NOTHING
467
+ `);
468
+ this.selectLarkMessageById = this.db.prepare(`
469
+ SELECT * FROM lark_messages WHERE lark_message_id = ?
470
+ `);
471
+ this.selectLarkMessageByEventId = this.db.prepare(`
472
+ SELECT * FROM lark_messages
473
+ WHERE event_id = ?
474
+ ORDER BY id ASC
475
+ LIMIT 1
476
+ `);
477
+ this.selectLarkMessageUsageTargetForTurn = this.db.prepare(`
478
+ SELECT * FROM lark_messages
479
+ WHERE thread_id = ?
480
+ AND codex_turn_id = ?
481
+ AND lark_message_id IS NOT NULL
482
+ AND route_kind <> 'steered_message'
483
+ ORDER BY received_at ASC, id ASC
484
+ LIMIT 1
485
+ `);
486
+ this.selectLatestSteeredLarkMessageForTurn = this.db.prepare(`
487
+ SELECT * FROM lark_messages
488
+ WHERE thread_id = ?
489
+ AND codex_turn_id = ?
490
+ AND lark_message_id IS NOT NULL
491
+ AND route_kind = 'steered_message'
492
+ ORDER BY received_at DESC, id DESC
493
+ LIMIT 1
494
+ `);
495
+ this.selectUnfinishedLarkMessages = this.db.prepare(`
496
+ SELECT * FROM lark_messages
497
+ WHERE status IN ('processing', 'queued')
498
+ ORDER BY received_at ASC, id ASC
499
+ `);
500
+ this.updateLarkMessageUsageStatement = this.db.prepare(`
501
+ UPDATE lark_messages
502
+ SET input_tokens = @inputTokens,
503
+ output_tokens = @outputTokens,
504
+ cached_input_tokens = @cachedInputTokens,
505
+ reasoning_output_tokens = @reasoningOutputTokens,
506
+ token_usage_json = @tokenUsageJson,
507
+ updated_at = @updatedAt
508
+ WHERE lark_message_id = @larkMessageId
509
+ `);
510
+ this.updateLarkMessageProcessingStatement = this.db.prepare(`
511
+ UPDATE lark_messages
512
+ SET status = 'processing',
513
+ conversation_key = COALESCE(?, conversation_key),
514
+ thread_id = COALESCE(?, thread_id),
515
+ codex_turn_id = COALESCE(?, codex_turn_id),
516
+ processing_started_at = COALESCE(processing_started_at, ?),
517
+ updated_at = ?
518
+ WHERE lark_message_id = ?
519
+ `);
520
+ this.updateLarkMessageQueuedStatement = this.db.prepare(`
521
+ UPDATE lark_messages
522
+ SET route_kind = 'queued_message',
523
+ status = 'queued',
524
+ thread_id = NULL,
525
+ codex_turn_id = NULL,
526
+ updated_at = ?
527
+ WHERE lark_message_id = ?
528
+ `);
529
+ this.updateQueuedLarkMessageStatement = this.db.prepare(`
530
+ UPDATE lark_messages
531
+ SET text = ?,
532
+ raw_event_json = COALESCE(?, raw_event_json),
533
+ updated_at = ?
534
+ WHERE lark_message_id = ?
535
+ AND status = 'queued'
536
+ `);
537
+ this.updateLarkMessageSideMetadataStatement = this.db.prepare(`
538
+ UPDATE lark_messages
539
+ SET side_id = COALESCE(?, side_id),
540
+ agent_card_message_id = COALESCE(?, agent_card_message_id),
541
+ updated_at = ?
542
+ WHERE lark_message_id = ?
543
+ `);
544
+ this.updateLarkMessageRecalledStatement = this.db.prepare(`
545
+ UPDATE lark_messages
546
+ SET status = 'recalled',
547
+ updated_at = ?
548
+ WHERE lark_message_id = ?
549
+ AND status = 'queued'
550
+ `);
551
+ this.updateLarkMessageSteeredStatement = this.db.prepare(`
552
+ UPDATE lark_messages
553
+ SET status = 'steered',
554
+ conversation_key = COALESCE(?, conversation_key),
555
+ thread_id = COALESCE(?, thread_id),
556
+ codex_turn_id = COALESCE(?, codex_turn_id),
557
+ updated_at = ?
558
+ WHERE lark_message_id = ?
559
+ `);
560
+ this.updateLarkMessageCompletedStatement = this.db.prepare(`
561
+ UPDATE lark_messages
562
+ SET status = 'completed',
563
+ completed_at = COALESCE(completed_at, ?),
564
+ updated_at = ?
565
+ WHERE lark_message_id = ?
566
+ `);
567
+ this.updateLarkMessageFailedStatement = this.db.prepare(`
568
+ UPDATE lark_messages
569
+ SET status = 'failed',
570
+ failed_at = COALESCE(failed_at, ?),
571
+ updated_at = ?
572
+ WHERE lark_message_id = ?
573
+ `);
574
+ this.updateLarkMessageInterruptedStatement = this.db.prepare(`
575
+ UPDATE lark_messages
576
+ SET status = 'interrupted',
577
+ failed_at = COALESCE(failed_at, ?),
578
+ updated_at = ?
579
+ WHERE lark_message_id = ?
580
+ `);
581
+ this.updateLarkMessageClearedStatement = this.db.prepare(`
582
+ UPDATE lark_messages
583
+ SET status = 'cleared',
584
+ cleared_at = COALESCE(cleared_at, ?),
585
+ updated_at = ?
586
+ WHERE lark_message_id = ?
587
+ `);
588
+ }
589
+ create(input) {
590
+ const params = this.toInsertParams(input);
591
+ const create = this.db.transaction(() => {
592
+ this.insertConversation.run(params);
593
+ return this.requireByConversationKey(params.conversationKey);
594
+ });
595
+ return create();
596
+ }
597
+ getOrCreate(input) {
598
+ const params = this.toInsertParams(input);
599
+ const getOrCreate = this.db.transaction(() => {
600
+ const existing = this.getByConversationKey(params.conversationKey);
601
+ if (existing) {
602
+ return existing;
603
+ }
604
+ this.insertConversation.run(params);
605
+ return this.requireByConversationKey(params.conversationKey);
606
+ });
607
+ return getOrCreate();
608
+ }
609
+ getByConversationKey(conversationKey) {
610
+ assertValidConversationKey(conversationKey);
611
+ return mapConversationRow(this.selectByConversationKey.get(conversationKey));
612
+ }
613
+ getByTypeAndChatId(type, chatId) {
614
+ assertValidConversationType(type);
615
+ assertExpectedConversationKey(conversationKeyForTypeAndChatId(type, chatId), type, chatId);
616
+ return mapConversationRow(this.selectByTypeAndChatId.get(type, chatId));
617
+ }
618
+ getByCodexThreadId(codexThreadId) {
619
+ assertNonEmpty(codexThreadId, "codexThreadId");
620
+ return mapConversationRow(this.selectByCodexThreadId.get(codexThreadId));
621
+ }
622
+ list() {
623
+ return this.selectAll.all().map((row) => mapRequiredConversationRow(row));
624
+ }
625
+ updateThreadBinding(conversationKey, update) {
626
+ assertValidConversationKey(conversationKey);
627
+ assertNonEmpty(update.codexThreadId, "codexThreadId");
628
+ const updateProfile = resolveOptionalInputProfile(update);
629
+ if (updateProfile !== undefined) {
630
+ assertValidProfile(updateProfile);
631
+ }
632
+ const updateProfileCodexHome = update.profileCodexHome;
633
+ if (updateProfileCodexHome !== undefined) {
634
+ assertNonEmpty(updateProfileCodexHome, "profileCodexHome");
635
+ }
636
+ if (update.workspace !== undefined) {
637
+ assertAbsolutePath(update.workspace, "workspace");
638
+ }
639
+ const updateBinding = this.db.transaction(() => {
640
+ const existing = this.requireByConversationKey(conversationKey);
641
+ const profile = updateProfile ?? existing.profile;
642
+ const profileCodexHome = updateProfileCodexHome ?? existing.profileCodexHome;
643
+ const workspace = update.workspace ?? existing.workspace;
644
+ this.updateThread.run(update.codexThreadId, profile, profileCodexHome, workspace, this.now(), conversationKey);
645
+ return this.requireByConversationKey(conversationKey);
646
+ });
647
+ return updateBinding();
648
+ }
649
+ updateConversationSettings(conversationKey, update) {
650
+ assertValidConversationKey(conversationKey);
651
+ if (update.type !== undefined) {
652
+ assertValidConversationType(update.type);
653
+ }
654
+ if (update.name !== undefined) {
655
+ assertNonEmpty(update.name, "name");
656
+ }
657
+ if (update.responseMode !== undefined) {
658
+ assertValidResponseMode(update.responseMode);
659
+ }
660
+ const updateSettings = this.db.transaction(() => {
661
+ const existing = this.requireByConversationKey(conversationKey);
662
+ const type = update.type ?? existing.type;
663
+ assertExpectedConversationKey(conversationKey, type, existing.chatId);
664
+ const name = update.name ?? existing.name;
665
+ const responseMode = update.responseMode ?? existing.responseMode;
666
+ this.updateSettings.run(type, name, responseMode, this.now(), conversationKey);
667
+ return this.requireByConversationKey(conversationKey);
668
+ });
669
+ return updateSettings();
670
+ }
671
+ markThreadHasRollout(conversationKey, codexThreadId) {
672
+ assertValidConversationKey(conversationKey);
673
+ assertNonEmpty(codexThreadId, "codexThreadId");
674
+ const now = this.now();
675
+ this.markCodexThreadRollout.run(now, conversationKey, codexThreadId);
676
+ }
677
+ deleteByConversationKey(conversationKey) {
678
+ assertValidConversationKey(conversationKey);
679
+ const remove = this.db.transaction(() => this.deleteByKey.run(conversationKey).changes > 0);
680
+ return remove();
681
+ }
682
+ upsertCodexThread(input) {
683
+ validateCodexThreadInput(input);
684
+ const now = this.now();
685
+ const profile = resolveRequiredInputProfile(input);
686
+ this.upsertCodexThreadStatement.run({
687
+ codexThreadId: input.codexThreadId,
688
+ conversationKey: input.conversationKey,
689
+ name: input.name ?? null,
690
+ larkThreadId: input.larkThreadId ?? null,
691
+ profile,
692
+ model: input.model ?? null,
693
+ effort: input.effort ?? null,
694
+ forkedFromCodexThreadId: input.forkedFromCodexThreadId ?? null,
695
+ forkedAt: input.forkedAt ?? null,
696
+ codexThreadHasRollout: input.codexThreadHasRollout === true ? 1 : 0,
697
+ totalTokens: 0,
698
+ tokenUsageJson: "{}",
699
+ createdAt: now,
700
+ updatedAt: now
701
+ });
702
+ return this.requireCodexThreadById(input.codexThreadId);
703
+ }
704
+ getCodexThreadById(codexThreadId) {
705
+ assertNonEmpty(codexThreadId, "codexThreadId");
706
+ return mapCodexThreadRow(this.selectCodexThreadById.get(codexThreadId));
707
+ }
708
+ getCodexThreadByConversationAndLarkThread(conversationKey, larkThreadId) {
709
+ assertValidConversationKey(conversationKey);
710
+ assertNonEmpty(larkThreadId, "larkThreadId");
711
+ return mapCodexThreadRow(this.selectCodexThreadByConversationAndLarkThread.get(conversationKey, larkThreadId));
712
+ }
713
+ replaceCodexThreadForLarkThread(conversationKey, larkThreadId, update) {
714
+ const input = {
715
+ conversationKey,
716
+ larkThreadId,
717
+ codexThreadId: update.codexThreadId,
718
+ profile: update.profile,
719
+ model: update.model,
720
+ effort: update.effort,
721
+ codexThreadHasRollout: update.codexThreadHasRollout
722
+ };
723
+ validateReplaceCodexThreadForLarkThread(input);
724
+ const profile = resolveRequiredInputProfile(input);
725
+ const now = this.now();
726
+ const codexThreadHasRollout = input.codexThreadHasRollout === true ? 1 : 0;
727
+ const replace = this.db.transaction(() => {
728
+ const result = this.replaceCodexThreadForLarkThreadStatement.run({
729
+ ...input,
730
+ profile,
731
+ codexThreadHasRollout,
732
+ updatedAt: now
733
+ });
734
+ if (result.changes === 0) {
735
+ this.upsertCodexThreadStatement.run({
736
+ codexThreadId: input.codexThreadId,
737
+ conversationKey: input.conversationKey,
738
+ name: null,
739
+ larkThreadId: input.larkThreadId,
740
+ profile,
741
+ model: input.model ?? null,
742
+ effort: input.effort ?? null,
743
+ forkedFromCodexThreadId: null,
744
+ forkedAt: null,
745
+ codexThreadHasRollout,
746
+ totalTokens: 0,
747
+ tokenUsageJson: "{}",
748
+ createdAt: now,
749
+ updatedAt: now
750
+ });
751
+ }
752
+ return this.requireCodexThreadById(input.codexThreadId);
753
+ });
754
+ return replace();
755
+ }
756
+ updateCodexThreadTokenUsage(input) {
757
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
758
+ assertValidConversationKey(input.conversationKey);
759
+ const profile = resolveRequiredInputProfile(input);
760
+ assertValidProfile(profile);
761
+ assertNonNegativeFinite(input.inputTokens, "inputTokens");
762
+ assertNonNegativeFinite(input.outputTokens, "outputTokens");
763
+ assertNonNegativeFinite(input.cachedInputTokens, "cachedInputTokens");
764
+ assertNonNegativeFinite(input.reasoningOutputTokens, "reasoningOutputTokens");
765
+ if (!Number.isFinite(input.totalTokens) || input.totalTokens < 0) {
766
+ throw new TwinnyError("totalTokens must be a non-negative finite number", "CODEX_THREAD_TOKEN_USAGE_INVALID");
767
+ }
768
+ assertNonNegativeFinite(input.contextTokens, "contextTokens");
769
+ assertNonNegativeFinite(input.contextWindow, "contextWindow");
770
+ assertNonEmpty(input.tokenUsageJson, "tokenUsageJson");
771
+ const now = this.now();
772
+ this.updateCodexThreadUsageStatement.run({
773
+ codexThreadId: input.codexThreadId,
774
+ conversationKey: input.conversationKey,
775
+ name: null,
776
+ profile,
777
+ model: null,
778
+ effort: null,
779
+ inputTokens: Math.trunc(input.inputTokens),
780
+ outputTokens: Math.trunc(input.outputTokens),
781
+ cachedInputTokens: Math.trunc(input.cachedInputTokens),
782
+ reasoningOutputTokens: Math.trunc(input.reasoningOutputTokens),
783
+ totalTokens: Math.trunc(input.totalTokens),
784
+ contextTokens: Math.trunc(input.contextTokens),
785
+ contextWindow: Math.trunc(input.contextWindow),
786
+ tokenUsageJson: input.tokenUsageJson,
787
+ createdAt: now,
788
+ updatedAt: now
789
+ });
790
+ return this.requireCodexThreadById(input.codexThreadId);
791
+ }
792
+ updateCodexThreadCard(input) {
793
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
794
+ assertValidConversationKey(input.conversationKey);
795
+ const profile = resolveRequiredInputProfile(input);
796
+ assertValidProfile(profile);
797
+ if (input.model !== undefined) {
798
+ assertNonEmpty(input.model, "model");
799
+ }
800
+ if (input.effort !== undefined) {
801
+ assertNonEmpty(input.effort, "effort");
802
+ }
803
+ if (input.larkThreadId !== undefined) {
804
+ assertNonEmpty(input.larkThreadId, "larkThreadId");
805
+ }
806
+ if (input.name !== undefined) {
807
+ assertNonEmpty(input.name, "name");
808
+ }
809
+ if (input.creatorOpenId !== undefined) {
810
+ assertNonEmpty(input.creatorOpenId, "creatorOpenId");
811
+ }
812
+ if (input.cardMessageId !== undefined) {
813
+ assertNonEmpty(input.cardMessageId, "cardMessageId");
814
+ }
815
+ const now = this.now();
816
+ this.updateCodexThreadCardStatement.run({
817
+ codexThreadId: input.codexThreadId,
818
+ conversationKey: input.conversationKey,
819
+ name: input.name ?? null,
820
+ profile,
821
+ model: input.model ?? null,
822
+ effort: input.effort ?? null,
823
+ larkThreadId: input.larkThreadId ?? null,
824
+ creatorOpenId: input.creatorOpenId ?? null,
825
+ cardMessageId: input.cardMessageId ?? null,
826
+ createdAt: now,
827
+ updatedAt: now
828
+ });
829
+ return this.requireCodexThreadById(input.codexThreadId);
830
+ }
831
+ updateCodexThreadModelSettings(input) {
832
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
833
+ assertNonEmpty(input.model, "model");
834
+ assertNonEmpty(input.effort, "effort");
835
+ const result = this.updateCodexThreadModelSettingsStatement.run(input.model, input.effort, this.now(), input.codexThreadId);
836
+ if (result.changes === 0) {
837
+ throw new TwinnyError(`Codex thread ${input.codexThreadId} was not found`, "CODEX_THREAD_NOT_FOUND");
838
+ }
839
+ return this.requireCodexThreadById(input.codexThreadId);
840
+ }
841
+ updateCodexThreadName(codexThreadId, name) {
842
+ assertNonEmpty(codexThreadId, "codexThreadId");
843
+ assertNonEmpty(name, "name");
844
+ const result = this.updateCodexThreadNameStatement.run(name, this.now(), codexThreadId);
845
+ if (result.changes === 0) {
846
+ return undefined;
847
+ }
848
+ return this.requireCodexThreadById(codexThreadId);
849
+ }
850
+ updateCodexThreadMode(conversationKey, codexThreadId, mode) {
851
+ assertValidConversationKey(conversationKey);
852
+ assertNonEmpty(codexThreadId, "codexThreadId");
853
+ assertValidCodexThreadMode(mode);
854
+ const result = this.updateCodexThreadModeStatement.run(mode, this.now(), conversationKey, codexThreadId);
855
+ if (result.changes === 0) {
856
+ throw new TwinnyError(`Codex thread ${codexThreadId} was not found`, "CODEX_THREAD_NOT_FOUND");
857
+ }
858
+ return this.requireCodexThreadById(codexThreadId);
859
+ }
860
+ updateCodexThreadStatus(conversationKey, codexThreadId, status) {
861
+ assertValidConversationKey(conversationKey);
862
+ assertNonEmpty(codexThreadId, "codexThreadId");
863
+ assertValidCodexThreadStatus(status);
864
+ const result = this.updateCodexThreadStatusStatement.run(status, this.now(), conversationKey, codexThreadId);
865
+ if (result.changes === 0) {
866
+ throw new TwinnyError(`Codex thread ${codexThreadId} was not found`, "CODEX_THREAD_NOT_FOUND");
867
+ }
868
+ return this.requireCodexThreadById(codexThreadId);
869
+ }
870
+ updateCodexThreadGoalStatus(input) {
871
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
872
+ assertValidCodexThreadGoalStatus(input.goalStatus);
873
+ const now = this.now();
874
+ const goalUpdatedAt = input.goalStatus === "none" ? null : Math.trunc(input.goalUpdatedAt ?? now);
875
+ const result = this.updateCodexThreadGoalStatusStatement.run(input.goalStatus, goalUpdatedAt, now, input.codexThreadId);
876
+ if (result.changes === 0) {
877
+ throw new TwinnyError(`Codex thread ${input.codexThreadId} was not found`, "CODEX_THREAD_NOT_FOUND");
878
+ }
879
+ return this.requireCodexThreadById(input.codexThreadId);
880
+ }
881
+ clearCodexThreadGoalStatus(codexThreadId) {
882
+ return this.updateCodexThreadGoalStatus({ codexThreadId, goalStatus: "none" });
883
+ }
884
+ getCodexThreadWorkStats(codexThreadId) {
885
+ assertNonEmpty(codexThreadId, "codexThreadId");
886
+ const row = this.selectCodexThreadWorkStats.get(codexThreadId);
887
+ return {
888
+ turnCount: Math.trunc(row?.turn_count ?? 0),
889
+ totalWorkDurationMs: Math.trunc(row?.total_work_duration_ms ?? 0)
890
+ };
891
+ }
892
+ getCodexThreadStatusStats(codexThreadId) {
893
+ assertNonEmpty(codexThreadId, "codexThreadId");
894
+ const row = this.selectCodexThreadStatusStats.get({ codexThreadId });
895
+ return {
896
+ userMessageCount: Math.trunc(row?.user_message_count ?? 0),
897
+ turnCount: Math.trunc(row?.turn_count ?? 0),
898
+ totalWorkDurationMs: Math.trunc(row?.total_work_duration_ms ?? 0)
899
+ };
900
+ }
901
+ getConversationStatusStats(conversationKey) {
902
+ assertValidConversationKey(conversationKey);
903
+ const row = this.selectConversationStatusStats.get({ conversationKey });
904
+ return {
905
+ topicCount: Math.trunc(row?.topic_count ?? 0),
906
+ userMessageCount: Math.trunc(row?.user_message_count ?? 0),
907
+ inputTokens: Math.trunc(row?.input_tokens ?? 0),
908
+ outputTokens: Math.trunc(row?.output_tokens ?? 0),
909
+ cachedInputTokens: Math.trunc(row?.cached_input_tokens ?? 0),
910
+ reasoningOutputTokens: Math.trunc(row?.reasoning_output_tokens ?? 0),
911
+ totalTokens: Math.trunc(row?.total_tokens ?? 0),
912
+ totalWorkDurationMs: Math.trunc(row?.total_work_duration_ms ?? 0)
913
+ };
914
+ }
915
+ insertLarkMessage(input) {
916
+ validateLarkMessageInput(input);
917
+ const now = this.now();
918
+ this.insertLarkMessageStatement.run({
919
+ larkMessageId: input.larkMessageId ?? null,
920
+ eventId: input.eventId,
921
+ larkUserId: input.larkUserId,
922
+ larkGroupId: input.larkGroupId ?? null,
923
+ larkThreadId: input.larkThreadId ?? null,
924
+ conversationKey: input.conversationKey ?? null,
925
+ codexThreadId: input.codexThreadId ?? null,
926
+ codexTurnId: input.codexTurnId ?? null,
927
+ routeKind: input.routeKind,
928
+ status: input.status,
929
+ text: input.text,
930
+ larkCreateTime: input.larkCreateTime ?? null,
931
+ sideId: input.sideId ?? null,
932
+ agentCardMessageId: input.agentCardMessageId ?? null,
933
+ receivedAt: now,
934
+ updatedAt: now,
935
+ rawEventJson: input.rawEventJson ?? null
936
+ });
937
+ return this.requireLarkMessageByEventId(input.eventId);
938
+ }
939
+ getLarkMessageById(larkMessageId) {
940
+ assertNonEmpty(larkMessageId, "larkMessageId");
941
+ return mapLarkMessageRow(this.selectLarkMessageById.get(larkMessageId));
942
+ }
943
+ getLarkMessageByEventId(eventId) {
944
+ assertNonEmpty(eventId, "eventId");
945
+ return mapLarkMessageRow(this.selectLarkMessageByEventId.get(eventId));
946
+ }
947
+ getLarkMessageUsageTargetForTurn(codexThreadId, codexTurnId) {
948
+ assertNonEmpty(codexThreadId, "codexThreadId");
949
+ assertNonEmpty(codexTurnId, "codexTurnId");
950
+ return mapLarkMessageRow(this.selectLarkMessageUsageTargetForTurn.get(codexThreadId, codexTurnId));
951
+ }
952
+ getLatestSteeredLarkMessageForTurn(codexThreadId, codexTurnId) {
953
+ assertNonEmpty(codexThreadId, "codexThreadId");
954
+ assertNonEmpty(codexTurnId, "codexTurnId");
955
+ return mapLarkMessageRow(this.selectLatestSteeredLarkMessageForTurn.get(codexThreadId, codexTurnId));
956
+ }
957
+ listUnfinishedLarkMessages() {
958
+ return this.selectUnfinishedLarkMessages.all().map((row) => mapRequiredLarkMessageRow(row));
959
+ }
960
+ updateLarkMessageTokenUsage(input) {
961
+ assertNonEmpty(input.larkMessageId, "larkMessageId");
962
+ assertNonNegativeFinite(input.inputTokens, "inputTokens");
963
+ assertNonNegativeFinite(input.outputTokens, "outputTokens");
964
+ assertNonNegativeFinite(input.cachedInputTokens, "cachedInputTokens");
965
+ assertNonNegativeFinite(input.reasoningOutputTokens, "reasoningOutputTokens");
966
+ assertNonEmpty(input.tokenUsageJson, "tokenUsageJson");
967
+ const result = this.updateLarkMessageUsageStatement.run({
968
+ larkMessageId: input.larkMessageId,
969
+ inputTokens: Math.trunc(input.inputTokens),
970
+ outputTokens: Math.trunc(input.outputTokens),
971
+ cachedInputTokens: Math.trunc(input.cachedInputTokens),
972
+ reasoningOutputTokens: Math.trunc(input.reasoningOutputTokens),
973
+ tokenUsageJson: input.tokenUsageJson,
974
+ updatedAt: this.now()
975
+ });
976
+ return result.changes === 0 ? undefined : this.requireLarkMessageById(input.larkMessageId);
977
+ }
978
+ markLarkMessageQueued(larkMessageId) {
979
+ assertNonEmpty(larkMessageId, "larkMessageId");
980
+ this.updateLarkMessageQueuedStatement.run(this.now(), larkMessageId);
981
+ }
982
+ updateQueuedLarkMessage(larkMessageId, update) {
983
+ assertNonEmpty(larkMessageId, "larkMessageId");
984
+ const result = this.updateQueuedLarkMessageStatement.run(update.text, update.rawEventJson ?? null, this.now(), larkMessageId);
985
+ return result.changes > 0;
986
+ }
987
+ updateLarkMessageSideMetadata(larkMessageId, update) {
988
+ assertNonEmpty(larkMessageId, "larkMessageId");
989
+ const result = this.updateLarkMessageSideMetadataStatement.run(update.sideId ?? null, update.agentCardMessageId ?? null, this.now(), larkMessageId);
990
+ return result.changes > 0;
991
+ }
992
+ markLarkMessageRecalled(larkMessageId) {
993
+ assertNonEmpty(larkMessageId, "larkMessageId");
994
+ const result = this.updateLarkMessageRecalledStatement.run(this.now(), larkMessageId);
995
+ return result.changes > 0;
996
+ }
997
+ markLarkMessagesProcessing(larkMessageIds, update = {}) {
998
+ validateLarkMessageIds(larkMessageIds);
999
+ validateLarkMessageBindingUpdate(update);
1000
+ const now = this.now();
1001
+ const mark = this.db.transaction(() => {
1002
+ for (const messageId of larkMessageIds) {
1003
+ this.updateLarkMessageProcessingStatement.run(update.conversationKey ?? null, update.codexThreadId ?? null, update.codexTurnId ?? null, now, now, messageId);
1004
+ }
1005
+ });
1006
+ mark();
1007
+ }
1008
+ markLarkMessagesSteered(larkMessageIds, update = {}) {
1009
+ validateLarkMessageIds(larkMessageIds);
1010
+ validateLarkMessageBindingUpdate(update);
1011
+ const now = this.now();
1012
+ const mark = this.db.transaction(() => {
1013
+ for (const messageId of larkMessageIds) {
1014
+ this.updateLarkMessageSteeredStatement.run(update.conversationKey ?? null, update.codexThreadId ?? null, update.codexTurnId ?? null, now, messageId);
1015
+ }
1016
+ });
1017
+ mark();
1018
+ }
1019
+ markLarkMessagesCompleted(larkMessageIds) {
1020
+ this.markLarkMessagesTerminal(larkMessageIds, this.updateLarkMessageCompletedStatement);
1021
+ }
1022
+ markLarkMessagesFailed(larkMessageIds) {
1023
+ this.markLarkMessagesTerminal(larkMessageIds, this.updateLarkMessageFailedStatement);
1024
+ }
1025
+ markLarkMessagesInterrupted(larkMessageIds) {
1026
+ this.markLarkMessagesTerminal(larkMessageIds, this.updateLarkMessageInterruptedStatement);
1027
+ }
1028
+ markLarkMessagesCleared(larkMessageIds) {
1029
+ this.markLarkMessagesTerminal(larkMessageIds, this.updateLarkMessageClearedStatement);
1030
+ }
1031
+ requireByConversationKey(conversationKey) {
1032
+ const record = this.getByConversationKey(conversationKey);
1033
+ if (!record) {
1034
+ throw new TwinnyError(`Conversation ${conversationKey} was not found`, "CONVERSATION_NOT_FOUND");
1035
+ }
1036
+ return record;
1037
+ }
1038
+ toInsertParams(input) {
1039
+ validateNewConversation(input);
1040
+ const now = this.now();
1041
+ const profile = resolveRequiredInputProfile(input);
1042
+ const profileCodexHome = input.profileCodexHome;
1043
+ return {
1044
+ conversationKey: input.conversationKey,
1045
+ type: input.type,
1046
+ chatId: input.chatId,
1047
+ name: input.name,
1048
+ responseMode: input.responseMode ?? (input.type === "p2p" ? "all" : "none"),
1049
+ profile,
1050
+ codexThreadId: input.codexThreadId,
1051
+ workspace: input.workspace,
1052
+ profileCodexHome: profileCodexHome,
1053
+ createdAt: now,
1054
+ updatedAt: now
1055
+ };
1056
+ }
1057
+ requireCodexThreadById(codexThreadId) {
1058
+ const record = this.getCodexThreadById(codexThreadId);
1059
+ if (!record) {
1060
+ throw new TwinnyError(`Codex thread ${codexThreadId} was not found`, "CODEX_THREAD_NOT_FOUND");
1061
+ }
1062
+ return record;
1063
+ }
1064
+ requireLarkMessageById(larkMessageId) {
1065
+ const record = this.getLarkMessageById(larkMessageId);
1066
+ if (!record) {
1067
+ throw new TwinnyError(`Lark message ${larkMessageId} was not found`, "LARK_MESSAGE_NOT_FOUND");
1068
+ }
1069
+ return record;
1070
+ }
1071
+ requireLarkMessageByEventId(eventId) {
1072
+ const record = this.getLarkMessageByEventId(eventId);
1073
+ if (!record) {
1074
+ throw new TwinnyError(`Lark event ${eventId} was not found`, "LARK_MESSAGE_NOT_FOUND");
1075
+ }
1076
+ return record;
1077
+ }
1078
+ markLarkMessagesTerminal(larkMessageIds, statement) {
1079
+ for (const messageId of larkMessageIds) {
1080
+ assertNonEmpty(messageId, "larkMessageId");
1081
+ }
1082
+ const now = this.now();
1083
+ const mark = this.db.transaction(() => {
1084
+ for (const messageId of larkMessageIds) {
1085
+ statement.run(now, now, messageId);
1086
+ }
1087
+ });
1088
+ mark();
1089
+ }
1090
+ }
1091
+ export function createConversationRepository(db, options = {}) {
1092
+ return new ConversationRepository(db, options);
1093
+ }
1094
+ function mapConversationRow(row) {
1095
+ if (!row) {
1096
+ return undefined;
1097
+ }
1098
+ return mapRequiredConversationRow(row);
1099
+ }
1100
+ function mapRequiredConversationRow(row) {
1101
+ return {
1102
+ id: row.id,
1103
+ conversationKey: row.conversation_key,
1104
+ type: row.type,
1105
+ chatId: row.chat_id,
1106
+ name: row.name,
1107
+ responseMode: row.response_mode,
1108
+ profile: row.profile,
1109
+ codexThreadId: row.thread_id,
1110
+ workspace: row.workspace,
1111
+ profileCodexHome: row.profile_codex_home,
1112
+ createdAt: row.created_at,
1113
+ updatedAt: row.updated_at
1114
+ };
1115
+ }
1116
+ function mapCodexThreadRow(row) {
1117
+ if (!row) {
1118
+ return undefined;
1119
+ }
1120
+ return {
1121
+ id: row.id,
1122
+ codexThreadId: row.thread_id,
1123
+ conversationKey: row.conversation_key,
1124
+ name: row.name,
1125
+ larkThreadId: row.lark_thread_id ?? undefined,
1126
+ profile: row.profile,
1127
+ model: row.model ?? undefined,
1128
+ effort: row.effort ?? undefined,
1129
+ mode: validCodexThreadMode(row.mode) ? row.mode : "default",
1130
+ status: validCodexThreadStatus(row.status) ? row.status : "idle",
1131
+ goalStatus: validCodexThreadGoalStatus(row.goal_status) ? row.goal_status : "none",
1132
+ goalUpdatedAt: row.goal_updated_at ?? undefined,
1133
+ forkedFromCodexThreadId: row.forked_from_thread_id ?? undefined,
1134
+ forkedAt: row.forked_at ?? undefined,
1135
+ creatorOpenId: row.creator_open_id ?? undefined,
1136
+ cardMessageId: row.card_message_id ?? undefined,
1137
+ codexThreadHasRollout: row.thread_has_rollout === 1,
1138
+ inputTokens: row.input_tokens,
1139
+ outputTokens: row.output_tokens,
1140
+ cachedInputTokens: row.cached_input_tokens,
1141
+ reasoningOutputTokens: row.reasoning_output_tokens,
1142
+ totalTokens: row.total_tokens,
1143
+ contextTokens: row.context_tokens,
1144
+ contextWindow: row.context_window,
1145
+ tokenUsageJson: row.token_usage_json,
1146
+ createdAt: row.created_at,
1147
+ updatedAt: row.updated_at
1148
+ };
1149
+ }
1150
+ function mapLarkMessageRow(row) {
1151
+ if (!row) {
1152
+ return undefined;
1153
+ }
1154
+ return mapRequiredLarkMessageRow(row);
1155
+ }
1156
+ function mapRequiredLarkMessageRow(row) {
1157
+ return {
1158
+ id: row.id,
1159
+ larkMessageId: row.lark_message_id ?? undefined,
1160
+ eventId: row.event_id,
1161
+ larkUserId: row.lark_user_id,
1162
+ larkGroupId: row.lark_group_id ?? undefined,
1163
+ larkThreadId: row.lark_thread_id ?? undefined,
1164
+ conversationKey: row.conversation_key ?? undefined,
1165
+ codexThreadId: row.thread_id ?? undefined,
1166
+ codexTurnId: row.codex_turn_id ?? undefined,
1167
+ routeKind: row.route_kind,
1168
+ status: row.status,
1169
+ text: row.text,
1170
+ larkCreateTime: row.lark_create_time ?? undefined,
1171
+ receivedAt: row.received_at,
1172
+ updatedAt: row.updated_at,
1173
+ processingStartedAt: row.processing_started_at ?? undefined,
1174
+ completedAt: row.completed_at ?? undefined,
1175
+ failedAt: row.failed_at ?? undefined,
1176
+ clearedAt: row.cleared_at ?? undefined,
1177
+ sideId: row.side_id ?? undefined,
1178
+ agentCardMessageId: row.agent_card_message_id ?? undefined,
1179
+ rawEventJson: row.raw_event_json ?? undefined,
1180
+ inputTokens: row.input_tokens,
1181
+ outputTokens: row.output_tokens,
1182
+ cachedInputTokens: row.cached_input_tokens,
1183
+ reasoningOutputTokens: row.reasoning_output_tokens,
1184
+ tokenUsageJson: row.token_usage_json
1185
+ };
1186
+ }
1187
+ function validateNewConversation(input) {
1188
+ assertExpectedConversationKey(input.conversationKey, input.type, input.chatId);
1189
+ assertNonEmpty(input.name, "name");
1190
+ if (input.responseMode !== undefined) {
1191
+ assertValidResponseMode(input.responseMode);
1192
+ }
1193
+ assertValidProfile(resolveRequiredInputProfile(input));
1194
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
1195
+ assertAbsolutePath(input.workspace, "workspace");
1196
+ assertNonEmpty(input.profileCodexHome, "profileCodexHome");
1197
+ }
1198
+ function validateCodexThreadInput(input) {
1199
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
1200
+ assertValidConversationKey(input.conversationKey);
1201
+ assertValidProfile(resolveRequiredInputProfile(input));
1202
+ if (input.name !== undefined) {
1203
+ assertNonEmpty(input.name, "name");
1204
+ }
1205
+ if (input.model !== undefined) {
1206
+ assertNonEmpty(input.model, "model");
1207
+ }
1208
+ if (input.effort !== undefined) {
1209
+ assertNonEmpty(input.effort, "effort");
1210
+ }
1211
+ if (input.larkThreadId !== undefined) {
1212
+ assertNonEmpty(input.larkThreadId, "larkThreadId");
1213
+ }
1214
+ if (input.forkedFromCodexThreadId !== undefined) {
1215
+ assertNonEmpty(input.forkedFromCodexThreadId, "forkedFromCodexThreadId");
1216
+ }
1217
+ }
1218
+ function validateReplaceCodexThreadForLarkThread(input) {
1219
+ assertValidConversationKey(input.conversationKey);
1220
+ assertNonEmpty(input.larkThreadId, "larkThreadId");
1221
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
1222
+ assertValidProfile(resolveRequiredInputProfile(input));
1223
+ if (input.model !== undefined) {
1224
+ assertNonEmpty(input.model, "model");
1225
+ }
1226
+ if (input.effort !== undefined) {
1227
+ assertNonEmpty(input.effort, "effort");
1228
+ }
1229
+ }
1230
+ function validateLarkMessageInput(input) {
1231
+ if (input.routeKind === "card_action" || input.routeKind === "menu_action") {
1232
+ if (input.larkMessageId !== undefined) {
1233
+ assertNonEmpty(input.larkMessageId, "larkMessageId");
1234
+ }
1235
+ }
1236
+ else {
1237
+ if (input.larkMessageId === undefined) {
1238
+ throw new TwinnyError("larkMessageId is required", "CONVERSATION_FIELD_EMPTY");
1239
+ }
1240
+ assertNonEmpty(input.larkMessageId, "larkMessageId");
1241
+ }
1242
+ assertNonEmpty(input.eventId, "eventId");
1243
+ assertNonEmpty(input.larkUserId, "larkUserId");
1244
+ assertValidRouteKind(input.routeKind);
1245
+ assertValidMessageStatus(input.status);
1246
+ if (input.conversationKey !== undefined) {
1247
+ assertValidConversationKey(input.conversationKey);
1248
+ }
1249
+ if (input.codexThreadId !== undefined) {
1250
+ assertNonEmpty(input.codexThreadId, "codexThreadId");
1251
+ }
1252
+ if (input.codexTurnId !== undefined) {
1253
+ assertNonEmpty(input.codexTurnId, "codexTurnId");
1254
+ }
1255
+ }
1256
+ function assertExpectedConversationKey(conversationKey, type, chatId) {
1257
+ assertValidConversationType(type);
1258
+ const expectedKey = conversationKeyForTypeAndChatId(type, chatId);
1259
+ if (conversationKey !== expectedKey) {
1260
+ throw new TwinnyError(`Conversation key for ${type} must be ${expectedKey}, received ${conversationKey}`, "CONVERSATION_KEY_MISMATCH");
1261
+ }
1262
+ }
1263
+ function conversationKeyForTypeAndChatId(type, chatId) {
1264
+ if (type === "p2p") {
1265
+ return createP2PConversationKey(chatId);
1266
+ }
1267
+ return createGroupConversationKey(chatId);
1268
+ }
1269
+ function assertValidConversationType(type) {
1270
+ if (type !== "p2p" && type !== "group" && type !== "topic_group") {
1271
+ throw new TwinnyError(`Unsupported conversation type: ${type}`, "CONVERSATION_TYPE_INVALID");
1272
+ }
1273
+ }
1274
+ function assertValidResponseMode(responseMode) {
1275
+ if (responseMode !== "all" &&
1276
+ responseMode !== "all_at" &&
1277
+ responseMode !== "owner" &&
1278
+ responseMode !== "owner_at" &&
1279
+ responseMode !== "none") {
1280
+ throw new TwinnyError(`Unsupported conversation response mode: ${responseMode}`, "CONVERSATION_RESPONSE_MODE_INVALID");
1281
+ }
1282
+ }
1283
+ function resolveRequiredInputProfile(input) {
1284
+ const profile = resolveOptionalInputProfile(input);
1285
+ if (!profile) {
1286
+ throw new TwinnyError("profile must not be empty", "CONVERSATION_FIELD_EMPTY");
1287
+ }
1288
+ return profile;
1289
+ }
1290
+ function resolveOptionalInputProfile(input) {
1291
+ return input.profile;
1292
+ }
1293
+ function assertValidProfile(profile) {
1294
+ if (!/^[A-Za-z0-9_.-]+$/.test(profile) || profile === "none") {
1295
+ throw new TwinnyError(`Unsupported profile: ${profile}`, "CONVERSATION_PROFILE_INVALID");
1296
+ }
1297
+ }
1298
+ function assertValidRouteKind(routeKind) {
1299
+ if (routeKind !== "message" &&
1300
+ routeKind !== "steered_message" &&
1301
+ routeKind !== "queued_message" &&
1302
+ routeKind !== "side_message" &&
1303
+ routeKind !== "goal_message" &&
1304
+ routeKind !== "control_message" &&
1305
+ routeKind !== "card_action" &&
1306
+ routeKind !== "menu_action") {
1307
+ throw new TwinnyError(`Unsupported Lark message route kind: ${routeKind}`, "LARK_MESSAGE_ROUTE_KIND_INVALID");
1308
+ }
1309
+ }
1310
+ function assertValidMessageStatus(status) {
1311
+ if (status !== "queued" &&
1312
+ status !== "recalled" &&
1313
+ status !== "processing" &&
1314
+ status !== "steered" &&
1315
+ status !== "completed" &&
1316
+ status !== "failed" &&
1317
+ status !== "interrupted" &&
1318
+ status !== "cleared") {
1319
+ throw new TwinnyError(`Unsupported Lark message status: ${status}`, "LARK_MESSAGE_STATUS_INVALID");
1320
+ }
1321
+ }
1322
+ function assertValidCodexThreadStatus(status) {
1323
+ if (!validCodexThreadStatus(status)) {
1324
+ throw new TwinnyError(`Unsupported Codex thread status: ${status}`, "CODEX_THREAD_STATUS_INVALID");
1325
+ }
1326
+ }
1327
+ function assertValidCodexThreadGoalStatus(status) {
1328
+ if (!validCodexThreadGoalStatus(status)) {
1329
+ throw new TwinnyError(`Unsupported Codex thread goal status: ${status}`, "CODEX_THREAD_GOAL_STATUS_INVALID");
1330
+ }
1331
+ }
1332
+ function assertValidCodexThreadMode(mode) {
1333
+ if (!validCodexThreadMode(mode)) {
1334
+ throw new TwinnyError(`Unsupported Codex thread mode: ${mode}`, "CODEX_THREAD_MODE_INVALID");
1335
+ }
1336
+ }
1337
+ function validCodexThreadMode(mode) {
1338
+ return mode === "default" || mode === "plan";
1339
+ }
1340
+ function validCodexThreadStatus(status) {
1341
+ return status === "idle" || status === "working" || status === "waiting";
1342
+ }
1343
+ function validCodexThreadGoalStatus(status) {
1344
+ return (status === "none" ||
1345
+ status === "active" ||
1346
+ status === "paused" ||
1347
+ status === "blocked" ||
1348
+ status === "usageLimited" ||
1349
+ status === "budgetLimited" ||
1350
+ status === "complete");
1351
+ }
1352
+ function validateLarkMessageIds(larkMessageIds) {
1353
+ for (const messageId of larkMessageIds) {
1354
+ assertNonEmpty(messageId, "larkMessageId");
1355
+ }
1356
+ }
1357
+ function validateLarkMessageBindingUpdate(update) {
1358
+ if (update.conversationKey !== undefined) {
1359
+ assertValidConversationKey(update.conversationKey);
1360
+ }
1361
+ if (update.codexThreadId !== undefined) {
1362
+ assertNonEmpty(update.codexThreadId, "codexThreadId");
1363
+ }
1364
+ if (update.codexTurnId !== undefined) {
1365
+ assertNonEmpty(update.codexTurnId, "codexTurnId");
1366
+ }
1367
+ }
1368
+ function assertAbsolutePath(value, field) {
1369
+ assertNonEmpty(value, field);
1370
+ if (!path.isAbsolute(value)) {
1371
+ throw new TwinnyError(`${field} must be an absolute path`, "CONVERSATION_PATH_INVALID");
1372
+ }
1373
+ }
1374
+ function assertNonEmpty(value, field) {
1375
+ if (value.trim() === "") {
1376
+ throw new TwinnyError(`${field} must not be empty`, "CONVERSATION_FIELD_EMPTY");
1377
+ }
1378
+ }
1379
+ function assertNonNegativeFinite(value, field) {
1380
+ if (!Number.isFinite(value) || value < 0) {
1381
+ throw new TwinnyError(`${field} must be a non-negative finite number`, "CODEX_THREAD_TOKEN_USAGE_INVALID");
1382
+ }
1383
+ }
1384
+ //# sourceMappingURL=repositories.js.map