remote-codex 0.1.5 → 0.1.7

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 (36) hide show
  1. package/apps/supervisor-api/dist/index.js +7749 -5501
  2. package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
  3. package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
  4. package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
  5. package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-DisVWgDR.js} +1 -1
  6. package/apps/supervisor-web/dist/index.html +2 -2
  7. package/package.json +5 -1
  8. package/packages/agent-runtime/src/index.ts +2 -0
  9. package/packages/agent-runtime/src/registry.ts +44 -0
  10. package/packages/agent-runtime/src/types.ts +531 -0
  11. package/packages/codex/src/appServerManager.test.ts +328 -0
  12. package/packages/codex/src/appServerManager.ts +656 -0
  13. package/packages/codex/src/historyItems.ts +1185 -0
  14. package/packages/codex/src/hookHistory.ts +224 -0
  15. package/packages/codex/src/index.ts +6 -0
  16. package/packages/codex/src/jsonrpc.test.ts +58 -0
  17. package/packages/codex/src/jsonrpc.ts +198 -0
  18. package/packages/codex/src/requestMapper.test.ts +127 -0
  19. package/packages/codex/src/requestMapper.ts +511 -0
  20. package/packages/codex/src/runtimeAdapter.ts +692 -0
  21. package/packages/codex/src/types.ts +403 -0
  22. package/packages/db/migrations/0014_thread_history_items.sql +12 -0
  23. package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
  24. package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
  25. package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
  26. package/packages/db/src/client.ts +53 -0
  27. package/packages/db/src/index.ts +5 -0
  28. package/packages/db/src/migrate.test.ts +36 -0
  29. package/packages/db/src/migrate.ts +84 -0
  30. package/packages/db/src/repositories.ts +893 -0
  31. package/packages/db/src/schema.ts +177 -0
  32. package/packages/db/src/seed.ts +51 -0
  33. package/packages/shared/src/index.ts +878 -0
  34. package/scripts/service-manager.mjs +6 -4
  35. package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
  36. package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
@@ -0,0 +1,893 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { and, desc, eq, inArray } from 'drizzle-orm';
4
+
5
+ import { DatabaseClient } from './client';
6
+ import { getDefaultHostRecord } from './client';
7
+ import {
8
+ notifications,
9
+ shellSessions,
10
+ threadActivityNotes,
11
+ threadForks,
12
+ threadGoals,
13
+ threadHistoryItems,
14
+ threadPendingSteers,
15
+ threadTurnMetadata,
16
+ threads,
17
+ viewerSessions,
18
+ policies,
19
+ workspaces,
20
+ } from './schema';
21
+
22
+ export interface CreateWorkspaceRecordInput {
23
+ absPath: string;
24
+ label: string;
25
+ }
26
+
27
+ export interface CreateThreadRecordInput {
28
+ workspaceId: string;
29
+ title: string;
30
+ provider?: string;
31
+ providerSessionId: string | null;
32
+ providerTurnId?: string | null;
33
+ model?: string | null;
34
+ reasoningEffort?: string | null;
35
+ fastMode?: boolean;
36
+ fastBaseModel?: string | null;
37
+ fastBaseReasoningEffort?: string | null;
38
+ collaborationMode?: string;
39
+ approvalMode: string;
40
+ sandboxMode?: string | null;
41
+ summaryText?: string | null;
42
+ source?: 'supervisor' | 'local_codex_import';
43
+ isConnected?: boolean;
44
+ }
45
+
46
+ export interface UpdateThreadRecordInput {
47
+ provider?: string;
48
+ providerSessionId?: string | null;
49
+ providerTurnId?: string | null;
50
+ title?: string;
51
+ model?: string | null;
52
+ reasoningEffort?: string | null;
53
+ fastMode?: boolean;
54
+ fastBaseModel?: string | null;
55
+ fastBaseReasoningEffort?: string | null;
56
+ collaborationMode?: string;
57
+ approvalMode?: string;
58
+ sandboxMode?: string | null;
59
+ status?: string;
60
+ summaryText?: string | null;
61
+ lastError?: string | null;
62
+ lastTurnStartedAt?: string | null;
63
+ lastTurnCompletedAt?: string | null;
64
+ isConnected?: boolean;
65
+ updatedAt?: string;
66
+ }
67
+
68
+ export interface UpsertThreadTurnMetadataInput {
69
+ threadId: string;
70
+ turnId: string;
71
+ model?: string | null;
72
+ reasoningEffort?: string | null;
73
+ reasoningEffortAvailable?: boolean | null;
74
+ pricingModelKey?: string | null;
75
+ pricingTierKey?: string | null;
76
+ tokenUsageJson?: string | null;
77
+ }
78
+
79
+ export interface CreateThreadPendingSteerRecordInput {
80
+ threadId: string;
81
+ turnId: string;
82
+ clientRequestId?: string | null;
83
+ displayPrompt: string;
84
+ submittedPrompt: string;
85
+ }
86
+
87
+ export interface UpsertThreadHistoryItemRecordInput {
88
+ threadId: string;
89
+ turnId: string;
90
+ itemId: string;
91
+ itemJson: string;
92
+ }
93
+
94
+ export interface CreateThreadActivityNoteRecordInput {
95
+ threadId: string;
96
+ kind: string;
97
+ text: string;
98
+ anchorTurnId?: string | null;
99
+ }
100
+
101
+ export interface CreateThreadForkRecordInput {
102
+ sourceThreadId: string;
103
+ sourceTurnId?: string | null;
104
+ sourceTurnIndex?: number | null;
105
+ forkedThreadId: string;
106
+ }
107
+
108
+ export interface UpsertThreadGoalRecordInput {
109
+ threadId: string;
110
+ providerSessionId: string;
111
+ localGoalId?: string | null;
112
+ objective: string;
113
+ status: string;
114
+ tokenBudget?: number | null;
115
+ tokensUsed?: number;
116
+ timeUsedSeconds?: number;
117
+ startedAt: string;
118
+ completedAt?: string | null;
119
+ createdAt?: string;
120
+ updatedAt?: string;
121
+ }
122
+
123
+ export interface CreateShellSessionRecordInput {
124
+ workspaceId: string;
125
+ threadId: string | null;
126
+ tmuxSessionName: string;
127
+ cwd: string;
128
+ status: string;
129
+ }
130
+
131
+ export interface UpdateShellSessionRecordInput {
132
+ tmuxSessionName?: string;
133
+ cwd?: string;
134
+ status?: string;
135
+ updatedAt?: string;
136
+ lastActivityAt?: string | null;
137
+ }
138
+
139
+ export interface CreateViewerSessionRecordInput {
140
+ threadId: string | null;
141
+ shellId: string | null;
142
+ activeTab?: string | null;
143
+ }
144
+
145
+ export interface UpdateViewerSessionRecordInput {
146
+ lastHeartbeatAt?: string | null;
147
+ activeTab?: string | null;
148
+ }
149
+
150
+ export function getPolicyRecordByKey(db: DatabaseClient, key: string) {
151
+ return db.select().from(policies).where(eq(policies.key, key)).get();
152
+ }
153
+
154
+ export function upsertPolicyRecord(db: DatabaseClient, key: string, valueJson: string) {
155
+ const now = new Date().toISOString();
156
+ const existing = getPolicyRecordByKey(db, key);
157
+
158
+ if (existing) {
159
+ db.update(policies)
160
+ .set({
161
+ valueJson,
162
+ updatedAt: now
163
+ })
164
+ .where(eq(policies.key, key))
165
+ .run();
166
+ return;
167
+ }
168
+
169
+ db.insert(policies)
170
+ .values({
171
+ id: `policy-${key.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
172
+ key,
173
+ valueJson,
174
+ createdAt: now,
175
+ updatedAt: now
176
+ })
177
+ .run();
178
+ }
179
+
180
+ export function listWorkspaceRecords(db: DatabaseClient) {
181
+ return db.select().from(workspaces).orderBy(desc(workspaces.isFavorite), workspaces.label).all();
182
+ }
183
+
184
+ export function getWorkspaceRecordById(db: DatabaseClient, id: string) {
185
+ return db.select().from(workspaces).where(eq(workspaces.id, id)).get();
186
+ }
187
+
188
+ export function getWorkspaceRecordByPath(db: DatabaseClient, absPath: string) {
189
+ return db.select().from(workspaces).where(eq(workspaces.absPath, absPath)).get();
190
+ }
191
+
192
+ export function createWorkspaceRecord(db: DatabaseClient, input: CreateWorkspaceRecordInput) {
193
+ const now = new Date().toISOString();
194
+ const host = getDefaultHostRecord();
195
+ const record = {
196
+ id: randomUUID(),
197
+ hostId: host.id,
198
+ label: input.label,
199
+ absPath: input.absPath,
200
+ isFavorite: false,
201
+ createdAt: now,
202
+ lastOpenedAt: null as string | null
203
+ };
204
+
205
+ db.insert(workspaces).values(record).run();
206
+
207
+ return record;
208
+ }
209
+
210
+ export function updateWorkspaceFavorite(
211
+ db: DatabaseClient,
212
+ id: string,
213
+ isFavorite: boolean
214
+ ) {
215
+ db.update(workspaces).set({ isFavorite }).where(eq(workspaces.id, id)).run();
216
+ }
217
+
218
+ export function updateWorkspaceLabel(db: DatabaseClient, id: string, label: string) {
219
+ db.update(workspaces).set({ label }).where(eq(workspaces.id, id)).run();
220
+ }
221
+
222
+ export function touchWorkspaceOpenedAt(db: DatabaseClient, id: string) {
223
+ db.update(workspaces)
224
+ .set({ lastOpenedAt: new Date().toISOString() })
225
+ .where(eq(workspaces.id, id))
226
+ .run();
227
+ }
228
+
229
+ export function listThreadRecords(db: DatabaseClient) {
230
+ return db.select().from(threads).orderBy(desc(threads.createdAt)).all();
231
+ }
232
+
233
+ export function listThreadRecordsByWorkspaceId(db: DatabaseClient, workspaceId: string) {
234
+ return db.select().from(threads).where(eq(threads.workspaceId, workspaceId)).orderBy(desc(threads.createdAt)).all();
235
+ }
236
+
237
+ export function listThreadRecordsByIds(db: DatabaseClient, ids: string[]) {
238
+ if (ids.length === 0) {
239
+ return [];
240
+ }
241
+
242
+ return db.select().from(threads).where(inArray(threads.id, ids)).all();
243
+ }
244
+
245
+ export function getThreadRecordById(db: DatabaseClient, id: string) {
246
+ return db.select().from(threads).where(eq(threads.id, id)).get();
247
+ }
248
+
249
+ export function getThreadRecordByProviderSessionId(
250
+ db: DatabaseClient,
251
+ provider: string,
252
+ providerSessionId: string,
253
+ ) {
254
+ return db
255
+ .select()
256
+ .from(threads)
257
+ .where(
258
+ and(
259
+ eq(threads.provider, provider),
260
+ eq(threads.providerSessionId, providerSessionId),
261
+ ),
262
+ )
263
+ .get();
264
+ }
265
+
266
+ export function createThreadRecord(db: DatabaseClient, input: CreateThreadRecordInput) {
267
+ const now = new Date().toISOString();
268
+ const record = {
269
+ id: randomUUID(),
270
+ workspaceId: input.workspaceId,
271
+ provider: input.provider ?? 'codex',
272
+ providerSessionId: input.providerSessionId,
273
+ providerTurnId: input.providerTurnId ?? null,
274
+ source: input.source ?? 'supervisor',
275
+ title: input.title,
276
+ model: input.model ?? null,
277
+ reasoningEffort: input.reasoningEffort ?? null,
278
+ fastMode: input.fastMode ?? false,
279
+ fastBaseModel: input.fastBaseModel ?? null,
280
+ fastBaseReasoningEffort: input.fastBaseReasoningEffort ?? null,
281
+ collaborationMode: input.collaborationMode ?? 'default',
282
+ approvalMode: input.approvalMode,
283
+ sandboxMode: input.sandboxMode ?? null,
284
+ status: 'idle',
285
+ summaryText: input.summaryText ?? null,
286
+ lastError: null as string | null,
287
+ createdAt: now,
288
+ updatedAt: now,
289
+ lastTurnStartedAt: null as string | null,
290
+ lastTurnCompletedAt: null as string | null,
291
+ lastViewedAt: null as string | null,
292
+ isPinned: false,
293
+ isConnected: input.isConnected ?? true
294
+ };
295
+
296
+ db.insert(threads).values(record).run();
297
+ return record;
298
+ }
299
+
300
+ export function updateThreadRecord(db: DatabaseClient, id: string, input: UpdateThreadRecordInput) {
301
+ const updates = {
302
+ ...input,
303
+ updatedAt: input.updatedAt ?? new Date().toISOString()
304
+ };
305
+
306
+ db.update(threads).set(updates).where(eq(threads.id, id)).run();
307
+ }
308
+
309
+ export function deleteThreadRecord(db: DatabaseClient, id: string) {
310
+ db.delete(threads).where(eq(threads.id, id)).run();
311
+ }
312
+
313
+ export function deleteThreadsByWorkspaceId(db: DatabaseClient, workspaceId: string) {
314
+ db.delete(threads).where(eq(threads.workspaceId, workspaceId)).run();
315
+ }
316
+
317
+ export function listThreadTurnMetadataByThreadId(db: DatabaseClient, threadId: string) {
318
+ return db.select().from(threadTurnMetadata).where(eq(threadTurnMetadata.threadId, threadId)).all();
319
+ }
320
+
321
+ export function getLatestThreadTurnMetadataByThreadId(
322
+ db: DatabaseClient,
323
+ threadId: string,
324
+ ) {
325
+ return db
326
+ .select()
327
+ .from(threadTurnMetadata)
328
+ .where(eq(threadTurnMetadata.threadId, threadId))
329
+ .orderBy(desc(threadTurnMetadata.createdAt))
330
+ .get();
331
+ }
332
+
333
+ export function getThreadTurnMetadataByThreadAndTurnId(
334
+ db: DatabaseClient,
335
+ threadId: string,
336
+ turnId: string,
337
+ ) {
338
+ return db
339
+ .select()
340
+ .from(threadTurnMetadata)
341
+ .where(
342
+ and(
343
+ eq(threadTurnMetadata.threadId, threadId),
344
+ eq(threadTurnMetadata.turnId, turnId),
345
+ ),
346
+ )
347
+ .get();
348
+ }
349
+
350
+ export function upsertThreadTurnMetadata(
351
+ db: DatabaseClient,
352
+ input: UpsertThreadTurnMetadataInput,
353
+ ) {
354
+ const now = new Date().toISOString();
355
+ const existing = db
356
+ .select()
357
+ .from(threadTurnMetadata)
358
+ .where(
359
+ and(
360
+ eq(threadTurnMetadata.threadId, input.threadId),
361
+ eq(threadTurnMetadata.turnId, input.turnId),
362
+ ),
363
+ )
364
+ .get();
365
+
366
+ if (existing) {
367
+ db.update(threadTurnMetadata)
368
+ .set({
369
+ model: input.model !== undefined ? input.model : existing.model,
370
+ reasoningEffort:
371
+ input.reasoningEffort !== undefined
372
+ ? input.reasoningEffort
373
+ : existing.reasoningEffort,
374
+ reasoningEffortAvailable:
375
+ input.reasoningEffortAvailable !== undefined
376
+ ? input.reasoningEffortAvailable
377
+ : existing.reasoningEffortAvailable,
378
+ pricingModelKey:
379
+ input.pricingModelKey !== undefined
380
+ ? input.pricingModelKey
381
+ : existing.pricingModelKey,
382
+ pricingTierKey:
383
+ input.pricingTierKey !== undefined
384
+ ? input.pricingTierKey
385
+ : existing.pricingTierKey,
386
+ tokenUsageJson:
387
+ input.tokenUsageJson !== undefined
388
+ ? input.tokenUsageJson
389
+ : existing.tokenUsageJson,
390
+ updatedAt: now,
391
+ })
392
+ .where(eq(threadTurnMetadata.id, existing.id))
393
+ .run();
394
+ return;
395
+ }
396
+
397
+ db.insert(threadTurnMetadata)
398
+ .values({
399
+ id: randomUUID(),
400
+ threadId: input.threadId,
401
+ turnId: input.turnId,
402
+ model: input.model ?? null,
403
+ reasoningEffort: input.reasoningEffort ?? null,
404
+ reasoningEffortAvailable: input.reasoningEffortAvailable ?? null,
405
+ pricingModelKey: input.pricingModelKey ?? null,
406
+ pricingTierKey: input.pricingTierKey ?? null,
407
+ tokenUsageJson: input.tokenUsageJson ?? null,
408
+ createdAt: now,
409
+ updatedAt: now,
410
+ })
411
+ .run();
412
+ }
413
+
414
+ export function deleteThreadTurnMetadataByThreadId(db: DatabaseClient, threadId: string) {
415
+ db.delete(threadTurnMetadata).where(eq(threadTurnMetadata.threadId, threadId)).run();
416
+ }
417
+
418
+ export function listThreadHistoryItemRecordsByThreadId(
419
+ db: DatabaseClient,
420
+ threadId: string,
421
+ ) {
422
+ return db
423
+ .select()
424
+ .from(threadHistoryItems)
425
+ .where(eq(threadHistoryItems.threadId, threadId))
426
+ .orderBy(threadHistoryItems.createdAt)
427
+ .all();
428
+ }
429
+
430
+ export function upsertThreadHistoryItemRecord(
431
+ db: DatabaseClient,
432
+ input: UpsertThreadHistoryItemRecordInput,
433
+ ) {
434
+ const now = new Date().toISOString();
435
+ const existing = db
436
+ .select()
437
+ .from(threadHistoryItems)
438
+ .where(
439
+ and(
440
+ eq(threadHistoryItems.threadId, input.threadId),
441
+ eq(threadHistoryItems.turnId, input.turnId),
442
+ eq(threadHistoryItems.itemId, input.itemId),
443
+ ),
444
+ )
445
+ .get();
446
+
447
+ if (existing) {
448
+ db.update(threadHistoryItems)
449
+ .set({
450
+ itemJson: input.itemJson,
451
+ updatedAt: now,
452
+ })
453
+ .where(eq(threadHistoryItems.id, existing.id))
454
+ .run();
455
+ return;
456
+ }
457
+
458
+ db.insert(threadHistoryItems)
459
+ .values({
460
+ id: randomUUID(),
461
+ threadId: input.threadId,
462
+ turnId: input.turnId,
463
+ itemId: input.itemId,
464
+ itemJson: input.itemJson,
465
+ createdAt: now,
466
+ updatedAt: now,
467
+ })
468
+ .run();
469
+ }
470
+
471
+ export function deleteThreadHistoryItemRecordsByThreadId(
472
+ db: DatabaseClient,
473
+ threadId: string,
474
+ ) {
475
+ db.delete(threadHistoryItems).where(eq(threadHistoryItems.threadId, threadId)).run();
476
+ }
477
+
478
+ export function listThreadPendingSteerRecordsByThreadId(
479
+ db: DatabaseClient,
480
+ threadId: string,
481
+ ) {
482
+ return db
483
+ .select()
484
+ .from(threadPendingSteers)
485
+ .where(eq(threadPendingSteers.threadId, threadId))
486
+ .orderBy(threadPendingSteers.createdAt)
487
+ .all();
488
+ }
489
+
490
+ export function createThreadPendingSteerRecord(
491
+ db: DatabaseClient,
492
+ input: CreateThreadPendingSteerRecordInput,
493
+ ) {
494
+ const now = new Date().toISOString();
495
+ const record = {
496
+ id: randomUUID(),
497
+ threadId: input.threadId,
498
+ turnId: input.turnId,
499
+ clientRequestId: input.clientRequestId ?? null,
500
+ displayPrompt: input.displayPrompt,
501
+ submittedPrompt: input.submittedPrompt,
502
+ createdAt: now,
503
+ updatedAt: now,
504
+ };
505
+
506
+ db.insert(threadPendingSteers).values(record).run();
507
+ return record;
508
+ }
509
+
510
+ export function deleteThreadPendingSteerRecordById(db: DatabaseClient, id: string) {
511
+ db.delete(threadPendingSteers).where(eq(threadPendingSteers.id, id)).run();
512
+ }
513
+
514
+ export function deleteThreadPendingSteerRecordsByThreadId(
515
+ db: DatabaseClient,
516
+ threadId: string,
517
+ ) {
518
+ db.delete(threadPendingSteers).where(eq(threadPendingSteers.threadId, threadId)).run();
519
+ }
520
+
521
+ export function listThreadActivityNotesByThreadId(
522
+ db: DatabaseClient,
523
+ threadId: string,
524
+ ) {
525
+ return db
526
+ .select()
527
+ .from(threadActivityNotes)
528
+ .where(eq(threadActivityNotes.threadId, threadId))
529
+ .orderBy(threadActivityNotes.createdAt)
530
+ .all();
531
+ }
532
+
533
+ export function createThreadActivityNoteRecord(
534
+ db: DatabaseClient,
535
+ input: CreateThreadActivityNoteRecordInput,
536
+ ) {
537
+ const record = {
538
+ id: randomUUID(),
539
+ threadId: input.threadId,
540
+ kind: input.kind,
541
+ text: input.text,
542
+ anchorTurnId: input.anchorTurnId ?? null,
543
+ createdAt: new Date().toISOString(),
544
+ };
545
+
546
+ db.insert(threadActivityNotes).values(record).run();
547
+ return record;
548
+ }
549
+
550
+ export function deleteThreadActivityNotesByThreadId(
551
+ db: DatabaseClient,
552
+ threadId: string,
553
+ ) {
554
+ db.delete(threadActivityNotes).where(eq(threadActivityNotes.threadId, threadId)).run();
555
+ }
556
+
557
+ export function listThreadForkRecordsBySourceThreadId(
558
+ db: DatabaseClient,
559
+ sourceThreadId: string,
560
+ ) {
561
+ return db
562
+ .select()
563
+ .from(threadForks)
564
+ .where(eq(threadForks.sourceThreadId, sourceThreadId))
565
+ .orderBy(threadForks.createdAt)
566
+ .all();
567
+ }
568
+
569
+ export function listThreadForkRecordsByForkedThreadId(
570
+ db: DatabaseClient,
571
+ forkedThreadId: string,
572
+ ) {
573
+ return db
574
+ .select()
575
+ .from(threadForks)
576
+ .where(eq(threadForks.forkedThreadId, forkedThreadId))
577
+ .orderBy(threadForks.createdAt)
578
+ .all();
579
+ }
580
+
581
+ export function createThreadForkRecord(
582
+ db: DatabaseClient,
583
+ input: CreateThreadForkRecordInput,
584
+ ) {
585
+ const record = {
586
+ id: randomUUID(),
587
+ sourceThreadId: input.sourceThreadId,
588
+ sourceTurnId: input.sourceTurnId ?? null,
589
+ sourceTurnIndex: input.sourceTurnIndex ?? null,
590
+ forkedThreadId: input.forkedThreadId,
591
+ createdAt: new Date().toISOString(),
592
+ };
593
+
594
+ db.insert(threadForks).values(record).run();
595
+ return record;
596
+ }
597
+
598
+ export function deleteThreadForkRecordsBySourceThreadId(
599
+ db: DatabaseClient,
600
+ sourceThreadId: string,
601
+ ) {
602
+ db.delete(threadForks).where(eq(threadForks.sourceThreadId, sourceThreadId)).run();
603
+ }
604
+
605
+ export function deleteThreadForkRecordsByForkedThreadId(
606
+ db: DatabaseClient,
607
+ forkedThreadId: string,
608
+ ) {
609
+ db.delete(threadForks).where(eq(threadForks.forkedThreadId, forkedThreadId)).run();
610
+ }
611
+
612
+ export function listThreadGoalRecordsByThreadId(db: DatabaseClient, threadId: string) {
613
+ return db
614
+ .select()
615
+ .from(threadGoals)
616
+ .where(eq(threadGoals.threadId, threadId))
617
+ .orderBy(desc(threadGoals.updatedAt))
618
+ .all();
619
+ }
620
+
621
+ export function getActiveThreadGoalRecord(db: DatabaseClient, threadId: string) {
622
+ const records = db
623
+ .select()
624
+ .from(threadGoals)
625
+ .where(eq(threadGoals.threadId, threadId))
626
+ .orderBy(desc(threadGoals.updatedAt))
627
+ .all();
628
+
629
+ return records.find((record) =>
630
+ ['active', 'paused', 'budgetLimited'].includes(record.status),
631
+ ) ?? null;
632
+ }
633
+
634
+ function getThreadGoalRecordForUpsert(
635
+ db: DatabaseClient,
636
+ input: UpsertThreadGoalRecordInput,
637
+ ) {
638
+ if (input.localGoalId) {
639
+ const byId = db
640
+ .select()
641
+ .from(threadGoals)
642
+ .where(eq(threadGoals.id, input.localGoalId))
643
+ .get();
644
+ if (byId?.threadId === input.threadId) {
645
+ return byId;
646
+ }
647
+ }
648
+
649
+ const active = getActiveThreadGoalRecord(db, input.threadId);
650
+ if (active) {
651
+ return active;
652
+ }
653
+
654
+ const matchingObjective = db
655
+ .select()
656
+ .from(threadGoals)
657
+ .where(
658
+ and(
659
+ eq(threadGoals.threadId, input.threadId),
660
+ eq(threadGoals.providerSessionId, input.providerSessionId),
661
+ eq(threadGoals.objective, input.objective),
662
+ ),
663
+ )
664
+ .orderBy(desc(threadGoals.updatedAt))
665
+ .get();
666
+ if (matchingObjective) {
667
+ return matchingObjective;
668
+ }
669
+
670
+ return (
671
+ db
672
+ .select()
673
+ .from(threadGoals)
674
+ .where(
675
+ and(
676
+ eq(threadGoals.threadId, input.threadId),
677
+ eq(threadGoals.providerSessionId, input.providerSessionId),
678
+ eq(threadGoals.objective, input.objective),
679
+ eq(threadGoals.createdAt, input.createdAt ?? input.startedAt),
680
+ ),
681
+ )
682
+ .orderBy(desc(threadGoals.updatedAt))
683
+ .get() ?? null
684
+ );
685
+ }
686
+
687
+ export function upsertThreadGoalRecord(
688
+ db: DatabaseClient,
689
+ input: UpsertThreadGoalRecordInput,
690
+ ) {
691
+ const now = new Date().toISOString();
692
+ const existing = getThreadGoalRecordForUpsert(db, input);
693
+ const terminalCompletedAt =
694
+ input.completedAt ??
695
+ (['complete', 'terminated'].includes(input.status)
696
+ ? input.updatedAt ?? now
697
+ : null);
698
+
699
+ if (existing) {
700
+ const updated = {
701
+ objective: input.objective,
702
+ status: input.status,
703
+ tokenBudget: input.tokenBudget ?? null,
704
+ tokensUsed: input.tokensUsed ?? existing.tokensUsed,
705
+ timeUsedSeconds: input.timeUsedSeconds ?? existing.timeUsedSeconds,
706
+ providerSessionId: input.providerSessionId,
707
+ startedAt: input.startedAt,
708
+ completedAt: terminalCompletedAt,
709
+ updatedAt: input.updatedAt ?? now,
710
+ };
711
+ db.update(threadGoals).set(updated).where(eq(threadGoals.id, existing.id)).run();
712
+ return { ...existing, ...updated };
713
+ }
714
+
715
+ const record = {
716
+ id: randomUUID(),
717
+ threadId: input.threadId,
718
+ providerSessionId: input.providerSessionId,
719
+ objective: input.objective,
720
+ status: input.status,
721
+ tokenBudget: input.tokenBudget ?? null,
722
+ tokensUsed: input.tokensUsed ?? 0,
723
+ timeUsedSeconds: input.timeUsedSeconds ?? 0,
724
+ startedAt: input.startedAt,
725
+ completedAt: terminalCompletedAt,
726
+ createdAt: input.createdAt ?? now,
727
+ updatedAt: input.updatedAt ?? now,
728
+ };
729
+ db.insert(threadGoals).values(record).run();
730
+ return record;
731
+ }
732
+
733
+ export function markActiveThreadGoalRecordTerminated(
734
+ db: DatabaseClient,
735
+ threadId: string,
736
+ ) {
737
+ const existing = getActiveThreadGoalRecord(db, threadId);
738
+ if (!existing) {
739
+ return null;
740
+ }
741
+
742
+ const now = new Date().toISOString();
743
+ const updates = {
744
+ status: 'terminated',
745
+ completedAt: now,
746
+ updatedAt: now,
747
+ };
748
+ db.update(threadGoals).set(updates).where(eq(threadGoals.id, existing.id)).run();
749
+ return { ...existing, ...updates };
750
+ }
751
+
752
+ export function deleteThreadGoalRecordsByThreadId(db: DatabaseClient, threadId: string) {
753
+ db.delete(threadGoals).where(eq(threadGoals.threadId, threadId)).run();
754
+ }
755
+
756
+ export function listShellSessionRecords(db: DatabaseClient) {
757
+ return db.select().from(shellSessions).orderBy(desc(shellSessions.updatedAt)).all();
758
+ }
759
+
760
+ export function listShellSessionRecordsByWorkspaceId(db: DatabaseClient, workspaceId: string) {
761
+ return db
762
+ .select()
763
+ .from(shellSessions)
764
+ .where(eq(shellSessions.workspaceId, workspaceId))
765
+ .orderBy(desc(shellSessions.updatedAt))
766
+ .all();
767
+ }
768
+
769
+ export function getShellSessionRecordById(db: DatabaseClient, id: string) {
770
+ return db.select().from(shellSessions).where(eq(shellSessions.id, id)).get();
771
+ }
772
+
773
+ export function getShellSessionRecordByThreadId(db: DatabaseClient, threadId: string) {
774
+ return db.select().from(shellSessions).where(eq(shellSessions.threadId, threadId)).get();
775
+ }
776
+
777
+ export function createShellSessionRecord(
778
+ db: DatabaseClient,
779
+ input: CreateShellSessionRecordInput,
780
+ ) {
781
+ const now = new Date().toISOString();
782
+ const record = {
783
+ id: randomUUID(),
784
+ workspaceId: input.workspaceId,
785
+ threadId: input.threadId,
786
+ tmuxSessionName: input.tmuxSessionName,
787
+ cwd: input.cwd,
788
+ status: input.status,
789
+ createdAt: now,
790
+ updatedAt: now,
791
+ lastActivityAt: now,
792
+ };
793
+
794
+ db.insert(shellSessions).values(record).run();
795
+ return record;
796
+ }
797
+
798
+ export function updateShellSessionRecord(
799
+ db: DatabaseClient,
800
+ id: string,
801
+ input: UpdateShellSessionRecordInput,
802
+ ) {
803
+ const updates = {
804
+ ...input,
805
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
806
+ };
807
+
808
+ db.update(shellSessions).set(updates).where(eq(shellSessions.id, id)).run();
809
+ }
810
+
811
+ export function deleteShellSessionRecord(db: DatabaseClient, id: string) {
812
+ db.delete(shellSessions).where(eq(shellSessions.id, id)).run();
813
+ }
814
+
815
+ export function deleteShellSessionsByThreadId(db: DatabaseClient, threadId: string) {
816
+ db.delete(shellSessions).where(eq(shellSessions.threadId, threadId)).run();
817
+ }
818
+
819
+ export function deleteShellSessionsByWorkspaceId(db: DatabaseClient, workspaceId: string) {
820
+ db.delete(shellSessions).where(eq(shellSessions.workspaceId, workspaceId)).run();
821
+ }
822
+
823
+ export function createViewerSessionRecord(
824
+ db: DatabaseClient,
825
+ input: CreateViewerSessionRecordInput,
826
+ ) {
827
+ const now = new Date().toISOString();
828
+ const record = {
829
+ id: randomUUID(),
830
+ threadId: input.threadId ?? null,
831
+ shellId: input.shellId ?? null,
832
+ connectedAt: now,
833
+ lastHeartbeatAt: now,
834
+ activeTab: input.activeTab ?? null,
835
+ };
836
+
837
+ db.insert(viewerSessions).values(record).run();
838
+ return record;
839
+ }
840
+
841
+ export function getViewerSessionRecordById(db: DatabaseClient, id: string) {
842
+ return db.select().from(viewerSessions).where(eq(viewerSessions.id, id)).get();
843
+ }
844
+
845
+ export function getViewerSessionRecordByShellId(db: DatabaseClient, shellId: string) {
846
+ return db.select().from(viewerSessions).where(eq(viewerSessions.shellId, shellId)).get();
847
+ }
848
+
849
+ export function updateViewerSessionRecord(
850
+ db: DatabaseClient,
851
+ id: string,
852
+ input: UpdateViewerSessionRecordInput,
853
+ ) {
854
+ db.update(viewerSessions)
855
+ .set(input)
856
+ .where(eq(viewerSessions.id, id))
857
+ .run();
858
+ }
859
+
860
+ export function clearViewerSessionShell(db: DatabaseClient, id: string) {
861
+ db.update(viewerSessions)
862
+ .set({
863
+ shellId: null,
864
+ lastHeartbeatAt: new Date().toISOString(),
865
+ activeTab: null,
866
+ })
867
+ .where(eq(viewerSessions.id, id))
868
+ .run();
869
+ }
870
+
871
+ export function deleteViewerSessionRecord(db: DatabaseClient, id: string) {
872
+ db.delete(viewerSessions).where(eq(viewerSessions.id, id)).run();
873
+ }
874
+
875
+ export function deleteViewerSessionsByShellId(db: DatabaseClient, shellId: string) {
876
+ db.delete(viewerSessions).where(eq(viewerSessions.shellId, shellId)).run();
877
+ }
878
+
879
+ export function deleteViewerSessionsByThreadId(db: DatabaseClient, threadId: string) {
880
+ db.delete(viewerSessions).where(eq(viewerSessions.threadId, threadId)).run();
881
+ }
882
+
883
+ export function deleteAllViewerSessionRecords(db: DatabaseClient) {
884
+ db.delete(viewerSessions).run();
885
+ }
886
+
887
+ export function deleteNotificationsByThreadId(db: DatabaseClient, threadId: string) {
888
+ db.delete(notifications).where(eq(notifications.threadId, threadId)).run();
889
+ }
890
+
891
+ export function deleteWorkspaceRecord(db: DatabaseClient, id: string) {
892
+ db.delete(workspaces).where(eq(workspaces.id, id)).run();
893
+ }