@pikku/kysely 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ce961b5: fix: improve MySQL compatibility in AI storage service by using varchar columns with explicit lengths instead of text for primary keys, foreign keys, and indexed columns, and handle duplicate index errors gracefully
8
+ - 3e04565: chore: update dependencies to latest minor/patch versions
9
+ - Updated dependencies [cc4c9e9]
10
+ - Updated dependencies [3e04565]
11
+ - @pikku/core@0.12.2
12
+
13
+ ## 0.12.1
14
+
15
+ ### Patch Changes
16
+
17
+ - e04531f: Code quality improvements: resolve oxlint warnings and apply autofixes across the codebase (unused bindings, unnecessary constructors, prefer `const` over `let`, etc.). No behaviour changes.
18
+ - Updated dependencies [62a8725]
19
+ - Updated dependencies [a3bdb0d]
20
+ - Updated dependencies [e0349ff]
21
+ - Updated dependencies [62a8725]
22
+ - Updated dependencies [e04531f]
23
+ - Updated dependencies [62a8725]
24
+ - Updated dependencies [a83efb8]
25
+ - Updated dependencies [8eed717]
26
+ - Updated dependencies [62a8725]
27
+ - Updated dependencies [62a8725]
28
+ - Updated dependencies [62a8725]
29
+
30
+ - @pikku/core@0.12.1
31
+
3
32
  - Updated dependencies
4
33
 
5
34
  ## 0.11.0
@@ -1,5 +1,5 @@
1
1
  import type { AIThread, AIMessage, AgentRunRow, AgentRunService } from '@pikku/core/ai-agent';
2
- import { Kysely } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
4
  export declare class KyselyAgentRunService implements AgentRunService {
5
5
  private db;
@@ -1,11 +1,12 @@
1
1
  import type { AIStorageService, AIRunStateService, CreateRunInput } from '@pikku/core/services';
2
2
  import type { AIThread, AIMessage, AgentRunState } from '@pikku/core/ai-agent';
3
- import { Kysely } from 'kysely';
3
+ import type { Kysely } from 'kysely';
4
4
  import type { KyselyPikkuDB } from './kysely-tables.js';
5
5
  export declare class KyselyAIStorageService implements AIStorageService, AIRunStateService {
6
6
  private db;
7
7
  private initialized;
8
8
  constructor(db: Kysely<KyselyPikkuDB>);
9
+ private createIndexSafe;
9
10
  init(): Promise<void>;
10
11
  createThread(resourceId: string, options?: {
11
12
  threadId?: string;
@@ -6,6 +6,17 @@ export class KyselyAIStorageService {
6
6
  constructor(db) {
7
7
  this.db = db;
8
8
  }
9
+ async createIndexSafe(builder) {
10
+ try {
11
+ await builder.execute();
12
+ }
13
+ catch (e) {
14
+ // Ignore "index already exists" errors (MySQL doesn't support IF NOT EXISTS for indexes)
15
+ if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061)
16
+ return;
17
+ throw e;
18
+ }
19
+ }
9
20
  async init() {
10
21
  if (this.initialized) {
11
22
  return;
@@ -13,68 +24,60 @@ export class KyselyAIStorageService {
13
24
  await this.db.schema
14
25
  .createTable('ai_threads')
15
26
  .ifNotExists()
16
- .addColumn('id', 'text', (col) => col.primaryKey())
17
- .addColumn('resource_id', 'text', (col) => col.notNull())
27
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
28
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
18
29
  .addColumn('title', 'text')
19
30
  .addColumn('metadata', 'text')
20
31
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
21
32
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
22
33
  .execute();
23
- await this.db.schema
34
+ await this.createIndexSafe(this.db.schema
24
35
  .createIndex('idx_ai_threads_resource')
25
- .ifNotExists()
26
36
  .on('ai_threads')
27
- .column('resource_id')
28
- .execute();
37
+ .column('resource_id'));
29
38
  await this.db.schema
30
39
  .createTable('ai_message')
31
40
  .ifNotExists()
32
- .addColumn('id', 'text', (col) => col.primaryKey())
33
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
34
- .addColumn('role', 'text', (col) => col.notNull())
41
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
42
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
43
+ .addColumn('role', 'varchar(50)', (col) => col.notNull())
35
44
  .addColumn('content', 'text')
36
45
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
37
46
  .execute();
38
- await this.db.schema
47
+ await this.createIndexSafe(this.db.schema
39
48
  .createIndex('idx_ai_message_thread')
40
- .ifNotExists()
41
49
  .on('ai_message')
42
- .columns(['thread_id', 'created_at'])
43
- .execute();
50
+ .columns(['thread_id', 'created_at']));
44
51
  await this.db.schema
45
52
  .createTable('ai_tool_call')
46
53
  .ifNotExists()
47
- .addColumn('id', 'text', (col) => col.primaryKey())
48
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
49
- .addColumn('message_id', 'text', (col) => col.notNull().references('ai_message.id').onDelete('cascade'))
50
- .addColumn('run_id', 'text')
51
- .addColumn('tool_name', 'text', (col) => col.notNull())
52
- .addColumn('args', 'text', (col) => col.notNull().defaultTo('{}'))
54
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
55
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
56
+ .addColumn('message_id', 'varchar(36)', (col) => col.notNull().references('ai_message.id').onDelete('cascade'))
57
+ .addColumn('run_id', 'varchar(36)')
58
+ .addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
59
+ .addColumn('args', 'text', (col) => col.notNull())
53
60
  .addColumn('result', 'text')
54
- .addColumn('approval_status', 'text')
55
- .addColumn('approval_type', 'text')
56
- .addColumn('agent_run_id', 'text')
57
- .addColumn('display_tool_name', 'text')
61
+ .addColumn('approval_status', 'varchar(50)')
62
+ .addColumn('approval_type', 'varchar(50)')
63
+ .addColumn('agent_run_id', 'varchar(36)')
64
+ .addColumn('display_tool_name', 'varchar(255)')
58
65
  .addColumn('display_args', 'text')
59
66
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
60
67
  .execute();
61
- await this.db.schema
68
+ await this.createIndexSafe(this.db.schema
62
69
  .createIndex('idx_ai_tool_call_thread')
63
- .ifNotExists()
64
70
  .on('ai_tool_call')
65
- .column('thread_id')
66
- .execute();
67
- await this.db.schema
71
+ .column('thread_id'));
72
+ await this.createIndexSafe(this.db.schema
68
73
  .createIndex('idx_ai_tool_call_message')
69
- .ifNotExists()
70
74
  .on('ai_tool_call')
71
- .column('message_id')
72
- .execute();
75
+ .column('message_id'));
73
76
  await this.db.schema
74
77
  .createTable('ai_working_memory')
75
78
  .ifNotExists()
76
- .addColumn('id', 'text', (col) => col.notNull())
77
- .addColumn('scope', 'text', (col) => col.notNull())
79
+ .addColumn('id', 'varchar(255)', (col) => col.notNull())
80
+ .addColumn('scope', 'varchar(50)', (col) => col.notNull())
78
81
  .addColumn('data', 'text', (col) => col.notNull())
79
82
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
80
83
  .addPrimaryKeyConstraint('ai_working_memory_pk', ['id', 'scope'])
@@ -82,25 +85,23 @@ export class KyselyAIStorageService {
82
85
  await this.db.schema
83
86
  .createTable('ai_run')
84
87
  .ifNotExists()
85
- .addColumn('run_id', 'text', (col) => col.primaryKey())
86
- .addColumn('agent_name', 'text', (col) => col.notNull())
87
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
88
- .addColumn('resource_id', 'text', (col) => col.notNull())
89
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('running'))
88
+ .addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
89
+ .addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
90
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
91
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
92
+ .addColumn('status', 'varchar(50)', (col) => col.notNull().defaultTo('running'))
90
93
  .addColumn('suspend_reason', 'text')
91
94
  .addColumn('missing_rpcs', 'text')
92
95
  .addColumn('usage_input_tokens', 'integer', (col) => col.notNull().defaultTo(0))
93
96
  .addColumn('usage_output_tokens', 'integer', (col) => col.notNull().defaultTo(0))
94
- .addColumn('usage_model', 'text', (col) => col.notNull().defaultTo(''))
97
+ .addColumn('usage_model', 'varchar(255)', (col) => col.notNull().defaultTo(''))
95
98
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
96
99
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
97
100
  .execute();
98
- await this.db.schema
101
+ await this.createIndexSafe(this.db.schema
99
102
  .createIndex('idx_ai_run_thread')
100
- .ifNotExists()
101
103
  .on('ai_run')
102
- .columns(['thread_id', 'created_at'])
103
- .execute();
104
+ .columns(['thread_id', 'created_at']));
104
105
  this.initialized = true;
105
106
  }
106
107
  async createThread(resourceId, options) {
@@ -192,16 +193,17 @@ export class KyselyAIStorageService {
192
193
  msgQuery = msgQuery.where('created_at', '<', cursorRow.created_at);
193
194
  }
194
195
  }
196
+ let msgResult;
195
197
  if (options?.cursor || options?.lastN) {
196
198
  const innerResult = await msgQuery
197
199
  .orderBy('created_at', 'desc')
198
200
  .limit(options?.lastN ?? 50)
199
201
  .execute();
200
202
  innerResult.reverse();
201
- var msgResult = innerResult;
203
+ msgResult = innerResult;
202
204
  }
203
205
  else {
204
- var msgResult = await msgQuery.orderBy('created_at', 'asc').execute();
206
+ msgResult = await msgQuery.orderBy('created_at', 'asc').execute();
205
207
  }
206
208
  const tcResult = await this.db
207
209
  .selectFrom('ai_tool_call')
@@ -218,10 +220,26 @@ export class KyselyAIStorageService {
218
220
  }
219
221
  const messages = [];
220
222
  for (const row of msgResult) {
223
+ const rawContent = row.content;
224
+ let parsedContent = rawContent ?? undefined;
225
+ if (rawContent) {
226
+ try {
227
+ const parsed = JSON.parse(rawContent);
228
+ if (Array.isArray(parsed)) {
229
+ parsedContent = parsed;
230
+ }
231
+ else if (typeof parsed === 'string') {
232
+ parsedContent = parsed;
233
+ }
234
+ }
235
+ catch {
236
+ // Not JSON, use raw string
237
+ }
238
+ }
221
239
  const msg = {
222
240
  id: row.id,
223
241
  role: row.role,
224
- content: row.content ?? undefined,
242
+ content: parsedContent,
225
243
  createdAt: new Date(row.created_at),
226
244
  };
227
245
  const tcs = tcByMessage.get(msg.id);
@@ -263,7 +281,7 @@ export class KyselyAIStorageService {
263
281
  id: msg.id,
264
282
  thread_id: threadId,
265
283
  role: msg.role,
266
- content: msg.content ?? null,
284
+ content: msg.content != null ? JSON.stringify(msg.content) : null,
267
285
  created_at: msg.createdAt ?? new Date(),
268
286
  })))
269
287
  .execute();
@@ -1,6 +1,7 @@
1
- import { CoreUserSession } from '@pikku/core';
2
- import { Channel, ChannelStore } from '@pikku/core/channel';
3
- import { Kysely } from 'kysely';
1
+ import type { CoreUserSession } from '@pikku/core';
2
+ import type { Channel } from '@pikku/core/channel';
3
+ import { ChannelStore } from '@pikku/core/channel';
4
+ import type { Kysely } from 'kysely';
4
5
  import type { KyselyPikkuDB } from './kysely-tables.js';
5
6
  export declare class KyselyChannelStore extends ChannelStore {
6
7
  private db;
@@ -1,5 +1,5 @@
1
1
  import type { DeploymentService, DeploymentServiceConfig, DeploymentConfig, DeploymentInfo } from '@pikku/core/services';
2
- import { Kysely } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
4
  export declare class KyselyDeploymentService implements DeploymentService {
5
5
  private db;
@@ -1,5 +1,5 @@
1
1
  import { EventHubStore } from '@pikku/core/channel';
2
- import { Kysely } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
4
  export declare class KyselyEventHubStore extends EventHubStore {
5
5
  private db;
@@ -1,5 +1,5 @@
1
1
  import type { WorkflowRun, StepState, WorkflowRunService } from '@pikku/core/workflow';
2
- import { Kysely } from 'kysely';
2
+ import type { Kysely } from 'kysely';
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
4
  export declare class KyselyWorkflowRunService implements WorkflowRunService {
5
5
  private db;
@@ -1,6 +1,6 @@
1
1
  import type { SerializedError } from '@pikku/core';
2
2
  import { PikkuWorkflowService, type WorkflowRun, type WorkflowRunWire, type StepState, type WorkflowStatus } from '@pikku/core/workflow';
3
- import { Kysely } from 'kysely';
3
+ import type { Kysely } from 'kysely';
4
4
  import type { KyselyPikkuDB } from './kysely-tables.js';
5
5
  export declare class KyselyWorkflowService extends PikkuWorkflowService {
6
6
  private db;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -19,7 +19,7 @@
19
19
  "release": "npm run build && npm test"
20
20
  },
21
21
  "peerDependencies": {
22
- "@pikku/core": "^0.12.0"
22
+ "@pikku/core": "^0.12.2"
23
23
  },
24
24
  "dependencies": {
25
25
  "kysely": "^0.28.11",
@@ -30,7 +30,7 @@
30
30
  "@types/better-sqlite3": "^7.6.13",
31
31
  "better-sqlite3": "^12.6.2",
32
32
  "kysely": "^0.28.11",
33
- "kysely-codegen": "^0.19.0",
33
+ "kysely-codegen": "^0.20.0",
34
34
  "kysely-plugin-serialize": "^0.8.2",
35
35
  "kysely-postgres-js": "^3.0.0",
36
36
  "postgres": "^3.4.8",
@@ -4,7 +4,7 @@ import type {
4
4
  AgentRunRow,
5
5
  AgentRunService,
6
6
  } from '@pikku/core/ai-agent'
7
- import { Kysely } from 'kysely'
7
+ import type { Kysely } from 'kysely'
8
8
  import type { KyselyPikkuDB } from './kysely-tables.js'
9
9
  import { parseJson } from './kysely-json.js'
10
10
 
@@ -4,7 +4,8 @@ import type {
4
4
  CreateRunInput,
5
5
  } from '@pikku/core/services'
6
6
  import type { AIThread, AIMessage, AgentRunState } from '@pikku/core/ai-agent'
7
- import { Kysely, sql } from 'kysely'
7
+ import type { Kysely } from 'kysely'
8
+ import { sql } from 'kysely'
8
9
  import type { KyselyPikkuDB } from './kysely-tables.js'
9
10
  import { parseJson } from './kysely-json.js'
10
11
 
@@ -15,6 +16,18 @@ export class KyselyAIStorageService
15
16
 
16
17
  constructor(private db: Kysely<KyselyPikkuDB>) {}
17
18
 
19
+ private async createIndexSafe(builder: {
20
+ execute(): Promise<void>
21
+ }): Promise<void> {
22
+ try {
23
+ await builder.execute()
24
+ } catch (e: any) {
25
+ // Ignore "index already exists" errors (MySQL doesn't support IF NOT EXISTS for indexes)
26
+ if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061) return
27
+ throw e
28
+ }
29
+ }
30
+
18
31
  public async init(): Promise<void> {
19
32
  if (this.initialized) {
20
33
  return
@@ -23,8 +36,8 @@ export class KyselyAIStorageService
23
36
  await this.db.schema
24
37
  .createTable('ai_threads')
25
38
  .ifNotExists()
26
- .addColumn('id', 'text', (col) => col.primaryKey())
27
- .addColumn('resource_id', 'text', (col) => col.notNull())
39
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
40
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
28
41
  .addColumn('title', 'text')
29
42
  .addColumn('metadata', 'text')
30
43
  .addColumn('created_at', 'timestamp', (col) =>
@@ -35,77 +48,77 @@ export class KyselyAIStorageService
35
48
  )
36
49
  .execute()
37
50
 
38
- await this.db.schema
39
- .createIndex('idx_ai_threads_resource')
40
- .ifNotExists()
41
- .on('ai_threads')
42
- .column('resource_id')
43
- .execute()
51
+ await this.createIndexSafe(
52
+ this.db.schema
53
+ .createIndex('idx_ai_threads_resource')
54
+ .on('ai_threads')
55
+ .column('resource_id')
56
+ )
44
57
 
45
58
  await this.db.schema
46
59
  .createTable('ai_message')
47
60
  .ifNotExists()
48
- .addColumn('id', 'text', (col) => col.primaryKey())
49
- .addColumn('thread_id', 'text', (col) =>
61
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
62
+ .addColumn('thread_id', 'varchar(36)', (col) =>
50
63
  col.notNull().references('ai_threads.id').onDelete('cascade')
51
64
  )
52
- .addColumn('role', 'text', (col) => col.notNull())
65
+ .addColumn('role', 'varchar(50)', (col) => col.notNull())
53
66
  .addColumn('content', 'text')
54
67
  .addColumn('created_at', 'timestamp', (col) =>
55
68
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
56
69
  )
57
70
  .execute()
58
71
 
59
- await this.db.schema
60
- .createIndex('idx_ai_message_thread')
61
- .ifNotExists()
62
- .on('ai_message')
63
- .columns(['thread_id', 'created_at'])
64
- .execute()
72
+ await this.createIndexSafe(
73
+ this.db.schema
74
+ .createIndex('idx_ai_message_thread')
75
+ .on('ai_message')
76
+ .columns(['thread_id', 'created_at'])
77
+ )
65
78
 
66
79
  await this.db.schema
67
80
  .createTable('ai_tool_call')
68
81
  .ifNotExists()
69
- .addColumn('id', 'text', (col) => col.primaryKey())
70
- .addColumn('thread_id', 'text', (col) =>
82
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
83
+ .addColumn('thread_id', 'varchar(36)', (col) =>
71
84
  col.notNull().references('ai_threads.id').onDelete('cascade')
72
85
  )
73
- .addColumn('message_id', 'text', (col) =>
86
+ .addColumn('message_id', 'varchar(36)', (col) =>
74
87
  col.notNull().references('ai_message.id').onDelete('cascade')
75
88
  )
76
- .addColumn('run_id', 'text')
77
- .addColumn('tool_name', 'text', (col) => col.notNull())
78
- .addColumn('args', 'text', (col) => col.notNull().defaultTo('{}'))
89
+ .addColumn('run_id', 'varchar(36)')
90
+ .addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
91
+ .addColumn('args', 'text', (col) => col.notNull())
79
92
  .addColumn('result', 'text')
80
- .addColumn('approval_status', 'text')
81
- .addColumn('approval_type', 'text')
82
- .addColumn('agent_run_id', 'text')
83
- .addColumn('display_tool_name', 'text')
93
+ .addColumn('approval_status', 'varchar(50)')
94
+ .addColumn('approval_type', 'varchar(50)')
95
+ .addColumn('agent_run_id', 'varchar(36)')
96
+ .addColumn('display_tool_name', 'varchar(255)')
84
97
  .addColumn('display_args', 'text')
85
98
  .addColumn('created_at', 'timestamp', (col) =>
86
99
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
87
100
  )
88
101
  .execute()
89
102
 
90
- await this.db.schema
91
- .createIndex('idx_ai_tool_call_thread')
92
- .ifNotExists()
93
- .on('ai_tool_call')
94
- .column('thread_id')
95
- .execute()
103
+ await this.createIndexSafe(
104
+ this.db.schema
105
+ .createIndex('idx_ai_tool_call_thread')
106
+ .on('ai_tool_call')
107
+ .column('thread_id')
108
+ )
96
109
 
97
- await this.db.schema
98
- .createIndex('idx_ai_tool_call_message')
99
- .ifNotExists()
100
- .on('ai_tool_call')
101
- .column('message_id')
102
- .execute()
110
+ await this.createIndexSafe(
111
+ this.db.schema
112
+ .createIndex('idx_ai_tool_call_message')
113
+ .on('ai_tool_call')
114
+ .column('message_id')
115
+ )
103
116
 
104
117
  await this.db.schema
105
118
  .createTable('ai_working_memory')
106
119
  .ifNotExists()
107
- .addColumn('id', 'text', (col) => col.notNull())
108
- .addColumn('scope', 'text', (col) => col.notNull())
120
+ .addColumn('id', 'varchar(255)', (col) => col.notNull())
121
+ .addColumn('scope', 'varchar(50)', (col) => col.notNull())
109
122
  .addColumn('data', 'text', (col) => col.notNull())
110
123
  .addColumn('updated_at', 'timestamp', (col) =>
111
124
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
@@ -116,13 +129,15 @@ export class KyselyAIStorageService
116
129
  await this.db.schema
117
130
  .createTable('ai_run')
118
131
  .ifNotExists()
119
- .addColumn('run_id', 'text', (col) => col.primaryKey())
120
- .addColumn('agent_name', 'text', (col) => col.notNull())
121
- .addColumn('thread_id', 'text', (col) =>
132
+ .addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
133
+ .addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
134
+ .addColumn('thread_id', 'varchar(36)', (col) =>
122
135
  col.notNull().references('ai_threads.id').onDelete('cascade')
123
136
  )
124
- .addColumn('resource_id', 'text', (col) => col.notNull())
125
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('running'))
137
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
138
+ .addColumn('status', 'varchar(50)', (col) =>
139
+ col.notNull().defaultTo('running')
140
+ )
126
141
  .addColumn('suspend_reason', 'text')
127
142
  .addColumn('missing_rpcs', 'text')
128
143
  .addColumn('usage_input_tokens', 'integer', (col) =>
@@ -131,7 +146,9 @@ export class KyselyAIStorageService
131
146
  .addColumn('usage_output_tokens', 'integer', (col) =>
132
147
  col.notNull().defaultTo(0)
133
148
  )
134
- .addColumn('usage_model', 'text', (col) => col.notNull().defaultTo(''))
149
+ .addColumn('usage_model', 'varchar(255)', (col) =>
150
+ col.notNull().defaultTo('')
151
+ )
135
152
  .addColumn('created_at', 'timestamp', (col) =>
136
153
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
137
154
  )
@@ -140,12 +157,12 @@ export class KyselyAIStorageService
140
157
  )
141
158
  .execute()
142
159
 
143
- await this.db.schema
144
- .createIndex('idx_ai_run_thread')
145
- .ifNotExists()
146
- .on('ai_run')
147
- .columns(['thread_id', 'created_at'])
148
- .execute()
160
+ await this.createIndexSafe(
161
+ this.db.schema
162
+ .createIndex('idx_ai_run_thread')
163
+ .on('ai_run')
164
+ .columns(['thread_id', 'created_at'])
165
+ )
149
166
 
150
167
  this.initialized = true
151
168
  }
@@ -261,15 +278,16 @@ export class KyselyAIStorageService
261
278
  }
262
279
  }
263
280
 
281
+ let msgResult
264
282
  if (options?.cursor || options?.lastN) {
265
283
  const innerResult = await msgQuery
266
284
  .orderBy('created_at', 'desc')
267
285
  .limit(options?.lastN ?? 50)
268
286
  .execute()
269
287
  innerResult.reverse()
270
- var msgResult = innerResult
288
+ msgResult = innerResult
271
289
  } else {
272
- var msgResult = await msgQuery.orderBy('created_at', 'asc').execute()
290
+ msgResult = await msgQuery.orderBy('created_at', 'asc').execute()
273
291
  }
274
292
 
275
293
  const tcResult = await this.db
@@ -288,10 +306,24 @@ export class KyselyAIStorageService
288
306
 
289
307
  const messages: AIMessage[] = []
290
308
  for (const row of msgResult) {
309
+ const rawContent = row.content as string | undefined
310
+ let parsedContent: AIMessage['content'] = rawContent ?? undefined
311
+ if (rawContent) {
312
+ try {
313
+ const parsed = JSON.parse(rawContent)
314
+ if (Array.isArray(parsed)) {
315
+ parsedContent = parsed
316
+ } else if (typeof parsed === 'string') {
317
+ parsedContent = parsed
318
+ }
319
+ } catch {
320
+ // Not JSON, use raw string
321
+ }
322
+ }
291
323
  const msg: AIMessage = {
292
324
  id: row.id,
293
325
  role: row.role as AIMessage['role'],
294
- content: row.content ?? undefined,
326
+ content: parsedContent,
295
327
  createdAt: new Date(row.created_at),
296
328
  }
297
329
 
@@ -340,7 +372,7 @@ export class KyselyAIStorageService
340
372
  id: msg.id,
341
373
  thread_id: threadId,
342
374
  role: msg.role,
343
- content: msg.content ?? null,
375
+ content: msg.content != null ? JSON.stringify(msg.content) : null,
344
376
  created_at: msg.createdAt ?? new Date(),
345
377
  }))
346
378
  )
@@ -1,6 +1,8 @@
1
- import { CoreUserSession } from '@pikku/core'
2
- import { Channel, ChannelStore } from '@pikku/core/channel'
3
- import { Kysely, sql } from 'kysely'
1
+ import type { CoreUserSession } from '@pikku/core'
2
+ import type { Channel } from '@pikku/core/channel'
3
+ import { ChannelStore } from '@pikku/core/channel'
4
+ import type { Kysely } from 'kysely'
5
+ import { sql } from 'kysely'
4
6
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
7
  import { parseJson } from './kysely-json.js'
6
8
 
@@ -5,7 +5,8 @@ import type {
5
5
  DeploymentInfo,
6
6
  } from '@pikku/core/services'
7
7
  import { getAllFunctionNames } from '@pikku/core/function'
8
- import { Kysely, sql } from 'kysely'
8
+ import type { Kysely } from 'kysely'
9
+ import { sql } from 'kysely'
9
10
  import type { KyselyPikkuDB } from './kysely-tables.js'
10
11
 
11
12
  export class KyselyDeploymentService implements DeploymentService {
@@ -1,5 +1,5 @@
1
1
  import { EventHubStore } from '@pikku/core/channel'
2
- import { Kysely } from 'kysely'
2
+ import type { Kysely } from 'kysely'
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js'
4
4
 
5
5
  export class KyselyEventHubStore extends EventHubStore {
@@ -4,7 +4,7 @@ import type {
4
4
  WorkflowStatus,
5
5
  WorkflowRunService,
6
6
  } from '@pikku/core/workflow'
7
- import { Kysely } from 'kysely'
7
+ import type { Kysely } from 'kysely'
8
8
  import type { KyselyPikkuDB } from './kysely-tables.js'
9
9
  import { parseJson } from './kysely-json.js'
10
10
 
@@ -6,7 +6,8 @@ import {
6
6
  type StepState,
7
7
  type WorkflowStatus,
8
8
  } from '@pikku/core/workflow'
9
- import { Kysely, sql } from 'kysely'
9
+ import type { Kysely } from 'kysely'
10
+ import { sql } from 'kysely'
10
11
  import type { KyselyPikkuDB } from './kysely-tables.js'
11
12
  import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
12
13
  import { parseJson } from './kysely-json.js'