@clinebot/core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +88 -0
  2. package/dist/account/cline-account-service.d.ts +34 -0
  3. package/dist/account/index.d.ts +3 -0
  4. package/dist/account/rpc.d.ts +38 -0
  5. package/dist/account/types.d.ts +74 -0
  6. package/dist/agents/agent-config-loader.d.ts +18 -0
  7. package/dist/agents/agent-config-parser.d.ts +25 -0
  8. package/dist/agents/hooks-config-loader.d.ts +23 -0
  9. package/dist/agents/index.d.ts +11 -0
  10. package/dist/agents/plugin-config-loader.d.ts +22 -0
  11. package/dist/agents/plugin-loader.d.ts +9 -0
  12. package/dist/agents/plugin-sandbox.d.ts +12 -0
  13. package/dist/agents/unified-config-file-watcher.d.ts +77 -0
  14. package/dist/agents/user-instruction-config-loader.d.ts +63 -0
  15. package/dist/auth/client.d.ts +11 -0
  16. package/dist/auth/cline.d.ts +41 -0
  17. package/dist/auth/codex.d.ts +39 -0
  18. package/dist/auth/oca.d.ts +22 -0
  19. package/dist/auth/server.d.ts +22 -0
  20. package/dist/auth/types.d.ts +72 -0
  21. package/dist/auth/utils.d.ts +32 -0
  22. package/dist/chat/chat-schema.d.ts +145 -0
  23. package/dist/default-tools/constants.d.ts +23 -0
  24. package/dist/default-tools/definitions.d.ts +96 -0
  25. package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
  26. package/dist/default-tools/executors/apply-patch.d.ts +26 -0
  27. package/dist/default-tools/executors/bash.d.ts +49 -0
  28. package/dist/default-tools/executors/editor.d.ts +31 -0
  29. package/dist/default-tools/executors/file-read.d.ts +40 -0
  30. package/dist/default-tools/executors/index.d.ts +44 -0
  31. package/dist/default-tools/executors/search.d.ts +50 -0
  32. package/dist/default-tools/executors/web-fetch.d.ts +58 -0
  33. package/dist/default-tools/index.d.ts +57 -0
  34. package/dist/default-tools/presets.d.ts +124 -0
  35. package/dist/default-tools/schemas.d.ts +121 -0
  36. package/dist/default-tools/types.d.ts +237 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +220 -0
  39. package/dist/input/file-indexer.d.ts +5 -0
  40. package/dist/input/index.d.ts +4 -0
  41. package/dist/input/mention-enricher.d.ts +12 -0
  42. package/dist/mcp/config-loader.d.ts +15 -0
  43. package/dist/mcp/index.d.ts +4 -0
  44. package/dist/mcp/manager.d.ts +24 -0
  45. package/dist/mcp/types.d.ts +66 -0
  46. package/dist/runtime/hook-file-hooks.d.ts +18 -0
  47. package/dist/runtime/rules.d.ts +5 -0
  48. package/dist/runtime/runtime-builder.d.ts +5 -0
  49. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
  50. package/dist/runtime/session-runtime.d.ts +36 -0
  51. package/dist/runtime/tool-approval.d.ts +9 -0
  52. package/dist/runtime/workflows.d.ts +13 -0
  53. package/dist/server/index.d.ts +47 -0
  54. package/dist/server/index.js +641 -0
  55. package/dist/session/default-session-manager.d.ts +77 -0
  56. package/dist/session/rpc-session-service.d.ts +12 -0
  57. package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
  58. package/dist/session/session-artifacts.d.ts +19 -0
  59. package/dist/session/session-graph.d.ts +15 -0
  60. package/dist/session/session-host.d.ts +21 -0
  61. package/dist/session/session-manager.d.ts +50 -0
  62. package/dist/session/session-manifest.d.ts +30 -0
  63. package/dist/session/session-service.d.ts +113 -0
  64. package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
  65. package/dist/session/unified-session-persistence-service.d.ts +93 -0
  66. package/dist/session/workspace-manager.d.ts +28 -0
  67. package/dist/session/workspace-manifest.d.ts +25 -0
  68. package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
  69. package/dist/storage/provider-settings-manager.d.ts +20 -0
  70. package/dist/storage/sqlite-session-store.d.ts +29 -0
  71. package/dist/storage/sqlite-team-store.d.ts +31 -0
  72. package/dist/storage/team-store.d.ts +2 -0
  73. package/dist/team/index.d.ts +1 -0
  74. package/dist/team/projections.d.ts +8 -0
  75. package/dist/types/common.d.ts +10 -0
  76. package/dist/types/config.d.ts +37 -0
  77. package/dist/types/events.d.ts +54 -0
  78. package/dist/types/provider-settings.d.ts +20 -0
  79. package/dist/types/sessions.d.ts +9 -0
  80. package/dist/types/storage.d.ts +37 -0
  81. package/dist/types/workspace.d.ts +7 -0
  82. package/dist/types.d.ts +26 -0
  83. package/package.json +63 -0
  84. package/src/account/cline-account-service.test.ts +101 -0
  85. package/src/account/cline-account-service.ts +267 -0
  86. package/src/account/index.ts +20 -0
  87. package/src/account/rpc.test.ts +62 -0
  88. package/src/account/rpc.ts +172 -0
  89. package/src/account/types.ts +80 -0
  90. package/src/agents/agent-config-loader.test.ts +234 -0
  91. package/src/agents/agent-config-loader.ts +107 -0
  92. package/src/agents/agent-config-parser.ts +191 -0
  93. package/src/agents/hooks-config-loader.ts +97 -0
  94. package/src/agents/index.ts +84 -0
  95. package/src/agents/plugin-config-loader.test.ts +91 -0
  96. package/src/agents/plugin-config-loader.ts +160 -0
  97. package/src/agents/plugin-loader.test.ts +102 -0
  98. package/src/agents/plugin-loader.ts +105 -0
  99. package/src/agents/plugin-sandbox.test.ts +120 -0
  100. package/src/agents/plugin-sandbox.ts +471 -0
  101. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  102. package/src/agents/unified-config-file-watcher.ts +483 -0
  103. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  104. package/src/agents/user-instruction-config-loader.ts +438 -0
  105. package/src/auth/client.test.ts +40 -0
  106. package/src/auth/client.ts +25 -0
  107. package/src/auth/cline.test.ts +130 -0
  108. package/src/auth/cline.ts +414 -0
  109. package/src/auth/codex.test.ts +170 -0
  110. package/src/auth/codex.ts +466 -0
  111. package/src/auth/oca.test.ts +215 -0
  112. package/src/auth/oca.ts +546 -0
  113. package/src/auth/server.ts +216 -0
  114. package/src/auth/types.ts +78 -0
  115. package/src/auth/utils.test.ts +128 -0
  116. package/src/auth/utils.ts +247 -0
  117. package/src/chat/chat-schema.ts +82 -0
  118. package/src/default-tools/constants.ts +35 -0
  119. package/src/default-tools/definitions.test.ts +233 -0
  120. package/src/default-tools/definitions.ts +632 -0
  121. package/src/default-tools/executors/apply-patch-parser.ts +520 -0
  122. package/src/default-tools/executors/apply-patch.ts +359 -0
  123. package/src/default-tools/executors/bash.ts +205 -0
  124. package/src/default-tools/executors/editor.ts +231 -0
  125. package/src/default-tools/executors/file-read.test.ts +25 -0
  126. package/src/default-tools/executors/file-read.ts +94 -0
  127. package/src/default-tools/executors/index.ts +75 -0
  128. package/src/default-tools/executors/search.ts +278 -0
  129. package/src/default-tools/executors/web-fetch.ts +259 -0
  130. package/src/default-tools/index.ts +161 -0
  131. package/src/default-tools/presets.test.ts +63 -0
  132. package/src/default-tools/presets.ts +168 -0
  133. package/src/default-tools/schemas.ts +228 -0
  134. package/src/default-tools/types.ts +324 -0
  135. package/src/index.ts +119 -0
  136. package/src/input/file-indexer.d.ts +11 -0
  137. package/src/input/file-indexer.test.ts +87 -0
  138. package/src/input/file-indexer.ts +280 -0
  139. package/src/input/index.ts +7 -0
  140. package/src/input/mention-enricher.test.ts +82 -0
  141. package/src/input/mention-enricher.ts +119 -0
  142. package/src/mcp/config-loader.test.ts +238 -0
  143. package/src/mcp/config-loader.ts +219 -0
  144. package/src/mcp/index.ts +26 -0
  145. package/src/mcp/manager.test.ts +106 -0
  146. package/src/mcp/manager.ts +262 -0
  147. package/src/mcp/types.ts +88 -0
  148. package/src/runtime/hook-file-hooks.test.ts +106 -0
  149. package/src/runtime/hook-file-hooks.ts +736 -0
  150. package/src/runtime/index.ts +27 -0
  151. package/src/runtime/rules.ts +34 -0
  152. package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
  153. package/src/runtime/runtime-builder.test.ts +215 -0
  154. package/src/runtime/runtime-builder.ts +515 -0
  155. package/src/runtime/runtime-parity.test.ts +132 -0
  156. package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
  157. package/src/runtime/session-runtime.ts +44 -0
  158. package/src/runtime/tool-approval.ts +104 -0
  159. package/src/runtime/workflows.test.ts +119 -0
  160. package/src/runtime/workflows.ts +54 -0
  161. package/src/server/index.ts +282 -0
  162. package/src/session/default-session-manager.e2e.test.ts +354 -0
  163. package/src/session/default-session-manager.test.ts +816 -0
  164. package/src/session/default-session-manager.ts +1286 -0
  165. package/src/session/index.ts +37 -0
  166. package/src/session/rpc-session-service.ts +189 -0
  167. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  168. package/src/session/runtime-oauth-token-manager.ts +265 -0
  169. package/src/session/session-artifacts.ts +106 -0
  170. package/src/session/session-graph.ts +90 -0
  171. package/src/session/session-host.ts +190 -0
  172. package/src/session/session-manager.ts +56 -0
  173. package/src/session/session-manifest.ts +29 -0
  174. package/src/session/session-service.team-persistence.test.ts +48 -0
  175. package/src/session/session-service.ts +610 -0
  176. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  177. package/src/session/unified-session-persistence-service.ts +781 -0
  178. package/src/session/workspace-manager.ts +98 -0
  179. package/src/session/workspace-manifest.ts +100 -0
  180. package/src/storage/artifact-store.ts +1 -0
  181. package/src/storage/index.ts +11 -0
  182. package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
  183. package/src/storage/provider-settings-legacy-migration.ts +637 -0
  184. package/src/storage/provider-settings-manager.test.ts +111 -0
  185. package/src/storage/provider-settings-manager.ts +129 -0
  186. package/src/storage/session-store.ts +1 -0
  187. package/src/storage/sqlite-session-store.ts +270 -0
  188. package/src/storage/sqlite-team-store.ts +443 -0
  189. package/src/storage/team-store.ts +5 -0
  190. package/src/team/index.ts +4 -0
  191. package/src/team/projections.ts +285 -0
  192. package/src/types/common.ts +14 -0
  193. package/src/types/config.ts +64 -0
  194. package/src/types/events.ts +46 -0
  195. package/src/types/index.ts +24 -0
  196. package/src/types/provider-settings.ts +43 -0
  197. package/src/types/sessions.ts +16 -0
  198. package/src/types/storage.ts +64 -0
  199. package/src/types/workspace.ts +7 -0
  200. package/src/types.ts +127 -0
@@ -0,0 +1,816 @@
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 { describe, expect, it, vi } from "vitest";
6
+ import { SessionSource } from "../types/common";
7
+ import type { CoreSessionConfig } from "../types/config";
8
+ import { DefaultSessionManager } from "./default-session-manager";
9
+ import type { SessionManifest } from "./session-manifest";
10
+
11
+ const distinctId = "test-machine-id";
12
+
13
+ function createResult(overrides: Partial<AgentResult> = {}): AgentResult {
14
+ return {
15
+ text: "ok",
16
+ iterations: 1,
17
+ finishReason: "completed",
18
+ usage: {
19
+ inputTokens: 1,
20
+ outputTokens: 2,
21
+ totalCost: 0,
22
+ },
23
+ messages: [],
24
+ toolCalls: [],
25
+ durationMs: 1,
26
+ model: {
27
+ id: "mock-model",
28
+ provider: "mock-provider",
29
+ },
30
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
31
+ endedAt: new Date("2026-01-01T00:00:01.000Z"),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function createManifest(sessionId: string): SessionManifest {
37
+ return {
38
+ version: 1,
39
+ session_id: sessionId,
40
+ source: SessionSource.CLI,
41
+ pid: process.pid,
42
+ started_at: "2026-01-01T00:00:00.000Z",
43
+ status: "running",
44
+ interactive: false,
45
+ provider: "mock-provider",
46
+ model: "mock-model",
47
+ cwd: "/tmp/project",
48
+ workspace_root: "/tmp/project",
49
+ enable_tools: true,
50
+ enable_spawn: true,
51
+ enable_teams: true,
52
+ prompt: "hello",
53
+ messages_path: "/tmp/messages.json",
54
+ };
55
+ }
56
+
57
+ function createConfig(
58
+ overrides: Partial<CoreSessionConfig> = {},
59
+ ): CoreSessionConfig {
60
+ return {
61
+ providerId: "mock-provider",
62
+ modelId: "mock-model",
63
+ cwd: "/tmp/project",
64
+ systemPrompt: "You are a test agent",
65
+ enableTools: true,
66
+ enableSpawnAgent: true,
67
+ enableAgentTeams: true,
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ describe("DefaultSessionManager", () => {
73
+ it("runs a non-interactive prompt and persists messages/status", async () => {
74
+ const sessionId = "sess-1";
75
+ const manifest = createManifest(sessionId);
76
+ const createRootSessionWithArtifacts = vi.fn().mockResolvedValue({
77
+ manifestPath: "/tmp/manifest.json",
78
+ transcriptPath: "/tmp/transcript.log",
79
+ hookPath: "/tmp/hook.log",
80
+ messagesPath: "/tmp/messages.json",
81
+ manifest,
82
+ });
83
+ const persistSessionMessages = vi.fn();
84
+ const updateSessionStatus = vi.fn().mockResolvedValue({
85
+ updated: true,
86
+ endedAt: "2026-01-01T00:00:05.000Z",
87
+ });
88
+ const writeSessionManifest = vi.fn();
89
+ const listSessions = vi.fn().mockResolvedValue([]);
90
+ const deleteSession = vi.fn().mockResolvedValue({ deleted: true });
91
+ const sessionService = {
92
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
93
+ createRootSessionWithArtifacts,
94
+ persistSessionMessages,
95
+ updateSessionStatus,
96
+ writeSessionManifest,
97
+ listSessions,
98
+ deleteSession,
99
+ };
100
+
101
+ const shutdown = vi.fn();
102
+ const runtimeBuilder = {
103
+ build: vi.fn().mockReturnValue({
104
+ tools: [],
105
+ shutdown,
106
+ }),
107
+ };
108
+ const run = vi.fn().mockResolvedValue(
109
+ createResult({
110
+ messages: [
111
+ { role: "user", content: [{ type: "text", text: "hello" }] },
112
+ ],
113
+ }),
114
+ );
115
+ const continueFn = vi.fn();
116
+ const agent = {
117
+ run,
118
+ continue: continueFn,
119
+ abort: vi.fn(),
120
+ shutdown: vi.fn().mockResolvedValue(undefined),
121
+ getMessages: vi.fn().mockReturnValue([]),
122
+ messages: [],
123
+ };
124
+
125
+ const manager = new DefaultSessionManager({
126
+ distinctId,
127
+ sessionService: sessionService as never,
128
+ runtimeBuilder,
129
+ createAgent: () => agent as never,
130
+ });
131
+
132
+ const started = await manager.start({
133
+ config: createConfig({ sessionId }),
134
+ prompt: "hello",
135
+ interactive: false,
136
+ });
137
+
138
+ expect(started.sessionId).toBe(sessionId);
139
+ expect(started.result?.finishReason).toBe("completed");
140
+ expect(run).toHaveBeenCalledTimes(1);
141
+ expect(continueFn).not.toHaveBeenCalled();
142
+ expect(persistSessionMessages).toHaveBeenCalledTimes(1);
143
+ expect(updateSessionStatus).toHaveBeenCalledWith(sessionId, "completed", 0);
144
+ expect(writeSessionManifest).toHaveBeenCalledTimes(1);
145
+ expect(shutdown).toHaveBeenCalledTimes(1);
146
+ });
147
+
148
+ it("persists assistant message metadata for usage and model identity", async () => {
149
+ const sessionId = "sess-meta";
150
+ const manifest = createManifest(sessionId);
151
+ const persistSessionMessages = vi.fn();
152
+ const sessionService = {
153
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
154
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
155
+ manifestPath: "/tmp/manifest-meta.json",
156
+ transcriptPath: "/tmp/transcript-meta.log",
157
+ hookPath: "/tmp/hook-meta.log",
158
+ messagesPath: "/tmp/messages-meta.json",
159
+ manifest,
160
+ }),
161
+ persistSessionMessages,
162
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
163
+ writeSessionManifest: vi.fn(),
164
+ listSessions: vi.fn().mockResolvedValue([]),
165
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
166
+ };
167
+ const runtimeBuilder = {
168
+ build: vi.fn().mockReturnValue({
169
+ tools: [],
170
+ shutdown: vi.fn(),
171
+ }),
172
+ };
173
+ const run = vi.fn().mockResolvedValue(
174
+ createResult({
175
+ usage: {
176
+ inputTokens: 33,
177
+ outputTokens: 12,
178
+ cacheReadTokens: 4,
179
+ cacheWriteTokens: 1,
180
+ totalCost: 0.42,
181
+ },
182
+ model: {
183
+ id: "claude-sonnet-4-6",
184
+ provider: "anthropic",
185
+ },
186
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
187
+ messages: [
188
+ { role: "user", content: [{ type: "text", text: "hello" }] },
189
+ { role: "assistant", content: [{ type: "text", text: "world" }] },
190
+ ],
191
+ }),
192
+ );
193
+ const manager = new DefaultSessionManager({
194
+ distinctId,
195
+ sessionService: sessionService as never,
196
+ runtimeBuilder,
197
+ createAgent: () =>
198
+ ({
199
+ run,
200
+ continue: vi.fn(),
201
+ abort: vi.fn(),
202
+ shutdown: vi.fn().mockResolvedValue(undefined),
203
+ getMessages: vi.fn().mockReturnValue([]),
204
+ messages: [],
205
+ }) as never,
206
+ });
207
+
208
+ await manager.start({
209
+ config: createConfig({
210
+ sessionId,
211
+ providerId: "anthropic",
212
+ modelId: "claude-sonnet-4-6",
213
+ }),
214
+ prompt: "hello",
215
+ interactive: false,
216
+ });
217
+
218
+ expect(persistSessionMessages).toHaveBeenCalledTimes(1);
219
+ const persisted = persistSessionMessages.mock.calls[0]?.[1];
220
+ expect(Array.isArray(persisted)).toBe(true);
221
+ expect(persisted?.[1]).toMatchObject({
222
+ role: "assistant",
223
+ providerId: "anthropic",
224
+ modelId: "claude-sonnet-4-6",
225
+ modelInfo: {
226
+ id: "claude-sonnet-4-6",
227
+ provider: "anthropic",
228
+ },
229
+ metrics: {
230
+ inputTokens: 33,
231
+ outputTokens: 12,
232
+ cacheReadTokens: 4,
233
+ cacheWriteTokens: 1,
234
+ cost: 0.42,
235
+ },
236
+ ts: new Date("2026-01-01T00:00:02.000Z").getTime(),
237
+ });
238
+ });
239
+
240
+ it("uses run for first send then continue for subsequent sends", async () => {
241
+ const sessionId = "sess-2";
242
+ const manifest = createManifest(sessionId);
243
+ const sessionService = {
244
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
245
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
246
+ manifestPath: "/tmp/manifest-2.json",
247
+ transcriptPath: "/tmp/transcript-2.log",
248
+ hookPath: "/tmp/hook-2.log",
249
+ messagesPath: "/tmp/messages-2.json",
250
+ manifest,
251
+ }),
252
+ persistSessionMessages: vi.fn(),
253
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
254
+ writeSessionManifest: vi.fn(),
255
+ listSessions: vi.fn().mockResolvedValue([]),
256
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
257
+ };
258
+ const runtimeBuilder = {
259
+ build: vi.fn().mockReturnValue({
260
+ tools: [],
261
+ shutdown: vi.fn(),
262
+ }),
263
+ };
264
+ const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
265
+ const continueFn = vi
266
+ .fn()
267
+ .mockResolvedValue(createResult({ text: "second" }));
268
+ const manager = new DefaultSessionManager({
269
+ distinctId,
270
+ sessionService: sessionService as never,
271
+ runtimeBuilder,
272
+ createAgent: () =>
273
+ ({
274
+ run,
275
+ continue: continueFn,
276
+ abort: vi.fn(),
277
+ shutdown: vi.fn().mockResolvedValue(undefined),
278
+ getMessages: vi.fn().mockReturnValue([]),
279
+ messages: [],
280
+ }) as never,
281
+ });
282
+
283
+ await manager.start({
284
+ config: createConfig({ sessionId }),
285
+ interactive: true,
286
+ });
287
+ const first = await manager.send({ sessionId, prompt: "first" });
288
+ const second = await manager.send({ sessionId, prompt: "second" });
289
+
290
+ expect(first?.text).toBe("first");
291
+ expect(second?.text).toBe("second");
292
+ expect(run).toHaveBeenCalledTimes(1);
293
+ expect(continueFn).toHaveBeenCalledTimes(1);
294
+ expect(sessionService.persistSessionMessages).toHaveBeenCalledTimes(2);
295
+ });
296
+
297
+ it("marks a failed single-run session as failed when run throws", async () => {
298
+ const sessionId = "sess-fail";
299
+ const manifest = createManifest(sessionId);
300
+ const sessionService = {
301
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
302
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
303
+ manifestPath: "/tmp/manifest-fail.json",
304
+ transcriptPath: "/tmp/transcript-fail.log",
305
+ hookPath: "/tmp/hook-fail.log",
306
+ messagesPath: "/tmp/messages-fail.json",
307
+ manifest,
308
+ }),
309
+ persistSessionMessages: vi.fn(),
310
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
311
+ writeSessionManifest: vi.fn(),
312
+ listSessions: vi.fn().mockResolvedValue([]),
313
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
314
+ };
315
+ const runtimeShutdown = vi.fn();
316
+ const runtimeBuilder = {
317
+ build: vi.fn().mockReturnValue({
318
+ tools: [],
319
+ shutdown: runtimeShutdown,
320
+ }),
321
+ };
322
+ const run = vi.fn().mockRejectedValue(new Error("run failed"));
323
+ const agentShutdown = vi.fn().mockResolvedValue(undefined);
324
+ const manager = new DefaultSessionManager({
325
+ distinctId,
326
+ sessionService: sessionService as never,
327
+ runtimeBuilder,
328
+ createAgent: () =>
329
+ ({
330
+ run,
331
+ continue: vi.fn(),
332
+ abort: vi.fn(),
333
+ shutdown: agentShutdown,
334
+ getMessages: vi.fn().mockReturnValue([]),
335
+ messages: [],
336
+ }) as never,
337
+ });
338
+
339
+ await expect(
340
+ manager.start({
341
+ config: createConfig({ sessionId }),
342
+ prompt: "hello",
343
+ interactive: false,
344
+ }),
345
+ ).rejects.toThrow("run failed");
346
+ expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
347
+ sessionId,
348
+ "failed",
349
+ 1,
350
+ );
351
+ expect(agentShutdown).toHaveBeenCalledTimes(1);
352
+ expect(runtimeShutdown).toHaveBeenCalledTimes(1);
353
+ });
354
+
355
+ it("does not persist or emit shutdown hooks when no prompt was submitted", async () => {
356
+ const sessionService = {
357
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
358
+ createRootSessionWithArtifacts: vi.fn(),
359
+ persistSessionMessages: vi.fn(),
360
+ updateSessionStatus: vi.fn(),
361
+ writeSessionManifest: vi.fn(),
362
+ listSessions: vi.fn().mockResolvedValue([]),
363
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
364
+ };
365
+ const runtimeShutdown = vi.fn();
366
+ const runtimeBuilder = {
367
+ build: vi.fn().mockReturnValue({
368
+ tools: [],
369
+ shutdown: runtimeShutdown,
370
+ }),
371
+ };
372
+ const agentShutdown = vi.fn().mockResolvedValue(undefined);
373
+ const manager = new DefaultSessionManager({
374
+ distinctId,
375
+ sessionService: sessionService as never,
376
+ runtimeBuilder,
377
+ createAgent: () =>
378
+ ({
379
+ run: vi.fn(),
380
+ continue: vi.fn(),
381
+ abort: vi.fn(),
382
+ shutdown: agentShutdown,
383
+ getMessages: vi.fn().mockReturnValue([]),
384
+ messages: [],
385
+ }) as never,
386
+ });
387
+
388
+ const started = await manager.start({
389
+ config: createConfig({ sessionId: "sess-no-prompt" }),
390
+ interactive: true,
391
+ });
392
+ await manager.stop(started.sessionId);
393
+
394
+ expect(
395
+ sessionService.createRootSessionWithArtifacts,
396
+ ).not.toHaveBeenCalled();
397
+ expect(sessionService.updateSessionStatus).not.toHaveBeenCalled();
398
+ expect(agentShutdown).not.toHaveBeenCalled();
399
+ expect(runtimeShutdown).toHaveBeenCalledTimes(1);
400
+ });
401
+
402
+ it("updates agent connection with refreshed OAuth key before turn", async () => {
403
+ const sessionId = "sess-oauth";
404
+ const manifest = createManifest(sessionId);
405
+ const sessionService = {
406
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
407
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
408
+ manifestPath: "/tmp/manifest-oauth.json",
409
+ transcriptPath: "/tmp/transcript-oauth.log",
410
+ hookPath: "/tmp/hook-oauth.log",
411
+ messagesPath: "/tmp/messages-oauth.json",
412
+ manifest,
413
+ }),
414
+ persistSessionMessages: vi.fn(),
415
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
416
+ writeSessionManifest: vi.fn(),
417
+ listSessions: vi.fn().mockResolvedValue([]),
418
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
419
+ };
420
+ const runtimeBuilder = {
421
+ build: vi.fn().mockReturnValue({
422
+ tools: [],
423
+ shutdown: vi.fn(),
424
+ }),
425
+ };
426
+ const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
427
+ const updateConnection = vi.fn();
428
+ const manager = new DefaultSessionManager({
429
+ distinctId,
430
+ sessionService: sessionService as never,
431
+ runtimeBuilder,
432
+ oauthTokenManager: {
433
+ resolveProviderApiKey: vi.fn().mockResolvedValue({
434
+ providerId: "openai-codex",
435
+ apiKey: "oauth-access-new",
436
+ refreshed: true,
437
+ }),
438
+ } as never,
439
+ createAgent: () =>
440
+ ({
441
+ run,
442
+ continue: vi.fn(),
443
+ abort: vi.fn(),
444
+ restore: vi.fn(),
445
+ updateConnection,
446
+ shutdown: vi.fn().mockResolvedValue(undefined),
447
+ getMessages: vi.fn().mockReturnValue([]),
448
+ messages: [],
449
+ }) as never,
450
+ });
451
+
452
+ await manager.start({
453
+ config: createConfig({
454
+ sessionId,
455
+ providerId: "openai-codex",
456
+ apiKey: "oauth-access-old",
457
+ }),
458
+ interactive: true,
459
+ });
460
+ await manager.send({ sessionId, prompt: "hello" });
461
+
462
+ expect(updateConnection).toHaveBeenCalledWith({
463
+ apiKey: "oauth-access-new",
464
+ });
465
+ expect(run).toHaveBeenCalledTimes(1);
466
+ });
467
+
468
+ it("hydrates provider-specific config from provider settings", async () => {
469
+ const sessionId = "sess-provider-config";
470
+ const manifest = createManifest(sessionId);
471
+ const sessionService = {
472
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
473
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
474
+ manifestPath: "/tmp/manifest-provider-config.json",
475
+ transcriptPath: "/tmp/transcript-provider-config.log",
476
+ hookPath: "/tmp/hook-provider-config.log",
477
+ messagesPath: "/tmp/messages-provider-config.json",
478
+ manifest,
479
+ }),
480
+ persistSessionMessages: vi.fn(),
481
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
482
+ writeSessionManifest: vi.fn(),
483
+ listSessions: vi.fn().mockResolvedValue([]),
484
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
485
+ };
486
+ const run = vi.fn().mockResolvedValue(
487
+ createResult({
488
+ model: {
489
+ id: "claude-sonnet-4@20250514",
490
+ provider: "vertex",
491
+ },
492
+ }),
493
+ );
494
+ const createAgent = vi.fn().mockReturnValue({
495
+ run,
496
+ continue: vi.fn(),
497
+ abort: vi.fn(),
498
+ restore: vi.fn(),
499
+ shutdown: vi.fn().mockResolvedValue(undefined),
500
+ getMessages: vi.fn().mockReturnValue([]),
501
+ messages: [],
502
+ });
503
+ const manager = new DefaultSessionManager({
504
+ distinctId,
505
+ sessionService: sessionService as never,
506
+ runtimeBuilder: {
507
+ build: vi.fn().mockReturnValue({
508
+ tools: [],
509
+ shutdown: vi.fn(),
510
+ }),
511
+ },
512
+ createAgent: createAgent as never,
513
+ providerSettingsManager: {
514
+ getProviderSettings: vi.fn().mockReturnValue({
515
+ provider: "vertex",
516
+ gcp: {
517
+ projectId: "test-project",
518
+ region: "us-central1",
519
+ },
520
+ }),
521
+ } as never,
522
+ });
523
+
524
+ await manager.start({
525
+ config: createConfig({
526
+ sessionId,
527
+ providerId: "vertex",
528
+ modelId: "claude-sonnet-4@20250514",
529
+ }),
530
+ interactive: true,
531
+ });
532
+ await manager.send({ sessionId, prompt: "hello" });
533
+
534
+ expect(createAgent).toHaveBeenCalledWith(
535
+ expect.objectContaining({
536
+ providerId: "vertex",
537
+ modelId: "claude-sonnet-4@20250514",
538
+ providerConfig: expect.objectContaining({
539
+ providerId: "vertex",
540
+ modelId: "claude-sonnet-4@20250514",
541
+ gcp: {
542
+ projectId: "test-project",
543
+ region: "us-central1",
544
+ },
545
+ }),
546
+ }),
547
+ );
548
+ });
549
+
550
+ it("formats prompt in core and merges explicit + mention user files", async () => {
551
+ const tempCwd = mkdtempSync(join(tmpdir(), "core-session-format-"));
552
+ try {
553
+ const srcDir = join(tempCwd, "src");
554
+ const docsDir = join(tempCwd, "docs");
555
+ mkdirSync(srcDir, { recursive: true });
556
+ mkdirSync(docsDir, { recursive: true });
557
+ const mentionPath = join(srcDir, "app.ts");
558
+ const explicitPath = join(docsDir, "note.md");
559
+ writeFileSync(mentionPath, "export const v = 1;\n", "utf8");
560
+ writeFileSync(explicitPath, "note\n", "utf8");
561
+
562
+ const sessionId = "sess-format";
563
+ const manifest = createManifest(sessionId);
564
+ const sessionService = {
565
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
566
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
567
+ manifestPath: "/tmp/manifest-format.json",
568
+ transcriptPath: "/tmp/transcript-format.log",
569
+ hookPath: "/tmp/hook-format.log",
570
+ messagesPath: "/tmp/messages-format.json",
571
+ manifest,
572
+ }),
573
+ persistSessionMessages: vi.fn(),
574
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
575
+ writeSessionManifest: vi.fn(),
576
+ listSessions: vi.fn().mockResolvedValue([]),
577
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
578
+ };
579
+ const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
580
+ const manager = new DefaultSessionManager({
581
+ distinctId,
582
+ sessionService: sessionService as never,
583
+ runtimeBuilder: {
584
+ build: vi.fn().mockReturnValue({
585
+ tools: [],
586
+ shutdown: vi.fn(),
587
+ }),
588
+ },
589
+ createAgent: () =>
590
+ ({
591
+ run,
592
+ continue: vi.fn(),
593
+ abort: vi.fn(),
594
+ shutdown: vi.fn().mockResolvedValue(undefined),
595
+ getMessages: vi.fn().mockReturnValue([]),
596
+ messages: [],
597
+ }) as never,
598
+ });
599
+
600
+ await manager.start({
601
+ config: createConfig({
602
+ sessionId,
603
+ cwd: join(tempCwd, "docs"),
604
+ workspaceRoot: tempCwd,
605
+ }),
606
+ interactive: true,
607
+ });
608
+ await manager.send({
609
+ sessionId,
610
+ prompt: '<user_input mode="act">explain @src/app.ts</user_input>',
611
+ userFiles: ["note.md"],
612
+ });
613
+
614
+ expect(run).toHaveBeenCalledWith(
615
+ '<user_input mode="act">explain @src/app.ts</user_input>',
616
+ undefined,
617
+ expect.arrayContaining([mentionPath, explicitPath]),
618
+ );
619
+ } finally {
620
+ rmSync(tempCwd, { recursive: true, force: true });
621
+ }
622
+ });
623
+
624
+ it("force refreshes and retries once when turn fails with auth error", async () => {
625
+ const sessionId = "sess-oauth-retry";
626
+ const manifest = createManifest(sessionId);
627
+ const sessionService = {
628
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
629
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
630
+ manifestPath: "/tmp/manifest-oauth-retry.json",
631
+ transcriptPath: "/tmp/transcript-oauth-retry.log",
632
+ hookPath: "/tmp/hook-oauth-retry.log",
633
+ messagesPath: "/tmp/messages-oauth-retry.json",
634
+ manifest,
635
+ }),
636
+ persistSessionMessages: vi.fn(),
637
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
638
+ writeSessionManifest: vi.fn(),
639
+ listSessions: vi.fn().mockResolvedValue([]),
640
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
641
+ };
642
+ const runtimeBuilder = {
643
+ build: vi.fn().mockReturnValue({
644
+ tools: [],
645
+ shutdown: vi.fn(),
646
+ }),
647
+ };
648
+ const run = vi
649
+ .fn()
650
+ .mockRejectedValueOnce(new Error("401 Unauthorized"))
651
+ .mockResolvedValueOnce(createResult({ text: "retried" }));
652
+ const restore = vi.fn();
653
+ const updateConnection = vi.fn();
654
+ const resolveProviderApiKey = vi
655
+ .fn()
656
+ .mockResolvedValueOnce(null)
657
+ .mockResolvedValueOnce({
658
+ providerId: "openai-codex",
659
+ apiKey: "oauth-access-new",
660
+ refreshed: true,
661
+ });
662
+ const manager = new DefaultSessionManager({
663
+ distinctId,
664
+ sessionService: sessionService as never,
665
+ runtimeBuilder,
666
+ oauthTokenManager: {
667
+ resolveProviderApiKey,
668
+ } as never,
669
+ createAgent: () =>
670
+ ({
671
+ run,
672
+ continue: vi.fn(),
673
+ abort: vi.fn(),
674
+ restore,
675
+ updateConnection,
676
+ shutdown: vi.fn().mockResolvedValue(undefined),
677
+ getMessages: vi.fn().mockReturnValue([]),
678
+ messages: [],
679
+ }) as never,
680
+ });
681
+
682
+ await manager.start({
683
+ config: createConfig({
684
+ sessionId,
685
+ providerId: "openai-codex",
686
+ apiKey: "oauth-access-old",
687
+ }),
688
+ interactive: true,
689
+ });
690
+ const result = await manager.send({ sessionId, prompt: "hello" });
691
+
692
+ expect(result?.text).toBe("retried");
693
+ expect(run).toHaveBeenCalledTimes(2);
694
+ expect(restore).toHaveBeenCalledTimes(1);
695
+ expect(resolveProviderApiKey).toHaveBeenNthCalledWith(1, {
696
+ providerId: "openai-codex",
697
+ forceRefresh: undefined,
698
+ });
699
+ expect(resolveProviderApiKey).toHaveBeenNthCalledWith(2, {
700
+ providerId: "openai-codex",
701
+ forceRefresh: true,
702
+ });
703
+ expect(updateConnection).toHaveBeenCalledWith({
704
+ apiKey: "oauth-access-new",
705
+ });
706
+ });
707
+
708
+ it("auto-continues when async teammate runs complete after lead turn", async () => {
709
+ const sessionId = "sess-team-auto-continue";
710
+ const manifest = createManifest(sessionId);
711
+ const sessionService = {
712
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
713
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
714
+ manifestPath: "/tmp/manifest-team-auto-continue.json",
715
+ transcriptPath: "/tmp/transcript-team-auto-continue.log",
716
+ hookPath: "/tmp/hook-team-auto-continue.log",
717
+ messagesPath: "/tmp/messages-team-auto-continue.json",
718
+ manifest,
719
+ }),
720
+ persistSessionMessages: vi.fn(),
721
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
722
+ writeSessionManifest: vi.fn(),
723
+ listSessions: vi.fn().mockResolvedValue([]),
724
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
725
+ };
726
+
727
+ let onTeamEvent: ((event: unknown) => void) | undefined;
728
+ const runtimeBuilder = {
729
+ build: vi
730
+ .fn()
731
+ .mockImplementation(
732
+ (input: { onTeamEvent?: (event: unknown) => void }) => {
733
+ onTeamEvent = input.onTeamEvent;
734
+ return {
735
+ tools: [],
736
+ shutdown: vi.fn(),
737
+ };
738
+ },
739
+ ),
740
+ };
741
+
742
+ const run = vi.fn().mockImplementation(async () => {
743
+ onTeamEvent?.({
744
+ type: "run_started",
745
+ run: {
746
+ id: "run_0001",
747
+ agentId: "investigator",
748
+ status: "running",
749
+ message: "Investigate",
750
+ priority: 0,
751
+ retryCount: 0,
752
+ maxRetries: 0,
753
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
754
+ },
755
+ });
756
+ setTimeout(() => {
757
+ onTeamEvent?.({
758
+ type: "run_completed",
759
+ run: {
760
+ id: "run_0001",
761
+ agentId: "investigator",
762
+ status: "completed",
763
+ message: "Investigate",
764
+ priority: 0,
765
+ retryCount: 0,
766
+ maxRetries: 0,
767
+ startedAt: new Date("2026-01-01T00:00:00.000Z"),
768
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
769
+ result: createResult({ iterations: 3 }),
770
+ },
771
+ });
772
+ }, 0);
773
+ return createResult({ text: "lead scheduled teammate" });
774
+ });
775
+ const continueFn = vi
776
+ .fn()
777
+ .mockResolvedValue(
778
+ createResult({ text: "lead processed teammate result" }),
779
+ );
780
+ const manager = new DefaultSessionManager({
781
+ distinctId,
782
+ sessionService: sessionService as never,
783
+ runtimeBuilder,
784
+ createAgent: () =>
785
+ ({
786
+ run,
787
+ continue: continueFn,
788
+ abort: vi.fn(),
789
+ shutdown: vi.fn().mockResolvedValue(undefined),
790
+ getMessages: vi.fn().mockReturnValue([]),
791
+ messages: [],
792
+ }) as never,
793
+ });
794
+
795
+ await manager.start({
796
+ config: createConfig({ sessionId }),
797
+ interactive: false,
798
+ });
799
+ const result = await manager.send({
800
+ sessionId,
801
+ prompt: "run teammate work",
802
+ });
803
+
804
+ expect(result?.text).toBe("lead processed teammate result");
805
+ expect(run).toHaveBeenCalledTimes(1);
806
+ expect(continueFn).toHaveBeenCalledTimes(1);
807
+ expect(continueFn.mock.calls[0]?.[0]).toContain(
808
+ "System-delivered teammate async run updates:",
809
+ );
810
+ expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
811
+ sessionId,
812
+ "completed",
813
+ 0,
814
+ );
815
+ });
816
+ });