@clinebot/core 0.0.14 → 0.0.15
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/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +100 -96
- package/dist/session/default-session-manager.d.ts +2 -1
- package/dist/session/file-session-service.d.ts +5 -0
- package/dist/session/session-host.d.ts +2 -1
- package/dist/storage/file-team-store.d.ts +27 -0
- package/dist/storage/team-store.d.ts +13 -0
- package/package.json +4 -4
- package/src/account/cline-account-service.ts +7 -0
- package/src/account/types.ts +6 -0
- package/src/index.node.ts +38 -0
- package/src/runtime/runtime-builder.team-persistence.test.ts +4 -5
- package/src/runtime/runtime-builder.ts +2 -3
- package/src/session/default-session-manager.ts +5 -1
- package/src/session/file-session-service.ts +280 -0
- package/src/session/session-host.test.ts +29 -0
- package/src/session/session-host.ts +17 -3
- package/src/session/session-team-coordination.ts +6 -5
- package/src/storage/file-team-store.ts +257 -0
- package/src/storage/team-store.ts +35 -0
|
@@ -6,11 +6,12 @@ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
|
|
|
6
6
|
import { type ToolExecutors } from "../tools";
|
|
7
7
|
import type { CoreSessionEvent } from "../types/events";
|
|
8
8
|
import type { SessionRecord } from "../types/sessions";
|
|
9
|
+
import type { FileSessionService } from "./file-session-service";
|
|
9
10
|
import type { RpcCoreSessionService } from "./rpc-session-service";
|
|
10
11
|
import { RuntimeOAuthTokenManager } from "./runtime-oauth-token-manager";
|
|
11
12
|
import type { SendSessionInput, SessionAccumulatedUsage, SessionManager, StartSessionInput, StartSessionResult } from "./session-manager";
|
|
12
13
|
import type { CoreSessionService } from "./session-service";
|
|
13
|
-
type SessionBackend = CoreSessionService | RpcCoreSessionService;
|
|
14
|
+
type SessionBackend = CoreSessionService | RpcCoreSessionService | FileSessionService;
|
|
14
15
|
export interface DefaultSessionManagerOptions {
|
|
15
16
|
distinctId: string;
|
|
16
17
|
sessionService: SessionBackend;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { AgentConfig, ToolApprovalRequest, ToolApprovalResult } from "@clinebot/agents";
|
|
2
2
|
import type { ITelemetryService } from "@clinebot/shared";
|
|
3
3
|
import type { ToolExecutors } from "../tools";
|
|
4
|
+
import { FileSessionService } from "./file-session-service";
|
|
4
5
|
import { RpcCoreSessionService } from "./rpc-session-service";
|
|
5
6
|
import type { SessionManager } from "./session-manager";
|
|
6
7
|
import { CoreSessionService } from "./session-service";
|
|
7
|
-
export type SessionBackend = RpcCoreSessionService | CoreSessionService;
|
|
8
|
+
export type SessionBackend = RpcCoreSessionService | CoreSessionService | FileSessionService;
|
|
8
9
|
export interface CreateSessionHostOptions {
|
|
9
10
|
distinctId?: string;
|
|
10
11
|
sessionService?: SessionBackend;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TeamEvent, TeamRuntimeState, TeamTeammateSpec } from "@clinebot/agents";
|
|
2
|
+
import type { TeamStore } from "../types/storage";
|
|
3
|
+
export interface FileTeamStoreOptions {
|
|
4
|
+
teamDir?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TeamRuntimeLoadResult {
|
|
7
|
+
state?: TeamRuntimeState;
|
|
8
|
+
teammates: TeamTeammateSpec[];
|
|
9
|
+
interruptedRunIds: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare class FileTeamStore implements TeamStore {
|
|
12
|
+
private readonly teamDirPath;
|
|
13
|
+
constructor(options?: FileTeamStoreOptions);
|
|
14
|
+
init(): void;
|
|
15
|
+
listTeamNames(): string[];
|
|
16
|
+
readState(teamName: string): TeamRuntimeState | undefined;
|
|
17
|
+
readHistory(teamName: string, limit?: number): unknown[];
|
|
18
|
+
loadRuntime(teamName: string): TeamRuntimeLoadResult;
|
|
19
|
+
handleTeamEvent(teamName: string, event: TeamEvent): void;
|
|
20
|
+
persistRuntime(teamName: string, state: TeamRuntimeState, teammates: TeamTeammateSpec[]): void;
|
|
21
|
+
markInProgressRunsInterrupted(teamName: string, reason: string): string[];
|
|
22
|
+
private ensureTeamDir;
|
|
23
|
+
private ensureTeamSubdir;
|
|
24
|
+
private statePath;
|
|
25
|
+
private historyPath;
|
|
26
|
+
private readEnvelope;
|
|
27
|
+
}
|
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
export type { TeamStore } from "../types/storage";
|
|
2
|
+
export { FileTeamStore, type FileTeamStoreOptions, } from "./file-team-store";
|
|
2
3
|
export { SqliteTeamStore, type SqliteTeamStoreOptions, } from "./sqlite-team-store";
|
|
4
|
+
import { FileTeamStore } from "./file-team-store";
|
|
5
|
+
import { type SqliteTeamStoreOptions } from "./sqlite-team-store";
|
|
6
|
+
export declare function createLocalTeamStore(options?: SqliteTeamStoreOptions): {
|
|
7
|
+
init(): void;
|
|
8
|
+
listTeamNames(): string[];
|
|
9
|
+
readState(teamName: string): ReturnType<FileTeamStore["readState"]>;
|
|
10
|
+
readHistory(teamName: string, limit?: number): unknown[];
|
|
11
|
+
loadRuntime(teamName: string): ReturnType<FileTeamStore["loadRuntime"]>;
|
|
12
|
+
handleTeamEvent: FileTeamStore["handleTeamEvent"];
|
|
13
|
+
persistRuntime: FileTeamStore["persistRuntime"];
|
|
14
|
+
markInProgressRunsInterrupted: FileTeamStore["markInProgressRunsInterrupted"];
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"main": "./dist/index.node.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@clinebot/agents": "0.0.
|
|
7
|
-
"@clinebot/llms": "0.0.
|
|
8
|
-
"@clinebot/shared": "0.0.
|
|
6
|
+
"@clinebot/agents": "0.0.15",
|
|
7
|
+
"@clinebot/llms": "0.0.15",
|
|
8
|
+
"@clinebot/shared": "0.0.15",
|
|
9
9
|
"@opentelemetry/api": "^1.9.0",
|
|
10
10
|
"@opentelemetry/api-logs": "^0.56.0",
|
|
11
11
|
"@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ClineAccountPaymentTransaction,
|
|
7
7
|
ClineAccountUsageTransaction,
|
|
8
8
|
ClineAccountUser,
|
|
9
|
+
UserRemoteConfigResponse,
|
|
9
10
|
} from "./types";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -63,6 +64,12 @@ export class ClineAccountService {
|
|
|
63
64
|
return this.request<ClineAccountUser>("/api/v1/users/me");
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
public async fetchRemoteConfig(): Promise<UserRemoteConfigResponse> {
|
|
68
|
+
return this.request<UserRemoteConfigResponse>(
|
|
69
|
+
"/api/v1/users/me/remote-config",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
public async fetchBalance(userId?: string): Promise<ClineAccountBalance> {
|
|
67
74
|
const resolvedUserId = await this.resolveUserId(userId);
|
|
68
75
|
return this.request<ClineAccountBalance>(
|
package/src/account/types.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface ClineAccountUser {
|
|
|
16
16
|
organizations: ClineAccountOrganization[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface UserRemoteConfigResponse {
|
|
20
|
+
organizationId: string;
|
|
21
|
+
value: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export interface ClineAccountBalance {
|
|
20
26
|
balance: number;
|
|
21
27
|
userId: string;
|
package/src/index.node.ts
CHANGED
|
@@ -103,6 +103,22 @@ export {
|
|
|
103
103
|
export async function loadOpenTelemetryAdapter() {
|
|
104
104
|
return import("./telemetry/opentelemetry.js");
|
|
105
105
|
}
|
|
106
|
+
export {
|
|
107
|
+
type ClineAccountBalance,
|
|
108
|
+
type ClineAccountOperations,
|
|
109
|
+
type ClineAccountOrganization,
|
|
110
|
+
type ClineAccountOrganizationBalance,
|
|
111
|
+
type ClineAccountOrganizationUsageTransaction,
|
|
112
|
+
type ClineAccountPaymentTransaction,
|
|
113
|
+
ClineAccountService,
|
|
114
|
+
type ClineAccountServiceOptions,
|
|
115
|
+
type ClineAccountUsageTransaction,
|
|
116
|
+
type ClineAccountUser,
|
|
117
|
+
executeRpcClineAccountAction,
|
|
118
|
+
isRpcClineAccountActionRequest,
|
|
119
|
+
RpcClineAccountService,
|
|
120
|
+
type RpcProviderActionExecutor,
|
|
121
|
+
} from "./account";
|
|
106
122
|
export { startLocalOAuthServer } from "./auth/server";
|
|
107
123
|
export type {
|
|
108
124
|
OAuthCredentials,
|
|
@@ -127,6 +143,28 @@ export {
|
|
|
127
143
|
getFileIndex,
|
|
128
144
|
prewarmFileIndex,
|
|
129
145
|
} from "./input";
|
|
146
|
+
export {
|
|
147
|
+
hasMcpSettingsFile,
|
|
148
|
+
InMemoryMcpManager,
|
|
149
|
+
type LoadMcpSettingsOptions,
|
|
150
|
+
loadMcpSettingsFile,
|
|
151
|
+
type McpConnectionStatus,
|
|
152
|
+
type McpManager,
|
|
153
|
+
type McpManagerOptions,
|
|
154
|
+
type McpServerClient,
|
|
155
|
+
type McpServerClientFactory,
|
|
156
|
+
type McpServerRegistration,
|
|
157
|
+
type McpServerSnapshot,
|
|
158
|
+
type McpServerTransportConfig,
|
|
159
|
+
type McpSettingsFile,
|
|
160
|
+
type McpSseTransportConfig,
|
|
161
|
+
type McpStdioTransportConfig,
|
|
162
|
+
type McpStreamableHttpTransportConfig,
|
|
163
|
+
type RegisterMcpServersFromSettingsOptions,
|
|
164
|
+
registerMcpServersFromSettingsFile,
|
|
165
|
+
resolveDefaultMcpSettingsPath,
|
|
166
|
+
resolveMcpServerRegistrations,
|
|
167
|
+
} from "./mcp";
|
|
130
168
|
export {
|
|
131
169
|
addLocalProvider,
|
|
132
170
|
ensureCustomProvidersLoaded,
|
|
@@ -49,13 +49,12 @@ vi.mock("../default-tools", () => ({
|
|
|
49
49
|
},
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
-
let teamStoreInstance:
|
|
53
|
-
class
|
|
52
|
+
let teamStoreInstance: MockTeamStore | undefined;
|
|
53
|
+
class MockTeamStore {
|
|
54
54
|
constructor() {
|
|
55
55
|
teamStoreInstance = this;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
init = vi.fn();
|
|
59
58
|
loadRuntime = vi.fn(() => ({
|
|
60
59
|
state: {
|
|
61
60
|
teamId: "team_1",
|
|
@@ -82,8 +81,8 @@ class MockSqliteTeamStore {
|
|
|
82
81
|
persistRuntime = vi.fn();
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
vi.mock("../storage/
|
|
86
|
-
|
|
84
|
+
vi.mock("../storage/team-store", () => ({
|
|
85
|
+
createLocalTeamStore: () => new MockTeamStore(),
|
|
87
86
|
}));
|
|
88
87
|
|
|
89
88
|
describe("DefaultRuntimeBuilder team persistence boundary", () => {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type SkillConfig,
|
|
15
15
|
type UserInstructionConfigWatcher,
|
|
16
16
|
} from "../agents";
|
|
17
|
-
import {
|
|
17
|
+
import { createLocalTeamStore } from "../storage/team-store";
|
|
18
18
|
import {
|
|
19
19
|
createBuiltinTools,
|
|
20
20
|
DEFAULT_MODEL_TOOL_ROUTING_RULES,
|
|
@@ -418,9 +418,8 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
|
|
|
418
418
|
|
|
419
419
|
let teamRuntime: AgentTeamsRuntime | undefined;
|
|
420
420
|
const teamStore = normalized.enableAgentTeams
|
|
421
|
-
?
|
|
421
|
+
? createLocalTeamStore()
|
|
422
422
|
: undefined;
|
|
423
|
-
teamStore?.init();
|
|
424
423
|
const restoredTeam = teamStore?.loadRuntime(effectiveTeamName);
|
|
425
424
|
const restoredTeamState = restoredTeam?.state;
|
|
426
425
|
const restoredTeammateSpecs = restoredTeam?.teammates ?? [];
|
|
@@ -36,6 +36,7 @@ import { SessionSource, type SessionStatus } from "../types/common";
|
|
|
36
36
|
import type { CoreSessionConfig } from "../types/config";
|
|
37
37
|
import type { CoreSessionEvent } from "../types/events";
|
|
38
38
|
import type { SessionRecord } from "../types/sessions";
|
|
39
|
+
import type { FileSessionService } from "./file-session-service";
|
|
39
40
|
import type { RpcCoreSessionService } from "./rpc-session-service";
|
|
40
41
|
import {
|
|
41
42
|
OAuthReauthRequiredError,
|
|
@@ -92,7 +93,10 @@ import {
|
|
|
92
93
|
createInitialAccumulatedUsage,
|
|
93
94
|
} from "./utils/usage";
|
|
94
95
|
|
|
95
|
-
type SessionBackend =
|
|
96
|
+
type SessionBackend =
|
|
97
|
+
| CoreSessionService
|
|
98
|
+
| RpcCoreSessionService
|
|
99
|
+
| FileSessionService;
|
|
96
100
|
|
|
97
101
|
const MAX_SCAN_LIMIT = 5000;
|
|
98
102
|
const MAX_USER_FILE_BYTES = 20 * 1_000 * 1_024;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { resolveSessionDataDir } from "@clinebot/shared/storage";
|
|
10
|
+
import type { SessionRow } from "./session-service";
|
|
11
|
+
import type {
|
|
12
|
+
PersistedSessionUpdateInput,
|
|
13
|
+
SessionPersistenceAdapter,
|
|
14
|
+
} from "./unified-session-persistence-service";
|
|
15
|
+
import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
|
|
16
|
+
|
|
17
|
+
interface FileSessionIndex {
|
|
18
|
+
version: 1;
|
|
19
|
+
sessions: Record<string, SessionRow>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FileSpawnRequest {
|
|
23
|
+
id: number;
|
|
24
|
+
rootSessionId: string;
|
|
25
|
+
parentAgentId: string;
|
|
26
|
+
task?: string;
|
|
27
|
+
systemPrompt?: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
consumedAt?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FileSpawnQueue {
|
|
33
|
+
version: 1;
|
|
34
|
+
nextId: number;
|
|
35
|
+
requests: FileSpawnRequest[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nowIso(): string {
|
|
39
|
+
return new Date().toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function atomicWriteJson(path: string, value: unknown): void {
|
|
43
|
+
const tempPath = `${path}.tmp`;
|
|
44
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
45
|
+
renameSync(tempPath, path);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultSessionsDir(): string {
|
|
49
|
+
return resolveSessionDataDir();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class FileSessionPersistenceAdapter implements SessionPersistenceAdapter {
|
|
53
|
+
constructor(
|
|
54
|
+
private readonly sessionsDirPath: string = defaultSessionsDir(),
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
ensureSessionsDir(): string {
|
|
58
|
+
if (!existsSync(this.sessionsDirPath)) {
|
|
59
|
+
mkdirSync(this.sessionsDirPath, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
return this.sessionsDirPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private indexPath(): string {
|
|
65
|
+
return join(this.ensureSessionsDir(), "sessions.index.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private spawnQueuePath(): string {
|
|
69
|
+
return join(this.ensureSessionsDir(), "subagent-spawn-queue.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private readIndex(): FileSessionIndex {
|
|
73
|
+
const path = this.indexPath();
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
return { version: 1, sessions: {} };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as FileSessionIndex;
|
|
79
|
+
if (parsed?.version === 1 && parsed.sessions) {
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore invalid persistence and fall back to an empty index.
|
|
84
|
+
}
|
|
85
|
+
return { version: 1, sessions: {} };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private writeIndex(index: FileSessionIndex): void {
|
|
89
|
+
atomicWriteJson(this.indexPath(), index);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private readQueue(): FileSpawnQueue {
|
|
93
|
+
const path = this.spawnQueuePath();
|
|
94
|
+
if (!existsSync(path)) {
|
|
95
|
+
return { version: 1, nextId: 1, requests: [] };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as FileSpawnQueue;
|
|
99
|
+
if (
|
|
100
|
+
parsed?.version === 1 &&
|
|
101
|
+
typeof parsed.nextId === "number" &&
|
|
102
|
+
Array.isArray(parsed.requests)
|
|
103
|
+
) {
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore invalid persistence and fall back to an empty queue.
|
|
108
|
+
}
|
|
109
|
+
return { version: 1, nextId: 1, requests: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private writeQueue(queue: FileSpawnQueue): void {
|
|
113
|
+
atomicWriteJson(this.spawnQueuePath(), queue);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async upsertSession(row: SessionRow): Promise<void> {
|
|
117
|
+
const index = this.readIndex();
|
|
118
|
+
index.sessions[row.sessionId] = row;
|
|
119
|
+
this.writeIndex(index);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getSession(sessionId: string): Promise<SessionRow | undefined> {
|
|
123
|
+
return this.readIndex().sessions[sessionId];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listSessions(options: {
|
|
127
|
+
limit: number;
|
|
128
|
+
parentSessionId?: string;
|
|
129
|
+
status?: string;
|
|
130
|
+
}): Promise<SessionRow[]> {
|
|
131
|
+
return Object.values(this.readIndex().sessions)
|
|
132
|
+
.filter((row) =>
|
|
133
|
+
options.parentSessionId !== undefined
|
|
134
|
+
? row.parentSessionId === options.parentSessionId
|
|
135
|
+
: true,
|
|
136
|
+
)
|
|
137
|
+
.filter((row) =>
|
|
138
|
+
options.status !== undefined ? row.status === options.status : true,
|
|
139
|
+
)
|
|
140
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
141
|
+
.slice(0, options.limit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async updateSession(
|
|
145
|
+
input: PersistedSessionUpdateInput,
|
|
146
|
+
): Promise<{ updated: boolean; statusLock: number }> {
|
|
147
|
+
const index = this.readIndex();
|
|
148
|
+
const existing = index.sessions[input.sessionId];
|
|
149
|
+
if (!existing) {
|
|
150
|
+
return { updated: false, statusLock: 0 };
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
input.expectedStatusLock !== undefined &&
|
|
154
|
+
existing.statusLock !== input.expectedStatusLock
|
|
155
|
+
) {
|
|
156
|
+
return { updated: false, statusLock: existing.statusLock };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nextStatusLock =
|
|
160
|
+
input.expectedStatusLock !== undefined
|
|
161
|
+
? input.expectedStatusLock + 1
|
|
162
|
+
: existing.statusLock;
|
|
163
|
+
const next: SessionRow = {
|
|
164
|
+
...existing,
|
|
165
|
+
status: input.status ?? existing.status,
|
|
166
|
+
endedAt:
|
|
167
|
+
input.endedAt !== undefined
|
|
168
|
+
? input.endedAt
|
|
169
|
+
: (existing.endedAt ?? null),
|
|
170
|
+
exitCode:
|
|
171
|
+
input.exitCode !== undefined
|
|
172
|
+
? input.exitCode
|
|
173
|
+
: (existing.exitCode ?? null),
|
|
174
|
+
prompt:
|
|
175
|
+
input.prompt !== undefined ? input.prompt : (existing.prompt ?? null),
|
|
176
|
+
metadata:
|
|
177
|
+
input.metadata !== undefined
|
|
178
|
+
? (input.metadata ?? null)
|
|
179
|
+
: (existing.metadata ?? null),
|
|
180
|
+
parentSessionId:
|
|
181
|
+
input.parentSessionId !== undefined
|
|
182
|
+
? (input.parentSessionId ?? null)
|
|
183
|
+
: (existing.parentSessionId ?? null),
|
|
184
|
+
parentAgentId:
|
|
185
|
+
input.parentAgentId !== undefined
|
|
186
|
+
? (input.parentAgentId ?? null)
|
|
187
|
+
: (existing.parentAgentId ?? null),
|
|
188
|
+
agentId:
|
|
189
|
+
input.agentId !== undefined
|
|
190
|
+
? (input.agentId ?? null)
|
|
191
|
+
: (existing.agentId ?? null),
|
|
192
|
+
conversationId:
|
|
193
|
+
input.conversationId !== undefined
|
|
194
|
+
? (input.conversationId ?? null)
|
|
195
|
+
: (existing.conversationId ?? null),
|
|
196
|
+
statusLock: nextStatusLock,
|
|
197
|
+
isSubagent:
|
|
198
|
+
input.setRunning || input.parentSessionId !== undefined
|
|
199
|
+
? true
|
|
200
|
+
: existing.isSubagent,
|
|
201
|
+
updatedAt: nowIso(),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (input.setRunning) {
|
|
205
|
+
next.status = "running";
|
|
206
|
+
next.endedAt = null;
|
|
207
|
+
next.exitCode = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
index.sessions[input.sessionId] = next;
|
|
211
|
+
this.writeIndex(index);
|
|
212
|
+
return { updated: true, statusLock: next.statusLock };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteSession(sessionId: string, cascade: boolean): Promise<boolean> {
|
|
216
|
+
const index = this.readIndex();
|
|
217
|
+
const existing = index.sessions[sessionId];
|
|
218
|
+
if (!existing) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
delete index.sessions[sessionId];
|
|
222
|
+
if (cascade) {
|
|
223
|
+
for (const row of Object.values(index.sessions)) {
|
|
224
|
+
if (row.parentSessionId === sessionId) {
|
|
225
|
+
delete index.sessions[row.sessionId];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.writeIndex(index);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async enqueueSpawnRequest(input: {
|
|
234
|
+
rootSessionId: string;
|
|
235
|
+
parentAgentId: string;
|
|
236
|
+
task?: string;
|
|
237
|
+
systemPrompt?: string;
|
|
238
|
+
}): Promise<void> {
|
|
239
|
+
const queue = this.readQueue();
|
|
240
|
+
queue.requests.push({
|
|
241
|
+
id: queue.nextId,
|
|
242
|
+
rootSessionId: input.rootSessionId,
|
|
243
|
+
parentAgentId: input.parentAgentId,
|
|
244
|
+
task: input.task,
|
|
245
|
+
systemPrompt: input.systemPrompt,
|
|
246
|
+
createdAt: nowIso(),
|
|
247
|
+
});
|
|
248
|
+
queue.nextId += 1;
|
|
249
|
+
this.writeQueue(queue);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async claimSpawnRequest(
|
|
253
|
+
rootSessionId: string,
|
|
254
|
+
parentAgentId: string,
|
|
255
|
+
): Promise<string | undefined> {
|
|
256
|
+
const queue = this.readQueue();
|
|
257
|
+
const request = queue.requests.find(
|
|
258
|
+
(item) =>
|
|
259
|
+
item.rootSessionId === rootSessionId &&
|
|
260
|
+
item.parentAgentId === parentAgentId &&
|
|
261
|
+
!item.consumedAt,
|
|
262
|
+
);
|
|
263
|
+
if (!request) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
request.consumedAt = nowIso();
|
|
267
|
+
this.writeQueue(queue);
|
|
268
|
+
return request.task;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class FileSessionService extends UnifiedSessionPersistenceService {
|
|
273
|
+
constructor(sessionsDir?: string) {
|
|
274
|
+
super(new FileSessionPersistenceAdapter(sessionsDir));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override ensureSessionsDir(): string {
|
|
278
|
+
return super.ensureSessionsDir();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@clinebot/rpc", () => ({
|
|
4
|
+
getRpcServerDefaultAddress: () => "ws://127.0.0.1:0",
|
|
5
|
+
getRpcServerHealth: vi.fn(async () => undefined),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
describe("resolveSessionBackend", () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("falls back to file session storage when sqlite initialization fails", async () => {
|
|
15
|
+
vi.doMock("../storage/sqlite-session-store", () => ({
|
|
16
|
+
SqliteSessionStore: class {
|
|
17
|
+
init(): void {
|
|
18
|
+
throw new Error("sqlite unavailable");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const { resolveSessionBackend } = await import("./session-host");
|
|
24
|
+
const { FileSessionService } = await import("./file-session-service");
|
|
25
|
+
|
|
26
|
+
const backend = await resolveSessionBackend({ backendMode: "local" });
|
|
27
|
+
expect(backend).toBeInstanceOf(FileSessionService);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -13,6 +13,7 @@ import { nanoid } from "nanoid";
|
|
|
13
13
|
import { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
14
14
|
import type { ToolExecutors } from "../tools";
|
|
15
15
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
16
|
+
import { FileSessionService } from "./file-session-service";
|
|
16
17
|
import { RpcCoreSessionService } from "./rpc-session-service";
|
|
17
18
|
import { tryAcquireRpcSpawnLease } from "./rpc-spawn-lease";
|
|
18
19
|
import type { SessionManager } from "./session-manager";
|
|
@@ -21,7 +22,10 @@ import { CoreSessionService } from "./session-service";
|
|
|
21
22
|
const DEFAULT_RPC_ADDRESS =
|
|
22
23
|
process.env.CLINE_RPC_ADDRESS?.trim() || getRpcServerDefaultAddress();
|
|
23
24
|
|
|
24
|
-
export type SessionBackend =
|
|
25
|
+
export type SessionBackend =
|
|
26
|
+
| RpcCoreSessionService
|
|
27
|
+
| CoreSessionService
|
|
28
|
+
| FileSessionService;
|
|
25
29
|
|
|
26
30
|
let cachedBackend: SessionBackend | undefined;
|
|
27
31
|
let backendInitPromise: Promise<SessionBackend> | undefined;
|
|
@@ -111,8 +115,18 @@ async function tryConnectRpcBackend(
|
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
function createLocalBackend():
|
|
115
|
-
|
|
118
|
+
function createLocalBackend(): SessionBackend {
|
|
119
|
+
try {
|
|
120
|
+
const store = new SqliteSessionStore();
|
|
121
|
+
store.init();
|
|
122
|
+
return new CoreSessionService(store);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn(
|
|
125
|
+
"SQLite session persistence unavailable, falling back to file-based session storage.",
|
|
126
|
+
error,
|
|
127
|
+
);
|
|
128
|
+
return new FileSessionService();
|
|
129
|
+
}
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
function resolveHostDistinctId(explicitDistinctId: string | undefined): string {
|
|
@@ -163,11 +163,12 @@ export function shouldAutoContinueTeamRuns(
|
|
|
163
163
|
session: ActiveSession,
|
|
164
164
|
finishReason: AgentResult["finishReason"],
|
|
165
165
|
): boolean {
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
if (session.aborting) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const canAutoContinue =
|
|
170
|
+
finishReason === "completed" || finishReason === "max_iterations";
|
|
171
|
+
if (!canAutoContinue) {
|
|
171
172
|
return false;
|
|
172
173
|
}
|
|
173
174
|
return (
|