pragma-so 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/cli/index.ts +882 -0
  2. package/index.ts +3 -0
  3. package/package.json +53 -0
  4. package/server/connectorBinaries.ts +103 -0
  5. package/server/connectorRegistry.ts +158 -0
  6. package/server/conversation/adapterRegistry.ts +53 -0
  7. package/server/conversation/adapters/claudeAdapter.ts +138 -0
  8. package/server/conversation/adapters/codexAdapter.ts +142 -0
  9. package/server/conversation/adapters.ts +224 -0
  10. package/server/conversation/executeRunner.ts +1191 -0
  11. package/server/conversation/gitWorkflow.ts +1037 -0
  12. package/server/conversation/models.ts +23 -0
  13. package/server/conversation/pragmaCli.ts +34 -0
  14. package/server/conversation/prompts.ts +335 -0
  15. package/server/conversation/store.ts +805 -0
  16. package/server/conversation/titleGenerator.ts +106 -0
  17. package/server/conversation/turnRunner.ts +365 -0
  18. package/server/conversation/types.ts +134 -0
  19. package/server/db.ts +837 -0
  20. package/server/http/middleware.ts +31 -0
  21. package/server/http/schemas.ts +430 -0
  22. package/server/http/validators.ts +38 -0
  23. package/server/index.ts +6560 -0
  24. package/server/process/runCommand.ts +142 -0
  25. package/server/stores/agentStore.ts +167 -0
  26. package/server/stores/connectorStore.ts +299 -0
  27. package/server/stores/humanStore.ts +28 -0
  28. package/server/stores/skillStore.ts +127 -0
  29. package/server/stores/taskStore.ts +371 -0
  30. package/shared/net.ts +24 -0
  31. package/tsconfig.json +14 -0
  32. package/ui/index.html +14 -0
  33. package/ui/public/favicon-32.png +0 -0
  34. package/ui/public/favicon.png +0 -0
  35. package/ui/src/App.jsx +1338 -0
  36. package/ui/src/api.js +954 -0
  37. package/ui/src/components/CodeView.jsx +319 -0
  38. package/ui/src/components/ConnectionsView.jsx +1004 -0
  39. package/ui/src/components/ContextView.jsx +315 -0
  40. package/ui/src/components/ConversationDrawer.jsx +963 -0
  41. package/ui/src/components/EmptyPane.jsx +20 -0
  42. package/ui/src/components/FeedView.jsx +773 -0
  43. package/ui/src/components/FilesView.jsx +257 -0
  44. package/ui/src/components/InlineChatView.jsx +158 -0
  45. package/ui/src/components/InputBar.jsx +476 -0
  46. package/ui/src/components/OnboardingModal.jsx +112 -0
  47. package/ui/src/components/OutputPanel.jsx +658 -0
  48. package/ui/src/components/PlanProposalPanel.jsx +177 -0
  49. package/ui/src/components/RightPanel.jsx +951 -0
  50. package/ui/src/components/SettingsView.jsx +186 -0
  51. package/ui/src/components/Sidebar.jsx +247 -0
  52. package/ui/src/components/TestingPane.jsx +198 -0
  53. package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
  54. package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
  55. package/ui/src/components/testing/TerminalPanel.jsx +104 -0
  56. package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
  57. package/ui/src/hooks/useAgents.js +81 -0
  58. package/ui/src/hooks/useConversation.js +252 -0
  59. package/ui/src/hooks/useTasks.js +161 -0
  60. package/ui/src/hooks/useWorkspace.js +259 -0
  61. package/ui/src/lib/agentIcon.js +10 -0
  62. package/ui/src/lib/conversationUtils.js +575 -0
  63. package/ui/src/main.jsx +10 -0
  64. package/ui/src/styles.css +6899 -0
  65. package/ui/vite.config.mjs +6 -0
@@ -0,0 +1,805 @@
1
+ import type { PGlite } from "@electric-sql/pglite";
2
+ import type {
3
+ ChatThreadListItem,
4
+ ConversationEvent,
5
+ ConversationMessage,
6
+ ConversationMode,
7
+ ConversationThread,
8
+ ConversationTurn,
9
+ HarnessId,
10
+ OpenPlanThreadListItem,
11
+ ReasoningEffort,
12
+ } from "./types";
13
+
14
+ export async function ensureConversationSchema(db: PGlite): Promise<void> {
15
+ await db.exec(`
16
+ CREATE TABLE IF NOT EXISTS conversation_threads (
17
+ id VARCHAR(64) PRIMARY KEY,
18
+ mode VARCHAR(16) NOT NULL,
19
+ status VARCHAR(32) NOT NULL DEFAULT 'open',
20
+ harness VARCHAR(32) NOT NULL,
21
+ model_label VARCHAR(128) NOT NULL,
22
+ model_id VARCHAR(128) NOT NULL,
23
+ harness_session_id VARCHAR(255),
24
+ task_id VARCHAR(64),
25
+ source_thread_id VARCHAR(64),
26
+ chat_title TEXT,
27
+ chat_preview TEXT,
28
+ chat_last_message_at TIMESTAMPTZ,
29
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS conversation_turns (
34
+ id VARCHAR(64) PRIMARY KEY,
35
+ thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
36
+ mode VARCHAR(16) NOT NULL,
37
+ user_message TEXT NOT NULL,
38
+ assistant_message TEXT,
39
+ reasoning_effort VARCHAR(16),
40
+ requested_recipient_agent_id VARCHAR(64),
41
+ selected_agent_id VARCHAR(64),
42
+ orchestrator_agent_id VARCHAR(64),
43
+ worker_session_id VARCHAR(255),
44
+ selection_status VARCHAR(32),
45
+ status VARCHAR(32) NOT NULL DEFAULT 'running',
46
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
47
+ completed_at TIMESTAMPTZ
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS conversation_messages (
51
+ id VARCHAR(64) PRIMARY KEY,
52
+ thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
53
+ turn_id VARCHAR(64) REFERENCES conversation_turns(id) ON DELETE SET NULL,
54
+ role VARCHAR(16) NOT NULL,
55
+ content TEXT NOT NULL,
56
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS conversation_events (
60
+ id VARCHAR(64) PRIMARY KEY,
61
+ seq SERIAL,
62
+ thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
63
+ turn_id VARCHAR(64) REFERENCES conversation_turns(id) ON DELETE SET NULL,
64
+ event_name VARCHAR(64) NOT NULL,
65
+ payload_json TEXT NOT NULL,
66
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_conversation_turns_thread ON conversation_turns(thread_id, created_at DESC);
70
+ CREATE INDEX IF NOT EXISTS idx_conversation_messages_thread ON conversation_messages(thread_id, created_at ASC);
71
+ CREATE INDEX IF NOT EXISTS idx_conversation_events_thread ON conversation_events(thread_id, created_at ASC);
72
+ `);
73
+
74
+ await db.exec(`
75
+ CREATE INDEX IF NOT EXISTS idx_conversation_chat_mode_sort ON conversation_threads(mode, created_at DESC);
76
+ `);
77
+
78
+ // Migration: add seq column to conversation_events if it doesn't exist
79
+ await db.exec(`
80
+ DO $$
81
+ BEGIN
82
+ IF NOT EXISTS (
83
+ SELECT 1 FROM information_schema.columns
84
+ WHERE table_name = 'conversation_events' AND column_name = 'seq'
85
+ ) THEN
86
+ ALTER TABLE conversation_events ADD COLUMN seq SERIAL;
87
+ CREATE INDEX idx_conversation_events_seq ON conversation_events(thread_id, seq ASC);
88
+ END IF;
89
+ END $$;
90
+ `);
91
+
92
+ // Ensure index exists for seq-based queries even on fresh DBs
93
+ await db.exec(`
94
+ CREATE INDEX IF NOT EXISTS idx_conversation_events_seq ON conversation_events(thread_id, seq ASC);
95
+ `);
96
+
97
+ // Migration: add plan_proposal_json column to conversation_turns if it doesn't exist
98
+ await db.exec(`
99
+ DO $$
100
+ BEGIN
101
+ IF NOT EXISTS (
102
+ SELECT 1 FROM information_schema.columns
103
+ WHERE table_name = 'conversation_turns' AND column_name = 'plan_proposal_json'
104
+ ) THEN
105
+ ALTER TABLE conversation_turns ADD COLUMN plan_proposal_json TEXT;
106
+ END IF;
107
+ END $$;
108
+ `);
109
+ }
110
+
111
+ export async function createThread(
112
+ db: PGlite,
113
+ input: {
114
+ id: string;
115
+ mode: ConversationMode;
116
+ harness: HarnessId;
117
+ modelLabel: string;
118
+ modelId: string;
119
+ sourceThreadId?: string | null;
120
+ taskId?: string | null;
121
+ },
122
+ ): Promise<void> {
123
+ await db.query(
124
+ `
125
+ INSERT INTO conversation_threads (
126
+ id,
127
+ mode,
128
+ status,
129
+ harness,
130
+ model_label,
131
+ model_id,
132
+ source_thread_id,
133
+ task_id
134
+ )
135
+ VALUES ($1, $2, 'open', $3, $4, $5, $6, $7)
136
+ `,
137
+ [
138
+ input.id,
139
+ input.mode,
140
+ input.harness,
141
+ input.modelLabel,
142
+ input.modelId,
143
+ input.sourceThreadId ?? null,
144
+ input.taskId ?? null,
145
+ ],
146
+ );
147
+ }
148
+
149
+ export async function getThreadById(
150
+ db: PGlite,
151
+ threadId: string,
152
+ ): Promise<ConversationThread | null> {
153
+ const result = await db.query<ConversationThread>(
154
+ `
155
+ SELECT id,
156
+ mode,
157
+ status,
158
+ harness,
159
+ model_label,
160
+ model_id,
161
+ harness_session_id,
162
+ task_id,
163
+ source_thread_id,
164
+ chat_title,
165
+ chat_preview,
166
+ chat_last_message_at,
167
+ created_at,
168
+ updated_at
169
+ FROM conversation_threads
170
+ WHERE id = $1
171
+ `,
172
+ [threadId],
173
+ );
174
+
175
+ return result.rows[0] ?? null;
176
+ }
177
+
178
+ export async function listChatThreads(
179
+ db: PGlite,
180
+ input?: { limit?: number; cursor?: string | null },
181
+ ): Promise<ChatThreadListItem[]> {
182
+ const requested = input?.limit ?? 20;
183
+ const limit = Math.max(1, Math.min(requested, 20));
184
+ const cursor = input?.cursor?.trim() || null;
185
+
186
+ const params: Array<string | number> = [limit];
187
+ let query = `
188
+ SELECT thread.id,
189
+ thread.chat_title,
190
+ thread.chat_preview,
191
+ thread.status,
192
+ thread.updated_at,
193
+ thread.chat_last_message_at,
194
+ newest_turn.status AS latest_turn_status
195
+ FROM conversation_threads AS thread
196
+ LEFT JOIN LATERAL (
197
+ SELECT status
198
+ FROM conversation_turns
199
+ WHERE thread_id = thread.id
200
+ ORDER BY created_at DESC
201
+ LIMIT 1
202
+ ) AS newest_turn ON TRUE
203
+ WHERE thread.mode = 'chat'
204
+ `;
205
+
206
+ if (cursor) {
207
+ params.push(cursor);
208
+ query += " AND thread.created_at < $2::timestamptz\n";
209
+ }
210
+
211
+ query += `
212
+ ORDER BY thread.created_at DESC
213
+ LIMIT $1
214
+ `;
215
+
216
+ const result = await db.query<ChatThreadListItem>(query, params);
217
+ return result.rows;
218
+ }
219
+
220
+ export async function listOpenPlanThreads(
221
+ db: PGlite,
222
+ input?: { limit?: number; cursor?: string | null },
223
+ ): Promise<OpenPlanThreadListItem[]> {
224
+ const requested = input?.limit ?? 20;
225
+ const limit = Math.max(1, Math.min(requested, 20));
226
+ const cursor = input?.cursor?.trim() || null;
227
+
228
+ const params: Array<string | number> = [limit];
229
+ let query = `
230
+ SELECT thread.id,
231
+ thread.status,
232
+ thread.created_at,
233
+ thread.updated_at,
234
+ thread.task_id,
235
+ latest_plan_turn.assistant_message AS latest_plan_assistant_message,
236
+ first_plan_turn.user_message AS first_user_message,
237
+ (latest_plan_turn.assistant_message IS NOT NULL) AS has_completed_plan_turn,
238
+ newest_turn.status AS latest_turn_status,
239
+ task.status AS task_status
240
+ FROM conversation_threads AS thread
241
+ LEFT JOIN LATERAL (
242
+ SELECT assistant_message
243
+ FROM conversation_turns
244
+ WHERE thread_id = thread.id
245
+ AND mode = 'plan'
246
+ AND status = 'completed'
247
+ AND assistant_message IS NOT NULL
248
+ AND assistant_message <> ''
249
+ ORDER BY created_at DESC
250
+ LIMIT 1
251
+ ) AS latest_plan_turn ON TRUE
252
+ LEFT JOIN LATERAL (
253
+ SELECT user_message
254
+ FROM conversation_turns
255
+ WHERE thread_id = thread.id
256
+ AND mode = 'plan'
257
+ ORDER BY created_at ASC
258
+ LIMIT 1
259
+ ) AS first_plan_turn ON TRUE
260
+ LEFT JOIN LATERAL (
261
+ SELECT status
262
+ FROM conversation_turns
263
+ WHERE thread_id = thread.id
264
+ AND mode = 'plan'
265
+ ORDER BY created_at DESC
266
+ LIMIT 1
267
+ ) AS newest_turn ON TRUE
268
+ LEFT JOIN tasks AS task ON task.id = thread.task_id
269
+ WHERE thread.mode = 'plan'
270
+ AND thread.status = 'open'
271
+ `;
272
+
273
+ if (cursor) {
274
+ params.push(cursor);
275
+ query += " AND thread.updated_at < $2::timestamptz\n";
276
+ }
277
+
278
+ query += `
279
+ ORDER BY thread.updated_at DESC
280
+ LIMIT $1
281
+ `;
282
+
283
+ const result = await db.query<OpenPlanThreadListItem>(query, params);
284
+ return result.rows;
285
+ }
286
+
287
+ export async function updateChatThreadMetadata(
288
+ db: PGlite,
289
+ input: {
290
+ threadId: string;
291
+ title?: string | null;
292
+ lastMessageAt?: string | null;
293
+ force?: boolean;
294
+ },
295
+ ): Promise<void> {
296
+ const titleSql = input.force
297
+ ? `chat_title = CASE
298
+ WHEN $2::text IS NOT NULL AND $2::text <> ''
299
+ THEN $2::text
300
+ ELSE chat_title
301
+ END`
302
+ : `chat_title = CASE
303
+ WHEN (chat_title IS NULL OR chat_title = '') AND $2::text IS NOT NULL AND $2::text <> ''
304
+ THEN $2::text
305
+ ELSE chat_title
306
+ END`;
307
+
308
+ await db.query(
309
+ `
310
+ UPDATE conversation_threads
311
+ SET ${titleSql},
312
+ chat_last_message_at = COALESCE($3::timestamptz, CURRENT_TIMESTAMP),
313
+ updated_at = CURRENT_TIMESTAMP
314
+ WHERE id = $1
315
+ `,
316
+ [input.threadId, input.title ?? null, input.lastMessageAt ?? null],
317
+ );
318
+ }
319
+
320
+ export async function updateThreadSession(
321
+ db: PGlite,
322
+ input: { threadId: string; sessionId: string },
323
+ ): Promise<void> {
324
+ await db.query(
325
+ `
326
+ UPDATE conversation_threads
327
+ SET harness_session_id = $2,
328
+ updated_at = CURRENT_TIMESTAMP
329
+ WHERE id = $1
330
+ `,
331
+ [input.threadId, input.sessionId],
332
+ );
333
+ }
334
+
335
+ export async function closeThread(db: PGlite, threadId: string): Promise<void> {
336
+ await db.query(
337
+ `
338
+ UPDATE conversation_threads
339
+ SET status = 'closed',
340
+ updated_at = CURRENT_TIMESTAMP
341
+ WHERE id = $1
342
+ `,
343
+ [threadId],
344
+ );
345
+ }
346
+
347
+ export async function reopenThread(db: PGlite, threadId: string): Promise<void> {
348
+ await db.query(
349
+ `
350
+ UPDATE conversation_threads
351
+ SET status = 'open',
352
+ updated_at = CURRENT_TIMESTAMP
353
+ WHERE id = $1
354
+ `,
355
+ [threadId],
356
+ );
357
+ }
358
+
359
+ export async function createTurn(
360
+ db: PGlite,
361
+ input: {
362
+ id: string;
363
+ threadId: string;
364
+ mode: ConversationMode;
365
+ userMessage: string;
366
+ reasoningEffort?: ReasoningEffort | null;
367
+ requestedRecipientAgentId?: string | null;
368
+ selectedAgentId?: string | null;
369
+ orchestratorAgentId?: string | null;
370
+ workerSessionId?: string | null;
371
+ selectionStatus?: "auto_selected" | "manual_selected" | "recipient_required" | "invalid" | null;
372
+ },
373
+ ): Promise<void> {
374
+ await db.query(
375
+ `
376
+ INSERT INTO conversation_turns (
377
+ id,
378
+ thread_id,
379
+ mode,
380
+ user_message,
381
+ reasoning_effort,
382
+ requested_recipient_agent_id,
383
+ selected_agent_id,
384
+ orchestrator_agent_id,
385
+ worker_session_id,
386
+ selection_status,
387
+ status
388
+ )
389
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'running')
390
+ `,
391
+ [
392
+ input.id,
393
+ input.threadId,
394
+ input.mode,
395
+ input.userMessage,
396
+ input.reasoningEffort ?? null,
397
+ input.requestedRecipientAgentId ?? null,
398
+ input.selectedAgentId ?? null,
399
+ input.orchestratorAgentId ?? null,
400
+ input.workerSessionId ?? null,
401
+ input.selectionStatus ?? null,
402
+ ],
403
+ );
404
+ }
405
+
406
+ export async function completeTurn(
407
+ db: PGlite,
408
+ input: {
409
+ turnId: string;
410
+ assistantMessage: string;
411
+ selectedAgentId?: string | null;
412
+ workerSessionId?: string | null;
413
+ selectionStatus?: "auto_selected" | "manual_selected" | "recipient_required" | "invalid" | null;
414
+ },
415
+ ): Promise<void> {
416
+ await db.query(
417
+ `
418
+ UPDATE conversation_turns
419
+ SET assistant_message = $2,
420
+ selected_agent_id = COALESCE($3, selected_agent_id),
421
+ worker_session_id = COALESCE($4, worker_session_id),
422
+ selection_status = COALESCE($5, selection_status),
423
+ status = 'completed',
424
+ completed_at = CURRENT_TIMESTAMP
425
+ WHERE id = $1
426
+ `,
427
+ [
428
+ input.turnId,
429
+ input.assistantMessage,
430
+ input.selectedAgentId ?? null,
431
+ input.workerSessionId ?? null,
432
+ input.selectionStatus ?? null,
433
+ ],
434
+ );
435
+ }
436
+
437
+ export async function failTurn(db: PGlite, turnId: string, message: string): Promise<void> {
438
+ await db.query(
439
+ `
440
+ UPDATE conversation_turns
441
+ SET assistant_message = COALESCE(assistant_message, $2),
442
+ status = 'failed',
443
+ completed_at = CURRENT_TIMESTAMP
444
+ WHERE id = $1
445
+ `,
446
+ [turnId, message],
447
+ );
448
+ }
449
+
450
+ export async function insertMessage(
451
+ db: PGlite,
452
+ input: {
453
+ id: string;
454
+ threadId: string;
455
+ turnId: string | null;
456
+ role: "user" | "assistant" | "system";
457
+ content: string;
458
+ },
459
+ ): Promise<void> {
460
+ await db.query(
461
+ `
462
+ INSERT INTO conversation_messages (id, thread_id, turn_id, role, content)
463
+ VALUES ($1, $2, $3, $4, $5)
464
+ `,
465
+ [input.id, input.threadId, input.turnId, input.role, input.content],
466
+ );
467
+ }
468
+
469
+ export async function insertEvent(
470
+ db: PGlite,
471
+ input: {
472
+ id: string;
473
+ threadId: string;
474
+ turnId: string | null;
475
+ eventName: string;
476
+ payload: unknown;
477
+ },
478
+ ): Promise<number> {
479
+ const result = await db.query<{ seq: number }>(
480
+ `
481
+ INSERT INTO conversation_events (id, thread_id, turn_id, event_name, payload_json)
482
+ VALUES ($1, $2, $3, $4, $5)
483
+ RETURNING seq
484
+ `,
485
+ [input.id, input.threadId, input.turnId, input.eventName, JSON.stringify(input.payload ?? null)],
486
+ );
487
+ return result.rows[0]?.seq ?? 0;
488
+ }
489
+
490
+ export async function getEventsSince(
491
+ db: PGlite,
492
+ threadId: string,
493
+ afterSeq: number,
494
+ ): Promise<Array<ConversationEvent & { seq: number }>> {
495
+ const result = await db.query<ConversationEvent & { seq: number }>(
496
+ `
497
+ SELECT id, seq, thread_id, turn_id, event_name, payload_json, created_at
498
+ FROM conversation_events
499
+ WHERE thread_id = $1
500
+ AND seq > $2
501
+ ORDER BY seq ASC
502
+ `,
503
+ [threadId, afterSeq],
504
+ );
505
+ return result.rows;
506
+ }
507
+
508
+ export async function getMaxEventSeq(
509
+ db: PGlite,
510
+ threadId: string,
511
+ ): Promise<number> {
512
+ const result = await db.query<{ max_seq: number | null }>(
513
+ `SELECT MAX(seq) AS max_seq FROM conversation_events WHERE thread_id = $1`,
514
+ [threadId],
515
+ );
516
+ return result.rows[0]?.max_seq ?? 0;
517
+ }
518
+
519
+ export async function setThreadTaskId(db: PGlite, threadId: string, taskId: string): Promise<void> {
520
+ await db.query(
521
+ `
522
+ UPDATE conversation_threads
523
+ SET task_id = $2,
524
+ updated_at = CURRENT_TIMESTAMP
525
+ WHERE id = $1
526
+ `,
527
+ [threadId, taskId],
528
+ );
529
+ }
530
+
531
+ export async function getThreadByTaskId(
532
+ db: PGlite,
533
+ taskId: string,
534
+ ): Promise<ConversationThread | null> {
535
+ const result = await db.query<ConversationThread>(
536
+ `
537
+ SELECT id,
538
+ mode,
539
+ status,
540
+ harness,
541
+ model_label,
542
+ model_id,
543
+ harness_session_id,
544
+ task_id,
545
+ source_thread_id,
546
+ chat_title,
547
+ chat_preview,
548
+ chat_last_message_at,
549
+ created_at,
550
+ updated_at
551
+ FROM conversation_threads
552
+ WHERE task_id = $1
553
+ ORDER BY created_at DESC
554
+ LIMIT 1
555
+ `,
556
+ [taskId],
557
+ );
558
+
559
+ return result.rows[0] ?? null;
560
+ }
561
+
562
+ export async function getLatestPlanTurn(
563
+ db: PGlite,
564
+ threadId: string,
565
+ ): Promise<ConversationTurn | null> {
566
+ const result = await db.query<ConversationTurn>(
567
+ `
568
+ SELECT id,
569
+ thread_id,
570
+ mode,
571
+ user_message,
572
+ assistant_message,
573
+ reasoning_effort,
574
+ requested_recipient_agent_id,
575
+ selected_agent_id,
576
+ orchestrator_agent_id,
577
+ worker_session_id,
578
+ selection_status,
579
+ status,
580
+ created_at,
581
+ completed_at
582
+ FROM conversation_turns
583
+ WHERE thread_id = $1
584
+ AND mode = 'plan'
585
+ ORDER BY created_at DESC
586
+ LIMIT 1
587
+ `,
588
+ [threadId],
589
+ );
590
+
591
+ return result.rows[0] ?? null;
592
+ }
593
+
594
+ export async function getLatestCompletedPlanTurn(
595
+ db: PGlite,
596
+ threadId: string,
597
+ ): Promise<ConversationTurn | null> {
598
+ const result = await db.query<ConversationTurn>(
599
+ `
600
+ SELECT id,
601
+ thread_id,
602
+ mode,
603
+ user_message,
604
+ assistant_message,
605
+ reasoning_effort,
606
+ requested_recipient_agent_id,
607
+ selected_agent_id,
608
+ orchestrator_agent_id,
609
+ worker_session_id,
610
+ selection_status,
611
+ status,
612
+ created_at,
613
+ completed_at
614
+ FROM conversation_turns
615
+ WHERE thread_id = $1
616
+ AND mode = 'plan'
617
+ AND status = 'completed'
618
+ ORDER BY created_at DESC
619
+ LIMIT 1
620
+ `,
621
+ [threadId],
622
+ );
623
+
624
+ return result.rows[0] ?? null;
625
+ }
626
+
627
+ export async function getLatestExecuteTurn(
628
+ db: PGlite,
629
+ threadId: string,
630
+ ): Promise<ConversationTurn | null> {
631
+ const result = await db.query<ConversationTurn>(
632
+ `
633
+ SELECT id,
634
+ thread_id,
635
+ mode,
636
+ user_message,
637
+ assistant_message,
638
+ reasoning_effort,
639
+ requested_recipient_agent_id,
640
+ selected_agent_id,
641
+ orchestrator_agent_id,
642
+ worker_session_id,
643
+ selection_status,
644
+ status,
645
+ created_at,
646
+ completed_at
647
+ FROM conversation_turns
648
+ WHERE thread_id = $1
649
+ AND mode = 'execute'
650
+ ORDER BY created_at DESC
651
+ LIMIT 1
652
+ `,
653
+ [threadId],
654
+ );
655
+
656
+ return result.rows[0] ?? null;
657
+ }
658
+
659
+ export async function getFirstExecuteTurn(
660
+ db: PGlite,
661
+ threadId: string,
662
+ ): Promise<ConversationTurn | null> {
663
+ const result = await db.query<ConversationTurn>(
664
+ `
665
+ SELECT id,
666
+ thread_id,
667
+ mode,
668
+ user_message,
669
+ assistant_message,
670
+ reasoning_effort,
671
+ requested_recipient_agent_id,
672
+ selected_agent_id,
673
+ orchestrator_agent_id,
674
+ worker_session_id,
675
+ selection_status,
676
+ status,
677
+ created_at,
678
+ completed_at
679
+ FROM conversation_turns
680
+ WHERE thread_id = $1
681
+ AND mode = 'execute'
682
+ ORDER BY created_at ASC
683
+ LIMIT 1
684
+ `,
685
+ [threadId],
686
+ );
687
+
688
+ return result.rows[0] ?? null;
689
+ }
690
+
691
+ export async function getThreadWithDetails(
692
+ db: PGlite,
693
+ threadId: string,
694
+ ): Promise<{
695
+ thread: ConversationThread | null;
696
+ turns: ConversationTurn[];
697
+ messages: ConversationMessage[];
698
+ events: ConversationEvent[];
699
+ }> {
700
+ const thread = await getThreadById(db, threadId);
701
+
702
+ const [turnsResult, messagesResult, eventsResult] = await Promise.all([
703
+ db.query<ConversationTurn>(
704
+ `
705
+ SELECT id,
706
+ thread_id,
707
+ mode,
708
+ user_message,
709
+ assistant_message,
710
+ reasoning_effort,
711
+ requested_recipient_agent_id,
712
+ selected_agent_id,
713
+ orchestrator_agent_id,
714
+ worker_session_id,
715
+ selection_status,
716
+ status,
717
+ created_at,
718
+ completed_at
719
+ FROM conversation_turns
720
+ WHERE thread_id = $1
721
+ ORDER BY created_at ASC
722
+ `,
723
+ [threadId],
724
+ ),
725
+ db.query<ConversationMessage>(
726
+ `
727
+ SELECT id, thread_id, turn_id, role, content, created_at
728
+ FROM conversation_messages
729
+ WHERE thread_id = $1
730
+ ORDER BY created_at ASC
731
+ `,
732
+ [threadId],
733
+ ),
734
+ db.query<ConversationEvent & { seq: number }>(
735
+ `
736
+ SELECT id, seq, thread_id, turn_id, event_name, payload_json, created_at
737
+ FROM conversation_events
738
+ WHERE thread_id = $1
739
+ ORDER BY seq ASC
740
+ `,
741
+ [threadId],
742
+ ),
743
+ ]);
744
+
745
+ return {
746
+ thread,
747
+ turns: turnsResult.rows,
748
+ messages: messagesResult.rows,
749
+ events: eventsResult.rows,
750
+ };
751
+ }
752
+
753
+ export async function getThreadMessages(
754
+ db: PGlite,
755
+ threadId: string,
756
+ limit = 40,
757
+ ): Promise<Array<{ role: string; content: string; created_at: string }>> {
758
+ const result = await db.query<{ role: string; content: string; created_at: string }>(
759
+ `SELECT role, content, created_at
760
+ FROM conversation_messages
761
+ WHERE thread_id = $1
762
+ ORDER BY created_at ASC
763
+ LIMIT $2`,
764
+ [threadId, limit],
765
+ );
766
+ return result.rows;
767
+ }
768
+
769
+ export async function storePlanProposal(
770
+ db: PGlite,
771
+ turnId: string,
772
+ proposal: unknown,
773
+ ): Promise<void> {
774
+ await db.query(
775
+ `UPDATE conversation_turns SET plan_proposal_json = $2 WHERE id = $1`,
776
+ [turnId, JSON.stringify(proposal)],
777
+ );
778
+ }
779
+
780
+ export async function getPlanProposal(
781
+ db: PGlite,
782
+ threadId: string,
783
+ ): Promise<unknown | null> {
784
+ const result = await db.query<{ plan_proposal_json: string | null }>(
785
+ `
786
+ SELECT plan_proposal_json
787
+ FROM conversation_turns
788
+ WHERE thread_id = $1
789
+ AND mode = 'plan'
790
+ AND plan_proposal_json IS NOT NULL
791
+ ORDER BY created_at DESC
792
+ LIMIT 1
793
+ `,
794
+ [threadId],
795
+ );
796
+ const row = result.rows[0];
797
+ if (!row?.plan_proposal_json) {
798
+ return null;
799
+ }
800
+ try {
801
+ return JSON.parse(row.plan_proposal_json);
802
+ } catch {
803
+ return null;
804
+ }
805
+ }