@clinebot/core 0.0.32 → 0.0.34

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 (85) hide show
  1. package/dist/auth/client.d.ts +19 -0
  2. package/dist/auth/client.d.ts.map +1 -1
  3. package/dist/auth/cline.d.ts.map +1 -1
  4. package/dist/auth/oca.d.ts.map +1 -1
  5. package/dist/auth/server.d.ts +32 -0
  6. package/dist/auth/server.d.ts.map +1 -1
  7. package/dist/auth/types.d.ts +29 -0
  8. package/dist/auth/types.d.ts.map +1 -1
  9. package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
  10. package/dist/extensions/context/basic-compaction.d.ts.map +1 -1
  11. package/dist/extensions/context/compaction-shared.d.ts +1 -1
  12. package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
  13. package/dist/extensions/context/compaction.d.ts.map +1 -1
  14. package/dist/extensions/index.d.ts +2 -1
  15. package/dist/extensions/index.d.ts.map +1 -1
  16. package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
  17. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  18. package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
  19. package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
  20. package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
  21. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  22. package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
  23. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  24. package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +227 -229
  28. package/dist/runtime/runtime-builder.d.ts +1 -1
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/subprocess-sandbox.d.ts +2 -0
  31. package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
  32. package/dist/runtime/tool-approval.d.ts.map +1 -1
  33. package/dist/session/default-session-manager.d.ts.map +1 -1
  34. package/dist/session/persistence-service.d.ts.map +1 -1
  35. package/dist/session/session-agent-events.d.ts.map +1 -1
  36. package/dist/session/session-artifacts.d.ts +2 -0
  37. package/dist/session/session-artifacts.d.ts.map +1 -1
  38. package/dist/session/session-config-builder.d.ts.map +1 -1
  39. package/dist/team/team-tools.d.ts.map +1 -1
  40. package/dist/types/config.d.ts +1 -0
  41. package/dist/types/config.d.ts.map +1 -1
  42. package/dist/types/events.d.ts +4 -0
  43. package/dist/types/events.d.ts.map +1 -1
  44. package/package.json +4 -4
  45. package/src/auth/client.test.ts +29 -0
  46. package/src/auth/client.ts +21 -0
  47. package/src/auth/cline.ts +2 -0
  48. package/src/auth/oca.ts +2 -0
  49. package/src/auth/server.test.ts +287 -0
  50. package/src/auth/server.ts +50 -1
  51. package/src/auth/types.ts +29 -0
  52. package/src/extensions/context/agentic-compaction.ts +22 -10
  53. package/src/extensions/context/basic-compaction.ts +43 -18
  54. package/src/extensions/context/compaction-shared.ts +1 -1
  55. package/src/extensions/context/compaction.test.ts +16 -10
  56. package/src/extensions/context/compaction.ts +35 -12
  57. package/src/extensions/index.ts +6 -0
  58. package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
  59. package/src/extensions/plugin/plugin-config-loader.ts +18 -10
  60. package/src/extensions/plugin/plugin-load-report.ts +20 -0
  61. package/src/extensions/plugin/plugin-loader.test.ts +45 -0
  62. package/src/extensions/plugin/plugin-loader.ts +57 -3
  63. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
  64. package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
  65. package/src/extensions/plugin/plugin-sandbox.ts +17 -6
  66. package/src/index.ts +11 -0
  67. package/src/providers/local-provider-service.test.ts +4 -4
  68. package/src/runtime/hook-file-hooks.test.ts +42 -7
  69. package/src/runtime/runtime-builder.test.ts +98 -0
  70. package/src/runtime/runtime-builder.ts +112 -65
  71. package/src/runtime/subprocess-sandbox.ts +26 -23
  72. package/src/runtime/tool-approval.ts +13 -15
  73. package/src/session/default-session-manager.ts +1 -3
  74. package/src/session/persistence-service.test.ts +38 -0
  75. package/src/session/persistence-service.ts +16 -1
  76. package/src/session/session-agent-events.ts +9 -1
  77. package/src/session/session-artifacts.ts +16 -0
  78. package/src/session/session-config-builder.ts +46 -0
  79. package/src/team/team-tools.test.ts +104 -0
  80. package/src/team/team-tools.ts +35 -16
  81. package/src/types/config.ts +1 -0
  82. package/src/types/events.ts +4 -0
  83. package/dist/runtime/team-runtime-registry.d.ts +0 -13
  84. package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
  85. package/src/runtime/team-runtime-registry.ts +0 -43
@@ -39,7 +39,6 @@ import type {
39
39
  RuntimeBuilderInput,
40
40
  BuiltRuntime as RuntimeEnvironment,
41
41
  } from "./session-runtime";
42
- import { TeamRuntimeRegistry } from "./team-runtime-registry";
43
42
 
44
43
  type SkillsExecutorMetadataItem = {
45
44
  id: string;
@@ -52,6 +51,29 @@ type SkillsExecutorWithMetadata = SkillsExecutor & {
52
51
  configuredSkills?: SkillsExecutorMetadataItem[];
53
52
  };
54
53
 
54
+ function isToolEnabledByPolicies(
55
+ toolName: string,
56
+ toolPolicies: CoreSessionConfig["toolPolicies"],
57
+ ): boolean {
58
+ const globalPolicy = toolPolicies?.["*"] ?? {};
59
+ const toolPolicy = toolPolicies?.[toolName] ?? {};
60
+ return (
61
+ {
62
+ ...globalPolicy,
63
+ ...toolPolicy,
64
+ }.enabled !== false
65
+ );
66
+ }
67
+
68
+ function filterToolsByPolicies(
69
+ tools: Tool[],
70
+ toolPolicies: CoreSessionConfig["toolPolicies"],
71
+ ): Tool[] {
72
+ return tools.filter((tool) =>
73
+ isToolEnabledByPolicies(tool.name, toolPolicies),
74
+ );
75
+ }
76
+
55
77
  export function createTeamName(): string {
56
78
  return `team-${nanoid(5)}`;
57
79
  }
@@ -63,6 +85,7 @@ function createBuiltinToolsList(
63
85
  yolo: boolean | undefined,
64
86
  modelId: string,
65
87
  toolRoutingRules: ToolRoutingRule[] | undefined,
88
+ toolPolicies: CoreSessionConfig["toolPolicies"],
66
89
  skillsExecutor?: SkillsExecutorWithMetadata,
67
90
  executorOverrides?: Partial<ToolExecutors>,
68
91
  ): Tool[] {
@@ -74,20 +97,23 @@ function createBuiltinToolsList(
74
97
  toolRoutingRules ?? DEFAULT_MODEL_TOOL_ROUTING_RULES,
75
98
  );
76
99
 
77
- return createBuiltinTools({
78
- cwd,
79
- ...preset,
80
- enableSkills: !!skillsExecutor,
81
- ...toolRoutingConfig,
82
- executors: {
83
- ...(skillsExecutor
84
- ? {
85
- skills: skillsExecutor,
86
- }
87
- : {}),
88
- ...(executorOverrides ?? {}),
89
- },
90
- });
100
+ return filterToolsByPolicies(
101
+ createBuiltinTools({
102
+ cwd,
103
+ ...preset,
104
+ enableSkills: !!skillsExecutor,
105
+ ...toolRoutingConfig,
106
+ executors: {
107
+ ...(skillsExecutor
108
+ ? {
109
+ skills: skillsExecutor,
110
+ }
111
+ : {}),
112
+ ...(executorOverrides ?? {}),
113
+ },
114
+ }),
115
+ toolPolicies,
116
+ );
91
117
  }
92
118
 
93
119
  const SKILL_FILE_NAME = "SKILL.md";
@@ -96,7 +122,7 @@ function listAvailableSkillNames(
96
122
  watcher: UserInstructionConfigWatcher,
97
123
  allowedSkillNames?: ReadonlyArray<string>,
98
124
  ): string[] {
99
- return listConfiguredSkills(watcher, allowedSkillNames)
125
+ return getConfiguredSkills(watcher, allowedSkillNames)
100
126
  .filter((skill) => !skill.disabled)
101
127
  .map((skill) => skill.name.trim())
102
128
  .filter((name) => name.length > 0)
@@ -143,10 +169,14 @@ function isSkillAllowed(
143
169
  );
144
170
  }
145
171
 
146
- function listConfiguredSkills(
172
+ type ConfiguredSkill = SkillsExecutorMetadataItem & {
173
+ skill: SkillConfig;
174
+ };
175
+
176
+ function getConfiguredSkills(
147
177
  watcher: UserInstructionConfigWatcher,
148
178
  allowedSkillNames?: ReadonlyArray<string>,
149
- ): SkillsExecutorMetadataItem[] {
179
+ ): ConfiguredSkill[] {
150
180
  const allowedSkills = toAllowedSkillSet(allowedSkillNames);
151
181
  const snapshot = watcher.getSnapshot("skill");
152
182
  return [...snapshot.entries()]
@@ -157,6 +187,7 @@ function listConfiguredSkills(
157
187
  name: skill.name.trim(),
158
188
  description: skill.description?.trim(),
159
189
  disabled: skill.disabled === true,
190
+ skill,
160
191
  };
161
192
  })
162
193
  .filter((skill) => isSkillAllowed(skill.id, skill.name, allowedSkills));
@@ -254,28 +285,22 @@ function resolveSkillRecord(
254
285
  requestedSkill: string,
255
286
  allowedSkillNames?: ReadonlyArray<string>,
256
287
  ): { id: string; skill: SkillConfig } | { error: string } {
257
- const allowedSkills = toAllowedSkillSet(allowedSkillNames);
258
288
  const normalized = requestedSkill.trim().replace(/^\/+/, "").toLowerCase();
259
289
  if (!normalized) {
260
290
  return { error: "Missing skill name." };
261
291
  }
262
292
 
263
- const snapshot = watcher.getSnapshot("skill");
264
- const scopedEntries = [...snapshot.entries()].filter(([id, record]) => {
265
- const skill = record.item as SkillConfig;
266
- return isSkillAllowed(id, skill.name, allowedSkills);
267
- });
268
- const scopedSnapshot = new Map(scopedEntries);
269
- const exact = scopedSnapshot.get(normalized);
293
+ const configuredSkills = getConfiguredSkills(watcher, allowedSkillNames);
294
+ const exact = configuredSkills.find((entry) => entry.id === normalized);
270
295
  if (exact) {
271
- const skill = exact.item as SkillConfig;
296
+ const { skill } = exact;
272
297
  if (skill.disabled === true) {
273
298
  return {
274
299
  error: `Skill "${skill.name}" is configured but disabled.`,
275
300
  };
276
301
  }
277
302
  return {
278
- id: normalized,
303
+ id: exact.id,
279
304
  skill,
280
305
  };
281
306
  }
@@ -284,30 +309,38 @@ function resolveSkillRecord(
284
309
  ? (normalized.split(":").at(-1) ?? normalized)
285
310
  : normalized;
286
311
 
287
- const suffixMatches = [...scopedSnapshot.entries()].filter(([id]) => {
312
+ const suffixMatches = configuredSkills.filter(({ id }) => {
288
313
  if (id === bareName) {
289
314
  return true;
290
315
  }
291
316
  return id.endsWith(`:${bareName}`);
292
317
  });
293
318
 
319
+ const enabledSuffixMatches = suffixMatches.filter(
320
+ ({ skill }) => skill.disabled !== true,
321
+ );
322
+
323
+ if (enabledSuffixMatches.length === 1) {
324
+ const { id, skill } = enabledSuffixMatches[0];
325
+ return { id, skill };
326
+ }
327
+
328
+ if (enabledSuffixMatches.length > 1) {
329
+ return {
330
+ error: `Skill "${requestedSkill}" is ambiguous. Use one of: ${enabledSuffixMatches.map(({ id }) => id).join(", ")}`,
331
+ };
332
+ }
333
+
294
334
  if (suffixMatches.length === 1) {
295
- const [id, record] = suffixMatches[0];
296
- const skill = record.item as SkillConfig;
297
- if (skill.disabled === true) {
298
- return {
299
- error: `Skill "${skill.name}" is configured but disabled.`,
300
- };
301
- }
335
+ const { skill } = suffixMatches[0];
302
336
  return {
303
- id,
304
- skill,
337
+ error: `Skill "${skill.name}" is configured but disabled.`,
305
338
  };
306
339
  }
307
340
 
308
341
  if (suffixMatches.length > 1) {
309
342
  return {
310
- error: `Skill "${requestedSkill}" is ambiguous. Use one of: ${suffixMatches.map(([id]) => id).join(", ")}`,
343
+ error: `Skill "${requestedSkill}" is ambiguous, and all matches are disabled: ${suffixMatches.map(({ id }) => id).join(", ")}`,
311
344
  };
312
345
  }
313
346
 
@@ -354,7 +387,10 @@ function createSkillsExecutor(
354
387
  }
355
388
  };
356
389
  Object.defineProperty(executor, "configuredSkills", {
357
- get: () => listConfiguredSkills(watcher, allowedSkillNames),
390
+ get: () =>
391
+ getConfiguredSkills(watcher, allowedSkillNames).map(
392
+ ({ skill: _skill, ...metadata }) => metadata,
393
+ ),
358
394
  enumerable: true,
359
395
  configurable: false,
360
396
  });
@@ -386,6 +422,7 @@ function normalizeConfig(
386
422
  | "enableTools"
387
423
  | "enableSpawnAgent"
388
424
  | "enableAgentTeams"
425
+ | "disableMcpSettingsTools"
389
426
  | "yolo"
390
427
  | "missionLogIntervalSteps"
391
428
  | "missionLogIntervalMs"
@@ -407,6 +444,7 @@ function normalizeConfig(
407
444
  config.enableSpawnAgent ?? preset.enableSpawnAgent ?? true,
408
445
  enableAgentTeams:
409
446
  config.enableAgentTeams ?? preset.enableAgentTeams ?? true,
447
+ disableMcpSettingsTools: config.disableMcpSettingsTools === true,
410
448
  yolo: config.yolo === true,
411
449
  missionLogIntervalSteps:
412
450
  typeof config.missionLogIntervalSteps === "number" &&
@@ -422,7 +460,15 @@ function normalizeConfig(
422
460
  }
423
461
 
424
462
  export class DefaultRuntimeBuilder implements RuntimeBuilder {
425
- private readonly teamRuntimeRegistry = new TeamRuntimeRegistry();
463
+ private readonly teamRuntimeEntries = new Map<
464
+ string,
465
+ {
466
+ runtime?: AgentTeamsRuntime;
467
+ delegatedAgentConfigProvider: ReturnType<
468
+ typeof createDelegatedAgentConfigProvider
469
+ >;
470
+ }
471
+ >();
426
472
 
427
473
  async build(input: RuntimeBuilderInput): Promise<RuntimeEnvironment> {
428
474
  const {
@@ -440,6 +486,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
440
486
  const normalized = normalizeConfig(config);
441
487
  const tools: Tool[] = [];
442
488
  const effectiveTeamName = config.teamName?.trim() || createTeamName();
489
+ const hasLocalSkills = hasSkillsFiles(config.cwd);
443
490
  let teamToolsRegistered = false;
444
491
  const watcherProvided = Boolean(sharedUserInstructionWatcher);
445
492
  let userInstructionWatcher = sharedUserInstructionWatcher;
@@ -447,11 +494,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
447
494
  let skillsExecutor: SkillsExecutorWithMetadata | undefined;
448
495
  let mcpShutdown: (() => Promise<void>) | undefined;
449
496
 
450
- if (
451
- !userInstructionWatcher &&
452
- normalized.enableTools &&
453
- hasSkillsFiles(config.cwd)
454
- ) {
497
+ if (!userInstructionWatcher && normalized.enableTools && hasLocalSkills) {
455
498
  userInstructionWatcher = createUserInstructionConfigWatcher({
456
499
  skills: { workspacePath: config.cwd },
457
500
  rules: { workspacePath: config.cwd },
@@ -464,8 +507,8 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
464
507
  normalized.enableTools &&
465
508
  userInstructionWatcher &&
466
509
  (watcherProvided ||
467
- hasSkillsFiles(config.cwd) ||
468
- listConfiguredSkills(userInstructionWatcher, config.skills).length > 0)
510
+ hasLocalSkills ||
511
+ getConfiguredSkills(userInstructionWatcher, config.skills).length > 0)
469
512
  ) {
470
513
  skillsExecutor = createSkillsExecutor(
471
514
  userInstructionWatcher,
@@ -483,13 +526,16 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
483
526
  normalized.yolo,
484
527
  config.modelId,
485
528
  config.toolRoutingRules,
529
+ config.toolPolicies,
486
530
  skillsExecutor,
487
531
  defaultToolExecutors,
488
532
  ),
489
533
  );
490
- const mcpRuntime = await loadConfiguredMcpTools(config.logger);
491
- tools.push(...mcpRuntime.tools);
492
- mcpShutdown = mcpRuntime.shutdown;
534
+ if (!normalized.disableMcpSettingsTools) {
535
+ const mcpRuntime = await loadConfiguredMcpTools(config.logger);
536
+ tools.push(...mcpRuntime.tools);
537
+ mcpShutdown = mcpRuntime.shutdown;
538
+ }
493
539
  }
494
540
 
495
541
  let teamRuntime: AgentTeamsRuntime | undefined;
@@ -526,21 +572,21 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
526
572
  telemetry: input.telemetry ?? config.telemetry,
527
573
  workspaceMetadata: config.workspaceMetadata,
528
574
  });
529
- this.teamRuntimeRegistry.getOrCreate(registryKey, () => ({
530
- delegatedAgentConfigProvider,
531
- }));
575
+ if (!this.teamRuntimeEntries.has(registryKey)) {
576
+ this.teamRuntimeEntries.set(registryKey, {
577
+ delegatedAgentConfigProvider,
578
+ });
579
+ }
532
580
 
533
581
  const ensureTeamRuntime = (): AgentTeamsRuntime | undefined => {
534
582
  if (!normalized.enableAgentTeams) {
535
583
  return undefined;
536
584
  }
537
585
 
538
- const registryEntry = this.teamRuntimeRegistry.getOrCreate(
539
- registryKey,
540
- () => ({
541
- delegatedAgentConfigProvider,
542
- }),
543
- );
586
+ const registryEntry = this.teamRuntimeEntries.get(registryKey) ?? {
587
+ delegatedAgentConfigProvider,
588
+ };
589
+ this.teamRuntimeEntries.set(registryKey, registryEntry);
544
590
  teamRuntime = registryEntry.runtime;
545
591
 
546
592
  if (!teamRuntime) {
@@ -592,7 +638,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
592
638
  const factory = input.teamToolsFactory ?? bootstrapAgentTeams;
593
639
  const teamBootstrap = factory({
594
640
  runtime: teamRuntime,
595
- leadAgentId: "lead",
641
+ leadAgentId: config.sessionId || "lead",
596
642
  restoredFromPersistence: Boolean(restoredTeamState),
597
643
  restoredTeammates: restoredTeammateSpecs,
598
644
  includeLeadSpawnTool: true,
@@ -610,6 +656,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
610
656
  normalized.yolo,
611
657
  config.modelId,
612
658
  config.toolRoutingRules,
659
+ config.toolPolicies,
613
660
  skillsExecutor,
614
661
  defaultToolExecutors,
615
662
  )
@@ -643,7 +690,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
643
690
 
644
691
  const completionGuard = normalized.enableAgentTeams
645
692
  ? () => {
646
- const rt = this.teamRuntimeRegistry.get(registryKey)?.runtime;
693
+ const rt = this.teamRuntimeEntries.get(registryKey)?.runtime;
647
694
  if (!rt) return undefined;
648
695
  const tasks = rt.listTasks();
649
696
  const hasInProgress = tasks.some(
@@ -675,13 +722,13 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
675
722
  : undefined;
676
723
 
677
724
  return {
678
- tools,
725
+ tools: filterToolsByPolicies(tools, config.toolPolicies),
679
726
  logger: logger ?? config.logger,
680
727
  telemetry: telemetry ?? config.telemetry,
681
728
  teamRuntime,
682
729
  teamRestoredFromPersistence: Boolean(restoredTeamState),
683
730
  delegatedAgentConfigProvider:
684
- this.teamRuntimeRegistry.get(registryKey)
731
+ this.teamRuntimeEntries.get(registryKey)
685
732
  ?.delegatedAgentConfigProvider ?? delegatedAgentConfigProvider,
686
733
  completionGuard,
687
734
  registerLeadAgent: (agent) => {
@@ -692,7 +739,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
692
739
  },
693
740
  shutdown: async (reason: string) => {
694
741
  shutdownTeamRuntime(teamRuntime, reason);
695
- this.teamRuntimeRegistry.delete(registryKey);
742
+ this.teamRuntimeEntries.delete(registryKey);
696
743
  await mcpShutdown?.();
697
744
  if (!watcherProvided) {
698
745
  userInstructionWatcher?.stop();
@@ -61,6 +61,22 @@ export class SubprocessSandbox {
61
61
  this.options = options;
62
62
  }
63
63
 
64
+ private get processLabel(): string {
65
+ return this.options.name ?? "sandbox";
66
+ }
67
+
68
+ private clearPendingRequest(id: string): PendingRequest | undefined {
69
+ const pending = this.pending.get(id);
70
+ if (!pending) {
71
+ return undefined;
72
+ }
73
+ this.pending.delete(id);
74
+ if (pending.timeout) {
75
+ clearTimeout(pending.timeout);
76
+ }
77
+ return pending;
78
+ }
79
+
64
80
  start(): void {
65
81
  if (this.process && this.process.exitCode === null) {
66
82
  return;
@@ -96,7 +112,7 @@ export class SubprocessSandbox {
96
112
  child.on("error", (error) => {
97
113
  this.failPending(
98
114
  new Error(
99
- `${this.options.name ?? "sandbox"} process error: ${asError(error).message}`,
115
+ `${this.processLabel} process error: ${asError(error).message}`,
100
116
  ),
101
117
  );
102
118
  });
@@ -119,9 +135,7 @@ export class SubprocessSandbox {
119
135
  this.start();
120
136
  const child = this.process;
121
137
  if (!child || child.exitCode !== null) {
122
- throw new Error(
123
- `${this.options.name ?? "sandbox"} process is not available`,
124
- );
138
+ throw new Error(`${this.processLabel} process is not available`);
125
139
  }
126
140
 
127
141
  const id = `req_${++this.requestCounter}`;
@@ -139,13 +153,13 @@ export class SubprocessSandbox {
139
153
  };
140
154
  if ((options.timeoutMs ?? 0) > 0) {
141
155
  pending.timeout = setTimeout(() => {
142
- this.pending.delete(id);
156
+ this.clearPendingRequest(id);
143
157
  this.shutdown().catch(() => {
144
158
  // Best-effort process shutdown after timeout.
145
159
  });
146
160
  reject(
147
161
  new Error(
148
- `${this.options.name ?? "sandbox"} call timed out after ${options.timeoutMs}ms: ${method}`,
162
+ `${this.processLabel} call timed out after ${options.timeoutMs}ms: ${method}`,
149
163
  ),
150
164
  );
151
165
  }, options.timeoutMs);
@@ -155,17 +169,13 @@ export class SubprocessSandbox {
155
169
  if (!error) {
156
170
  return;
157
171
  }
158
- const entry = this.pending.get(id);
172
+ const entry = this.clearPendingRequest(id);
159
173
  if (!entry) {
160
174
  return;
161
175
  }
162
- this.pending.delete(id);
163
- if (entry.timeout) {
164
- clearTimeout(entry.timeout);
165
- }
166
176
  entry.reject(
167
177
  new Error(
168
- `${this.options.name ?? "sandbox"} failed to send call "${method}": ${asError(error).message}`,
178
+ `${this.processLabel} failed to send call "${method}": ${asError(error).message}`,
169
179
  ),
170
180
  );
171
181
  });
@@ -176,7 +186,7 @@ export class SubprocessSandbox {
176
186
  const child = this.process;
177
187
  this.process = null;
178
188
  if (!child || child.exitCode !== null) {
179
- this.failPending(new Error(`${this.options.name ?? "sandbox"} shutdown`));
189
+ this.failPending(new Error(`${this.processLabel} shutdown`));
180
190
  return;
181
191
  }
182
192
  await new Promise<void>((resolve) => {
@@ -199,7 +209,7 @@ export class SubprocessSandbox {
199
209
  resolve();
200
210
  }
201
211
  });
202
- this.failPending(new Error(`${this.options.name ?? "sandbox"} shutdown`));
212
+ this.failPending(new Error(`${this.processLabel} shutdown`));
203
213
  }
204
214
 
205
215
  private onMessage(
@@ -220,23 +230,16 @@ export class SubprocessSandbox {
220
230
  if (message.type !== "response" || !message.id) {
221
231
  return;
222
232
  }
223
- const pending = this.pending.get(message.id);
233
+ const pending = this.clearPendingRequest(message.id);
224
234
  if (!pending) {
225
235
  return;
226
236
  }
227
- this.pending.delete(message.id);
228
- if (pending.timeout) {
229
- clearTimeout(pending.timeout);
230
- }
231
237
  if (message.ok) {
232
238
  pending.resolve(message.result);
233
239
  return;
234
240
  }
235
241
  pending.reject(
236
- new Error(
237
- message.error?.message ||
238
- `${this.options.name ?? "sandbox"} call failed`,
239
- ),
242
+ new Error(message.error?.message || `${this.processLabel} call failed`),
240
243
  );
241
244
  }
242
245
 
@@ -18,6 +18,14 @@ function delay(ms: number): Promise<void> {
18
18
  return new Promise((resolve) => setTimeout(resolve, ms));
19
19
  }
20
20
 
21
+ async function unlinkIfPresent(path: string): Promise<void> {
22
+ try {
23
+ await unlink(path);
24
+ } catch {
25
+ // Best-effort cleanup.
26
+ }
27
+ }
28
+
21
29
  export async function requestDesktopToolApproval(
22
30
  request: ToolApprovalRequest,
23
31
  options: DesktopToolApprovalOptions = {},
@@ -77,16 +85,10 @@ export async function requestDesktopToolApproval(
77
85
  approved: parsed.approved === true,
78
86
  reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
79
87
  };
80
- try {
81
- await unlink(decisionPath);
82
- } catch {
83
- // Best-effort cleanup.
84
- }
85
- try {
86
- await unlink(requestPath);
87
- } catch {
88
- // Best-effort cleanup.
89
- }
88
+ await Promise.all([
89
+ unlinkIfPresent(decisionPath),
90
+ unlinkIfPresent(requestPath),
91
+ ]);
90
92
  return result;
91
93
  } catch {
92
94
  // Decision not available yet.
@@ -94,11 +96,7 @@ export async function requestDesktopToolApproval(
94
96
  await delay(pollIntervalMs);
95
97
  }
96
98
 
97
- try {
98
- await unlink(requestPath);
99
- } catch {
100
- // Best-effort cleanup.
101
- }
99
+ await unlinkIfPresent(requestPath);
102
100
 
103
101
  return { approved: false, reason: "Tool approval request timed out" };
104
102
  }
@@ -1391,9 +1391,7 @@ export class DefaultSessionManager implements SessionManager {
1391
1391
  });
1392
1392
  } catch (error) {
1393
1393
  if (error instanceof OAuthReauthRequiredError) {
1394
- throw new Error(
1395
- `OAuth session for "${error.providerId}" requires re-authentication. Run "clite auth ${error.providerId}" and retry.`,
1396
- );
1394
+ throw new Error(`${error.providerId} requires re-authentication.`);
1397
1395
  }
1398
1396
  throw error;
1399
1397
  }
@@ -200,4 +200,42 @@ describe("UnifiedSessionPersistenceService", () => {
200
200
  );
201
201
  expect(row?.transcriptPath).toMatch(/\.log$/);
202
202
  });
203
+
204
+ it("deletes the full root session directory even when artifact paths are stale", async () => {
205
+ const sessionsDir = mkdtempSync(join(tmpdir(), "delete-root-session-dir-"));
206
+ tempDirs.push(sessionsDir);
207
+
208
+ const store = new SqliteSessionStore({ sessionsDir });
209
+ stores.push(store);
210
+ const service = new CoreSessionService(store);
211
+ const sessionId = "root-session-delete";
212
+ const artifacts = await service.createRootSessionWithArtifacts({
213
+ sessionId,
214
+ source: SessionSource.CLI,
215
+ pid: process.pid,
216
+ interactive: false,
217
+ provider: "anthropic",
218
+ model: "claude-sonnet-4-6",
219
+ cwd: "/tmp/project",
220
+ workspaceRoot: "/tmp/project",
221
+ enableTools: true,
222
+ enableSpawn: false,
223
+ enableTeams: false,
224
+ prompt: "delete me",
225
+ startedAt: "2026-04-10T19:00:00.000Z",
226
+ });
227
+
228
+ store.run(`UPDATE sessions SET messages_path = NULL WHERE session_id = ?`, [
229
+ sessionId,
230
+ ]);
231
+
232
+ expect(existsSync(artifacts.messagesPath)).toBe(true);
233
+ expect(existsSync(join(sessionsDir, sessionId))).toBe(true);
234
+
235
+ const result = await service.deleteSession(sessionId);
236
+
237
+ expect(result).toEqual({ deleted: true });
238
+ expect(existsSync(artifacts.messagesPath)).toBe(false);
239
+ expect(existsSync(join(sessionsDir, sessionId))).toBe(false);
240
+ });
203
241
  });
@@ -4,6 +4,7 @@ import {
4
4
  readFileSync,
5
5
  writeFileSync,
6
6
  } from "node:fs";
7
+ import { dirname } from "node:path";
7
8
  import type * as LlmsProviders from "@clinebot/llms";
8
9
  import type { AgentResult } from "@clinebot/shared";
9
10
  import { resolveRootSessionId } from "@clinebot/shared";
@@ -922,7 +923,21 @@ export class UnifiedSessionPersistenceService {
922
923
  unlinkIfExists(row.hookPath);
923
924
  unlinkIfExists(row.messagesPath);
924
925
  unlinkIfExists(this.artifacts.sessionManifestPath(id, false));
925
- this.artifacts.removeSessionDirIfEmpty(id);
926
+ if (row.isSubagent) {
927
+ this.artifacts.removeSessionDirIfEmpty(id);
928
+ } else {
929
+ const candidateDirs = new Set<string>([
930
+ this.artifacts.sessionArtifactsDir(id),
931
+ ]);
932
+ for (const path of [row.transcriptPath, row.hookPath, row.messagesPath]) {
933
+ if (typeof path === "string" && path.trim().length > 0) {
934
+ candidateDirs.add(dirname(path));
935
+ }
936
+ }
937
+ for (const dir of candidateDirs) {
938
+ this.artifacts.removeDir(dir);
939
+ }
940
+ }
926
941
  return { deleted: true };
927
942
  }
928
943
  }
@@ -234,7 +234,15 @@ export function handleAgentEvent(
234
234
 
235
235
  emit({
236
236
  type: "agent_event",
237
- payload: { sessionId, event },
237
+ payload: {
238
+ sessionId,
239
+ event,
240
+ teamAgentId: overrides?.teamAgentId,
241
+ teamRole:
242
+ overrides !== undefined
243
+ ? (overrides.teamRole ?? (isPrimaryAgentEvent ? "lead" : undefined))
244
+ : undefined,
245
+ },
238
246
  });
239
247
  emit({
240
248
  type: "chunk",
@@ -3,6 +3,7 @@ import {
3
3
  mkdirSync,
4
4
  readdirSync,
5
5
  rmdirSync,
6
+ rmSync,
6
7
  unlinkSync,
7
8
  } from "node:fs";
8
9
  import { dirname, join } from "node:path";
@@ -116,6 +117,21 @@ export class SessionArtifacts {
116
117
  }
117
118
  }
118
119
 
120
+ public removeSessionDir(sessionId: string): void {
121
+ this.removeDir(this.sessionArtifactsDir(sessionId));
122
+ }
123
+
124
+ public removeDir(dir: string): void {
125
+ if (!existsSync(dir)) {
126
+ return;
127
+ }
128
+ try {
129
+ rmSync(dir, { recursive: true, force: true });
130
+ } catch {
131
+ // Best-effort cleanup.
132
+ }
133
+ }
134
+
119
135
  public subagentArtifactPaths(
120
136
  sessionId: string,
121
137
  subAgentId: string,