@clinebot/core 0.0.7 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.js +124 -122
  7. package/dist/input/mention-enricher.d.ts +1 -0
  8. package/dist/providers/local-provider-service.d.ts +1 -1
  9. package/dist/runtime/session-runtime.d.ts +1 -1
  10. package/dist/session/default-session-manager.d.ts +13 -17
  11. package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
  12. package/dist/session/session-agent-events.d.ts +15 -0
  13. package/dist/session/session-config-builder.d.ts +13 -0
  14. package/dist/session/session-manager.d.ts +2 -2
  15. package/dist/session/session-team-coordination.d.ts +12 -0
  16. package/dist/session/session-telemetry.d.ts +9 -0
  17. package/dist/session/unified-session-persistence-service.d.ts +12 -16
  18. package/dist/session/utils/helpers.d.ts +2 -2
  19. package/dist/session/utils/types.d.ts +2 -1
  20. package/dist/telemetry/core-events.d.ts +122 -0
  21. package/dist/tools/definitions.d.ts +1 -1
  22. package/dist/tools/executors/file-read.d.ts +1 -1
  23. package/dist/tools/index.d.ts +1 -1
  24. package/dist/tools/presets.d.ts +1 -1
  25. package/dist/tools/schemas.d.ts +46 -3
  26. package/dist/tools/types.d.ts +3 -3
  27. package/dist/types/config.d.ts +1 -1
  28. package/dist/types/provider-settings.d.ts +4 -4
  29. package/dist/types.d.ts +1 -1
  30. package/package.json +4 -3
  31. package/src/auth/cline.ts +35 -1
  32. package/src/auth/codex.ts +27 -2
  33. package/src/auth/oca.ts +31 -4
  34. package/src/auth/types.ts +3 -0
  35. package/src/index.ts +27 -0
  36. package/src/input/mention-enricher.test.ts +3 -0
  37. package/src/input/mention-enricher.ts +3 -0
  38. package/src/providers/local-provider-service.ts +6 -7
  39. package/src/runtime/hook-file-hooks.ts +11 -10
  40. package/src/runtime/session-runtime.ts +1 -1
  41. package/src/session/default-session-manager.e2e.test.ts +2 -1
  42. package/src/session/default-session-manager.test.ts +131 -0
  43. package/src/session/default-session-manager.ts +372 -602
  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 +14 -4
  54. package/src/session/utils/types.ts +2 -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,
@@ -385,13 +303,14 @@ export class DefaultSessionManager implements SessionManager {
385
303
  started: false,
386
304
  aborting: false,
387
305
  interactive: input.interactive === true,
306
+ persistedMessages: input.initialMessages,
388
307
  activeTeamRunIds: new Set<string>(),
389
308
  pendingTeamRunUpdates: [],
390
309
  teamRunWaiters: [],
391
- pluginSandboxShutdown: loadedPlugins.shutdown,
310
+ pluginSandboxShutdown,
392
311
  };
393
- this.sessions.set(active.sessionId, active);
394
- this.emitStatus(active.sessionId, "running");
312
+ this.sessions.set(sessionId, active);
313
+ this.emitStatus(sessionId, "running");
395
314
 
396
315
  let result: AgentResult | undefined;
397
316
  try {
@@ -422,10 +341,7 @@ export class DefaultSessionManager implements SessionManager {
422
341
  }
423
342
 
424
343
  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
- }
344
+ const session = this.getSessionOrThrow(input.sessionId);
429
345
  session.config.telemetry?.capture({
430
346
  event: "session.input_sent",
431
347
  properties: {
@@ -455,37 +371,34 @@ export class DefaultSessionManager implements SessionManager {
455
371
  sessionId: string,
456
372
  ): Promise<SessionAccumulatedUsage | undefined> {
457
373
  const usage = this.usageBySession.get(sessionId);
458
- if (!usage) {
459
- return undefined;
460
- }
461
- return { ...usage };
374
+ return usage ? { ...usage } : undefined;
462
375
  }
463
376
 
464
- async abort(sessionId: string): Promise<void> {
377
+ async abort(sessionId: string, reason?: unknown): Promise<void> {
465
378
  const session = this.sessions.get(sessionId);
466
- if (!session) {
467
- return;
468
- }
379
+ if (!session) return;
469
380
  session.config.telemetry?.capture({
470
381
  event: "session.aborted",
471
382
  properties: { sessionId },
472
383
  });
473
384
  session.aborting = true;
474
- session.agent.abort();
385
+ (
386
+ session.agent as Agent & {
387
+ abort: (abortReason?: unknown) => void;
388
+ }
389
+ ).abort(reason);
475
390
  }
476
391
 
477
392
  async stop(sessionId: string): Promise<void> {
478
393
  const session = this.sessions.get(sessionId);
479
- if (!session) {
480
- return;
481
- }
394
+ if (!session) return;
482
395
  session.config.telemetry?.capture({
483
396
  event: "session.stopped",
484
397
  properties: { sessionId },
485
398
  });
486
399
  await this.shutdownSession(session, {
487
400
  status: "cancelled",
488
- exitCode: null,
401
+ exitCode: 0,
489
402
  shutdownReason: "session_stop",
490
403
  endReason: "stopped",
491
404
  });
@@ -493,18 +406,16 @@ export class DefaultSessionManager implements SessionManager {
493
406
 
494
407
  async dispose(reason = "session_manager_dispose"): Promise<void> {
495
408
  const sessions = [...this.sessions.values()];
496
- if (sessions.length === 0) {
497
- return;
498
- }
409
+ if (sessions.length === 0) return;
499
410
  await Promise.allSettled(
500
- sessions.map(async (session) => {
501
- await this.shutdownSession(session, {
411
+ sessions.map((session) =>
412
+ this.shutdownSession(session, {
502
413
  status: "cancelled",
503
- exitCode: null,
414
+ exitCode: 0,
504
415
  shutdownReason: reason,
505
416
  endReason: "disposed",
506
- });
507
- }),
417
+ }),
418
+ ),
508
419
  );
509
420
  this.usageBySession.clear();
510
421
  }
@@ -516,7 +427,7 @@ export class DefaultSessionManager implements SessionManager {
516
427
 
517
428
  async list(limit = 200): Promise<SessionRecord[]> {
518
429
  const rows = await this.listRows(limit);
519
- return rows.map((row) => toSessionRecord(row));
430
+ return rows.map(toSessionRecord);
520
431
  }
521
432
 
522
433
  async delete(sessionId: string): Promise<boolean> {
@@ -535,9 +446,7 @@ export class DefaultSessionManager implements SessionManager {
535
446
 
536
447
  async readTranscript(sessionId: string, maxChars?: number): Promise<string> {
537
448
  const row = await this.getRow(sessionId);
538
- if (!row?.transcript_path || !existsSync(row.transcript_path)) {
539
- return "";
540
- }
449
+ if (!row?.transcript_path || !existsSync(row.transcript_path)) return "";
541
450
  const raw = readFileSync(row.transcript_path, "utf8");
542
451
  if (typeof maxChars === "number" && Number.isFinite(maxChars)) {
543
452
  return raw.slice(-Math.max(0, Math.floor(maxChars)));
@@ -548,21 +457,17 @@ export class DefaultSessionManager implements SessionManager {
548
457
  async readMessages(sessionId: string): Promise<LlmsProviders.Message[]> {
549
458
  const row = await this.getRow(sessionId);
550
459
  const messagesPath = row?.messages_path?.trim();
551
- if (!messagesPath || !existsSync(messagesPath)) {
552
- return [];
553
- }
460
+ if (!messagesPath || !existsSync(messagesPath)) return [];
554
461
  try {
555
- const raw = readFileSync(messagesPath, "utf8");
556
- if (!raw.trim()) {
557
- return [];
462
+ const raw = readFileSync(messagesPath, "utf8").trim();
463
+ if (!raw) return [];
464
+ const parsed = JSON.parse(raw) as unknown;
465
+ if (Array.isArray(parsed)) return parsed as LlmsProviders.Message[];
466
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
467
+ const messages = (parsed as { messages?: unknown }).messages;
468
+ if (Array.isArray(messages)) return messages as LlmsProviders.Message[];
558
469
  }
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[];
470
+ return [];
566
471
  } catch {
567
472
  return [];
568
473
  }
@@ -570,15 +475,11 @@ export class DefaultSessionManager implements SessionManager {
570
475
 
571
476
  async readHooks(sessionId: string, limit = 200): Promise<unknown[]> {
572
477
  const row = await this.getRow(sessionId);
573
- if (!row?.hook_path || !existsSync(row.hook_path)) {
574
- return [];
575
- }
478
+ if (!row?.hook_path || !existsSync(row.hook_path)) return [];
576
479
  const lines = readFileSync(row.hook_path, "utf8")
577
480
  .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) => {
481
+ .filter((line) => line.trim().length > 0);
482
+ return lines.slice(-Math.max(1, Math.floor(limit))).map((line) => {
582
483
  try {
583
484
  return JSON.parse(line) as unknown;
584
485
  } catch {
@@ -594,6 +495,14 @@ export class DefaultSessionManager implements SessionManager {
594
495
  };
595
496
  }
596
497
 
498
+ async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
499
+ const session = this.getSessionOrThrow(sessionId);
500
+ session.config.modelId = modelId;
501
+ this.updateAgentConnection(session, { modelId });
502
+ }
503
+
504
+ // ── Turn execution ──────────────────────────────────────────────────
505
+
597
506
  private async runTurn(
598
507
  session: ActiveSession,
599
508
  input: {
@@ -604,9 +513,8 @@ export class DefaultSessionManager implements SessionManager {
604
513
  ): Promise<AgentResult> {
605
514
  const preparedInput = await this.prepareTurnInput(session, input);
606
515
  const prompt = preparedInput.prompt.trim();
607
- if (!prompt) {
608
- throw new Error("prompt cannot be empty");
609
- }
516
+ if (!prompt) throw new Error("prompt cannot be empty");
517
+
610
518
  if (!session.artifacts && !session.pendingPrompt) {
611
519
  session.pendingPrompt = prompt;
612
520
  }
@@ -620,12 +528,10 @@ export class DefaultSessionManager implements SessionManager {
620
528
  preparedInput.userFiles,
621
529
  );
622
530
 
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(
531
+ while (shouldAutoContinueTeamRuns(session, result.finishReason)) {
532
+ const updates = await waitForTeamRunUpdates(session);
533
+ if (updates.length === 0) break;
534
+ const continuationPrompt = buildTeamRunContinuationPrompt(
629
535
  session,
630
536
  updates,
631
537
  );
@@ -643,28 +549,44 @@ export class DefaultSessionManager implements SessionManager {
643
549
  ): Promise<AgentResult> {
644
550
  const shouldContinue =
645
551
  session.started || session.agent.getMessages().length > 0;
646
- const baselineMessages = session.agent.getMessages();
552
+ const baselineMessages =
553
+ session.persistedMessages ?? session.agent.getMessages();
647
554
  const usageBaseline =
648
555
  this.usageBySession.get(session.sessionId) ??
649
556
  createInitialAccumulatedUsage();
650
557
  session.turnUsageBaseline = usageBaseline;
558
+
559
+ captureModeSwitch(
560
+ session.config.telemetry,
561
+ session.sessionId,
562
+ session.config.mode,
563
+ );
564
+ captureConversationTurnEvent(session.config.telemetry, {
565
+ ulid: session.sessionId,
566
+ provider: session.config.providerId,
567
+ model: session.config.modelId,
568
+ source: "user",
569
+ mode: session.config.mode,
570
+ isNativeToolCall: false,
571
+ });
572
+
651
573
  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
- );
574
+ const runFn = shouldContinue
575
+ ? () => session.agent.continue(prompt, userImages, userFiles)
576
+ : () => session.agent.run(prompt, userImages, userFiles);
577
+ const result = await this.runWithAuthRetry(
578
+ session,
579
+ runFn,
580
+ baselineMessages,
581
+ );
582
+
663
583
  session.started = true;
664
584
  const persistedMessages = withLatestAssistantTurnMetadata(
665
585
  result.messages,
666
586
  result,
587
+ baselineMessages,
667
588
  );
589
+ session.persistedMessages = persistedMessages;
668
590
  this.usageBySession.set(
669
591
  session.sessionId,
670
592
  accumulateUsageTotals(usageBaseline, result.usage),
@@ -677,7 +599,6 @@ export class DefaultSessionManager implements SessionManager {
677
599
  );
678
600
  return result;
679
601
  } catch (error) {
680
- // Persist whatever was rendered so far even when a turn fails.
681
602
  await this.invoke<void>(
682
603
  "persistSessionMessages",
683
604
  session.sessionId,
@@ -698,7 +619,7 @@ export class DefaultSessionManager implements SessionManager {
698
619
  userFiles?: string[];
699
620
  },
700
621
  ): Promise<PreparedTurnInput> {
701
- const mentionBaseDir = session.config.workspaceRoot ?? session.config.cwd;
622
+ const mentionBaseDir = resolveWorkspacePath(session.config);
702
623
  const normalizedPrompt = normalizeUserInput(input.prompt).trim();
703
624
  if (!normalizedPrompt) {
704
625
  return {
@@ -715,10 +636,9 @@ export class DefaultSessionManager implements SessionManager {
715
636
  normalizedPrompt,
716
637
  mentionBaseDir,
717
638
  );
718
- const prompt = formatUserInputBlock(
719
- enriched.prompt,
720
- session.config.mode === "plan" ? "plan" : "act",
721
- );
639
+ emitMentionTelemetry(session.config.telemetry, enriched);
640
+
641
+ const prompt = formatModePrompt(enriched.prompt, session.config.mode);
722
642
  const explicitUserFiles = this.resolveAbsoluteFilePaths(
723
643
  session.config.cwd,
724
644
  input.userFiles,
@@ -738,23 +658,11 @@ export class DefaultSessionManager implements SessionManager {
738
658
  };
739
659
  }
740
660
 
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
- }
661
+ // ── Session lifecycle ───────────────────────────────────────────────
753
662
 
754
663
  private async ensureSessionPersisted(session: ActiveSession): Promise<void> {
755
- if (session.artifacts) {
756
- return;
757
- }
664
+ if (session.artifacts) return;
665
+ const workspacePath = resolveWorkspacePath(session.config);
758
666
  session.artifacts = (await this.invoke("createRootSessionWithArtifacts", {
759
667
  sessionId: session.sessionId,
760
668
  source: session.source,
@@ -763,7 +671,7 @@ export class DefaultSessionManager implements SessionManager {
763
671
  provider: session.config.providerId,
764
672
  model: session.config.modelId,
765
673
  cwd: session.config.cwd,
766
- workspaceRoot: session.config.workspaceRoot ?? session.config.cwd,
674
+ workspaceRoot: workspacePath,
767
675
  teamName: session.config.teamName,
768
676
  enableTools: session.config.enableTools,
769
677
  enableSpawn: session.config.enableSpawnAgent,
@@ -777,24 +685,14 @@ export class DefaultSessionManager implements SessionManager {
777
685
  session: ActiveSession,
778
686
  finishReason: AgentResult["finishReason"],
779
687
  ): 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
- }
688
+ if (hasPendingTeamRunWork(session)) return;
689
+ const isAborted = finishReason === "aborted" || session.aborting;
690
+ await this.shutdownSession(session, {
691
+ status: isAborted ? "cancelled" : "completed",
692
+ exitCode: 0,
693
+ shutdownReason: "session_complete",
694
+ endReason: finishReason,
695
+ });
798
696
  }
799
697
 
800
698
  private async failSession(session: ActiveSession): Promise<void> {
@@ -815,17 +713,23 @@ export class DefaultSessionManager implements SessionManager {
815
713
  endReason: string;
816
714
  },
817
715
  ): Promise<void> {
818
- this.notifyTeamRunWaiters(session);
819
- if (session.artifacts) {
820
- await this.updateStatus(session, input.status, input.exitCode);
716
+ if (input.status === "completed") {
717
+ captureTaskCompleted(session.config.telemetry, {
718
+ ulid: session.sessionId,
719
+ provider: session.config.providerId,
720
+ modelId: session.config.modelId,
721
+ mode: session.config.mode,
722
+ durationMs: Date.now() - Date.parse(session.startedAt),
723
+ });
821
724
  }
725
+ notifyTeamRunWaiters(session);
726
+
822
727
  if (session.artifacts) {
728
+ await this.updateStatus(session, input.status, input.exitCode);
823
729
  await session.agent.shutdown(input.shutdownReason);
824
730
  }
825
731
  await Promise.resolve(session.runtime.shutdown(input.shutdownReason));
826
- if (session.pluginSandboxShutdown) {
827
- await session.pluginSandboxShutdown();
828
- }
732
+ await session.pluginSandboxShutdown?.();
829
733
  this.sessions.delete(session.sessionId);
830
734
  this.emit({
831
735
  type: "ended",
@@ -842,18 +746,14 @@ export class DefaultSessionManager implements SessionManager {
842
746
  status: SessionStatus,
843
747
  exitCode?: number | null,
844
748
  ): Promise<void> {
845
- if (!session.artifacts) {
846
- return;
847
- }
749
+ if (!session.artifacts) return;
848
750
  const result = await this.invoke<{ updated: boolean; endedAt?: string }>(
849
751
  "updateSessionStatus",
850
752
  session.sessionId,
851
753
  status,
852
754
  exitCode,
853
755
  );
854
- if (!result.updated) {
855
- return;
856
- }
756
+ if (!result.updated) return;
857
757
  session.artifacts.manifest.status = status;
858
758
  session.artifacts.manifest.ended_at = result.endedAt ?? nowIso();
859
759
  session.artifacts.manifest.exit_code =
@@ -866,51 +766,47 @@ export class DefaultSessionManager implements SessionManager {
866
766
  this.emitStatus(session.sessionId, status);
867
767
  }
868
768
 
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
- }
769
+ // ── Agent event handling ────────────────────────────────────────────
883
770
 
884
- private async getRow(
771
+ private onAgentEvent(
885
772
  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);
773
+ config: CoreSessionConfig,
774
+ event: AgentEvent,
775
+ ): void {
776
+ const ctx: AgentEventContext = {
777
+ sessionId,
778
+ config,
779
+ liveSession: this.sessions.get(sessionId),
780
+ usageBySession: this.usageBySession,
781
+ persistMessages: (sid, messages, systemPrompt) => {
782
+ void this.invoke<void>(
783
+ "persistSessionMessages",
784
+ sid,
785
+ messages,
786
+ systemPrompt,
787
+ );
788
+ },
789
+ emit: (e) => this.emit(e),
790
+ };
791
+ handleAgentEvent(ctx, event);
893
792
  }
894
793
 
794
+ // ── Spawn / sub-agents ──────────────────────────────────────────────
795
+
895
796
  private createSpawnTool(
896
797
  config: CoreSessionConfig,
897
798
  rootSessionId: string,
898
799
  ): 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
800
  const createSubAgentTools = () => {
913
- const tools = createBaseTools();
801
+ const tools: Tool[] = config.enableTools
802
+ ? createBuiltinTools({
803
+ cwd: config.cwd,
804
+ ...(config.mode === "plan"
805
+ ? ToolPresets.readonly
806
+ : ToolPresets.development),
807
+ executors: this.defaultToolExecutors,
808
+ })
809
+ : [];
914
810
  if (config.enableSpawnAgent) {
915
811
  tools.push(this.createSpawnTool(config, rootSessionId));
916
812
  }
@@ -936,242 +832,53 @@ export class DefaultSessionManager implements SessionManager {
936
832
  requestToolApproval: this.defaultRequestToolApproval,
937
833
  logger: config.logger,
938
834
  onSubAgentStart: (context) => {
835
+ this.subAgentStarts.set(context.subAgentId, {
836
+ startedAt: Date.now(),
837
+ rootSessionId,
838
+ });
939
839
  void this.invokeOptional("handleSubAgentStart", rootSessionId, context);
940
840
  },
941
841
  onSubAgentEnd: (context) => {
842
+ const started = this.subAgentStarts.get(context.subAgentId);
843
+ const durationMs = started ? Date.now() - started.startedAt : 0;
844
+ const outputLines = context.result?.text
845
+ ? context.result.text.split("\n").length
846
+ : 0;
847
+ captureSubagentExecution(config.telemetry, {
848
+ ulid: rootSessionId,
849
+ durationMs,
850
+ outputLines,
851
+ success: !context.error,
852
+ });
853
+ this.subAgentStarts.delete(context.subAgentId);
942
854
  void this.invokeOptional("handleSubAgentEnd", rootSessionId, context);
943
855
  },
944
856
  }) as Tool;
945
857
  }
946
858
 
859
+ // ── Team run coordination ───────────────────────────────────────────
860
+
947
861
  private async handleTeamEvent(
948
862
  rootSessionId: string,
949
863
  event: TeamEvent,
950
864
  ): Promise<void> {
951
865
  const session = this.sessions.get(rootSessionId);
952
866
  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
- });
867
+ trackTeamRunState(session, event);
1103
868
  }
1104
- }
1105
869
 
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",
870
+ await dispatchTeamEventToBackend(
871
+ rootSessionId,
872
+ event,
873
+ this.invokeOptional.bind(this),
1128
874
  );
1129
- }
1130
875
 
1131
- private emit(event: CoreSessionEvent): void {
1132
- for (const listener of this.listeners) {
1133
- listener(event);
876
+ if (session) {
877
+ emitTeamProgress(session, rootSessionId, event, (e) => this.emit(e));
1134
878
  }
1135
879
  }
1136
880
 
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
- }
881
+ // ── OAuth & auth ────────────────────────────────────────────────────
1175
882
 
1176
883
  private async runWithAuthRetry(
1177
884
  session: ActiveSession,
@@ -1181,37 +888,15 @@ export class DefaultSessionManager implements SessionManager {
1181
888
  try {
1182
889
  return await run();
1183
890
  } catch (error) {
1184
- if (!this.isLikelyAuthError(error, session.config.providerId)) {
891
+ if (!isLikelyAuthError(error, session.config.providerId)) {
1185
892
  throw error;
1186
893
  }
1187
-
1188
894
  await this.syncOAuthCredentials(session, { forceRefresh: true });
1189
895
  session.agent.restore(baselineMessages);
1190
896
  return run();
1191
897
  }
1192
898
  }
1193
899
 
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
900
  private async syncOAuthCredentials(
1216
901
  session: ActiveSession,
1217
902
  options?: { forceRefresh?: boolean },
@@ -1230,32 +915,117 @@ export class DefaultSessionManager implements SessionManager {
1230
915
  }
1231
916
  throw error;
1232
917
  }
1233
- if (!resolved?.apiKey) {
1234
- return;
1235
- }
1236
- if (session.config.apiKey === resolved.apiKey) {
1237
- return;
1238
- }
918
+ if (!resolved?.apiKey || session.config.apiKey === resolved.apiKey) return;
1239
919
  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
920
+ this.updateAgentConnection(session, { apiKey: resolved.apiKey });
1245
921
  session.runtime.teamRuntime?.updateTeammateConnections({
1246
922
  apiKey: resolved.apiKey,
1247
923
  });
1248
924
  }
1249
925
 
1250
- async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
926
+ // ── Utility methods ─────────────────────────────────────────────────
927
+
928
+ private getSessionOrThrow(sessionId: string): ActiveSession {
1251
929
  const session = this.sessions.get(sessionId);
1252
- if (!session) {
1253
- throw new Error(`session not found: ${sessionId}`);
1254
- }
1255
- session.config.modelId = modelId;
930
+ if (!session) throw new Error(`session not found: ${sessionId}`);
931
+ return session;
932
+ }
933
+
934
+ private resolveAbsoluteFilePaths(cwd: string, paths?: string[]): string[] {
935
+ if (!paths || paths.length === 0) return [];
936
+ const resolved = paths
937
+ .map((p) => p.trim())
938
+ .filter((p) => p.length > 0)
939
+ .map((p) => (isAbsolute(p) ? p : resolve(cwd, p)));
940
+ return Array.from(new Set(resolved));
941
+ }
942
+
943
+ private updateAgentConnection(
944
+ session: ActiveSession,
945
+ overrides: { apiKey?: string; modelId?: string },
946
+ ): void {
1256
947
  const agentWithConnection = session.agent as Agent & {
1257
- updateConnection?: (overrides: { modelId?: string }) => void;
948
+ updateConnection?: (overrides: {
949
+ apiKey?: string;
950
+ modelId?: string;
951
+ }) => void;
1258
952
  };
1259
- agentWithConnection.updateConnection?.({ modelId });
953
+ agentWithConnection.updateConnection?.(overrides);
954
+ }
955
+
956
+ private emitStatus(sessionId: string, status: string): void {
957
+ this.emit({
958
+ type: "status",
959
+ payload: { sessionId, status },
960
+ });
961
+ }
962
+
963
+ private emit(event: CoreSessionEvent): void {
964
+ for (const listener of this.listeners) listener(event);
965
+ }
966
+
967
+ private async listRows(limit: number): Promise<SessionRowShape[]> {
968
+ return this.invoke<SessionRowShape[]>(
969
+ "listSessions",
970
+ Math.min(Math.max(1, Math.floor(limit)), MAX_SCAN_LIMIT),
971
+ );
972
+ }
973
+
974
+ private async getRow(
975
+ sessionId: string,
976
+ ): Promise<SessionRowShape | undefined> {
977
+ const target = sessionId.trim();
978
+ if (!target) return undefined;
979
+ const rows = await this.listRows(MAX_SCAN_LIMIT);
980
+ return rows.find((row) => row.session_id === target);
981
+ }
982
+
983
+ // ── Session service invocation ──────────────────────────────────────
984
+
985
+ private async invoke<T>(method: string, ...args: unknown[]): Promise<T> {
986
+ const callable = (
987
+ this.sessionService as unknown as Record<string, unknown>
988
+ )[method];
989
+ if (typeof callable !== "function") {
990
+ throw new Error(`session service method not available: ${method}`);
991
+ }
992
+ return Promise.resolve(
993
+ (callable as (...params: unknown[]) => T | Promise<T>).apply(
994
+ this.sessionService,
995
+ args,
996
+ ),
997
+ );
998
+ }
999
+
1000
+ private async invokeOptional(
1001
+ method: string,
1002
+ ...args: unknown[]
1003
+ ): Promise<void> {
1004
+ const callable = (
1005
+ this.sessionService as unknown as Record<string, unknown>
1006
+ )[method];
1007
+ if (typeof callable !== "function") return;
1008
+ await Promise.resolve(
1009
+ (callable as (...params: unknown[]) => unknown).apply(
1010
+ this.sessionService,
1011
+ args,
1012
+ ),
1013
+ );
1014
+ }
1015
+
1016
+ private async invokeOptionalValue<T = unknown>(
1017
+ method: string,
1018
+ ...args: unknown[]
1019
+ ): Promise<T | undefined> {
1020
+ const callable = (
1021
+ this.sessionService as unknown as Record<string, unknown>
1022
+ )[method];
1023
+ if (typeof callable !== "function") return undefined;
1024
+ return await Promise.resolve(
1025
+ (callable as (...params: unknown[]) => T | Promise<T>).apply(
1026
+ this.sessionService,
1027
+ args,
1028
+ ),
1029
+ );
1260
1030
  }
1261
1031
  }