@clinebot/core 0.0.0

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