@clinebot/core 0.0.12 → 0.0.14

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.
@@ -17,7 +17,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
17
  import type { SessionSource, SessionStatus } from "../types/common";
18
18
  import { DefaultSessionManager } from "./default-session-manager";
19
19
  import type { SessionManifest } from "./session-manifest";
20
- import type { RootSessionArtifacts, SessionRowShape } from "./session-service";
20
+ import type { RootSessionArtifacts, SessionRow } from "./session-service";
21
21
 
22
22
  function nowIso(): string {
23
23
  return new Date().toISOString();
@@ -47,7 +47,7 @@ function createResult(overrides: Partial<AgentResult> = {}): AgentResult {
47
47
  }
48
48
 
49
49
  class LocalFileSessionService {
50
- private readonly rows = new Map<string, SessionRowShape>();
50
+ private readonly rows = new Map<string, SessionRow>();
51
51
 
52
52
  constructor(private readonly sessionsDir: string) {}
53
53
 
@@ -115,33 +115,33 @@ class LocalFileSessionService {
115
115
  );
116
116
 
117
117
  this.rows.set(sessionId, {
118
- session_id: sessionId,
118
+ sessionId,
119
119
  source: input.source,
120
120
  pid: input.pid,
121
- started_at: startedAt,
122
- ended_at: null,
123
- exit_code: null,
121
+ startedAt,
122
+ endedAt: null,
123
+ exitCode: null,
124
124
  status: "running",
125
- status_lock: 0,
126
- interactive: input.interactive ? 1 : 0,
125
+ statusLock: 0,
126
+ interactive: input.interactive,
127
127
  provider: input.provider,
128
128
  model: input.model,
129
129
  cwd: input.cwd,
130
- workspace_root: input.workspaceRoot,
131
- team_name: input.teamName ?? null,
132
- enable_tools: input.enableTools ? 1 : 0,
133
- enable_spawn: input.enableSpawn ? 1 : 0,
134
- enable_teams: input.enableTeams ? 1 : 0,
135
- parent_session_id: null,
136
- parent_agent_id: null,
137
- agent_id: null,
138
- conversation_id: null,
139
- is_subagent: 0,
130
+ workspaceRoot: input.workspaceRoot,
131
+ teamName: input.teamName ?? null,
132
+ enableTools: input.enableTools,
133
+ enableSpawn: input.enableSpawn,
134
+ enableTeams: input.enableTeams,
135
+ parentSessionId: null,
136
+ parentAgentId: null,
137
+ agentId: null,
138
+ conversationId: null,
139
+ isSubagent: false,
140
140
  prompt: prompt ?? null,
141
- transcript_path: transcriptPath,
142
- hook_path: hookPath,
143
- messages_path: messagesPath,
144
- updated_at: startedAt,
141
+ transcriptPath,
142
+ hookPath,
143
+ messagesPath,
144
+ updatedAt: startedAt,
145
145
  });
146
146
 
147
147
  return {
@@ -159,7 +159,7 @@ class LocalFileSessionService {
159
159
  systemPrompt?: string,
160
160
  ): void {
161
161
  const row = this.rows.get(sessionId);
162
- if (!row?.messages_path) {
162
+ if (!row?.messagesPath) {
163
163
  throw new Error(`session not found: ${sessionId}`);
164
164
  }
165
165
  const payload: {
@@ -172,7 +172,7 @@ class LocalFileSessionService {
172
172
  payload.systemPrompt = systemPrompt;
173
173
  }
174
174
  writeFileSync(
175
- row.messages_path,
175
+ row.messagesPath,
176
176
  `${JSON.stringify(payload, null, 2)}\n`,
177
177
  "utf8",
178
178
  );
@@ -189,10 +189,10 @@ class LocalFileSessionService {
189
189
  }
190
190
  const endedAt = nowIso();
191
191
  row.status = status;
192
- row.ended_at = endedAt;
193
- row.exit_code = typeof exitCode === "number" ? exitCode : null;
194
- row.updated_at = endedAt;
195
- row.status_lock = (row.status_lock ?? 0) + 1;
192
+ row.endedAt = endedAt;
193
+ row.exitCode = typeof exitCode === "number" ? exitCode : null;
194
+ row.updatedAt = endedAt;
195
+ row.statusLock = row.statusLock + 1;
196
196
  return { updated: true, endedAt };
197
197
  }
198
198
 
@@ -204,7 +204,7 @@ class LocalFileSessionService {
204
204
  );
205
205
  }
206
206
 
207
- listSessions(limit = 200): SessionRowShape[] {
207
+ listSessions(limit = 200): SessionRow[] {
208
208
  return Array.from(this.rows.values()).slice(0, limit);
209
209
  }
210
210
 
@@ -214,9 +214,9 @@ class LocalFileSessionService {
214
214
  return { deleted: false };
215
215
  }
216
216
  this.rows.delete(sessionId);
217
- unlinkSync(row.transcript_path);
218
- unlinkSync(row.hook_path);
219
- unlinkSync(row.messages_path ?? "");
217
+ unlinkSync(row.transcriptPath);
218
+ unlinkSync(row.hookPath);
219
+ unlinkSync(row.messagesPath ?? "");
220
220
  unlinkSync(join(this.sessionsDir, sessionId, `${sessionId}.json`));
221
221
  return { deleted: true };
222
222
  }
@@ -64,7 +64,7 @@ import { SessionManifestSchema } from "./session-manifest";
64
64
  import type {
65
65
  CoreSessionService,
66
66
  RootSessionArtifacts,
67
- SessionRowShape,
67
+ SessionRow,
68
68
  } from "./session-service";
69
69
  import {
70
70
  buildTeamRunContinuationPrompt,
@@ -462,8 +462,8 @@ export class DefaultSessionManager implements SessionManager {
462
462
 
463
463
  async readTranscript(sessionId: string, maxChars?: number): Promise<string> {
464
464
  const row = await this.getRow(sessionId);
465
- if (!row?.transcript_path || !existsSync(row.transcript_path)) return "";
466
- const raw = readFileSync(row.transcript_path, "utf8");
465
+ if (!row?.transcriptPath || !existsSync(row.transcriptPath)) return "";
466
+ const raw = readFileSync(row.transcriptPath, "utf8");
467
467
  if (typeof maxChars === "number" && Number.isFinite(maxChars)) {
468
468
  return raw.slice(-Math.max(0, Math.floor(maxChars)));
469
469
  }
@@ -472,7 +472,7 @@ export class DefaultSessionManager implements SessionManager {
472
472
 
473
473
  async readMessages(sessionId: string): Promise<LlmsProviders.Message[]> {
474
474
  const row = await this.getRow(sessionId);
475
- const messagesPath = row?.messages_path?.trim();
475
+ const messagesPath = row?.messagesPath?.trim();
476
476
  if (!messagesPath || !existsSync(messagesPath)) return [];
477
477
  try {
478
478
  const raw = readFileSync(messagesPath, "utf8").trim();
@@ -491,8 +491,8 @@ export class DefaultSessionManager implements SessionManager {
491
491
 
492
492
  async readHooks(sessionId: string, limit = 200): Promise<unknown[]> {
493
493
  const row = await this.getRow(sessionId);
494
- if (!row?.hook_path || !existsSync(row.hook_path)) return [];
495
- const lines = readFileSync(row.hook_path, "utf8")
494
+ if (!row?.hookPath || !existsSync(row.hookPath)) return [];
495
+ const lines = readFileSync(row.hookPath, "utf8")
496
496
  .split("\n")
497
497
  .filter((line) => line.trim().length > 0);
498
498
  return lines.slice(-Math.max(1, Math.floor(limit))).map((line) => {
@@ -1164,20 +1164,18 @@ export class DefaultSessionManager implements SessionManager {
1164
1164
  for (const listener of this.listeners) listener(event);
1165
1165
  }
1166
1166
 
1167
- private async listRows(limit: number): Promise<SessionRowShape[]> {
1168
- return this.invoke<SessionRowShape[]>(
1167
+ private async listRows(limit: number): Promise<SessionRow[]> {
1168
+ return this.invoke<SessionRow[]>(
1169
1169
  "listSessions",
1170
1170
  Math.min(Math.max(1, Math.floor(limit)), MAX_SCAN_LIMIT),
1171
1171
  );
1172
1172
  }
1173
1173
 
1174
- private async getRow(
1175
- sessionId: string,
1176
- ): Promise<SessionRowShape | undefined> {
1174
+ private async getRow(sessionId: string): Promise<SessionRow | undefined> {
1177
1175
  const target = sessionId.trim();
1178
1176
  if (!target) return undefined;
1179
1177
  const rows = await this.listRows(MAX_SCAN_LIMIT);
1180
- return rows.find((row) => row.session_id === target);
1178
+ return rows.find((row) => row.sessionId === target);
1181
1179
  }
1182
1180
 
1183
1181
  // ── Session service invocation ──────────────────────────────────────
@@ -1,91 +1,13 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import { RpcSessionClient, type RpcSessionRow } from "@clinebot/rpc";
3
- import { nowIso } from "./session-artifacts";
4
- import type { SessionRowShape } from "./session-service";
3
+ import type { SessionRow } from "./session-service";
5
4
  import type {
6
5
  PersistedSessionUpdateInput,
7
6
  SessionPersistenceAdapter,
8
7
  } from "./unified-session-persistence-service";
9
8
  import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
10
9
 
11
- function toShape(row: RpcSessionRow): SessionRowShape {
12
- return {
13
- session_id: row.sessionId,
14
- source: row.source,
15
- pid: row.pid,
16
- started_at: row.startedAt,
17
- ended_at: row.endedAt ?? null,
18
- exit_code: row.exitCode ?? null,
19
- status: row.status,
20
- status_lock: row.statusLock,
21
- interactive: row.interactive ? 1 : 0,
22
- provider: row.provider,
23
- model: row.model,
24
- cwd: row.cwd,
25
- workspace_root: row.workspaceRoot,
26
- team_name: row.teamName ?? null,
27
- enable_tools: row.enableTools ? 1 : 0,
28
- enable_spawn: row.enableSpawn ? 1 : 0,
29
- enable_teams: row.enableTeams ? 1 : 0,
30
- parent_session_id: row.parentSessionId ?? null,
31
- parent_agent_id: row.parentAgentId ?? null,
32
- agent_id: row.agentId ?? null,
33
- conversation_id: row.conversationId ?? null,
34
- is_subagent: row.isSubagent ? 1 : 0,
35
- prompt: row.prompt ?? null,
36
- metadata_json: row.metadata ? JSON.stringify(row.metadata) : null,
37
- transcript_path: row.transcriptPath,
38
- hook_path: row.hookPath,
39
- messages_path: row.messagesPath ?? null,
40
- updated_at: row.updatedAt,
41
- };
42
- }
43
-
44
- function fromShape(row: SessionRowShape): RpcSessionRow {
45
- return {
46
- sessionId: row.session_id,
47
- source: row.source,
48
- pid: row.pid,
49
- startedAt: row.started_at,
50
- endedAt: row.ended_at ?? null,
51
- exitCode: row.exit_code ?? null,
52
- status: row.status,
53
- statusLock: row.status_lock ?? 0,
54
- interactive: row.interactive === 1,
55
- provider: row.provider,
56
- model: row.model,
57
- cwd: row.cwd,
58
- workspaceRoot: row.workspace_root,
59
- teamName: row.team_name ?? undefined,
60
- enableTools: row.enable_tools === 1,
61
- enableSpawn: row.enable_spawn === 1,
62
- enableTeams: row.enable_teams === 1,
63
- parentSessionId: row.parent_session_id ?? undefined,
64
- parentAgentId: row.parent_agent_id ?? undefined,
65
- agentId: row.agent_id ?? undefined,
66
- conversationId: row.conversation_id ?? undefined,
67
- isSubagent: row.is_subagent === 1,
68
- prompt: row.prompt ?? undefined,
69
- metadata: (() => {
70
- if (!row.metadata_json) {
71
- return undefined;
72
- }
73
- try {
74
- const parsed = JSON.parse(row.metadata_json) as unknown;
75
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
76
- return parsed as Record<string, unknown>;
77
- }
78
- } catch {
79
- // Ignore malformed metadata payloads.
80
- }
81
- return undefined;
82
- })(),
83
- transcriptPath: row.transcript_path,
84
- hookPath: row.hook_path,
85
- messagesPath: row.messages_path ?? undefined,
86
- updatedAt: row.updated_at ?? nowIso(),
87
- };
88
- }
10
+ // ── Adapter ──────────────────────────────────────────────────────────
89
11
 
90
12
  class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
91
13
  constructor(private readonly client: RpcSessionClient) {}
@@ -94,39 +16,34 @@ class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
94
16
  return "";
95
17
  }
96
18
 
97
- async upsertSession(row: SessionRowShape): Promise<void> {
98
- await this.client.upsertSession(fromShape(row));
19
+ async upsertSession(row: SessionRow): Promise<void> {
20
+ await this.client.upsertSession(row as RpcSessionRow);
99
21
  }
100
22
 
101
- async getSession(sessionId: string): Promise<SessionRowShape | undefined> {
23
+ async getSession(sessionId: string): Promise<SessionRow | undefined> {
102
24
  const row = await this.client.getSession(sessionId);
103
- return row ? toShape(row) : undefined;
25
+ return (row as SessionRow | undefined) ?? undefined;
104
26
  }
105
27
 
106
28
  async listSessions(options: {
107
29
  limit: number;
108
30
  parentSessionId?: string;
109
31
  status?: string;
110
- }): Promise<SessionRowShape[]> {
32
+ }): Promise<SessionRow[]> {
111
33
  const rows = await this.client.listSessions(options);
112
- return rows.map((row) => toShape(row));
34
+ return rows as SessionRow[];
113
35
  }
114
36
 
115
37
  async updateSession(
116
38
  input: PersistedSessionUpdateInput,
117
39
  ): Promise<{ updated: boolean; statusLock: number }> {
118
- const changed = await this.client.updateSession({
40
+ return this.client.updateSession({
119
41
  sessionId: input.sessionId,
120
42
  status: input.status,
121
43
  endedAt: input.endedAt,
122
44
  exitCode: input.exitCode,
123
45
  prompt: input.prompt,
124
- metadata:
125
- input.metadataJson === undefined
126
- ? undefined
127
- : input.metadataJson
128
- ? (JSON.parse(input.metadataJson) as Record<string, unknown>)
129
- : null,
46
+ metadata: input.metadata,
130
47
  parentSessionId: input.parentSessionId,
131
48
  parentAgentId: input.parentAgentId,
132
49
  agentId: input.agentId,
@@ -134,11 +51,10 @@ class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
134
51
  expectedStatusLock: input.expectedStatusLock,
135
52
  setRunning: input.setRunning,
136
53
  });
137
- return changed;
138
54
  }
139
55
 
140
56
  async deleteSession(sessionId: string, cascade: boolean): Promise<boolean> {
141
- return await this.client.deleteSession(sessionId, cascade);
57
+ return this.client.deleteSession(sessionId, cascade);
142
58
  }
143
59
 
144
60
  async enqueueSpawnRequest(input: {
@@ -154,10 +70,12 @@ class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
154
70
  rootSessionId: string,
155
71
  parentAgentId: string,
156
72
  ): Promise<string | undefined> {
157
- return await this.client.claimSpawnRequest(rootSessionId, parentAgentId);
73
+ return this.client.claimSpawnRequest(rootSessionId, parentAgentId);
158
74
  }
159
75
  }
160
76
 
77
+ // ── Service ──────────────────────────────────────────────────────────
78
+
161
79
  export interface RpcCoreSessionServiceOptions {
162
80
  address?: string;
163
81
  sessionsDir: string;
@@ -24,35 +24,35 @@ import type {
24
24
  } from "./unified-session-persistence-service";
25
25
  import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
26
26
 
27
- export interface SessionRowShape {
28
- session_id: string;
27
+ export interface SessionRow {
28
+ sessionId: string;
29
29
  source: string;
30
30
  pid: number;
31
- started_at: string;
32
- ended_at?: string | null;
33
- exit_code?: number | null;
31
+ startedAt: string;
32
+ endedAt?: string | null;
33
+ exitCode?: number | null;
34
34
  status: SessionStatus;
35
- status_lock?: number;
36
- interactive: number;
35
+ statusLock: number;
36
+ interactive: boolean;
37
37
  provider: string;
38
38
  model: string;
39
39
  cwd: string;
40
- workspace_root: string;
41
- team_name?: string | null;
42
- enable_tools: number;
43
- enable_spawn: number;
44
- enable_teams: number;
45
- parent_session_id?: string | null;
46
- parent_agent_id?: string | null;
47
- agent_id?: string | null;
48
- conversation_id?: string | null;
49
- is_subagent: number;
40
+ workspaceRoot: string;
41
+ teamName?: string | null;
42
+ enableTools: boolean;
43
+ enableSpawn: boolean;
44
+ enableTeams: boolean;
45
+ parentSessionId?: string | null;
46
+ parentAgentId?: string | null;
47
+ agentId?: string | null;
48
+ conversationId?: string | null;
49
+ isSubagent: boolean;
50
50
  prompt?: string | null;
51
- metadata_json?: string | null;
52
- transcript_path: string;
53
- hook_path: string;
54
- messages_path?: string | null;
55
- updated_at?: string;
51
+ metadata?: Record<string, unknown> | null;
52
+ transcriptPath: string;
53
+ hookPath: string;
54
+ messagesPath?: string | null;
55
+ updatedAt: string;
56
56
  }
57
57
 
58
58
  export interface CreateRootSessionInput {
@@ -110,6 +110,74 @@ export interface UpsertSubagentInput {
110
110
  rootSessionId?: string;
111
111
  }
112
112
 
113
+ // ── SQLite helpers ───────────────────────────────────────────────────
114
+
115
+ /** SELECT clause that aliases snake_case columns to camelCase SessionRow keys. */
116
+ const SESSION_SELECT_COLUMNS = `
117
+ session_id AS sessionId,
118
+ source,
119
+ pid,
120
+ started_at AS startedAt,
121
+ ended_at AS endedAt,
122
+ exit_code AS exitCode,
123
+ status,
124
+ status_lock AS statusLock,
125
+ interactive,
126
+ provider,
127
+ model,
128
+ cwd,
129
+ workspace_root AS workspaceRoot,
130
+ team_name AS teamName,
131
+ enable_tools AS enableTools,
132
+ enable_spawn AS enableSpawn,
133
+ enable_teams AS enableTeams,
134
+ parent_session_id AS parentSessionId,
135
+ parent_agent_id AS parentAgentId,
136
+ agent_id AS agentId,
137
+ conversation_id AS conversationId,
138
+ is_subagent AS isSubagent,
139
+ prompt,
140
+ metadata_json AS metadata,
141
+ transcript_path AS transcriptPath,
142
+ hook_path AS hookPath,
143
+ messages_path AS messagesPath,
144
+ updated_at AS updatedAt`;
145
+
146
+ /**
147
+ * Patch a raw SQLite result into a proper SessionRow.
148
+ * SQLite returns 0/1 for booleans and a JSON string for metadata —
149
+ * this converts them in-place to avoid allocating a second object.
150
+ */
151
+ function patchSqliteRow(raw: Record<string, unknown>): SessionRow {
152
+ raw.interactive = raw.interactive === 1;
153
+ raw.enableTools = raw.enableTools === 1;
154
+ raw.enableSpawn = raw.enableSpawn === 1;
155
+ raw.enableTeams = raw.enableTeams === 1;
156
+ raw.isSubagent = raw.isSubagent === 1;
157
+ const meta = raw.metadata;
158
+ if (typeof meta === "string" && meta.trim()) {
159
+ try {
160
+ const parsed = JSON.parse(meta) as unknown;
161
+ raw.metadata =
162
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
163
+ ? parsed
164
+ : null;
165
+ } catch {
166
+ raw.metadata = null;
167
+ }
168
+ } else {
169
+ raw.metadata = null;
170
+ }
171
+ return raw as unknown as SessionRow;
172
+ }
173
+
174
+ function stringifyMetadata(
175
+ metadata: Record<string, unknown> | null | undefined,
176
+ ): string | null {
177
+ if (!metadata || Object.keys(metadata).length === 0) return null;
178
+ return JSON.stringify(metadata);
179
+ }
180
+
113
181
  function reviveTeamStateDates(state: TeamRuntimeState): TeamRuntimeState {
114
182
  return {
115
183
  ...state,
@@ -327,7 +395,7 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
327
395
  return this.store.ensureSessionsDir();
328
396
  }
329
397
 
330
- async upsertSession(row: SessionRowShape): Promise<void> {
398
+ async upsertSession(row: SessionRow): Promise<void> {
331
399
  this.store.run(
332
400
  `INSERT OR REPLACE INTO sessions (
333
401
  session_id, source, pid, started_at, ended_at, exit_code, status, status_lock, interactive,
@@ -336,55 +404,51 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
336
404
  metadata_json, transcript_path, hook_path, messages_path, updated_at
337
405
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
338
406
  [
339
- row.session_id,
407
+ row.sessionId,
340
408
  row.source,
341
409
  row.pid,
342
- row.started_at,
343
- row.ended_at ?? null,
344
- row.exit_code ?? null,
410
+ row.startedAt,
411
+ row.endedAt ?? null,
412
+ row.exitCode ?? null,
345
413
  row.status,
346
- typeof row.status_lock === "number" ? row.status_lock : 0,
347
- row.interactive,
414
+ row.statusLock,
415
+ row.interactive ? 1 : 0,
348
416
  row.provider,
349
417
  row.model,
350
418
  row.cwd,
351
- row.workspace_root,
352
- row.team_name ?? null,
353
- row.enable_tools,
354
- row.enable_spawn,
355
- row.enable_teams,
356
- row.parent_session_id ?? null,
357
- row.parent_agent_id ?? null,
358
- row.agent_id ?? null,
359
- row.conversation_id ?? null,
360
- row.is_subagent,
419
+ row.workspaceRoot,
420
+ row.teamName ?? null,
421
+ row.enableTools ? 1 : 0,
422
+ row.enableSpawn ? 1 : 0,
423
+ row.enableTeams ? 1 : 0,
424
+ row.parentSessionId ?? null,
425
+ row.parentAgentId ?? null,
426
+ row.agentId ?? null,
427
+ row.conversationId ?? null,
428
+ row.isSubagent ? 1 : 0,
361
429
  row.prompt ?? null,
362
- row.metadata_json ?? null,
363
- row.transcript_path,
364
- row.hook_path,
365
- row.messages_path ?? null,
366
- row.updated_at ?? nowIso(),
430
+ stringifyMetadata(row.metadata),
431
+ row.transcriptPath,
432
+ row.hookPath,
433
+ row.messagesPath ?? null,
434
+ row.updatedAt,
367
435
  ],
368
436
  );
369
437
  }
370
438
 
371
- async getSession(sessionId: string): Promise<SessionRowShape | undefined> {
372
- const row = this.store.queryOne<SessionRowShape>(
373
- `SELECT session_id, source, pid, started_at, ended_at, exit_code, status, status_lock, interactive,
374
- provider, model, cwd, workspace_root, team_name, enable_tools, enable_spawn, enable_teams,
375
- parent_session_id, parent_agent_id, agent_id, conversation_id, is_subagent, prompt,
376
- metadata_json, transcript_path, hook_path, messages_path, updated_at
377
- FROM sessions WHERE session_id = ?`,
439
+ async getSession(sessionId: string): Promise<SessionRow | undefined> {
440
+ const row = this.store.queryOne<Record<string, unknown>>(
441
+ `SELECT ${SESSION_SELECT_COLUMNS} FROM sessions WHERE session_id = ?`,
378
442
  [sessionId],
379
443
  );
380
- return row ?? undefined;
444
+ return row ? patchSqliteRow(row) : undefined;
381
445
  }
382
446
 
383
447
  async listSessions(options: {
384
448
  limit: number;
385
449
  parentSessionId?: string;
386
450
  status?: string;
387
- }): Promise<SessionRowShape[]> {
451
+ }): Promise<SessionRow[]> {
388
452
  const whereClauses: string[] = [];
389
453
  const params: unknown[] = [];
390
454
  if (options.parentSessionId) {
@@ -397,17 +461,16 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
397
461
  }
398
462
  const where =
399
463
  whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
400
- return this.store.queryAll<SessionRowShape>(
401
- `SELECT session_id, source, pid, started_at, ended_at, exit_code, status, status_lock, interactive,
402
- provider, model, cwd, workspace_root, team_name, enable_tools, enable_spawn, enable_teams,
403
- parent_session_id, parent_agent_id, agent_id, conversation_id, is_subagent, prompt,
404
- metadata_json, transcript_path, hook_path, messages_path, updated_at
464
+ return this.store
465
+ .queryAll<Record<string, unknown>>(
466
+ `SELECT ${SESSION_SELECT_COLUMNS}
405
467
  FROM sessions
406
468
  ${where}
407
469
  ORDER BY started_at DESC
408
470
  LIMIT ?`,
409
- [...params, options.limit],
410
- );
471
+ [...params, options.limit],
472
+ )
473
+ .map(patchSqliteRow);
411
474
  }
412
475
 
413
476
  async updateSession(
@@ -459,9 +522,9 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
459
522
  fields.push("prompt = ?");
460
523
  params.push(input.prompt ?? null);
461
524
  }
462
- if (input.metadataJson !== undefined) {
525
+ if (input.metadata !== undefined) {
463
526
  fields.push("metadata_json = ?");
464
- params.push(input.metadataJson ?? null);
527
+ params.push(stringifyMetadata(input.metadata));
465
528
  }
466
529
  if (input.parentSessionId !== undefined) {
467
530
  fields.push("parent_session_id = ?");
@@ -481,7 +544,7 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
481
544
  }
482
545
  if (fields.length === 0) {
483
546
  const row = await this.getSession(input.sessionId);
484
- return { updated: !!row, statusLock: row?.status_lock ?? 0 };
547
+ return { updated: !!row, statusLock: row?.statusLock ?? 0 };
485
548
  }
486
549
 
487
550
  let statusLock = 0;
@@ -505,7 +568,7 @@ class LocalSessionPersistenceAdapter implements SessionPersistenceAdapter {
505
568
  }
506
569
  if (input.expectedStatusLock === undefined) {
507
570
  const row = await this.getSession(input.sessionId);
508
- statusLock = row?.status_lock ?? 0;
571
+ statusLock = row?.statusLock ?? 0;
509
572
  }
510
573
  return { updated: true, statusLock };
511
574
  }
@@ -45,11 +45,11 @@ describe("UnifiedSessionPersistenceService", () => {
45
45
  const rows = await service.listSessions(10);
46
46
  expect(rows).toHaveLength(1);
47
47
  expect(rows[0]).toMatchObject({
48
- session_id: sessionId,
48
+ sessionId,
49
49
  status: "failed",
50
- exit_code: 1,
50
+ exitCode: 1,
51
51
  });
52
- expect(rows[0]?.ended_at).toBeTruthy();
52
+ expect(rows[0]?.endedAt).toBeTruthy();
53
53
 
54
54
  const manifest = JSON.parse(
55
55
  readFileSync(artifacts.manifestPath, "utf8"),