@m6d/cortex-server 2.3.0 → 2.5.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 (62) hide show
  1. package/contracts/src/runtime/index.ts +99 -3
  2. package/dist/contracts/src/graph/helpers.d.ts +5 -5
  3. package/dist/contracts/src/runtime/index.d.ts +175 -3
  4. package/dist/src/lib/adapters/database/index.d.ts +20 -2
  5. package/dist/src/lib/adapters/database/mssql/attachments.d.ts +9 -9
  6. package/dist/src/lib/adapters/database/mssql/index.d.ts +133 -28
  7. package/dist/src/lib/adapters/database/mssql/llm-requests.d.ts +3 -2
  8. package/dist/src/lib/adapters/database/mssql/messages.d.ts +6 -5
  9. package/dist/src/lib/adapters/database/mssql/outbox.d.ts +103 -0
  10. package/dist/src/lib/adapters/database/mssql/threads.d.ts +21 -14
  11. package/dist/src/lib/adapters/database/postgres/attachments.d.ts +9 -9
  12. package/dist/src/lib/adapters/database/postgres/index.d.ts +133 -28
  13. package/dist/src/lib/adapters/database/postgres/llm-requests.d.ts +3 -2
  14. package/dist/src/lib/adapters/database/postgres/messages.d.ts +6 -5
  15. package/dist/src/lib/adapters/database/postgres/outbox.d.ts +103 -0
  16. package/dist/src/lib/adapters/database/postgres/threads.d.ts +21 -14
  17. package/dist/src/lib/ai/attachments.d.ts +2 -2
  18. package/dist/src/lib/ai/client-tools.d.ts +1 -1
  19. package/dist/src/lib/auth/middleware.d.ts +2 -2
  20. package/dist/src/lib/auth/playground.d.ts +14 -0
  21. package/dist/src/lib/cc/client.d.ts +9 -1
  22. package/dist/src/lib/cc/flusher.d.ts +40 -0
  23. package/dist/src/lib/cc/sync-events.d.ts +45 -0
  24. package/dist/src/lib/cc/types.d.ts +1 -1
  25. package/dist/src/lib/config.d.ts +11 -0
  26. package/dist/src/lib/db/schema.mssql.d.ts +161 -0
  27. package/dist/src/lib/db/schema.pg.d.ts +199 -0
  28. package/dist/src/lib/factory.d.ts +3 -0
  29. package/dist/src/lib/routes/owned-thread.d.ts +5 -4
  30. package/package.json +2 -2
  31. package/src/lib/adapters/database/index.ts +15 -1
  32. package/src/lib/adapters/database/mssql/index.ts +13 -4
  33. package/src/lib/adapters/database/mssql/llm-requests.ts +35 -15
  34. package/src/lib/adapters/database/mssql/messages.ts +81 -30
  35. package/src/lib/adapters/database/mssql/outbox.ts +71 -0
  36. package/src/lib/adapters/database/mssql/threads.ts +42 -11
  37. package/src/lib/adapters/database/postgres/index.ts +13 -4
  38. package/src/lib/adapters/database/postgres/llm-requests.ts +35 -15
  39. package/src/lib/adapters/database/postgres/messages.ts +81 -30
  40. package/src/lib/adapters/database/postgres/outbox.ts +66 -0
  41. package/src/lib/adapters/database/postgres/threads.ts +39 -11
  42. package/src/lib/ai/finish-turn.ts +3 -1
  43. package/src/lib/ai/prompt.ts +3 -1
  44. package/src/lib/auth/middleware.ts +34 -8
  45. package/src/lib/auth/playground.ts +72 -0
  46. package/src/lib/cc/client.ts +21 -1
  47. package/src/lib/cc/flusher.ts +147 -0
  48. package/src/lib/cc/sync-events.ts +65 -0
  49. package/src/lib/cc/types.ts +3 -0
  50. package/src/lib/config.ts +11 -0
  51. package/src/lib/db/migrations/mssql/20260829050547_friendly_red_shift/migration.sql +8 -0
  52. package/src/lib/db/migrations/mssql/20260829050547_friendly_red_shift/snapshot.json +581 -0
  53. package/src/lib/db/migrations/mssql/20260830022424_easy_nicolaos/migration.sql +5 -0
  54. package/src/lib/db/migrations/mssql/20260830022424_easy_nicolaos/snapshot.json +614 -0
  55. package/src/lib/db/migrations/pg/20260829050547_perfect_wildside/migration.sql +7 -0
  56. package/src/lib/db/migrations/pg/20260829050547_perfect_wildside/snapshot.json +650 -0
  57. package/src/lib/db/migrations/pg/20260830022424_serious_deadpool/migration.sql +4 -0
  58. package/src/lib/db/migrations/pg/20260830022424_serious_deadpool/snapshot.json +690 -0
  59. package/src/lib/db/schema.mssql.ts +32 -1
  60. package/src/lib/db/schema.pg.ts +24 -0
  61. package/src/lib/factory.ts +22 -8
  62. package/src/lib/routes/threads.ts +5 -1
@@ -101,6 +101,22 @@ export declare const threads: import("drizzle-orm/mssql-core").MsSqlTableWithCol
101
101
  identity: undefined;
102
102
  generated: undefined;
103
103
  }, {}>;
104
+ isTest: import("drizzle-orm/mssql-core").MsSqlColumn<{
105
+ name: string;
106
+ tableName: "threads";
107
+ dataType: "boolean";
108
+ data: boolean;
109
+ driverParam: string | number;
110
+ notNull: true;
111
+ hasDefault: true;
112
+ isPrimaryKey: false;
113
+ isAutoincrement: false;
114
+ hasRuntimeDefault: false;
115
+ enumValues: undefined;
116
+ baseColumn: never;
117
+ identity: undefined;
118
+ generated: undefined;
119
+ }, {}>;
104
120
  session: import("drizzle-orm/mssql-core").MsSqlColumn<{
105
121
  name: string;
106
122
  tableName: "threads";
@@ -573,3 +589,148 @@ export declare const attachments: import("drizzle-orm/mssql-core").MsSqlTableWit
573
589
  };
574
590
  dialect: "mssql";
575
591
  }>;
592
+ /**
593
+ * Events awaiting the cc sync flusher. Insertion order (the identity id) is
594
+ * flush order; `ack` deletes.
595
+ */
596
+ /** Single row naming this database's generation. `epoch` is minted on first
597
+ * flush and changes only when the database is recreated, letting cc tell a
598
+ * reset outbox sequence from a stale sync batch. */
599
+ export declare const syncMeta: import("drizzle-orm/mssql-core").MsSqlTableWithColumns<{
600
+ name: "sync_meta";
601
+ schema: "ai";
602
+ columns: {
603
+ id: import("drizzle-orm/mssql-core").MsSqlColumn<{
604
+ name: string;
605
+ tableName: "sync_meta";
606
+ dataType: "number int32";
607
+ data: number;
608
+ driverParam: number;
609
+ notNull: true;
610
+ hasDefault: false;
611
+ isPrimaryKey: true;
612
+ isAutoincrement: false;
613
+ hasRuntimeDefault: false;
614
+ enumValues: undefined;
615
+ baseColumn: never;
616
+ identity: undefined;
617
+ generated: undefined;
618
+ }, {}>;
619
+ epoch: import("drizzle-orm/mssql-core").MsSqlColumn<{
620
+ name: string;
621
+ tableName: "sync_meta";
622
+ dataType: "string";
623
+ data: string;
624
+ driverParam: string | number;
625
+ notNull: true;
626
+ hasDefault: false;
627
+ isPrimaryKey: false;
628
+ isAutoincrement: false;
629
+ hasRuntimeDefault: false;
630
+ enumValues: [string, ...string[]];
631
+ baseColumn: never;
632
+ identity: undefined;
633
+ generated: undefined;
634
+ }, {}>;
635
+ };
636
+ dialect: "mssql";
637
+ }>;
638
+ export declare const syncOutbox: import("drizzle-orm/mssql-core").MsSqlTableWithColumns<{
639
+ name: "sync_outbox";
640
+ schema: "ai";
641
+ columns: {
642
+ id: import("drizzle-orm/mssql-core").MsSqlColumn<{
643
+ name: string;
644
+ tableName: "sync_outbox";
645
+ dataType: "number int53";
646
+ data: number;
647
+ driverParam: string;
648
+ notNull: true;
649
+ hasDefault: true;
650
+ isPrimaryKey: true;
651
+ isAutoincrement: false;
652
+ hasRuntimeDefault: false;
653
+ enumValues: undefined;
654
+ baseColumn: never;
655
+ identity: undefined;
656
+ generated: {};
657
+ }, {}>;
658
+ event: import("drizzle-orm/mssql-core").MsSqlColumn<{
659
+ name: string;
660
+ tableName: "sync_outbox";
661
+ dataType: "object json";
662
+ data: {
663
+ type: "thread.upsert";
664
+ id: string;
665
+ agentSlug: string;
666
+ userId: string;
667
+ title: string | null;
668
+ isTest: boolean;
669
+ createdAt: string;
670
+ updatedAt: string;
671
+ } | {
672
+ type: "thread.delete";
673
+ id: string;
674
+ } | {
675
+ type: "message.upsert";
676
+ id: string;
677
+ threadId: string;
678
+ role: "system" | "user" | "assistant" | "tool";
679
+ text: string | null;
680
+ ordinal: number;
681
+ content: {
682
+ [x: string]: unknown;
683
+ };
684
+ createdAt: string;
685
+ } | {
686
+ type: "usage.record";
687
+ id: string;
688
+ messageId: string;
689
+ threadId: string;
690
+ stepNumber: number;
691
+ tokenUsage: {
692
+ input: {
693
+ noCache: number;
694
+ cacheRead: number;
695
+ cacheWrite: number;
696
+ total: number;
697
+ };
698
+ output: {
699
+ reasoning: number;
700
+ text: number;
701
+ total: number;
702
+ };
703
+ total: number;
704
+ };
705
+ createdAt: string;
706
+ };
707
+ driverParam: string;
708
+ notNull: true;
709
+ hasDefault: false;
710
+ isPrimaryKey: false;
711
+ isAutoincrement: false;
712
+ hasRuntimeDefault: false;
713
+ enumValues: undefined;
714
+ baseColumn: never;
715
+ identity: undefined;
716
+ generated: undefined;
717
+ }, {}>;
718
+ createdAt: import("drizzle-orm/mssql-core").MsSqlColumn<{
719
+ name: string;
720
+ tableName: "sync_outbox";
721
+ dataType: "object date";
722
+ data: Date;
723
+ driverParam: string | Date;
724
+ notNull: true;
725
+ hasDefault: true;
726
+ isPrimaryKey: false;
727
+ isAutoincrement: false;
728
+ hasRuntimeDefault: true;
729
+ enumValues: undefined;
730
+ baseColumn: never;
731
+ identity: undefined;
732
+ generated: undefined;
733
+ }, {}>;
734
+ };
735
+ dialect: "mssql";
736
+ }>;
@@ -100,6 +100,21 @@ export declare const threads: import("drizzle-orm/pg-core").PgTableWithColumns<{
100
100
  identity: undefined;
101
101
  generated: undefined;
102
102
  }>;
103
+ isTest: import("drizzle-orm/pg-core").PgBuildColumn<"threads", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgBooleanBuilder>>, {
104
+ name: string;
105
+ tableName: "threads";
106
+ dataType: "boolean";
107
+ data: boolean;
108
+ driverParam: boolean;
109
+ notNull: true;
110
+ hasDefault: true;
111
+ isPrimaryKey: false;
112
+ isAutoincrement: false;
113
+ hasRuntimeDefault: false;
114
+ enumValues: undefined;
115
+ identity: undefined;
116
+ generated: undefined;
117
+ }>;
103
118
  session: import("drizzle-orm/pg-core").PgBuildColumn<"threads", import("drizzle-orm/pg-core").Set$Type<import("drizzle-orm/pg-core").PgJsonbBuilder, Record<string, unknown>>, {
104
119
  name: string;
105
120
  tableName: "threads";
@@ -544,3 +559,187 @@ export declare const attachments: import("drizzle-orm/pg-core").PgTableWithColum
544
559
  };
545
560
  dialect: "pg";
546
561
  }>;
562
+ /**
563
+ * Events awaiting the cc sync flusher. Insertion order (the bigserial id) is
564
+ * flush order; `ack` deletes.
565
+ */
566
+ /** Single row naming this database's generation. `epoch` is minted on first
567
+ * flush and changes only when the database is recreated, letting cc tell a
568
+ * reset outbox sequence from a stale sync batch. */
569
+ export declare const syncMeta: import("drizzle-orm/pg-core").PgTableWithColumns<{
570
+ name: "sync_meta";
571
+ schema: "ai";
572
+ columns: {
573
+ id: import("drizzle-orm/pg-core").PgBuildColumn<"sync_meta", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgIntegerBuilder>, {
574
+ name: string;
575
+ tableName: "sync_meta";
576
+ dataType: "number int32";
577
+ data: number;
578
+ driverParam: string | number;
579
+ notNull: true;
580
+ hasDefault: false;
581
+ isPrimaryKey: false;
582
+ isAutoincrement: false;
583
+ hasRuntimeDefault: false;
584
+ enumValues: undefined;
585
+ identity: undefined;
586
+ generated: undefined;
587
+ }>;
588
+ epoch: import("drizzle-orm/pg-core").PgBuildColumn<"sync_meta", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgVarcharBuilder<[string, ...string[]]>>, {
589
+ name: string;
590
+ tableName: "sync_meta";
591
+ dataType: "string";
592
+ data: string;
593
+ driverParam: string;
594
+ notNull: true;
595
+ hasDefault: false;
596
+ isPrimaryKey: false;
597
+ isAutoincrement: false;
598
+ hasRuntimeDefault: false;
599
+ enumValues: undefined;
600
+ identity: undefined;
601
+ generated: undefined;
602
+ }>;
603
+ };
604
+ dialect: "pg";
605
+ }>;
606
+ export declare const syncOutbox: import("drizzle-orm/pg-core").PgTableWithColumns<{
607
+ name: "sync_outbox";
608
+ schema: "ai";
609
+ columns: {
610
+ id: import("drizzle-orm/pg-core").PgBuildColumn<"sync_outbox", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgBigSerial53Builder>, {
611
+ name: string;
612
+ tableName: "sync_outbox";
613
+ dataType: "number int53";
614
+ data: number;
615
+ driverParam: number;
616
+ notNull: true;
617
+ hasDefault: true;
618
+ isPrimaryKey: false;
619
+ isAutoincrement: false;
620
+ hasRuntimeDefault: false;
621
+ enumValues: undefined;
622
+ identity: undefined;
623
+ generated: undefined;
624
+ }>;
625
+ event: import("drizzle-orm/pg-core").PgBuildColumn<"sync_outbox", import("drizzle-orm/pg-core").Set$Type<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgJsonbBuilder>, {
626
+ type: "thread.upsert";
627
+ id: string;
628
+ agentSlug: string;
629
+ userId: string;
630
+ title: string | null;
631
+ isTest: boolean;
632
+ createdAt: string;
633
+ updatedAt: string;
634
+ } | {
635
+ type: "thread.delete";
636
+ id: string;
637
+ } | {
638
+ type: "message.upsert";
639
+ id: string;
640
+ threadId: string;
641
+ role: "system" | "user" | "assistant" | "tool";
642
+ text: string | null;
643
+ ordinal: number;
644
+ content: {
645
+ [x: string]: unknown;
646
+ };
647
+ createdAt: string;
648
+ } | {
649
+ type: "usage.record";
650
+ id: string;
651
+ messageId: string;
652
+ threadId: string;
653
+ stepNumber: number;
654
+ tokenUsage: {
655
+ input: {
656
+ noCache: number;
657
+ cacheRead: number;
658
+ cacheWrite: number;
659
+ total: number;
660
+ };
661
+ output: {
662
+ reasoning: number;
663
+ text: number;
664
+ total: number;
665
+ };
666
+ total: number;
667
+ };
668
+ createdAt: string;
669
+ }>, {
670
+ name: string;
671
+ tableName: "sync_outbox";
672
+ dataType: "object json";
673
+ data: {
674
+ type: "thread.upsert";
675
+ id: string;
676
+ agentSlug: string;
677
+ userId: string;
678
+ title: string | null;
679
+ isTest: boolean;
680
+ createdAt: string;
681
+ updatedAt: string;
682
+ } | {
683
+ type: "thread.delete";
684
+ id: string;
685
+ } | {
686
+ type: "message.upsert";
687
+ id: string;
688
+ threadId: string;
689
+ role: "system" | "user" | "assistant" | "tool";
690
+ text: string | null;
691
+ ordinal: number;
692
+ content: {
693
+ [x: string]: unknown;
694
+ };
695
+ createdAt: string;
696
+ } | {
697
+ type: "usage.record";
698
+ id: string;
699
+ messageId: string;
700
+ threadId: string;
701
+ stepNumber: number;
702
+ tokenUsage: {
703
+ input: {
704
+ noCache: number;
705
+ cacheRead: number;
706
+ cacheWrite: number;
707
+ total: number;
708
+ };
709
+ output: {
710
+ reasoning: number;
711
+ text: number;
712
+ total: number;
713
+ };
714
+ total: number;
715
+ };
716
+ createdAt: string;
717
+ };
718
+ driverParam: unknown;
719
+ notNull: true;
720
+ hasDefault: false;
721
+ isPrimaryKey: false;
722
+ isAutoincrement: false;
723
+ hasRuntimeDefault: false;
724
+ enumValues: undefined;
725
+ identity: undefined;
726
+ generated: undefined;
727
+ }>;
728
+ createdAt: import("drizzle-orm/pg-core").PgBuildColumn<"sync_outbox", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").SetHasRuntimeDefault<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
729
+ name: string;
730
+ tableName: "sync_outbox";
731
+ dataType: "object date";
732
+ data: Date;
733
+ driverParam: string;
734
+ notNull: true;
735
+ hasDefault: true;
736
+ isPrimaryKey: false;
737
+ isAutoincrement: false;
738
+ hasRuntimeDefault: false;
739
+ enumValues: undefined;
740
+ identity: undefined;
741
+ generated: undefined;
742
+ }>;
743
+ };
744
+ dialect: "pg";
745
+ }>;
@@ -9,6 +9,8 @@ export type CortexInstance = {
9
9
  fetch: Hono<AppEnv>["fetch"];
10
10
  websocket: typeof websocket;
11
11
  }>;
12
+ /** Stops the cc sync flusher (a no-op before `serve` or without cc). */
13
+ stop(): void;
12
14
  };
13
15
  export declare function createCortex(config: CortexConfig): {
14
16
  serve(): Promise<{
@@ -17,4 +19,5 @@ export declare function createCortex(config: CortexConfig): {
17
19
  fetch: (request: Request, Env?: {} | undefined, executionCtx?: import("hono").ExecutionContext) => Response | Promise<Response>;
18
20
  websocket: import("hono/bun").BunWebSocketHandler<import("hono/bun").BunWebSocketData>;
19
21
  }>;
22
+ stop(): void;
20
23
  };
@@ -6,12 +6,13 @@ import type { CortexAppEnv } from "../types";
6
6
  * would confirm the thread exists, making cross-agent ids probeable.
7
7
  */
8
8
  export declare function requireOwnedThread(c: Context<CortexAppEnv>, threadId: string): Promise<{
9
- id: string;
10
- userId: string;
11
9
  agentId: string;
10
+ userId: string;
11
+ id: string;
12
12
  title: string | null;
13
- session: Record<string, unknown> | null;
14
- contextMeta: import("..").ThreadContextMeta | null;
13
+ isTest: boolean;
15
14
  createdAt: Date;
16
15
  updatedAt: Date;
16
+ session: Record<string, unknown> | null;
17
+ contextMeta: import("..").ThreadContextMeta | null;
17
18
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-server",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Reusable AI agent chat server library for Hono + Bun",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "devDependencies": {
50
50
  "@tanstack/ai": "^0.43.1",
51
51
  "@tanstack/ai-openai": "^0.18.0",
52
- "@types/bun": "latest",
52
+ "@types/bun": "1.3.14",
53
53
  "@types/minio": "^7.1.1",
54
54
  "@types/mssql": "^9.1.5",
55
55
  "@types/pg": "^8.15.6",
@@ -1,3 +1,4 @@
1
+ import type { SyncEvent } from "@cortex/contracts/runtime";
1
2
  import type {
2
3
  Attachment,
3
4
  ChatMessage,
@@ -28,7 +29,7 @@ export type DatabaseAdapter = {
28
29
  threads: {
29
30
  list(userId: string, agentId: string): Promise<Thread[]>;
30
31
  getById(userId: string, threadId: string): Promise<Thread | null>;
31
- create(userId: string, agentId: string): Promise<Thread>;
32
+ create(userId: string, agentId: string, options?: { isTest?: boolean }): Promise<Thread>;
32
33
  delete(userId: string, threadId: string): Promise<void>;
33
34
  touch(threadId: string): Promise<Thread>;
34
35
  updateTitle(threadId: string, title: string): Promise<void>;
@@ -45,6 +46,7 @@ export type DatabaseAdapter = {
45
46
  };
46
47
  llmRequests: {
47
48
  insert(
49
+ threadId: string,
48
50
  requests: {
49
51
  messageId: string;
50
52
  stepNumber: number;
@@ -75,4 +77,16 @@ export type DatabaseAdapter = {
75
77
  ): Promise<void>;
76
78
  remove(id: string, userId: string): Promise<Attachment | undefined>;
77
79
  };
80
+ /**
81
+ * The cc sync queue. Write sites enqueue through the dialect's
82
+ * `enqueueSyncEvents` inside their own transaction; the flusher drains via
83
+ * `peek` (id ascending = insertion order) and `ack` (delete) here.
84
+ */
85
+ outbox: {
86
+ enqueue(events: SyncEvent[]): Promise<void>;
87
+ peek(limit: number): Promise<{ id: number; event: SyncEvent }[]>;
88
+ ack(ids: number[]): Promise<void>;
89
+ /** This database generation's id, minted on first use (see `syncMeta`). */
90
+ epoch(): Promise<string>;
91
+ };
78
92
  };
@@ -5,19 +5,28 @@ import { createThreadsRepository } from "./threads";
5
5
  import { createMessagesRepository } from "./messages";
6
6
  import { createLlmRequestsRepository } from "./llm-requests";
7
7
  import { createAttachmentsRepository } from "./attachments";
8
+ import { createOutboxRepository, enqueueSyncEvents } from "./outbox";
8
9
 
9
10
  /**
10
11
  * The MSSQL implementation of the database contract. One repository per table;
11
12
  * only threads takes storage, because deleting a thread is the one operation
12
13
  * that also has to reach into object storage.
13
14
  */
14
- export function createMssqlAdapter(connectionString: string, storage?: StorageAdapter) {
15
+ export function createMssqlAdapter(
16
+ connectionString: string,
17
+ storage?: StorageAdapter,
18
+ options?: { syncMirror?: boolean },
19
+ ) {
15
20
  const db = createMssqlDb(connectionString);
21
+ // Without a Control Center there is no flusher to drain the outbox, so
22
+ // enqueueing would only grow sync_outbox forever.
23
+ const enqueue = options?.syncMirror === false ? async () => {} : enqueueSyncEvents;
16
24
 
17
25
  return {
18
- threads: createThreadsRepository(db, storage),
19
- messages: createMessagesRepository(db),
20
- llmRequests: createLlmRequestsRepository(db),
26
+ threads: createThreadsRepository(db, storage, enqueue),
27
+ messages: createMessagesRepository(db, enqueue),
28
+ llmRequests: createLlmRequestsRepository(db, enqueue),
21
29
  attachments: createAttachmentsRepository(db),
30
+ outbox: createOutboxRepository(db),
22
31
  } satisfies DatabaseAdapter;
23
32
  }
@@ -1,28 +1,48 @@
1
1
  import { asc, eq, type InferInsertModel } from "drizzle-orm";
2
2
  import { llmRequests } from "@/db/schema.mssql";
3
+ import { usageRecordEvent } from "@/cc/sync-events";
3
4
  import type { DatabaseAdapter } from "@/adapters/database/index";
4
5
  import type { MssqlDb } from "./client";
6
+ import { enqueueSyncEvents } from "./outbox";
5
7
 
6
- export function createLlmRequestsRepository(db: MssqlDb) {
8
+ export function createLlmRequestsRepository(
9
+ db: MssqlDb,
10
+ enqueue: typeof enqueueSyncEvents = enqueueSyncEvents,
11
+ ) {
7
12
  return {
8
- async insert(requests) {
13
+ async insert(threadId, requests) {
9
14
  if (!requests.length) return;
10
15
 
11
- await db
12
- .insert(llmRequests)
13
- .values(
14
- requests.map(
15
- (r) =>
16
- ({
16
+ await db.transaction(async function (tx) {
17
+ await tx
18
+ .insert(llmRequests)
19
+ .values(
20
+ requests.map(
21
+ (r) =>
22
+ ({
23
+ messageId: r.messageId,
24
+ stepNumber: r.stepNumber,
25
+ prompt: r.prompt,
26
+ output: r.output,
27
+ tokenUsage: r.tokenUsage,
28
+ }) satisfies InferInsertModel<typeof llmRequests>,
29
+ ),
30
+ )
31
+ .execute();
32
+ await enqueue(
33
+ tx,
34
+ requests
35
+ .filter((r) => r.tokenUsage !== null)
36
+ .map((r) =>
37
+ usageRecordEvent({
38
+ threadId,
17
39
  messageId: r.messageId,
18
40
  stepNumber: r.stepNumber,
19
- prompt: r.prompt,
20
- output: r.output,
21
- tokenUsage: r.tokenUsage,
22
- }) satisfies InferInsertModel<typeof llmRequests>,
23
- ),
24
- )
25
- .execute();
41
+ tokenUsage: r.tokenUsage!,
42
+ }),
43
+ ),
44
+ );
45
+ });
26
46
  },
27
47
 
28
48
  /** Ordered by step so the inspector replays a turn in the order it ran. */