@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.
- package/README.md +1 -1
- package/dist/agents/plugin-loader.d.ts +1 -0
- package/dist/index.node.d.ts +4 -0
- package/dist/index.node.js +216 -195
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/session/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +6 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/tools/schemas.d.ts +1 -0
- package/package.json +7 -5
- package/src/agents/plugin-loader.test.ts +11 -11
- package/src/agents/plugin-loader.ts +5 -3
- package/src/auth/cline.ts +1 -29
- package/src/index.node.ts +10 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/index.ts +10 -0
- package/src/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +32 -32
- package/src/session/default-session-manager.ts +10 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-service.ts +127 -64
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +114 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/tools/definitions.test.ts +50 -0
- package/src/tools/definitions.ts +26 -0
- package/src/tools/schemas.ts +5 -6
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
118
|
+
sessionId,
|
|
119
119
|
source: input.source,
|
|
120
120
|
pid: input.pid,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
startedAt,
|
|
122
|
+
endedAt: null,
|
|
123
|
+
exitCode: null,
|
|
124
124
|
status: "running",
|
|
125
|
-
|
|
126
|
-
interactive: input.interactive
|
|
125
|
+
statusLock: 0,
|
|
126
|
+
interactive: input.interactive,
|
|
127
127
|
provider: input.provider,
|
|
128
128
|
model: input.model,
|
|
129
129
|
cwd: input.cwd,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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?.
|
|
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.
|
|
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.
|
|
193
|
-
row.
|
|
194
|
-
row.
|
|
195
|
-
row.
|
|
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):
|
|
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.
|
|
218
|
-
unlinkSync(row.
|
|
219
|
-
unlinkSync(row.
|
|
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
|
-
|
|
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?.
|
|
466
|
-
const raw = readFileSync(row.
|
|
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?.
|
|
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?.
|
|
495
|
-
const lines = readFileSync(row.
|
|
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<
|
|
1168
|
-
return this.invoke<
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
98
|
-
await this.client.upsertSession(
|
|
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<
|
|
23
|
+
async getSession(sessionId: string): Promise<SessionRow | undefined> {
|
|
102
24
|
const row = await this.client.getSession(sessionId);
|
|
103
|
-
return row
|
|
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<
|
|
32
|
+
}): Promise<SessionRow[]> {
|
|
111
33
|
const rows = await this.client.listSessions(options);
|
|
112
|
-
return rows
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
-
|
|
27
|
+
export interface SessionRow {
|
|
28
|
+
sessionId: string;
|
|
29
29
|
source: string;
|
|
30
30
|
pid: number;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
startedAt: string;
|
|
32
|
+
endedAt?: string | null;
|
|
33
|
+
exitCode?: number | null;
|
|
34
34
|
status: SessionStatus;
|
|
35
|
-
|
|
36
|
-
interactive:
|
|
35
|
+
statusLock: number;
|
|
36
|
+
interactive: boolean;
|
|
37
37
|
provider: string;
|
|
38
38
|
model: string;
|
|
39
39
|
cwd: string;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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.
|
|
407
|
+
row.sessionId,
|
|
340
408
|
row.source,
|
|
341
409
|
row.pid,
|
|
342
|
-
row.
|
|
343
|
-
row.
|
|
344
|
-
row.
|
|
410
|
+
row.startedAt,
|
|
411
|
+
row.endedAt ?? null,
|
|
412
|
+
row.exitCode ?? null,
|
|
345
413
|
row.status,
|
|
346
|
-
|
|
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.
|
|
352
|
-
row.
|
|
353
|
-
row.
|
|
354
|
-
row.
|
|
355
|
-
row.
|
|
356
|
-
row.
|
|
357
|
-
row.
|
|
358
|
-
row.
|
|
359
|
-
row.
|
|
360
|
-
row.
|
|
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.
|
|
363
|
-
row.
|
|
364
|
-
row.
|
|
365
|
-
row.
|
|
366
|
-
row.
|
|
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<
|
|
372
|
-
const row = this.store.queryOne<
|
|
373
|
-
`SELECT
|
|
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
|
|
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<
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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.
|
|
525
|
+
if (input.metadata !== undefined) {
|
|
463
526
|
fields.push("metadata_json = ?");
|
|
464
|
-
params.push(input.
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
48
|
+
sessionId,
|
|
49
49
|
status: "failed",
|
|
50
|
-
|
|
50
|
+
exitCode: 1,
|
|
51
51
|
});
|
|
52
|
-
expect(rows[0]?.
|
|
52
|
+
expect(rows[0]?.endedAt).toBeTruthy();
|
|
53
53
|
|
|
54
54
|
const manifest = JSON.parse(
|
|
55
55
|
readFileSync(artifacts.manifestPath, "utf8"),
|