@clinebot/core 0.0.35 → 0.0.36

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 (335) hide show
  1. package/README.md +1 -2
  2. package/dist/ClineCore.d.ts +53 -39
  3. package/dist/ClineCore.d.ts.map +1 -1
  4. package/dist/account/index.d.ts +1 -1
  5. package/dist/account/index.d.ts.map +1 -1
  6. package/dist/account/rpc.d.ts +6 -6
  7. package/dist/account/rpc.d.ts.map +1 -1
  8. package/dist/cron/index.d.ts +6 -0
  9. package/dist/cron/index.d.ts.map +1 -0
  10. package/dist/cron/resource-limiter.d.ts +9 -0
  11. package/dist/cron/resource-limiter.d.ts.map +1 -0
  12. package/dist/cron/schedule-command-service.d.ts +10 -0
  13. package/dist/cron/schedule-command-service.d.ts.map +1 -0
  14. package/dist/cron/schedule-service.d.ts +100 -0
  15. package/dist/cron/schedule-service.d.ts.map +1 -0
  16. package/dist/cron/scheduler.d.ts +66 -0
  17. package/dist/cron/scheduler.d.ts.map +1 -0
  18. package/dist/cron/sqlite-schedule-store.d.ts +52 -0
  19. package/dist/cron/sqlite-schedule-store.d.ts.map +1 -0
  20. package/dist/extensions/config/agent-config-loader.d.ts +4 -3
  21. package/dist/extensions/config/agent-config-loader.d.ts.map +1 -1
  22. package/dist/extensions/config/runtime-commands.d.ts +1 -0
  23. package/dist/extensions/config/runtime-commands.d.ts.map +1 -1
  24. package/dist/extensions/config/user-instruction-config-loader.d.ts +1 -0
  25. package/dist/extensions/config/user-instruction-config-loader.d.ts.map +1 -1
  26. package/dist/extensions/context/agentic-compaction.d.ts +2 -2
  27. package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
  28. package/dist/extensions/context/compaction-shared.d.ts +5 -4
  29. package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
  30. package/dist/extensions/context/compaction.d.ts.map +1 -1
  31. package/dist/extensions/plugin/plugin-config-loader.d.ts +9 -2
  32. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  33. package/dist/extensions/plugin/plugin-loader.d.ts +5 -3
  34. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  35. package/dist/extensions/plugin/plugin-module-import.d.ts.map +1 -1
  36. package/dist/extensions/plugin/plugin-sandbox.d.ts +15 -2
  37. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  38. package/dist/extensions/plugin/plugin-targeting.d.ts +7 -0
  39. package/dist/extensions/plugin/plugin-targeting.d.ts.map +1 -0
  40. package/dist/extensions/plugin-sandbox-bootstrap.js +211 -211
  41. package/dist/extensions/tools/definitions.d.ts +1 -1
  42. package/dist/extensions/tools/definitions.d.ts.map +1 -1
  43. package/dist/extensions/tools/executors/apply-patch.d.ts +3 -1
  44. package/dist/extensions/tools/executors/apply-patch.d.ts.map +1 -1
  45. package/dist/extensions/tools/executors/search.d.ts +1 -1
  46. package/dist/extensions/tools/executors/search.d.ts.map +1 -1
  47. package/dist/extensions/tools/index.d.ts +2 -0
  48. package/dist/extensions/tools/index.d.ts.map +1 -1
  49. package/dist/extensions/tools/presets.d.ts +26 -43
  50. package/dist/extensions/tools/presets.d.ts.map +1 -1
  51. package/dist/extensions/tools/runtime.d.ts +25 -0
  52. package/dist/extensions/tools/runtime.d.ts.map +1 -0
  53. package/dist/extensions/tools/schemas.d.ts.map +1 -1
  54. package/dist/extensions/tools/team/team-tools.d.ts +1 -0
  55. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
  56. package/dist/hooks/hook-file-hooks.d.ts +4 -1
  57. package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
  58. package/dist/hooks/index.d.ts +0 -1
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/subprocess.d.ts +8 -1
  61. package/dist/hooks/subprocess.d.ts.map +1 -1
  62. package/dist/hub/browser-websocket.d.ts +18 -0
  63. package/dist/hub/browser-websocket.d.ts.map +1 -0
  64. package/dist/hub/client.d.ts +45 -0
  65. package/dist/hub/client.d.ts.map +1 -0
  66. package/dist/hub/connect.d.ts +15 -0
  67. package/dist/hub/connect.d.ts.map +1 -0
  68. package/dist/hub/daemon-entry.d.ts +2 -0
  69. package/dist/hub/daemon-entry.d.ts.map +1 -0
  70. package/dist/hub/daemon-entry.js +1045 -0
  71. package/dist/hub/daemon.d.ts +5 -0
  72. package/dist/hub/daemon.d.ts.map +1 -0
  73. package/dist/hub/defaults.d.ts +13 -0
  74. package/dist/hub/defaults.d.ts.map +1 -0
  75. package/dist/hub/discovery.d.ts +29 -0
  76. package/dist/hub/discovery.d.ts.map +1 -0
  77. package/dist/hub/index.d.ts +15 -0
  78. package/dist/hub/index.d.ts.map +1 -0
  79. package/dist/hub/index.js +1044 -0
  80. package/dist/hub/native-transport.d.ts +17 -0
  81. package/dist/hub/native-transport.d.ts.map +1 -0
  82. package/dist/hub/runtime-handlers.d.ts +11 -0
  83. package/dist/hub/runtime-handlers.d.ts.map +1 -0
  84. package/dist/hub/server.d.ts +86 -0
  85. package/dist/hub/server.d.ts.map +1 -0
  86. package/dist/hub/session-client.d.ts +87 -0
  87. package/dist/hub/session-client.d.ts.map +1 -0
  88. package/dist/hub/start-shared-server.d.ts +19 -0
  89. package/dist/hub/start-shared-server.d.ts.map +1 -0
  90. package/dist/hub/transport.d.ts +8 -0
  91. package/dist/hub/transport.d.ts.map +1 -0
  92. package/dist/hub/ui-client.d.ts +44 -0
  93. package/dist/hub/ui-client.d.ts.map +1 -0
  94. package/dist/hub/workspace.d.ts +4 -0
  95. package/dist/hub/workspace.d.ts.map +1 -0
  96. package/dist/index.d.ts +26 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +498 -476
  99. package/dist/llms/configured-provider-registry.d.ts +28 -0
  100. package/dist/llms/configured-provider-registry.d.ts.map +1 -0
  101. package/dist/llms/provider-defaults.d.ts +27 -0
  102. package/dist/llms/provider-defaults.d.ts.map +1 -0
  103. package/dist/llms/provider-settings.d.ts +202 -0
  104. package/dist/llms/provider-settings.d.ts.map +1 -0
  105. package/dist/llms/runtime-config.d.ts +4 -0
  106. package/dist/llms/runtime-config.d.ts.map +1 -0
  107. package/dist/llms/runtime-registry.d.ts +20 -0
  108. package/dist/llms/runtime-registry.d.ts.map +1 -0
  109. package/dist/llms/runtime-types.d.ts +85 -0
  110. package/dist/llms/runtime-types.d.ts.map +1 -0
  111. package/dist/runtime/host.d.ts +1 -2
  112. package/dist/runtime/host.d.ts.map +1 -1
  113. package/dist/runtime/rules.d.ts +1 -0
  114. package/dist/runtime/rules.d.ts.map +1 -1
  115. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  116. package/dist/runtime/runtime-host.d.ts +22 -24
  117. package/dist/runtime/runtime-host.d.ts.map +1 -1
  118. package/dist/runtime/runtime-oauth-token-manager.d.ts.map +1 -1
  119. package/dist/runtime/session-runtime.d.ts +1 -19
  120. package/dist/runtime/session-runtime.d.ts.map +1 -1
  121. package/dist/services/global-settings.d.ts +12 -0
  122. package/dist/services/global-settings.d.ts.map +1 -0
  123. package/dist/services/local-runtime-bootstrap.d.ts +9 -3
  124. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
  125. package/dist/services/plugin-tools.d.ts +16 -0
  126. package/dist/services/plugin-tools.d.ts.map +1 -0
  127. package/dist/services/providers/local-provider-registry.d.ts +4 -4
  128. package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
  129. package/dist/services/providers/local-provider-service.d.ts +13 -13
  130. package/dist/services/providers/local-provider-service.d.ts.map +1 -1
  131. package/dist/services/session-data.d.ts +1 -1
  132. package/dist/services/session-data.d.ts.map +1 -1
  133. package/dist/services/storage/provider-settings-legacy-migration.d.ts +1 -1
  134. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  135. package/dist/services/telemetry/index.js +28 -15
  136. package/dist/services/workspace-manifest.d.ts +11 -0
  137. package/dist/services/workspace-manifest.d.ts.map +1 -1
  138. package/dist/session/persistence-service.d.ts +11 -23
  139. package/dist/session/persistence-service.d.ts.map +1 -1
  140. package/dist/session/session-manifest-store.d.ts +22 -0
  141. package/dist/session/session-manifest-store.d.ts.map +1 -0
  142. package/dist/session/session-row.d.ts +93 -0
  143. package/dist/session/session-row.d.ts.map +1 -0
  144. package/dist/session/session-service.d.ts +2 -102
  145. package/dist/session/session-service.d.ts.map +1 -1
  146. package/dist/session/subagent-session-manager.d.ts +36 -0
  147. package/dist/session/subagent-session-manager.d.ts.map +1 -0
  148. package/dist/session/team-persistence-store.d.ts +24 -0
  149. package/dist/session/team-persistence-store.d.ts.map +1 -0
  150. package/dist/transports/hub.d.ts +47 -0
  151. package/dist/transports/hub.d.ts.map +1 -0
  152. package/dist/transports/local.d.ts +10 -6
  153. package/dist/transports/local.d.ts.map +1 -1
  154. package/dist/transports/remote.d.ts +10 -0
  155. package/dist/transports/remote.d.ts.map +1 -0
  156. package/dist/transports/runtime-host-support.d.ts +3 -2
  157. package/dist/transports/runtime-host-support.d.ts.map +1 -1
  158. package/dist/types/chat-schema.d.ts +10 -12
  159. package/dist/types/chat-schema.d.ts.map +1 -1
  160. package/dist/types/config.d.ts +8 -7
  161. package/dist/types/config.d.ts.map +1 -1
  162. package/dist/types/provider-settings.d.ts +4 -5
  163. package/dist/types/provider-settings.d.ts.map +1 -1
  164. package/dist/types/session.d.ts +2 -1
  165. package/dist/types/session.d.ts.map +1 -1
  166. package/dist/types.d.ts +8 -1
  167. package/dist/types.d.ts.map +1 -1
  168. package/package.json +20 -6
  169. package/src/ClineCore.ts +68 -40
  170. package/src/account/index.ts +3 -3
  171. package/src/account/rpc.ts +12 -12
  172. package/src/cron/index.ts +5 -0
  173. package/src/cron/resource-limiter.ts +46 -0
  174. package/src/cron/schedule-command-service.ts +193 -0
  175. package/src/cron/schedule-service.ts +703 -0
  176. package/src/cron/scheduler.ts +637 -0
  177. package/src/cron/sqlite-schedule-store.ts +708 -0
  178. package/src/extensions/config/agent-config-loader.ts +17 -7
  179. package/src/extensions/config/runtime-commands.ts +6 -0
  180. package/src/extensions/config/user-instruction-config-loader.ts +1 -0
  181. package/src/extensions/context/agentic-compaction.ts +3 -3
  182. package/src/extensions/context/basic-compaction.ts +2 -2
  183. package/src/extensions/context/compaction-shared.ts +5 -4
  184. package/src/extensions/context/compaction.ts +3 -3
  185. package/src/extensions/plugin/plugin-config-loader.ts +17 -2
  186. package/src/extensions/plugin/plugin-loader.ts +48 -4
  187. package/src/extensions/plugin/plugin-module-import.ts +0 -2
  188. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +93 -39
  189. package/src/extensions/plugin/plugin-sandbox.ts +47 -27
  190. package/src/extensions/plugin/plugin-targeting.ts +32 -0
  191. package/src/extensions/tools/definitions.ts +30 -49
  192. package/src/extensions/tools/executors/apply-patch.ts +69 -80
  193. package/src/extensions/tools/executors/search.ts +195 -3
  194. package/src/extensions/tools/index.ts +10 -0
  195. package/src/extensions/tools/presets.ts +31 -46
  196. package/src/extensions/tools/runtime.ts +261 -0
  197. package/src/extensions/tools/schemas.ts +4 -2
  198. package/src/extensions/tools/team/team-tools.ts +21 -0
  199. package/src/hooks/hook-file-hooks.ts +8 -2
  200. package/src/hooks/index.ts +0 -7
  201. package/src/hooks/subprocess-runner.ts +1 -1
  202. package/src/hooks/subprocess.ts +9 -0
  203. package/src/hub/browser-websocket.ts +137 -0
  204. package/src/hub/client.ts +574 -0
  205. package/src/hub/connect.ts +156 -0
  206. package/src/hub/daemon-entry.ts +87 -0
  207. package/src/hub/daemon.ts +181 -0
  208. package/src/hub/defaults.ts +43 -0
  209. package/src/hub/discovery.ts +247 -0
  210. package/src/hub/index.ts +14 -0
  211. package/src/hub/native-transport.ts +31 -0
  212. package/src/hub/runtime-handlers.ts +140 -0
  213. package/src/hub/server.ts +1888 -0
  214. package/src/hub/session-client.ts +460 -0
  215. package/src/hub/start-shared-server.ts +58 -0
  216. package/src/hub/transport.ts +14 -0
  217. package/src/hub/ui-client.ts +122 -0
  218. package/src/hub/workspace.ts +19 -0
  219. package/src/index.ts +124 -68
  220. package/src/llms/configured-provider-registry.ts +193 -0
  221. package/src/llms/provider-defaults.ts +637 -0
  222. package/src/llms/provider-settings.ts +263 -0
  223. package/src/llms/runtime-config.ts +43 -0
  224. package/src/llms/runtime-registry.ts +171 -0
  225. package/src/llms/runtime-types.ts +121 -0
  226. package/src/runtime/host.ts +107 -269
  227. package/src/runtime/index.ts +1 -0
  228. package/src/runtime/rules.ts +12 -0
  229. package/src/runtime/runtime-builder.ts +24 -8
  230. package/src/runtime/runtime-host.ts +89 -61
  231. package/src/runtime/runtime-oauth-token-manager.ts +11 -15
  232. package/src/runtime/session-runtime.ts +0 -24
  233. package/src/services/global-settings.ts +122 -0
  234. package/src/services/local-runtime-bootstrap.ts +51 -13
  235. package/src/services/plugin-tools.ts +85 -0
  236. package/src/services/providers/local-provider-registry.ts +6 -6
  237. package/src/services/providers/local-provider-service.ts +42 -37
  238. package/src/services/session-data.ts +15 -9
  239. package/src/services/storage/provider-settings-legacy-migration.ts +6 -4
  240. package/src/services/storage/provider-settings-manager.ts +1 -1
  241. package/src/services/workspace-manifest.ts +18 -0
  242. package/src/session/file-session-service.ts +1 -1
  243. package/src/session/index.ts +6 -27
  244. package/src/session/persistence-service.ts +119 -504
  245. package/src/session/session-manifest-store.ts +158 -0
  246. package/src/session/session-row.ts +199 -0
  247. package/src/session/session-service.ts +17 -376
  248. package/src/session/session-team-coordination.ts +1 -1
  249. package/src/session/subagent-session-manager.ts +397 -0
  250. package/src/session/team-persistence-store.ts +176 -0
  251. package/src/transports/hub.ts +656 -0
  252. package/src/transports/local.ts +135 -40
  253. package/src/transports/remote.ts +26 -0
  254. package/src/transports/runtime-host-support.ts +63 -9
  255. package/src/types/chat-schema.ts +4 -5
  256. package/src/types/config.ts +8 -7
  257. package/src/types/provider-settings.ts +11 -7
  258. package/src/types/session.ts +2 -4
  259. package/src/types.ts +27 -1
  260. package/dist/hooks/persistent.d.ts +0 -64
  261. package/dist/hooks/persistent.d.ts.map +0 -1
  262. package/dist/runtime/rpc-runtime-ensure.d.ts +0 -65
  263. package/dist/runtime/rpc-runtime-ensure.d.ts.map +0 -1
  264. package/dist/runtime/rpc-spawn-lease.d.ts +0 -8
  265. package/dist/runtime/rpc-spawn-lease.d.ts.map +0 -1
  266. package/dist/session/rpc-session-service.d.ts +0 -16
  267. package/dist/session/rpc-session-service.d.ts.map +0 -1
  268. package/dist/session/sqlite-rpc-session-backend.d.ts +0 -31
  269. package/dist/session/sqlite-rpc-session-backend.d.ts.map +0 -1
  270. package/dist/transports/rpc.d.ts +0 -51
  271. package/dist/transports/rpc.d.ts.map +0 -1
  272. package/src/ClineCore.test.ts +0 -226
  273. package/src/account/cline-account-service.test.ts +0 -185
  274. package/src/account/featurebase-token.test.ts +0 -175
  275. package/src/account/rpc.test.ts +0 -63
  276. package/src/auth/bounded-ttl-cache.test.ts +0 -38
  277. package/src/auth/client.test.ts +0 -69
  278. package/src/auth/cline.test.ts +0 -267
  279. package/src/auth/codex.test.ts +0 -170
  280. package/src/auth/oca.test.ts +0 -340
  281. package/src/auth/server.test.ts +0 -287
  282. package/src/auth/utils.test.ts +0 -128
  283. package/src/extensions/config/agent-config-loader.test.ts +0 -236
  284. package/src/extensions/config/hooks-config-loader.test.ts +0 -20
  285. package/src/extensions/config/runtime-commands.test.ts +0 -115
  286. package/src/extensions/config/unified-config-file-watcher.test.ts +0 -196
  287. package/src/extensions/config/user-instruction-config-loader.test.ts +0 -246
  288. package/src/extensions/context/compaction.test.ts +0 -483
  289. package/src/extensions/mcp/config-loader.test.ts +0 -238
  290. package/src/extensions/mcp/manager.test.ts +0 -105
  291. package/src/extensions/plugin/plugin-config-loader.test.ts +0 -184
  292. package/src/extensions/plugin/plugin-loader.test.ts +0 -292
  293. package/src/extensions/plugin/plugin-sandbox.test.ts +0 -423
  294. package/src/extensions/tools/definitions.test.ts +0 -780
  295. package/src/extensions/tools/executors/bash.test.ts +0 -87
  296. package/src/extensions/tools/executors/editor.test.ts +0 -35
  297. package/src/extensions/tools/executors/file-read.test.ts +0 -125
  298. package/src/extensions/tools/model-tool-routing.test.ts +0 -86
  299. package/src/extensions/tools/presets.test.ts +0 -70
  300. package/src/extensions/tools/team/multi-agent.lifecycle.test.ts +0 -455
  301. package/src/extensions/tools/team/spawn-agent-tool.test.ts +0 -381
  302. package/src/extensions/tools/team/team-tools.test.ts +0 -918
  303. package/src/hooks/checkpoint-hooks.test.ts +0 -168
  304. package/src/hooks/hook-file-hooks.test.ts +0 -311
  305. package/src/hooks/persistent.ts +0 -661
  306. package/src/runtime/history.test.ts +0 -114
  307. package/src/runtime/host.test.ts +0 -230
  308. package/src/runtime/rpc-runtime-ensure.test.ts +0 -123
  309. package/src/runtime/rpc-runtime-ensure.ts +0 -659
  310. package/src/runtime/rpc-spawn-lease.test.ts +0 -81
  311. package/src/runtime/rpc-spawn-lease.ts +0 -156
  312. package/src/runtime/runtime-builder.team-persistence.test.ts +0 -245
  313. package/src/runtime/runtime-builder.test.ts +0 -615
  314. package/src/runtime/runtime-oauth-token-manager.test.ts +0 -137
  315. package/src/runtime/runtime-parity.test.ts +0 -143
  316. package/src/services/providers/local-provider-service.test.ts +0 -1062
  317. package/src/services/session-data.test.ts +0 -160
  318. package/src/services/storage/provider-settings-legacy-migration.test.ts +0 -424
  319. package/src/services/storage/provider-settings-manager.test.ts +0 -191
  320. package/src/services/telemetry/OpenTelemetryAdapter.test.ts +0 -157
  321. package/src/services/telemetry/OpenTelemetryProvider.test.ts +0 -326
  322. package/src/services/telemetry/TelemetryLoggerSink.test.ts +0 -42
  323. package/src/services/telemetry/TelemetryService.test.ts +0 -134
  324. package/src/services/telemetry/distinct-id.test.ts +0 -57
  325. package/src/services/workspace/file-indexer.d.ts +0 -11
  326. package/src/services/workspace/file-indexer.test.ts +0 -156
  327. package/src/services/workspace/mention-enricher.test.ts +0 -106
  328. package/src/session/persistence-service.test.ts +0 -300
  329. package/src/session/rpc-session-service.ts +0 -114
  330. package/src/session/session-service.team-persistence.test.ts +0 -48
  331. package/src/session/sqlite-rpc-session-backend.ts +0 -301
  332. package/src/transports/local.e2e.test.ts +0 -380
  333. package/src/transports/local.test.ts +0 -2559
  334. package/src/transports/rpc.test.ts +0 -82
  335. package/src/transports/rpc.ts +0 -665
@@ -1,2559 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { AgentResult } from "@clinebot/shared";
6
- import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
7
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
- import {
9
- type StartSessionInput,
10
- splitCoreSessionConfig,
11
- } from "../runtime/runtime-host";
12
- import { TelemetryService } from "../services/telemetry/TelemetryService";
13
- import type { SessionManifest } from "../session/session-manifest";
14
- import { SessionSource } from "../types/common";
15
- import type { CoreSessionConfig } from "../types/config";
16
- import { LocalRuntimeHost as RuntimeHostUnderTest } from "./local";
17
-
18
- const distinctId = "test-machine-id";
19
-
20
- function createResult(overrides: Partial<AgentResult> = {}): AgentResult {
21
- return {
22
- text: "ok",
23
- iterations: 1,
24
- finishReason: "completed",
25
- usage: {
26
- inputTokens: 1,
27
- outputTokens: 2,
28
- totalCost: 0,
29
- },
30
- messages: [],
31
- toolCalls: [],
32
- durationMs: 1,
33
- model: {
34
- id: "mock-model",
35
- provider: "mock-provider",
36
- },
37
- startedAt: new Date("2026-01-01T00:00:00.000Z"),
38
- endedAt: new Date("2026-01-01T00:00:01.000Z"),
39
- ...overrides,
40
- };
41
- }
42
-
43
- function createManifest(sessionId: string): SessionManifest {
44
- return {
45
- version: 1,
46
- session_id: sessionId,
47
- source: SessionSource.CLI,
48
- pid: process.pid,
49
- started_at: "2026-01-01T00:00:00.000Z",
50
- status: "running",
51
- interactive: false,
52
- provider: "mock-provider",
53
- model: "mock-model",
54
- cwd: "/tmp/project",
55
- workspace_root: "/tmp/project",
56
- enable_tools: true,
57
- enable_spawn: true,
58
- enable_teams: true,
59
- prompt: "hello",
60
- messages_path: "/tmp/messages.json",
61
- };
62
- }
63
-
64
- type PluginEventTestHarness = {
65
- handlePluginEvent: (
66
- rootSessionId: string,
67
- event: { name: string; payload?: unknown },
68
- ) => Promise<void>;
69
- getPendingPrompts: (
70
- sessionId: string,
71
- ) => Array<{ prompt: string; delivery: "queue" | "steer" }>;
72
- };
73
-
74
- function createPluginEventHarness(
75
- manager: RuntimeHostUnderTest,
76
- ): PluginEventTestHarness {
77
- const target = manager as object;
78
- return {
79
- handlePluginEvent: async (rootSessionId, event) => {
80
- const handler = Reflect.get(target, "handlePluginEvent");
81
- if (typeof handler !== "function") {
82
- throw new Error("handlePluginEvent test hook unavailable");
83
- }
84
- await Reflect.apply(
85
- handler as (
86
- rootSessionId: string,
87
- event: { name: string; payload?: unknown },
88
- ) => Promise<void>,
89
- target,
90
- [rootSessionId, event],
91
- );
92
- },
93
- getPendingPrompts: (sessionId) => {
94
- const getter = Reflect.get(target, "getSessionOrThrow");
95
- if (typeof getter !== "function") {
96
- throw new Error("getSessionOrThrow test hook unavailable");
97
- }
98
- const session = Reflect.apply(
99
- getter as (sessionId: string) => {
100
- pendingPrompts: Array<{
101
- id: string;
102
- prompt: string;
103
- delivery: "queue" | "steer";
104
- userFiles?: unknown;
105
- userImages?: unknown;
106
- }>;
107
- },
108
- target,
109
- [sessionId],
110
- );
111
- return session.pendingPrompts.map(({ prompt, delivery }) => ({
112
- prompt,
113
- delivery,
114
- }));
115
- },
116
- };
117
- }
118
-
119
- function createConfig(
120
- overrides: Partial<CoreSessionConfig> = {},
121
- ): CoreSessionConfig {
122
- return {
123
- providerId: "mock-provider",
124
- modelId: "mock-model",
125
- cwd: "/tmp/project",
126
- systemPrompt: "You are a test agent",
127
- mode: "act",
128
- enableTools: true,
129
- enableSpawnAgent: true,
130
- enableAgentTeams: true,
131
- ...overrides,
132
- };
133
- }
134
-
135
- function normalizeStartInput(
136
- input: Omit<StartSessionInput, "config" | "localRuntime"> & {
137
- config: CoreSessionConfig;
138
- },
139
- ): StartSessionInput {
140
- const split = splitCoreSessionConfig(input.config);
141
- return {
142
- ...input,
143
- ...split,
144
- };
145
- }
146
-
147
- function createGitRepo(cwd: string): void {
148
- execFileSync("git", ["-C", cwd, "init"], { stdio: "pipe" });
149
- execFileSync("git", ["-C", cwd, "config", "user.name", "Codex Test"], {
150
- stdio: "pipe",
151
- });
152
- execFileSync(
153
- "git",
154
- ["-C", cwd, "config", "user.email", "codex@example.com"],
155
- {
156
- stdio: "pipe",
157
- },
158
- );
159
- writeFileSync(join(cwd, "note.txt"), "base\n", "utf8");
160
- execFileSync("git", ["-C", cwd, "add", "note.txt"], { stdio: "pipe" });
161
- execFileSync("git", ["-C", cwd, "commit", "-m", "initial"], {
162
- stdio: "pipe",
163
- });
164
- }
165
-
166
- describe("LocalRuntimeHost", () => {
167
- const envSnapshot = {
168
- HOME: process.env.HOME,
169
- CLINE_DIR: process.env.CLINE_DIR,
170
- };
171
- let isolatedHomeDir = "";
172
-
173
- beforeEach(() => {
174
- isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
175
- process.env.HOME = isolatedHomeDir;
176
- process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
177
- setHomeDir(isolatedHomeDir);
178
- setClineDir(process.env.CLINE_DIR);
179
- });
180
-
181
- afterEach(() => {
182
- process.env.HOME = envSnapshot.HOME;
183
- process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
184
- setHomeDir(envSnapshot.HOME ?? "~");
185
- setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
186
- rmSync(isolatedHomeDir, { recursive: true, force: true });
187
- });
188
-
189
- it("emits session lifecycle telemetry when configured", async () => {
190
- const sessionId = "sess-telemetry";
191
- const manifest = createManifest(sessionId);
192
- const adapter = {
193
- name: "test",
194
- emit: vi.fn(),
195
- emitRequired: vi.fn(),
196
- recordCounter: vi.fn(),
197
- recordHistogram: vi.fn(),
198
- recordGauge: vi.fn(),
199
- isEnabled: vi.fn(() => true),
200
- flush: vi.fn().mockResolvedValue(undefined),
201
- dispose: vi.fn().mockResolvedValue(undefined),
202
- };
203
- const telemetry = new TelemetryService({
204
- adapters: [adapter],
205
- distinctId: distinctId,
206
- });
207
- const sessionService = {
208
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
209
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
210
- manifestPath: "/tmp/manifest.json",
211
- messagesPath: "/tmp/messages.json",
212
- manifest,
213
- }),
214
- persistSessionMessages: vi.fn(),
215
- updateSessionStatus: vi.fn().mockResolvedValue({
216
- updated: true,
217
- endedAt: "2026-01-01T00:00:05.000Z",
218
- }),
219
- writeSessionManifest: vi.fn(),
220
- listSessions: vi.fn().mockResolvedValue([]),
221
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
222
- };
223
- const runtimeBuilder = {
224
- build: vi.fn().mockReturnValue({
225
- tools: [],
226
- teamRuntime: {
227
- getTeamId: vi.fn().mockReturnValue("team_test-team"),
228
- getTeamName: vi.fn().mockReturnValue("test-team"),
229
- },
230
- teamRestoredFromPersistence: false,
231
- shutdown: vi.fn(),
232
- }),
233
- };
234
- const agent = {
235
- run: vi.fn().mockResolvedValue(createResult()),
236
- continue: vi.fn().mockResolvedValue(createResult()),
237
- getMessages: vi.fn().mockReturnValue([]),
238
- getAgentId: vi.fn().mockReturnValue("agent-root-1"),
239
- getConversationId: vi.fn().mockReturnValue("conv-root-1"),
240
- abort: vi.fn(),
241
- shutdown: vi.fn().mockResolvedValue(undefined),
242
- };
243
- const manager = new RuntimeHostUnderTest({
244
- distinctId,
245
- sessionService: sessionService as never,
246
- runtimeBuilder: runtimeBuilder as never,
247
- createAgent: () => agent as never,
248
- telemetry,
249
- });
250
-
251
- await manager.start(
252
- normalizeStartInput({
253
- config: createConfig({ telemetry, sessionId }),
254
- prompt: "hello",
255
- }),
256
- );
257
-
258
- expect(adapter.emit).toHaveBeenCalledWith(
259
- "session.started",
260
- expect.objectContaining({
261
- sessionId,
262
- agentId: "agent-root-1",
263
- agentKind: "team_lead",
264
- conversationId: "conv-root-1",
265
- teamRole: "lead",
266
- distinct_id: distinctId,
267
- }),
268
- );
269
- expect(adapter.emit).toHaveBeenCalledWith(
270
- "task.agent_created",
271
- expect.objectContaining({
272
- ulid: sessionId,
273
- agentId: "agent-root-1",
274
- agentKind: "team_lead",
275
- conversationId: "conv-root-1",
276
- teamRole: "lead",
277
- distinct_id: distinctId,
278
- }),
279
- );
280
- expect(adapter.emit).toHaveBeenCalledWith(
281
- "task.agent_team_created",
282
- expect.objectContaining({
283
- ulid: sessionId,
284
- leadAgentId: "agent-root-1",
285
- restoredFromPersistence: false,
286
- distinct_id: distinctId,
287
- }),
288
- );
289
- });
290
-
291
- it("persists custom session sources without coercing them to builtin values", async () => {
292
- const sessionId = "sess-kanban";
293
- const manifest = {
294
- ...createManifest(sessionId),
295
- source: "kanban",
296
- };
297
- const sessionService = {
298
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
299
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
300
- manifestPath: "/tmp/manifest.json",
301
- messagesPath: "/tmp/messages.json",
302
- manifest,
303
- }),
304
- persistSessionMessages: vi.fn(),
305
- updateSessionStatus: vi.fn().mockResolvedValue({
306
- updated: true,
307
- endedAt: "2026-01-01T00:00:05.000Z",
308
- }),
309
- writeSessionManifest: vi.fn(),
310
- listSessions: vi.fn().mockResolvedValue([]),
311
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
312
- };
313
- const runtimeBuilder = {
314
- build: vi.fn().mockReturnValue({
315
- tools: [],
316
- teamRuntime: undefined,
317
- teamRestoredFromPersistence: false,
318
- shutdown: vi.fn(),
319
- }),
320
- };
321
- const agent = {
322
- run: vi.fn().mockResolvedValue(createResult()),
323
- continue: vi.fn().mockResolvedValue(createResult()),
324
- getMessages: vi.fn().mockReturnValue([]),
325
- getAgentId: vi.fn().mockReturnValue("agent-root-1"),
326
- getConversationId: vi.fn().mockReturnValue("conv-root-1"),
327
- abort: vi.fn(),
328
- shutdown: vi.fn().mockResolvedValue(undefined),
329
- };
330
- const manager = new RuntimeHostUnderTest({
331
- distinctId,
332
- sessionService: sessionService as never,
333
- runtimeBuilder: runtimeBuilder as never,
334
- createAgent: () => agent as never,
335
- });
336
-
337
- const started = await manager.start(
338
- normalizeStartInput({
339
- source: "kanban",
340
- config: createConfig({ sessionId }),
341
- prompt: "hello",
342
- }),
343
- );
344
-
345
- expect(sessionService.createRootSessionWithArtifacts).toHaveBeenCalledWith(
346
- expect.objectContaining({
347
- sessionId,
348
- source: "kanban",
349
- }),
350
- );
351
- expect(started.manifest.source).toBe("kanban");
352
- });
353
-
354
- it("reuses the persisted team name when resuming a session", async () => {
355
- const sessionId = "sess-team-resume";
356
- const manifest = createManifest(sessionId);
357
- const sessionService = {
358
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
359
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
360
- manifestPath: "/tmp/manifest.json",
361
- messagesPath: "/tmp/messages.json",
362
- manifest,
363
- }),
364
- persistSessionMessages: vi.fn(),
365
- updateSessionStatus: vi.fn().mockResolvedValue({
366
- updated: true,
367
- endedAt: "2026-01-01T00:00:05.000Z",
368
- }),
369
- writeSessionManifest: vi.fn(),
370
- listSessions: vi.fn().mockResolvedValue([
371
- {
372
- sessionId,
373
- source: SessionSource.CLI,
374
- pid: process.pid,
375
- startedAt: "2026-01-01T00:00:00.000Z",
376
- endedAt: null,
377
- exitCode: null,
378
- status: "running",
379
- statusLock: 0,
380
- interactive: true,
381
- provider: "mock-provider",
382
- model: "mock-model",
383
- cwd: "/tmp/project",
384
- workspaceRoot: "/tmp/project",
385
- teamName: "persisted-team",
386
- enableTools: true,
387
- enableSpawn: true,
388
- enableTeams: true,
389
- parentSessionId: null,
390
- parentAgentId: null,
391
- agentId: null,
392
- conversationId: null,
393
- isSubagent: false,
394
- prompt: null,
395
- metadata: null,
396
- messagesPath: "/tmp/messages.json",
397
- updatedAt: "2026-01-01T00:00:00.000Z",
398
- },
399
- ]),
400
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
401
- };
402
- const runtimeBuilder = {
403
- build: vi.fn().mockReturnValue({
404
- tools: [],
405
- teamRuntime: {
406
- getTeamId: vi.fn().mockReturnValue("team_persisted-team"),
407
- getTeamName: vi.fn().mockReturnValue("persisted-team"),
408
- },
409
- teamRestoredFromPersistence: true,
410
- shutdown: vi.fn(),
411
- }),
412
- };
413
- const agent = {
414
- run: vi.fn().mockResolvedValue(createResult()),
415
- continue: vi.fn().mockResolvedValue(createResult()),
416
- getMessages: vi.fn().mockReturnValue([]),
417
- getAgentId: vi.fn().mockReturnValue("agent-root-1"),
418
- getConversationId: vi.fn().mockReturnValue("conv-root-1"),
419
- abort: vi.fn(),
420
- shutdown: vi.fn().mockResolvedValue(undefined),
421
- };
422
- const manager = new RuntimeHostUnderTest({
423
- distinctId,
424
- sessionService: sessionService as never,
425
- runtimeBuilder: runtimeBuilder as never,
426
- createAgent: () => agent as never,
427
- });
428
-
429
- await manager.start(
430
- normalizeStartInput({
431
- config: createConfig({ sessionId, teamName: undefined }),
432
- }),
433
- );
434
-
435
- expect(runtimeBuilder.build).toHaveBeenCalledWith(
436
- expect.objectContaining({
437
- config: expect.objectContaining({
438
- sessionId,
439
- teamName: "persisted-team",
440
- }),
441
- }),
442
- );
443
- expect(
444
- sessionService.createRootSessionWithArtifacts,
445
- ).not.toHaveBeenCalled();
446
- });
447
-
448
- it("runs a non-interactive prompt and persists messages/status", async () => {
449
- const sessionId = "sess-1";
450
- const manifest = createManifest(sessionId);
451
- const createRootSessionWithArtifacts = vi.fn().mockResolvedValue({
452
- manifestPath: "/tmp/manifest.json",
453
- messagesPath: "/tmp/messages.json",
454
- manifest,
455
- });
456
- const persistSessionMessages = vi.fn();
457
- const updateSessionStatus = vi.fn().mockResolvedValue({
458
- updated: true,
459
- endedAt: "2026-01-01T00:00:05.000Z",
460
- });
461
- const writeSessionManifest = vi.fn();
462
- const listSessions = vi.fn().mockResolvedValue([]);
463
- const deleteSession = vi.fn().mockResolvedValue({ deleted: true });
464
- const sessionService = {
465
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
466
- createRootSessionWithArtifacts,
467
- persistSessionMessages,
468
- updateSessionStatus,
469
- writeSessionManifest,
470
- listSessions,
471
- deleteSession,
472
- };
473
-
474
- const shutdown = vi.fn();
475
- const runtimeBuilder = {
476
- build: vi.fn().mockReturnValue({
477
- tools: [],
478
- shutdown,
479
- }),
480
- };
481
- const run = vi.fn().mockResolvedValue(
482
- createResult({
483
- messages: [
484
- { role: "user", content: [{ type: "text", text: "hello" }] },
485
- ],
486
- }),
487
- );
488
- const continueFn = vi.fn();
489
- const agent = {
490
- run,
491
- continue: continueFn,
492
- abort: vi.fn(),
493
- shutdown: vi.fn().mockResolvedValue(undefined),
494
- getMessages: vi.fn().mockReturnValue([]),
495
- messages: [],
496
- };
497
-
498
- const manager = new RuntimeHostUnderTest({
499
- distinctId,
500
- sessionService: sessionService as never,
501
- runtimeBuilder,
502
- createAgent: () => agent as never,
503
- });
504
-
505
- const started = await manager.start(
506
- normalizeStartInput({
507
- config: createConfig({ sessionId }),
508
- prompt: "hello",
509
- interactive: false,
510
- }),
511
- );
512
-
513
- expect(started.sessionId).toBe(sessionId);
514
- expect(started.result?.finishReason).toBe("completed");
515
- expect(run).toHaveBeenCalledTimes(1);
516
- expect(continueFn).not.toHaveBeenCalled();
517
- expect(persistSessionMessages).toHaveBeenCalledTimes(1);
518
- expect(updateSessionStatus).toHaveBeenCalledWith(sessionId, "completed", 0);
519
- expect(writeSessionManifest).toHaveBeenCalledTimes(1);
520
- expect(shutdown).toHaveBeenCalledTimes(1);
521
- });
522
-
523
- it("preserves manifest metadata updates and persists total cost", async () => {
524
- const sessionId = "sess-history-meta";
525
- let storedManifest: SessionManifest = {
526
- ...createManifest(sessionId),
527
- metadata: {
528
- checkpoint: {
529
- latest: {
530
- ref: "abc123",
531
- createdAt: 1,
532
- runCount: 1,
533
- },
534
- history: [
535
- {
536
- ref: "abc123",
537
- createdAt: 1,
538
- runCount: 1,
539
- },
540
- ],
541
- },
542
- },
543
- };
544
- const createRootSessionWithArtifacts = vi.fn().mockResolvedValue({
545
- manifestPath: "/tmp/manifest-history-meta.json",
546
- messagesPath: "/tmp/messages-history-meta.json",
547
- manifest: { ...storedManifest },
548
- });
549
- const persistSessionMessages = vi.fn();
550
- const updateSession = vi.fn().mockImplementation(async (input) => {
551
- storedManifest = {
552
- ...storedManifest,
553
- metadata: input.metadata,
554
- };
555
- return { updated: true };
556
- });
557
- const updateSessionStatus = vi.fn().mockResolvedValue({
558
- updated: true,
559
- endedAt: "2026-01-01T00:00:05.000Z",
560
- });
561
- const readSessionManifest = vi
562
- .fn()
563
- .mockImplementation(() => storedManifest);
564
- const writeSessionManifest = vi
565
- .fn()
566
- .mockImplementation((_path, manifest) => {
567
- storedManifest = manifest;
568
- });
569
- const sessionService = {
570
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
571
- createRootSessionWithArtifacts,
572
- persistSessionMessages,
573
- updateSession,
574
- updateSessionStatus,
575
- readSessionManifest,
576
- writeSessionManifest,
577
- listSessions: vi.fn().mockResolvedValue([]),
578
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
579
- };
580
-
581
- const runtimeBuilder = {
582
- build: vi.fn().mockReturnValue({
583
- tools: [],
584
- shutdown: vi.fn(),
585
- }),
586
- };
587
- const agent = {
588
- run: vi.fn().mockResolvedValue(
589
- createResult({
590
- usage: {
591
- inputTokens: 3,
592
- outputTokens: 4,
593
- totalCost: 0.42,
594
- },
595
- messages: [
596
- { role: "user", content: [{ type: "text", text: "hello" }] },
597
- ],
598
- }),
599
- ),
600
- continue: vi.fn(),
601
- abort: vi.fn(),
602
- shutdown: vi.fn().mockResolvedValue(undefined),
603
- getMessages: vi.fn().mockReturnValue([]),
604
- messages: [],
605
- };
606
-
607
- const manager = new RuntimeHostUnderTest({
608
- distinctId,
609
- sessionService: sessionService as never,
610
- runtimeBuilder,
611
- createAgent: () => agent as never,
612
- });
613
-
614
- await manager.start(
615
- normalizeStartInput({
616
- config: createConfig({ sessionId }),
617
- prompt: "hello",
618
- interactive: false,
619
- }),
620
- );
621
-
622
- expect(updateSession).toHaveBeenCalledWith({
623
- sessionId,
624
- metadata: {
625
- checkpoint: {
626
- latest: {
627
- ref: "abc123",
628
- createdAt: 1,
629
- runCount: 1,
630
- },
631
- history: [
632
- {
633
- ref: "abc123",
634
- createdAt: 1,
635
- runCount: 1,
636
- },
637
- ],
638
- },
639
- totalCost: 0.42,
640
- },
641
- });
642
- expect(writeSessionManifest).toHaveBeenCalledWith(
643
- "/tmp/manifest-history-meta.json",
644
- expect.objectContaining({
645
- metadata: {
646
- checkpoint: {
647
- latest: {
648
- ref: "abc123",
649
- createdAt: 1,
650
- runCount: 1,
651
- },
652
- history: [
653
- {
654
- ref: "abc123",
655
- createdAt: 1,
656
- runCount: 1,
657
- },
658
- ],
659
- },
660
- totalCost: 0.42,
661
- },
662
- status: "completed",
663
- }),
664
- );
665
- });
666
-
667
- it("does not install checkpoint hooks when checkpoint.enabled is not set in config", async () => {
668
- const sessionId = "sess-checkpoint-default-off";
669
- const manifest = createManifest(sessionId);
670
- const updateSession = vi.fn().mockResolvedValue({ updated: true });
671
- const sessionService = {
672
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
673
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
674
- manifestPath: "/tmp/manifest-checkpoint-default-off.json",
675
- messagesPath: "/tmp/messages-checkpoint-default-off.json",
676
- manifest,
677
- }),
678
- persistSessionMessages: vi.fn(),
679
- updateSession,
680
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
681
- writeSessionManifest: vi.fn(),
682
- listSessions: vi.fn().mockResolvedValue([
683
- {
684
- sessionId,
685
- provider: "mock-provider",
686
- model: "mock-model",
687
- cwd: "/tmp/project",
688
- workspaceRoot: "/tmp/project",
689
- createdAt: "2026-01-01T00:00:00.000Z",
690
- updatedAt: "2026-01-01T00:00:00.000Z",
691
- status: "running",
692
- metadata: undefined,
693
- },
694
- ]),
695
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
696
- };
697
- const runtimeBuilder = {
698
- build: vi.fn().mockImplementation(() => {
699
- return {
700
- tools: [],
701
- shutdown: vi.fn(),
702
- };
703
- }),
704
- };
705
- const manager = new RuntimeHostUnderTest({
706
- distinctId,
707
- sessionService: sessionService as never,
708
- runtimeBuilder,
709
- createAgent: (config) =>
710
- ({
711
- run: vi.fn().mockImplementation(async () => {
712
- await config.hooks?.onRunStart?.({
713
- agentId: "agent_1",
714
- conversationId: "conv_1",
715
- parentAgentId: null,
716
- userMessage: "hello",
717
- });
718
- await config.hooks?.onBeforeAgentStart?.({
719
- agentId: "agent_1",
720
- conversationId: "conv_1",
721
- parentAgentId: null,
722
- iteration: 1,
723
- systemPrompt: "system",
724
- messages: [],
725
- });
726
- return createResult();
727
- }),
728
- continue: vi.fn(),
729
- abort: vi.fn(),
730
- shutdown: vi.fn().mockResolvedValue(undefined),
731
- getMessages: vi.fn().mockReturnValue([]),
732
- messages: [],
733
- }) as never,
734
- });
735
-
736
- await manager.start(
737
- normalizeStartInput({
738
- config: createConfig({ sessionId }),
739
- prompt: "hello",
740
- interactive: false,
741
- }),
742
- );
743
- expect(updateSession).toHaveBeenCalledTimes(1);
744
- expect(updateSession).toHaveBeenLastCalledWith({
745
- sessionId,
746
- metadata: {
747
- totalCost: 0,
748
- },
749
- });
750
- });
751
-
752
- it("installs checkpoint hooks when checkpoint.enabled=true in config", async () => {
753
- const sessionId = "sess-checkpoint-config-on";
754
- const repoCwd = mkdtempSync(join(isolatedHomeDir, "checkpoint-repo-"));
755
- createGitRepo(repoCwd);
756
- const manifest = createManifest(sessionId);
757
- const updateSession = vi.fn().mockResolvedValue({ updated: true });
758
- const sessionService = {
759
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
760
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
761
- manifestPath: "/tmp/manifest-checkpoint-env-on.json",
762
- messagesPath: "/tmp/messages-checkpoint-env-on.json",
763
- manifest,
764
- }),
765
- persistSessionMessages: vi.fn(),
766
- updateSession,
767
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
768
- writeSessionManifest: vi.fn(),
769
- listSessions: vi.fn().mockResolvedValue([
770
- {
771
- sessionId,
772
- provider: "mock-provider",
773
- model: "mock-model",
774
- cwd: repoCwd,
775
- workspaceRoot: repoCwd,
776
- createdAt: "2026-01-01T00:00:00.000Z",
777
- updatedAt: "2026-01-01T00:00:00.000Z",
778
- status: "running",
779
- metadata: undefined,
780
- },
781
- ]),
782
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
783
- };
784
- const runtimeBuilder = {
785
- build: vi.fn().mockImplementation(() => {
786
- return {
787
- tools: [],
788
- shutdown: vi.fn(),
789
- };
790
- }),
791
- };
792
- const manager = new RuntimeHostUnderTest({
793
- distinctId,
794
- sessionService: sessionService as never,
795
- runtimeBuilder,
796
- createAgent: (config) =>
797
- ({
798
- run: vi.fn().mockImplementation(async () => {
799
- await config.hooks?.onRunStart?.({
800
- agentId: "agent_1",
801
- conversationId: "conv_1",
802
- parentAgentId: null,
803
- userMessage: "hello",
804
- });
805
- await config.hooks?.onBeforeAgentStart?.({
806
- agentId: "agent_1",
807
- conversationId: "conv_1",
808
- parentAgentId: null,
809
- iteration: 1,
810
- systemPrompt: "system",
811
- messages: [],
812
- });
813
- return createResult();
814
- }),
815
- continue: vi.fn(),
816
- abort: vi.fn(),
817
- shutdown: vi.fn().mockResolvedValue(undefined),
818
- getMessages: vi.fn().mockReturnValue([]),
819
- messages: [],
820
- }) as never,
821
- });
822
-
823
- await manager.start(
824
- normalizeStartInput({
825
- config: {
826
- ...createConfig({ sessionId, cwd: repoCwd }),
827
- checkpoint: { enabled: true },
828
- },
829
- prompt: "hello",
830
- interactive: false,
831
- }),
832
- );
833
- expect(updateSession).toHaveBeenCalledTimes(2);
834
- expect(updateSession).toHaveBeenNthCalledWith(1, {
835
- sessionId,
836
- metadata: expect.objectContaining({
837
- checkpoint: expect.objectContaining({
838
- latest: expect.objectContaining({
839
- ref: expect.stringMatching(/^[0-9a-f]{40}$/),
840
- runCount: 1,
841
- }),
842
- }),
843
- }),
844
- });
845
- expect(updateSession).toHaveBeenNthCalledWith(2, {
846
- sessionId,
847
- metadata: expect.objectContaining({
848
- totalCost: 0,
849
- }),
850
- });
851
- });
852
-
853
- it("persists assistant message metadata for usage and model identity", async () => {
854
- const sessionId = "sess-meta";
855
- const manifest = createManifest(sessionId);
856
- const persistSessionMessages = vi.fn();
857
- const sessionService = {
858
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
859
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
860
- manifestPath: "/tmp/manifest-meta.json",
861
- messagesPath: "/tmp/messages-meta.json",
862
- manifest,
863
- }),
864
- persistSessionMessages,
865
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
866
- writeSessionManifest: vi.fn(),
867
- listSessions: vi.fn().mockResolvedValue([]),
868
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
869
- };
870
- const updateConnectionDefaults = vi.fn();
871
- const runtimeBuilder = {
872
- build: vi.fn().mockReturnValue({
873
- tools: [],
874
- delegatedAgentConfigProvider: {
875
- getRuntimeConfig: vi.fn(),
876
- getConnectionConfig: vi.fn(),
877
- updateConnectionDefaults,
878
- },
879
- shutdown: vi.fn(),
880
- }),
881
- };
882
- const run = vi.fn().mockResolvedValue(
883
- createResult({
884
- usage: {
885
- inputTokens: 33,
886
- outputTokens: 12,
887
- cacheReadTokens: 4,
888
- cacheWriteTokens: 1,
889
- totalCost: 0.42,
890
- },
891
- model: {
892
- id: "claude-sonnet-4-6",
893
- provider: "anthropic",
894
- info: {
895
- id: "claude-sonnet-4-6",
896
- family: "claude-sonnet-4",
897
- },
898
- },
899
- endedAt: new Date("2026-01-01T00:00:02.000Z"),
900
- messages: [
901
- { role: "user", content: [{ type: "text", text: "hello" }] },
902
- { role: "assistant", content: [{ type: "text", text: "world" }] },
903
- ],
904
- }),
905
- );
906
- const manager = new RuntimeHostUnderTest({
907
- distinctId,
908
- sessionService: sessionService as never,
909
- runtimeBuilder,
910
- createAgent: () =>
911
- ({
912
- run,
913
- continue: vi.fn(),
914
- abort: vi.fn(),
915
- shutdown: vi.fn().mockResolvedValue(undefined),
916
- getMessages: vi.fn().mockReturnValue([]),
917
- messages: [],
918
- }) as never,
919
- });
920
-
921
- await manager.start(
922
- normalizeStartInput({
923
- config: createConfig({
924
- sessionId,
925
- providerId: "anthropic",
926
- modelId: "claude-sonnet-4-6",
927
- }),
928
- prompt: "hello",
929
- interactive: false,
930
- }),
931
- );
932
-
933
- expect(persistSessionMessages).toHaveBeenCalledTimes(1);
934
- const persisted = persistSessionMessages.mock.calls[0]?.[1];
935
- expect(Array.isArray(persisted)).toBe(true);
936
- expect(persisted?.[1]).toMatchObject({
937
- role: "assistant",
938
- modelInfo: {
939
- id: "claude-sonnet-4-6",
940
- provider: "anthropic",
941
- family: "claude-sonnet-4",
942
- },
943
- metrics: {
944
- inputTokens: 33,
945
- outputTokens: 12,
946
- cacheReadTokens: 4,
947
- cacheWriteTokens: 1,
948
- cost: 0.42,
949
- },
950
- ts: new Date("2026-01-01T00:00:02.000Z").getTime(),
951
- });
952
- });
953
-
954
- it("queues sandbox steer messages back into the active session", async () => {
955
- const sessionId = "sess-steer";
956
- const manifest = createManifest(sessionId);
957
- const sessionService = {
958
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
959
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
960
- manifestPath: "/tmp/manifest.json",
961
- messagesPath: "/tmp/messages.json",
962
- manifest,
963
- }),
964
- persistSessionMessages: vi.fn(),
965
- updateSessionStatus: vi.fn().mockResolvedValue({
966
- updated: true,
967
- endedAt: "2026-01-01T00:00:05.000Z",
968
- }),
969
- writeSessionManifest: vi.fn(),
970
- listSessions: vi.fn().mockResolvedValue([]),
971
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
972
- };
973
- const updateConnectionDefaults = vi.fn();
974
- const runtimeBuilder = {
975
- build: vi.fn().mockReturnValue({
976
- tools: [],
977
- delegatedAgentConfigProvider: {
978
- getRuntimeConfig: vi.fn(),
979
- getConnectionConfig: vi.fn(),
980
- updateConnectionDefaults,
981
- },
982
- shutdown: vi.fn(),
983
- }),
984
- };
985
- const run = vi.fn().mockResolvedValue(
986
- createResult({
987
- messages: [
988
- { role: "user", content: [{ type: "text", text: "hello" }] },
989
- ],
990
- }),
991
- );
992
- const continueFn = vi.fn().mockResolvedValue(
993
- createResult({
994
- text: "steered",
995
- messages: [
996
- { role: "user", content: [{ type: "text", text: "hello" }] },
997
- {
998
- role: "assistant",
999
- content: [{ type: "text", text: "steered" }],
1000
- },
1001
- ],
1002
- }),
1003
- );
1004
- const agent = {
1005
- run,
1006
- continue: continueFn,
1007
- abort: vi.fn(),
1008
- shutdown: vi.fn().mockResolvedValue(undefined),
1009
- getMessages: vi
1010
- .fn()
1011
- .mockReturnValue([
1012
- { role: "user", content: [{ type: "text", text: "hello" }] },
1013
- ]),
1014
- canStartRun: vi.fn().mockReturnValue(true),
1015
- };
1016
-
1017
- const manager = new RuntimeHostUnderTest({
1018
- distinctId,
1019
- sessionService: sessionService as never,
1020
- runtimeBuilder,
1021
- createAgent: () => agent as never,
1022
- });
1023
-
1024
- await manager.start(
1025
- normalizeStartInput({
1026
- config: createConfig({ sessionId }),
1027
- prompt: "hello",
1028
- interactive: true,
1029
- }),
1030
- );
1031
-
1032
- const harness = createPluginEventHarness(manager);
1033
- await harness.handlePluginEvent(sessionId, {
1034
- name: "steer_message",
1035
- payload: { prompt: "async result" },
1036
- });
1037
- await vi.waitFor(() => {
1038
- expect(continueFn).toHaveBeenCalledTimes(2);
1039
- });
1040
- expect(continueFn).toHaveBeenLastCalledWith(
1041
- '<user_input mode="act">async result</user_input>',
1042
- undefined,
1043
- undefined,
1044
- );
1045
- });
1046
-
1047
- it("promotes queued prompts to the front when they become steer", async () => {
1048
- const sessionId = "sess-steer-priority";
1049
- const manifest = createManifest(sessionId);
1050
- const sessionService = {
1051
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1052
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1053
- manifestPath: "/tmp/manifest.json",
1054
- messagesPath: "/tmp/messages.json",
1055
- manifest,
1056
- }),
1057
- persistSessionMessages: vi.fn(),
1058
- updateSessionStatus: vi.fn().mockResolvedValue({
1059
- updated: true,
1060
- endedAt: "2026-01-01T00:00:05.000Z",
1061
- }),
1062
- writeSessionManifest: vi.fn(),
1063
- listSessions: vi.fn().mockResolvedValue([]),
1064
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1065
- };
1066
- const updateConnectionDefaults = vi.fn();
1067
- const runtimeBuilder = {
1068
- build: vi.fn().mockReturnValue({
1069
- tools: [],
1070
- delegatedAgentConfigProvider: {
1071
- getRuntimeConfig: vi.fn(),
1072
- getConnectionConfig: vi.fn(),
1073
- updateConnectionDefaults,
1074
- },
1075
- shutdown: vi.fn(),
1076
- }),
1077
- };
1078
- const agent = {
1079
- run: vi.fn().mockResolvedValue(createResult()),
1080
- continue: vi.fn().mockResolvedValue(createResult()),
1081
- abort: vi.fn(),
1082
- shutdown: vi.fn().mockResolvedValue(undefined),
1083
- getMessages: vi.fn().mockReturnValue([]),
1084
- canStartRun: vi.fn().mockReturnValue(false),
1085
- };
1086
-
1087
- const manager = new RuntimeHostUnderTest({
1088
- distinctId,
1089
- sessionService: sessionService as never,
1090
- runtimeBuilder,
1091
- createAgent: () => agent as never,
1092
- });
1093
-
1094
- await manager.start(
1095
- normalizeStartInput({
1096
- config: createConfig({ sessionId }),
1097
- prompt: "hello",
1098
- interactive: true,
1099
- }),
1100
- );
1101
-
1102
- const harness = createPluginEventHarness(manager);
1103
-
1104
- await harness.handlePluginEvent(sessionId, {
1105
- name: "queue_message",
1106
- payload: { prompt: "queued first" },
1107
- });
1108
- await harness.handlePluginEvent(sessionId, {
1109
- name: "queue_message",
1110
- payload: { prompt: "queued second" },
1111
- });
1112
- await harness.handlePluginEvent(sessionId, {
1113
- name: "steer_message",
1114
- payload: { prompt: "queued first" },
1115
- });
1116
-
1117
- expect(harness.getPendingPrompts(sessionId)).toEqual([
1118
- { prompt: "queued first", delivery: "steer" },
1119
- { prompt: "queued second", delivery: "queue" },
1120
- ]);
1121
- });
1122
-
1123
- it("drops and ignores queued prompts once a session is aborting", async () => {
1124
- const sessionId = "sess-abort-pending";
1125
- const manifest = createManifest(sessionId);
1126
- const sessionService = {
1127
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1128
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1129
- manifestPath: "/tmp/manifest.json",
1130
- messagesPath: "/tmp/messages.json",
1131
- manifest,
1132
- }),
1133
- persistSessionMessages: vi.fn(),
1134
- updateSessionStatus: vi.fn().mockResolvedValue({
1135
- updated: true,
1136
- endedAt: "2026-01-01T00:00:05.000Z",
1137
- }),
1138
- writeSessionManifest: vi.fn(),
1139
- listSessions: vi.fn().mockResolvedValue([]),
1140
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1141
- };
1142
- const runtimeBuilder = {
1143
- build: vi.fn().mockReturnValue({
1144
- tools: [],
1145
- shutdown: vi.fn(),
1146
- }),
1147
- };
1148
- const agent = {
1149
- run: vi.fn().mockResolvedValue(createResult()),
1150
- continue: vi.fn().mockResolvedValue(createResult()),
1151
- abort: vi.fn(),
1152
- shutdown: vi.fn().mockResolvedValue(undefined),
1153
- getMessages: vi.fn().mockReturnValue([]),
1154
- canStartRun: vi.fn().mockReturnValue(false),
1155
- };
1156
-
1157
- const manager = new RuntimeHostUnderTest({
1158
- distinctId,
1159
- sessionService: sessionService as never,
1160
- runtimeBuilder,
1161
- createAgent: () => agent as never,
1162
- });
1163
-
1164
- await manager.start(
1165
- normalizeStartInput({
1166
- config: createConfig({ sessionId }),
1167
- prompt: "hello",
1168
- interactive: true,
1169
- }),
1170
- );
1171
-
1172
- const harness = createPluginEventHarness(manager);
1173
- await harness.handlePluginEvent(sessionId, {
1174
- name: "queue_message",
1175
- payload: { prompt: "queued before abort" },
1176
- });
1177
- expect(harness.getPendingPrompts(sessionId)).toEqual([
1178
- { prompt: "queued before abort", delivery: "queue" },
1179
- ]);
1180
-
1181
- await manager.abort(sessionId, new Error("test abort"));
1182
- expect(agent.abort).toHaveBeenCalledTimes(1);
1183
- expect(harness.getPendingPrompts(sessionId)).toEqual([]);
1184
-
1185
- await harness.handlePluginEvent(sessionId, {
1186
- name: "queue_message",
1187
- payload: { prompt: "queued after abort" },
1188
- });
1189
- expect(harness.getPendingPrompts(sessionId)).toEqual([]);
1190
- });
1191
-
1192
- it("preserves per-turn metadata on prior assistant messages across turns", async () => {
1193
- const sessionId = "sess-meta-multi";
1194
- const manifest = createManifest(sessionId);
1195
- const persistSessionMessages = vi.fn();
1196
- const runtimeBuilder = {
1197
- build: vi.fn().mockReturnValue({
1198
- tools: [],
1199
- shutdown: vi.fn(),
1200
- }),
1201
- };
1202
- const firstTurnMessages = [
1203
- {
1204
- role: "user" as const,
1205
- content: [{ type: "text" as const, text: "hello" }],
1206
- },
1207
- {
1208
- role: "assistant" as const,
1209
- content: [{ type: "text" as const, text: "world" }],
1210
- },
1211
- ];
1212
- const secondTurnMessages = [
1213
- ...firstTurnMessages,
1214
- {
1215
- role: "user" as const,
1216
- content: [{ type: "text" as const, text: "again" }],
1217
- },
1218
- {
1219
- role: "assistant" as const,
1220
- content: [{ type: "text" as const, text: "still here" }],
1221
- },
1222
- ];
1223
- const run = vi.fn().mockResolvedValue(
1224
- createResult({
1225
- usage: {
1226
- inputTokens: 33,
1227
- outputTokens: 12,
1228
- cacheReadTokens: 4,
1229
- cacheWriteTokens: 1,
1230
- totalCost: 0.42,
1231
- },
1232
- model: {
1233
- id: "claude-sonnet-4-6",
1234
- provider: "anthropic",
1235
- },
1236
- endedAt: new Date("2026-01-01T00:00:02.000Z"),
1237
- messages: firstTurnMessages,
1238
- }),
1239
- );
1240
- const continueFn = vi.fn().mockResolvedValue(
1241
- createResult({
1242
- usage: {
1243
- inputTokens: 10,
1244
- outputTokens: 5,
1245
- cacheReadTokens: 2,
1246
- cacheWriteTokens: 0,
1247
- totalCost: 0.12,
1248
- },
1249
- model: {
1250
- id: "claude-sonnet-4-6",
1251
- provider: "anthropic",
1252
- },
1253
- endedAt: new Date("2026-01-01T00:00:03.000Z"),
1254
- messages: secondTurnMessages,
1255
- }),
1256
- );
1257
- const agent = {
1258
- run,
1259
- continue: continueFn,
1260
- abort: vi.fn(),
1261
- shutdown: vi.fn().mockResolvedValue(undefined),
1262
- restore: vi.fn(),
1263
- getMessages: vi.fn().mockReturnValue([]),
1264
- messages: [],
1265
- };
1266
- const sessionService = {
1267
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1268
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1269
- manifestPath: "/tmp/manifest-meta-multi.json",
1270
- messagesPath: "/tmp/messages-meta-multi.json",
1271
- manifest,
1272
- }),
1273
- persistSessionMessages,
1274
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1275
- writeSessionManifest: vi.fn(),
1276
- listSessions: vi.fn().mockResolvedValue([]),
1277
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1278
- };
1279
- const manager = new RuntimeHostUnderTest({
1280
- distinctId,
1281
- sessionService: sessionService as never,
1282
- runtimeBuilder,
1283
- createAgent: () => agent as never,
1284
- });
1285
-
1286
- await manager.start(
1287
- normalizeStartInput({
1288
- config: createConfig({
1289
- sessionId,
1290
- providerId: "anthropic",
1291
- modelId: "claude-sonnet-4-6",
1292
- }),
1293
- interactive: true,
1294
- }),
1295
- );
1296
-
1297
- await manager.send({ sessionId, prompt: "hello" });
1298
- await manager.send({ sessionId, prompt: "again" });
1299
-
1300
- const persisted = persistSessionMessages.mock.calls[1]?.[1];
1301
- expect(persisted?.[1]).toMatchObject({
1302
- role: "assistant",
1303
- metrics: {
1304
- inputTokens: 33,
1305
- outputTokens: 12,
1306
- cacheReadTokens: 4,
1307
- cacheWriteTokens: 1,
1308
- cost: 0.42,
1309
- },
1310
- });
1311
- expect(persisted?.[3]).toMatchObject({
1312
- role: "assistant",
1313
- metrics: {
1314
- inputTokens: 10,
1315
- outputTokens: 5,
1316
- cacheReadTokens: 2,
1317
- cacheWriteTokens: 0,
1318
- cost: 0.12,
1319
- },
1320
- });
1321
- });
1322
-
1323
- it("persists rendered messages when a turn fails", async () => {
1324
- const sessionId = "sess-failed-turn";
1325
- const manifest = createManifest(sessionId);
1326
- const persistSessionMessages = vi.fn();
1327
- const sessionService = {
1328
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1329
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1330
- manifestPath: "/tmp/manifest-failed-turn.json",
1331
- messagesPath: "/tmp/messages-failed-turn.json",
1332
- manifest,
1333
- }),
1334
- persistSessionMessages,
1335
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1336
- writeSessionManifest: vi.fn(),
1337
- listSessions: vi.fn().mockResolvedValue([]),
1338
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1339
- };
1340
- const runtimeBuilder = {
1341
- build: vi.fn().mockReturnValue({
1342
- tools: [],
1343
- shutdown: vi.fn(),
1344
- }),
1345
- };
1346
- const renderedMessages = [
1347
- { role: "user", content: [{ type: "text", text: "hello" }] },
1348
- { role: "assistant", content: [{ type: "text", text: "partial" }] },
1349
- ];
1350
- const manager = new RuntimeHostUnderTest({
1351
- distinctId,
1352
- sessionService: sessionService as never,
1353
- runtimeBuilder,
1354
- createAgent: () =>
1355
- ({
1356
- run: vi.fn().mockRejectedValue(new Error("boom")),
1357
- continue: vi.fn(),
1358
- abort: vi.fn(),
1359
- restore: vi.fn(),
1360
- shutdown: vi.fn().mockResolvedValue(undefined),
1361
- getMessages: vi
1362
- .fn()
1363
- .mockReturnValueOnce([])
1364
- .mockReturnValue(renderedMessages),
1365
- messages: [],
1366
- }) as never,
1367
- });
1368
-
1369
- await expect(
1370
- manager.start(
1371
- normalizeStartInput({
1372
- config: createConfig({ sessionId }),
1373
- prompt: "hello",
1374
- interactive: false,
1375
- }),
1376
- ),
1377
- ).rejects.toThrow("boom");
1378
-
1379
- expect(persistSessionMessages).toHaveBeenCalledTimes(1);
1380
- expect(persistSessionMessages).toHaveBeenCalledWith(
1381
- sessionId,
1382
- renderedMessages,
1383
- "You are a test agent",
1384
- );
1385
- expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
1386
- sessionId,
1387
- "failed",
1388
- 1,
1389
- );
1390
- });
1391
-
1392
- it("uses run for first send then continue for subsequent sends", async () => {
1393
- const sessionId = "sess-2";
1394
- const manifest = createManifest(sessionId);
1395
- const sessionService = {
1396
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1397
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1398
- manifestPath: "/tmp/manifest-2.json",
1399
- messagesPath: "/tmp/messages-2.json",
1400
- manifest,
1401
- }),
1402
- persistSessionMessages: vi.fn(),
1403
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1404
- writeSessionManifest: vi.fn(),
1405
- listSessions: vi.fn().mockResolvedValue([]),
1406
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1407
- };
1408
- const runtimeBuilder = {
1409
- build: vi.fn().mockReturnValue({
1410
- tools: [],
1411
- shutdown: vi.fn(),
1412
- }),
1413
- };
1414
- const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
1415
- const continueFn = vi
1416
- .fn()
1417
- .mockResolvedValue(createResult({ text: "second" }));
1418
- const manager = new RuntimeHostUnderTest({
1419
- distinctId,
1420
- sessionService: sessionService as never,
1421
- runtimeBuilder,
1422
- createAgent: () =>
1423
- ({
1424
- run,
1425
- continue: continueFn,
1426
- abort: vi.fn(),
1427
- shutdown: vi.fn().mockResolvedValue(undefined),
1428
- getMessages: vi.fn().mockReturnValue([]),
1429
- messages: [],
1430
- }) as never,
1431
- });
1432
-
1433
- await manager.start(
1434
- normalizeStartInput({
1435
- config: createConfig({ sessionId }),
1436
- interactive: true,
1437
- }),
1438
- );
1439
- const first = await manager.send({ sessionId, prompt: "first" });
1440
- const second = await manager.send({ sessionId, prompt: "second" });
1441
-
1442
- expect(first?.text).toBe("first");
1443
- expect(second?.text).toBe("second");
1444
- expect(run).toHaveBeenCalledTimes(1);
1445
- expect(continueFn).toHaveBeenCalledTimes(1);
1446
- expect(sessionService.persistSessionMessages).toHaveBeenCalledTimes(2);
1447
- });
1448
-
1449
- it("tracks accumulated usage per session across turns", async () => {
1450
- const sessionId = "sess-usage";
1451
- const manifest = createManifest(sessionId);
1452
- const sessionService = {
1453
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1454
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1455
- manifestPath: "/tmp/manifest-usage.json",
1456
- messagesPath: "/tmp/messages-usage.json",
1457
- manifest,
1458
- }),
1459
- persistSessionMessages: vi.fn(),
1460
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1461
- writeSessionManifest: vi.fn(),
1462
- listSessions: vi.fn().mockResolvedValue([]),
1463
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1464
- };
1465
- const runtimeBuilder = {
1466
- build: vi.fn().mockReturnValue({
1467
- tools: [],
1468
- shutdown: vi.fn(),
1469
- }),
1470
- };
1471
- const run = vi.fn().mockResolvedValue(
1472
- createResult({
1473
- text: "first",
1474
- usage: {
1475
- inputTokens: 10,
1476
- outputTokens: 3,
1477
- cacheReadTokens: 1,
1478
- cacheWriteTokens: 2,
1479
- totalCost: 0.11,
1480
- },
1481
- }),
1482
- );
1483
- const continueFn = vi.fn().mockResolvedValue(
1484
- createResult({
1485
- text: "second",
1486
- usage: {
1487
- inputTokens: 8,
1488
- outputTokens: 4,
1489
- cacheReadTokens: 2,
1490
- cacheWriteTokens: 0,
1491
- totalCost: 0.09,
1492
- },
1493
- }),
1494
- );
1495
- const manager = new RuntimeHostUnderTest({
1496
- distinctId,
1497
- sessionService: sessionService as never,
1498
- runtimeBuilder,
1499
- createAgent: () =>
1500
- ({
1501
- run,
1502
- continue: continueFn,
1503
- abort: vi.fn(),
1504
- shutdown: vi.fn().mockResolvedValue(undefined),
1505
- getMessages: vi.fn().mockReturnValue([]),
1506
- messages: [],
1507
- }) as never,
1508
- });
1509
-
1510
- await manager.start(
1511
- normalizeStartInput({
1512
- config: createConfig({ sessionId }),
1513
- interactive: true,
1514
- }),
1515
- );
1516
-
1517
- await manager.send({ sessionId, prompt: "first" });
1518
- expect(await manager.getAccumulatedUsage(sessionId)).toEqual({
1519
- inputTokens: 10,
1520
- outputTokens: 3,
1521
- cacheReadTokens: 1,
1522
- cacheWriteTokens: 2,
1523
- totalCost: 0.11,
1524
- });
1525
-
1526
- await manager.send({ sessionId, prompt: "second" });
1527
- expect(await manager.getAccumulatedUsage(sessionId)).toEqual({
1528
- inputTokens: 18,
1529
- outputTokens: 7,
1530
- cacheReadTokens: 3,
1531
- cacheWriteTokens: 2,
1532
- totalCost: 0.2,
1533
- });
1534
- });
1535
-
1536
- it("queues sends with explicit queue or steer delivery and emits snapshots", async () => {
1537
- const sessionId = "sess-delivery-queue";
1538
- const manifest = createManifest(sessionId);
1539
- const sessionService = {
1540
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1541
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1542
- manifestPath: "/tmp/manifest-queue.json",
1543
- messagesPath: "/tmp/messages-queue.json",
1544
- manifest,
1545
- }),
1546
- persistSessionMessages: vi.fn(),
1547
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1548
- writeSessionManifest: vi.fn(),
1549
- listSessions: vi.fn().mockResolvedValue([]),
1550
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1551
- };
1552
- const runtimeBuilder = {
1553
- build: vi.fn().mockReturnValue({
1554
- tools: [],
1555
- shutdown: vi.fn(),
1556
- }),
1557
- };
1558
- let canStartRun = false;
1559
- const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
1560
- const continueFn = vi
1561
- .fn()
1562
- .mockResolvedValue(createResult({ text: "next" }));
1563
- const manager = new RuntimeHostUnderTest({
1564
- distinctId,
1565
- sessionService: sessionService as never,
1566
- runtimeBuilder,
1567
- createAgent: () =>
1568
- ({
1569
- run,
1570
- continue: continueFn,
1571
- canStartRun: vi.fn(() => canStartRun),
1572
- abort: vi.fn(),
1573
- shutdown: vi.fn().mockResolvedValue(undefined),
1574
- getMessages: vi.fn().mockReturnValue([]),
1575
- messages: [],
1576
- }) as never,
1577
- });
1578
- const events: Array<unknown> = [];
1579
- manager.subscribe((event) => {
1580
- events.push(event);
1581
- });
1582
-
1583
- await manager.start(
1584
- normalizeStartInput({
1585
- config: createConfig({ sessionId }),
1586
- interactive: true,
1587
- }),
1588
- );
1589
-
1590
- await expect(
1591
- manager.send({ sessionId, prompt: "queued first", delivery: "queue" }),
1592
- ).resolves.toBeUndefined();
1593
- await expect(
1594
- manager.send({ sessionId, prompt: "queued second", delivery: "steer" }),
1595
- ).resolves.toBeUndefined();
1596
-
1597
- expect(run).not.toHaveBeenCalled();
1598
- expect(continueFn).not.toHaveBeenCalled();
1599
- const promptSnapshots = events
1600
- .filter((event) => {
1601
- return (
1602
- typeof event === "object" &&
1603
- event !== null &&
1604
- "type" in event &&
1605
- event.type === "pending_prompts"
1606
- );
1607
- })
1608
- .map((event) => (event as { payload: { prompts: unknown[] } }).payload);
1609
- expect(promptSnapshots.at(-1)).toEqual({
1610
- prompts: [
1611
- expect.objectContaining({
1612
- prompt: "queued second",
1613
- delivery: "steer",
1614
- attachmentCount: 0,
1615
- }),
1616
- expect.objectContaining({
1617
- prompt: "queued first",
1618
- delivery: "queue",
1619
- attachmentCount: 0,
1620
- }),
1621
- ],
1622
- sessionId,
1623
- });
1624
-
1625
- canStartRun = true;
1626
- await manager.send({ sessionId, prompt: "run now" });
1627
- expect(run).toHaveBeenCalledTimes(1);
1628
- expect(
1629
- events.some((event) => {
1630
- return (
1631
- typeof event === "object" &&
1632
- event !== null &&
1633
- "type" in event &&
1634
- event.type === "pending_prompt_submitted" &&
1635
- "payload" in event &&
1636
- (event.payload as { prompt?: string }).prompt === "queued second"
1637
- );
1638
- }),
1639
- ).toBe(true);
1640
- });
1641
-
1642
- it("returns undefined accumulated usage for unknown sessions", async () => {
1643
- const manager = new RuntimeHostUnderTest({
1644
- distinctId,
1645
- sessionService: {
1646
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1647
- listSessions: vi.fn().mockResolvedValue([]),
1648
- deleteSession: vi.fn().mockResolvedValue({ deleted: false }),
1649
- } as never,
1650
- runtimeBuilder: {
1651
- build: vi.fn().mockReturnValue({
1652
- tools: [],
1653
- shutdown: vi.fn(),
1654
- }),
1655
- },
1656
- createAgent: () =>
1657
- ({
1658
- run: vi.fn(),
1659
- continue: vi.fn(),
1660
- abort: vi.fn(),
1661
- shutdown: vi.fn().mockResolvedValue(undefined),
1662
- getMessages: vi.fn().mockReturnValue([]),
1663
- messages: [],
1664
- }) as never,
1665
- });
1666
-
1667
- expect(
1668
- await manager.getAccumulatedUsage("missing-session"),
1669
- ).toBeUndefined();
1670
- });
1671
-
1672
- it("marks a failed single-run session as failed when run throws", async () => {
1673
- const sessionId = "sess-fail";
1674
- const manifest = createManifest(sessionId);
1675
- const sessionService = {
1676
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1677
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1678
- manifestPath: "/tmp/manifest-fail.json",
1679
- messagesPath: "/tmp/messages-fail.json",
1680
- manifest,
1681
- }),
1682
- persistSessionMessages: vi.fn(),
1683
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1684
- writeSessionManifest: vi.fn(),
1685
- listSessions: vi.fn().mockResolvedValue([]),
1686
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1687
- };
1688
- const runtimeShutdown = vi.fn();
1689
- const runtimeBuilder = {
1690
- build: vi.fn().mockReturnValue({
1691
- tools: [],
1692
- shutdown: runtimeShutdown,
1693
- }),
1694
- };
1695
- const run = vi.fn().mockRejectedValue(new Error("run failed"));
1696
- const agentShutdown = vi.fn().mockResolvedValue(undefined);
1697
- const manager = new RuntimeHostUnderTest({
1698
- distinctId,
1699
- sessionService: sessionService as never,
1700
- runtimeBuilder,
1701
- createAgent: () =>
1702
- ({
1703
- run,
1704
- continue: vi.fn(),
1705
- abort: vi.fn(),
1706
- shutdown: agentShutdown,
1707
- getMessages: vi.fn().mockReturnValue([]),
1708
- messages: [],
1709
- }) as never,
1710
- });
1711
-
1712
- await expect(
1713
- manager.start(
1714
- normalizeStartInput({
1715
- config: createConfig({ sessionId }),
1716
- prompt: "hello",
1717
- interactive: false,
1718
- }),
1719
- ),
1720
- ).rejects.toThrow("run failed");
1721
- expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
1722
- sessionId,
1723
- "failed",
1724
- 1,
1725
- );
1726
- expect(agentShutdown).toHaveBeenCalledTimes(1);
1727
- expect(runtimeShutdown).toHaveBeenCalledTimes(1);
1728
- });
1729
-
1730
- it("does not persist or emit shutdown hooks when no prompt was submitted", async () => {
1731
- const sessionService = {
1732
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1733
- createRootSessionWithArtifacts: vi.fn(),
1734
- persistSessionMessages: vi.fn(),
1735
- updateSessionStatus: vi.fn(),
1736
- writeSessionManifest: vi.fn(),
1737
- listSessions: vi.fn().mockResolvedValue([]),
1738
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1739
- };
1740
- const runtimeShutdown = vi.fn();
1741
- const runtimeBuilder = {
1742
- build: vi.fn().mockReturnValue({
1743
- tools: [],
1744
- shutdown: runtimeShutdown,
1745
- }),
1746
- };
1747
- const agentShutdown = vi.fn().mockResolvedValue(undefined);
1748
- const manager = new RuntimeHostUnderTest({
1749
- distinctId,
1750
- sessionService: sessionService as never,
1751
- runtimeBuilder,
1752
- createAgent: () =>
1753
- ({
1754
- run: vi.fn(),
1755
- continue: vi.fn(),
1756
- abort: vi.fn(),
1757
- shutdown: agentShutdown,
1758
- getMessages: vi.fn().mockReturnValue([]),
1759
- messages: [],
1760
- }) as never,
1761
- });
1762
-
1763
- const started = await manager.start(
1764
- normalizeStartInput({
1765
- config: createConfig({ sessionId: "sess-no-prompt" }),
1766
- interactive: true,
1767
- }),
1768
- );
1769
- await manager.stop(started.sessionId);
1770
-
1771
- expect(
1772
- sessionService.createRootSessionWithArtifacts,
1773
- ).not.toHaveBeenCalled();
1774
- expect(sessionService.updateSessionStatus).not.toHaveBeenCalled();
1775
- expect(agentShutdown).not.toHaveBeenCalled();
1776
- expect(runtimeShutdown).toHaveBeenCalledTimes(1);
1777
- });
1778
-
1779
- it("updates agent connection with refreshed OAuth key before turn", async () => {
1780
- const sessionId = "sess-oauth";
1781
- const manifest = createManifest(sessionId);
1782
- const sessionService = {
1783
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1784
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1785
- manifestPath: "/tmp/manifest-oauth.json",
1786
- messagesPath: "/tmp/messages-oauth.json",
1787
- manifest,
1788
- }),
1789
- persistSessionMessages: vi.fn(),
1790
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1791
- writeSessionManifest: vi.fn(),
1792
- listSessions: vi.fn().mockResolvedValue([]),
1793
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1794
- };
1795
- const updateConnectionDefaults = vi.fn();
1796
- const runtimeBuilder = {
1797
- build: vi.fn().mockReturnValue({
1798
- tools: [],
1799
- delegatedAgentConfigProvider: {
1800
- getRuntimeConfig: vi.fn(),
1801
- getConnectionConfig: vi.fn(),
1802
- updateConnectionDefaults,
1803
- },
1804
- shutdown: vi.fn(),
1805
- }),
1806
- };
1807
- const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
1808
- const updateConnection = vi.fn();
1809
- const manager = new RuntimeHostUnderTest({
1810
- distinctId,
1811
- sessionService: sessionService as never,
1812
- runtimeBuilder,
1813
- oauthTokenManager: {
1814
- resolveProviderApiKey: vi.fn().mockResolvedValue({
1815
- providerId: "openai-codex",
1816
- apiKey: "oauth-access-new",
1817
- refreshed: true,
1818
- }),
1819
- } as never,
1820
- createAgent: () =>
1821
- ({
1822
- run,
1823
- continue: vi.fn(),
1824
- abort: vi.fn(),
1825
- restore: vi.fn(),
1826
- updateConnection,
1827
- shutdown: vi.fn().mockResolvedValue(undefined),
1828
- getMessages: vi.fn().mockReturnValue([]),
1829
- messages: [],
1830
- }) as never,
1831
- });
1832
-
1833
- await manager.start(
1834
- normalizeStartInput({
1835
- config: createConfig({
1836
- sessionId,
1837
- providerId: "openai-codex",
1838
- apiKey: "oauth-access-old",
1839
- }),
1840
- interactive: true,
1841
- }),
1842
- );
1843
- await manager.send({ sessionId, prompt: "hello" });
1844
-
1845
- expect(updateConnectionDefaults).toHaveBeenCalledWith({
1846
- apiKey: "oauth-access-new",
1847
- });
1848
- expect(updateConnection).toHaveBeenCalledWith({
1849
- apiKey: "oauth-access-new",
1850
- });
1851
- expect(run).toHaveBeenCalledTimes(1);
1852
- });
1853
-
1854
- it("hydrates provider-specific config from provider settings", async () => {
1855
- const sessionId = "sess-provider-config";
1856
- const manifest = createManifest(sessionId);
1857
- const sessionService = {
1858
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1859
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1860
- manifestPath: "/tmp/manifest-provider-config.json",
1861
- messagesPath: "/tmp/messages-provider-config.json",
1862
- manifest,
1863
- }),
1864
- persistSessionMessages: vi.fn(),
1865
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1866
- writeSessionManifest: vi.fn(),
1867
- listSessions: vi.fn().mockResolvedValue([]),
1868
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1869
- };
1870
- const run = vi.fn().mockResolvedValue(
1871
- createResult({
1872
- model: {
1873
- id: "claude-sonnet-4@20250514",
1874
- provider: "vertex",
1875
- },
1876
- }),
1877
- );
1878
- const createAgent = vi.fn().mockReturnValue({
1879
- run,
1880
- continue: vi.fn(),
1881
- abort: vi.fn(),
1882
- restore: vi.fn(),
1883
- shutdown: vi.fn().mockResolvedValue(undefined),
1884
- getMessages: vi.fn().mockReturnValue([]),
1885
- messages: [],
1886
- });
1887
- const manager = new RuntimeHostUnderTest({
1888
- distinctId,
1889
- sessionService: sessionService as never,
1890
- runtimeBuilder: {
1891
- build: vi.fn().mockReturnValue({
1892
- tools: [],
1893
- shutdown: vi.fn(),
1894
- }),
1895
- },
1896
- createAgent: createAgent as never,
1897
- providerSettingsManager: {
1898
- getProviderSettings: vi.fn().mockReturnValue({
1899
- provider: "vertex",
1900
- gcp: {
1901
- projectId: "test-project",
1902
- region: "us-central1",
1903
- },
1904
- }),
1905
- } as never,
1906
- });
1907
-
1908
- await manager.start(
1909
- normalizeStartInput({
1910
- config: createConfig({
1911
- sessionId,
1912
- providerId: "vertex",
1913
- modelId: "claude-sonnet-4@20250514",
1914
- }),
1915
- interactive: true,
1916
- }),
1917
- );
1918
- await manager.send({ sessionId, prompt: "hello" });
1919
-
1920
- expect(createAgent).toHaveBeenCalledWith(
1921
- expect.objectContaining({
1922
- providerId: "vertex",
1923
- modelId: "claude-sonnet-4@20250514",
1924
- providerConfig: expect.objectContaining({
1925
- providerId: "vertex",
1926
- modelId: "claude-sonnet-4@20250514",
1927
- gcp: {
1928
- projectId: "test-project",
1929
- region: "us-central1",
1930
- },
1931
- }),
1932
- }),
1933
- );
1934
- });
1935
-
1936
- it("forwards loopDetection config to the agent constructor", async () => {
1937
- const sessionId = "sess-loop-detection";
1938
- const manifest = createManifest(sessionId);
1939
- const sessionService = {
1940
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
1941
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
1942
- manifestPath: "/tmp/manifest-loop.json",
1943
- messagesPath: "/tmp/messages-loop.json",
1944
- manifest,
1945
- }),
1946
- persistSessionMessages: vi.fn(),
1947
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
1948
- writeSessionManifest: vi.fn(),
1949
- listSessions: vi.fn().mockResolvedValue([]),
1950
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
1951
- };
1952
- const run = vi.fn().mockResolvedValue(createResult());
1953
- const createAgent = vi.fn().mockReturnValue({
1954
- run,
1955
- continue: vi.fn(),
1956
- abort: vi.fn(),
1957
- restore: vi.fn(),
1958
- shutdown: vi.fn().mockResolvedValue(undefined),
1959
- getMessages: vi.fn().mockReturnValue([]),
1960
- messages: [],
1961
- });
1962
- const manager = new RuntimeHostUnderTest({
1963
- distinctId,
1964
- sessionService: sessionService as never,
1965
- runtimeBuilder: {
1966
- build: vi.fn().mockReturnValue({
1967
- tools: [],
1968
- shutdown: vi.fn(),
1969
- }),
1970
- },
1971
- createAgent: createAgent as never,
1972
- });
1973
-
1974
- await manager.start(
1975
- normalizeStartInput({
1976
- config: createConfig({
1977
- sessionId,
1978
- execution: {
1979
- loopDetection: { softThreshold: 4, hardThreshold: 8 },
1980
- },
1981
- }),
1982
- interactive: true,
1983
- }),
1984
- );
1985
- await manager.send({ sessionId, prompt: "test" });
1986
-
1987
- expect(createAgent).toHaveBeenCalledWith(
1988
- expect.objectContaining({
1989
- execution: {
1990
- loopDetection: { softThreshold: 4, hardThreshold: 8 },
1991
- },
1992
- }),
1993
- );
1994
- });
1995
-
1996
- it("injects a core-owned compaction prepareTurn callback into the agent constructor", async () => {
1997
- const sessionId = "sess-compaction";
1998
- const manifest = createManifest(sessionId);
1999
- const sessionService = {
2000
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2001
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2002
- manifestPath: "/tmp/manifest-compaction.json",
2003
- messagesPath: "/tmp/messages-compaction.json",
2004
- manifest,
2005
- }),
2006
- persistSessionMessages: vi.fn(),
2007
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2008
- writeSessionManifest: vi.fn(),
2009
- listSessions: vi.fn().mockResolvedValue([]),
2010
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2011
- };
2012
- const run = vi.fn().mockResolvedValue(createResult());
2013
- const createAgent = vi.fn().mockReturnValue({
2014
- run,
2015
- continue: vi.fn(),
2016
- abort: vi.fn(),
2017
- restore: vi.fn(),
2018
- shutdown: vi.fn().mockResolvedValue(undefined),
2019
- getMessages: vi.fn().mockReturnValue([]),
2020
- messages: [],
2021
- });
2022
- const compact = vi.fn();
2023
- const manager = new RuntimeHostUnderTest({
2024
- distinctId,
2025
- sessionService: sessionService as never,
2026
- runtimeBuilder: {
2027
- build: vi.fn().mockReturnValue({
2028
- tools: [],
2029
- shutdown: vi.fn(),
2030
- }),
2031
- },
2032
- createAgent: createAgent as never,
2033
- });
2034
-
2035
- await manager.start(
2036
- normalizeStartInput({
2037
- config: createConfig({
2038
- sessionId,
2039
- compaction: {
2040
- enabled: true,
2041
- strategy: "basic",
2042
- compact,
2043
- },
2044
- }),
2045
- interactive: true,
2046
- }),
2047
- );
2048
- await manager.send({ sessionId, prompt: "test" });
2049
-
2050
- expect(createAgent).toHaveBeenCalledWith(
2051
- expect.objectContaining({
2052
- prepareTurn: expect.any(Function),
2053
- }),
2054
- );
2055
- });
2056
-
2057
- it("formats prompt in core and merges explicit + mention user files", async () => {
2058
- const tempCwd = mkdtempSync(join(tmpdir(), "core-session-format-"));
2059
- try {
2060
- const srcDir = join(tempCwd, "src");
2061
- const docsDir = join(tempCwd, "docs");
2062
- mkdirSync(srcDir, { recursive: true });
2063
- mkdirSync(docsDir, { recursive: true });
2064
- const mentionPath = join(srcDir, "app.ts");
2065
- const explicitPath = join(docsDir, "note.md");
2066
- writeFileSync(mentionPath, "export const v = 1;\n", "utf8");
2067
- writeFileSync(explicitPath, "note\n", "utf8");
2068
-
2069
- const sessionId = "sess-format";
2070
- const manifest = createManifest(sessionId);
2071
- const sessionService = {
2072
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2073
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2074
- manifestPath: "/tmp/manifest-format.json",
2075
- messagesPath: "/tmp/messages-format.json",
2076
- manifest,
2077
- }),
2078
- persistSessionMessages: vi.fn(),
2079
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2080
- writeSessionManifest: vi.fn(),
2081
- listSessions: vi.fn().mockResolvedValue([]),
2082
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2083
- };
2084
- const run = vi.fn().mockResolvedValue(createResult({ text: "ok" }));
2085
- const manager = new RuntimeHostUnderTest({
2086
- distinctId,
2087
- sessionService: sessionService as never,
2088
- runtimeBuilder: {
2089
- build: vi.fn().mockReturnValue({
2090
- tools: [],
2091
- shutdown: vi.fn(),
2092
- }),
2093
- },
2094
- createAgent: () =>
2095
- ({
2096
- run,
2097
- continue: vi.fn(),
2098
- abort: vi.fn(),
2099
- shutdown: vi.fn().mockResolvedValue(undefined),
2100
- getMessages: vi.fn().mockReturnValue([]),
2101
- messages: [],
2102
- }) as never,
2103
- });
2104
-
2105
- await manager.start(
2106
- normalizeStartInput({
2107
- config: createConfig({
2108
- sessionId,
2109
- cwd: join(tempCwd, "docs"),
2110
- workspaceRoot: tempCwd,
2111
- }),
2112
- interactive: true,
2113
- }),
2114
- );
2115
- await manager.send({
2116
- sessionId,
2117
- prompt: '<user_input mode="act">explain @src/app.ts</user_input>',
2118
- userFiles: ["note.md"],
2119
- });
2120
-
2121
- expect(run).toHaveBeenCalledWith(
2122
- '<user_input mode="act">explain @src/app.ts</user_input>',
2123
- undefined,
2124
- expect.arrayContaining([mentionPath, explicitPath]),
2125
- );
2126
- } finally {
2127
- rmSync(tempCwd, { recursive: true, force: true });
2128
- }
2129
- });
2130
-
2131
- it("force refreshes and retries once when turn fails with auth error", async () => {
2132
- const sessionId = "sess-oauth-retry";
2133
- const manifest = createManifest(sessionId);
2134
- const sessionService = {
2135
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2136
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2137
- manifestPath: "/tmp/manifest-oauth-retry.json",
2138
- messagesPath: "/tmp/messages-oauth-retry.json",
2139
- manifest,
2140
- }),
2141
- persistSessionMessages: vi.fn(),
2142
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2143
- writeSessionManifest: vi.fn(),
2144
- listSessions: vi.fn().mockResolvedValue([]),
2145
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2146
- };
2147
- const updateConnectionDefaults = vi.fn();
2148
- const runtimeBuilder = {
2149
- build: vi.fn().mockReturnValue({
2150
- tools: [],
2151
- delegatedAgentConfigProvider: {
2152
- getRuntimeConfig: vi.fn(),
2153
- getConnectionConfig: vi.fn(),
2154
- updateConnectionDefaults,
2155
- },
2156
- shutdown: vi.fn(),
2157
- }),
2158
- };
2159
- const run = vi
2160
- .fn()
2161
- .mockRejectedValueOnce(new Error("401 Unauthorized"))
2162
- .mockResolvedValueOnce(createResult({ text: "retried" }));
2163
- const restore = vi.fn();
2164
- const updateConnection = vi.fn();
2165
- const resolveProviderApiKey = vi
2166
- .fn()
2167
- .mockResolvedValueOnce(null)
2168
- .mockResolvedValueOnce({
2169
- providerId: "openai-codex",
2170
- apiKey: "oauth-access-new",
2171
- refreshed: true,
2172
- });
2173
- const manager = new RuntimeHostUnderTest({
2174
- distinctId,
2175
- sessionService: sessionService as never,
2176
- runtimeBuilder,
2177
- oauthTokenManager: {
2178
- resolveProviderApiKey,
2179
- } as never,
2180
- createAgent: () =>
2181
- ({
2182
- run,
2183
- continue: vi.fn(),
2184
- abort: vi.fn(),
2185
- restore,
2186
- updateConnection,
2187
- shutdown: vi.fn().mockResolvedValue(undefined),
2188
- getMessages: vi.fn().mockReturnValue([]),
2189
- messages: [],
2190
- }) as never,
2191
- });
2192
-
2193
- await manager.start(
2194
- normalizeStartInput({
2195
- config: createConfig({
2196
- sessionId,
2197
- providerId: "openai-codex",
2198
- apiKey: "oauth-access-old",
2199
- }),
2200
- interactive: true,
2201
- }),
2202
- );
2203
- const result = await manager.send({ sessionId, prompt: "hello" });
2204
-
2205
- expect(result?.text).toBe("retried");
2206
- expect(run).toHaveBeenCalledTimes(2);
2207
- expect(restore).toHaveBeenCalledTimes(1);
2208
- expect(resolveProviderApiKey).toHaveBeenNthCalledWith(1, {
2209
- providerId: "openai-codex",
2210
- forceRefresh: undefined,
2211
- });
2212
- expect(resolveProviderApiKey).toHaveBeenNthCalledWith(2, {
2213
- providerId: "openai-codex",
2214
- forceRefresh: true,
2215
- });
2216
- expect(updateConnection).toHaveBeenCalledWith({
2217
- apiKey: "oauth-access-new",
2218
- });
2219
- expect(updateConnectionDefaults).toHaveBeenCalledWith({
2220
- apiKey: "oauth-access-new",
2221
- });
2222
- });
2223
-
2224
- it("auto-continues when async teammate runs complete after lead turn", async () => {
2225
- const sessionId = "sess-team-auto-continue";
2226
- const manifest = createManifest(sessionId);
2227
- const sessionService = {
2228
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2229
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2230
- manifestPath: "/tmp/manifest-team-auto-continue.json",
2231
- messagesPath: "/tmp/messages-team-auto-continue.json",
2232
- manifest,
2233
- }),
2234
- persistSessionMessages: vi.fn(),
2235
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2236
- writeSessionManifest: vi.fn(),
2237
- listSessions: vi.fn().mockResolvedValue([]),
2238
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2239
- };
2240
-
2241
- let onTeamEvent: ((event: unknown) => void) | undefined;
2242
- const runtimeBuilder = {
2243
- build: vi
2244
- .fn()
2245
- .mockImplementation(
2246
- (input: { onTeamEvent?: (event: unknown) => void }) => {
2247
- onTeamEvent = input.onTeamEvent;
2248
- return {
2249
- tools: [],
2250
- shutdown: vi.fn(),
2251
- };
2252
- },
2253
- ),
2254
- };
2255
-
2256
- const run = vi.fn().mockImplementation(async () => {
2257
- onTeamEvent?.({
2258
- type: "run_started",
2259
- run: {
2260
- id: "run_0001",
2261
- agentId: "investigator",
2262
- status: "running",
2263
- message: "Investigate",
2264
- priority: 0,
2265
- retryCount: 0,
2266
- maxRetries: 0,
2267
- startedAt: new Date("2026-01-01T00:00:00.000Z"),
2268
- },
2269
- });
2270
- setTimeout(() => {
2271
- onTeamEvent?.({
2272
- type: "run_completed",
2273
- run: {
2274
- id: "run_0001",
2275
- agentId: "investigator",
2276
- status: "completed",
2277
- message: "Investigate",
2278
- priority: 0,
2279
- retryCount: 0,
2280
- maxRetries: 0,
2281
- startedAt: new Date("2026-01-01T00:00:00.000Z"),
2282
- endedAt: new Date("2026-01-01T00:00:02.000Z"),
2283
- result: createResult({ iterations: 3 }),
2284
- },
2285
- });
2286
- }, 0);
2287
- return createResult({
2288
- text: "lead scheduled teammate",
2289
- messages: [
2290
- { role: "user", content: "run teammate work" },
2291
- { role: "assistant", content: "lead scheduled teammate" },
2292
- ],
2293
- });
2294
- });
2295
- const continueFn = vi.fn().mockResolvedValue(
2296
- createResult({
2297
- text: "lead processed teammate result",
2298
- messages: [
2299
- { role: "user", content: "run teammate work" },
2300
- { role: "assistant", content: "lead scheduled teammate" },
2301
- {
2302
- role: "user",
2303
- content:
2304
- "System-delivered teammate async run updates:\n- investigator completed",
2305
- },
2306
- { role: "assistant", content: "lead processed teammate result" },
2307
- ],
2308
- }),
2309
- );
2310
- const manager = new RuntimeHostUnderTest({
2311
- distinctId,
2312
- sessionService: sessionService as never,
2313
- runtimeBuilder,
2314
- createAgent: () =>
2315
- ({
2316
- run,
2317
- continue: continueFn,
2318
- abort: vi.fn(),
2319
- shutdown: vi.fn().mockResolvedValue(undefined),
2320
- getMessages: vi.fn().mockReturnValue([]),
2321
- messages: [],
2322
- }) as never,
2323
- });
2324
-
2325
- await manager.start(
2326
- normalizeStartInput({
2327
- config: createConfig({ sessionId }),
2328
- interactive: false,
2329
- }),
2330
- );
2331
- const result = await manager.send({
2332
- sessionId,
2333
- prompt: "run teammate work",
2334
- });
2335
-
2336
- expect(result?.text).toBe("lead processed teammate result");
2337
- expect(run).toHaveBeenCalledTimes(1);
2338
- expect(continueFn).toHaveBeenCalledTimes(1);
2339
- expect(continueFn.mock.calls[0]?.[0]).toContain(
2340
- "System-delivered teammate async run updates:",
2341
- );
2342
- const finalPersistedMessages = (
2343
- sessionService.persistSessionMessages as ReturnType<typeof vi.fn>
2344
- ).mock.calls.at(-1)?.[1] as Array<Record<string, unknown>> | undefined;
2345
- expect(finalPersistedMessages?.at(-1)).toMatchObject({
2346
- role: "assistant",
2347
- metrics: {
2348
- inputTokens: 1,
2349
- outputTokens: 2,
2350
- cost: 0,
2351
- },
2352
- modelInfo: {
2353
- id: "mock-model",
2354
- provider: "mock-provider",
2355
- },
2356
- });
2357
- expect(sessionService.updateSessionStatus).toHaveBeenCalledWith(
2358
- sessionId,
2359
- "completed",
2360
- 0,
2361
- );
2362
- });
2363
-
2364
- it("persists failed teammate task messages for team-task sub-sessions", async () => {
2365
- const sessionId = "sess-team-task-failure-messages";
2366
- const manifest = createManifest(sessionId);
2367
- const sessionService = {
2368
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2369
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2370
- manifestPath: "/tmp/manifest-team-task-failure-messages.json",
2371
- messagesPath: "/tmp/messages-team-task-failure-messages.json",
2372
- manifest,
2373
- }),
2374
- persistSessionMessages: vi.fn(),
2375
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2376
- writeSessionManifest: vi.fn(),
2377
- listSessions: vi.fn().mockResolvedValue([]),
2378
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2379
- onTeamTaskStart: vi.fn().mockResolvedValue(undefined),
2380
- onTeamTaskEnd: vi.fn().mockResolvedValue(undefined),
2381
- };
2382
-
2383
- let onTeamEvent: ((event: unknown) => void) | undefined;
2384
- const runtimeBuilder = {
2385
- build: vi
2386
- .fn()
2387
- .mockImplementation(
2388
- (input: { onTeamEvent?: (event: unknown) => void }) => {
2389
- onTeamEvent = input.onTeamEvent;
2390
- return {
2391
- tools: [],
2392
- shutdown: vi.fn(),
2393
- };
2394
- },
2395
- ),
2396
- };
2397
-
2398
- const failedMessages = [
2399
- { role: "user", content: [{ type: "text", text: "delegated prompt" }] },
2400
- { role: "assistant", content: [{ type: "text", text: "partial work" }] },
2401
- ];
2402
- const manager = new RuntimeHostUnderTest({
2403
- distinctId,
2404
- sessionService: sessionService as never,
2405
- runtimeBuilder,
2406
- createAgent: () =>
2407
- ({
2408
- run: vi.fn().mockImplementation(async () => {
2409
- onTeamEvent?.({
2410
- type: "task_start",
2411
- agentId: "providers-investigator",
2412
- message: "Investigate provider boundaries",
2413
- });
2414
- onTeamEvent?.({
2415
- type: "task_end",
2416
- agentId: "providers-investigator",
2417
- error: new Error("401 Unauthorized"),
2418
- messages: failedMessages,
2419
- });
2420
- return createResult({ text: "lead handled failure" });
2421
- }),
2422
- continue: vi.fn(),
2423
- abort: vi.fn(),
2424
- shutdown: vi.fn().mockResolvedValue(undefined),
2425
- getMessages: vi.fn().mockReturnValue([]),
2426
- messages: [],
2427
- }) as never,
2428
- });
2429
-
2430
- await manager.start(
2431
- normalizeStartInput({
2432
- config: createConfig({ sessionId }),
2433
- prompt: "run teammate work",
2434
- interactive: false,
2435
- }),
2436
- );
2437
-
2438
- expect(sessionService.onTeamTaskStart).toHaveBeenCalledTimes(1);
2439
- expect(sessionService.onTeamTaskEnd).toHaveBeenCalledWith(
2440
- sessionId,
2441
- "providers-investigator",
2442
- "failed",
2443
- "[error] 401 Unauthorized",
2444
- undefined,
2445
- failedMessages,
2446
- );
2447
- });
2448
-
2449
- it("persists teammate progress updates for team-task sub-sessions", async () => {
2450
- const sessionId = "sess-team-task-progress";
2451
- const manifest = createManifest(sessionId);
2452
- const sessionService = {
2453
- ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
2454
- createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
2455
- manifestPath: "/tmp/manifest-team-task-progress.json",
2456
- messagesPath: "/tmp/messages-team-task-progress.json",
2457
- manifest,
2458
- }),
2459
- persistSessionMessages: vi.fn(),
2460
- updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
2461
- writeSessionManifest: vi.fn(),
2462
- listSessions: vi.fn().mockResolvedValue([]),
2463
- deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
2464
- onTeamTaskStart: vi.fn().mockResolvedValue(undefined),
2465
- onTeamTaskEnd: vi.fn().mockResolvedValue(undefined),
2466
- onTeamTaskProgress: vi.fn().mockResolvedValue(undefined),
2467
- };
2468
-
2469
- let onTeamEvent: ((event: unknown) => void) | undefined;
2470
- const runtimeBuilder = {
2471
- build: vi
2472
- .fn()
2473
- .mockImplementation(
2474
- (input: { onTeamEvent?: (event: unknown) => void }) => {
2475
- onTeamEvent = input.onTeamEvent;
2476
- return {
2477
- tools: [],
2478
- shutdown: vi.fn(),
2479
- };
2480
- },
2481
- ),
2482
- };
2483
-
2484
- const manager = new RuntimeHostUnderTest({
2485
- distinctId,
2486
- sessionService: sessionService as never,
2487
- runtimeBuilder,
2488
- createAgent: () =>
2489
- ({
2490
- run: vi.fn().mockImplementation(async () => {
2491
- onTeamEvent?.({
2492
- type: "task_start",
2493
- agentId: "providers-investigator",
2494
- message: "Investigate provider boundaries",
2495
- });
2496
- onTeamEvent?.({
2497
- type: "run_progress",
2498
- run: {
2499
- id: "run_00002",
2500
- agentId: "providers-investigator",
2501
- status: "running",
2502
- message: "Investigate provider boundaries",
2503
- priority: 0,
2504
- retryCount: 0,
2505
- maxRetries: 0,
2506
- continueConversation: false,
2507
- startedAt: new Date("2026-01-01T00:00:00.000Z"),
2508
- lastProgressAt: new Date("2026-01-01T00:00:01.000Z"),
2509
- lastProgressMessage: "heartbeat",
2510
- currentActivity: "heartbeat",
2511
- },
2512
- message: "heartbeat",
2513
- });
2514
- onTeamEvent?.({
2515
- type: "agent_event",
2516
- agentId: "providers-investigator",
2517
- event: {
2518
- type: "content_start",
2519
- contentType: "text",
2520
- text: "Drafting the provider boundary analysis now.",
2521
- },
2522
- });
2523
- onTeamEvent?.({
2524
- type: "task_end",
2525
- agentId: "providers-investigator",
2526
- result: createResult(),
2527
- });
2528
- return createResult({ text: "lead handled progress" });
2529
- }),
2530
- continue: vi.fn(),
2531
- abort: vi.fn(),
2532
- shutdown: vi.fn().mockResolvedValue(undefined),
2533
- getMessages: vi.fn().mockReturnValue([]),
2534
- messages: [],
2535
- }) as never,
2536
- });
2537
-
2538
- await manager.start(
2539
- normalizeStartInput({
2540
- config: createConfig({ sessionId }),
2541
- prompt: "run teammate work",
2542
- interactive: false,
2543
- }),
2544
- );
2545
-
2546
- expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
2547
- sessionId,
2548
- "providers-investigator",
2549
- "heartbeat",
2550
- { kind: "heartbeat" },
2551
- );
2552
- expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
2553
- sessionId,
2554
- "providers-investigator",
2555
- "Drafting the provider boundary analysis now.",
2556
- { kind: "text" },
2557
- );
2558
- });
2559
- });