@clinebot/core 0.0.7 → 0.0.11
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/auth/cline.d.ts +2 -0
- package/dist/auth/codex.d.ts +5 -1
- package/dist/auth/oca.d.ts +7 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.node.js +124 -122
- package/dist/input/mention-enricher.d.ts +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +13 -17
- package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
- package/dist/session/session-agent-events.d.ts +15 -0
- package/dist/session/session-config-builder.d.ts +13 -0
- package/dist/session/session-manager.d.ts +2 -2
- package/dist/session/session-team-coordination.d.ts +12 -0
- package/dist/session/session-telemetry.d.ts +9 -0
- package/dist/session/unified-session-persistence-service.d.ts +12 -16
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/session/utils/types.d.ts +2 -1
- package/dist/telemetry/core-events.d.ts +122 -0
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/executors/file-read.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/schemas.d.ts +46 -3
- package/dist/tools/types.d.ts +3 -3
- package/dist/types/config.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +4 -4
- package/dist/types.d.ts +1 -1
- package/package.json +4 -3
- package/src/auth/cline.ts +35 -1
- package/src/auth/codex.ts +27 -2
- package/src/auth/oca.ts +31 -4
- package/src/auth/types.ts +3 -0
- package/src/index.ts +27 -0
- package/src/input/mention-enricher.test.ts +3 -0
- package/src/input/mention-enricher.ts +3 -0
- package/src/providers/local-provider-service.ts +6 -7
- package/src/runtime/hook-file-hooks.ts +11 -10
- package/src/runtime/session-runtime.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -1
- package/src/session/default-session-manager.test.ts +131 -0
- package/src/session/default-session-manager.ts +372 -602
- package/src/session/runtime-oauth-token-manager.ts +21 -14
- package/src/session/session-agent-events.ts +159 -0
- package/src/session/session-config-builder.ts +111 -0
- package/src/session/session-host.ts +13 -0
- package/src/session/session-manager.ts +2 -2
- package/src/session/session-team-coordination.ts +198 -0
- package/src/session/session-telemetry.ts +95 -0
- package/src/session/unified-session-persistence-service.test.ts +81 -0
- package/src/session/unified-session-persistence-service.ts +470 -469
- package/src/session/utils/helpers.ts +14 -4
- package/src/session/utils/types.ts +2 -1
- package/src/storage/provider-settings-legacy-migration.ts +3 -3
- package/src/telemetry/core-events.ts +344 -0
- package/src/tools/definitions.test.ts +121 -7
- package/src/tools/definitions.ts +60 -24
- package/src/tools/executors/file-read.test.ts +29 -5
- package/src/tools/executors/file-read.ts +17 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/presets.ts +1 -1
- package/src/tools/schemas.ts +65 -5
- package/src/tools/types.ts +7 -3
- package/src/types/config.ts +1 -1
- package/src/types/provider-settings.ts +6 -6
- package/src/types.ts +1 -1
package/src/auth/cline.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import {
|
|
3
|
+
captureAuthFailed,
|
|
4
|
+
captureAuthLoggedOut,
|
|
5
|
+
captureAuthStarted,
|
|
6
|
+
captureAuthSucceeded,
|
|
7
|
+
identifyAccount,
|
|
8
|
+
} from "../telemetry/core-events";
|
|
1
9
|
import { startLocalOAuthServer } from "./server.js";
|
|
2
10
|
import type {
|
|
3
11
|
OAuthCredentials,
|
|
@@ -63,6 +71,7 @@ export interface ClineOAuthProviderOptions {
|
|
|
63
71
|
callbackPath?: string;
|
|
64
72
|
callbackPorts?: number[];
|
|
65
73
|
requestTimeoutMs?: number;
|
|
74
|
+
telemetry?: ITelemetryService;
|
|
66
75
|
/**
|
|
67
76
|
* Optional identity provider name for token exchange.
|
|
68
77
|
*/
|
|
@@ -253,6 +262,7 @@ export async function loginClineOAuth(
|
|
|
253
262
|
callbacks: OAuthLoginCallbacks;
|
|
254
263
|
},
|
|
255
264
|
): Promise<ClineOAuthCredentials> {
|
|
265
|
+
captureAuthStarted(options.telemetry, options.provider ?? "cline");
|
|
256
266
|
const callbackPorts = options.callbackPorts?.length
|
|
257
267
|
? options.callbackPorts
|
|
258
268
|
: DEFAULT_CALLBACK_PORTS;
|
|
@@ -312,7 +322,26 @@ export async function loginClineOAuth(
|
|
|
312
322
|
throw new Error("Missing authorization code");
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
|
|
325
|
+
const credentials = await exchangeAuthorizationCode(
|
|
326
|
+
code,
|
|
327
|
+
callbackUrl,
|
|
328
|
+
options,
|
|
329
|
+
provider,
|
|
330
|
+
);
|
|
331
|
+
captureAuthSucceeded(options.telemetry, provider ?? "cline");
|
|
332
|
+
identifyAccount(options.telemetry, {
|
|
333
|
+
id: credentials.accountId,
|
|
334
|
+
email: credentials.email,
|
|
335
|
+
provider: provider ?? "cline",
|
|
336
|
+
});
|
|
337
|
+
return credentials;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
captureAuthFailed(
|
|
340
|
+
options.telemetry,
|
|
341
|
+
options.provider ?? "cline",
|
|
342
|
+
error instanceof Error ? error.message : String(error),
|
|
343
|
+
);
|
|
344
|
+
throw error;
|
|
316
345
|
} finally {
|
|
317
346
|
localServer.close();
|
|
318
347
|
}
|
|
@@ -384,6 +413,11 @@ export async function getValidClineCredentials(
|
|
|
384
413
|
return await refreshClineToken(currentCredentials, providerOptions);
|
|
385
414
|
} catch (error) {
|
|
386
415
|
if (error instanceof ClineOAuthTokenError && error.isLikelyInvalidGrant()) {
|
|
416
|
+
captureAuthLoggedOut(
|
|
417
|
+
providerOptions.telemetry,
|
|
418
|
+
providerOptions.provider ?? "cline",
|
|
419
|
+
"invalid_grant",
|
|
420
|
+
);
|
|
387
421
|
return null;
|
|
388
422
|
}
|
|
389
423
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/codex.ts
CHANGED
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
* It is only intended for CLI use, not browser environments.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
8
9
|
import { nanoid } from "nanoid";
|
|
10
|
+
import {
|
|
11
|
+
captureAuthFailed,
|
|
12
|
+
captureAuthLoggedOut,
|
|
13
|
+
captureAuthStarted,
|
|
14
|
+
captureAuthSucceeded,
|
|
15
|
+
identifyAccount,
|
|
16
|
+
} from "../telemetry/core-events";
|
|
9
17
|
import { startLocalOAuthServer } from "./server.js";
|
|
10
18
|
import type {
|
|
11
19
|
OAuthCredentials,
|
|
@@ -295,7 +303,9 @@ export async function loginOpenAICodex(options: {
|
|
|
295
303
|
onProgress?: (message: string) => void;
|
|
296
304
|
onManualCodeInput?: () => Promise<string>;
|
|
297
305
|
originator?: string;
|
|
306
|
+
telemetry?: ITelemetryService;
|
|
298
307
|
}): Promise<OAuthCredentials> {
|
|
308
|
+
captureAuthStarted(options.telemetry, "openai-codex");
|
|
299
309
|
const callbackConfig = resolveCallbackServerConfig();
|
|
300
310
|
const { verifier, state, url } = await createAuthorizationFlow(
|
|
301
311
|
options.originator,
|
|
@@ -352,7 +362,21 @@ export async function loginOpenAICodex(options: {
|
|
|
352
362
|
throw new Error("Token exchange failed");
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
|
|
365
|
+
const credentials = toCodexCredentials(tokenResult);
|
|
366
|
+
captureAuthSucceeded(options.telemetry, "openai-codex");
|
|
367
|
+
identifyAccount(options.telemetry, {
|
|
368
|
+
id: credentials.accountId,
|
|
369
|
+
email: credentials.email,
|
|
370
|
+
provider: "openai-codex",
|
|
371
|
+
});
|
|
372
|
+
return credentials;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
captureAuthFailed(
|
|
375
|
+
options.telemetry,
|
|
376
|
+
"openai-codex",
|
|
377
|
+
error instanceof Error ? error.message : String(error),
|
|
378
|
+
);
|
|
379
|
+
throw error;
|
|
356
380
|
} finally {
|
|
357
381
|
server.close();
|
|
358
382
|
}
|
|
@@ -378,7 +402,7 @@ export async function refreshOpenAICodexToken(
|
|
|
378
402
|
|
|
379
403
|
export async function getValidOpenAICodexCredentials(
|
|
380
404
|
currentCredentials: OAuthCredentials | null,
|
|
381
|
-
options?: RefreshTokenResolution,
|
|
405
|
+
options?: RefreshTokenResolution & { telemetry?: ITelemetryService },
|
|
382
406
|
): Promise<OAuthCredentials | null> {
|
|
383
407
|
if (!currentCredentials) {
|
|
384
408
|
return null;
|
|
@@ -409,6 +433,7 @@ export async function getValidOpenAICodexCredentials(
|
|
|
409
433
|
error instanceof OpenAICodexOAuthTokenError &&
|
|
410
434
|
error.isLikelyInvalidGrant()
|
|
411
435
|
) {
|
|
436
|
+
captureAuthLoggedOut(options?.telemetry, "openai-codex", "invalid_grant");
|
|
412
437
|
return null;
|
|
413
438
|
}
|
|
414
439
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/oca.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
1
2
|
import { nanoid } from "nanoid";
|
|
3
|
+
import {
|
|
4
|
+
captureAuthFailed,
|
|
5
|
+
captureAuthLoggedOut,
|
|
6
|
+
captureAuthStarted,
|
|
7
|
+
captureAuthSucceeded,
|
|
8
|
+
identifyAccount,
|
|
9
|
+
} from "../telemetry/core-events";
|
|
2
10
|
import { startLocalOAuthServer } from "./server.js";
|
|
3
11
|
import type {
|
|
4
12
|
OAuthCredentials,
|
|
@@ -325,8 +333,12 @@ function buildAuthorizationUrl(input: {
|
|
|
325
333
|
}
|
|
326
334
|
|
|
327
335
|
export async function loginOcaOAuth(
|
|
328
|
-
options: OcaOAuthProviderOptions & {
|
|
336
|
+
options: OcaOAuthProviderOptions & {
|
|
337
|
+
callbacks: OAuthLoginCallbacks;
|
|
338
|
+
telemetry?: ITelemetryService;
|
|
339
|
+
},
|
|
329
340
|
): Promise<OAuthCredentials> {
|
|
341
|
+
captureAuthStarted(options.telemetry, "oca");
|
|
330
342
|
const config = resolveConfig(options.config);
|
|
331
343
|
const mode = resolveMode(options.mode);
|
|
332
344
|
const callbackPorts = options.callbackPorts?.length
|
|
@@ -391,13 +403,27 @@ export async function loginOcaOAuth(
|
|
|
391
403
|
throw new Error("State mismatch");
|
|
392
404
|
}
|
|
393
405
|
|
|
394
|
-
|
|
406
|
+
const credentials = await exchangeAuthorizationCode({
|
|
395
407
|
code,
|
|
396
408
|
state: returnedState,
|
|
397
409
|
mode,
|
|
398
410
|
config,
|
|
399
411
|
requestTimeoutMs,
|
|
400
412
|
});
|
|
413
|
+
captureAuthSucceeded(options.telemetry, "oca");
|
|
414
|
+
identifyAccount(options.telemetry, {
|
|
415
|
+
id: credentials.accountId,
|
|
416
|
+
email: credentials.email,
|
|
417
|
+
provider: "oca",
|
|
418
|
+
});
|
|
419
|
+
return credentials;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
captureAuthFailed(
|
|
422
|
+
options.telemetry,
|
|
423
|
+
"oca",
|
|
424
|
+
error instanceof Error ? error.message : String(error),
|
|
425
|
+
);
|
|
426
|
+
throw error;
|
|
401
427
|
} finally {
|
|
402
428
|
localServer.close();
|
|
403
429
|
}
|
|
@@ -447,8 +473,8 @@ export async function refreshOcaToken(
|
|
|
447
473
|
|
|
448
474
|
export async function getValidOcaCredentials(
|
|
449
475
|
currentCredentials: OAuthCredentials | null,
|
|
450
|
-
options?: OcaTokenResolution,
|
|
451
|
-
providerOptions?: OcaOAuthProviderOptions,
|
|
476
|
+
options?: OcaTokenResolution & { telemetry?: ITelemetryService },
|
|
477
|
+
providerOptions?: OcaOAuthProviderOptions & { telemetry?: ITelemetryService },
|
|
452
478
|
): Promise<OAuthCredentials | null> {
|
|
453
479
|
if (!currentCredentials) {
|
|
454
480
|
return null;
|
|
@@ -475,6 +501,7 @@ export async function getValidOcaCredentials(
|
|
|
475
501
|
return await refreshOcaToken(currentCredentials, providerOptions);
|
|
476
502
|
} catch (error) {
|
|
477
503
|
if (error instanceof OcaOAuthTokenError && error.isLikelyInvalidGrant()) {
|
|
504
|
+
captureAuthLoggedOut(providerOptions?.telemetry, "oca", "invalid_grant");
|
|
478
505
|
return null;
|
|
479
506
|
}
|
|
480
507
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
|
|
1
3
|
export interface OAuthPrompt {
|
|
2
4
|
message: string;
|
|
3
5
|
defaultValue?: string;
|
|
@@ -69,6 +71,7 @@ export interface OcaOAuthProviderOptions {
|
|
|
69
71
|
requestTimeoutMs?: number;
|
|
70
72
|
refreshBufferMs?: number;
|
|
71
73
|
retryableTokenGraceMs?: number;
|
|
74
|
+
telemetry?: ITelemetryService;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
export interface OcaTokenResolution {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
type ToolApprovalResult,
|
|
14
14
|
type ToolContext,
|
|
15
15
|
} from "@clinebot/agents";
|
|
16
|
+
export { LlmsModels, LlmsProviders } from "@clinebot/llms";
|
|
16
17
|
// Shared contracts and path helpers re-exported for app consumers.
|
|
17
18
|
export type {
|
|
18
19
|
AgentMode,
|
|
@@ -57,7 +58,10 @@ export {
|
|
|
57
58
|
ensureHookLogDir,
|
|
58
59
|
ensureParentDir,
|
|
59
60
|
resolveClineDataDir,
|
|
61
|
+
resolveClineDir,
|
|
60
62
|
resolveSessionDataDir,
|
|
63
|
+
setClineDir,
|
|
64
|
+
setClineDirIfUnset,
|
|
61
65
|
setHomeDir,
|
|
62
66
|
setHomeDirIfUnset,
|
|
63
67
|
} from "@clinebot/shared/storage";
|
|
@@ -123,6 +127,29 @@ export {
|
|
|
123
127
|
buildTeamProgressSummary,
|
|
124
128
|
toTeamProgressLifecycleEvent,
|
|
125
129
|
} from "./team";
|
|
130
|
+
export {
|
|
131
|
+
captureAuthFailed,
|
|
132
|
+
captureAuthLoggedOut,
|
|
133
|
+
captureAuthStarted,
|
|
134
|
+
captureAuthSucceeded,
|
|
135
|
+
captureConversationTurnEvent,
|
|
136
|
+
captureDiffEditFailure,
|
|
137
|
+
captureHookDiscovery,
|
|
138
|
+
captureMentionFailed,
|
|
139
|
+
captureMentionSearchResults,
|
|
140
|
+
captureMentionUsed,
|
|
141
|
+
captureModeSwitch,
|
|
142
|
+
captureProviderApiError,
|
|
143
|
+
captureSkillUsed,
|
|
144
|
+
captureSubagentExecution,
|
|
145
|
+
captureTaskCompleted,
|
|
146
|
+
captureTaskCreated,
|
|
147
|
+
captureTaskRestarted,
|
|
148
|
+
captureTokenUsage,
|
|
149
|
+
captureToolUsage,
|
|
150
|
+
identifyAccount,
|
|
151
|
+
LegacyTelemetryEvents,
|
|
152
|
+
} from "./telemetry/core-events";
|
|
126
153
|
export type { ITelemetryAdapter } from "./telemetry/ITelemetryAdapter";
|
|
127
154
|
export {
|
|
128
155
|
LoggerTelemetryAdapter,
|
|
@@ -32,6 +32,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
32
32
|
cwd,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
expect(result.mentions).toEqual(["src/index.ts"]);
|
|
35
36
|
expect(result.matchedFiles).toEqual(["src/index.ts"]);
|
|
36
37
|
expect(result.ignoredMentions).toEqual([]);
|
|
37
38
|
expect(result.prompt).toBe("Review @src/index.ts");
|
|
@@ -50,6 +51,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
50
51
|
cwd,
|
|
51
52
|
);
|
|
52
53
|
|
|
54
|
+
expect(result.mentions).toEqual(["missing/file.ts"]);
|
|
53
55
|
expect(result.matchedFiles).toEqual([]);
|
|
54
56
|
expect(result.ignoredMentions).toEqual(["missing/file.ts"]);
|
|
55
57
|
expect(result.prompt).toBe(
|
|
@@ -72,6 +74,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
72
74
|
{ maxTotalBytes: 5, maxFiles: 2, maxFileBytes: 5 },
|
|
73
75
|
);
|
|
74
76
|
|
|
77
|
+
expect(result.mentions).toEqual(["a.ts", "b.ts"]);
|
|
75
78
|
expect(result.matchedFiles).toEqual(["a.ts"]);
|
|
76
79
|
expect(result.ignoredMentions).toEqual(["b.ts"]);
|
|
77
80
|
expect(result.prompt).toBe("Use @a.ts and @b.ts");
|
|
@@ -13,6 +13,7 @@ export interface MentionEnricherOptions extends FastFileIndexOptions {
|
|
|
13
13
|
|
|
14
14
|
export interface MentionEnrichmentResult {
|
|
15
15
|
prompt: string;
|
|
16
|
+
mentions: string[];
|
|
16
17
|
matchedFiles: string[];
|
|
17
18
|
ignoredMentions: string[];
|
|
18
19
|
}
|
|
@@ -60,6 +61,7 @@ export async function enrichPromptWithMentions(
|
|
|
60
61
|
if (mentions.length === 0) {
|
|
61
62
|
return {
|
|
62
63
|
prompt: input,
|
|
64
|
+
mentions: [],
|
|
63
65
|
matchedFiles: [],
|
|
64
66
|
ignoredMentions: [],
|
|
65
67
|
};
|
|
@@ -113,6 +115,7 @@ export async function enrichPromptWithMentions(
|
|
|
113
115
|
|
|
114
116
|
return {
|
|
115
117
|
prompt: input,
|
|
118
|
+
mentions,
|
|
116
119
|
matchedFiles: matched,
|
|
117
120
|
ignoredMentions: ignored,
|
|
118
121
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import { models } from "@clinebot/llms";
|
|
3
|
+
import { LlmsModels, type LlmsProviders } from "@clinebot/llms";
|
|
5
4
|
import type {
|
|
6
5
|
RpcAddProviderActionRequest,
|
|
7
6
|
RpcOAuthProviderId,
|
|
@@ -215,7 +214,7 @@ function registerCustomProvider(
|
|
|
215
214
|
]),
|
|
216
215
|
);
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
LlmsModels.registerProvider({
|
|
219
218
|
provider: {
|
|
220
219
|
id: providerId,
|
|
221
220
|
name: entry.provider.name.trim() || titleCaseFromId(providerId),
|
|
@@ -318,7 +317,7 @@ export async function addLocalProvider(
|
|
|
318
317
|
}> {
|
|
319
318
|
const providerId = request.providerId.trim().toLowerCase();
|
|
320
319
|
if (!providerId) throw new Error("providerId is required");
|
|
321
|
-
if (
|
|
320
|
+
if (LlmsModels.hasProvider(providerId)) {
|
|
322
321
|
throw new Error(`provider "${providerId}" already exists`);
|
|
323
322
|
}
|
|
324
323
|
const providerName = request.name.trim();
|
|
@@ -411,10 +410,10 @@ export async function listLocalProviders(
|
|
|
411
410
|
settingsPath: string;
|
|
412
411
|
}> {
|
|
413
412
|
const state = manager.read();
|
|
414
|
-
const ids =
|
|
413
|
+
const ids = LlmsModels.getProviderIds().sort((a, b) => a.localeCompare(b));
|
|
415
414
|
const providerItems = await Promise.all(
|
|
416
415
|
ids.map(async (id): Promise<RpcProviderListItem> => {
|
|
417
|
-
const info = await
|
|
416
|
+
const info = await LlmsModels.getProvider(id);
|
|
418
417
|
const providerModels = await getLocalProviderModels(id);
|
|
419
418
|
const persistedSettings = state.providers[id]?.settings;
|
|
420
419
|
const providerName = info?.name ?? titleCaseFromId(id);
|
|
@@ -450,7 +449,7 @@ export async function getLocalProviderModels(
|
|
|
450
449
|
providerId: string,
|
|
451
450
|
): Promise<{ providerId: string; models: RpcProviderModel[] }> {
|
|
452
451
|
const id = providerId.trim();
|
|
453
|
-
const modelMap = await
|
|
452
|
+
const modelMap = await LlmsModels.getModelsForProvider(id);
|
|
454
453
|
const items = Object.entries(modelMap)
|
|
455
454
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
456
455
|
.map(([modelId, info]) => toRpcProviderModel(modelId, info));
|
|
@@ -214,18 +214,14 @@ async function writeToChildStdin(
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
await new Promise<void>((resolve, reject) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
221
|
-
resolve();
|
|
217
|
+
let settled = false;
|
|
218
|
+
const finish = (error?: Error | null) => {
|
|
219
|
+
if (settled) {
|
|
222
220
|
return;
|
|
223
221
|
}
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
stdin.once("error", onError);
|
|
227
|
-
stdin.end(body, (error?: Error | null) => {
|
|
222
|
+
settled = true;
|
|
228
223
|
stdin.off("error", onError);
|
|
224
|
+
stdin.off("close", onClose);
|
|
229
225
|
if (error) {
|
|
230
226
|
const code = (error as Error & { code?: string }).code;
|
|
231
227
|
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
@@ -236,7 +232,12 @@ async function writeToChildStdin(
|
|
|
236
232
|
return;
|
|
237
233
|
}
|
|
238
234
|
resolve();
|
|
239
|
-
}
|
|
235
|
+
};
|
|
236
|
+
const onError = (error: Error) => finish(error);
|
|
237
|
+
const onClose = () => finish();
|
|
238
|
+
stdin.on("error", onError);
|
|
239
|
+
stdin.once("close", onClose);
|
|
240
|
+
stdin.end(body, (error?: Error | null) => finish(error));
|
|
240
241
|
});
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -40,7 +40,7 @@ export interface RuntimeBuilder {
|
|
|
40
40
|
export interface SessionRuntime {
|
|
41
41
|
start(config: CoreSessionConfig): Promise<{ sessionId: string }>;
|
|
42
42
|
send(sessionId: string, prompt: string): Promise<AgentResult | undefined>;
|
|
43
|
-
abort(sessionId: string): Promise<void>;
|
|
43
|
+
abort(sessionId: string, reason?: unknown): Promise<void>;
|
|
44
44
|
stop(sessionId: string): Promise<void>;
|
|
45
45
|
poll(): Promise<string[]>;
|
|
46
46
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { AgentResult } from "@clinebot/agents";
|
|
13
|
-
import type {
|
|
13
|
+
import type { LlmsProviders } from "@clinebot/llms";
|
|
14
14
|
import { nanoid } from "nanoid";
|
|
15
15
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
16
16
|
import type { SessionSource, SessionStatus } from "../types/common";
|
|
@@ -348,6 +348,7 @@ describe("DefaultSessionManager e2e", () => {
|
|
|
348
348
|
await manager.stop(started.sessionId);
|
|
349
349
|
const stopped = await manager.get(started.sessionId);
|
|
350
350
|
expect(stopped?.status).toBe("cancelled");
|
|
351
|
+
expect(stopped?.exitCode).toBe(0);
|
|
351
352
|
expect(agentShutdown).toHaveBeenCalledTimes(1);
|
|
352
353
|
expect(runtimeShutdown).toHaveBeenCalledTimes(1);
|
|
353
354
|
const parsedManifest = JSON.parse(
|
|
@@ -309,6 +309,137 @@ describe("DefaultSessionManager", () => {
|
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
it("preserves per-turn metadata on prior assistant messages across turns", async () => {
|
|
313
|
+
const sessionId = "sess-meta-multi";
|
|
314
|
+
const manifest = createManifest(sessionId);
|
|
315
|
+
const persistSessionMessages = vi.fn();
|
|
316
|
+
const runtimeBuilder = {
|
|
317
|
+
build: vi.fn().mockReturnValue({
|
|
318
|
+
tools: [],
|
|
319
|
+
shutdown: vi.fn(),
|
|
320
|
+
}),
|
|
321
|
+
};
|
|
322
|
+
const firstTurnMessages = [
|
|
323
|
+
{
|
|
324
|
+
role: "user" as const,
|
|
325
|
+
content: [{ type: "text" as const, text: "hello" }],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
role: "assistant" as const,
|
|
329
|
+
content: [{ type: "text" as const, text: "world" }],
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
const secondTurnMessages = [
|
|
333
|
+
...firstTurnMessages,
|
|
334
|
+
{
|
|
335
|
+
role: "user" as const,
|
|
336
|
+
content: [{ type: "text" as const, text: "again" }],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
role: "assistant" as const,
|
|
340
|
+
content: [{ type: "text" as const, text: "still here" }],
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
const run = vi.fn().mockResolvedValue(
|
|
344
|
+
createResult({
|
|
345
|
+
usage: {
|
|
346
|
+
inputTokens: 33,
|
|
347
|
+
outputTokens: 12,
|
|
348
|
+
cacheReadTokens: 4,
|
|
349
|
+
cacheWriteTokens: 1,
|
|
350
|
+
totalCost: 0.42,
|
|
351
|
+
},
|
|
352
|
+
model: {
|
|
353
|
+
id: "claude-sonnet-4-6",
|
|
354
|
+
provider: "anthropic",
|
|
355
|
+
},
|
|
356
|
+
endedAt: new Date("2026-01-01T00:00:02.000Z"),
|
|
357
|
+
messages: firstTurnMessages,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
const continueFn = vi.fn().mockResolvedValue(
|
|
361
|
+
createResult({
|
|
362
|
+
usage: {
|
|
363
|
+
inputTokens: 10,
|
|
364
|
+
outputTokens: 5,
|
|
365
|
+
cacheReadTokens: 2,
|
|
366
|
+
cacheWriteTokens: 0,
|
|
367
|
+
totalCost: 0.12,
|
|
368
|
+
},
|
|
369
|
+
model: {
|
|
370
|
+
id: "claude-sonnet-4-6",
|
|
371
|
+
provider: "anthropic",
|
|
372
|
+
},
|
|
373
|
+
endedAt: new Date("2026-01-01T00:00:03.000Z"),
|
|
374
|
+
messages: secondTurnMessages,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
const agent = {
|
|
378
|
+
run,
|
|
379
|
+
continue: continueFn,
|
|
380
|
+
abort: vi.fn(),
|
|
381
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
382
|
+
restore: vi.fn(),
|
|
383
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
384
|
+
messages: [],
|
|
385
|
+
};
|
|
386
|
+
const sessionService = {
|
|
387
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
388
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
389
|
+
manifestPath: "/tmp/manifest-meta-multi.json",
|
|
390
|
+
transcriptPath: "/tmp/transcript-meta-multi.log",
|
|
391
|
+
hookPath: "/tmp/hook-meta-multi.log",
|
|
392
|
+
messagesPath: "/tmp/messages-meta-multi.json",
|
|
393
|
+
manifest,
|
|
394
|
+
}),
|
|
395
|
+
persistSessionMessages,
|
|
396
|
+
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
|
|
397
|
+
writeSessionManifest: vi.fn(),
|
|
398
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
399
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
400
|
+
};
|
|
401
|
+
const manager = new DefaultSessionManager({
|
|
402
|
+
distinctId,
|
|
403
|
+
sessionService: sessionService as never,
|
|
404
|
+
runtimeBuilder,
|
|
405
|
+
createAgent: () => agent as never,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await manager.start({
|
|
409
|
+
config: createConfig({
|
|
410
|
+
sessionId,
|
|
411
|
+
providerId: "anthropic",
|
|
412
|
+
modelId: "claude-sonnet-4-6",
|
|
413
|
+
}),
|
|
414
|
+
interactive: true,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await manager.send({ sessionId, prompt: "hello" });
|
|
418
|
+
await manager.send({ sessionId, prompt: "again" });
|
|
419
|
+
|
|
420
|
+
const persisted = persistSessionMessages.mock.calls[1]?.[1];
|
|
421
|
+
expect(persisted?.[1]).toMatchObject({
|
|
422
|
+
role: "assistant",
|
|
423
|
+
metrics: {
|
|
424
|
+
inputTokens: 33,
|
|
425
|
+
outputTokens: 12,
|
|
426
|
+
cacheReadTokens: 4,
|
|
427
|
+
cacheWriteTokens: 1,
|
|
428
|
+
cost: 0.42,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
expect(persisted?.[3]).toMatchObject({
|
|
432
|
+
role: "assistant",
|
|
433
|
+
metrics: {
|
|
434
|
+
inputTokens: 10,
|
|
435
|
+
outputTokens: 5,
|
|
436
|
+
cacheReadTokens: 2,
|
|
437
|
+
cacheWriteTokens: 0,
|
|
438
|
+
cost: 0.12,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
312
443
|
it("persists rendered messages when a turn fails", async () => {
|
|
313
444
|
const sessionId = "sess-failed-turn";
|
|
314
445
|
const manifest = createManifest(sessionId);
|