@clinebot/core 0.0.7 → 0.0.10

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