remote-codex 0.1.6 → 0.1.8

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