@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.
- package/CHANGELOG.md +78 -0
- package/assets/agents/coder.md +72 -119
- package/assets/agents/orchestrator.md +26 -90
- package/assets/agents/reviewer.md +76 -47
- package/assets/agents/writer.md +82 -63
- package/assets/output-styles/silent.md +141 -8
- package/assets/rules/code-standards.md +9 -33
- package/assets/rules/core.md +67 -59
- package/package.json +2 -12
- package/src/commands/flow/execute.ts +470 -0
- package/src/commands/flow/index.ts +11 -0
- package/src/commands/flow/prompt.ts +35 -0
- package/src/commands/flow/setup.ts +312 -0
- package/src/commands/flow/targets.ts +18 -0
- package/src/commands/flow/types.ts +47 -0
- package/src/commands/flow-command.ts +18 -967
- package/src/commands/flow-orchestrator.ts +14 -5
- package/src/commands/hook-command.ts +1 -1
- package/src/commands/init-core.ts +12 -3
- package/src/commands/run-command.ts +1 -1
- package/src/config/rules.ts +1 -1
- package/src/core/error-handling.ts +1 -1
- package/src/core/loop-controller.ts +1 -1
- package/src/core/state-detector.ts +1 -1
- package/src/core/target-manager.ts +1 -1
- package/src/index.ts +1 -1
- package/src/shared/files/index.ts +1 -1
- package/src/shared/processing/index.ts +1 -1
- package/src/targets/claude-code.ts +3 -3
- package/src/targets/opencode.ts +3 -3
- package/src/utils/agent-enhancer.ts +2 -2
- package/src/utils/{mcp-config.ts → config/mcp-config.ts} +4 -4
- package/src/utils/{paths.ts → config/paths.ts} +1 -1
- package/src/utils/{settings.ts → config/settings.ts} +1 -1
- package/src/utils/{target-config.ts → config/target-config.ts} +5 -5
- package/src/utils/{target-utils.ts → config/target-utils.ts} +3 -3
- package/src/utils/display/banner.ts +25 -0
- package/src/utils/display/status.ts +55 -0
- package/src/utils/{file-operations.ts → files/file-operations.ts} +2 -2
- package/src/utils/files/jsonc.ts +36 -0
- package/src/utils/{sync-utils.ts → files/sync-utils.ts} +3 -3
- package/src/utils/index.ts +42 -61
- package/src/utils/version.ts +47 -0
- package/src/components/benchmark-monitor.tsx +0 -331
- package/src/components/reindex-progress.tsx +0 -261
- package/src/composables/functional/index.ts +0 -14
- package/src/composables/functional/useEnvironment.ts +0 -171
- package/src/composables/functional/useFileSystem.ts +0 -139
- package/src/composables/index.ts +0 -4
- package/src/composables/useEnv.ts +0 -13
- package/src/composables/useRuntimeConfig.ts +0 -27
- package/src/core/ai-sdk.ts +0 -603
- package/src/core/app-factory.ts +0 -381
- package/src/core/builtin-agents.ts +0 -9
- package/src/core/command-system.ts +0 -550
- package/src/core/config-system.ts +0 -550
- package/src/core/connection-pool.ts +0 -390
- package/src/core/di-container.ts +0 -155
- package/src/core/headless-display.ts +0 -96
- package/src/core/interfaces/index.ts +0 -22
- package/src/core/interfaces/repository.interface.ts +0 -91
- package/src/core/interfaces/service.interface.ts +0 -133
- package/src/core/interfaces.ts +0 -96
- package/src/core/result.ts +0 -351
- package/src/core/service-config.ts +0 -252
- package/src/core/session-service.ts +0 -121
- package/src/core/storage-factory.ts +0 -115
- package/src/core/stream-handler.ts +0 -288
- package/src/core/type-utils.ts +0 -427
- package/src/core/unified-storage.ts +0 -456
- package/src/core/validation/limit.ts +0 -46
- package/src/core/validation/query.ts +0 -20
- package/src/db/auto-migrate.ts +0 -322
- package/src/db/base-database-client.ts +0 -144
- package/src/db/cache-db.ts +0 -218
- package/src/db/cache-schema.ts +0 -75
- package/src/db/database.ts +0 -70
- package/src/db/index.ts +0 -252
- package/src/db/memory-db.ts +0 -153
- package/src/db/memory-schema.ts +0 -29
- package/src/db/schema.ts +0 -289
- package/src/db/session-repository.ts +0 -733
- package/src/domains/index.ts +0 -6
- package/src/domains/utilities/index.ts +0 -6
- package/src/domains/utilities/time/index.ts +0 -5
- package/src/domains/utilities/time/tools.ts +0 -291
- package/src/services/agent-service.ts +0 -273
- package/src/services/evaluation-service.ts +0 -271
- package/src/services/functional/evaluation-logic.ts +0 -296
- package/src/services/functional/file-processor.ts +0 -273
- package/src/services/functional/index.ts +0 -12
- package/src/services/memory.service.ts +0 -476
- package/src/types/api/batch.ts +0 -108
- package/src/types/api/errors.ts +0 -118
- package/src/types/api/index.ts +0 -55
- package/src/types/api/requests.ts +0 -76
- package/src/types/api/responses.ts +0 -180
- package/src/types/api/websockets.ts +0 -85
- package/src/types/benchmark.ts +0 -49
- package/src/types/database.types.ts +0 -510
- package/src/types/memory-types.ts +0 -63
- package/src/utils/advanced-tokenizer.ts +0 -191
- package/src/utils/ai-model-fetcher.ts +0 -19
- package/src/utils/async-file-operations.ts +0 -516
- package/src/utils/audio-player.ts +0 -345
- package/src/utils/codebase-helpers.ts +0 -211
- package/src/utils/console-ui.ts +0 -79
- package/src/utils/database-errors.ts +0 -140
- package/src/utils/debug-logger.ts +0 -49
- package/src/utils/file-scanner.ts +0 -259
- package/src/utils/help.ts +0 -20
- package/src/utils/immutable-cache.ts +0 -106
- package/src/utils/jsonc.ts +0 -158
- package/src/utils/memory-tui.ts +0 -414
- package/src/utils/models-dev.ts +0 -91
- package/src/utils/parallel-operations.ts +0 -487
- package/src/utils/process-manager.ts +0 -155
- package/src/utils/prompts.ts +0 -120
- package/src/utils/search-tool-builder.ts +0 -214
- package/src/utils/session-manager.ts +0 -168
- package/src/utils/session-title.ts +0 -87
- package/src/utils/simplified-errors.ts +0 -410
- package/src/utils/template-engine.ts +0 -94
- package/src/utils/test-audio.ts +0 -71
- package/src/utils/todo-context.ts +0 -46
- package/src/utils/token-counter.ts +0 -288
- /package/src/utils/{cli-output.ts → display/cli-output.ts} +0 -0
- /package/src/utils/{logger.ts → display/logger.ts} +0 -0
- /package/src/utils/{notifications.ts → display/notifications.ts} +0 -0
- /package/src/utils/{secret-utils.ts → security/secret-utils.ts} +0 -0
- /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
|
-
}
|