@clinebot/core 0.0.20 → 0.0.21

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 (356) hide show
  1. package/dist/account/cline-account-service.d.ts +3 -2
  2. package/dist/account/cline-account-service.d.ts.map +1 -0
  3. package/dist/account/index.d.ts +1 -0
  4. package/dist/account/index.d.ts.map +1 -0
  5. package/dist/account/rpc.d.ts +1 -0
  6. package/dist/account/rpc.d.ts.map +1 -0
  7. package/dist/account/types.d.ts +1 -0
  8. package/dist/account/types.d.ts.map +1 -0
  9. package/dist/agents/agent-config-loader.d.ts +1 -0
  10. package/dist/agents/agent-config-loader.d.ts.map +1 -0
  11. package/dist/agents/agent-config-parser.d.ts +1 -0
  12. package/dist/agents/agent-config-parser.d.ts.map +1 -0
  13. package/dist/agents/hooks-config-loader.d.ts +1 -0
  14. package/dist/agents/hooks-config-loader.d.ts.map +1 -0
  15. package/dist/agents/index.d.ts +1 -0
  16. package/dist/agents/index.d.ts.map +1 -0
  17. package/dist/agents/plugin-config-loader.d.ts +1 -0
  18. package/dist/agents/plugin-config-loader.d.ts.map +1 -0
  19. package/dist/agents/plugin-loader.d.ts +1 -0
  20. package/dist/agents/plugin-loader.d.ts.map +1 -0
  21. package/dist/agents/plugin-sandbox.d.ts +1 -0
  22. package/dist/agents/plugin-sandbox.d.ts.map +1 -0
  23. package/dist/agents/unified-config-file-watcher.d.ts +1 -0
  24. package/dist/agents/unified-config-file-watcher.d.ts.map +1 -0
  25. package/dist/agents/user-instruction-config-loader.d.ts +1 -0
  26. package/dist/agents/user-instruction-config-loader.d.ts.map +1 -0
  27. package/dist/auth/client.d.ts +1 -0
  28. package/dist/auth/client.d.ts.map +1 -0
  29. package/dist/auth/cline.d.ts +1 -0
  30. package/dist/auth/cline.d.ts.map +1 -0
  31. package/dist/auth/codex.d.ts +1 -0
  32. package/dist/auth/codex.d.ts.map +1 -0
  33. package/dist/auth/oca.d.ts +1 -0
  34. package/dist/auth/oca.d.ts.map +1 -0
  35. package/dist/auth/server.d.ts +1 -0
  36. package/dist/auth/server.d.ts.map +1 -0
  37. package/dist/auth/types.d.ts +1 -0
  38. package/dist/auth/types.d.ts.map +1 -0
  39. package/dist/auth/utils.d.ts +1 -0
  40. package/dist/auth/utils.d.ts.map +1 -0
  41. package/dist/chat/chat-schema.d.ts +13 -12
  42. package/dist/chat/chat-schema.d.ts.map +1 -0
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.node.d.ts +2 -0
  46. package/dist/index.node.d.ts.map +1 -0
  47. package/dist/index.node.js +303 -302
  48. package/dist/input/file-indexer.d.ts +1 -0
  49. package/dist/input/file-indexer.d.ts.map +1 -0
  50. package/dist/input/index.d.ts +1 -0
  51. package/dist/input/index.d.ts.map +1 -0
  52. package/dist/input/mention-enricher.d.ts +1 -0
  53. package/dist/input/mention-enricher.d.ts.map +1 -0
  54. package/dist/mcp/config-loader.d.ts +1 -0
  55. package/dist/mcp/config-loader.d.ts.map +1 -0
  56. package/dist/mcp/index.d.ts +1 -0
  57. package/dist/mcp/index.d.ts.map +1 -0
  58. package/dist/mcp/manager.d.ts +1 -0
  59. package/dist/mcp/manager.d.ts.map +1 -0
  60. package/dist/mcp/types.d.ts +1 -0
  61. package/dist/mcp/types.d.ts.map +1 -0
  62. package/dist/providers/local-provider-registry.d.ts +36 -0
  63. package/dist/providers/local-provider-registry.d.ts.map +1 -0
  64. package/dist/providers/local-provider-service.d.ts +2 -1
  65. package/dist/providers/local-provider-service.d.ts.map +1 -0
  66. package/dist/runtime/commands.d.ts +1 -0
  67. package/dist/runtime/commands.d.ts.map +1 -0
  68. package/dist/runtime/hook-file-hooks.d.ts +1 -0
  69. package/dist/runtime/hook-file-hooks.d.ts.map +1 -0
  70. package/dist/runtime/rules.d.ts +1 -0
  71. package/dist/runtime/rules.d.ts.map +1 -0
  72. package/dist/runtime/runtime-builder.d.ts +1 -0
  73. package/dist/runtime/runtime-builder.d.ts.map +1 -0
  74. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +1 -0
  75. package/dist/runtime/sandbox/subprocess-sandbox.d.ts.map +1 -0
  76. package/dist/runtime/session-runtime.d.ts +2 -0
  77. package/dist/runtime/session-runtime.d.ts.map +1 -0
  78. package/dist/runtime/skills.d.ts +1 -0
  79. package/dist/runtime/skills.d.ts.map +1 -0
  80. package/dist/runtime/tool-approval.d.ts +1 -0
  81. package/dist/runtime/tool-approval.d.ts.map +1 -0
  82. package/dist/runtime/workflows.d.ts +1 -0
  83. package/dist/runtime/workflows.d.ts.map +1 -0
  84. package/dist/session/default-session-manager.d.ts +4 -0
  85. package/dist/session/default-session-manager.d.ts.map +1 -0
  86. package/dist/session/file-session-service.d.ts +1 -0
  87. package/dist/session/file-session-service.d.ts.map +1 -0
  88. package/dist/session/rpc-session-service.d.ts +1 -0
  89. package/dist/session/rpc-session-service.d.ts.map +1 -0
  90. package/dist/session/rpc-spawn-lease.d.ts +1 -0
  91. package/dist/session/rpc-spawn-lease.d.ts.map +1 -0
  92. package/dist/session/runtime-oauth-token-manager.d.ts +1 -0
  93. package/dist/session/runtime-oauth-token-manager.d.ts.map +1 -0
  94. package/dist/session/session-agent-events.d.ts +20 -1
  95. package/dist/session/session-agent-events.d.ts.map +1 -0
  96. package/dist/session/session-artifacts.d.ts +1 -0
  97. package/dist/session/session-artifacts.d.ts.map +1 -0
  98. package/dist/session/session-config-builder.d.ts +1 -0
  99. package/dist/session/session-config-builder.d.ts.map +1 -0
  100. package/dist/session/session-graph.d.ts +1 -0
  101. package/dist/session/session-graph.d.ts.map +1 -0
  102. package/dist/session/session-host.d.ts +1 -0
  103. package/dist/session/session-host.d.ts.map +1 -0
  104. package/dist/session/session-manager.d.ts +1 -0
  105. package/dist/session/session-manager.d.ts.map +1 -0
  106. package/dist/session/session-manifest.d.ts +2 -1
  107. package/dist/session/session-manifest.d.ts.map +1 -0
  108. package/dist/session/session-service.d.ts +1 -0
  109. package/dist/session/session-service.d.ts.map +1 -0
  110. package/dist/session/session-team-coordination.d.ts +1 -0
  111. package/dist/session/session-team-coordination.d.ts.map +1 -0
  112. package/dist/session/session-telemetry.d.ts +3 -1
  113. package/dist/session/session-telemetry.d.ts.map +1 -0
  114. package/dist/session/sqlite-rpc-session-backend.d.ts +1 -0
  115. package/dist/session/sqlite-rpc-session-backend.d.ts.map +1 -0
  116. package/dist/session/unified-session-persistence-service.d.ts +1 -0
  117. package/dist/session/unified-session-persistence-service.d.ts.map +1 -0
  118. package/dist/session/utils/helpers.d.ts +1 -0
  119. package/dist/session/utils/helpers.d.ts.map +1 -0
  120. package/dist/session/utils/types.d.ts +1 -0
  121. package/dist/session/utils/types.d.ts.map +1 -0
  122. package/dist/session/utils/usage.d.ts +1 -0
  123. package/dist/session/utils/usage.d.ts.map +1 -0
  124. package/dist/session/workspace-manager.d.ts +1 -0
  125. package/dist/session/workspace-manager.d.ts.map +1 -0
  126. package/dist/session/workspace-manifest.d.ts +1 -0
  127. package/dist/session/workspace-manifest.d.ts.map +1 -0
  128. package/dist/storage/file-team-store.d.ts +1 -0
  129. package/dist/storage/file-team-store.d.ts.map +1 -0
  130. package/dist/storage/provider-settings-legacy-migration.d.ts +1 -0
  131. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -0
  132. package/dist/storage/provider-settings-manager.d.ts +1 -0
  133. package/dist/storage/provider-settings-manager.d.ts.map +1 -0
  134. package/dist/storage/sqlite-session-store.d.ts +1 -0
  135. package/dist/storage/sqlite-session-store.d.ts.map +1 -0
  136. package/dist/storage/sqlite-team-store.d.ts +1 -0
  137. package/dist/storage/sqlite-team-store.d.ts.map +1 -0
  138. package/dist/storage/team-store.d.ts +1 -0
  139. package/dist/storage/team-store.d.ts.map +1 -0
  140. package/dist/team/index.d.ts +1 -0
  141. package/dist/team/index.d.ts.map +1 -0
  142. package/dist/team/projections.d.ts +1 -0
  143. package/dist/team/projections.d.ts.map +1 -0
  144. package/dist/telemetry/ITelemetryAdapter.d.ts +1 -0
  145. package/dist/telemetry/ITelemetryAdapter.d.ts.map +1 -0
  146. package/dist/telemetry/LoggerTelemetryAdapter.d.ts +1 -0
  147. package/dist/telemetry/LoggerTelemetryAdapter.d.ts.map +1 -0
  148. package/dist/telemetry/OpenTelemetryAdapter.d.ts +1 -0
  149. package/dist/telemetry/OpenTelemetryAdapter.d.ts.map +1 -0
  150. package/dist/telemetry/OpenTelemetryProvider.d.ts +1 -0
  151. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -0
  152. package/dist/telemetry/TelemetryService.d.ts +1 -0
  153. package/dist/telemetry/TelemetryService.d.ts.map +1 -0
  154. package/dist/telemetry/core-events.d.ts +55 -22
  155. package/dist/telemetry/core-events.d.ts.map +1 -0
  156. package/dist/telemetry/opentelemetry.d.ts +1 -0
  157. package/dist/telemetry/opentelemetry.d.ts.map +1 -0
  158. package/dist/tools/constants.d.ts +1 -0
  159. package/dist/tools/constants.d.ts.map +1 -0
  160. package/dist/tools/definitions.d.ts +8 -1
  161. package/dist/tools/definitions.d.ts.map +1 -0
  162. package/dist/tools/executors/apply-patch-parser.d.ts +1 -0
  163. package/dist/tools/executors/apply-patch-parser.d.ts.map +1 -0
  164. package/dist/tools/executors/apply-patch.d.ts +1 -0
  165. package/dist/tools/executors/apply-patch.d.ts.map +1 -0
  166. package/dist/tools/executors/bash.d.ts +2 -1
  167. package/dist/tools/executors/bash.d.ts.map +1 -0
  168. package/dist/tools/executors/editor.d.ts +1 -0
  169. package/dist/tools/executors/editor.d.ts.map +1 -0
  170. package/dist/tools/executors/file-read.d.ts +1 -0
  171. package/dist/tools/executors/file-read.d.ts.map +1 -0
  172. package/dist/tools/executors/index.d.ts +14 -7
  173. package/dist/tools/executors/index.d.ts.map +1 -0
  174. package/dist/tools/executors/search.d.ts +1 -0
  175. package/dist/tools/executors/search.d.ts.map +1 -0
  176. package/dist/tools/executors/web-fetch.d.ts +1 -0
  177. package/dist/tools/executors/web-fetch.d.ts.map +1 -0
  178. package/dist/tools/helpers.d.ts +15 -0
  179. package/dist/tools/helpers.d.ts.map +1 -0
  180. package/dist/tools/index.d.ts +2 -1
  181. package/dist/tools/index.d.ts.map +1 -0
  182. package/dist/tools/model-tool-routing.d.ts +1 -0
  183. package/dist/tools/model-tool-routing.d.ts.map +1 -0
  184. package/dist/tools/presets.d.ts +1 -0
  185. package/dist/tools/presets.d.ts.map +1 -0
  186. package/dist/tools/schemas.d.ts +41 -0
  187. package/dist/tools/schemas.d.ts.map +1 -0
  188. package/dist/tools/types.d.ts +3 -2
  189. package/dist/tools/types.d.ts.map +1 -0
  190. package/dist/types/common.d.ts +1 -0
  191. package/dist/types/common.d.ts.map +1 -0
  192. package/dist/types/config.d.ts +1 -0
  193. package/dist/types/config.d.ts.map +1 -0
  194. package/dist/types/events.d.ts +1 -0
  195. package/dist/types/events.d.ts.map +1 -0
  196. package/dist/types/provider-settings.d.ts +1 -0
  197. package/dist/types/provider-settings.d.ts.map +1 -0
  198. package/dist/types/sessions.d.ts +1 -0
  199. package/dist/types/sessions.d.ts.map +1 -0
  200. package/dist/types/storage.d.ts +1 -0
  201. package/dist/types/storage.d.ts.map +1 -0
  202. package/dist/types/workspace.d.ts +1 -0
  203. package/dist/types/workspace.d.ts.map +1 -0
  204. package/dist/types.d.ts +1 -0
  205. package/dist/types.d.ts.map +1 -0
  206. package/package.json +6 -4
  207. package/src/account/cline-account-service.test.ts +0 -101
  208. package/src/account/cline-account-service.ts +0 -287
  209. package/src/account/index.ts +0 -22
  210. package/src/account/rpc.test.ts +0 -62
  211. package/src/account/rpc.ts +0 -172
  212. package/src/account/types.ts +0 -98
  213. package/src/agents/agent-config-loader.test.ts +0 -236
  214. package/src/agents/agent-config-loader.ts +0 -108
  215. package/src/agents/agent-config-parser.ts +0 -198
  216. package/src/agents/hooks-config-loader.test.ts +0 -20
  217. package/src/agents/hooks-config-loader.ts +0 -118
  218. package/src/agents/index.ts +0 -85
  219. package/src/agents/plugin-config-loader.test.ts +0 -140
  220. package/src/agents/plugin-config-loader.ts +0 -97
  221. package/src/agents/plugin-loader.test.ts +0 -228
  222. package/src/agents/plugin-loader.ts +0 -172
  223. package/src/agents/plugin-sandbox-bootstrap.ts +0 -445
  224. package/src/agents/plugin-sandbox.test.ts +0 -317
  225. package/src/agents/plugin-sandbox.ts +0 -341
  226. package/src/agents/unified-config-file-watcher.test.ts +0 -196
  227. package/src/agents/unified-config-file-watcher.ts +0 -483
  228. package/src/agents/user-instruction-config-loader.test.ts +0 -158
  229. package/src/agents/user-instruction-config-loader.ts +0 -438
  230. package/src/auth/client.test.ts +0 -40
  231. package/src/auth/client.ts +0 -25
  232. package/src/auth/cline.test.ts +0 -130
  233. package/src/auth/cline.ts +0 -420
  234. package/src/auth/codex.test.ts +0 -170
  235. package/src/auth/codex.ts +0 -491
  236. package/src/auth/oca.test.ts +0 -215
  237. package/src/auth/oca.ts +0 -573
  238. package/src/auth/server.ts +0 -216
  239. package/src/auth/types.ts +0 -81
  240. package/src/auth/utils.test.ts +0 -128
  241. package/src/auth/utils.ts +0 -247
  242. package/src/chat/chat-schema.ts +0 -82
  243. package/src/index.node.ts +0 -285
  244. package/src/index.ts +0 -211
  245. package/src/input/file-indexer.d.ts +0 -11
  246. package/src/input/file-indexer.test.ts +0 -127
  247. package/src/input/file-indexer.ts +0 -327
  248. package/src/input/index.ts +0 -7
  249. package/src/input/mention-enricher.test.ts +0 -85
  250. package/src/input/mention-enricher.ts +0 -122
  251. package/src/mcp/config-loader.test.ts +0 -238
  252. package/src/mcp/config-loader.ts +0 -219
  253. package/src/mcp/index.ts +0 -26
  254. package/src/mcp/manager.test.ts +0 -106
  255. package/src/mcp/manager.ts +0 -262
  256. package/src/mcp/types.ts +0 -88
  257. package/src/providers/local-provider-service.ts +0 -608
  258. package/src/runtime/commands.test.ts +0 -98
  259. package/src/runtime/commands.ts +0 -83
  260. package/src/runtime/hook-file-hooks.test.ts +0 -237
  261. package/src/runtime/hook-file-hooks.ts +0 -859
  262. package/src/runtime/index.ts +0 -37
  263. package/src/runtime/rules.ts +0 -34
  264. package/src/runtime/runtime-builder.team-persistence.test.ts +0 -202
  265. package/src/runtime/runtime-builder.test.ts +0 -371
  266. package/src/runtime/runtime-builder.ts +0 -589
  267. package/src/runtime/runtime-parity.test.ts +0 -143
  268. package/src/runtime/sandbox/subprocess-sandbox.ts +0 -231
  269. package/src/runtime/session-runtime.ts +0 -46
  270. package/src/runtime/skills.ts +0 -44
  271. package/src/runtime/tool-approval.ts +0 -104
  272. package/src/runtime/workflows.test.ts +0 -119
  273. package/src/runtime/workflows.ts +0 -45
  274. package/src/session/default-session-manager.e2e.test.ts +0 -384
  275. package/src/session/default-session-manager.test.ts +0 -1741
  276. package/src/session/default-session-manager.ts +0 -1233
  277. package/src/session/file-session-service.ts +0 -280
  278. package/src/session/index.ts +0 -42
  279. package/src/session/rpc-session-service.ts +0 -107
  280. package/src/session/rpc-spawn-lease.test.ts +0 -49
  281. package/src/session/rpc-spawn-lease.ts +0 -122
  282. package/src/session/runtime-oauth-token-manager.test.ts +0 -137
  283. package/src/session/runtime-oauth-token-manager.ts +0 -272
  284. package/src/session/session-agent-events.ts +0 -159
  285. package/src/session/session-artifacts.ts +0 -106
  286. package/src/session/session-config-builder.ts +0 -113
  287. package/src/session/session-graph.ts +0 -92
  288. package/src/session/session-host.test.ts +0 -29
  289. package/src/session/session-host.ts +0 -242
  290. package/src/session/session-manager.ts +0 -69
  291. package/src/session/session-manifest.ts +0 -29
  292. package/src/session/session-service.team-persistence.test.ts +0 -48
  293. package/src/session/session-service.ts +0 -673
  294. package/src/session/session-team-coordination.ts +0 -229
  295. package/src/session/session-telemetry.ts +0 -95
  296. package/src/session/sqlite-rpc-session-backend.ts +0 -303
  297. package/src/session/unified-session-persistence-service.test.ts +0 -85
  298. package/src/session/unified-session-persistence-service.ts +0 -996
  299. package/src/session/utils/helpers.ts +0 -139
  300. package/src/session/utils/types.ts +0 -57
  301. package/src/session/utils/usage.ts +0 -32
  302. package/src/session/workspace-manager.ts +0 -98
  303. package/src/session/workspace-manifest.ts +0 -100
  304. package/src/storage/artifact-store.ts +0 -1
  305. package/src/storage/file-team-store.ts +0 -257
  306. package/src/storage/index.ts +0 -11
  307. package/src/storage/provider-settings-legacy-migration.test.ts +0 -307
  308. package/src/storage/provider-settings-legacy-migration.ts +0 -689
  309. package/src/storage/provider-settings-manager.test.ts +0 -145
  310. package/src/storage/provider-settings-manager.ts +0 -150
  311. package/src/storage/session-store.ts +0 -1
  312. package/src/storage/sqlite-session-store.ts +0 -275
  313. package/src/storage/sqlite-team-store.ts +0 -454
  314. package/src/storage/team-store.ts +0 -40
  315. package/src/team/index.ts +0 -4
  316. package/src/team/projections.ts +0 -285
  317. package/src/telemetry/ITelemetryAdapter.ts +0 -94
  318. package/src/telemetry/LoggerTelemetryAdapter.test.ts +0 -42
  319. package/src/telemetry/LoggerTelemetryAdapter.ts +0 -114
  320. package/src/telemetry/OpenTelemetryAdapter.test.ts +0 -157
  321. package/src/telemetry/OpenTelemetryAdapter.ts +0 -348
  322. package/src/telemetry/OpenTelemetryProvider.test.ts +0 -113
  323. package/src/telemetry/OpenTelemetryProvider.ts +0 -322
  324. package/src/telemetry/TelemetryService.test.ts +0 -134
  325. package/src/telemetry/TelemetryService.ts +0 -141
  326. package/src/telemetry/core-events.ts +0 -344
  327. package/src/telemetry/opentelemetry.ts +0 -20
  328. package/src/tools/constants.ts +0 -35
  329. package/src/tools/definitions.test.ts +0 -658
  330. package/src/tools/definitions.ts +0 -726
  331. package/src/tools/executors/apply-patch-parser.ts +0 -520
  332. package/src/tools/executors/apply-patch.ts +0 -359
  333. package/src/tools/executors/bash.ts +0 -205
  334. package/src/tools/executors/editor.test.ts +0 -35
  335. package/src/tools/executors/editor.ts +0 -219
  336. package/src/tools/executors/file-read.test.ts +0 -49
  337. package/src/tools/executors/file-read.ts +0 -110
  338. package/src/tools/executors/index.ts +0 -75
  339. package/src/tools/executors/search.ts +0 -278
  340. package/src/tools/executors/web-fetch.ts +0 -259
  341. package/src/tools/index.ts +0 -168
  342. package/src/tools/model-tool-routing.test.ts +0 -86
  343. package/src/tools/model-tool-routing.ts +0 -132
  344. package/src/tools/presets.test.ts +0 -62
  345. package/src/tools/presets.ts +0 -168
  346. package/src/tools/schemas.ts +0 -284
  347. package/src/tools/types.ts +0 -328
  348. package/src/types/common.ts +0 -14
  349. package/src/types/config.ts +0 -84
  350. package/src/types/events.ts +0 -74
  351. package/src/types/index.ts +0 -24
  352. package/src/types/provider-settings.ts +0 -43
  353. package/src/types/sessions.ts +0 -16
  354. package/src/types/storage.ts +0 -64
  355. package/src/types/workspace.ts +0 -7
  356. package/src/types.ts +0 -128
@@ -1,996 +0,0 @@
1
- import {
2
- appendFileSync,
3
- existsSync,
4
- readFileSync,
5
- writeFileSync,
6
- } from "node:fs";
7
- import type {
8
- HookEventPayload,
9
- SubAgentEndContext,
10
- SubAgentStartContext,
11
- } from "@clinebot/agents";
12
- import type { LlmsProviders } from "@clinebot/llms";
13
- import { normalizeUserInput, resolveRootSessionId } from "@clinebot/shared";
14
- import { nanoid } from "nanoid";
15
- import { z } from "zod";
16
- import type { SessionStatus } from "../types/common";
17
- import { nowIso, SessionArtifacts, unlinkIfExists } from "./session-artifacts";
18
- import {
19
- deriveSubsessionStatus,
20
- makeSubSessionId,
21
- makeTeamTaskSubSessionId,
22
- } from "./session-graph";
23
- import {
24
- type SessionManifest,
25
- SessionManifestSchema,
26
- } from "./session-manifest";
27
- import type {
28
- CreateRootSessionWithArtifactsInput,
29
- RootSessionArtifacts,
30
- SessionRow,
31
- UpsertSubagentInput,
32
- } from "./session-service";
33
-
34
- const SUBSESSION_SOURCE = "cli_subagent";
35
- const MAX_TITLE_LENGTH = 120;
36
- const OCC_MAX_RETRIES = 4;
37
-
38
- const SpawnAgentInputSchema = z
39
- .object({
40
- task: z.string().optional(),
41
- systemPrompt: z.string().optional(),
42
- })
43
- .passthrough();
44
-
45
- // ── Metadata helpers ──────────────────────────────────────────────────
46
-
47
- function normalizeTitle(title?: string | null): string | undefined {
48
- const trimmed = title?.trim();
49
- return trimmed ? trimmed.slice(0, MAX_TITLE_LENGTH) : undefined;
50
- }
51
-
52
- function deriveTitleFromPrompt(prompt?: string | null): string | undefined {
53
- const normalized = normalizeUserInput(prompt ?? "").trim();
54
- if (!normalized) return undefined;
55
- return normalizeTitle(normalized.split("\n")[0]?.trim());
56
- }
57
-
58
- /** Strip invalid title from metadata, drop empty objects. */
59
- function sanitizeMetadata(
60
- metadata: Record<string, unknown> | null | undefined,
61
- ): Record<string, unknown> | undefined {
62
- if (!metadata) return undefined;
63
- const next = { ...metadata };
64
- const title = normalizeTitle(
65
- typeof next.title === "string" ? next.title : undefined,
66
- );
67
- if (title) {
68
- next.title = title;
69
- } else {
70
- delete next.title;
71
- }
72
- return Object.keys(next).length > 0 ? next : undefined;
73
- }
74
-
75
- /** Resolve title from explicit title, prompt, or existing metadata. */
76
- function resolveMetadataWithTitle(input: {
77
- metadata?: Record<string, unknown> | null;
78
- title?: string | null;
79
- prompt?: string | null;
80
- }): Record<string, unknown> | undefined {
81
- const base = sanitizeMetadata(input.metadata) ?? {};
82
- const title =
83
- input.title !== undefined
84
- ? normalizeTitle(input.title)
85
- : deriveTitleFromPrompt(input.prompt);
86
- if (title) base.title = title;
87
- return Object.keys(base).length > 0 ? base : undefined;
88
- }
89
-
90
- // ── File helpers ──────────────────────────────────────────────────────
91
-
92
- function writeEmptyMessagesFile(path: string, startedAt: string): void {
93
- writeFileSync(
94
- path,
95
- `${JSON.stringify({ version: 1, updated_at: startedAt, messages: [] }, null, 2)}\n`,
96
- "utf8",
97
- );
98
- }
99
-
100
- // ── Interfaces ────────────────────────────────────────────────────────
101
-
102
- export interface PersistedSessionUpdateInput {
103
- sessionId: string;
104
- expectedStatusLock?: number;
105
- status?: SessionStatus;
106
- endedAt?: string | null;
107
- exitCode?: number | null;
108
- prompt?: string | null;
109
- metadata?: Record<string, unknown> | null;
110
- title?: string | null;
111
- parentSessionId?: string | null;
112
- parentAgentId?: string | null;
113
- agentId?: string | null;
114
- conversationId?: string | null;
115
- setRunning?: boolean;
116
- }
117
-
118
- export interface SessionPersistenceAdapter {
119
- ensureSessionsDir(): string;
120
- upsertSession(row: SessionRow): Promise<void>;
121
- getSession(sessionId: string): Promise<SessionRow | undefined>;
122
- listSessions(options: {
123
- limit: number;
124
- parentSessionId?: string;
125
- status?: string;
126
- }): Promise<SessionRow[]>;
127
- updateSession(
128
- input: PersistedSessionUpdateInput,
129
- ): Promise<{ updated: boolean; statusLock: number }>;
130
- deleteSession(sessionId: string, cascade: boolean): Promise<boolean>;
131
- enqueueSpawnRequest(input: {
132
- rootSessionId: string;
133
- parentAgentId: string;
134
- task?: string;
135
- systemPrompt?: string;
136
- }): Promise<void>;
137
- claimSpawnRequest(
138
- rootSessionId: string,
139
- parentAgentId: string,
140
- ): Promise<string | undefined>;
141
- }
142
-
143
- // ── Service ───────────────────────────────────────────────────────────
144
-
145
- export class UnifiedSessionPersistenceService {
146
- private readonly teamTaskSessionsByAgent = new Map<string, string[]>();
147
- private readonly teamTaskLastHeartbeatBySession = new Map<string, number>();
148
- private readonly teamTaskLastProgressLineBySession = new Map<
149
- string,
150
- string
151
- >();
152
- protected readonly artifacts: SessionArtifacts;
153
- private static readonly STALE_REASON = "failed_external_process_exit";
154
- private static readonly STALE_SOURCE = "stale_session_reconciler";
155
- private static readonly TEAM_HEARTBEAT_LOG_INTERVAL_MS = 30_000;
156
-
157
- constructor(private readonly adapter: SessionPersistenceAdapter) {
158
- this.artifacts = new SessionArtifacts(() => this.ensureSessionsDir());
159
- }
160
-
161
- ensureSessionsDir(): string {
162
- return this.adapter.ensureSessionsDir();
163
- }
164
-
165
- // ── Manifest I/O ──────────────────────────────────────────────────
166
-
167
- private writeManifestFile(
168
- manifestPath: string,
169
- manifest: SessionManifest,
170
- ): void {
171
- writeFileSync(
172
- manifestPath,
173
- `${JSON.stringify(SessionManifestSchema.parse(manifest), null, 2)}\n`,
174
- "utf8",
175
- );
176
- }
177
-
178
- writeSessionManifest(manifestPath: string, manifest: SessionManifest): void {
179
- this.writeManifestFile(manifestPath, manifest);
180
- }
181
-
182
- private readManifestFile(sessionId: string): {
183
- path: string;
184
- manifest?: SessionManifest;
185
- } {
186
- const manifestPath = this.artifacts.sessionManifestPath(sessionId, false);
187
- if (!existsSync(manifestPath)) return { path: manifestPath };
188
- try {
189
- return {
190
- path: manifestPath,
191
- manifest: SessionManifestSchema.parse(
192
- JSON.parse(readFileSync(manifestPath, "utf8")) as SessionManifest,
193
- ),
194
- };
195
- } catch {
196
- return { path: manifestPath };
197
- }
198
- }
199
-
200
- private buildManifestFromRow(
201
- row: SessionRow,
202
- overrides?: {
203
- status?: SessionStatus;
204
- endedAt?: string | null;
205
- exitCode?: number | null;
206
- metadata?: Record<string, unknown>;
207
- },
208
- ): SessionManifest {
209
- return SessionManifestSchema.parse({
210
- version: 1,
211
- session_id: row.sessionId,
212
- source: row.source,
213
- pid: row.pid,
214
- started_at: row.startedAt,
215
- ended_at: overrides?.endedAt ?? row.endedAt ?? undefined,
216
- exit_code: overrides?.exitCode ?? row.exitCode ?? undefined,
217
- status: overrides?.status ?? row.status,
218
- interactive: row.interactive,
219
- provider: row.provider,
220
- model: row.model,
221
- cwd: row.cwd,
222
- workspace_root: row.workspaceRoot,
223
- team_name: row.teamName ?? undefined,
224
- enable_tools: row.enableTools,
225
- enable_spawn: row.enableSpawn,
226
- enable_teams: row.enableTeams,
227
- prompt: row.prompt ?? undefined,
228
- metadata: overrides?.metadata ?? row.metadata ?? undefined,
229
- messages_path: row.messagesPath ?? undefined,
230
- });
231
- }
232
-
233
- // ── Path resolution ───────────────────────────────────────────────
234
-
235
- private async resolveArtifactPath(
236
- sessionId: string,
237
- kind: "transcriptPath" | "hookPath" | "messagesPath",
238
- fallback: (id: string) => string,
239
- ): Promise<string> {
240
- const row = await this.adapter.getSession(sessionId);
241
- const value = row?.[kind];
242
- return typeof value === "string" && value.trim().length > 0
243
- ? value
244
- : fallback(sessionId);
245
- }
246
-
247
- // ── Team task queue ───────────────────────────────────────────────
248
-
249
- private teamTaskQueueKey(rootSessionId: string, agentId: string): string {
250
- return `${rootSessionId}::${agentId}`;
251
- }
252
-
253
- private activeTeamTaskSessionId(
254
- rootSessionId: string,
255
- parentAgentId: string,
256
- ): string | undefined {
257
- const queue = this.teamTaskSessionsByAgent.get(
258
- this.teamTaskQueueKey(rootSessionId, parentAgentId),
259
- );
260
- return queue?.at(-1);
261
- }
262
-
263
- // ── Root session ──────────────────────────────────────────────────
264
-
265
- async createRootSessionWithArtifacts(
266
- input: CreateRootSessionWithArtifactsInput,
267
- ): Promise<RootSessionArtifacts> {
268
- const startedAt = input.startedAt ?? nowIso();
269
- const providedId = input.sessionId.trim();
270
- const sessionId =
271
- providedId.length > 0 ? providedId : `${Date.now()}_${nanoid(5)}`;
272
- const transcriptPath = this.artifacts.sessionTranscriptPath(sessionId);
273
- const hookPath = this.artifacts.sessionHookPath(sessionId);
274
- const messagesPath = this.artifacts.sessionMessagesPath(sessionId);
275
- const manifestPath = this.artifacts.sessionManifestPath(sessionId);
276
-
277
- const metadata = resolveMetadataWithTitle({
278
- metadata: input.metadata,
279
- prompt: input.prompt,
280
- });
281
- const manifest = SessionManifestSchema.parse({
282
- version: 1,
283
- session_id: sessionId,
284
- source: input.source,
285
- pid: input.pid,
286
- started_at: startedAt,
287
- status: "running",
288
- interactive: input.interactive,
289
- provider: input.provider,
290
- model: input.model,
291
- cwd: input.cwd,
292
- workspace_root: input.workspaceRoot,
293
- team_name: input.teamName,
294
- enable_tools: input.enableTools,
295
- enable_spawn: input.enableSpawn,
296
- enable_teams: input.enableTeams,
297
- prompt: input.prompt?.trim() || undefined,
298
- metadata,
299
- messages_path: messagesPath,
300
- });
301
-
302
- await this.adapter.upsertSession({
303
- sessionId,
304
- source: input.source,
305
- pid: input.pid,
306
- startedAt,
307
- endedAt: null,
308
- exitCode: null,
309
- status: "running",
310
- statusLock: 0,
311
- interactive: input.interactive,
312
- provider: input.provider,
313
- model: input.model,
314
- cwd: input.cwd,
315
- workspaceRoot: input.workspaceRoot,
316
- teamName: input.teamName ?? null,
317
- enableTools: input.enableTools,
318
- enableSpawn: input.enableSpawn,
319
- enableTeams: input.enableTeams,
320
- parentSessionId: null,
321
- parentAgentId: null,
322
- agentId: null,
323
- conversationId: null,
324
- isSubagent: false,
325
- prompt: manifest.prompt ?? null,
326
- metadata: sanitizeMetadata(manifest.metadata),
327
- transcriptPath,
328
- hookPath,
329
- messagesPath,
330
- updatedAt: nowIso(),
331
- });
332
-
333
- writeEmptyMessagesFile(messagesPath, startedAt);
334
- this.writeManifestFile(manifestPath, manifest);
335
- return { manifestPath, transcriptPath, hookPath, messagesPath, manifest };
336
- }
337
-
338
- // ── Session status updates ────────────────────────────────────────
339
-
340
- async updateSessionStatus(
341
- sessionId: string,
342
- status: SessionStatus,
343
- exitCode?: number | null,
344
- ): Promise<{ updated: boolean; endedAt?: string }> {
345
- for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
346
- const row = await this.adapter.getSession(sessionId);
347
- if (!row) return { updated: false };
348
-
349
- const endedAt = nowIso();
350
- const changed = await this.adapter.updateSession({
351
- sessionId,
352
- status,
353
- endedAt,
354
- exitCode: typeof exitCode === "number" ? exitCode : null,
355
- expectedStatusLock: row.statusLock,
356
- });
357
- if (changed.updated) {
358
- if (status === "cancelled") {
359
- await this.applyStatusToRunningChildSessions(sessionId, "cancelled");
360
- }
361
- return { updated: true, endedAt };
362
- }
363
- }
364
- return { updated: false };
365
- }
366
-
367
- async updateSession(input: {
368
- sessionId: string;
369
- prompt?: string | null;
370
- metadata?: Record<string, unknown> | null;
371
- title?: string | null;
372
- }): Promise<{ updated: boolean }> {
373
- for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
374
- const row = await this.adapter.getSession(input.sessionId);
375
- if (!row) return { updated: false };
376
-
377
- const existingMeta = row.metadata ?? undefined;
378
- const baseMeta =
379
- input.metadata !== undefined
380
- ? (sanitizeMetadata(input.metadata) ?? {})
381
- : (sanitizeMetadata(existingMeta) ?? {});
382
-
383
- const existingTitle = normalizeTitle(
384
- typeof existingMeta?.title === "string"
385
- ? (existingMeta.title as string)
386
- : undefined,
387
- );
388
- const nextTitle =
389
- input.title !== undefined
390
- ? normalizeTitle(input.title)
391
- : input.prompt !== undefined
392
- ? deriveTitleFromPrompt(input.prompt)
393
- : existingTitle;
394
-
395
- if (nextTitle) {
396
- baseMeta.title = nextTitle;
397
- } else {
398
- delete baseMeta.title;
399
- }
400
-
401
- const hasMetadataChange =
402
- input.metadata !== undefined ||
403
- input.prompt !== undefined ||
404
- input.title !== undefined;
405
-
406
- const changed = await this.adapter.updateSession({
407
- sessionId: input.sessionId,
408
- prompt: input.prompt,
409
- metadata: hasMetadataChange
410
- ? Object.keys(baseMeta).length > 0
411
- ? baseMeta
412
- : null
413
- : undefined,
414
- title: nextTitle,
415
- expectedStatusLock: row.statusLock,
416
- });
417
- if (!changed.updated) continue;
418
-
419
- const { path: manifestPath, manifest } = this.readManifestFile(
420
- input.sessionId,
421
- );
422
- if (manifest) {
423
- if (input.prompt !== undefined) {
424
- manifest.prompt = input.prompt ?? undefined;
425
- }
426
- const manifestMeta =
427
- input.metadata !== undefined
428
- ? (sanitizeMetadata(input.metadata) ?? {})
429
- : (sanitizeMetadata(manifest.metadata) ?? {});
430
- if (nextTitle) manifestMeta.title = nextTitle;
431
- manifest.metadata =
432
- Object.keys(manifestMeta).length > 0 ? manifestMeta : undefined;
433
- this.writeManifestFile(manifestPath, manifest);
434
- }
435
- return { updated: true };
436
- }
437
- return { updated: false };
438
- }
439
-
440
- // ── Spawn queue ───────────────────────────────────────────────────
441
-
442
- async queueSpawnRequest(event: HookEventPayload): Promise<void> {
443
- if (event.hookName !== "tool_call" || event.parent_agent_id !== null)
444
- return;
445
- if (event.tool_call?.name !== "spawn_agent") return;
446
-
447
- const rootSessionId = resolveRootSessionId(event.sessionContext);
448
- if (!rootSessionId) return;
449
-
450
- const parsed = SpawnAgentInputSchema.safeParse(event.tool_call.input);
451
- await this.adapter.enqueueSpawnRequest({
452
- rootSessionId,
453
- parentAgentId: event.agent_id,
454
- task: parsed.success ? parsed.data.task : undefined,
455
- systemPrompt: parsed.success ? parsed.data.systemPrompt : undefined,
456
- });
457
- }
458
-
459
- // ── Subagent sessions ─────────────────────────────────────────────
460
-
461
- private buildSubsessionRow(
462
- root: SessionRow,
463
- opts: {
464
- sessionId: string;
465
- parentSessionId: string;
466
- parentAgentId: string;
467
- agentId: string;
468
- conversationId?: string | null;
469
- prompt: string;
470
- startedAt: string;
471
- transcriptPath: string;
472
- hookPath: string;
473
- messagesPath: string;
474
- },
475
- ): SessionRow {
476
- return {
477
- sessionId: opts.sessionId,
478
- source: SUBSESSION_SOURCE,
479
- pid: process.ppid,
480
- startedAt: opts.startedAt,
481
- endedAt: null,
482
- exitCode: null,
483
- status: "running",
484
- statusLock: 0,
485
- interactive: false,
486
- provider: root.provider,
487
- model: root.model,
488
- cwd: root.cwd,
489
- workspaceRoot: root.workspaceRoot,
490
- teamName: root.teamName ?? null,
491
- enableTools: root.enableTools,
492
- enableSpawn: root.enableSpawn,
493
- enableTeams: root.enableTeams,
494
- parentSessionId: opts.parentSessionId,
495
- parentAgentId: opts.parentAgentId,
496
- agentId: opts.agentId,
497
- conversationId: opts.conversationId ?? null,
498
- isSubagent: true,
499
- prompt: opts.prompt,
500
- metadata: resolveMetadataWithTitle({ prompt: opts.prompt }),
501
- transcriptPath: opts.transcriptPath,
502
- hookPath: opts.hookPath,
503
- messagesPath: opts.messagesPath,
504
- updatedAt: opts.startedAt,
505
- };
506
- }
507
-
508
- async upsertSubagentSession(
509
- input: UpsertSubagentInput,
510
- ): Promise<string | undefined> {
511
- const rootSessionId = input.rootSessionId;
512
- if (!rootSessionId) return undefined;
513
-
514
- const root = await this.adapter.getSession(rootSessionId);
515
- if (!root) return undefined;
516
-
517
- const sessionId = makeSubSessionId(rootSessionId, input.agentId);
518
- const existing = await this.adapter.getSession(sessionId);
519
- const startedAt = nowIso();
520
- const artifactPaths = this.artifacts.subagentArtifactPaths(
521
- sessionId,
522
- input.agentId,
523
- this.activeTeamTaskSessionId(rootSessionId, input.parentAgentId),
524
- );
525
-
526
- let prompt = input.prompt ?? existing?.prompt ?? undefined;
527
- if (!prompt) {
528
- prompt =
529
- (await this.adapter.claimSpawnRequest(
530
- rootSessionId,
531
- input.parentAgentId,
532
- )) ?? `Subagent run by ${input.parentAgentId}`;
533
- }
534
-
535
- if (!existing) {
536
- await this.adapter.upsertSession(
537
- this.buildSubsessionRow(root, {
538
- sessionId,
539
- parentSessionId: rootSessionId,
540
- parentAgentId: input.parentAgentId,
541
- agentId: input.agentId,
542
- conversationId: input.conversationId,
543
- prompt,
544
- startedAt,
545
- ...artifactPaths,
546
- }),
547
- );
548
- writeEmptyMessagesFile(artifactPaths.messagesPath, startedAt);
549
- return sessionId;
550
- }
551
-
552
- await this.adapter.updateSession({
553
- sessionId,
554
- setRunning: true,
555
- parentSessionId: rootSessionId,
556
- parentAgentId: input.parentAgentId,
557
- agentId: input.agentId,
558
- conversationId: input.conversationId,
559
- prompt: existing.prompt ?? prompt ?? null,
560
- metadata: resolveMetadataWithTitle({
561
- metadata: existing.metadata ?? undefined,
562
- prompt: existing.prompt ?? prompt ?? null,
563
- }),
564
- expectedStatusLock: existing.statusLock,
565
- });
566
- return sessionId;
567
- }
568
-
569
- async upsertSubagentSessionFromHook(
570
- event: HookEventPayload,
571
- ): Promise<string | undefined> {
572
- if (!event.parent_agent_id) return undefined;
573
-
574
- const rootSessionId = resolveRootSessionId(event.sessionContext);
575
- if (!rootSessionId) return undefined;
576
-
577
- if (event.hookName === "session_shutdown") {
578
- const sessionId = makeSubSessionId(rootSessionId, event.agent_id);
579
- const existing = await this.adapter.getSession(sessionId);
580
- return existing ? sessionId : undefined;
581
- }
582
- return await this.upsertSubagentSession({
583
- agentId: event.agent_id,
584
- parentAgentId: event.parent_agent_id,
585
- conversationId: event.taskId,
586
- rootSessionId,
587
- });
588
- }
589
-
590
- // ── Subagent audit / transcript ───────────────────────────────────
591
-
592
- async appendSubagentHookAudit(
593
- subSessionId: string,
594
- event: HookEventPayload,
595
- ): Promise<void> {
596
- const path = await this.resolveArtifactPath(
597
- subSessionId,
598
- "hookPath",
599
- (id) => this.artifacts.sessionHookPath(id),
600
- );
601
- appendFileSync(
602
- path,
603
- `${JSON.stringify({ ts: nowIso(), ...event })}\n`,
604
- "utf8",
605
- );
606
- }
607
-
608
- async appendSubagentTranscriptLine(
609
- subSessionId: string,
610
- line: string,
611
- ): Promise<void> {
612
- if (!line.trim()) return;
613
- const path = await this.resolveArtifactPath(
614
- subSessionId,
615
- "transcriptPath",
616
- (id) => this.artifacts.sessionTranscriptPath(id),
617
- );
618
- appendFileSync(path, `${line}\n`, "utf8");
619
- }
620
-
621
- async persistSessionMessages(
622
- sessionId: string,
623
- messages: LlmsProviders.Message[],
624
- systemPrompt?: string,
625
- ): Promise<void> {
626
- const path = await this.resolveArtifactPath(
627
- sessionId,
628
- "messagesPath",
629
- (id) => this.artifacts.sessionMessagesPath(id),
630
- );
631
- const payload: {
632
- version: number;
633
- updated_at: string;
634
- systemPrompt?: string;
635
- messages: LlmsProviders.Message[];
636
- } = { version: 1, updated_at: nowIso(), messages };
637
- if (systemPrompt) payload.systemPrompt = systemPrompt;
638
- writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
639
- }
640
-
641
- // ── Subagent status ───────────────────────────────────────────────
642
-
643
- async applySubagentStatus(
644
- subSessionId: string,
645
- event: HookEventPayload,
646
- ): Promise<void> {
647
- await this.applySubagentStatusBySessionId(
648
- subSessionId,
649
- deriveSubsessionStatus(event),
650
- );
651
- }
652
-
653
- async applySubagentStatusBySessionId(
654
- subSessionId: string,
655
- status: SessionStatus,
656
- ): Promise<void> {
657
- const row = await this.adapter.getSession(subSessionId);
658
- if (!row) return;
659
-
660
- const endedAt = status === "running" ? null : nowIso();
661
- const exitCode = status === "running" ? null : status === "failed" ? 1 : 0;
662
- await this.adapter.updateSession({
663
- sessionId: subSessionId,
664
- status,
665
- endedAt,
666
- exitCode,
667
- expectedStatusLock: row.statusLock,
668
- });
669
- }
670
-
671
- async applyStatusToRunningChildSessions(
672
- parentSessionId: string,
673
- status: Exclude<SessionStatus, "running">,
674
- ): Promise<void> {
675
- if (!parentSessionId) return;
676
- const rows = await this.adapter.listSessions({
677
- limit: 2000,
678
- parentSessionId,
679
- status: "running",
680
- });
681
- for (const row of rows) {
682
- await this.applySubagentStatusBySessionId(row.sessionId, status);
683
- }
684
- }
685
-
686
- // ── Team tasks ────────────────────────────────────────────────────
687
-
688
- async onTeamTaskStart(
689
- rootSessionId: string,
690
- agentId: string,
691
- message: string,
692
- ): Promise<void> {
693
- const root = await this.adapter.getSession(rootSessionId);
694
- if (!root) return;
695
-
696
- const sessionId = makeTeamTaskSubSessionId(rootSessionId, agentId);
697
- const startedAt = nowIso();
698
- const transcriptPath = this.artifacts.sessionTranscriptPath(sessionId);
699
- const hookPath = this.artifacts.sessionHookPath(sessionId);
700
- const messagesPath = this.artifacts.sessionMessagesPath(sessionId);
701
-
702
- await this.adapter.upsertSession(
703
- this.buildSubsessionRow(root, {
704
- sessionId,
705
- parentSessionId: rootSessionId,
706
- parentAgentId: "lead",
707
- agentId,
708
- prompt: message || `Team task for ${agentId}`,
709
- startedAt,
710
- transcriptPath,
711
- hookPath,
712
- messagesPath,
713
- }),
714
- );
715
- writeEmptyMessagesFile(messagesPath, startedAt);
716
- await this.appendSubagentTranscriptLine(sessionId, `[start] ${message}`);
717
-
718
- const key = this.teamTaskQueueKey(rootSessionId, agentId);
719
- const queue = this.teamTaskSessionsByAgent.get(key) ?? [];
720
- queue.push(sessionId);
721
- this.teamTaskSessionsByAgent.set(key, queue);
722
- }
723
-
724
- async onTeamTaskEnd(
725
- rootSessionId: string,
726
- agentId: string,
727
- status: SessionStatus,
728
- summary?: string,
729
- messages?: LlmsProviders.Message[],
730
- ): Promise<void> {
731
- const key = this.teamTaskQueueKey(rootSessionId, agentId);
732
- const queue = this.teamTaskSessionsByAgent.get(key);
733
- if (!queue || queue.length === 0) return;
734
-
735
- const sessionId = queue.shift();
736
- if (queue.length === 0) this.teamTaskSessionsByAgent.delete(key);
737
- if (!sessionId) return;
738
-
739
- if (messages) await this.persistSessionMessages(sessionId, messages);
740
- await this.appendSubagentTranscriptLine(
741
- sessionId,
742
- summary ?? `[done] ${status}`,
743
- );
744
- await this.applySubagentStatusBySessionId(sessionId, status);
745
- this.teamTaskLastHeartbeatBySession.delete(sessionId);
746
- this.teamTaskLastProgressLineBySession.delete(sessionId);
747
- }
748
-
749
- async onTeamTaskProgress(
750
- rootSessionId: string,
751
- agentId: string,
752
- progress: string,
753
- options?: { kind?: "heartbeat" | "progress" | "text" },
754
- ): Promise<void> {
755
- const key = this.teamTaskQueueKey(rootSessionId, agentId);
756
- const sessionId = this.teamTaskSessionsByAgent.get(key)?.[0];
757
- if (!sessionId) return;
758
-
759
- const trimmed = progress.trim();
760
- if (!trimmed) return;
761
-
762
- const kind = options?.kind ?? "progress";
763
- if (kind === "heartbeat") {
764
- const now = Date.now();
765
- const last = this.teamTaskLastHeartbeatBySession.get(sessionId) ?? 0;
766
- if (
767
- now - last <
768
- UnifiedSessionPersistenceService.TEAM_HEARTBEAT_LOG_INTERVAL_MS
769
- ) {
770
- return;
771
- }
772
- this.teamTaskLastHeartbeatBySession.set(sessionId, now);
773
- }
774
-
775
- const line =
776
- kind === "heartbeat"
777
- ? "[progress] heartbeat"
778
- : kind === "text"
779
- ? `[progress] text: ${trimmed}`
780
- : `[progress] ${trimmed}`;
781
- if (this.teamTaskLastProgressLineBySession.get(sessionId) === line) return;
782
- this.teamTaskLastProgressLineBySession.set(sessionId, line);
783
- await this.appendSubagentTranscriptLine(sessionId, line);
784
- }
785
-
786
- // ── SubAgent lifecycle ────────────────────────────────────────────
787
-
788
- async handleSubAgentStart(
789
- rootSessionId: string,
790
- context: SubAgentStartContext,
791
- ): Promise<void> {
792
- const subSessionId = await this.upsertSubagentSession({
793
- agentId: context.subAgentId,
794
- parentAgentId: context.parentAgentId,
795
- conversationId: context.conversationId,
796
- prompt: context.input.task,
797
- rootSessionId,
798
- });
799
- if (!subSessionId) return;
800
- await this.appendSubagentTranscriptLine(
801
- subSessionId,
802
- `[start] ${context.input.task}`,
803
- );
804
- await this.applySubagentStatusBySessionId(subSessionId, "running");
805
- }
806
-
807
- async handleSubAgentEnd(
808
- rootSessionId: string,
809
- context: SubAgentEndContext,
810
- ): Promise<void> {
811
- const subSessionId = await this.upsertSubagentSession({
812
- agentId: context.subAgentId,
813
- parentAgentId: context.parentAgentId,
814
- conversationId: context.conversationId,
815
- prompt: context.input.task,
816
- rootSessionId,
817
- });
818
- if (!subSessionId) return;
819
-
820
- if (context.error) {
821
- await this.appendSubagentTranscriptLine(
822
- subSessionId,
823
- `[error] ${context.error.message}`,
824
- );
825
- await this.applySubagentStatusBySessionId(subSessionId, "failed");
826
- return;
827
- }
828
- const reason = context.result?.finishReason ?? "completed";
829
- await this.appendSubagentTranscriptLine(subSessionId, `[done] ${reason}`);
830
- await this.applySubagentStatusBySessionId(
831
- subSessionId,
832
- reason === "aborted" ? "cancelled" : "completed",
833
- );
834
- }
835
-
836
- // ── Stale session reconciliation ──────────────────────────────────
837
-
838
- private isPidAlive(pid: number): boolean {
839
- if (!Number.isFinite(pid) || pid <= 0) return false;
840
- try {
841
- process.kill(Math.floor(pid), 0);
842
- return true;
843
- } catch (error) {
844
- return (
845
- typeof error === "object" &&
846
- error !== null &&
847
- "code" in error &&
848
- (error as { code?: string }).code === "EPERM"
849
- );
850
- }
851
- }
852
-
853
- private async reconcileDeadRunningSession(
854
- row: SessionRow,
855
- ): Promise<SessionRow | undefined> {
856
- if (row.status !== "running" || this.isPidAlive(row.pid)) return row;
857
-
858
- const detectedAt = nowIso();
859
- const reason = UnifiedSessionPersistenceService.STALE_REASON;
860
-
861
- for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
862
- const latest = await this.adapter.getSession(row.sessionId);
863
- if (!latest) return undefined;
864
- if (latest.status !== "running") return latest;
865
-
866
- const nextMetadata = {
867
- ...(latest.metadata ?? {}),
868
- terminal_marker: reason,
869
- terminal_marker_at: detectedAt,
870
- terminal_marker_pid: latest.pid,
871
- terminal_marker_source: UnifiedSessionPersistenceService.STALE_SOURCE,
872
- };
873
-
874
- const changed = await this.adapter.updateSession({
875
- sessionId: latest.sessionId,
876
- status: "failed",
877
- endedAt: detectedAt,
878
- exitCode: 1,
879
- metadata: nextMetadata,
880
- expectedStatusLock: latest.statusLock,
881
- });
882
- if (!changed.updated) continue;
883
-
884
- await this.applyStatusToRunningChildSessions(latest.sessionId, "failed");
885
-
886
- const manifest = this.buildManifestFromRow(latest, {
887
- status: "failed",
888
- endedAt: detectedAt,
889
- exitCode: 1,
890
- metadata: nextMetadata,
891
- });
892
- const { path: manifestPath } = this.readManifestFile(latest.sessionId);
893
- this.writeManifestFile(manifestPath, manifest);
894
-
895
- // Write termination markers to hook + transcript files
896
- appendFileSync(
897
- latest.hookPath,
898
- `${JSON.stringify({
899
- ts: detectedAt,
900
- hookName: "session_shutdown",
901
- reason,
902
- sessionId: latest.sessionId,
903
- pid: latest.pid,
904
- source: UnifiedSessionPersistenceService.STALE_SOURCE,
905
- })}\n`,
906
- "utf8",
907
- );
908
- appendFileSync(
909
- latest.transcriptPath,
910
- `[shutdown] ${reason} (pid=${latest.pid})\n`,
911
- "utf8",
912
- );
913
-
914
- return {
915
- ...latest,
916
- status: "failed",
917
- endedAt: detectedAt,
918
- exitCode: 1,
919
- metadata: nextMetadata,
920
- statusLock: changed.statusLock,
921
- updatedAt: detectedAt,
922
- };
923
- }
924
- return await this.adapter.getSession(row.sessionId);
925
- }
926
-
927
- // ── List / reconcile / delete ─────────────────────────────────────
928
-
929
- async listSessions(limit = 200): Promise<SessionRow[]> {
930
- const requestedLimit = Math.max(1, Math.floor(limit));
931
- const scanLimit = Math.min(requestedLimit * 5, 2000);
932
- await this.reconcileDeadSessions(scanLimit);
933
-
934
- const rows = await this.adapter.listSessions({ limit: scanLimit });
935
- return rows.slice(0, requestedLimit).map((row) => {
936
- const meta = sanitizeMetadata(row.metadata ?? undefined);
937
- const { manifest } = this.readManifestFile(row.sessionId);
938
- const manifestTitle = normalizeTitle(
939
- typeof manifest?.metadata?.title === "string"
940
- ? (manifest.metadata.title as string)
941
- : undefined,
942
- );
943
- const resolved = manifestTitle
944
- ? { ...(meta ?? {}), title: manifestTitle }
945
- : meta;
946
- return { ...row, metadata: resolved };
947
- });
948
- }
949
-
950
- async reconcileDeadSessions(limit = 2000): Promise<number> {
951
- const rows = await this.adapter.listSessions({
952
- limit: Math.max(1, Math.floor(limit)),
953
- status: "running",
954
- });
955
- let reconciled = 0;
956
- for (const row of rows) {
957
- const updated = await this.reconcileDeadRunningSession(row);
958
- if (updated && updated.status !== row.status) reconciled++;
959
- }
960
- return reconciled;
961
- }
962
-
963
- async deleteSession(sessionId: string): Promise<{ deleted: boolean }> {
964
- const id = sessionId.trim();
965
- if (!id) throw new Error("session id is required");
966
-
967
- const row = await this.adapter.getSession(id);
968
- if (!row) return { deleted: false };
969
-
970
- await this.adapter.deleteSession(id, false);
971
-
972
- if (!row.isSubagent) {
973
- const children = await this.adapter.listSessions({
974
- limit: 2000,
975
- parentSessionId: id,
976
- });
977
- await this.adapter.deleteSession(id, true);
978
- for (const child of children) {
979
- unlinkIfExists(child.transcriptPath);
980
- unlinkIfExists(child.hookPath);
981
- unlinkIfExists(child.messagesPath);
982
- unlinkIfExists(
983
- this.artifacts.sessionManifestPath(child.sessionId, false),
984
- );
985
- this.artifacts.removeSessionDirIfEmpty(child.sessionId);
986
- }
987
- }
988
-
989
- unlinkIfExists(row.transcriptPath);
990
- unlinkIfExists(row.hookPath);
991
- unlinkIfExists(row.messagesPath);
992
- unlinkIfExists(this.artifacts.sessionManifestPath(id, false));
993
- this.artifacts.removeSessionDirIfEmpty(id);
994
- return { deleted: true };
995
- }
996
- }