@clinebot/core 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,1931 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { AgentResult } from "@clinebot/agents";
5
+ import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { TelemetryService } from "../telemetry/TelemetryService";
8
+ import { SessionSource } from "../types/common";
9
+ import type { CoreSessionConfig } from "../types/config";
10
+ import { DefaultSessionManager } from "./default-session-manager";
11
+ import type { SessionManifest } from "./session-manifest";
12
+
13
+ const distinctId = "test-machine-id";
14
+
15
+ function createResult(overrides: Partial<AgentResult> = {}): AgentResult {
16
+ return {
17
+ text: "ok",
18
+ iterations: 1,
19
+ finishReason: "completed",
20
+ usage: {
21
+ inputTokens: 1,
22
+ outputTokens: 2,
23
+ totalCost: 0,
24
+ },
25
+ messages: [],
26
+ toolCalls: [],
27
+ durationMs: 1,
28
+ model: {
29
+ id: "mock-model",
30
+ provider: "mock-provider",
31
+ },
32
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
33
+ endedAt: new Date("2026-01-01T00:00:01.000Z"),
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ function createManifest(sessionId: string): SessionManifest {
39
+ return {
40
+ version: 1,
41
+ session_id: sessionId,
42
+ source: SessionSource.CLI,
43
+ pid: process.pid,
44
+ started_at: "2026-01-01T00:00:00.000Z",
45
+ status: "running",
46
+ interactive: false,
47
+ provider: "mock-provider",
48
+ model: "mock-model",
49
+ cwd: "/tmp/project",
50
+ workspace_root: "/tmp/project",
51
+ enable_tools: true,
52
+ enable_spawn: true,
53
+ enable_teams: true,
54
+ prompt: "hello",
55
+ messages_path: "/tmp/messages.json",
56
+ };
57
+ }
58
+
59
+ type PluginEventTestHarness = {
60
+ handlePluginEvent: (
61
+ rootSessionId: string,
62
+ event: { name: string; payload?: unknown },
63
+ ) => Promise<void>;
64
+ getPendingPrompts: (
65
+ sessionId: string,
66
+ ) => Array<{ prompt: string; delivery: "queue" | "steer" }>;
67
+ };
68
+
69
+ function createPluginEventHarness(
70
+ manager: DefaultSessionManager,
71
+ ): PluginEventTestHarness {
72
+ const target = manager as object;
73
+ return {
74
+ handlePluginEvent: async (rootSessionId, event) => {
75
+ const handler = Reflect.get(target, "handlePluginEvent");
76
+ if (typeof handler !== "function") {
77
+ throw new Error("handlePluginEvent test hook unavailable");
78
+ }
79
+ await Reflect.apply(
80
+ handler as (
81
+ rootSessionId: string,
82
+ event: { name: string; payload?: unknown },
83
+ ) => Promise<void>,
84
+ target,
85
+ [rootSessionId, event],
86
+ );
87
+ },
88
+ getPendingPrompts: (sessionId) => {
89
+ const getter = Reflect.get(target, "getSessionOrThrow");
90
+ if (typeof getter !== "function") {
91
+ throw new Error("getSessionOrThrow test hook unavailable");
92
+ }
93
+ const session = Reflect.apply(
94
+ getter as (sessionId: string) => {
95
+ pendingPrompts: Array<{
96
+ id: string;
97
+ prompt: string;
98
+ delivery: "queue" | "steer";
99
+ userFiles?: unknown;
100
+ userImages?: unknown;
101
+ }>;
102
+ },
103
+ target,
104
+ [sessionId],
105
+ );
106
+ return session.pendingPrompts.map(({ prompt, delivery }) => ({
107
+ prompt,
108
+ delivery,
109
+ }));
110
+ },
111
+ };
112
+ }
113
+
114
+ function createConfig(
115
+ overrides: Partial<CoreSessionConfig> = {},
116
+ ): CoreSessionConfig {
117
+ return {
118
+ providerId: "mock-provider",
119
+ modelId: "mock-model",
120
+ cwd: "/tmp/project",
121
+ systemPrompt: "You are a test agent",
122
+ enableTools: true,
123
+ enableSpawnAgent: true,
124
+ enableAgentTeams: true,
125
+ ...overrides,
126
+ };
127
+ }
128
+
129
+ describe("DefaultSessionManager", () => {
130
+ const envSnapshot = {
131
+ HOME: process.env.HOME,
132
+ CLINE_DIR: process.env.CLINE_DIR,
133
+ };
134
+ let isolatedHomeDir = "";
135
+
136
+ beforeEach(() => {
137
+ isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
138
+ process.env.HOME = isolatedHomeDir;
139
+ process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
140
+ setHomeDir(isolatedHomeDir);
141
+ setClineDir(process.env.CLINE_DIR);
142
+ });
143
+
144
+ afterEach(() => {
145
+ process.env.HOME = envSnapshot.HOME;
146
+ process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
147
+ setHomeDir(envSnapshot.HOME ?? "~");
148
+ setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
149
+ rmSync(isolatedHomeDir, { recursive: true, force: true });
150
+ });
151
+
152
+ it("emits session lifecycle telemetry when configured", async () => {
153
+ const sessionId = "sess-telemetry";
154
+ const manifest = createManifest(sessionId);
155
+ const adapter = {
156
+ name: "test",
157
+ emit: vi.fn(),
158
+ emitRequired: vi.fn(),
159
+ recordCounter: vi.fn(),
160
+ recordHistogram: vi.fn(),
161
+ recordGauge: vi.fn(),
162
+ isEnabled: vi.fn(() => true),
163
+ flush: vi.fn().mockResolvedValue(undefined),
164
+ dispose: vi.fn().mockResolvedValue(undefined),
165
+ };
166
+ const telemetry = new TelemetryService({
167
+ adapters: [adapter],
168
+ distinctId: distinctId,
169
+ });
170
+ const sessionService = {
171
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
172
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
173
+ manifestPath: "/tmp/manifest.json",
174
+ transcriptPath: "/tmp/transcript.log",
175
+ hookPath: "/tmp/hook.log",
176
+ messagesPath: "/tmp/messages.json",
177
+ manifest,
178
+ }),
179
+ persistSessionMessages: vi.fn(),
180
+ updateSessionStatus: vi.fn().mockResolvedValue({
181
+ updated: true,
182
+ endedAt: "2026-01-01T00:00:05.000Z",
183
+ }),
184
+ writeSessionManifest: vi.fn(),
185
+ listSessions: vi.fn().mockResolvedValue([]),
186
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
187
+ };
188
+ const runtimeBuilder = {
189
+ build: vi.fn().mockReturnValue({
190
+ tools: [],
191
+ teamRuntime: {
192
+ getTeamId: vi.fn().mockReturnValue("team_test-team"),
193
+ getTeamName: vi.fn().mockReturnValue("test-team"),
194
+ },
195
+ teamRestoredFromPersistence: false,
196
+ shutdown: vi.fn(),
197
+ }),
198
+ };
199
+ const agent = {
200
+ run: vi.fn().mockResolvedValue(createResult()),
201
+ continue: vi.fn().mockResolvedValue(createResult()),
202
+ getMessages: vi.fn().mockReturnValue([]),
203
+ getAgentId: vi.fn().mockReturnValue("agent-root-1"),
204
+ getConversationId: vi.fn().mockReturnValue("conv-root-1"),
205
+ abort: vi.fn(),
206
+ shutdown: vi.fn().mockResolvedValue(undefined),
207
+ };
208
+ const manager = new DefaultSessionManager({
209
+ distinctId,
210
+ sessionService: sessionService as never,
211
+ runtimeBuilder: runtimeBuilder as never,
212
+ createAgent: () => agent as never,
213
+ telemetry,
214
+ });
215
+
216
+ await manager.start({
217
+ config: createConfig({ telemetry, sessionId }),
218
+ prompt: "hello",
219
+ });
220
+
221
+ expect(adapter.emit).toHaveBeenCalledWith(
222
+ "session.started",
223
+ expect.objectContaining({
224
+ sessionId,
225
+ agentId: "agent-root-1",
226
+ agentKind: "team_lead",
227
+ conversationId: "conv-root-1",
228
+ teamRole: "lead",
229
+ distinct_id: distinctId,
230
+ }),
231
+ );
232
+ expect(adapter.emit).toHaveBeenCalledWith(
233
+ "task.agent_created",
234
+ expect.objectContaining({
235
+ ulid: sessionId,
236
+ agentId: "agent-root-1",
237
+ agentKind: "team_lead",
238
+ conversationId: "conv-root-1",
239
+ teamRole: "lead",
240
+ distinct_id: distinctId,
241
+ }),
242
+ );
243
+ expect(adapter.emit).toHaveBeenCalledWith(
244
+ "task.agent_team_created",
245
+ expect.objectContaining({
246
+ ulid: sessionId,
247
+ leadAgentId: "agent-root-1",
248
+ restoredFromPersistence: false,
249
+ distinct_id: distinctId,
250
+ }),
251
+ );
252
+ });
253
+
254
+ it("persists custom session sources without coercing them to builtin values", async () => {
255
+ const sessionId = "sess-kanban";
256
+ const manifest = {
257
+ ...createManifest(sessionId),
258
+ source: "kanban",
259
+ };
260
+ const sessionService = {
261
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
262
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
263
+ manifestPath: "/tmp/manifest.json",
264
+ transcriptPath: "/tmp/transcript.log",
265
+ hookPath: "/tmp/hook.log",
266
+ messagesPath: "/tmp/messages.json",
267
+ manifest,
268
+ }),
269
+ persistSessionMessages: vi.fn(),
270
+ updateSessionStatus: vi.fn().mockResolvedValue({
271
+ updated: true,
272
+ endedAt: "2026-01-01T00:00:05.000Z",
273
+ }),
274
+ writeSessionManifest: vi.fn(),
275
+ listSessions: vi.fn().mockResolvedValue([]),
276
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
277
+ };
278
+ const runtimeBuilder = {
279
+ build: vi.fn().mockReturnValue({
280
+ tools: [],
281
+ teamRuntime: undefined,
282
+ teamRestoredFromPersistence: false,
283
+ shutdown: vi.fn(),
284
+ }),
285
+ };
286
+ const agent = {
287
+ run: vi.fn().mockResolvedValue(createResult()),
288
+ continue: vi.fn().mockResolvedValue(createResult()),
289
+ getMessages: vi.fn().mockReturnValue([]),
290
+ getAgentId: vi.fn().mockReturnValue("agent-root-1"),
291
+ getConversationId: vi.fn().mockReturnValue("conv-root-1"),
292
+ abort: vi.fn(),
293
+ shutdown: vi.fn().mockResolvedValue(undefined),
294
+ };
295
+ const manager = new DefaultSessionManager({
296
+ distinctId,
297
+ sessionService: sessionService as never,
298
+ runtimeBuilder: runtimeBuilder as never,
299
+ createAgent: () => agent as never,
300
+ });
301
+
302
+ const started = await manager.start({
303
+ source: "kanban",
304
+ config: createConfig({ sessionId }),
305
+ prompt: "hello",
306
+ });
307
+
308
+ expect(sessionService.createRootSessionWithArtifacts).toHaveBeenCalledWith(
309
+ expect.objectContaining({
310
+ sessionId,
311
+ source: "kanban",
312
+ }),
313
+ );
314
+ expect(started.manifest.source).toBe("kanban");
315
+ });
316
+
317
+ it("runs a non-interactive prompt and persists messages/status", async () => {
318
+ const sessionId = "sess-1";
319
+ const manifest = createManifest(sessionId);
320
+ const createRootSessionWithArtifacts = vi.fn().mockResolvedValue({
321
+ manifestPath: "/tmp/manifest.json",
322
+ transcriptPath: "/tmp/transcript.log",
323
+ hookPath: "/tmp/hook.log",
324
+ messagesPath: "/tmp/messages.json",
325
+ manifest,
326
+ });
327
+ const persistSessionMessages = vi.fn();
328
+ const updateSessionStatus = vi.fn().mockResolvedValue({
329
+ updated: true,
330
+ endedAt: "2026-01-01T00:00:05.000Z",
331
+ });
332
+ const writeSessionManifest = vi.fn();
333
+ const listSessions = vi.fn().mockResolvedValue([]);
334
+ const deleteSession = vi.fn().mockResolvedValue({ deleted: true });
335
+ const sessionService = {
336
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
337
+ createRootSessionWithArtifacts,
338
+ persistSessionMessages,
339
+ updateSessionStatus,
340
+ writeSessionManifest,
341
+ listSessions,
342
+ deleteSession,
343
+ };
344
+
345
+ const shutdown = vi.fn();
346
+ const runtimeBuilder = {
347
+ build: vi.fn().mockReturnValue({
348
+ tools: [],
349
+ shutdown,
350
+ }),
351
+ };
352
+ const run = vi.fn().mockResolvedValue(
353
+ createResult({
354
+ messages: [
355
+ { role: "user", content: [{ type: "text", text: "hello" }] },
356
+ ],
357
+ }),
358
+ );
359
+ const continueFn = vi.fn();
360
+ const agent = {
361
+ run,
362
+ continue: continueFn,
363
+ abort: vi.fn(),
364
+ shutdown: vi.fn().mockResolvedValue(undefined),
365
+ getMessages: vi.fn().mockReturnValue([]),
366
+ messages: [],
367
+ };
368
+
369
+ const manager = new DefaultSessionManager({
370
+ distinctId,
371
+ sessionService: sessionService as never,
372
+ runtimeBuilder,
373
+ createAgent: () => agent as never,
374
+ });
375
+
376
+ const started = await manager.start({
377
+ config: createConfig({ sessionId }),
378
+ prompt: "hello",
379
+ interactive: false,
380
+ });
381
+
382
+ expect(started.sessionId).toBe(sessionId);
383
+ expect(started.result?.finishReason).toBe("completed");
384
+ expect(run).toHaveBeenCalledTimes(1);
385
+ expect(continueFn).not.toHaveBeenCalled();
386
+ expect(persistSessionMessages).toHaveBeenCalledTimes(1);
387
+ expect(updateSessionStatus).toHaveBeenCalledWith(sessionId, "completed", 0);
388
+ expect(writeSessionManifest).toHaveBeenCalledTimes(1);
389
+ expect(shutdown).toHaveBeenCalledTimes(1);
390
+ });
391
+
392
+ it("persists assistant message metadata for usage and model identity", async () => {
393
+ const sessionId = "sess-meta";
394
+ const manifest = createManifest(sessionId);
395
+ const persistSessionMessages = vi.fn();
396
+ const sessionService = {
397
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
398
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
399
+ manifestPath: "/tmp/manifest-meta.json",
400
+ transcriptPath: "/tmp/transcript-meta.log",
401
+ hookPath: "/tmp/hook-meta.log",
402
+ messagesPath: "/tmp/messages-meta.json",
403
+ manifest,
404
+ }),
405
+ persistSessionMessages,
406
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
407
+ writeSessionManifest: vi.fn(),
408
+ listSessions: vi.fn().mockResolvedValue([]),
409
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
410
+ };
411
+ const updateConnectionDefaults = vi.fn();
412
+ const runtimeBuilder = {
413
+ build: vi.fn().mockReturnValue({
414
+ tools: [],
415
+ delegatedAgentConfigProvider: {
416
+ getRuntimeConfig: vi.fn(),
417
+ getConnectionConfig: vi.fn(),
418
+ updateConnectionDefaults,
419
+ },
420
+ shutdown: vi.fn(),
421
+ }),
422
+ };
423
+ const run = vi.fn().mockResolvedValue(
424
+ createResult({
425
+ usage: {
426
+ inputTokens: 33,
427
+ outputTokens: 12,
428
+ cacheReadTokens: 4,
429
+ cacheWriteTokens: 1,
430
+ totalCost: 0.42,
431
+ },
432
+ model: {
433
+ id: "claude-sonnet-4-6",
434
+ provider: "anthropic",
435
+ },
436
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
437
+ messages: [
438
+ { role: "user", content: [{ type: "text", text: "hello" }] },
439
+ { role: "assistant", content: [{ type: "text", text: "world" }] },
440
+ ],
441
+ }),
442
+ );
443
+ const manager = new DefaultSessionManager({
444
+ distinctId,
445
+ sessionService: sessionService as never,
446
+ runtimeBuilder,
447
+ createAgent: () =>
448
+ ({
449
+ run,
450
+ continue: vi.fn(),
451
+ abort: vi.fn(),
452
+ shutdown: vi.fn().mockResolvedValue(undefined),
453
+ getMessages: vi.fn().mockReturnValue([]),
454
+ messages: [],
455
+ }) as never,
456
+ });
457
+
458
+ await manager.start({
459
+ config: createConfig({
460
+ sessionId,
461
+ providerId: "anthropic",
462
+ modelId: "claude-sonnet-4-6",
463
+ }),
464
+ prompt: "hello",
465
+ interactive: false,
466
+ });
467
+
468
+ expect(persistSessionMessages).toHaveBeenCalledTimes(1);
469
+ const persisted = persistSessionMessages.mock.calls[0]?.[1];
470
+ expect(Array.isArray(persisted)).toBe(true);
471
+ expect(persisted?.[1]).toMatchObject({
472
+ role: "assistant",
473
+ providerId: "anthropic",
474
+ modelId: "claude-sonnet-4-6",
475
+ modelInfo: {
476
+ id: "claude-sonnet-4-6",
477
+ provider: "anthropic",
478
+ },
479
+ metrics: {
480
+ inputTokens: 33,
481
+ outputTokens: 12,
482
+ cacheReadTokens: 4,
483
+ cacheWriteTokens: 1,
484
+ cost: 0.42,
485
+ },
486
+ ts: new Date("2026-01-01T00:00:02.000Z").getTime(),
487
+ });
488
+ });
489
+
490
+ it("queues sandbox steer messages back into the active session", async () => {
491
+ const sessionId = "sess-steer";
492
+ const manifest = createManifest(sessionId);
493
+ const sessionService = {
494
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
495
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
496
+ manifestPath: "/tmp/manifest.json",
497
+ transcriptPath: "/tmp/transcript.log",
498
+ hookPath: "/tmp/hook.log",
499
+ messagesPath: "/tmp/messages.json",
500
+ manifest,
501
+ }),
502
+ persistSessionMessages: vi.fn(),
503
+ updateSessionStatus: vi.fn().mockResolvedValue({
504
+ updated: true,
505
+ endedAt: "2026-01-01T00:00:05.000Z",
506
+ }),
507
+ writeSessionManifest: vi.fn(),
508
+ listSessions: vi.fn().mockResolvedValue([]),
509
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
510
+ };
511
+ const updateConnectionDefaults = vi.fn();
512
+ const runtimeBuilder = {
513
+ build: vi.fn().mockReturnValue({
514
+ tools: [],
515
+ delegatedAgentConfigProvider: {
516
+ getRuntimeConfig: vi.fn(),
517
+ getConnectionConfig: vi.fn(),
518
+ updateConnectionDefaults,
519
+ },
520
+ shutdown: vi.fn(),
521
+ }),
522
+ };
523
+ const run = vi.fn().mockResolvedValue(
524
+ createResult({
525
+ messages: [
526
+ { role: "user", content: [{ type: "text", text: "hello" }] },
527
+ ],
528
+ }),
529
+ );
530
+ const continueFn = vi.fn().mockResolvedValue(
531
+ createResult({
532
+ text: "steered",
533
+ messages: [
534
+ { role: "user", content: [{ type: "text", text: "hello" }] },
535
+ {
536
+ role: "assistant",
537
+ content: [{ type: "text", text: "steered" }],
538
+ },
539
+ ],
540
+ }),
541
+ );
542
+ const agent = {
543
+ run,
544
+ continue: continueFn,
545
+ abort: vi.fn(),
546
+ shutdown: vi.fn().mockResolvedValue(undefined),
547
+ getMessages: vi
548
+ .fn()
549
+ .mockReturnValue([
550
+ { role: "user", content: [{ type: "text", text: "hello" }] },
551
+ ]),
552
+ canStartRun: vi.fn().mockReturnValue(true),
553
+ };
554
+
555
+ const manager = new DefaultSessionManager({
556
+ distinctId,
557
+ sessionService: sessionService as never,
558
+ runtimeBuilder,
559
+ createAgent: () => agent as never,
560
+ });
561
+
562
+ await manager.start({
563
+ config: createConfig({ sessionId }),
564
+ prompt: "hello",
565
+ interactive: true,
566
+ });
567
+
568
+ const harness = createPluginEventHarness(manager);
569
+ await harness.handlePluginEvent(sessionId, {
570
+ name: "steer_message",
571
+ payload: { prompt: "async result" },
572
+ });
573
+ await vi.waitFor(() => {
574
+ expect(continueFn).toHaveBeenCalledTimes(2);
575
+ });
576
+ expect(continueFn).toHaveBeenLastCalledWith(
577
+ '<user_input mode="act">async result</user_input>',
578
+ undefined,
579
+ undefined,
580
+ );
581
+ });
582
+
583
+ it("promotes queued prompts to the front when they become steer", async () => {
584
+ const sessionId = "sess-steer-priority";
585
+ const manifest = createManifest(sessionId);
586
+ const sessionService = {
587
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
588
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
589
+ manifestPath: "/tmp/manifest.json",
590
+ transcriptPath: "/tmp/transcript.log",
591
+ hookPath: "/tmp/hook.log",
592
+ messagesPath: "/tmp/messages.json",
593
+ manifest,
594
+ }),
595
+ persistSessionMessages: vi.fn(),
596
+ updateSessionStatus: vi.fn().mockResolvedValue({
597
+ updated: true,
598
+ endedAt: "2026-01-01T00:00:05.000Z",
599
+ }),
600
+ writeSessionManifest: vi.fn(),
601
+ listSessions: vi.fn().mockResolvedValue([]),
602
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
603
+ };
604
+ const updateConnectionDefaults = vi.fn();
605
+ const runtimeBuilder = {
606
+ build: vi.fn().mockReturnValue({
607
+ tools: [],
608
+ delegatedAgentConfigProvider: {
609
+ getRuntimeConfig: vi.fn(),
610
+ getConnectionConfig: vi.fn(),
611
+ updateConnectionDefaults,
612
+ },
613
+ shutdown: vi.fn(),
614
+ }),
615
+ };
616
+ const agent = {
617
+ run: vi.fn().mockResolvedValue(createResult()),
618
+ continue: vi.fn().mockResolvedValue(createResult()),
619
+ abort: vi.fn(),
620
+ shutdown: vi.fn().mockResolvedValue(undefined),
621
+ getMessages: vi.fn().mockReturnValue([]),
622
+ canStartRun: vi.fn().mockReturnValue(false),
623
+ };
624
+
625
+ const manager = new DefaultSessionManager({
626
+ distinctId,
627
+ sessionService: sessionService as never,
628
+ runtimeBuilder,
629
+ createAgent: () => agent as never,
630
+ });
631
+
632
+ await manager.start({
633
+ config: createConfig({ sessionId }),
634
+ prompt: "hello",
635
+ interactive: true,
636
+ });
637
+
638
+ const harness = createPluginEventHarness(manager);
639
+
640
+ await harness.handlePluginEvent(sessionId, {
641
+ name: "queue_message",
642
+ payload: { prompt: "queued first" },
643
+ });
644
+ await harness.handlePluginEvent(sessionId, {
645
+ name: "queue_message",
646
+ payload: { prompt: "queued second" },
647
+ });
648
+ await harness.handlePluginEvent(sessionId, {
649
+ name: "steer_message",
650
+ payload: { prompt: "queued first" },
651
+ });
652
+
653
+ expect(harness.getPendingPrompts(sessionId)).toEqual([
654
+ { prompt: "queued first", delivery: "steer" },
655
+ { prompt: "queued second", delivery: "queue" },
656
+ ]);
657
+ });
658
+
659
+ it("preserves per-turn metadata on prior assistant messages across turns", async () => {
660
+ const sessionId = "sess-meta-multi";
661
+ const manifest = createManifest(sessionId);
662
+ const persistSessionMessages = vi.fn();
663
+ const runtimeBuilder = {
664
+ build: vi.fn().mockReturnValue({
665
+ tools: [],
666
+ shutdown: vi.fn(),
667
+ }),
668
+ };
669
+ const firstTurnMessages = [
670
+ {
671
+ role: "user" as const,
672
+ content: [{ type: "text" as const, text: "hello" }],
673
+ },
674
+ {
675
+ role: "assistant" as const,
676
+ content: [{ type: "text" as const, text: "world" }],
677
+ },
678
+ ];
679
+ const secondTurnMessages = [
680
+ ...firstTurnMessages,
681
+ {
682
+ role: "user" as const,
683
+ content: [{ type: "text" as const, text: "again" }],
684
+ },
685
+ {
686
+ role: "assistant" as const,
687
+ content: [{ type: "text" as const, text: "still here" }],
688
+ },
689
+ ];
690
+ const run = vi.fn().mockResolvedValue(
691
+ createResult({
692
+ usage: {
693
+ inputTokens: 33,
694
+ outputTokens: 12,
695
+ cacheReadTokens: 4,
696
+ cacheWriteTokens: 1,
697
+ totalCost: 0.42,
698
+ },
699
+ model: {
700
+ id: "claude-sonnet-4-6",
701
+ provider: "anthropic",
702
+ },
703
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
704
+ messages: firstTurnMessages,
705
+ }),
706
+ );
707
+ const continueFn = vi.fn().mockResolvedValue(
708
+ createResult({
709
+ usage: {
710
+ inputTokens: 10,
711
+ outputTokens: 5,
712
+ cacheReadTokens: 2,
713
+ cacheWriteTokens: 0,
714
+ totalCost: 0.12,
715
+ },
716
+ model: {
717
+ id: "claude-sonnet-4-6",
718
+ provider: "anthropic",
719
+ },
720
+ endedAt: new Date("2026-01-01T00:00:03.000Z"),
721
+ messages: secondTurnMessages,
722
+ }),
723
+ );
724
+ const agent = {
725
+ run,
726
+ continue: continueFn,
727
+ abort: vi.fn(),
728
+ shutdown: vi.fn().mockResolvedValue(undefined),
729
+ restore: vi.fn(),
730
+ getMessages: vi.fn().mockReturnValue([]),
731
+ messages: [],
732
+ };
733
+ const sessionService = {
734
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
735
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
736
+ manifestPath: "/tmp/manifest-meta-multi.json",
737
+ transcriptPath: "/tmp/transcript-meta-multi.log",
738
+ hookPath: "/tmp/hook-meta-multi.log",
739
+ messagesPath: "/tmp/messages-meta-multi.json",
740
+ manifest,
741
+ }),
742
+ persistSessionMessages,
743
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
744
+ writeSessionManifest: vi.fn(),
745
+ listSessions: vi.fn().mockResolvedValue([]),
746
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
747
+ };
748
+ const manager = new DefaultSessionManager({
749
+ distinctId,
750
+ sessionService: sessionService as never,
751
+ runtimeBuilder,
752
+ createAgent: () => agent as never,
753
+ });
754
+
755
+ await manager.start({
756
+ config: createConfig({
757
+ sessionId,
758
+ providerId: "anthropic",
759
+ modelId: "claude-sonnet-4-6",
760
+ }),
761
+ interactive: true,
762
+ });
763
+
764
+ await manager.send({ sessionId, prompt: "hello" });
765
+ await manager.send({ sessionId, prompt: "again" });
766
+
767
+ const persisted = persistSessionMessages.mock.calls[1]?.[1];
768
+ expect(persisted?.[1]).toMatchObject({
769
+ role: "assistant",
770
+ metrics: {
771
+ inputTokens: 33,
772
+ outputTokens: 12,
773
+ cacheReadTokens: 4,
774
+ cacheWriteTokens: 1,
775
+ cost: 0.42,
776
+ },
777
+ });
778
+ expect(persisted?.[3]).toMatchObject({
779
+ role: "assistant",
780
+ metrics: {
781
+ inputTokens: 10,
782
+ outputTokens: 5,
783
+ cacheReadTokens: 2,
784
+ cacheWriteTokens: 0,
785
+ cost: 0.12,
786
+ },
787
+ });
788
+ });
789
+
790
+ it("persists rendered messages when a turn fails", async () => {
791
+ const sessionId = "sess-failed-turn";
792
+ const manifest = createManifest(sessionId);
793
+ const persistSessionMessages = vi.fn();
794
+ const sessionService = {
795
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
796
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
797
+ manifestPath: "/tmp/manifest-failed-turn.json",
798
+ transcriptPath: "/tmp/transcript-failed-turn.log",
799
+ hookPath: "/tmp/hook-failed-turn.log",
800
+ messagesPath: "/tmp/messages-failed-turn.json",
801
+ manifest,
802
+ }),
803
+ persistSessionMessages,
804
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
805
+ writeSessionManifest: vi.fn(),
806
+ listSessions: vi.fn().mockResolvedValue([]),
807
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
808
+ };
809
+ const runtimeBuilder = {
810
+ build: vi.fn().mockReturnValue({
811
+ tools: [],
812
+ shutdown: vi.fn(),
813
+ }),
814
+ };
815
+ const renderedMessages = [
816
+ { role: "user", content: [{ type: "text", text: "hello" }] },
817
+ { role: "assistant", content: [{ type: "text", text: "partial" }] },
818
+ ];
819
+ const manager = new DefaultSessionManager({
820
+ distinctId,
821
+ sessionService: sessionService as never,
822
+ runtimeBuilder,
823
+ createAgent: () =>
824
+ ({
825
+ run: vi.fn().mockRejectedValue(new Error("boom")),
826
+ continue: vi.fn(),
827
+ abort: vi.fn(),
828
+ restore: vi.fn(),
829
+ shutdown: vi.fn().mockResolvedValue(undefined),
830
+ getMessages: vi
831
+ .fn()
832
+ .mockReturnValueOnce([])
833
+ .mockReturnValue(renderedMessages),
834
+ messages: [],
835
+ }) as never,
836
+ });
837
+
838
+ await expect(
839
+ manager.start({
840
+ config: createConfig({ sessionId }),
841
+ prompt: "hello",
842
+ interactive: false,
843
+ }),
844
+ ).rejects.toThrow("boom");
845
+
846
+ expect(persistSessionMessages).toHaveBeenCalledTimes(1);
847
+ expect(persistSessionMessages).toHaveBeenCalledWith(
848
+ sessionId,
849
+ renderedMessages,
850
+ "You are a test agent",
851
+ );
852
+ expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
853
+ sessionId,
854
+ "failed",
855
+ 1,
856
+ );
857
+ });
858
+
859
+ it("uses run for first send then continue for subsequent sends", async () => {
860
+ const sessionId = "sess-2";
861
+ const manifest = createManifest(sessionId);
862
+ const sessionService = {
863
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
864
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
865
+ manifestPath: "/tmp/manifest-2.json",
866
+ transcriptPath: "/tmp/transcript-2.log",
867
+ hookPath: "/tmp/hook-2.log",
868
+ messagesPath: "/tmp/messages-2.json",
869
+ manifest,
870
+ }),
871
+ persistSessionMessages: vi.fn(),
872
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
873
+ writeSessionManifest: vi.fn(),
874
+ listSessions: vi.fn().mockResolvedValue([]),
875
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
876
+ };
877
+ const runtimeBuilder = {
878
+ build: vi.fn().mockReturnValue({
879
+ tools: [],
880
+ shutdown: vi.fn(),
881
+ }),
882
+ };
883
+ const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
884
+ const continueFn = vi
885
+ .fn()
886
+ .mockResolvedValue(createResult({ text: "second" }));
887
+ const manager = new DefaultSessionManager({
888
+ distinctId,
889
+ sessionService: sessionService as never,
890
+ runtimeBuilder,
891
+ createAgent: () =>
892
+ ({
893
+ run,
894
+ continue: continueFn,
895
+ abort: vi.fn(),
896
+ shutdown: vi.fn().mockResolvedValue(undefined),
897
+ getMessages: vi.fn().mockReturnValue([]),
898
+ messages: [],
899
+ }) as never,
900
+ });
901
+
902
+ await manager.start({
903
+ config: createConfig({ sessionId }),
904
+ interactive: true,
905
+ });
906
+ const first = await manager.send({ sessionId, prompt: "first" });
907
+ const second = await manager.send({ sessionId, prompt: "second" });
908
+
909
+ expect(first?.text).toBe("first");
910
+ expect(second?.text).toBe("second");
911
+ expect(run).toHaveBeenCalledTimes(1);
912
+ expect(continueFn).toHaveBeenCalledTimes(1);
913
+ expect(sessionService.persistSessionMessages).toHaveBeenCalledTimes(2);
914
+ });
915
+
916
+ it("tracks accumulated usage per session across turns", async () => {
917
+ const sessionId = "sess-usage";
918
+ const manifest = createManifest(sessionId);
919
+ const sessionService = {
920
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
921
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
922
+ manifestPath: "/tmp/manifest-usage.json",
923
+ transcriptPath: "/tmp/transcript-usage.log",
924
+ hookPath: "/tmp/hook-usage.log",
925
+ messagesPath: "/tmp/messages-usage.json",
926
+ manifest,
927
+ }),
928
+ persistSessionMessages: vi.fn(),
929
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
930
+ writeSessionManifest: vi.fn(),
931
+ listSessions: vi.fn().mockResolvedValue([]),
932
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
933
+ };
934
+ const runtimeBuilder = {
935
+ build: vi.fn().mockReturnValue({
936
+ tools: [],
937
+ shutdown: vi.fn(),
938
+ }),
939
+ };
940
+ const run = vi.fn().mockResolvedValue(
941
+ createResult({
942
+ text: "first",
943
+ usage: {
944
+ inputTokens: 10,
945
+ outputTokens: 3,
946
+ cacheReadTokens: 1,
947
+ cacheWriteTokens: 2,
948
+ totalCost: 0.11,
949
+ },
950
+ }),
951
+ );
952
+ const continueFn = vi.fn().mockResolvedValue(
953
+ createResult({
954
+ text: "second",
955
+ usage: {
956
+ inputTokens: 8,
957
+ outputTokens: 4,
958
+ cacheReadTokens: 2,
959
+ cacheWriteTokens: 0,
960
+ totalCost: 0.09,
961
+ },
962
+ }),
963
+ );
964
+ const manager = new DefaultSessionManager({
965
+ distinctId,
966
+ sessionService: sessionService as never,
967
+ runtimeBuilder,
968
+ createAgent: () =>
969
+ ({
970
+ run,
971
+ continue: continueFn,
972
+ abort: vi.fn(),
973
+ shutdown: vi.fn().mockResolvedValue(undefined),
974
+ getMessages: vi.fn().mockReturnValue([]),
975
+ messages: [],
976
+ }) as never,
977
+ });
978
+
979
+ await manager.start({
980
+ config: createConfig({ sessionId }),
981
+ interactive: true,
982
+ });
983
+
984
+ await manager.send({ sessionId, prompt: "first" });
985
+ expect(await manager.getAccumulatedUsage(sessionId)).toEqual({
986
+ inputTokens: 10,
987
+ outputTokens: 3,
988
+ cacheReadTokens: 1,
989
+ cacheWriteTokens: 2,
990
+ totalCost: 0.11,
991
+ });
992
+
993
+ await manager.send({ sessionId, prompt: "second" });
994
+ expect(await manager.getAccumulatedUsage(sessionId)).toEqual({
995
+ inputTokens: 18,
996
+ outputTokens: 7,
997
+ cacheReadTokens: 3,
998
+ cacheWriteTokens: 2,
999
+ totalCost: 0.2,
1000
+ });
1001
+ });
1002
+
1003
+ it("queues sends with explicit queue or steer delivery and emits snapshots", async () => {
1004
+ const sessionId = "sess-delivery-queue";
1005
+ const manifest = createManifest(sessionId);
1006
+ const sessionService = {
1007
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1008
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1009
+ manifestPath: "/tmp/manifest-queue.json",
1010
+ transcriptPath: "/tmp/transcript-queue.log",
1011
+ hookPath: "/tmp/hook-queue.log",
1012
+ messagesPath: "/tmp/messages-queue.json",
1013
+ manifest,
1014
+ }),
1015
+ persistSessionMessages: vi.fn(),
1016
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1017
+ writeSessionManifest: vi.fn(),
1018
+ listSessions: vi.fn().mockResolvedValue([]),
1019
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1020
+ };
1021
+ const runtimeBuilder = {
1022
+ build: vi.fn().mockReturnValue({
1023
+ tools: [],
1024
+ shutdown: vi.fn(),
1025
+ }),
1026
+ };
1027
+ let canStartRun = false;
1028
+ const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
1029
+ const continueFn = vi
1030
+ .fn()
1031
+ .mockResolvedValue(createResult({ text: "next" }));
1032
+ const manager = new DefaultSessionManager({
1033
+ distinctId,
1034
+ sessionService: sessionService as never,
1035
+ runtimeBuilder,
1036
+ createAgent: () =>
1037
+ ({
1038
+ run,
1039
+ continue: continueFn,
1040
+ canStartRun: vi.fn(() => canStartRun),
1041
+ abort: vi.fn(),
1042
+ shutdown: vi.fn().mockResolvedValue(undefined),
1043
+ getMessages: vi.fn().mockReturnValue([]),
1044
+ messages: [],
1045
+ }) as never,
1046
+ });
1047
+ const events: Array<unknown> = [];
1048
+ manager.subscribe((event) => {
1049
+ events.push(event);
1050
+ });
1051
+
1052
+ await manager.start({
1053
+ config: createConfig({ sessionId }),
1054
+ interactive: true,
1055
+ });
1056
+
1057
+ await expect(
1058
+ manager.send({ sessionId, prompt: "queued first", delivery: "queue" }),
1059
+ ).resolves.toBeUndefined();
1060
+ await expect(
1061
+ manager.send({ sessionId, prompt: "queued second", delivery: "steer" }),
1062
+ ).resolves.toBeUndefined();
1063
+
1064
+ expect(run).not.toHaveBeenCalled();
1065
+ expect(continueFn).not.toHaveBeenCalled();
1066
+ const promptSnapshots = events
1067
+ .filter((event) => {
1068
+ return (
1069
+ typeof event === "object" &&
1070
+ event !== null &&
1071
+ "type" in event &&
1072
+ event.type === "pending_prompts"
1073
+ );
1074
+ })
1075
+ .map((event) => (event as { payload: { prompts: unknown[] } }).payload);
1076
+ expect(promptSnapshots.at(-1)).toEqual({
1077
+ prompts: [
1078
+ expect.objectContaining({
1079
+ prompt: "queued second",
1080
+ delivery: "steer",
1081
+ attachmentCount: 0,
1082
+ }),
1083
+ expect.objectContaining({
1084
+ prompt: "queued first",
1085
+ delivery: "queue",
1086
+ attachmentCount: 0,
1087
+ }),
1088
+ ],
1089
+ sessionId,
1090
+ });
1091
+
1092
+ canStartRun = true;
1093
+ await manager.send({ sessionId, prompt: "run now" });
1094
+ expect(run).toHaveBeenCalledTimes(1);
1095
+ expect(
1096
+ events.some((event) => {
1097
+ return (
1098
+ typeof event === "object" &&
1099
+ event !== null &&
1100
+ "type" in event &&
1101
+ event.type === "pending_prompt_submitted" &&
1102
+ "payload" in event &&
1103
+ (event.payload as { prompt?: string }).prompt === "queued second"
1104
+ );
1105
+ }),
1106
+ ).toBe(true);
1107
+ });
1108
+
1109
+ it("returns undefined accumulated usage for unknown sessions", async () => {
1110
+ const manager = new DefaultSessionManager({
1111
+ distinctId,
1112
+ sessionService: {
1113
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1114
+ listSessions: vi.fn().mockResolvedValue([]),
1115
+ deleteSession: vi.fn().mockResolvedValue({ deleted: false }),
1116
+ } as never,
1117
+ runtimeBuilder: {
1118
+ build: vi.fn().mockReturnValue({
1119
+ tools: [],
1120
+ shutdown: vi.fn(),
1121
+ }),
1122
+ },
1123
+ createAgent: () =>
1124
+ ({
1125
+ run: vi.fn(),
1126
+ continue: vi.fn(),
1127
+ abort: vi.fn(),
1128
+ shutdown: vi.fn().mockResolvedValue(undefined),
1129
+ getMessages: vi.fn().mockReturnValue([]),
1130
+ messages: [],
1131
+ }) as never,
1132
+ });
1133
+
1134
+ expect(
1135
+ await manager.getAccumulatedUsage("missing-session"),
1136
+ ).toBeUndefined();
1137
+ });
1138
+
1139
+ it("marks a failed single-run session as failed when run throws", async () => {
1140
+ const sessionId = "sess-fail";
1141
+ const manifest = createManifest(sessionId);
1142
+ const sessionService = {
1143
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1144
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1145
+ manifestPath: "/tmp/manifest-fail.json",
1146
+ transcriptPath: "/tmp/transcript-fail.log",
1147
+ hookPath: "/tmp/hook-fail.log",
1148
+ messagesPath: "/tmp/messages-fail.json",
1149
+ manifest,
1150
+ }),
1151
+ persistSessionMessages: vi.fn(),
1152
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1153
+ writeSessionManifest: vi.fn(),
1154
+ listSessions: vi.fn().mockResolvedValue([]),
1155
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1156
+ };
1157
+ const runtimeShutdown = vi.fn();
1158
+ const runtimeBuilder = {
1159
+ build: vi.fn().mockReturnValue({
1160
+ tools: [],
1161
+ shutdown: runtimeShutdown,
1162
+ }),
1163
+ };
1164
+ const run = vi.fn().mockRejectedValue(new Error("run failed"));
1165
+ const agentShutdown = vi.fn().mockResolvedValue(undefined);
1166
+ const manager = new DefaultSessionManager({
1167
+ distinctId,
1168
+ sessionService: sessionService as never,
1169
+ runtimeBuilder,
1170
+ createAgent: () =>
1171
+ ({
1172
+ run,
1173
+ continue: vi.fn(),
1174
+ abort: vi.fn(),
1175
+ shutdown: agentShutdown,
1176
+ getMessages: vi.fn().mockReturnValue([]),
1177
+ messages: [],
1178
+ }) as never,
1179
+ });
1180
+
1181
+ await expect(
1182
+ manager.start({
1183
+ config: createConfig({ sessionId }),
1184
+ prompt: "hello",
1185
+ interactive: false,
1186
+ }),
1187
+ ).rejects.toThrow("run failed");
1188
+ expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
1189
+ sessionId,
1190
+ "failed",
1191
+ 1,
1192
+ );
1193
+ expect(agentShutdown).toHaveBeenCalledTimes(1);
1194
+ expect(runtimeShutdown).toHaveBeenCalledTimes(1);
1195
+ });
1196
+
1197
+ it("does not persist or emit shutdown hooks when no prompt was submitted", async () => {
1198
+ const sessionService = {
1199
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1200
+ createRootSessionWithArtifacts: vi.fn(),
1201
+ persistSessionMessages: vi.fn(),
1202
+ updateSessionStatus: vi.fn(),
1203
+ writeSessionManifest: vi.fn(),
1204
+ listSessions: vi.fn().mockResolvedValue([]),
1205
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1206
+ };
1207
+ const runtimeShutdown = vi.fn();
1208
+ const runtimeBuilder = {
1209
+ build: vi.fn().mockReturnValue({
1210
+ tools: [],
1211
+ shutdown: runtimeShutdown,
1212
+ }),
1213
+ };
1214
+ const agentShutdown = vi.fn().mockResolvedValue(undefined);
1215
+ const manager = new DefaultSessionManager({
1216
+ distinctId,
1217
+ sessionService: sessionService as never,
1218
+ runtimeBuilder,
1219
+ createAgent: () =>
1220
+ ({
1221
+ run: vi.fn(),
1222
+ continue: vi.fn(),
1223
+ abort: vi.fn(),
1224
+ shutdown: agentShutdown,
1225
+ getMessages: vi.fn().mockReturnValue([]),
1226
+ messages: [],
1227
+ }) as never,
1228
+ });
1229
+
1230
+ const started = await manager.start({
1231
+ config: createConfig({ sessionId: "sess-no-prompt" }),
1232
+ interactive: true,
1233
+ });
1234
+ await manager.stop(started.sessionId);
1235
+
1236
+ expect(
1237
+ sessionService.createRootSessionWithArtifacts,
1238
+ ).not.toHaveBeenCalled();
1239
+ expect(sessionService.updateSessionStatus).not.toHaveBeenCalled();
1240
+ expect(agentShutdown).not.toHaveBeenCalled();
1241
+ expect(runtimeShutdown).toHaveBeenCalledTimes(1);
1242
+ });
1243
+
1244
+ it("updates agent connection with refreshed OAuth key before turn", async () => {
1245
+ const sessionId = "sess-oauth";
1246
+ const manifest = createManifest(sessionId);
1247
+ const sessionService = {
1248
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1249
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1250
+ manifestPath: "/tmp/manifest-oauth.json",
1251
+ transcriptPath: "/tmp/transcript-oauth.log",
1252
+ hookPath: "/tmp/hook-oauth.log",
1253
+ messagesPath: "/tmp/messages-oauth.json",
1254
+ manifest,
1255
+ }),
1256
+ persistSessionMessages: vi.fn(),
1257
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1258
+ writeSessionManifest: vi.fn(),
1259
+ listSessions: vi.fn().mockResolvedValue([]),
1260
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1261
+ };
1262
+ const updateConnectionDefaults = vi.fn();
1263
+ const runtimeBuilder = {
1264
+ build: vi.fn().mockReturnValue({
1265
+ tools: [],
1266
+ delegatedAgentConfigProvider: {
1267
+ getRuntimeConfig: vi.fn(),
1268
+ getConnectionConfig: vi.fn(),
1269
+ updateConnectionDefaults,
1270
+ },
1271
+ shutdown: vi.fn(),
1272
+ }),
1273
+ };
1274
+ const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
1275
+ const updateConnection = vi.fn();
1276
+ const manager = new DefaultSessionManager({
1277
+ distinctId,
1278
+ sessionService: sessionService as never,
1279
+ runtimeBuilder,
1280
+ oauthTokenManager: {
1281
+ resolveProviderApiKey: vi.fn().mockResolvedValue({
1282
+ providerId: "openai-codex",
1283
+ apiKey: "oauth-access-new",
1284
+ refreshed: true,
1285
+ }),
1286
+ } as never,
1287
+ createAgent: () =>
1288
+ ({
1289
+ run,
1290
+ continue: vi.fn(),
1291
+ abort: vi.fn(),
1292
+ restore: vi.fn(),
1293
+ updateConnection,
1294
+ shutdown: vi.fn().mockResolvedValue(undefined),
1295
+ getMessages: vi.fn().mockReturnValue([]),
1296
+ messages: [],
1297
+ }) as never,
1298
+ });
1299
+
1300
+ await manager.start({
1301
+ config: createConfig({
1302
+ sessionId,
1303
+ providerId: "openai-codex",
1304
+ apiKey: "oauth-access-old",
1305
+ }),
1306
+ interactive: true,
1307
+ });
1308
+ await manager.send({ sessionId, prompt: "hello" });
1309
+
1310
+ expect(updateConnectionDefaults).toHaveBeenCalledWith({
1311
+ apiKey: "oauth-access-new",
1312
+ });
1313
+ expect(updateConnection).toHaveBeenCalledWith({
1314
+ apiKey: "oauth-access-new",
1315
+ });
1316
+ expect(run).toHaveBeenCalledTimes(1);
1317
+ });
1318
+
1319
+ it("hydrates provider-specific config from provider settings", async () => {
1320
+ const sessionId = "sess-provider-config";
1321
+ const manifest = createManifest(sessionId);
1322
+ const sessionService = {
1323
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1324
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1325
+ manifestPath: "/tmp/manifest-provider-config.json",
1326
+ transcriptPath: "/tmp/transcript-provider-config.log",
1327
+ hookPath: "/tmp/hook-provider-config.log",
1328
+ messagesPath: "/tmp/messages-provider-config.json",
1329
+ manifest,
1330
+ }),
1331
+ persistSessionMessages: vi.fn(),
1332
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1333
+ writeSessionManifest: vi.fn(),
1334
+ listSessions: vi.fn().mockResolvedValue([]),
1335
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1336
+ };
1337
+ const run = vi.fn().mockResolvedValue(
1338
+ createResult({
1339
+ model: {
1340
+ id: "claude-sonnet-4@20250514",
1341
+ provider: "vertex",
1342
+ },
1343
+ }),
1344
+ );
1345
+ const createAgent = vi.fn().mockReturnValue({
1346
+ run,
1347
+ continue: vi.fn(),
1348
+ abort: vi.fn(),
1349
+ restore: vi.fn(),
1350
+ shutdown: vi.fn().mockResolvedValue(undefined),
1351
+ getMessages: vi.fn().mockReturnValue([]),
1352
+ messages: [],
1353
+ });
1354
+ const manager = new DefaultSessionManager({
1355
+ distinctId,
1356
+ sessionService: sessionService as never,
1357
+ runtimeBuilder: {
1358
+ build: vi.fn().mockReturnValue({
1359
+ tools: [],
1360
+ shutdown: vi.fn(),
1361
+ }),
1362
+ },
1363
+ createAgent: createAgent as never,
1364
+ providerSettingsManager: {
1365
+ getProviderSettings: vi.fn().mockReturnValue({
1366
+ provider: "vertex",
1367
+ gcp: {
1368
+ projectId: "test-project",
1369
+ region: "us-central1",
1370
+ },
1371
+ }),
1372
+ } as never,
1373
+ });
1374
+
1375
+ await manager.start({
1376
+ config: createConfig({
1377
+ sessionId,
1378
+ providerId: "vertex",
1379
+ modelId: "claude-sonnet-4@20250514",
1380
+ }),
1381
+ interactive: true,
1382
+ });
1383
+ await manager.send({ sessionId, prompt: "hello" });
1384
+
1385
+ expect(createAgent).toHaveBeenCalledWith(
1386
+ expect.objectContaining({
1387
+ providerId: "vertex",
1388
+ modelId: "claude-sonnet-4@20250514",
1389
+ providerConfig: expect.objectContaining({
1390
+ providerId: "vertex",
1391
+ modelId: "claude-sonnet-4@20250514",
1392
+ gcp: {
1393
+ projectId: "test-project",
1394
+ region: "us-central1",
1395
+ },
1396
+ }),
1397
+ }),
1398
+ );
1399
+ });
1400
+
1401
+ it("forwards loopDetection config to the agent constructor", async () => {
1402
+ const sessionId = "sess-loop-detection";
1403
+ const manifest = createManifest(sessionId);
1404
+ const sessionService = {
1405
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1406
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1407
+ manifestPath: "/tmp/manifest-loop.json",
1408
+ transcriptPath: "/tmp/transcript-loop.log",
1409
+ hookPath: "/tmp/hook-loop.log",
1410
+ messagesPath: "/tmp/messages-loop.json",
1411
+ manifest,
1412
+ }),
1413
+ persistSessionMessages: vi.fn(),
1414
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1415
+ writeSessionManifest: vi.fn(),
1416
+ listSessions: vi.fn().mockResolvedValue([]),
1417
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1418
+ };
1419
+ const run = vi.fn().mockResolvedValue(createResult());
1420
+ const createAgent = vi.fn().mockReturnValue({
1421
+ run,
1422
+ continue: vi.fn(),
1423
+ abort: vi.fn(),
1424
+ restore: vi.fn(),
1425
+ shutdown: vi.fn().mockResolvedValue(undefined),
1426
+ getMessages: vi.fn().mockReturnValue([]),
1427
+ messages: [],
1428
+ });
1429
+ const manager = new DefaultSessionManager({
1430
+ distinctId,
1431
+ sessionService: sessionService as never,
1432
+ runtimeBuilder: {
1433
+ build: vi.fn().mockReturnValue({
1434
+ tools: [],
1435
+ shutdown: vi.fn(),
1436
+ }),
1437
+ },
1438
+ createAgent: createAgent as never,
1439
+ });
1440
+
1441
+ await manager.start({
1442
+ config: createConfig({
1443
+ sessionId,
1444
+ execution: {
1445
+ loopDetection: { softThreshold: 4, hardThreshold: 8 },
1446
+ },
1447
+ }),
1448
+ interactive: true,
1449
+ });
1450
+ await manager.send({ sessionId, prompt: "test" });
1451
+
1452
+ expect(createAgent).toHaveBeenCalledWith(
1453
+ expect.objectContaining({
1454
+ execution: {
1455
+ loopDetection: { softThreshold: 4, hardThreshold: 8 },
1456
+ },
1457
+ }),
1458
+ );
1459
+ });
1460
+
1461
+ it("formats prompt in core and merges explicit + mention user files", async () => {
1462
+ const tempCwd = mkdtempSync(join(tmpdir(), "core-session-format-"));
1463
+ try {
1464
+ const srcDir = join(tempCwd, "src");
1465
+ const docsDir = join(tempCwd, "docs");
1466
+ mkdirSync(srcDir, { recursive: true });
1467
+ mkdirSync(docsDir, { recursive: true });
1468
+ const mentionPath = join(srcDir, "app.ts");
1469
+ const explicitPath = join(docsDir, "note.md");
1470
+ writeFileSync(mentionPath, "export const v = 1;\n", "utf8");
1471
+ writeFileSync(explicitPath, "note\n", "utf8");
1472
+
1473
+ const sessionId = "sess-format";
1474
+ const manifest = createManifest(sessionId);
1475
+ const sessionService = {
1476
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1477
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1478
+ manifestPath: "/tmp/manifest-format.json",
1479
+ transcriptPath: "/tmp/transcript-format.log",
1480
+ hookPath: "/tmp/hook-format.log",
1481
+ messagesPath: "/tmp/messages-format.json",
1482
+ manifest,
1483
+ }),
1484
+ persistSessionMessages: vi.fn(),
1485
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1486
+ writeSessionManifest: vi.fn(),
1487
+ listSessions: vi.fn().mockResolvedValue([]),
1488
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1489
+ };
1490
+ const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
1491
+ const manager = new DefaultSessionManager({
1492
+ distinctId,
1493
+ sessionService: sessionService as never,
1494
+ runtimeBuilder: {
1495
+ build: vi.fn().mockReturnValue({
1496
+ tools: [],
1497
+ shutdown: vi.fn(),
1498
+ }),
1499
+ },
1500
+ createAgent: () =>
1501
+ ({
1502
+ run,
1503
+ continue: vi.fn(),
1504
+ abort: vi.fn(),
1505
+ shutdown: vi.fn().mockResolvedValue(undefined),
1506
+ getMessages: vi.fn().mockReturnValue([]),
1507
+ messages: [],
1508
+ }) as never,
1509
+ });
1510
+
1511
+ await manager.start({
1512
+ config: createConfig({
1513
+ sessionId,
1514
+ cwd: join(tempCwd, "docs"),
1515
+ workspaceRoot: tempCwd,
1516
+ }),
1517
+ interactive: true,
1518
+ });
1519
+ await manager.send({
1520
+ sessionId,
1521
+ prompt: '<user_input mode="act">explain @src/app.ts</user_input>',
1522
+ userFiles: ["note.md"],
1523
+ });
1524
+
1525
+ expect(run).toHaveBeenCalledWith(
1526
+ '<user_input mode="act">explain @src/app.ts</user_input>',
1527
+ undefined,
1528
+ expect.arrayContaining([mentionPath, explicitPath]),
1529
+ );
1530
+ } finally {
1531
+ rmSync(tempCwd, { recursive: true, force: true });
1532
+ }
1533
+ });
1534
+
1535
+ it("force refreshes and retries once when turn fails with auth error", async () => {
1536
+ const sessionId = "sess-oauth-retry";
1537
+ const manifest = createManifest(sessionId);
1538
+ const sessionService = {
1539
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1540
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1541
+ manifestPath: "/tmp/manifest-oauth-retry.json",
1542
+ transcriptPath: "/tmp/transcript-oauth-retry.log",
1543
+ hookPath: "/tmp/hook-oauth-retry.log",
1544
+ messagesPath: "/tmp/messages-oauth-retry.json",
1545
+ manifest,
1546
+ }),
1547
+ persistSessionMessages: vi.fn(),
1548
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1549
+ writeSessionManifest: vi.fn(),
1550
+ listSessions: vi.fn().mockResolvedValue([]),
1551
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1552
+ };
1553
+ const updateConnectionDefaults = vi.fn();
1554
+ const runtimeBuilder = {
1555
+ build: vi.fn().mockReturnValue({
1556
+ tools: [],
1557
+ delegatedAgentConfigProvider: {
1558
+ getRuntimeConfig: vi.fn(),
1559
+ getConnectionConfig: vi.fn(),
1560
+ updateConnectionDefaults,
1561
+ },
1562
+ shutdown: vi.fn(),
1563
+ }),
1564
+ };
1565
+ const run = vi
1566
+ .fn()
1567
+ .mockRejectedValueOnce(new Error("401 Unauthorized"))
1568
+ .mockResolvedValueOnce(createResult({ text: "retried" }));
1569
+ const restore = vi.fn();
1570
+ const updateConnection = vi.fn();
1571
+ const resolveProviderApiKey = vi
1572
+ .fn()
1573
+ .mockResolvedValueOnce(null)
1574
+ .mockResolvedValueOnce({
1575
+ providerId: "openai-codex",
1576
+ apiKey: "oauth-access-new",
1577
+ refreshed: true,
1578
+ });
1579
+ const manager = new DefaultSessionManager({
1580
+ distinctId,
1581
+ sessionService: sessionService as never,
1582
+ runtimeBuilder,
1583
+ oauthTokenManager: {
1584
+ resolveProviderApiKey,
1585
+ } as never,
1586
+ createAgent: () =>
1587
+ ({
1588
+ run,
1589
+ continue: vi.fn(),
1590
+ abort: vi.fn(),
1591
+ restore,
1592
+ updateConnection,
1593
+ shutdown: vi.fn().mockResolvedValue(undefined),
1594
+ getMessages: vi.fn().mockReturnValue([]),
1595
+ messages: [],
1596
+ }) as never,
1597
+ });
1598
+
1599
+ await manager.start({
1600
+ config: createConfig({
1601
+ sessionId,
1602
+ providerId: "openai-codex",
1603
+ apiKey: "oauth-access-old",
1604
+ }),
1605
+ interactive: true,
1606
+ });
1607
+ const result = await manager.send({ sessionId, prompt: "hello" });
1608
+
1609
+ expect(result?.text).toBe("retried");
1610
+ expect(run).toHaveBeenCalledTimes(2);
1611
+ expect(restore).toHaveBeenCalledTimes(1);
1612
+ expect(resolveProviderApiKey).toHaveBeenNthCalledWith(1, {
1613
+ providerId: "openai-codex",
1614
+ forceRefresh: undefined,
1615
+ });
1616
+ expect(resolveProviderApiKey).toHaveBeenNthCalledWith(2, {
1617
+ providerId: "openai-codex",
1618
+ forceRefresh: true,
1619
+ });
1620
+ expect(updateConnection).toHaveBeenCalledWith({
1621
+ apiKey: "oauth-access-new",
1622
+ });
1623
+ expect(updateConnectionDefaults).toHaveBeenCalledWith({
1624
+ apiKey: "oauth-access-new",
1625
+ });
1626
+ });
1627
+
1628
+ it("auto-continues when async teammate runs complete after lead turn", async () => {
1629
+ const sessionId = "sess-team-auto-continue";
1630
+ const manifest = createManifest(sessionId);
1631
+ const sessionService = {
1632
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1633
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1634
+ manifestPath: "/tmp/manifest-team-auto-continue.json",
1635
+ transcriptPath: "/tmp/transcript-team-auto-continue.log",
1636
+ hookPath: "/tmp/hook-team-auto-continue.log",
1637
+ messagesPath: "/tmp/messages-team-auto-continue.json",
1638
+ manifest,
1639
+ }),
1640
+ persistSessionMessages: vi.fn(),
1641
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1642
+ writeSessionManifest: vi.fn(),
1643
+ listSessions: vi.fn().mockResolvedValue([]),
1644
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1645
+ };
1646
+
1647
+ let onTeamEvent: ((event: unknown) => void) | undefined;
1648
+ const runtimeBuilder = {
1649
+ build: vi
1650
+ .fn()
1651
+ .mockImplementation(
1652
+ (input: { onTeamEvent?: (event: unknown) => void }) => {
1653
+ onTeamEvent = input.onTeamEvent;
1654
+ return {
1655
+ tools: [],
1656
+ shutdown: vi.fn(),
1657
+ };
1658
+ },
1659
+ ),
1660
+ };
1661
+
1662
+ const run = vi.fn().mockImplementation(async () => {
1663
+ onTeamEvent?.({
1664
+ type: "run_started",
1665
+ run: {
1666
+ id: "run_0001",
1667
+ agentId: "investigator",
1668
+ status: "running",
1669
+ message: "Investigate",
1670
+ priority: 0,
1671
+ retryCount: 0,
1672
+ maxRetries: 0,
1673
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
1674
+ },
1675
+ });
1676
+ setTimeout(() => {
1677
+ onTeamEvent?.({
1678
+ type: "run_completed",
1679
+ run: {
1680
+ id: "run_0001",
1681
+ agentId: "investigator",
1682
+ status: "completed",
1683
+ message: "Investigate",
1684
+ priority: 0,
1685
+ retryCount: 0,
1686
+ maxRetries: 0,
1687
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
1688
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
1689
+ result: createResult({ iterations: 3 }),
1690
+ },
1691
+ });
1692
+ }, 0);
1693
+ return createResult({ text: "lead scheduled teammate" });
1694
+ });
1695
+ const continueFn = vi
1696
+ .fn()
1697
+ .mockResolvedValue(
1698
+ createResult({ text: "lead processed teammate result" }),
1699
+ );
1700
+ const manager = new DefaultSessionManager({
1701
+ distinctId,
1702
+ sessionService: sessionService as never,
1703
+ runtimeBuilder,
1704
+ createAgent: () =>
1705
+ ({
1706
+ run,
1707
+ continue: continueFn,
1708
+ abort: vi.fn(),
1709
+ shutdown: vi.fn().mockResolvedValue(undefined),
1710
+ getMessages: vi.fn().mockReturnValue([]),
1711
+ messages: [],
1712
+ }) as never,
1713
+ });
1714
+
1715
+ await manager.start({
1716
+ config: createConfig({ sessionId }),
1717
+ interactive: false,
1718
+ });
1719
+ const result = await manager.send({
1720
+ sessionId,
1721
+ prompt: "run teammate work",
1722
+ });
1723
+
1724
+ expect(result?.text).toBe("lead processed teammate result");
1725
+ expect(run).toHaveBeenCalledTimes(1);
1726
+ expect(continueFn).toHaveBeenCalledTimes(1);
1727
+ expect(continueFn.mock.calls[0]?.[0]).toContain(
1728
+ "System-delivered teammate async run updates:",
1729
+ );
1730
+ expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
1731
+ sessionId,
1732
+ "completed",
1733
+ 0,
1734
+ );
1735
+ });
1736
+
1737
+ it("persists failed teammate task messages for team-task sub-sessions", async () => {
1738
+ const sessionId = "sess-team-task-failure-messages";
1739
+ const manifest = createManifest(sessionId);
1740
+ const sessionService = {
1741
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1742
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1743
+ manifestPath: "/tmp/manifest-team-task-failure-messages.json",
1744
+ transcriptPath: "/tmp/transcript-team-task-failure-messages.log",
1745
+ hookPath: "/tmp/hook-team-task-failure-messages.log",
1746
+ messagesPath: "/tmp/messages-team-task-failure-messages.json",
1747
+ manifest,
1748
+ }),
1749
+ persistSessionMessages: vi.fn(),
1750
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1751
+ writeSessionManifest: vi.fn(),
1752
+ listSessions: vi.fn().mockResolvedValue([]),
1753
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1754
+ onTeamTaskStart: vi.fn().mockResolvedValue(undefined),
1755
+ onTeamTaskEnd: vi.fn().mockResolvedValue(undefined),
1756
+ };
1757
+
1758
+ let onTeamEvent: ((event: unknown) => void) | undefined;
1759
+ const runtimeBuilder = {
1760
+ build: vi
1761
+ .fn()
1762
+ .mockImplementation(
1763
+ (input: { onTeamEvent?: (event: unknown) => void }) => {
1764
+ onTeamEvent = input.onTeamEvent;
1765
+ return {
1766
+ tools: [],
1767
+ shutdown: vi.fn(),
1768
+ };
1769
+ },
1770
+ ),
1771
+ };
1772
+
1773
+ const failedMessages = [
1774
+ { role: "user", content: [{ type: "text", text: "delegated prompt" }] },
1775
+ { role: "assistant", content: [{ type: "text", text: "partial work" }] },
1776
+ ];
1777
+ const manager = new DefaultSessionManager({
1778
+ distinctId,
1779
+ sessionService: sessionService as never,
1780
+ runtimeBuilder,
1781
+ createAgent: () =>
1782
+ ({
1783
+ run: vi.fn().mockImplementation(async () => {
1784
+ onTeamEvent?.({
1785
+ type: "task_start",
1786
+ agentId: "providers-investigator",
1787
+ message: "Investigate provider boundaries",
1788
+ });
1789
+ onTeamEvent?.({
1790
+ type: "task_end",
1791
+ agentId: "providers-investigator",
1792
+ error: new Error("401 Unauthorized"),
1793
+ messages: failedMessages,
1794
+ });
1795
+ return createResult({ text: "lead handled failure" });
1796
+ }),
1797
+ continue: vi.fn(),
1798
+ abort: vi.fn(),
1799
+ shutdown: vi.fn().mockResolvedValue(undefined),
1800
+ getMessages: vi.fn().mockReturnValue([]),
1801
+ messages: [],
1802
+ }) as never,
1803
+ });
1804
+
1805
+ await manager.start({
1806
+ config: createConfig({ sessionId }),
1807
+ prompt: "run teammate work",
1808
+ interactive: false,
1809
+ });
1810
+
1811
+ expect(sessionService.onTeamTaskStart).toHaveBeenCalledTimes(1);
1812
+ expect(sessionService.onTeamTaskEnd).toHaveBeenCalledWith(
1813
+ sessionId,
1814
+ "providers-investigator",
1815
+ "failed",
1816
+ "[error] 401 Unauthorized",
1817
+ failedMessages,
1818
+ );
1819
+ });
1820
+
1821
+ it("persists teammate progress updates for team-task sub-sessions", async () => {
1822
+ const sessionId = "sess-team-task-progress";
1823
+ const manifest = createManifest(sessionId);
1824
+ const sessionService = {
1825
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1826
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1827
+ manifestPath: "/tmp/manifest-team-task-progress.json",
1828
+ transcriptPath: "/tmp/transcript-team-task-progress.log",
1829
+ hookPath: "/tmp/hook-team-task-progress.log",
1830
+ messagesPath: "/tmp/messages-team-task-progress.json",
1831
+ manifest,
1832
+ }),
1833
+ persistSessionMessages: vi.fn(),
1834
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1835
+ writeSessionManifest: vi.fn(),
1836
+ listSessions: vi.fn().mockResolvedValue([]),
1837
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1838
+ onTeamTaskStart: vi.fn().mockResolvedValue(undefined),
1839
+ onTeamTaskEnd: vi.fn().mockResolvedValue(undefined),
1840
+ onTeamTaskProgress: vi.fn().mockResolvedValue(undefined),
1841
+ };
1842
+
1843
+ let onTeamEvent: ((event: unknown) => void) | undefined;
1844
+ const runtimeBuilder = {
1845
+ build: vi
1846
+ .fn()
1847
+ .mockImplementation(
1848
+ (input: { onTeamEvent?: (event: unknown) => void }) => {
1849
+ onTeamEvent = input.onTeamEvent;
1850
+ return {
1851
+ tools: [],
1852
+ shutdown: vi.fn(),
1853
+ };
1854
+ },
1855
+ ),
1856
+ };
1857
+
1858
+ const manager = new DefaultSessionManager({
1859
+ distinctId,
1860
+ sessionService: sessionService as never,
1861
+ runtimeBuilder,
1862
+ createAgent: () =>
1863
+ ({
1864
+ run: vi.fn().mockImplementation(async () => {
1865
+ onTeamEvent?.({
1866
+ type: "task_start",
1867
+ agentId: "providers-investigator",
1868
+ message: "Investigate provider boundaries",
1869
+ });
1870
+ onTeamEvent?.({
1871
+ type: "run_progress",
1872
+ run: {
1873
+ id: "run_00002",
1874
+ agentId: "providers-investigator",
1875
+ status: "running",
1876
+ message: "Investigate provider boundaries",
1877
+ priority: 0,
1878
+ retryCount: 0,
1879
+ maxRetries: 0,
1880
+ continueConversation: false,
1881
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
1882
+ lastProgressAt: new Date("2026-01-01T00:00:01.000Z"),
1883
+ lastProgressMessage: "heartbeat",
1884
+ currentActivity: "heartbeat",
1885
+ },
1886
+ message: "heartbeat",
1887
+ });
1888
+ onTeamEvent?.({
1889
+ type: "agent_event",
1890
+ agentId: "providers-investigator",
1891
+ event: {
1892
+ type: "content_start",
1893
+ contentType: "text",
1894
+ text: "Drafting the provider boundary analysis now.",
1895
+ },
1896
+ });
1897
+ onTeamEvent?.({
1898
+ type: "task_end",
1899
+ agentId: "providers-investigator",
1900
+ result: createResult(),
1901
+ });
1902
+ return createResult({ text: "lead handled progress" });
1903
+ }),
1904
+ continue: vi.fn(),
1905
+ abort: vi.fn(),
1906
+ shutdown: vi.fn().mockResolvedValue(undefined),
1907
+ getMessages: vi.fn().mockReturnValue([]),
1908
+ messages: [],
1909
+ }) as never,
1910
+ });
1911
+
1912
+ await manager.start({
1913
+ config: createConfig({ sessionId }),
1914
+ prompt: "run teammate work",
1915
+ interactive: false,
1916
+ });
1917
+
1918
+ expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
1919
+ sessionId,
1920
+ "providers-investigator",
1921
+ "heartbeat",
1922
+ { kind: "heartbeat" },
1923
+ );
1924
+ expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
1925
+ sessionId,
1926
+ "providers-investigator",
1927
+ "Drafting the provider boundary analysis now.",
1928
+ { kind: "text" },
1929
+ );
1930
+ });
1931
+ });