@clinebot/core 0.0.6 → 0.0.10

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.
Files changed (83) hide show
  1. package/dist/agents/hooks-config-loader.d.ts +1 -0
  2. package/dist/auth/cline.d.ts +2 -0
  3. package/dist/auth/codex.d.ts +5 -1
  4. package/dist/auth/oca.d.ts +7 -1
  5. package/dist/auth/types.d.ts +2 -0
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.node.d.ts +2 -0
  8. package/dist/index.node.js +164 -162
  9. package/dist/input/mention-enricher.d.ts +1 -0
  10. package/dist/providers/local-provider-service.d.ts +1 -1
  11. package/dist/runtime/session-runtime.d.ts +1 -1
  12. package/dist/session/default-session-manager.d.ts +13 -17
  13. package/dist/session/rpc-spawn-lease.d.ts +7 -0
  14. package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
  15. package/dist/session/session-agent-events.d.ts +15 -0
  16. package/dist/session/session-config-builder.d.ts +13 -0
  17. package/dist/session/session-manager.d.ts +2 -2
  18. package/dist/session/session-team-coordination.d.ts +12 -0
  19. package/dist/session/session-telemetry.d.ts +9 -0
  20. package/dist/session/unified-session-persistence-service.d.ts +12 -16
  21. package/dist/session/utils/helpers.d.ts +1 -1
  22. package/dist/session/utils/types.d.ts +1 -1
  23. package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
  24. package/dist/telemetry/core-events.d.ts +122 -0
  25. package/dist/tools/definitions.d.ts +1 -1
  26. package/dist/tools/executors/file-read.d.ts +1 -1
  27. package/dist/tools/index.d.ts +1 -1
  28. package/dist/tools/presets.d.ts +1 -1
  29. package/dist/tools/schemas.d.ts +48 -11
  30. package/dist/tools/types.d.ts +3 -3
  31. package/dist/types/config.d.ts +1 -1
  32. package/dist/types/events.d.ts +1 -1
  33. package/dist/types/provider-settings.d.ts +4 -4
  34. package/dist/types.d.ts +1 -1
  35. package/package.json +4 -3
  36. package/src/agents/hooks-config-loader.ts +2 -0
  37. package/src/auth/cline.ts +35 -1
  38. package/src/auth/codex.ts +27 -2
  39. package/src/auth/oca.ts +31 -4
  40. package/src/auth/types.ts +3 -0
  41. package/src/index.node.ts +4 -0
  42. package/src/index.ts +27 -0
  43. package/src/input/file-indexer.test.ts +40 -0
  44. package/src/input/file-indexer.ts +21 -0
  45. package/src/input/mention-enricher.test.ts +3 -0
  46. package/src/input/mention-enricher.ts +3 -0
  47. package/src/providers/local-provider-service.ts +6 -7
  48. package/src/runtime/hook-file-hooks.test.ts +51 -1
  49. package/src/runtime/hook-file-hooks.ts +91 -11
  50. package/src/runtime/session-runtime.ts +1 -1
  51. package/src/session/default-session-manager.e2e.test.ts +2 -1
  52. package/src/session/default-session-manager.ts +367 -601
  53. package/src/session/rpc-spawn-lease.test.ts +49 -0
  54. package/src/session/rpc-spawn-lease.ts +122 -0
  55. package/src/session/runtime-oauth-token-manager.ts +21 -14
  56. package/src/session/session-agent-events.ts +159 -0
  57. package/src/session/session-config-builder.ts +111 -0
  58. package/src/session/session-graph.ts +2 -0
  59. package/src/session/session-host.ts +21 -0
  60. package/src/session/session-manager.ts +2 -2
  61. package/src/session/session-team-coordination.ts +198 -0
  62. package/src/session/session-telemetry.ts +95 -0
  63. package/src/session/unified-session-persistence-service.test.ts +81 -0
  64. package/src/session/unified-session-persistence-service.ts +470 -469
  65. package/src/session/utils/helpers.ts +1 -1
  66. package/src/session/utils/types.ts +1 -1
  67. package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
  68. package/src/storage/provider-settings-legacy-migration.ts +63 -11
  69. package/src/telemetry/core-events.ts +344 -0
  70. package/src/tools/definitions.test.ts +203 -36
  71. package/src/tools/definitions.ts +66 -28
  72. package/src/tools/executors/editor.test.ts +35 -0
  73. package/src/tools/executors/editor.ts +33 -46
  74. package/src/tools/executors/file-read.test.ts +29 -5
  75. package/src/tools/executors/file-read.ts +17 -6
  76. package/src/tools/index.ts +2 -0
  77. package/src/tools/presets.ts +1 -1
  78. package/src/tools/schemas.ts +88 -38
  79. package/src/tools/types.ts +7 -3
  80. package/src/types/config.ts +1 -1
  81. package/src/types/events.ts +6 -1
  82. package/src/types/provider-settings.ts +6 -6
  83. package/src/types.ts +1 -1
@@ -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
+ }
@@ -1,4 +1,9 @@
1
- import type { providers as LlmsProviders } from "@clinebot/llms";
1
+ import type { LlmsProviders } from "@clinebot/llms";
2
+ import {
3
+ type ITelemetryService,
4
+ isOAuthProviderId,
5
+ type OAuthProviderId,
6
+ } from "@clinebot/shared";
2
7
  import {
3
8
  type ClineOAuthCredentials,
4
9
  getValidClineCredentials,
@@ -11,14 +16,7 @@ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
11
16
  const DEFAULT_CLINE_API_BASE_URL = "https://api.cline.bot";
12
17
  const WORKOS_TOKEN_PREFIX = "workos:";
13
18
 
14
- const MANAGED_OAUTH_PROVIDERS = ["cline", "oca", "openai-codex"] as const;
15
- type ManagedOAuthProviderId = (typeof MANAGED_OAUTH_PROVIDERS)[number];
16
-
17
- function isManagedOAuthProviderId(
18
- providerId: string,
19
- ): providerId is ManagedOAuthProviderId {
20
- return (MANAGED_OAUTH_PROVIDERS as readonly string[]).includes(providerId);
21
- }
19
+ type ManagedOAuthProviderId = OAuthProviderId;
22
20
 
23
21
  function toStoredAccessToken(
24
22
  providerId: ManagedOAuthProviderId,
@@ -143,21 +141,26 @@ export type RuntimeOAuthResolution = {
143
141
 
144
142
  export class RuntimeOAuthTokenManager {
145
143
  private readonly providerSettingsManager: ProviderSettingsManager;
144
+ private readonly telemetry?: ITelemetryService;
146
145
  private readonly refreshInFlight = new Map<
147
146
  ManagedOAuthProviderId,
148
147
  Promise<RuntimeOAuthResolution | null>
149
148
  >();
150
149
 
151
- constructor(options?: { providerSettingsManager?: ProviderSettingsManager }) {
150
+ constructor(options?: {
151
+ providerSettingsManager?: ProviderSettingsManager;
152
+ telemetry?: ITelemetryService;
153
+ }) {
152
154
  this.providerSettingsManager =
153
155
  options?.providerSettingsManager ?? new ProviderSettingsManager();
156
+ this.telemetry = options?.telemetry;
154
157
  }
155
158
 
156
159
  public async resolveProviderApiKey(input: {
157
160
  providerId: string;
158
161
  forceRefresh?: boolean;
159
162
  }): Promise<RuntimeOAuthResolution | null> {
160
- if (!isManagedOAuthProviderId(input.providerId)) {
163
+ if (!isOAuthProviderId(input.providerId)) {
161
164
  return null;
162
165
  }
163
166
  return this.resolveWithSingleFlight(input.providerId, input.forceRefresh);
@@ -249,6 +252,7 @@ export class RuntimeOAuthTokenManager {
249
252
  currentCredentials,
250
253
  {
251
254
  apiBaseUrl: settings.baseUrl?.trim() || DEFAULT_CLINE_API_BASE_URL,
255
+ telemetry: this.telemetry,
252
256
  },
253
257
  { forceRefresh },
254
258
  );
@@ -256,10 +260,13 @@ export class RuntimeOAuthTokenManager {
256
260
  if (providerId === "oca") {
257
261
  return getValidOcaCredentials(
258
262
  currentCredentials,
259
- { forceRefresh },
260
- { mode: settings.oca?.mode },
263
+ { forceRefresh, telemetry: this.telemetry },
264
+ { mode: settings.oca?.mode, telemetry: this.telemetry },
261
265
  );
262
266
  }
263
- return getValidOpenAICodexCredentials(currentCredentials, { forceRefresh });
267
+ return getValidOpenAICodexCredentials(currentCredentials, {
268
+ forceRefresh,
269
+ telemetry: this.telemetry,
270
+ });
264
271
  }
265
272
  }
@@ -0,0 +1,159 @@
1
+ import type { AgentEvent } from "@clinebot/agents";
2
+ import {
3
+ captureConversationTurnEvent,
4
+ captureDiffEditFailure,
5
+ captureProviderApiError,
6
+ captureSkillUsed,
7
+ captureTokenUsage,
8
+ captureToolUsage,
9
+ } from "../telemetry/core-events";
10
+ import type { CoreSessionConfig } from "../types/config";
11
+ import type { CoreSessionEvent } from "../types/events";
12
+ import type { SessionAccumulatedUsage } from "./session-manager";
13
+ import { serializeAgentEvent } from "./utils/helpers";
14
+ import type { ActiveSession } from "./utils/types";
15
+ import { accumulateUsageTotals } from "./utils/usage";
16
+
17
+ export function extractSkillNameFromToolInput(
18
+ input: unknown,
19
+ ): string | undefined {
20
+ if (!input || typeof input !== "object") return undefined;
21
+ const record = input as Record<string, unknown>;
22
+ const skillName = record.skill ?? record.skill_name ?? record.skillName;
23
+ if (typeof skillName !== "string") return undefined;
24
+ const trimmed = skillName.trim();
25
+ return trimmed.length > 0 ? trimmed : undefined;
26
+ }
27
+
28
+ export interface AgentEventContext {
29
+ sessionId: string;
30
+ config: CoreSessionConfig;
31
+ liveSession: ActiveSession | undefined;
32
+ usageBySession: Map<string, SessionAccumulatedUsage>;
33
+ persistMessages: (
34
+ sessionId: string,
35
+ messages: unknown[],
36
+ systemPrompt?: string,
37
+ ) => void;
38
+ emit: (event: CoreSessionEvent) => void;
39
+ }
40
+
41
+ export function handleAgentEvent(
42
+ ctx: AgentEventContext,
43
+ event: AgentEvent,
44
+ ): void {
45
+ const { sessionId, config, liveSession, emit } = ctx;
46
+ const telemetry = config.telemetry;
47
+
48
+ if (
49
+ event.type === "content_start" &&
50
+ event.contentType === "tool" &&
51
+ event.toolName === "skills"
52
+ ) {
53
+ const skillName = extractSkillNameFromToolInput(event.input);
54
+ if (skillName) {
55
+ captureSkillUsed(telemetry, {
56
+ ulid: sessionId,
57
+ skillName,
58
+ skillSource: "project",
59
+ skillsAvailableGlobal: 0,
60
+ skillsAvailableProject: 0,
61
+ provider: config.providerId,
62
+ modelId: config.modelId,
63
+ });
64
+ }
65
+ }
66
+
67
+ if (event.type === "content_end" && event.contentType === "tool") {
68
+ const toolName = event.toolName ?? "unknown";
69
+ const success = !event.error;
70
+ captureToolUsage(telemetry, {
71
+ ulid: sessionId,
72
+ tool: toolName,
73
+ autoApproved: undefined,
74
+ success,
75
+ modelId: config.modelId,
76
+ provider: config.providerId,
77
+ isNativeToolCall: false,
78
+ });
79
+ if (!success && (toolName === "editor" || toolName === "apply_patch")) {
80
+ captureDiffEditFailure(telemetry, {
81
+ ulid: sessionId,
82
+ modelId: config.modelId,
83
+ provider: config.providerId,
84
+ errorType: event.error,
85
+ isNativeToolCall: false,
86
+ });
87
+ }
88
+ }
89
+
90
+ if (event.type === "notice" && event.reason === "api_error") {
91
+ captureProviderApiError(telemetry, {
92
+ ulid: sessionId,
93
+ model: config.modelId,
94
+ provider: config.providerId,
95
+ errorMessage: event.message,
96
+ });
97
+ }
98
+
99
+ if (event.type === "error") {
100
+ captureProviderApiError(telemetry, {
101
+ ulid: sessionId,
102
+ model: config.modelId,
103
+ provider: config.providerId,
104
+ errorMessage: event.error?.message ?? "unknown error",
105
+ });
106
+ }
107
+
108
+ if (event.type === "usage" && liveSession?.turnUsageBaseline) {
109
+ ctx.usageBySession.set(
110
+ sessionId,
111
+ accumulateUsageTotals(liveSession.turnUsageBaseline, {
112
+ inputTokens: event.totalInputTokens,
113
+ outputTokens: event.totalOutputTokens,
114
+ totalCost: event.totalCost,
115
+ }),
116
+ );
117
+ captureConversationTurnEvent(telemetry, {
118
+ ulid: sessionId,
119
+ provider: config.providerId,
120
+ model: config.modelId,
121
+ source: "assistant",
122
+ mode: config.mode,
123
+ tokensIn: event.inputTokens,
124
+ tokensOut: event.outputTokens,
125
+ cacheWriteTokens: event.cacheWriteTokens,
126
+ cacheReadTokens: event.cacheReadTokens,
127
+ totalCost: event.cost,
128
+ isNativeToolCall: false,
129
+ });
130
+ captureTokenUsage(telemetry, {
131
+ ulid: sessionId,
132
+ tokensIn: event.inputTokens,
133
+ tokensOut: event.outputTokens,
134
+ model: config.modelId,
135
+ });
136
+ }
137
+
138
+ if (event.type === "iteration_end") {
139
+ ctx.persistMessages(
140
+ sessionId,
141
+ liveSession?.agent.getMessages() ?? [],
142
+ liveSession?.config.systemPrompt,
143
+ );
144
+ }
145
+
146
+ emit({
147
+ type: "agent_event",
148
+ payload: { sessionId, event },
149
+ });
150
+ emit({
151
+ type: "chunk",
152
+ payload: {
153
+ sessionId,
154
+ stream: "agent",
155
+ chunk: serializeAgentEvent(event),
156
+ ts: Date.now(),
157
+ },
158
+ });
159
+ }
@@ -0,0 +1,111 @@
1
+ import type { LlmsProviders } from "@clinebot/llms";
2
+ import type { ITelemetryService } from "@clinebot/shared";
3
+ import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
4
+ import {
5
+ createHookAuditHooks,
6
+ createHookConfigFileHooks,
7
+ mergeAgentHooks,
8
+ } from "../runtime/hook-file-hooks";
9
+ import type { ProviderSettingsManager } from "../storage/provider-settings-manager";
10
+ import type { CoreSessionConfig } from "../types/config";
11
+ import {
12
+ type ProviderSettings,
13
+ toProviderConfig,
14
+ } from "../types/provider-settings";
15
+ import type { StartSessionInput } from "./session-manager";
16
+ import { hasRuntimeHooks, mergeAgentExtensions } from "./utils/helpers";
17
+
18
+ export function resolveWorkspacePath(config: CoreSessionConfig): string {
19
+ return config.workspaceRoot ?? config.cwd;
20
+ }
21
+
22
+ export async function buildEffectiveConfig(
23
+ input: StartSessionInput,
24
+ hookPath: string,
25
+ sessionId: string,
26
+ defaultTelemetry: ITelemetryService | undefined,
27
+ ): Promise<{
28
+ config: CoreSessionConfig;
29
+ pluginSandboxShutdown?: () => Promise<void>;
30
+ }> {
31
+ const workspacePath = resolveWorkspacePath(input.config);
32
+
33
+ const fileHooks = createHookConfigFileHooks({
34
+ cwd: input.config.cwd,
35
+ workspacePath,
36
+ rootSessionId: sessionId,
37
+ hookLogPath: hookPath,
38
+ logger: input.config.logger,
39
+ });
40
+ const auditHooks = hasRuntimeHooks(input.config.hooks)
41
+ ? undefined
42
+ : createHookAuditHooks({
43
+ hookLogPath: hookPath,
44
+ rootSessionId: sessionId,
45
+ workspacePath,
46
+ });
47
+ const effectiveHooks = mergeAgentHooks([
48
+ input.config.hooks,
49
+ fileHooks,
50
+ auditHooks,
51
+ ]);
52
+
53
+ const loadedPlugins = await resolveAndLoadAgentPlugins({
54
+ pluginPaths: input.config.pluginPaths,
55
+ workspacePath,
56
+ cwd: input.config.cwd,
57
+ });
58
+ const effectiveExtensions = mergeAgentExtensions(
59
+ input.config.extensions,
60
+ loadedPlugins.extensions,
61
+ );
62
+
63
+ return {
64
+ config: {
65
+ ...input.config,
66
+ hooks: effectiveHooks,
67
+ extensions: effectiveExtensions,
68
+ telemetry: input.config.telemetry ?? defaultTelemetry,
69
+ },
70
+ pluginSandboxShutdown: loadedPlugins.shutdown,
71
+ };
72
+ }
73
+
74
+ export function buildResolvedProviderConfig(
75
+ config: CoreSessionConfig,
76
+ providerSettingsManager: ProviderSettingsManager,
77
+ resolveReasoningFn: (
78
+ config: CoreSessionConfig,
79
+ storedReasoning: ProviderSettings["reasoning"],
80
+ ) => ProviderSettings["reasoning"],
81
+ ): LlmsProviders.ProviderConfig {
82
+ const stored = providerSettingsManager.getProviderSettings(config.providerId);
83
+ const settings: ProviderSettings = {
84
+ ...(stored ?? {}),
85
+ provider: config.providerId,
86
+ model: config.modelId,
87
+ apiKey: config.apiKey ?? stored?.apiKey,
88
+ baseUrl: config.baseUrl ?? stored?.baseUrl,
89
+ headers: config.headers ?? stored?.headers,
90
+ reasoning: resolveReasoningFn(config, stored?.reasoning),
91
+ };
92
+ const providerConfig = toProviderConfig(settings);
93
+ if (config.knownModels) {
94
+ providerConfig.knownModels = config.knownModels;
95
+ }
96
+ return providerConfig;
97
+ }
98
+
99
+ export function resolveReasoningSettings(
100
+ config: CoreSessionConfig,
101
+ storedReasoning: ProviderSettings["reasoning"],
102
+ ): ProviderSettings["reasoning"] {
103
+ const hasThinking = typeof config.thinking === "boolean";
104
+ const hasEffort = typeof config.reasoningEffort === "string";
105
+ if (!hasThinking && !hasEffort) return storedReasoning;
106
+ return {
107
+ ...(storedReasoning ?? {}),
108
+ ...(hasThinking ? { enabled: config.thinking } : {}),
109
+ ...(hasEffort ? { effort: config.reasoningEffort } : {}),
110
+ };
111
+ }
@@ -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 (
@@ -14,6 +14,7 @@ import { SqliteSessionStore } from "../storage/sqlite-session-store";
14
14
  import type { ToolExecutors } from "../tools";
15
15
  import { DefaultSessionManager } from "./default-session-manager";
16
16
  import { RpcCoreSessionService } from "./rpc-session-service";
17
+ import { tryAcquireRpcSpawnLease } from "./rpc-spawn-lease";
17
18
  import type { SessionManager } from "./session-manager";
18
19
  import { CoreSessionService } from "./session-service";
19
20
 
@@ -43,14 +44,29 @@ export interface CreateSessionHostOptions {
43
44
 
44
45
  export type SessionHost = SessionManager;
45
46
 
47
+ async function reconcileDeadSessionsIfSupported(
48
+ backend: SessionBackend,
49
+ ): Promise<void> {
50
+ const service = backend as SessionBackend & {
51
+ reconcileDeadSessions?: (limit?: number) => Promise<number>;
52
+ };
53
+ await service.reconcileDeadSessions?.().catch(() => {});
54
+ }
55
+
46
56
  function startRpcServerInBackground(address: string): void {
57
+ const lease = tryAcquireRpcSpawnLease(address);
58
+ if (!lease) {
59
+ return;
60
+ }
47
61
  const launcher = process.execPath;
48
62
  const entryArg = process.argv[1]?.trim();
49
63
  if (!entryArg) {
64
+ lease.release();
50
65
  return;
51
66
  }
52
67
  const entry = resolve(process.cwd(), entryArg);
53
68
  if (!existsSync(entry)) {
69
+ lease.release();
54
70
  return;
55
71
  }
56
72
  const conditionsArg = process.execArgv.find((arg) =>
@@ -75,6 +91,7 @@ function startRpcServerInBackground(address: string): void {
75
91
  cwd: process.cwd(),
76
92
  });
77
93
  child.unref();
94
+ setTimeout(() => lease.release(), 10_000).unref();
78
95
  }
79
96
 
80
97
  async function tryConnectRpcBackend(
@@ -148,12 +165,14 @@ export async function resolveSessionBackend(
148
165
  backendInitPromise = (async () => {
149
166
  if (mode === "local") {
150
167
  cachedBackend = createLocalBackend();
168
+ await reconcileDeadSessionsIfSupported(cachedBackend);
151
169
  return cachedBackend;
152
170
  }
153
171
 
154
172
  const existingRpcBackend = await tryConnectRpcBackend(address);
155
173
  if (existingRpcBackend) {
156
174
  cachedBackend = existingRpcBackend;
175
+ await reconcileDeadSessionsIfSupported(cachedBackend);
157
176
  return cachedBackend;
158
177
  }
159
178
 
@@ -172,6 +191,7 @@ export async function resolveSessionBackend(
172
191
  const rpcBackend = await tryConnectRpcBackend(address);
173
192
  if (rpcBackend) {
174
193
  cachedBackend = rpcBackend;
194
+ await reconcileDeadSessionsIfSupported(cachedBackend);
175
195
  return cachedBackend;
176
196
  }
177
197
  if (delayMs > 0) {
@@ -181,6 +201,7 @@ export async function resolveSessionBackend(
181
201
  }
182
202
 
183
203
  cachedBackend = createLocalBackend();
204
+ await reconcileDeadSessionsIfSupported(cachedBackend);
184
205
  return cachedBackend;
185
206
  })().finally(() => {
186
207
  backendInitPromise = undefined;
@@ -1,5 +1,5 @@
1
1
  import type { AgentResult } from "@clinebot/agents";
2
- import type { providers as LlmsProviders } from "@clinebot/llms";
2
+ import type { LlmsProviders } from "@clinebot/llms";
3
3
  import type { SessionSource } from "../types/common";
4
4
  import type { CoreSessionConfig } from "../types/config";
5
5
  import type { CoreSessionEvent } from "../types/events";
@@ -54,7 +54,7 @@ export interface SessionManager {
54
54
  getAccumulatedUsage(
55
55
  sessionId: string,
56
56
  ): Promise<SessionAccumulatedUsage | undefined>;
57
- abort(sessionId: string): Promise<void>;
57
+ abort(sessionId: string, reason?: unknown): Promise<void>;
58
58
  stop(sessionId: string): Promise<void>;
59
59
  dispose(reason?: string): Promise<void>;
60
60
  get(sessionId: string): Promise<SessionRecord | undefined>;