@sylphx/flow 1.7.0 → 1.8.1

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 (131) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/assets/agents/coder.md +72 -119
  3. package/assets/agents/orchestrator.md +26 -90
  4. package/assets/agents/reviewer.md +76 -47
  5. package/assets/agents/writer.md +82 -63
  6. package/assets/output-styles/silent.md +141 -8
  7. package/assets/rules/code-standards.md +9 -33
  8. package/assets/rules/core.md +67 -59
  9. package/package.json +2 -12
  10. package/src/commands/flow/execute.ts +470 -0
  11. package/src/commands/flow/index.ts +11 -0
  12. package/src/commands/flow/prompt.ts +35 -0
  13. package/src/commands/flow/setup.ts +312 -0
  14. package/src/commands/flow/targets.ts +18 -0
  15. package/src/commands/flow/types.ts +47 -0
  16. package/src/commands/flow-command.ts +18 -967
  17. package/src/commands/flow-orchestrator.ts +14 -5
  18. package/src/commands/hook-command.ts +1 -1
  19. package/src/commands/init-core.ts +12 -3
  20. package/src/commands/run-command.ts +1 -1
  21. package/src/config/rules.ts +1 -1
  22. package/src/core/error-handling.ts +1 -1
  23. package/src/core/loop-controller.ts +1 -1
  24. package/src/core/state-detector.ts +1 -1
  25. package/src/core/target-manager.ts +1 -1
  26. package/src/index.ts +1 -1
  27. package/src/shared/files/index.ts +1 -1
  28. package/src/shared/processing/index.ts +1 -1
  29. package/src/targets/claude-code.ts +3 -3
  30. package/src/targets/opencode.ts +3 -3
  31. package/src/utils/agent-enhancer.ts +2 -2
  32. package/src/utils/{mcp-config.ts → config/mcp-config.ts} +4 -4
  33. package/src/utils/{paths.ts → config/paths.ts} +1 -1
  34. package/src/utils/{settings.ts → config/settings.ts} +1 -1
  35. package/src/utils/{target-config.ts → config/target-config.ts} +5 -5
  36. package/src/utils/{target-utils.ts → config/target-utils.ts} +3 -3
  37. package/src/utils/display/banner.ts +25 -0
  38. package/src/utils/display/status.ts +55 -0
  39. package/src/utils/{file-operations.ts → files/file-operations.ts} +2 -2
  40. package/src/utils/files/jsonc.ts +36 -0
  41. package/src/utils/{sync-utils.ts → files/sync-utils.ts} +3 -3
  42. package/src/utils/index.ts +42 -61
  43. package/src/utils/version.ts +47 -0
  44. package/src/components/benchmark-monitor.tsx +0 -331
  45. package/src/components/reindex-progress.tsx +0 -261
  46. package/src/composables/functional/index.ts +0 -14
  47. package/src/composables/functional/useEnvironment.ts +0 -171
  48. package/src/composables/functional/useFileSystem.ts +0 -139
  49. package/src/composables/index.ts +0 -4
  50. package/src/composables/useEnv.ts +0 -13
  51. package/src/composables/useRuntimeConfig.ts +0 -27
  52. package/src/core/ai-sdk.ts +0 -603
  53. package/src/core/app-factory.ts +0 -381
  54. package/src/core/builtin-agents.ts +0 -9
  55. package/src/core/command-system.ts +0 -550
  56. package/src/core/config-system.ts +0 -550
  57. package/src/core/connection-pool.ts +0 -390
  58. package/src/core/di-container.ts +0 -155
  59. package/src/core/headless-display.ts +0 -96
  60. package/src/core/interfaces/index.ts +0 -22
  61. package/src/core/interfaces/repository.interface.ts +0 -91
  62. package/src/core/interfaces/service.interface.ts +0 -133
  63. package/src/core/interfaces.ts +0 -96
  64. package/src/core/result.ts +0 -351
  65. package/src/core/service-config.ts +0 -252
  66. package/src/core/session-service.ts +0 -121
  67. package/src/core/storage-factory.ts +0 -115
  68. package/src/core/stream-handler.ts +0 -288
  69. package/src/core/type-utils.ts +0 -427
  70. package/src/core/unified-storage.ts +0 -456
  71. package/src/core/validation/limit.ts +0 -46
  72. package/src/core/validation/query.ts +0 -20
  73. package/src/db/auto-migrate.ts +0 -322
  74. package/src/db/base-database-client.ts +0 -144
  75. package/src/db/cache-db.ts +0 -218
  76. package/src/db/cache-schema.ts +0 -75
  77. package/src/db/database.ts +0 -70
  78. package/src/db/index.ts +0 -252
  79. package/src/db/memory-db.ts +0 -153
  80. package/src/db/memory-schema.ts +0 -29
  81. package/src/db/schema.ts +0 -289
  82. package/src/db/session-repository.ts +0 -733
  83. package/src/domains/index.ts +0 -6
  84. package/src/domains/utilities/index.ts +0 -6
  85. package/src/domains/utilities/time/index.ts +0 -5
  86. package/src/domains/utilities/time/tools.ts +0 -291
  87. package/src/services/agent-service.ts +0 -273
  88. package/src/services/evaluation-service.ts +0 -271
  89. package/src/services/functional/evaluation-logic.ts +0 -296
  90. package/src/services/functional/file-processor.ts +0 -273
  91. package/src/services/functional/index.ts +0 -12
  92. package/src/services/memory.service.ts +0 -476
  93. package/src/types/api/batch.ts +0 -108
  94. package/src/types/api/errors.ts +0 -118
  95. package/src/types/api/index.ts +0 -55
  96. package/src/types/api/requests.ts +0 -76
  97. package/src/types/api/responses.ts +0 -180
  98. package/src/types/api/websockets.ts +0 -85
  99. package/src/types/benchmark.ts +0 -49
  100. package/src/types/database.types.ts +0 -510
  101. package/src/types/memory-types.ts +0 -63
  102. package/src/utils/advanced-tokenizer.ts +0 -191
  103. package/src/utils/ai-model-fetcher.ts +0 -19
  104. package/src/utils/async-file-operations.ts +0 -516
  105. package/src/utils/audio-player.ts +0 -345
  106. package/src/utils/codebase-helpers.ts +0 -211
  107. package/src/utils/console-ui.ts +0 -79
  108. package/src/utils/database-errors.ts +0 -140
  109. package/src/utils/debug-logger.ts +0 -49
  110. package/src/utils/file-scanner.ts +0 -259
  111. package/src/utils/help.ts +0 -20
  112. package/src/utils/immutable-cache.ts +0 -106
  113. package/src/utils/jsonc.ts +0 -158
  114. package/src/utils/memory-tui.ts +0 -414
  115. package/src/utils/models-dev.ts +0 -91
  116. package/src/utils/parallel-operations.ts +0 -487
  117. package/src/utils/process-manager.ts +0 -155
  118. package/src/utils/prompts.ts +0 -120
  119. package/src/utils/search-tool-builder.ts +0 -214
  120. package/src/utils/session-manager.ts +0 -168
  121. package/src/utils/session-title.ts +0 -87
  122. package/src/utils/simplified-errors.ts +0 -410
  123. package/src/utils/template-engine.ts +0 -94
  124. package/src/utils/test-audio.ts +0 -71
  125. package/src/utils/todo-context.ts +0 -46
  126. package/src/utils/token-counter.ts +0 -288
  127. /package/src/utils/{cli-output.ts → display/cli-output.ts} +0 -0
  128. /package/src/utils/{logger.ts → display/logger.ts} +0 -0
  129. /package/src/utils/{notifications.ts → display/notifications.ts} +0 -0
  130. /package/src/utils/{secret-utils.ts → security/secret-utils.ts} +0 -0
  131. /package/src/utils/{security.ts → security/security.ts} +0 -0
@@ -1,733 +0,0 @@
1
- /**
2
- * Session Repository
3
- * Database operations for chat sessions using Drizzle ORM
4
- *
5
- * Advantages over file-based storage:
6
- * - Indexed queries: Fast search by title, provider, date
7
- * - Pagination: Load only needed sessions (no memory bloat)
8
- * - Aggregations: Count messages without loading full session
9
- * - Transactions: Data consistency for complex operations
10
- * - Concurrent access: Proper locking and consistency
11
- * - Efficient updates: Update specific fields without rewriting entire file
12
- */
13
-
14
- import { eq, desc, and, like, sql, inArray } from 'drizzle-orm';
15
- import type { LibSQLDatabase } from 'drizzle-orm/libsql';
16
- import { randomUUID } from 'node:crypto';
17
- import {
18
- sessions,
19
- messages,
20
- messageParts,
21
- messageAttachments,
22
- messageUsage,
23
- todos,
24
- messageTodoSnapshots,
25
- type Session,
26
- type NewSession,
27
- type Message,
28
- type NewMessage,
29
- } from './schema.js';
30
- import type {
31
- Session as SessionType,
32
- SessionMessage,
33
- MessagePart,
34
- FileAttachment,
35
- TokenUsage,
36
- MessageMetadata,
37
- StreamingPart,
38
- } from '../types/session.types.js';
39
- import type { Todo as TodoType } from '../types/todo.types.js';
40
- import type { ProviderId } from '../config/ai-config.js';
41
-
42
- /**
43
- * Retry helper for handling SQLITE_BUSY errors
44
- * Exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms
45
- */
46
- async function retryOnBusy<T>(
47
- operation: () => Promise<T>,
48
- maxRetries = 5
49
- ): Promise<T> {
50
- let lastError: any;
51
-
52
- for (let attempt = 0; attempt < maxRetries; attempt++) {
53
- try {
54
- return await operation();
55
- } catch (error: any) {
56
- lastError = error;
57
-
58
- // Only retry on SQLITE_BUSY errors
59
- if (error.message?.includes('SQLITE_BUSY') || error.code === 'SQLITE_BUSY') {
60
- const delay = 50 * Math.pow(2, attempt);
61
- await new Promise(resolve => setTimeout(resolve, delay));
62
- continue;
63
- }
64
-
65
- // Other errors: throw immediately
66
- throw error;
67
- }
68
- }
69
-
70
- // Max retries exceeded
71
- throw lastError;
72
- }
73
-
74
- export class SessionRepository {
75
- constructor(private db: LibSQLDatabase) {}
76
-
77
- /**
78
- * Create a new session
79
- */
80
- async createSession(provider: ProviderId, model: string): Promise<SessionType> {
81
- const now = Date.now();
82
- const sessionId = `session-${now}`;
83
-
84
- const newSession: NewSession = {
85
- id: sessionId,
86
- provider,
87
- model,
88
- nextTodoId: 1,
89
- created: now,
90
- updated: now,
91
- };
92
-
93
- await this.db.insert(sessions).values(newSession);
94
-
95
- return {
96
- id: sessionId,
97
- provider,
98
- model,
99
- messages: [],
100
- todos: [],
101
- nextTodoId: 1,
102
- created: now,
103
- updated: now,
104
- };
105
- }
106
-
107
- /**
108
- * Create session with specific ID and timestamps (for migration)
109
- */
110
- async createSessionFromData(sessionData: {
111
- id: string;
112
- provider: ProviderId;
113
- model: string;
114
- title?: string;
115
- nextTodoId: number;
116
- created: number;
117
- updated: number;
118
- }): Promise<void> {
119
- await retryOnBusy(async () => {
120
- const newSession: NewSession = {
121
- id: sessionData.id,
122
- title: sessionData.title || null,
123
- provider: sessionData.provider,
124
- model: sessionData.model,
125
- nextTodoId: sessionData.nextTodoId,
126
- created: sessionData.created,
127
- updated: sessionData.updated,
128
- };
129
-
130
- await this.db.insert(sessions).values(newSession);
131
- });
132
- }
133
-
134
- /**
135
- * Get recent sessions with pagination
136
- * HUGE performance improvement: Only load 20 recent sessions instead of all
137
- */
138
- async getRecentSessions(limit = 20, offset = 0): Promise<SessionType[]> {
139
- // Get session metadata only (no messages yet - lazy loading!)
140
- const sessionRecords = await this.db
141
- .select()
142
- .from(sessions)
143
- .orderBy(desc(sessions.updated))
144
- .limit(limit)
145
- .offset(offset);
146
-
147
- // For each session, load messages, todos, etc.
148
- const fullSessions = await Promise.all(
149
- sessionRecords.map((session) => this.getSessionById(session.id))
150
- );
151
-
152
- return fullSessions.filter((s): s is SessionType => s !== null);
153
- }
154
-
155
- /**
156
- * Get session by ID with all related data
157
- */
158
- async getSessionById(sessionId: string): Promise<SessionType | null> {
159
- // Get session metadata
160
- const [session] = await this.db
161
- .select()
162
- .from(sessions)
163
- .where(eq(sessions.id, sessionId))
164
- .limit(1);
165
-
166
- if (!session) {
167
- return null;
168
- }
169
-
170
- // Get messages with all parts, attachments, usage
171
- const sessionMessages = await this.getSessionMessages(sessionId);
172
-
173
- // Get todos
174
- const sessionTodos = await this.getSessionTodos(sessionId);
175
-
176
- // Build return object
177
- const result: SessionType = {
178
- id: session.id,
179
- title: session.title || undefined,
180
- provider: session.provider as ProviderId,
181
- model: session.model,
182
- messages: sessionMessages,
183
- todos: sessionTodos,
184
- nextTodoId: session.nextTodoId,
185
- created: session.created,
186
- updated: session.updated,
187
- };
188
-
189
- return result;
190
- }
191
-
192
- /**
193
- * Get messages for a session
194
- * Assembles message parts, attachments, usage into SessionMessage format
195
- * OPTIMIZED: Batch queries instead of N+1 queries
196
- */
197
- private async getSessionMessages(sessionId: string): Promise<SessionMessage[]> {
198
- // Get all messages for session
199
- const messageRecords = await this.db
200
- .select()
201
- .from(messages)
202
- .where(eq(messages.sessionId, sessionId))
203
- .orderBy(messages.ordering);
204
-
205
- if (messageRecords.length === 0) {
206
- return [];
207
- }
208
-
209
- // Batch fetch all related data (MASSIVE performance improvement!)
210
- const messageIds = messageRecords.map((m) => m.id);
211
-
212
- // Fetch all parts, attachments, usage, snapshots in parallel (OPTIMIZED!)
213
- const [allParts, allAttachments, allUsage, allSnapshots] = await Promise.all([
214
- // Get all message parts for all messages
215
- this.db
216
- .select()
217
- .from(messageParts)
218
- .where(inArray(messageParts.messageId, messageIds))
219
- .orderBy(messageParts.ordering),
220
-
221
- // Get all attachments for all messages
222
- this.db
223
- .select()
224
- .from(messageAttachments)
225
- .where(inArray(messageAttachments.messageId, messageIds)),
226
-
227
- // Get all usage for all messages
228
- this.db
229
- .select()
230
- .from(messageUsage)
231
- .where(inArray(messageUsage.messageId, messageIds)),
232
-
233
- // Get all todo snapshots for all messages
234
- this.db
235
- .select()
236
- .from(messageTodoSnapshots)
237
- .where(inArray(messageTodoSnapshots.messageId, messageIds))
238
- .orderBy(messageTodoSnapshots.ordering),
239
- ]);
240
-
241
- // Group by message ID for O(1) lookup
242
- const partsByMessage = new Map<string, typeof allParts>();
243
- const attachmentsByMessage = new Map<string, typeof allAttachments>();
244
- const usageByMessage = new Map<string, (typeof allUsage)[0]>();
245
- const snapshotsByMessage = new Map<string, typeof allSnapshots>();
246
-
247
- for (const part of allParts) {
248
- if (!partsByMessage.has(part.messageId)) {
249
- partsByMessage.set(part.messageId, []);
250
- }
251
- partsByMessage.get(part.messageId)!.push(part);
252
- }
253
-
254
- for (const attachment of allAttachments) {
255
- if (!attachmentsByMessage.has(attachment.messageId)) {
256
- attachmentsByMessage.set(attachment.messageId, []);
257
- }
258
- attachmentsByMessage.get(attachment.messageId)!.push(attachment);
259
- }
260
-
261
- for (const usage of allUsage) {
262
- usageByMessage.set(usage.messageId, usage);
263
- }
264
-
265
- for (const snapshot of allSnapshots) {
266
- if (!snapshotsByMessage.has(snapshot.messageId)) {
267
- snapshotsByMessage.set(snapshot.messageId, []);
268
- }
269
- snapshotsByMessage.get(snapshot.messageId)!.push(snapshot);
270
- }
271
-
272
- // Assemble messages using grouped data
273
- const fullMessages = messageRecords.map((msg) => {
274
- const parts = partsByMessage.get(msg.id) || [];
275
- const attachments = attachmentsByMessage.get(msg.id) || [];
276
- const usage = usageByMessage.get(msg.id);
277
- const todoSnap = snapshotsByMessage.get(msg.id) || [];
278
-
279
- const sessionMessage: SessionMessage = {
280
- role: msg.role as 'user' | 'assistant',
281
- content: parts.map((p) => JSON.parse(p.content) as MessagePart),
282
- timestamp: msg.timestamp,
283
- status: (msg.status as 'active' | 'completed' | 'error' | 'abort') || 'completed',
284
- };
285
-
286
- if (msg.metadata) {
287
- sessionMessage.metadata = JSON.parse(msg.metadata) as MessageMetadata;
288
- }
289
-
290
- // Self-healing: Normalize attachments on read
291
- // Old/corrupted data might have invalid entries - filter them out
292
- if (attachments.length > 0) {
293
- const validAttachments = attachments.filter((a) =>
294
- a && typeof a === 'object' && a.path && a.relativePath
295
- );
296
-
297
- if (validAttachments.length > 0) {
298
- sessionMessage.attachments = validAttachments.map((a) => ({
299
- path: a.path,
300
- relativePath: a.relativePath,
301
- size: a.size || undefined,
302
- }));
303
- }
304
- // If all invalid, leave attachments undefined (no broken data in memory)
305
- }
306
-
307
- if (usage) {
308
- sessionMessage.usage = {
309
- promptTokens: usage.promptTokens,
310
- completionTokens: usage.completionTokens,
311
- totalTokens: usage.totalTokens,
312
- };
313
- }
314
-
315
- if (msg.finishReason) {
316
- sessionMessage.finishReason = msg.finishReason;
317
- }
318
-
319
- if (todoSnap.length > 0) {
320
- sessionMessage.todoSnapshot = todoSnap.map((t) => ({
321
- id: t.todoId,
322
- content: t.content,
323
- activeForm: t.activeForm,
324
- status: t.status as 'pending' | 'in_progress' | 'completed',
325
- ordering: t.ordering,
326
- }));
327
- }
328
-
329
- return sessionMessage;
330
- });
331
-
332
- return fullMessages;
333
- }
334
-
335
- /**
336
- * Get todos for a session
337
- */
338
- private async getSessionTodos(sessionId: string): Promise<TodoType[]> {
339
- const todoRecords = await this.db
340
- .select()
341
- .from(todos)
342
- .where(eq(todos.sessionId, sessionId))
343
- .orderBy(todos.ordering);
344
-
345
- return todoRecords.map((t) => ({
346
- id: t.id,
347
- content: t.content,
348
- activeForm: t.activeForm,
349
- status: t.status as 'pending' | 'in_progress' | 'completed',
350
- ordering: t.ordering,
351
- }));
352
- }
353
-
354
- /**
355
- * Add message to session
356
- * Atomically inserts message with all parts, attachments, usage
357
- */
358
- async addMessage(
359
- sessionId: string,
360
- role: 'user' | 'assistant',
361
- content: MessagePart[],
362
- attachments?: FileAttachment[],
363
- usage?: TokenUsage,
364
- finishReason?: string,
365
- metadata?: MessageMetadata,
366
- todoSnapshot?: TodoType[],
367
- status?: 'active' | 'completed' | 'error' | 'abort'
368
- ): Promise<string> {
369
- return await retryOnBusy(async () => {
370
- const messageId = randomUUID();
371
- const now = Date.now();
372
-
373
- // Get current message count for ordering
374
- const [{ count }] = await this.db
375
- .select({ count: sql<number>`count(*)` })
376
- .from(messages)
377
- .where(eq(messages.sessionId, sessionId));
378
-
379
- const ordering = count;
380
-
381
- // Insert in transaction
382
- await this.db.transaction(async (tx) => {
383
- // Insert message
384
- await tx.insert(messages).values({
385
- id: messageId,
386
- sessionId,
387
- role,
388
- timestamp: now,
389
- ordering,
390
- finishReason: finishReason || null,
391
- status: status || 'completed',
392
- metadata: metadata ? JSON.stringify(metadata) : null,
393
- });
394
-
395
- // Insert message parts
396
- for (let i = 0; i < content.length; i++) {
397
- await tx.insert(messageParts).values({
398
- id: randomUUID(),
399
- messageId,
400
- ordering: i,
401
- type: content[i].type,
402
- content: JSON.stringify(content[i]),
403
- });
404
- }
405
-
406
- // Insert attachments
407
- if (attachments && attachments.length > 0) {
408
- for (const att of attachments) {
409
- await tx.insert(messageAttachments).values({
410
- id: randomUUID(),
411
- messageId,
412
- path: att.path,
413
- relativePath: att.relativePath,
414
- size: att.size || null,
415
- });
416
- }
417
- }
418
-
419
- // Insert usage
420
- if (usage) {
421
- await tx.insert(messageUsage).values({
422
- messageId,
423
- promptTokens: usage.promptTokens,
424
- completionTokens: usage.completionTokens,
425
- totalTokens: usage.totalTokens,
426
- });
427
- }
428
-
429
- // Insert todo snapshot
430
- if (todoSnapshot && todoSnapshot.length > 0) {
431
- for (const todo of todoSnapshot) {
432
- await tx.insert(messageTodoSnapshots).values({
433
- id: randomUUID(),
434
- messageId,
435
- todoId: todo.id,
436
- content: todo.content,
437
- activeForm: todo.activeForm,
438
- status: todo.status,
439
- ordering: todo.ordering,
440
- });
441
- }
442
- }
443
-
444
- // Update session timestamp
445
- await tx
446
- .update(sessions)
447
- .set({ updated: now })
448
- .where(eq(sessions.id, sessionId));
449
- });
450
-
451
- return messageId;
452
- });
453
- }
454
-
455
- /**
456
- * Update session title
457
- */
458
- async updateSessionTitle(sessionId: string, title: string): Promise<void> {
459
- await this.db
460
- .update(sessions)
461
- .set({ title, updated: Date.now() })
462
- .where(eq(sessions.id, sessionId));
463
- }
464
-
465
- /**
466
- * Update session model
467
- */
468
- async updateSessionModel(sessionId: string, model: string): Promise<void> {
469
- await this.db
470
- .update(sessions)
471
- .set({ model, updated: Date.now() })
472
- .where(eq(sessions.id, sessionId));
473
- }
474
-
475
- /**
476
- * Update session provider and model
477
- */
478
- async updateSessionProvider(sessionId: string, provider: ProviderId, model: string): Promise<void> {
479
- await this.db
480
- .update(sessions)
481
- .set({ provider, model, updated: Date.now() })
482
- .where(eq(sessions.id, sessionId));
483
- }
484
-
485
- /**
486
- * Update message parts (used during streaming)
487
- * Replaces all parts for a message atomically
488
- */
489
- async updateMessageParts(messageId: string, parts: MessagePart[]): Promise<void> {
490
- await retryOnBusy(async () => {
491
- await this.db.transaction(async (tx) => {
492
- // Delete existing parts
493
- await tx.delete(messageParts).where(eq(messageParts.messageId, messageId));
494
-
495
- // Insert new parts
496
- for (let i = 0; i < parts.length; i++) {
497
- await tx.insert(messageParts).values({
498
- id: randomUUID(),
499
- messageId,
500
- ordering: i,
501
- type: parts[i].type,
502
- content: JSON.stringify(parts[i]),
503
- });
504
- }
505
- });
506
- });
507
- }
508
-
509
- /**
510
- * Update message status (used when streaming completes/aborts)
511
- */
512
- async updateMessageStatus(
513
- messageId: string,
514
- status: 'active' | 'completed' | 'error' | 'abort',
515
- finishReason?: string
516
- ): Promise<void> {
517
- await retryOnBusy(async () => {
518
- // Only update finishReason if explicitly provided
519
- const updates: {
520
- status: 'active' | 'completed' | 'error' | 'abort';
521
- finishReason?: string | null;
522
- } = { status };
523
-
524
- if (finishReason !== undefined) {
525
- updates.finishReason = finishReason || null;
526
- }
527
-
528
- await this.db
529
- .update(messages)
530
- .set(updates)
531
- .where(eq(messages.id, messageId));
532
- });
533
- }
534
-
535
- /**
536
- * Update message usage (used when streaming completes)
537
- * Inserts or replaces usage data for a message
538
- */
539
- async updateMessageUsage(messageId: string, usage: TokenUsage): Promise<void> {
540
- await retryOnBusy(async () => {
541
- // Check if usage already exists
542
- const [existing] = await this.db
543
- .select()
544
- .from(messageUsage)
545
- .where(eq(messageUsage.messageId, messageId))
546
- .limit(1);
547
-
548
- if (existing) {
549
- // Update existing usage
550
- await this.db
551
- .update(messageUsage)
552
- .set({
553
- promptTokens: usage.promptTokens,
554
- completionTokens: usage.completionTokens,
555
- totalTokens: usage.totalTokens,
556
- })
557
- .where(eq(messageUsage.messageId, messageId));
558
- } else {
559
- // Insert new usage
560
- await this.db.insert(messageUsage).values({
561
- messageId,
562
- promptTokens: usage.promptTokens,
563
- completionTokens: usage.completionTokens,
564
- totalTokens: usage.totalTokens,
565
- });
566
- }
567
- });
568
- }
569
-
570
- /**
571
- * Delete session (CASCADE will delete all related data)
572
- */
573
- async deleteSession(sessionId: string): Promise<void> {
574
- await this.db.delete(sessions).where(eq(sessions.id, sessionId));
575
- }
576
-
577
- /**
578
- * Search sessions by title
579
- * HUGE performance improvement: Uses index, no need to load all sessions
580
- */
581
- async searchSessionsByTitle(query: string, limit = 20): Promise<SessionType[]> {
582
- const sessionRecords = await this.db
583
- .select()
584
- .from(sessions)
585
- .where(like(sessions.title, `%${query}%`))
586
- .orderBy(desc(sessions.updated))
587
- .limit(limit);
588
-
589
- const fullSessions = await Promise.all(
590
- sessionRecords.map((session) => this.getSessionById(session.id))
591
- );
592
-
593
- return fullSessions.filter((s): s is SessionType => s !== null);
594
- }
595
-
596
- /**
597
- * Get session count
598
- * Efficient: No need to load sessions into memory
599
- */
600
- async getSessionCount(): Promise<number> {
601
- const [{ count }] = await this.db
602
- .select({ count: sql<number>`count(*)` })
603
- .from(sessions);
604
-
605
- return count;
606
- }
607
-
608
- /**
609
- * Get message count for session
610
- * Efficient: No need to load messages
611
- */
612
- async getMessageCount(sessionId: string): Promise<number> {
613
- const [{ count }] = await this.db
614
- .select({ count: sql<number>`count(*)` })
615
- .from(messages)
616
- .where(eq(messages.sessionId, sessionId));
617
-
618
- return count;
619
- }
620
-
621
- /**
622
- * Get most recently updated session (for headless mode continuation)
623
- * Returns the last active session
624
- */
625
- async getLastSession(): Promise<SessionType | null> {
626
- // Get most recent session by updated timestamp
627
- const [lastSession] = await this.db
628
- .select()
629
- .from(sessions)
630
- .orderBy(desc(sessions.updated))
631
- .limit(1);
632
-
633
- if (!lastSession) {
634
- return null;
635
- }
636
-
637
- // Load full session data
638
- return this.getSessionById(lastSession.id);
639
- }
640
-
641
- /**
642
- * Update todos for session
643
- */
644
- async updateTodos(sessionId: string, newTodos: TodoType[], nextTodoId: number): Promise<void> {
645
- await retryOnBusy(async () => {
646
- await this.db.transaction(async (tx) => {
647
- // Delete existing todos
648
- await tx.delete(todos).where(eq(todos.sessionId, sessionId));
649
-
650
- // Insert new todos
651
- for (const todo of newTodos) {
652
- await tx.insert(todos).values({
653
- id: todo.id,
654
- sessionId,
655
- content: todo.content,
656
- activeForm: todo.activeForm,
657
- status: todo.status,
658
- ordering: todo.ordering,
659
- });
660
- }
661
-
662
- // Update nextTodoId and timestamp
663
- await tx
664
- .update(sessions)
665
- .set({ nextTodoId, updated: Date.now() })
666
- .where(eq(sessions.id, sessionId));
667
- });
668
- });
669
- }
670
-
671
- /**
672
- * Get recent user messages for command history
673
- * Returns last N user messages across all sessions (most recent first)
674
- */
675
- async getRecentUserMessages(limit = 100): Promise<string[]> {
676
- return retryOnBusy(async () => {
677
- // Query user messages ordered by timestamp DESC
678
- const userMessages = await this.db
679
- .select({
680
- messageId: messages.id,
681
- timestamp: messages.timestamp,
682
- })
683
- .from(messages)
684
- .where(eq(messages.role, 'user'))
685
- .orderBy(desc(messages.timestamp))
686
- .limit(limit);
687
-
688
- if (userMessages.length === 0) {
689
- return [];
690
- }
691
-
692
- // Get text parts for these messages
693
- const messageIds = userMessages.map(m => m.messageId);
694
- const parts = await this.db
695
- .select()
696
- .from(messageParts)
697
- .where(
698
- and(
699
- inArray(messageParts.messageId, messageIds),
700
- eq(messageParts.type, 'text')
701
- )
702
- )
703
- .orderBy(messageParts.ordering);
704
-
705
- // Group parts by message and extract text content
706
- const messageTexts = new Map<string, string[]>();
707
- for (const part of parts) {
708
- const content = JSON.parse(part.content);
709
- const text = content.content || '';
710
- if (text.trim()) {
711
- if (!messageTexts.has(part.messageId)) {
712
- messageTexts.set(part.messageId, []);
713
- }
714
- messageTexts.get(part.messageId)!.push(text);
715
- }
716
- }
717
-
718
- // Build result in timestamp order (most recent first)
719
- const result: string[] = [];
720
- for (const msg of userMessages) {
721
- const texts = messageTexts.get(msg.messageId);
722
- if (texts && texts.length > 0) {
723
- const fullText = texts.join(' ').trim();
724
- if (fullText) {
725
- result.push(fullText);
726
- }
727
- }
728
- }
729
-
730
- return result;
731
- });
732
- }
733
- }