@clinebot/core 0.0.5 → 0.0.7
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/agents/hooks-config-loader.d.ts +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +134 -107
- package/dist/runtime/session-runtime.d.ts +3 -1
- package/dist/session/default-session-manager.d.ts +4 -0
- package/dist/session/rpc-spawn-lease.d.ts +7 -0
- package/dist/session/session-host.d.ts +2 -0
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
- package/dist/telemetry/ITelemetryAdapter.d.ts +54 -0
- package/dist/telemetry/LoggerTelemetryAdapter.d.ts +21 -0
- package/dist/telemetry/OpenTelemetryAdapter.d.ts +43 -0
- package/dist/telemetry/OpenTelemetryProvider.d.ts +41 -0
- package/dist/telemetry/TelemetryService.d.ts +34 -0
- package/dist/telemetry/opentelemetry.d.ts +3 -0
- package/dist/telemetry/opentelemetry.js +27 -0
- package/dist/tools/schemas.d.ts +8 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/events.d.ts +1 -1
- package/package.json +16 -3
- package/src/agents/hooks-config-loader.ts +21 -1
- package/src/index.node.ts +7 -0
- package/src/index.ts +16 -0
- package/src/input/file-indexer.test.ts +40 -0
- package/src/input/file-indexer.ts +21 -0
- package/src/runtime/hook-file-hooks.test.ts +98 -1
- package/src/runtime/hook-file-hooks.ts +93 -11
- package/src/runtime/runtime-builder.test.ts +20 -0
- package/src/runtime/runtime-builder.ts +1 -0
- package/src/runtime/session-runtime.ts +3 -1
- package/src/session/default-session-manager.test.ts +72 -0
- package/src/session/default-session-manager.ts +59 -1
- package/src/session/rpc-spawn-lease.test.ts +49 -0
- package/src/session/rpc-spawn-lease.ts +122 -0
- package/src/session/session-graph.ts +2 -0
- package/src/session/session-host.ts +14 -1
- package/src/session/session-manager.ts +1 -0
- package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
- package/src/storage/provider-settings-legacy-migration.ts +60 -8
- package/src/telemetry/ITelemetryAdapter.ts +94 -0
- package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
- package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
- package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
- package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
- package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
- package/src/telemetry/OpenTelemetryProvider.ts +322 -0
- package/src/telemetry/TelemetryService.test.ts +134 -0
- package/src/telemetry/TelemetryService.ts +141 -0
- package/src/telemetry/opentelemetry.ts +20 -0
- package/src/tools/definitions.test.ts +82 -29
- package/src/tools/definitions.ts +41 -32
- package/src/tools/executors/editor.test.ts +35 -0
- package/src/tools/executors/editor.ts +33 -46
- package/src/tools/schemas.ts +34 -35
- package/src/types/config.ts +2 -0
- package/src/types/events.ts +6 -1
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { AgentResult } from "@clinebot/agents";
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { TelemetryService } from "../telemetry/TelemetryService";
|
|
6
7
|
import { SessionSource } from "../types/common";
|
|
7
8
|
import type { CoreSessionConfig } from "../types/config";
|
|
8
9
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
@@ -70,6 +71,77 @@ function createConfig(
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
describe("DefaultSessionManager", () => {
|
|
74
|
+
it("emits session lifecycle telemetry when configured", async () => {
|
|
75
|
+
const sessionId = "sess-telemetry";
|
|
76
|
+
const manifest = createManifest(sessionId);
|
|
77
|
+
const adapter = {
|
|
78
|
+
name: "test",
|
|
79
|
+
emit: vi.fn(),
|
|
80
|
+
emitRequired: vi.fn(),
|
|
81
|
+
recordCounter: vi.fn(),
|
|
82
|
+
recordHistogram: vi.fn(),
|
|
83
|
+
recordGauge: vi.fn(),
|
|
84
|
+
isEnabled: vi.fn(() => true),
|
|
85
|
+
flush: vi.fn().mockResolvedValue(undefined),
|
|
86
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
87
|
+
};
|
|
88
|
+
const telemetry = new TelemetryService({
|
|
89
|
+
adapters: [adapter],
|
|
90
|
+
distinctId: distinctId,
|
|
91
|
+
});
|
|
92
|
+
const sessionService = {
|
|
93
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
94
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
95
|
+
manifestPath: "/tmp/manifest.json",
|
|
96
|
+
transcriptPath: "/tmp/transcript.log",
|
|
97
|
+
hookPath: "/tmp/hook.log",
|
|
98
|
+
messagesPath: "/tmp/messages.json",
|
|
99
|
+
manifest,
|
|
100
|
+
}),
|
|
101
|
+
persistSessionMessages: vi.fn(),
|
|
102
|
+
updateSessionStatus: vi.fn().mockResolvedValue({
|
|
103
|
+
updated: true,
|
|
104
|
+
endedAt: "2026-01-01T00:00:05.000Z",
|
|
105
|
+
}),
|
|
106
|
+
writeSessionManifest: vi.fn(),
|
|
107
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
108
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
109
|
+
};
|
|
110
|
+
const runtimeBuilder = {
|
|
111
|
+
build: vi.fn().mockReturnValue({
|
|
112
|
+
tools: [],
|
|
113
|
+
shutdown: vi.fn(),
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
const agent = {
|
|
117
|
+
run: vi.fn().mockResolvedValue(createResult()),
|
|
118
|
+
continue: vi.fn().mockResolvedValue(createResult()),
|
|
119
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
120
|
+
abort: vi.fn(),
|
|
121
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
122
|
+
};
|
|
123
|
+
const manager = new DefaultSessionManager({
|
|
124
|
+
distinctId,
|
|
125
|
+
sessionService: sessionService as never,
|
|
126
|
+
runtimeBuilder: runtimeBuilder as never,
|
|
127
|
+
createAgent: () => agent as never,
|
|
128
|
+
telemetry,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await manager.start({
|
|
132
|
+
config: createConfig({ telemetry, sessionId }),
|
|
133
|
+
prompt: "hello",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(adapter.emit).toHaveBeenCalledWith(
|
|
137
|
+
"session.started",
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
sessionId,
|
|
140
|
+
distinct_id: distinctId,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
73
145
|
it("runs a non-interactive prompt and persists messages/status", async () => {
|
|
74
146
|
const sessionId = "sess-1";
|
|
75
147
|
const manifest = createManifest(sessionId);
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
type ToolApprovalResult,
|
|
15
15
|
} from "@clinebot/agents";
|
|
16
16
|
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
formatUserInputBlock,
|
|
19
|
+
type ITelemetryService,
|
|
20
|
+
normalizeUserInput,
|
|
21
|
+
} from "@clinebot/shared";
|
|
18
22
|
import { setHomeDirIfUnset } from "@clinebot/shared/storage";
|
|
19
23
|
import { nanoid } from "nanoid";
|
|
20
24
|
import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
|
|
@@ -89,6 +93,7 @@ export interface DefaultSessionManagerOptions {
|
|
|
89
93
|
toolPolicies?: AgentConfig["toolPolicies"];
|
|
90
94
|
providerSettingsManager?: ProviderSettingsManager;
|
|
91
95
|
oauthTokenManager?: RuntimeOAuthTokenManager;
|
|
96
|
+
telemetry?: ITelemetryService;
|
|
92
97
|
requestToolApproval?: (
|
|
93
98
|
request: ToolApprovalRequest,
|
|
94
99
|
) => Promise<ToolApprovalResult>;
|
|
@@ -120,6 +125,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
120
125
|
private readonly defaultToolPolicies?: AgentConfig["toolPolicies"];
|
|
121
126
|
private readonly providerSettingsManager: ProviderSettingsManager;
|
|
122
127
|
private readonly oauthTokenManager: RuntimeOAuthTokenManager;
|
|
128
|
+
private readonly defaultTelemetry?: ITelemetryService;
|
|
123
129
|
private readonly defaultRequestToolApproval?: (
|
|
124
130
|
request: ToolApprovalRequest,
|
|
125
131
|
) => Promise<ToolApprovalResult>;
|
|
@@ -143,6 +149,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
143
149
|
new RuntimeOAuthTokenManager({
|
|
144
150
|
providerSettingsManager: this.providerSettingsManager,
|
|
145
151
|
});
|
|
152
|
+
this.defaultTelemetry = options.telemetry;
|
|
146
153
|
this.defaultRequestToolApproval = options.requestToolApproval;
|
|
147
154
|
}
|
|
148
155
|
|
|
@@ -263,6 +270,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
263
270
|
...input.config,
|
|
264
271
|
hooks: effectiveHooks,
|
|
265
272
|
extensions: effectiveExtensions,
|
|
273
|
+
telemetry: input.config.telemetry ?? this.defaultTelemetry,
|
|
266
274
|
};
|
|
267
275
|
const providerConfig =
|
|
268
276
|
this.buildResolvedProviderConfig(effectiveConfigBase);
|
|
@@ -276,6 +284,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
276
284
|
hooks: effectiveHooks,
|
|
277
285
|
extensions: effectiveExtensions,
|
|
278
286
|
logger: effectiveConfig.logger,
|
|
287
|
+
telemetry: effectiveConfig.telemetry,
|
|
279
288
|
onTeamEvent: (event: TeamEvent) => {
|
|
280
289
|
void this.handleTeamEvent(sessionId, event);
|
|
281
290
|
effectiveConfig.onTeamEvent?.(event);
|
|
@@ -287,6 +296,18 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
287
296
|
input.defaultToolExecutors ?? this.defaultToolExecutors,
|
|
288
297
|
});
|
|
289
298
|
const tools = [...runtime.tools, ...(effectiveConfig.extraTools ?? [])];
|
|
299
|
+
effectiveConfig.telemetry?.capture({
|
|
300
|
+
event: "session.started",
|
|
301
|
+
properties: {
|
|
302
|
+
sessionId,
|
|
303
|
+
source,
|
|
304
|
+
providerId: effectiveConfig.providerId,
|
|
305
|
+
modelId: effectiveConfig.modelId,
|
|
306
|
+
enableTools: effectiveConfig.enableTools,
|
|
307
|
+
enableSpawnAgent: effectiveConfig.enableSpawnAgent,
|
|
308
|
+
enableAgentTeams: effectiveConfig.enableAgentTeams,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
290
311
|
const agent = this.createAgentInstance({
|
|
291
312
|
providerId: providerConfig.providerId,
|
|
292
313
|
modelId: providerConfig.modelId,
|
|
@@ -326,6 +347,14 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
326
347
|
}),
|
|
327
348
|
);
|
|
328
349
|
}
|
|
350
|
+
if (event.type === "iteration_end") {
|
|
351
|
+
void this.invoke<void>(
|
|
352
|
+
"persistSessionMessages",
|
|
353
|
+
sessionId,
|
|
354
|
+
liveSession?.agent.getMessages() ?? [],
|
|
355
|
+
liveSession?.config.systemPrompt,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
329
358
|
this.emit({
|
|
330
359
|
type: "agent_event",
|
|
331
360
|
payload: {
|
|
@@ -397,6 +426,15 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
397
426
|
if (!session) {
|
|
398
427
|
throw new Error(`session not found: ${input.sessionId}`);
|
|
399
428
|
}
|
|
429
|
+
session.config.telemetry?.capture({
|
|
430
|
+
event: "session.input_sent",
|
|
431
|
+
properties: {
|
|
432
|
+
sessionId: input.sessionId,
|
|
433
|
+
promptLength: input.prompt.length,
|
|
434
|
+
userImageCount: input.userImages?.length ?? 0,
|
|
435
|
+
userFileCount: input.userFiles?.length ?? 0,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
400
438
|
try {
|
|
401
439
|
const result = await this.runTurn(session, {
|
|
402
440
|
prompt: input.prompt,
|
|
@@ -428,6 +466,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
428
466
|
if (!session) {
|
|
429
467
|
return;
|
|
430
468
|
}
|
|
469
|
+
session.config.telemetry?.capture({
|
|
470
|
+
event: "session.aborted",
|
|
471
|
+
properties: { sessionId },
|
|
472
|
+
});
|
|
431
473
|
session.aborting = true;
|
|
432
474
|
session.agent.abort();
|
|
433
475
|
}
|
|
@@ -437,6 +479,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
437
479
|
if (!session) {
|
|
438
480
|
return;
|
|
439
481
|
}
|
|
482
|
+
session.config.telemetry?.capture({
|
|
483
|
+
event: "session.stopped",
|
|
484
|
+
properties: { sessionId },
|
|
485
|
+
});
|
|
440
486
|
await this.shutdownSession(session, {
|
|
441
487
|
status: "cancelled",
|
|
442
488
|
exitCode: null,
|
|
@@ -1200,4 +1246,16 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
1200
1246
|
apiKey: resolved.apiKey,
|
|
1201
1247
|
});
|
|
1202
1248
|
}
|
|
1249
|
+
|
|
1250
|
+
async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
|
|
1251
|
+
const session = this.sessions.get(sessionId);
|
|
1252
|
+
if (!session) {
|
|
1253
|
+
throw new Error(`session not found: ${sessionId}`);
|
|
1254
|
+
}
|
|
1255
|
+
session.config.modelId = modelId;
|
|
1256
|
+
const agentWithConnection = session.agent as Agent & {
|
|
1257
|
+
updateConnection?: (overrides: { modelId?: string }) => void;
|
|
1258
|
+
};
|
|
1259
|
+
agentWithConnection.updateConnection?.({ modelId });
|
|
1260
|
+
}
|
|
1203
1261
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { tryAcquireRpcSpawnLease } from "./rpc-spawn-lease";
|
|
6
|
+
|
|
7
|
+
describe("tryAcquireRpcSpawnLease", () => {
|
|
8
|
+
const tempDirs: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
delete process.env.CLINE_DATA_DIR;
|
|
12
|
+
for (const dir of tempDirs.splice(0)) {
|
|
13
|
+
rmSync(dir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("allows only one active lease per address", () => {
|
|
18
|
+
const dataDir = mkdtempSync(path.join(os.tmpdir(), "rpc-spawn-lease-"));
|
|
19
|
+
tempDirs.push(dataDir);
|
|
20
|
+
process.env.CLINE_DATA_DIR = dataDir;
|
|
21
|
+
|
|
22
|
+
const first = tryAcquireRpcSpawnLease("127.0.0.1:4317");
|
|
23
|
+
const second = tryAcquireRpcSpawnLease("127.0.0.1:4317");
|
|
24
|
+
|
|
25
|
+
expect(first).toBeDefined();
|
|
26
|
+
expect(second).toBeUndefined();
|
|
27
|
+
|
|
28
|
+
first?.release();
|
|
29
|
+
|
|
30
|
+
const third = tryAcquireRpcSpawnLease("127.0.0.1:4317");
|
|
31
|
+
expect(third).toBeDefined();
|
|
32
|
+
third?.release();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("lets different addresses acquire independent leases", () => {
|
|
36
|
+
const dataDir = mkdtempSync(path.join(os.tmpdir(), "rpc-spawn-lease-"));
|
|
37
|
+
tempDirs.push(dataDir);
|
|
38
|
+
process.env.CLINE_DATA_DIR = dataDir;
|
|
39
|
+
|
|
40
|
+
const first = tryAcquireRpcSpawnLease("127.0.0.1:4317");
|
|
41
|
+
const second = tryAcquireRpcSpawnLease("127.0.0.1:4318");
|
|
42
|
+
|
|
43
|
+
expect(first).toBeDefined();
|
|
44
|
+
expect(second).toBeDefined();
|
|
45
|
+
|
|
46
|
+
first?.release();
|
|
47
|
+
second?.release();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import { resolveSessionDataDir } from "@clinebot/shared/storage";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LEASE_TTL_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
interface RpcSpawnLeaseRecord {
|
|
16
|
+
address: string;
|
|
17
|
+
pid: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RpcSpawnLease {
|
|
22
|
+
path: string;
|
|
23
|
+
release: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function encodeAddress(address: string): string {
|
|
27
|
+
return Buffer.from(address).toString("base64url");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getLeasePath(address: string): string {
|
|
31
|
+
return resolve(
|
|
32
|
+
resolveSessionDataDir(),
|
|
33
|
+
"rpc",
|
|
34
|
+
"spawn-leases",
|
|
35
|
+
`${encodeAddress(address)}.lock`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isProcessAlive(pid: number): boolean {
|
|
40
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldClearLease(path: string, ttlMs: number): boolean {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(path, "utf8");
|
|
54
|
+
const parsed = JSON.parse(raw) as Partial<RpcSpawnLeaseRecord>;
|
|
55
|
+
const createdAt = Number(parsed.createdAt ?? 0);
|
|
56
|
+
if (!Number.isFinite(createdAt) || createdAt <= 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (Date.now() - createdAt > ttlMs) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return !isProcessAlive(Number(parsed.pid ?? 0));
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function tryAcquireRpcSpawnLease(
|
|
69
|
+
address: string,
|
|
70
|
+
options?: { ttlMs?: number },
|
|
71
|
+
): RpcSpawnLease | undefined {
|
|
72
|
+
const ttlMs = Math.max(1_000, options?.ttlMs ?? DEFAULT_LEASE_TTL_MS);
|
|
73
|
+
const path = getLeasePath(address);
|
|
74
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
75
|
+
|
|
76
|
+
if (existsSync(path) && shouldClearLease(path, ttlMs)) {
|
|
77
|
+
rmSync(path, { force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let fd: number | undefined;
|
|
81
|
+
try {
|
|
82
|
+
fd = openSync(path, "wx");
|
|
83
|
+
const record: RpcSpawnLeaseRecord = {
|
|
84
|
+
address,
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
createdAt: Date.now(),
|
|
87
|
+
};
|
|
88
|
+
writeFileSync(fd, JSON.stringify(record), "utf8");
|
|
89
|
+
} catch {
|
|
90
|
+
if (typeof fd === "number") {
|
|
91
|
+
try {
|
|
92
|
+
closeSync(fd);
|
|
93
|
+
} catch {
|
|
94
|
+
// Best effort.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let released = false;
|
|
101
|
+
return {
|
|
102
|
+
path,
|
|
103
|
+
release: () => {
|
|
104
|
+
if (released) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
released = true;
|
|
108
|
+
try {
|
|
109
|
+
if (typeof fd === "number") {
|
|
110
|
+
closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Best effort.
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
rmSync(path, { force: true });
|
|
117
|
+
} catch {
|
|
118
|
+
// Best effort.
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -70,6 +70,8 @@ export function deriveSubsessionStatus(event: HookEventPayload): SessionStatus {
|
|
|
70
70
|
switch (event.hookName) {
|
|
71
71
|
case "agent_end":
|
|
72
72
|
return "completed";
|
|
73
|
+
case "agent_error":
|
|
74
|
+
return "failed";
|
|
73
75
|
case "session_shutdown": {
|
|
74
76
|
const reason = String(event.reason ?? "").toLowerCase();
|
|
75
77
|
if (
|
|
@@ -7,12 +7,14 @@ import type {
|
|
|
7
7
|
ToolApprovalResult,
|
|
8
8
|
} from "@clinebot/agents";
|
|
9
9
|
import { getRpcServerDefaultAddress, getRpcServerHealth } from "@clinebot/rpc";
|
|
10
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
10
11
|
import { resolveSessionDataDir } from "@clinebot/shared/storage";
|
|
11
12
|
import { nanoid } from "nanoid";
|
|
12
13
|
import { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
13
14
|
import type { ToolExecutors } from "../tools";
|
|
14
15
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
15
16
|
import { RpcCoreSessionService } from "./rpc-session-service";
|
|
17
|
+
import { tryAcquireRpcSpawnLease } from "./rpc-spawn-lease";
|
|
16
18
|
import type { SessionManager } from "./session-manager";
|
|
17
19
|
import { CoreSessionService } from "./session-service";
|
|
18
20
|
|
|
@@ -33,6 +35,7 @@ export interface CreateSessionHostOptions {
|
|
|
33
35
|
rpcConnectAttempts?: number;
|
|
34
36
|
rpcConnectDelayMs?: number;
|
|
35
37
|
defaultToolExecutors?: Partial<ToolExecutors>;
|
|
38
|
+
telemetry?: ITelemetryService;
|
|
36
39
|
toolPolicies?: AgentConfig["toolPolicies"];
|
|
37
40
|
requestToolApproval?: (
|
|
38
41
|
request: ToolApprovalRequest,
|
|
@@ -42,13 +45,19 @@ export interface CreateSessionHostOptions {
|
|
|
42
45
|
export type SessionHost = SessionManager;
|
|
43
46
|
|
|
44
47
|
function startRpcServerInBackground(address: string): void {
|
|
48
|
+
const lease = tryAcquireRpcSpawnLease(address);
|
|
49
|
+
if (!lease) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
45
52
|
const launcher = process.execPath;
|
|
46
53
|
const entryArg = process.argv[1]?.trim();
|
|
47
54
|
if (!entryArg) {
|
|
55
|
+
lease.release();
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
50
58
|
const entry = resolve(process.cwd(), entryArg);
|
|
51
59
|
if (!existsSync(entry)) {
|
|
60
|
+
lease.release();
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
const conditionsArg = process.execArgv.find((arg) =>
|
|
@@ -73,6 +82,7 @@ function startRpcServerInBackground(address: string): void {
|
|
|
73
82
|
cwd: process.cwd(),
|
|
74
83
|
});
|
|
75
84
|
child.unref();
|
|
85
|
+
setTimeout(() => lease.release(), 10_000).unref();
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
async function tryConnectRpcBackend(
|
|
@@ -190,13 +200,16 @@ export async function resolveSessionBackend(
|
|
|
190
200
|
export async function createSessionHost(
|
|
191
201
|
options: CreateSessionHostOptions,
|
|
192
202
|
): Promise<SessionHost> {
|
|
203
|
+
const distinctId = resolveHostDistinctId(options.distinctId);
|
|
204
|
+
options.telemetry?.setDistinctId(distinctId);
|
|
193
205
|
const backend =
|
|
194
206
|
options.sessionService ?? (await resolveSessionBackend(options));
|
|
195
207
|
return new DefaultSessionManager({
|
|
196
208
|
sessionService: backend,
|
|
197
209
|
defaultToolExecutors: options.defaultToolExecutors,
|
|
210
|
+
telemetry: options.telemetry,
|
|
198
211
|
toolPolicies: options.toolPolicies,
|
|
199
212
|
requestToolApproval: options.requestToolApproval,
|
|
200
|
-
distinctId
|
|
213
|
+
distinctId,
|
|
201
214
|
});
|
|
202
215
|
}
|
|
@@ -64,4 +64,5 @@ export interface SessionManager {
|
|
|
64
64
|
readTranscript(sessionId: string, maxChars?: number): Promise<string>;
|
|
65
65
|
readHooks(sessionId: string, limit?: number): Promise<unknown[]>;
|
|
66
66
|
subscribe(listener: (event: CoreSessionEvent) => void): () => void;
|
|
67
|
+
updateSessionModel?(sessionId: string, modelId: string): Promise<void>;
|
|
67
68
|
}
|
|
@@ -2,7 +2,11 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type LegacyClineUserInfo,
|
|
7
|
+
migrateLegacyProviderSettings,
|
|
8
|
+
resolveLegacyClineAuth,
|
|
9
|
+
} from "./provider-settings-legacy-migration";
|
|
6
10
|
import { ProviderSettingsManager } from "./provider-settings-manager";
|
|
7
11
|
|
|
8
12
|
describe("migrateLegacyProviderSettings", () => {
|
|
@@ -173,3 +177,131 @@ describe("migrateLegacyProviderSettings", () => {
|
|
|
173
177
|
);
|
|
174
178
|
});
|
|
175
179
|
});
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// resolveLegacyClineAuth – pure in-memory tests
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
/** Builds a realistic LegacyClineUserInfo JSON string. */
|
|
186
|
+
function makeClineAccountJson(
|
|
187
|
+
overrides: Partial<LegacyClineUserInfo> & { userId?: string } = {},
|
|
188
|
+
): string {
|
|
189
|
+
return JSON.stringify({
|
|
190
|
+
idToken: overrides.idToken ?? "id-token-abc",
|
|
191
|
+
expiresAt: overrides.expiresAt ?? 1750000000000,
|
|
192
|
+
refreshToken: overrides.refreshToken ?? "refresh-token-xyz",
|
|
193
|
+
userInfo: overrides.userInfo ?? {
|
|
194
|
+
id: overrides.userId ?? "user-42",
|
|
195
|
+
email: "test@example.com",
|
|
196
|
+
displayName: "Test User",
|
|
197
|
+
termsAcceptedAt: "2025-01-01T00:00:00Z",
|
|
198
|
+
clineBenchConsent: false,
|
|
199
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
200
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
201
|
+
},
|
|
202
|
+
provider: overrides.provider ?? "google",
|
|
203
|
+
startedAt: overrides.startedAt ?? Date.now(),
|
|
204
|
+
} satisfies LegacyClineUserInfo);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
describe("resolveLegacyClineAuth", () => {
|
|
208
|
+
it("extracts all auth fields from a complete legacy account JSON", () => {
|
|
209
|
+
const result = resolveLegacyClineAuth(
|
|
210
|
+
makeClineAccountJson({
|
|
211
|
+
idToken: "my-id-token",
|
|
212
|
+
expiresAt: 1750000000000,
|
|
213
|
+
refreshToken: "my-refresh",
|
|
214
|
+
userId: "user-123",
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(result).toEqual({
|
|
219
|
+
accessToken: "my-id-token",
|
|
220
|
+
refreshToken: "my-refresh",
|
|
221
|
+
expiresAt: 1750000000000,
|
|
222
|
+
accountId: "user-123",
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("maps idToken to accessToken", () => {
|
|
227
|
+
const result = resolveLegacyClineAuth(
|
|
228
|
+
makeClineAccountJson({ idToken: "tok-abc" }),
|
|
229
|
+
);
|
|
230
|
+
expect(result?.accessToken).toBe("tok-abc");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("preserves expiresAt as a number", () => {
|
|
234
|
+
const result = resolveLegacyClineAuth(
|
|
235
|
+
makeClineAccountJson({ expiresAt: 9999999999999 }),
|
|
236
|
+
);
|
|
237
|
+
expect(result?.expiresAt).toBe(9999999999999);
|
|
238
|
+
expect(typeof result?.expiresAt).toBe("number");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("maps userInfo.id to accountId", () => {
|
|
242
|
+
const result = resolveLegacyClineAuth(
|
|
243
|
+
makeClineAccountJson({ userId: "uid-xyz" }),
|
|
244
|
+
);
|
|
245
|
+
expect(result?.accountId).toBe("uid-xyz");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns undefined accountId when userInfo is missing entirely", () => {
|
|
249
|
+
const raw = JSON.stringify({
|
|
250
|
+
idToken: "tok",
|
|
251
|
+
expiresAt: 1000,
|
|
252
|
+
refreshToken: "ref",
|
|
253
|
+
provider: "google",
|
|
254
|
+
startedAt: 1,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = resolveLegacyClineAuth(raw);
|
|
258
|
+
expect(result).toBeDefined();
|
|
259
|
+
expect(result?.accessToken).toBe("tok");
|
|
260
|
+
expect(result?.accountId).toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("returns undefined accountId when userInfo.id is missing", () => {
|
|
264
|
+
const raw = JSON.stringify({
|
|
265
|
+
idToken: "tok",
|
|
266
|
+
expiresAt: 1000,
|
|
267
|
+
refreshToken: "ref",
|
|
268
|
+
userInfo: {
|
|
269
|
+
email: "x@y.com",
|
|
270
|
+
displayName: "X",
|
|
271
|
+
termsAcceptedAt: "2025-01-01T00:00:00Z",
|
|
272
|
+
clineBenchConsent: false,
|
|
273
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
274
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
275
|
+
},
|
|
276
|
+
provider: "google",
|
|
277
|
+
startedAt: 1,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const result = resolveLegacyClineAuth(raw);
|
|
281
|
+
expect(result).toBeDefined();
|
|
282
|
+
expect(result?.accountId).toBeUndefined();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("returns undefined for invalid json", () => {
|
|
286
|
+
expect(resolveLegacyClineAuth(undefined)).toBeUndefined();
|
|
287
|
+
expect(resolveLegacyClineAuth("")).toBeUndefined();
|
|
288
|
+
expect(resolveLegacyClineAuth(" \n\t ")).toBeUndefined();
|
|
289
|
+
expect(resolveLegacyClineAuth("not-json{{{")).toBeUndefined();
|
|
290
|
+
expect(resolveLegacyClineAuth("null")).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns undefined fields when idToken/refreshToken are missing from JSON", () => {
|
|
294
|
+
const raw = JSON.stringify({
|
|
295
|
+
userInfo: { id: "uid" },
|
|
296
|
+
provider: "google",
|
|
297
|
+
startedAt: 1,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const result = resolveLegacyClineAuth(raw);
|
|
301
|
+
expect(result).toBeDefined();
|
|
302
|
+
expect(result?.accessToken).toBeUndefined();
|
|
303
|
+
expect(result?.refreshToken).toBeUndefined();
|
|
304
|
+
expect(result?.expiresAt).toBeUndefined();
|
|
305
|
+
expect(result?.accountId).toBe("uid");
|
|
306
|
+
});
|
|
307
|
+
});
|