@clinebot/core 0.0.22 → 0.0.24

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 (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,1422 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import {
6
+ Agent,
7
+ type AgentConfig,
8
+ type AgentEvent,
9
+ type AgentResult,
10
+ createSpawnAgentTool,
11
+ type TeamEvent,
12
+ type Tool,
13
+ type ToolApprovalRequest,
14
+ type ToolApprovalResult,
15
+ } from "@clinebot/agents";
16
+ import type * as LlmsProviders from "@clinebot/llms/providers";
17
+ import {
18
+ createSessionId,
19
+ type ITelemetryService,
20
+ isLikelyAuthError,
21
+ normalizeUserInput,
22
+ } from "@clinebot/shared";
23
+ import { setHomeDirIfUnset } from "@clinebot/shared/storage";
24
+ import { nanoid } from "nanoid";
25
+ import { enrichPromptWithMentions } from "../input";
26
+ import { DefaultRuntimeBuilder } from "../runtime/runtime-builder";
27
+ import type { RuntimeBuilder } from "../runtime/session-runtime";
28
+ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
29
+ import {
30
+ captureAgentCreated,
31
+ captureAgentTeamCreated,
32
+ captureConversationTurnEvent,
33
+ captureModeSwitch,
34
+ captureSubagentExecution,
35
+ captureTaskCompleted,
36
+ } from "../telemetry/core-events";
37
+ import { resolveCoreDistinctId } from "../telemetry/distinct-id";
38
+ import { createBuiltinTools, type ToolExecutors, ToolPresets } from "../tools";
39
+ import { SessionSource, type SessionStatus } from "../types/common";
40
+ import type { CoreSessionConfig } from "../types/config";
41
+ import type { CoreSessionEvent } from "../types/events";
42
+ import type { SessionRecord } from "../types/sessions";
43
+ import type { FileSessionService } from "./file-session-service";
44
+ import type { RpcCoreSessionService } from "./rpc-session-service";
45
+ import {
46
+ OAuthReauthRequiredError,
47
+ type RuntimeOAuthResolution,
48
+ RuntimeOAuthTokenManager,
49
+ } from "./runtime-oauth-token-manager";
50
+ import {
51
+ type AgentEventContext,
52
+ buildTelemetryAgentIdentity,
53
+ extractAgentEventMetadata,
54
+ handleAgentEvent,
55
+ } from "./session-agent-events";
56
+ import { nowIso } from "./session-artifacts";
57
+ import {
58
+ buildEffectiveConfig,
59
+ buildResolvedProviderConfig,
60
+ resolveReasoningSettings,
61
+ resolveWorkspacePath,
62
+ } from "./session-config-builder";
63
+ import type {
64
+ SendSessionInput,
65
+ SessionAccumulatedUsage,
66
+ SessionManager,
67
+ StartSessionInput,
68
+ StartSessionResult,
69
+ } from "./session-manager";
70
+ import { SessionManifestSchema } from "./session-manifest";
71
+ import type {
72
+ CoreSessionService,
73
+ RootSessionArtifacts,
74
+ SessionRow,
75
+ } from "./session-service";
76
+ import {
77
+ buildTeamRunContinuationPrompt,
78
+ dispatchTeamEventToBackend,
79
+ emitTeamProgress,
80
+ formatModePrompt,
81
+ hasPendingTeamRunWork,
82
+ notifyTeamRunWaiters,
83
+ shouldAutoContinueTeamRuns,
84
+ trackTeamRunState,
85
+ waitForTeamRunUpdates,
86
+ } from "./session-team-coordination";
87
+ import {
88
+ emitMentionTelemetry,
89
+ emitSessionCreationTelemetry,
90
+ } from "./session-telemetry";
91
+ import {
92
+ extractWorkspaceMetadataFromSystemPrompt,
93
+ toSessionRecord,
94
+ withLatestAssistantTurnMetadata,
95
+ } from "./utils/helpers";
96
+ import type { ActiveSession, PreparedTurnInput } from "./utils/types";
97
+ import {
98
+ accumulateUsageTotals,
99
+ createInitialAccumulatedUsage,
100
+ } from "./utils/usage";
101
+
102
+ type SessionBackend =
103
+ | CoreSessionService
104
+ | RpcCoreSessionService
105
+ | FileSessionService;
106
+
107
+ const MAX_SCAN_LIMIT = 5000;
108
+ const MAX_USER_FILE_BYTES = 20 * 1_000 * 1_024;
109
+
110
+ async function loadUserFileContent(path: string): Promise<string> {
111
+ const fileStat = await stat(path);
112
+ if (!fileStat.isFile()) {
113
+ throw new Error("Path is not a file");
114
+ }
115
+ if (fileStat.size > MAX_USER_FILE_BYTES) {
116
+ throw new Error("File is too large to read into context.");
117
+ }
118
+ const content = await readFile(path, "utf8");
119
+ if (content.includes("\u0000")) {
120
+ throw new Error("Cannot read binary file into context.");
121
+ }
122
+ return content;
123
+ }
124
+
125
+ export interface DefaultSessionManagerOptions {
126
+ distinctId?: string;
127
+ sessionService: SessionBackend;
128
+ runtimeBuilder?: RuntimeBuilder;
129
+ createAgent?: (config: AgentConfig) => Agent;
130
+ defaultToolExecutors?: Partial<ToolExecutors>;
131
+ toolPolicies?: AgentConfig["toolPolicies"];
132
+ providerSettingsManager?: ProviderSettingsManager;
133
+ oauthTokenManager?: RuntimeOAuthTokenManager;
134
+ telemetry?: ITelemetryService;
135
+ requestToolApproval?: (
136
+ request: ToolApprovalRequest,
137
+ ) => Promise<ToolApprovalResult>;
138
+ }
139
+
140
+ export class DefaultSessionManager implements SessionManager {
141
+ private readonly sessionService: SessionBackend;
142
+ private readonly runtimeBuilder: RuntimeBuilder;
143
+ private readonly createAgentInstance: (config: AgentConfig) => Agent;
144
+ private readonly defaultToolExecutors?: Partial<ToolExecutors>;
145
+ private readonly defaultToolPolicies?: AgentConfig["toolPolicies"];
146
+ private readonly providerSettingsManager: ProviderSettingsManager;
147
+ private readonly oauthTokenManager: RuntimeOAuthTokenManager;
148
+ private readonly defaultTelemetry?: ITelemetryService;
149
+ private readonly defaultRequestToolApproval?: (
150
+ request: ToolApprovalRequest,
151
+ ) => Promise<ToolApprovalResult>;
152
+ private readonly listeners = new Set<(event: CoreSessionEvent) => void>();
153
+ private readonly sessions = new Map<string, ActiveSession>();
154
+ private readonly usageBySession = new Map<string, SessionAccumulatedUsage>();
155
+ private readonly subAgentStarts = new Map<
156
+ string,
157
+ { startedAt: number; rootSessionId: string }
158
+ >();
159
+
160
+ constructor(options: DefaultSessionManagerOptions) {
161
+ const homeDir = homedir();
162
+ if (homeDir) setHomeDirIfUnset(homeDir);
163
+ const distinctId = resolveCoreDistinctId(options.distinctId);
164
+ this.sessionService = options.sessionService;
165
+ this.runtimeBuilder = options.runtimeBuilder ?? new DefaultRuntimeBuilder();
166
+ this.createAgentInstance =
167
+ options.createAgent ?? ((config) => new Agent(config));
168
+ this.defaultToolExecutors = options.defaultToolExecutors;
169
+ this.defaultToolPolicies = options.toolPolicies;
170
+ this.providerSettingsManager =
171
+ options.providerSettingsManager ?? new ProviderSettingsManager();
172
+ this.oauthTokenManager =
173
+ options.oauthTokenManager ??
174
+ new RuntimeOAuthTokenManager({
175
+ providerSettingsManager: this.providerSettingsManager,
176
+ telemetry: options.telemetry,
177
+ });
178
+ this.defaultTelemetry = options.telemetry;
179
+ this.defaultTelemetry?.setDistinctId(distinctId);
180
+ this.defaultRequestToolApproval = options.requestToolApproval;
181
+ }
182
+
183
+ // ── Public API ──────────────────────────────────────────────────────
184
+
185
+ async start(input: StartSessionInput): Promise<StartSessionResult> {
186
+ const source = input.source ?? SessionSource.CLI;
187
+ const startedAt = nowIso();
188
+ const requestedSessionId = input.config.sessionId?.trim() ?? "";
189
+ const sessionId = requestedSessionId || createSessionId();
190
+ this.usageBySession.set(sessionId, createInitialAccumulatedUsage());
191
+
192
+ const sessionsDir =
193
+ ((await this.invokeOptionalValue("ensureSessionsDir")) as
194
+ | string
195
+ | undefined) ?? "";
196
+ if (!sessionsDir) {
197
+ throw new Error(
198
+ "session service method not available: ensureSessionsDir",
199
+ );
200
+ }
201
+
202
+ const sessionDir = join(sessionsDir, sessionId);
203
+ const transcriptPath = join(sessionDir, `${sessionId}.log`);
204
+ const hookPath = join(sessionDir, `${sessionId}.hooks.jsonl`);
205
+ const messagesPath = join(sessionDir, `${sessionId}.messages.json`);
206
+ const manifestPath = join(sessionDir, `${sessionId}.json`);
207
+ const workspacePath = resolveWorkspacePath(input.config);
208
+
209
+ const manifest = SessionManifestSchema.parse({
210
+ version: 1,
211
+ session_id: sessionId,
212
+ source,
213
+ pid: process.pid,
214
+ started_at: startedAt,
215
+ status: "running",
216
+ interactive: input.interactive === true,
217
+ provider: input.config.providerId,
218
+ model: input.config.modelId,
219
+ cwd: input.config.cwd,
220
+ workspace_root: workspacePath,
221
+ team_name: input.config.teamName,
222
+ enable_tools: input.config.enableTools,
223
+ enable_spawn: input.config.enableSpawnAgent,
224
+ enable_teams: input.config.enableAgentTeams,
225
+ prompt: input.prompt?.trim() || undefined,
226
+ messages_path: messagesPath,
227
+ });
228
+
229
+ const { config: effectiveConfig, pluginSandboxShutdown } =
230
+ await buildEffectiveConfig(
231
+ input,
232
+ hookPath,
233
+ sessionId,
234
+ this.defaultTelemetry,
235
+ (e) => void this.handlePluginEvent(sessionId, e),
236
+ );
237
+ const providerConfig = buildResolvedProviderConfig(
238
+ effectiveConfig,
239
+ this.providerSettingsManager,
240
+ resolveReasoningSettings,
241
+ );
242
+ const configWithProvider: CoreSessionConfig = {
243
+ ...effectiveConfig,
244
+ providerConfig,
245
+ };
246
+
247
+ const runtime = this.runtimeBuilder.build({
248
+ config: configWithProvider,
249
+ hooks: effectiveConfig.hooks,
250
+ extensions: effectiveConfig.extensions,
251
+ logger: configWithProvider.logger,
252
+ telemetry: configWithProvider.telemetry,
253
+ onTeamEvent: (event: TeamEvent) => {
254
+ void this.handleTeamEvent(sessionId, event);
255
+ configWithProvider.onTeamEvent?.(event);
256
+ },
257
+ createSpawnTool: () =>
258
+ this.createSpawnTool(configWithProvider, sessionId),
259
+ onTeamRestored: input.onTeamRestored,
260
+ userInstructionWatcher: input.userInstructionWatcher,
261
+ defaultToolExecutors:
262
+ input.defaultToolExecutors ?? this.defaultToolExecutors,
263
+ });
264
+ if (runtime.teamRuntime && !configWithProvider.teamName?.trim()) {
265
+ configWithProvider.teamName = runtime.teamRuntime.getTeamName();
266
+ }
267
+
268
+ const tools = [...runtime.tools, ...(configWithProvider.extraTools ?? [])];
269
+
270
+ const agent = this.createAgentInstance({
271
+ providerId: providerConfig.providerId,
272
+ modelId: providerConfig.modelId,
273
+ apiKey: providerConfig.apiKey,
274
+ baseUrl: providerConfig.baseUrl,
275
+ headers: providerConfig.headers,
276
+ knownModels: providerConfig.knownModels,
277
+ providerConfig,
278
+ thinking: configWithProvider.thinking,
279
+ reasoningEffort:
280
+ configWithProvider.reasoningEffort ?? providerConfig.reasoningEffort,
281
+ systemPrompt: configWithProvider.systemPrompt,
282
+ maxIterations: configWithProvider.maxIterations,
283
+ execution: configWithProvider.execution,
284
+ tools,
285
+ hooks: effectiveConfig.hooks,
286
+ extensions: effectiveConfig.extensions,
287
+ hookErrorMode: configWithProvider.hookErrorMode,
288
+ initialMessages: input.initialMessages,
289
+ userFileContentLoader: loadUserFileContent,
290
+ toolPolicies: input.toolPolicies ?? this.defaultToolPolicies,
291
+ requestToolApproval:
292
+ input.requestToolApproval ?? this.defaultRequestToolApproval,
293
+ onConsecutiveMistakeLimitReached:
294
+ configWithProvider.onConsecutiveMistakeLimitReached,
295
+ completionGuard: runtime.completionGuard,
296
+ logger: runtime.logger ?? configWithProvider.logger,
297
+ onEvent: (event: AgentEvent) =>
298
+ this.onAgentEvent(sessionId, configWithProvider, event),
299
+ });
300
+ const rootAgentIdentity = buildTelemetryAgentIdentity({
301
+ agentId: this.readAgentId(agent),
302
+ conversationId: this.readAgentConversationId(agent),
303
+ teamId: runtime.teamRuntime?.getTeamId(),
304
+ teamName: runtime.teamRuntime?.getTeamName(),
305
+ teamRole: runtime.teamRuntime ? "lead" : undefined,
306
+ });
307
+ emitSessionCreationTelemetry(
308
+ configWithProvider,
309
+ sessionId,
310
+ source,
311
+ requestedSessionId.length > 0,
312
+ workspacePath,
313
+ rootAgentIdentity,
314
+ );
315
+ if (rootAgentIdentity) {
316
+ captureAgentCreated(configWithProvider.telemetry, {
317
+ ulid: sessionId,
318
+ modelId: configWithProvider.modelId,
319
+ provider: configWithProvider.providerId,
320
+ ...rootAgentIdentity,
321
+ });
322
+ }
323
+ if (runtime.teamRuntime) {
324
+ captureAgentTeamCreated(configWithProvider.telemetry, {
325
+ ulid: sessionId,
326
+ teamId: runtime.teamRuntime.getTeamId(),
327
+ teamName: runtime.teamRuntime.getTeamName(),
328
+ leadAgentId: this.readAgentId(agent),
329
+ restoredFromPersistence: runtime.teamRestoredFromPersistence === true,
330
+ });
331
+ }
332
+
333
+ const active: ActiveSession = {
334
+ sessionId,
335
+ config: configWithProvider,
336
+ source,
337
+ startedAt,
338
+ pendingPrompt: manifest.prompt,
339
+ runtime,
340
+ agent,
341
+ started: false,
342
+ aborting: false,
343
+ interactive: input.interactive === true,
344
+ persistedMessages: input.initialMessages,
345
+ activeTeamRunIds: new Set<string>(),
346
+ pendingTeamRunUpdates: [],
347
+ teamRunWaiters: [],
348
+ pendingPrompts: [],
349
+ drainingPendingPrompts: false,
350
+ pluginSandboxShutdown,
351
+ };
352
+ this.sessions.set(sessionId, active);
353
+ this.emitStatus(sessionId, "running");
354
+
355
+ let result: AgentResult | undefined;
356
+ try {
357
+ if (input.prompt?.trim()) {
358
+ result = await this.runTurn(active, {
359
+ prompt: input.prompt,
360
+ userImages: input.userImages,
361
+ userFiles: input.userFiles,
362
+ });
363
+ if (!active.interactive) {
364
+ await this.finalizeSingleRun(active, result.finishReason);
365
+ }
366
+ }
367
+ } catch (error) {
368
+ await this.failSession(active);
369
+ throw error;
370
+ }
371
+
372
+ return {
373
+ sessionId,
374
+ manifest,
375
+ manifestPath,
376
+ transcriptPath,
377
+ hookPath,
378
+ messagesPath,
379
+ result,
380
+ };
381
+ }
382
+
383
+ async send(input: SendSessionInput): Promise<AgentResult | undefined> {
384
+ const session = this.getSessionOrThrow(input.sessionId);
385
+ session.config.telemetry?.capture({
386
+ event: "session.input_sent",
387
+ properties: {
388
+ sessionId: input.sessionId,
389
+ promptLength: input.prompt.length,
390
+ userImageCount: input.userImages?.length ?? 0,
391
+ userFileCount: input.userFiles?.length ?? 0,
392
+ delivery: input.delivery ?? "immediate",
393
+ },
394
+ });
395
+ if (input.delivery === "queue" || input.delivery === "steer") {
396
+ this.enqueuePendingPrompt(input.sessionId, {
397
+ prompt: input.prompt,
398
+ delivery: input.delivery,
399
+ userImages: input.userImages,
400
+ userFiles: input.userFiles,
401
+ });
402
+ return undefined;
403
+ }
404
+ try {
405
+ const result = await this.runTurn(session, {
406
+ prompt: input.prompt,
407
+ userImages: input.userImages,
408
+ userFiles: input.userFiles,
409
+ });
410
+ if (!session.interactive) {
411
+ await this.finalizeSingleRun(session, result.finishReason);
412
+ }
413
+ queueMicrotask(() => {
414
+ void this.drainPendingPrompts(input.sessionId);
415
+ });
416
+ return result;
417
+ } catch (error) {
418
+ await this.failSession(session);
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ async getAccumulatedUsage(
424
+ sessionId: string,
425
+ ): Promise<SessionAccumulatedUsage | undefined> {
426
+ const usage = this.usageBySession.get(sessionId);
427
+ return usage ? { ...usage } : undefined;
428
+ }
429
+
430
+ async abort(sessionId: string, reason?: unknown): Promise<void> {
431
+ const session = this.sessions.get(sessionId);
432
+ if (!session) return;
433
+ session.config.telemetry?.capture({
434
+ event: "session.aborted",
435
+ properties: { sessionId },
436
+ });
437
+ session.aborting = true;
438
+ (
439
+ session.agent as Agent & {
440
+ abort: (abortReason?: unknown) => void;
441
+ }
442
+ ).abort(reason);
443
+ }
444
+
445
+ async stop(sessionId: string): Promise<void> {
446
+ const session = this.sessions.get(sessionId);
447
+ if (!session) return;
448
+ session.config.telemetry?.capture({
449
+ event: "session.stopped",
450
+ properties: { sessionId },
451
+ });
452
+ // Abort the agent first if it's running, so shutdown can proceed
453
+ session.aborting = true;
454
+ (
455
+ session.agent as Agent & {
456
+ abort: (abortReason?: unknown) => void;
457
+ }
458
+ ).abort(new Error("session_stop"));
459
+ await this.shutdownSession(session, {
460
+ status: "cancelled",
461
+ exitCode: 0,
462
+ shutdownReason: "session_stop",
463
+ endReason: "stopped",
464
+ });
465
+ }
466
+
467
+ async dispose(reason = "session_manager_dispose"): Promise<void> {
468
+ const sessions = [...this.sessions.values()];
469
+ if (sessions.length === 0) return;
470
+ await Promise.allSettled(
471
+ sessions.map((session) =>
472
+ this.shutdownSession(session, {
473
+ status: "cancelled",
474
+ exitCode: 0,
475
+ shutdownReason: reason,
476
+ endReason: "disposed",
477
+ }),
478
+ ),
479
+ );
480
+ this.usageBySession.clear();
481
+ }
482
+
483
+ async get(sessionId: string): Promise<SessionRecord | undefined> {
484
+ const row = await this.getRow(sessionId);
485
+ return row ? toSessionRecord(row) : undefined;
486
+ }
487
+
488
+ async list(limit = 200): Promise<SessionRecord[]> {
489
+ const rows = await this.listRows(limit);
490
+ return rows.map(toSessionRecord);
491
+ }
492
+
493
+ async delete(sessionId: string): Promise<boolean> {
494
+ if (this.sessions.has(sessionId)) {
495
+ await this.stop(sessionId);
496
+ }
497
+ const result = await this.invoke<{ deleted: boolean }>(
498
+ "deleteSession",
499
+ sessionId,
500
+ );
501
+ if (result.deleted) {
502
+ this.usageBySession.delete(sessionId);
503
+ }
504
+ return result.deleted;
505
+ }
506
+
507
+ async readTranscript(sessionId: string, maxChars?: number): Promise<string> {
508
+ const row = await this.getRow(sessionId);
509
+ if (!row?.transcriptPath || !existsSync(row.transcriptPath)) return "";
510
+ const raw = readFileSync(row.transcriptPath, "utf8");
511
+ if (typeof maxChars === "number" && Number.isFinite(maxChars)) {
512
+ return raw.slice(-Math.max(0, Math.floor(maxChars)));
513
+ }
514
+ return raw;
515
+ }
516
+
517
+ async readMessages(sessionId: string): Promise<LlmsProviders.Message[]> {
518
+ const row = await this.getRow(sessionId);
519
+ const messagesPath = row?.messagesPath?.trim();
520
+ if (!messagesPath || !existsSync(messagesPath)) return [];
521
+ try {
522
+ const raw = readFileSync(messagesPath, "utf8").trim();
523
+ if (!raw) return [];
524
+ const parsed = JSON.parse(raw) as unknown;
525
+ if (Array.isArray(parsed)) return parsed as LlmsProviders.Message[];
526
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
527
+ const messages = (parsed as { messages?: unknown }).messages;
528
+ if (Array.isArray(messages)) return messages as LlmsProviders.Message[];
529
+ }
530
+ return [];
531
+ } catch {
532
+ return [];
533
+ }
534
+ }
535
+
536
+ async readHooks(sessionId: string, limit = 200): Promise<unknown[]> {
537
+ const row = await this.getRow(sessionId);
538
+ if (!row?.hookPath || !existsSync(row.hookPath)) return [];
539
+ const lines = readFileSync(row.hookPath, "utf8")
540
+ .split("\n")
541
+ .filter((line) => line.trim().length > 0);
542
+ return lines.slice(-Math.max(1, Math.floor(limit))).map((line) => {
543
+ try {
544
+ return JSON.parse(line) as unknown;
545
+ } catch {
546
+ return { raw: line };
547
+ }
548
+ });
549
+ }
550
+
551
+ subscribe(listener: (event: CoreSessionEvent) => void): () => void {
552
+ this.listeners.add(listener);
553
+ return () => {
554
+ this.listeners.delete(listener);
555
+ };
556
+ }
557
+
558
+ async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
559
+ const session = this.getSessionOrThrow(sessionId);
560
+ session.config.modelId = modelId;
561
+ session.runtime.delegatedAgentConfigProvider?.updateConnectionDefaults({
562
+ modelId,
563
+ });
564
+ this.updateAgentConnection(session, { modelId });
565
+ }
566
+
567
+ // ── Turn execution ──────────────────────────────────────────────────
568
+
569
+ private async runTurn(
570
+ session: ActiveSession,
571
+ input: {
572
+ prompt: string;
573
+ userImages?: string[];
574
+ userFiles?: string[];
575
+ },
576
+ ): Promise<AgentResult> {
577
+ const preparedInput = await this.prepareTurnInput(session, input);
578
+ const prompt = preparedInput.prompt.trim();
579
+ if (!prompt) throw new Error("prompt cannot be empty");
580
+
581
+ if (!session.artifacts && !session.pendingPrompt) {
582
+ session.pendingPrompt = prompt;
583
+ }
584
+ await this.ensureSessionPersisted(session);
585
+ await this.syncOAuthCredentials(session);
586
+
587
+ let result = await this.executeAgentTurn(
588
+ session,
589
+ prompt,
590
+ preparedInput.userImages,
591
+ preparedInput.userFiles,
592
+ );
593
+
594
+ while (shouldAutoContinueTeamRuns(session, result.finishReason)) {
595
+ const updates = await waitForTeamRunUpdates(session);
596
+ if (updates.length === 0) break;
597
+ const continuationPrompt = buildTeamRunContinuationPrompt(
598
+ session,
599
+ updates,
600
+ );
601
+ result = await this.executeAgentTurn(session, continuationPrompt);
602
+ }
603
+
604
+ return result;
605
+ }
606
+
607
+ private async executeAgentTurn(
608
+ session: ActiveSession,
609
+ prompt: string,
610
+ userImages?: string[],
611
+ userFiles?: string[],
612
+ ): Promise<AgentResult> {
613
+ const shouldContinue =
614
+ session.started || session.agent.getMessages().length > 0;
615
+ const baselineMessages =
616
+ session.persistedMessages ?? session.agent.getMessages();
617
+ const usageBaseline =
618
+ this.usageBySession.get(session.sessionId) ??
619
+ createInitialAccumulatedUsage();
620
+ session.turnUsageBaseline = usageBaseline;
621
+
622
+ captureModeSwitch(
623
+ session.config.telemetry,
624
+ session.sessionId,
625
+ session.config.mode,
626
+ );
627
+ captureConversationTurnEvent(session.config.telemetry, {
628
+ ulid: session.sessionId,
629
+ provider: session.config.providerId,
630
+ model: session.config.modelId,
631
+ source: "user",
632
+ mode: session.config.mode,
633
+ ...this.getSessionAgentTelemetryIdentity(session),
634
+ });
635
+
636
+ try {
637
+ const runFn = shouldContinue
638
+ ? () => session.agent.continue(prompt, userImages, userFiles)
639
+ : () => session.agent.run(prompt, userImages, userFiles);
640
+ const result = await this.runWithAuthRetry(
641
+ session,
642
+ runFn,
643
+ baselineMessages,
644
+ );
645
+
646
+ session.started = true;
647
+ const persistedMessages = withLatestAssistantTurnMetadata(
648
+ result.messages,
649
+ result,
650
+ baselineMessages,
651
+ );
652
+ session.persistedMessages = persistedMessages;
653
+ this.usageBySession.set(
654
+ session.sessionId,
655
+ accumulateUsageTotals(usageBaseline, result.usage),
656
+ );
657
+ await this.invoke<void>(
658
+ "persistSessionMessages",
659
+ session.sessionId,
660
+ persistedMessages,
661
+ session.config.systemPrompt,
662
+ );
663
+ return result;
664
+ } catch (error) {
665
+ await this.invoke<void>(
666
+ "persistSessionMessages",
667
+ session.sessionId,
668
+ session.agent.getMessages(),
669
+ session.config.systemPrompt,
670
+ );
671
+ throw error;
672
+ } finally {
673
+ session.turnUsageBaseline = undefined;
674
+ }
675
+ }
676
+
677
+ private async prepareTurnInput(
678
+ session: ActiveSession,
679
+ input: {
680
+ prompt: string;
681
+ userImages?: string[];
682
+ userFiles?: string[];
683
+ },
684
+ ): Promise<PreparedTurnInput> {
685
+ const mentionBaseDir = resolveWorkspacePath(session.config);
686
+ const normalizedPrompt = normalizeUserInput(input.prompt).trim();
687
+ if (!normalizedPrompt) {
688
+ return {
689
+ prompt: "",
690
+ userImages: input.userImages,
691
+ userFiles: this.resolveAbsoluteFilePaths(
692
+ session.config.cwd,
693
+ input.userFiles,
694
+ ),
695
+ };
696
+ }
697
+
698
+ const enriched = await enrichPromptWithMentions(
699
+ normalizedPrompt,
700
+ mentionBaseDir,
701
+ );
702
+ emitMentionTelemetry(session.config.telemetry, enriched);
703
+
704
+ const prompt = formatModePrompt(enriched.prompt, session.config.mode);
705
+ const explicitUserFiles = this.resolveAbsoluteFilePaths(
706
+ session.config.cwd,
707
+ input.userFiles,
708
+ );
709
+ const mentionedFiles = this.resolveAbsoluteFilePaths(
710
+ mentionBaseDir,
711
+ enriched.matchedFiles,
712
+ );
713
+ const mergedUserFiles = Array.from(
714
+ new Set([...explicitUserFiles, ...mentionedFiles]),
715
+ );
716
+
717
+ return {
718
+ prompt,
719
+ userImages: input.userImages,
720
+ userFiles: mergedUserFiles.length > 0 ? mergedUserFiles : undefined,
721
+ };
722
+ }
723
+
724
+ // ── Session lifecycle ───────────────────────────────────────────────
725
+
726
+ private async ensureSessionPersisted(session: ActiveSession): Promise<void> {
727
+ if (session.artifacts) return;
728
+ const workspacePath = resolveWorkspacePath(session.config);
729
+ session.artifacts = (await this.invoke("createRootSessionWithArtifacts", {
730
+ sessionId: session.sessionId,
731
+ source: session.source,
732
+ pid: process.pid,
733
+ interactive: session.interactive,
734
+ provider: session.config.providerId,
735
+ model: session.config.modelId,
736
+ cwd: session.config.cwd,
737
+ workspaceRoot: workspacePath,
738
+ teamName: session.config.teamName,
739
+ enableTools: session.config.enableTools,
740
+ enableSpawn: session.config.enableSpawnAgent,
741
+ enableTeams: session.config.enableAgentTeams,
742
+ prompt: session.pendingPrompt,
743
+ startedAt: session.startedAt,
744
+ })) as RootSessionArtifacts;
745
+ }
746
+
747
+ private async finalizeSingleRun(
748
+ session: ActiveSession,
749
+ finishReason: AgentResult["finishReason"],
750
+ ): Promise<void> {
751
+ if (hasPendingTeamRunWork(session)) return;
752
+ const isAborted = finishReason === "aborted" || session.aborting;
753
+ await this.shutdownSession(session, {
754
+ status: isAborted ? "cancelled" : "completed",
755
+ exitCode: 0,
756
+ shutdownReason: "session_complete",
757
+ endReason: finishReason,
758
+ });
759
+ }
760
+
761
+ private async failSession(session: ActiveSession): Promise<void> {
762
+ await this.shutdownSession(session, {
763
+ status: "failed",
764
+ exitCode: 1,
765
+ shutdownReason: "session_error",
766
+ endReason: "error",
767
+ });
768
+ }
769
+
770
+ private async shutdownSession(
771
+ session: ActiveSession,
772
+ input: {
773
+ status: SessionStatus;
774
+ exitCode: number | null;
775
+ shutdownReason: string;
776
+ endReason: string;
777
+ },
778
+ ): Promise<void> {
779
+ if (input.status === "completed") {
780
+ captureTaskCompleted(session.config.telemetry, {
781
+ ulid: session.sessionId,
782
+ provider: session.config.providerId,
783
+ modelId: session.config.modelId,
784
+ mode: session.config.mode,
785
+ durationMs: Date.now() - Date.parse(session.startedAt),
786
+ ...this.getSessionAgentTelemetryIdentity(session),
787
+ });
788
+ }
789
+ notifyTeamRunWaiters(session);
790
+
791
+ if (session.artifacts) {
792
+ await this.updateStatus(session, input.status, input.exitCode);
793
+ await session.agent.shutdown(input.shutdownReason);
794
+ }
795
+ await Promise.resolve(session.runtime.shutdown(input.shutdownReason));
796
+ await session.pluginSandboxShutdown?.();
797
+ this.sessions.delete(session.sessionId);
798
+ this.emit({
799
+ type: "ended",
800
+ payload: {
801
+ sessionId: session.sessionId,
802
+ reason: input.endReason,
803
+ ts: Date.now(),
804
+ },
805
+ });
806
+ }
807
+
808
+ private async updateStatus(
809
+ session: ActiveSession,
810
+ status: SessionStatus,
811
+ exitCode?: number | null,
812
+ ): Promise<void> {
813
+ if (!session.artifacts) return;
814
+ const result = await this.invoke<{ updated: boolean; endedAt?: string }>(
815
+ "updateSessionStatus",
816
+ session.sessionId,
817
+ status,
818
+ exitCode,
819
+ );
820
+ if (!result.updated) return;
821
+ session.artifacts.manifest.status = status;
822
+ session.artifacts.manifest.ended_at = result.endedAt ?? nowIso();
823
+ session.artifacts.manifest.exit_code =
824
+ typeof exitCode === "number" ? exitCode : null;
825
+ await this.invoke<void>(
826
+ "writeSessionManifest",
827
+ session.artifacts.manifestPath,
828
+ session.artifacts.manifest,
829
+ );
830
+ this.emitStatus(session.sessionId, status);
831
+ }
832
+
833
+ private async handlePluginEvent(
834
+ rootSessionId: string,
835
+ event: { name: string; payload?: unknown },
836
+ ): Promise<void> {
837
+ if (
838
+ event.name !== "steer_message" &&
839
+ event.name !== "queue_message" &&
840
+ event.name !== "pending_prompt"
841
+ ) {
842
+ return;
843
+ }
844
+ const payload =
845
+ event.payload && typeof event.payload === "object"
846
+ ? (event.payload as Record<string, unknown>)
847
+ : undefined;
848
+ const targetSessionId =
849
+ typeof payload?.sessionId === "string" &&
850
+ payload.sessionId.trim().length > 0
851
+ ? payload.sessionId.trim()
852
+ : rootSessionId;
853
+ const prompt =
854
+ typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
855
+ if (!prompt) {
856
+ return;
857
+ }
858
+ const delivery =
859
+ event.name === "steer_message"
860
+ ? "steer"
861
+ : event.name === "queue_message"
862
+ ? "queue"
863
+ : payload?.delivery === "steer"
864
+ ? "steer"
865
+ : "queue";
866
+ this.enqueuePendingPrompt(targetSessionId, {
867
+ prompt,
868
+ delivery,
869
+ });
870
+ }
871
+
872
+ private enqueuePendingPrompt(
873
+ sessionId: string,
874
+ entry: {
875
+ prompt: string;
876
+ delivery: "queue" | "steer";
877
+ userImages?: string[];
878
+ userFiles?: string[];
879
+ },
880
+ ): void {
881
+ const session = this.sessions.get(sessionId);
882
+ if (!session) {
883
+ return;
884
+ }
885
+ const { prompt, delivery, userImages, userFiles } = entry;
886
+ const existingIndex = session.pendingPrompts.findIndex(
887
+ (queued) => queued.prompt === prompt,
888
+ );
889
+ if (existingIndex >= 0) {
890
+ const [existing] = session.pendingPrompts.splice(existingIndex, 1);
891
+ if (delivery === "steer" || existing.delivery === "steer") {
892
+ session.pendingPrompts.unshift({
893
+ id: existing.id,
894
+ prompt,
895
+ delivery: "steer",
896
+ userImages: userImages ?? existing.userImages,
897
+ userFiles: userFiles ?? existing.userFiles,
898
+ });
899
+ } else {
900
+ session.pendingPrompts.push({
901
+ ...existing,
902
+ userImages: userImages ?? existing.userImages,
903
+ userFiles: userFiles ?? existing.userFiles,
904
+ });
905
+ }
906
+ } else if (delivery === "steer") {
907
+ session.pendingPrompts.unshift({
908
+ id: `pending_${Date.now()}_${nanoid(5)}`,
909
+ prompt,
910
+ delivery,
911
+ userImages,
912
+ userFiles,
913
+ });
914
+ } else {
915
+ session.pendingPrompts.push({
916
+ id: `pending_${Date.now()}_${nanoid(5)}`,
917
+ prompt,
918
+ delivery,
919
+ userImages,
920
+ userFiles,
921
+ });
922
+ }
923
+ this.emitPendingPrompts(session);
924
+ queueMicrotask(() => {
925
+ void this.drainPendingPrompts(sessionId);
926
+ });
927
+ }
928
+
929
+ private async drainPendingPrompts(sessionId: string): Promise<void> {
930
+ const session = this.sessions.get(sessionId);
931
+ if (!session || session.drainingPendingPrompts) {
932
+ return;
933
+ }
934
+ const canStartRun =
935
+ typeof (session.agent as Agent & { canStartRun?: () => boolean })
936
+ .canStartRun === "function"
937
+ ? (
938
+ session.agent as Agent & {
939
+ canStartRun: () => boolean;
940
+ }
941
+ ).canStartRun()
942
+ : true;
943
+ if (!canStartRun) {
944
+ return;
945
+ }
946
+ const next = session.pendingPrompts.shift();
947
+ if (!next) {
948
+ return;
949
+ }
950
+ this.emitPendingPrompts(session);
951
+ this.emitPendingPromptSubmitted(session, next);
952
+ session.drainingPendingPrompts = true;
953
+ try {
954
+ await this.send({
955
+ sessionId,
956
+ prompt: next.prompt,
957
+ userImages: next.userImages,
958
+ userFiles: next.userFiles,
959
+ });
960
+ } catch (error) {
961
+ const message = error instanceof Error ? error.message : String(error);
962
+ if (message.includes("already in progress")) {
963
+ session.pendingPrompts.unshift(next);
964
+ this.emitPendingPrompts(session);
965
+ } else {
966
+ throw error;
967
+ }
968
+ } finally {
969
+ session.drainingPendingPrompts = false;
970
+ if (session.pendingPrompts.length > 0) {
971
+ queueMicrotask(() => {
972
+ void this.drainPendingPrompts(sessionId);
973
+ });
974
+ }
975
+ }
976
+ }
977
+
978
+ // ── Agent event handling ────────────────────────────────────────────
979
+
980
+ private onAgentEvent(
981
+ sessionId: string,
982
+ config: CoreSessionConfig,
983
+ event: AgentEvent,
984
+ ): void {
985
+ const liveSession = this.sessions.get(sessionId);
986
+ const ctx: AgentEventContext = {
987
+ sessionId,
988
+ config,
989
+ liveSession,
990
+ usageBySession: this.usageBySession,
991
+ persistMessages: (sid, messages, systemPrompt) => {
992
+ void this.invoke<void>(
993
+ "persistSessionMessages",
994
+ sid,
995
+ messages,
996
+ systemPrompt,
997
+ );
998
+ },
999
+ emit: (e) => this.emit(e),
1000
+ };
1001
+ const eventMetadata = extractAgentEventMetadata(event);
1002
+ const isRootAgentEvent =
1003
+ liveSession &&
1004
+ eventMetadata.agentId === this.readAgentId(liveSession.agent);
1005
+ handleAgentEvent(
1006
+ ctx,
1007
+ event,
1008
+ isRootAgentEvent
1009
+ ? {
1010
+ isPrimaryAgentEvent: true,
1011
+ ...(liveSession?.runtime.teamRuntime
1012
+ ? { teamRole: "lead" as const }
1013
+ : {}),
1014
+ }
1015
+ : { isPrimaryAgentEvent: false },
1016
+ );
1017
+ }
1018
+
1019
+ private emitPendingPrompts(session: ActiveSession): void {
1020
+ this.emit({
1021
+ type: "pending_prompts",
1022
+ payload: {
1023
+ sessionId: session.sessionId,
1024
+ prompts: session.pendingPrompts.map((entry) => ({
1025
+ id: entry.id,
1026
+ prompt: entry.prompt,
1027
+ delivery: entry.delivery,
1028
+ attachmentCount:
1029
+ (entry.userImages?.length ?? 0) + (entry.userFiles?.length ?? 0),
1030
+ })),
1031
+ },
1032
+ });
1033
+ }
1034
+
1035
+ private emitPendingPromptSubmitted(
1036
+ session: ActiveSession,
1037
+ entry: {
1038
+ id: string;
1039
+ prompt: string;
1040
+ delivery: "queue" | "steer";
1041
+ userImages?: string[];
1042
+ userFiles?: string[];
1043
+ },
1044
+ ): void {
1045
+ this.emit({
1046
+ type: "pending_prompt_submitted",
1047
+ payload: {
1048
+ sessionId: session.sessionId,
1049
+ id: entry.id,
1050
+ prompt: entry.prompt,
1051
+ delivery: entry.delivery,
1052
+ attachmentCount:
1053
+ (entry.userImages?.length ?? 0) + (entry.userFiles?.length ?? 0),
1054
+ },
1055
+ });
1056
+ }
1057
+
1058
+ // ── Spawn / sub-agents ──────────────────────────────────────────────
1059
+
1060
+ private createSpawnTool(
1061
+ config: CoreSessionConfig,
1062
+ rootSessionId: string,
1063
+ ): Tool {
1064
+ const createSubAgentTools = () => {
1065
+ const tools: Tool[] = config.enableTools
1066
+ ? createBuiltinTools({
1067
+ cwd: config.cwd,
1068
+ ...(config.mode === "plan"
1069
+ ? ToolPresets.readonly
1070
+ : ToolPresets.development),
1071
+ executors: this.defaultToolExecutors,
1072
+ })
1073
+ : [];
1074
+ if (config.enableSpawnAgent) {
1075
+ tools.push(this.createSpawnTool(config, rootSessionId));
1076
+ }
1077
+ return tools;
1078
+ };
1079
+
1080
+ return createSpawnAgentTool({
1081
+ configProvider: {
1082
+ getRuntimeConfig: () =>
1083
+ this.sessions
1084
+ .get(rootSessionId)
1085
+ ?.runtime.delegatedAgentConfigProvider?.getRuntimeConfig() ?? {
1086
+ providerId: config.providerId,
1087
+ modelId: config.modelId,
1088
+ cwd: config.cwd,
1089
+ apiKey: config.apiKey,
1090
+ baseUrl: config.baseUrl,
1091
+ headers: config.headers,
1092
+ providerConfig: config.providerConfig,
1093
+ knownModels: config.knownModels,
1094
+ thinking: config.thinking,
1095
+ clineWorkspaceMetadata:
1096
+ config.providerId === "cline"
1097
+ ? extractWorkspaceMetadataFromSystemPrompt(config.systemPrompt)
1098
+ : undefined,
1099
+ maxIterations: config.maxIterations,
1100
+ hooks: config.hooks,
1101
+ extensions: config.extensions,
1102
+ logger: config.logger,
1103
+ telemetry: config.telemetry,
1104
+ },
1105
+ getConnectionConfig: () =>
1106
+ this.sessions
1107
+ .get(rootSessionId)
1108
+ ?.runtime.delegatedAgentConfigProvider?.getConnectionConfig() ?? {
1109
+ providerId: config.providerId,
1110
+ modelId: config.modelId,
1111
+ apiKey: config.apiKey,
1112
+ baseUrl: config.baseUrl,
1113
+ headers: config.headers,
1114
+ providerConfig: config.providerConfig,
1115
+ knownModels: config.knownModels,
1116
+ thinking: config.thinking,
1117
+ },
1118
+ updateConnectionDefaults: () => {},
1119
+ },
1120
+ createSubAgentTools,
1121
+ onSubAgentEvent: (event) =>
1122
+ this.onAgentEvent(rootSessionId, config, event),
1123
+ onSubAgentStart: (context) => {
1124
+ const teamRuntime =
1125
+ this.sessions.get(rootSessionId)?.runtime.teamRuntime;
1126
+ this.subAgentStarts.set(context.subAgentId, {
1127
+ startedAt: Date.now(),
1128
+ rootSessionId,
1129
+ });
1130
+ const agentIdentity = buildTelemetryAgentIdentity({
1131
+ agentId: context.subAgentId,
1132
+ conversationId: context.conversationId,
1133
+ parentAgentId: context.parentAgentId,
1134
+ teamId: teamRuntime?.getTeamId(),
1135
+ teamName: teamRuntime?.getTeamName(),
1136
+ createdByAgentId: context.parentAgentId,
1137
+ });
1138
+ if (agentIdentity) {
1139
+ captureAgentCreated(config.telemetry, {
1140
+ ulid: rootSessionId,
1141
+ modelId: config.modelId,
1142
+ provider: config.providerId,
1143
+ ...agentIdentity,
1144
+ });
1145
+ }
1146
+ captureSubagentExecution(config.telemetry, {
1147
+ event: "started",
1148
+ ulid: rootSessionId,
1149
+ durationMs: 0,
1150
+ parentId: context.parentAgentId,
1151
+ agentId: context.subAgentId,
1152
+ ...agentIdentity,
1153
+ });
1154
+ void this.invokeOptional("handleSubAgentStart", rootSessionId, context);
1155
+ },
1156
+ onSubAgentEnd: (context) => {
1157
+ const teamRuntime =
1158
+ this.sessions.get(rootSessionId)?.runtime.teamRuntime;
1159
+ const started = this.subAgentStarts.get(context.subAgentId);
1160
+ const durationMs = started ? Date.now() - started.startedAt : 0;
1161
+ const outputLines = context.result?.text
1162
+ ? context.result.text.split("\n").length
1163
+ : 0;
1164
+ captureSubagentExecution(config.telemetry, {
1165
+ event: "ended",
1166
+ ulid: rootSessionId,
1167
+ durationMs,
1168
+ outputLines,
1169
+ errorMessage: context.error ? String(context.error) : undefined,
1170
+ agentId: context.subAgentId,
1171
+ parentId: context.parentAgentId,
1172
+ ...buildTelemetryAgentIdentity({
1173
+ agentId: context.subAgentId,
1174
+ conversationId: context.conversationId,
1175
+ parentAgentId: context.parentAgentId,
1176
+ teamId: teamRuntime?.getTeamId(),
1177
+ teamName: teamRuntime?.getTeamName(),
1178
+ createdByAgentId: context.parentAgentId,
1179
+ }),
1180
+ });
1181
+ this.subAgentStarts.delete(context.subAgentId);
1182
+ void this.invokeOptional("handleSubAgentEnd", rootSessionId, context);
1183
+ },
1184
+ }) as Tool;
1185
+ }
1186
+
1187
+ // ── Team run coordination ───────────────────────────────────────────
1188
+
1189
+ private async handleTeamEvent(
1190
+ rootSessionId: string,
1191
+ event: TeamEvent,
1192
+ ): Promise<void> {
1193
+ const session = this.sessions.get(rootSessionId);
1194
+ if (session) {
1195
+ trackTeamRunState(session, event);
1196
+ if (event.type === "agent_event") {
1197
+ const ctx: AgentEventContext = {
1198
+ sessionId: rootSessionId,
1199
+ config: session.config,
1200
+ liveSession: session,
1201
+ usageBySession: this.usageBySession,
1202
+ persistMessages: (sid, messages, systemPrompt) => {
1203
+ void this.invoke<void>(
1204
+ "persistSessionMessages",
1205
+ sid,
1206
+ messages,
1207
+ systemPrompt,
1208
+ );
1209
+ },
1210
+ emit: (e) => this.emit(e),
1211
+ };
1212
+ handleAgentEvent(ctx, event.event, {
1213
+ teamRole: "teammate",
1214
+ teamAgentId: event.agentId,
1215
+ isPrimaryAgentEvent: false,
1216
+ });
1217
+ }
1218
+ if (event.type === "teammate_spawned") {
1219
+ const agentIdentity = buildTelemetryAgentIdentity({
1220
+ agentId: event.teammate.runtimeAgentId ?? event.agentId,
1221
+ conversationId: event.teammate.conversationId,
1222
+ parentAgentId: event.teammate.parentAgentId,
1223
+ createdByAgentId: this.readAgentId(session.agent),
1224
+ teamId: session.runtime.teamRuntime?.getTeamId(),
1225
+ teamName: session.runtime.teamRuntime?.getTeamName(),
1226
+ teamRole: "teammate",
1227
+ teamAgentId: event.agentId,
1228
+ });
1229
+ if (agentIdentity) {
1230
+ captureAgentCreated(session.config.telemetry, {
1231
+ ulid: rootSessionId,
1232
+ modelId: event.teammate.modelId ?? session.config.modelId,
1233
+ provider: session.config.providerId,
1234
+ ...agentIdentity,
1235
+ });
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ await dispatchTeamEventToBackend(
1241
+ rootSessionId,
1242
+ event,
1243
+ this.invokeOptional.bind(this),
1244
+ );
1245
+
1246
+ if (session) {
1247
+ emitTeamProgress(session, rootSessionId, event, (e) => this.emit(e));
1248
+ }
1249
+ }
1250
+
1251
+ // ── OAuth & auth ────────────────────────────────────────────────────
1252
+
1253
+ private async runWithAuthRetry(
1254
+ session: ActiveSession,
1255
+ run: () => Promise<AgentResult>,
1256
+ baselineMessages: LlmsProviders.Message[],
1257
+ ): Promise<AgentResult> {
1258
+ try {
1259
+ return await run();
1260
+ } catch (error) {
1261
+ if (!isLikelyAuthError(error, session.config.providerId)) {
1262
+ throw error;
1263
+ }
1264
+ await this.syncOAuthCredentials(session, { forceRefresh: true });
1265
+ session.agent.restore(baselineMessages);
1266
+ return run();
1267
+ }
1268
+ }
1269
+
1270
+ private async syncOAuthCredentials(
1271
+ session: ActiveSession,
1272
+ options?: { forceRefresh?: boolean },
1273
+ ): Promise<void> {
1274
+ let resolved: RuntimeOAuthResolution | null = null;
1275
+ try {
1276
+ resolved = await this.oauthTokenManager.resolveProviderApiKey({
1277
+ providerId: session.config.providerId,
1278
+ forceRefresh: options?.forceRefresh,
1279
+ });
1280
+ } catch (error) {
1281
+ if (error instanceof OAuthReauthRequiredError) {
1282
+ throw new Error(
1283
+ `OAuth session for "${error.providerId}" requires re-authentication. Run "clite auth ${error.providerId}" and retry.`,
1284
+ );
1285
+ }
1286
+ throw error;
1287
+ }
1288
+ if (!resolved?.apiKey || session.config.apiKey === resolved.apiKey) return;
1289
+ session.config.apiKey = resolved.apiKey;
1290
+ this.updateAgentConnection(session, { apiKey: resolved.apiKey });
1291
+ session.runtime.delegatedAgentConfigProvider?.updateConnectionDefaults({
1292
+ apiKey: resolved.apiKey,
1293
+ });
1294
+ session.runtime.teamRuntime?.updateTeammateConnections({
1295
+ apiKey: resolved.apiKey,
1296
+ });
1297
+ }
1298
+
1299
+ // ── Utility methods ─────────────────────────────────────────────────
1300
+
1301
+ private getSessionOrThrow(sessionId: string): ActiveSession {
1302
+ const session = this.sessions.get(sessionId);
1303
+ if (!session) throw new Error(`session not found: ${sessionId}`);
1304
+ return session;
1305
+ }
1306
+
1307
+ private resolveAbsoluteFilePaths(cwd: string, paths?: string[]): string[] {
1308
+ if (!paths || paths.length === 0) return [];
1309
+ const resolved = paths
1310
+ .map((p) => p.trim())
1311
+ .filter((p) => p.length > 0)
1312
+ .map((p) => (isAbsolute(p) ? p : resolve(cwd, p)));
1313
+ return Array.from(new Set(resolved));
1314
+ }
1315
+
1316
+ private updateAgentConnection(
1317
+ session: ActiveSession,
1318
+ overrides: { apiKey?: string; modelId?: string },
1319
+ ): void {
1320
+ const agentWithConnection = session.agent as Agent & {
1321
+ updateConnection?: (overrides: {
1322
+ apiKey?: string;
1323
+ modelId?: string;
1324
+ }) => void;
1325
+ };
1326
+ agentWithConnection.updateConnection?.(overrides);
1327
+ }
1328
+
1329
+ private getSessionAgentTelemetryIdentity(session: ActiveSession) {
1330
+ return buildTelemetryAgentIdentity({
1331
+ agentId: this.readAgentId(session.agent),
1332
+ conversationId: this.readAgentConversationId(session.agent),
1333
+ teamId: session.runtime.teamRuntime?.getTeamId(),
1334
+ teamName: session.runtime.teamRuntime?.getTeamName(),
1335
+ teamRole: session.runtime.teamRuntime ? "lead" : undefined,
1336
+ });
1337
+ }
1338
+
1339
+ private readAgentId(agent: Agent): string | undefined {
1340
+ return (agent as Agent & { getAgentId?: () => string }).getAgentId?.();
1341
+ }
1342
+
1343
+ private readAgentConversationId(agent: Agent): string | undefined {
1344
+ return (
1345
+ agent as Agent & { getConversationId?: () => string }
1346
+ ).getConversationId?.();
1347
+ }
1348
+
1349
+ private emitStatus(sessionId: string, status: string): void {
1350
+ this.emit({
1351
+ type: "status",
1352
+ payload: { sessionId, status },
1353
+ });
1354
+ }
1355
+
1356
+ private emit(event: CoreSessionEvent): void {
1357
+ for (const listener of this.listeners) listener(event);
1358
+ }
1359
+
1360
+ private async listRows(limit: number): Promise<SessionRow[]> {
1361
+ return this.invoke<SessionRow[]>(
1362
+ "listSessions",
1363
+ Math.min(Math.max(1, Math.floor(limit)), MAX_SCAN_LIMIT),
1364
+ );
1365
+ }
1366
+
1367
+ private async getRow(sessionId: string): Promise<SessionRow | undefined> {
1368
+ const target = sessionId.trim();
1369
+ if (!target) return undefined;
1370
+ const rows = await this.listRows(MAX_SCAN_LIMIT);
1371
+ return rows.find((row) => row.sessionId === target);
1372
+ }
1373
+
1374
+ // ── Session service invocation ──────────────────────────────────────
1375
+
1376
+ private async invoke<T>(method: string, ...args: unknown[]): Promise<T> {
1377
+ const callable = (
1378
+ this.sessionService as unknown as Record<string, unknown>
1379
+ )[method];
1380
+ if (typeof callable !== "function") {
1381
+ throw new Error(`session service method not available: ${method}`);
1382
+ }
1383
+ return Promise.resolve(
1384
+ (callable as (...params: unknown[]) => T | Promise<T>).apply(
1385
+ this.sessionService,
1386
+ args,
1387
+ ),
1388
+ );
1389
+ }
1390
+
1391
+ private async invokeOptional(
1392
+ method: string,
1393
+ ...args: unknown[]
1394
+ ): Promise<void> {
1395
+ const callable = (
1396
+ this.sessionService as unknown as Record<string, unknown>
1397
+ )[method];
1398
+ if (typeof callable !== "function") return;
1399
+ await Promise.resolve(
1400
+ (callable as (...params: unknown[]) => unknown).apply(
1401
+ this.sessionService,
1402
+ args,
1403
+ ),
1404
+ );
1405
+ }
1406
+
1407
+ private async invokeOptionalValue<T = unknown>(
1408
+ method: string,
1409
+ ...args: unknown[]
1410
+ ): Promise<T | undefined> {
1411
+ const callable = (
1412
+ this.sessionService as unknown as Record<string, unknown>
1413
+ )[method];
1414
+ if (typeof callable !== "function") return undefined;
1415
+ return await Promise.resolve(
1416
+ (callable as (...params: unknown[]) => T | Promise<T>).apply(
1417
+ this.sessionService,
1418
+ args,
1419
+ ),
1420
+ );
1421
+ }
1422
+ }