@namzu/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/CHANGELOG.md +74 -2
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +3 -2
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +5 -2
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/bridge/a2a/index.d.ts +1 -1
  9. package/dist/bridge/a2a/index.d.ts.map +1 -1
  10. package/dist/bridge/a2a/index.js +1 -1
  11. package/dist/bridge/a2a/index.js.map +1 -1
  12. package/dist/bridge/a2a/message.d.ts +0 -2
  13. package/dist/bridge/a2a/message.d.ts.map +1 -1
  14. package/dist/bridge/a2a/message.js +0 -26
  15. package/dist/bridge/a2a/message.js.map +1 -1
  16. package/dist/bridge/a2a/task.d.ts +4 -3
  17. package/dist/bridge/a2a/task.d.ts.map +1 -1
  18. package/dist/bridge/a2a/task.js +4 -4
  19. package/dist/bridge/a2a/task.js.map +1 -1
  20. package/dist/contracts/api.d.ts +6 -38
  21. package/dist/contracts/api.d.ts.map +1 -1
  22. package/dist/contracts/ids.d.ts +1 -1
  23. package/dist/contracts/ids.d.ts.map +1 -1
  24. package/dist/contracts/index.d.ts +3 -5
  25. package/dist/contracts/index.d.ts.map +1 -1
  26. package/dist/contracts/index.js +1 -1
  27. package/dist/contracts/index.js.map +1 -1
  28. package/dist/contracts/schemas.d.ts +1 -31
  29. package/dist/contracts/schemas.d.ts.map +1 -1
  30. package/dist/contracts/schemas.js +1 -7
  31. package/dist/contracts/schemas.js.map +1 -1
  32. package/dist/index.d.ts +2 -7
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -6
  35. package/dist/index.js.map +1 -1
  36. package/dist/manager/agent/__tests__/lifecycle.test.js +27 -13
  37. package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
  38. package/dist/manager/agent/lifecycle.d.ts +9 -0
  39. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  40. package/dist/manager/agent/lifecycle.js +93 -31
  41. package/dist/manager/agent/lifecycle.js.map +1 -1
  42. package/dist/manager/index.d.ts +2 -0
  43. package/dist/manager/index.d.ts.map +1 -1
  44. package/dist/manager/index.js +1 -0
  45. package/dist/manager/index.js.map +1 -1
  46. package/dist/manager/run/persistence.d.ts +3 -1
  47. package/dist/manager/run/persistence.d.ts.map +1 -1
  48. package/dist/manager/run/persistence.js +5 -0
  49. package/dist/manager/run/persistence.js.map +1 -1
  50. package/dist/manager/thread/__tests__/lifecycle.test.d.ts +2 -0
  51. package/dist/manager/thread/__tests__/lifecycle.test.d.ts.map +1 -0
  52. package/dist/manager/thread/__tests__/lifecycle.test.js +216 -0
  53. package/dist/manager/thread/__tests__/lifecycle.test.js.map +1 -0
  54. package/dist/manager/thread/lifecycle.d.ts +105 -0
  55. package/dist/manager/thread/lifecycle.d.ts.map +1 -0
  56. package/dist/manager/thread/lifecycle.js +186 -0
  57. package/dist/manager/thread/lifecycle.js.map +1 -0
  58. package/dist/rag/retriever.js +2 -2
  59. package/dist/registry/tool/execute.js +1 -1
  60. package/dist/registry/tool/execute.js.map +1 -1
  61. package/dist/runtime/query/__tests__/context.test.js +8 -7
  62. package/dist/runtime/query/__tests__/context.test.js.map +1 -1
  63. package/dist/runtime/query/context-cache.d.ts +3 -3
  64. package/dist/runtime/query/context-cache.d.ts.map +1 -1
  65. package/dist/runtime/query/context-cache.js +2 -2
  66. package/dist/runtime/query/context-cache.js.map +1 -1
  67. package/dist/runtime/query/context.d.ts +12 -21
  68. package/dist/runtime/query/context.d.ts.map +1 -1
  69. package/dist/runtime/query/context.js +3 -1
  70. package/dist/runtime/query/context.js.map +1 -1
  71. package/dist/runtime/query/index.d.ts +13 -15
  72. package/dist/runtime/query/index.d.ts.map +1 -1
  73. package/dist/runtime/query/index.js +2 -1
  74. package/dist/runtime/query/index.js.map +1 -1
  75. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  76. package/dist/runtime/query/iteration/index.js +1 -1
  77. package/dist/runtime/query/iteration/index.js.map +1 -1
  78. package/dist/session/__tests__/integration/_fixtures.d.ts +11 -4
  79. package/dist/session/__tests__/integration/_fixtures.d.ts.map +1 -1
  80. package/dist/session/__tests__/integration/_fixtures.js +23 -6
  81. package/dist/session/__tests__/integration/_fixtures.js.map +1 -1
  82. package/dist/session/__tests__/integration/archive-gate.test.d.ts +15 -0
  83. package/dist/session/__tests__/integration/archive-gate.test.d.ts.map +1 -0
  84. package/dist/session/__tests__/integration/archive-gate.test.js +214 -0
  85. package/dist/session/__tests__/integration/archive-gate.test.js.map +1 -0
  86. package/dist/session/__tests__/integration/capacity-caps.test.js +13 -6
  87. package/dist/session/__tests__/integration/capacity-caps.test.js.map +1 -1
  88. package/dist/session/__tests__/integration/e2e-spawn.test.js +14 -2
  89. package/dist/session/__tests__/integration/e2e-spawn.test.js.map +1 -1
  90. package/dist/session/__tests__/integration/event-stream-ordering.test.js +14 -7
  91. package/dist/session/__tests__/integration/event-stream-ordering.test.js.map +1 -1
  92. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js +26 -14
  93. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js.map +1 -1
  94. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js +30 -20
  95. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js.map +1 -1
  96. package/dist/session/__tests__/integration/handoff-single-e2e.test.js +25 -9
  97. package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
  98. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js +11 -10
  99. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js.map +1 -1
  100. package/dist/session/__tests__/integration/prev-artifact-dag.test.js +5 -4
  101. package/dist/session/__tests__/integration/prev-artifact-dag.test.js.map +1 -1
  102. package/dist/session/__tests__/integration/retention-archive.test.js +3 -2
  103. package/dist/session/__tests__/integration/retention-archive.test.js.map +1 -1
  104. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts +26 -0
  105. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts.map +1 -0
  106. package/dist/session/__tests__/integration/spawn-rollback.test.js +236 -0
  107. package/dist/session/__tests__/integration/spawn-rollback.test.js.map +1 -0
  108. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js +2 -1
  109. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js.map +1 -1
  110. package/dist/session/__tests__/integration/tenant-isolation.test.js +14 -5
  111. package/dist/session/__tests__/integration/tenant-isolation.test.js.map +1 -1
  112. package/dist/session/errors.d.ts +79 -0
  113. package/dist/session/errors.d.ts.map +1 -1
  114. package/dist/session/errors.js +57 -0
  115. package/dist/session/errors.js.map +1 -1
  116. package/dist/session/handoff/__tests__/broadcast.test.js +49 -31
  117. package/dist/session/handoff/__tests__/broadcast.test.js.map +1 -1
  118. package/dist/session/handoff/__tests__/capacity.test.js +21 -18
  119. package/dist/session/handoff/__tests__/capacity.test.js.map +1 -1
  120. package/dist/session/handoff/__tests__/single.test.js +39 -30
  121. package/dist/session/handoff/__tests__/single.test.js.map +1 -1
  122. package/dist/session/handoff/assignment.d.ts +13 -1
  123. package/dist/session/handoff/assignment.d.ts.map +1 -1
  124. package/dist/session/handoff/broadcast.d.ts +7 -0
  125. package/dist/session/handoff/broadcast.d.ts.map +1 -1
  126. package/dist/session/handoff/broadcast.js +16 -1
  127. package/dist/session/handoff/broadcast.js.map +1 -1
  128. package/dist/session/handoff/single.d.ts +7 -0
  129. package/dist/session/handoff/single.d.ts.map +1 -1
  130. package/dist/session/handoff/single.js +13 -1
  131. package/dist/session/handoff/single.js.map +1 -1
  132. package/dist/session/hierarchy/__tests__/session.test.js +2 -0
  133. package/dist/session/hierarchy/__tests__/session.test.js.map +1 -1
  134. package/dist/session/hierarchy/index.d.ts +1 -0
  135. package/dist/session/hierarchy/index.d.ts.map +1 -1
  136. package/dist/session/hierarchy/index.js.map +1 -1
  137. package/dist/session/hierarchy/session.d.ts +15 -3
  138. package/dist/session/hierarchy/session.d.ts.map +1 -1
  139. package/dist/session/hierarchy/session.js.map +1 -1
  140. package/dist/session/hierarchy/thread.d.ts +54 -0
  141. package/dist/session/hierarchy/thread.d.ts.map +1 -0
  142. package/dist/session/hierarchy/thread.js +2 -0
  143. package/dist/session/hierarchy/thread.js.map +1 -0
  144. package/dist/session/migration/id-prefix.d.ts +8 -13
  145. package/dist/session/migration/id-prefix.d.ts.map +1 -1
  146. package/dist/session/migration/id-prefix.js +8 -13
  147. package/dist/session/migration/id-prefix.js.map +1 -1
  148. package/dist/session/retention/__tests__/archive.test.js +3 -2
  149. package/dist/session/retention/__tests__/archive.test.js.map +1 -1
  150. package/dist/session/summary/__tests__/materialize.test.js +4 -3
  151. package/dist/session/summary/__tests__/materialize.test.js.map +1 -1
  152. package/dist/store/index.d.ts +0 -2
  153. package/dist/store/index.d.ts.map +1 -1
  154. package/dist/store/index.js +0 -1
  155. package/dist/store/index.js.map +1 -1
  156. package/dist/store/session/__tests__/disk.test.js +32 -5
  157. package/dist/store/session/__tests__/disk.test.js.map +1 -1
  158. package/dist/store/session/__tests__/memory.test.js +50 -9
  159. package/dist/store/session/__tests__/memory.test.js.map +1 -1
  160. package/dist/store/session/disk.d.ts +2 -1
  161. package/dist/store/session/disk.d.ts.map +1 -1
  162. package/dist/store/session/disk.js +61 -0
  163. package/dist/store/session/disk.js.map +1 -1
  164. package/dist/store/session/index.d.ts.map +1 -1
  165. package/dist/store/session/index.js +3 -4
  166. package/dist/store/session/index.js.map +1 -1
  167. package/dist/store/session/memory.d.ts +2 -1
  168. package/dist/store/session/memory.d.ts.map +1 -1
  169. package/dist/store/session/memory.js +13 -0
  170. package/dist/store/session/memory.js.map +1 -1
  171. package/dist/store/thread/disk.d.ts +41 -0
  172. package/dist/store/thread/disk.d.ts.map +1 -0
  173. package/dist/store/thread/disk.js +229 -0
  174. package/dist/store/thread/disk.js.map +1 -0
  175. package/dist/store/thread/index.d.ts +4 -0
  176. package/dist/store/thread/index.d.ts.map +1 -0
  177. package/dist/store/thread/index.js +6 -0
  178. package/dist/store/thread/index.js.map +1 -0
  179. package/dist/store/thread/memory.d.ts +23 -0
  180. package/dist/store/thread/memory.d.ts.map +1 -0
  181. package/dist/store/thread/memory.js +90 -0
  182. package/dist/store/thread/memory.js.map +1 -0
  183. package/dist/telemetry/runtime-accessors.d.ts +4 -0
  184. package/dist/telemetry/runtime-accessors.d.ts.map +1 -0
  185. package/dist/telemetry/runtime-accessors.js +17 -0
  186. package/dist/telemetry/runtime-accessors.js.map +1 -0
  187. package/dist/types/agent/base.d.ts +17 -21
  188. package/dist/types/agent/base.d.ts.map +1 -1
  189. package/dist/types/agent/factory.d.ts +8 -2
  190. package/dist/types/agent/factory.d.ts.map +1 -1
  191. package/dist/types/agent/task.d.ts +18 -11
  192. package/dist/types/agent/task.d.ts.map +1 -1
  193. package/dist/types/ids/index.d.ts +5 -9
  194. package/dist/types/ids/index.d.ts.map +1 -1
  195. package/dist/types/ids/index.js +4 -4
  196. package/dist/types/ids/index.js.map +1 -1
  197. package/dist/types/rag/retrieval.d.ts +4 -3
  198. package/dist/types/rag/retrieval.d.ts.map +1 -1
  199. package/dist/types/run/config.d.ts +6 -5
  200. package/dist/types/run/config.d.ts.map +1 -1
  201. package/dist/types/run/metadata.d.ts +5 -18
  202. package/dist/types/run/metadata.d.ts.map +1 -1
  203. package/dist/types/session/ids.d.ts +4 -13
  204. package/dist/types/session/ids.d.ts.map +1 -1
  205. package/dist/types/session/ids.js +3 -6
  206. package/dist/types/session/ids.js.map +1 -1
  207. package/dist/types/session/index.d.ts +1 -1
  208. package/dist/types/session/index.d.ts.map +1 -1
  209. package/dist/types/session/store.d.ts +32 -10
  210. package/dist/types/session/store.d.ts.map +1 -1
  211. package/dist/types/session/store.js +3 -8
  212. package/dist/types/session/store.js.map +1 -1
  213. package/dist/types/thread/index.d.ts +2 -0
  214. package/dist/types/thread/index.d.ts.map +1 -0
  215. package/dist/types/thread/index.js +5 -0
  216. package/dist/types/thread/index.js.map +1 -0
  217. package/dist/types/thread/store.d.ts +86 -0
  218. package/dist/types/thread/store.d.ts.map +1 -0
  219. package/dist/types/thread/store.js +22 -0
  220. package/dist/types/thread/store.js.map +1 -0
  221. package/dist/utils/id.d.ts +1 -12
  222. package/dist/utils/id.d.ts.map +1 -1
  223. package/dist/utils/id.js +3 -23
  224. package/dist/utils/id.js.map +1 -1
  225. package/package.json +11 -20
  226. package/src/agents/ReactiveAgent.ts +3 -2
  227. package/src/agents/SupervisorAgent.ts +5 -2
  228. package/src/bridge/a2a/index.ts +0 -1
  229. package/src/bridge/a2a/message.ts +0 -32
  230. package/src/bridge/a2a/task.ts +8 -7
  231. package/src/contracts/api.ts +6 -42
  232. package/src/contracts/ids.ts +1 -1
  233. package/src/contracts/index.ts +2 -8
  234. package/src/contracts/schemas.ts +1 -8
  235. package/src/index.ts +3 -15
  236. package/src/manager/agent/__tests__/lifecycle.test.ts +34 -13
  237. package/src/manager/agent/lifecycle.ts +114 -35
  238. package/src/manager/index.ts +3 -0
  239. package/src/manager/run/persistence.ts +7 -1
  240. package/src/manager/thread/__tests__/lifecycle.test.ts +286 -0
  241. package/src/manager/thread/lifecycle.ts +217 -0
  242. package/src/rag/retriever.ts +2 -2
  243. package/src/registry/tool/execute.ts +1 -1
  244. package/src/runtime/query/__tests__/context.test.ts +9 -8
  245. package/src/runtime/query/context-cache.ts +4 -4
  246. package/src/runtime/query/context.ts +15 -22
  247. package/src/runtime/query/index.ts +16 -17
  248. package/src/runtime/query/iteration/index.ts +1 -1
  249. package/src/session/__tests__/integration/_fixtures.ts +36 -8
  250. package/src/session/__tests__/integration/archive-gate.test.ts +288 -0
  251. package/src/session/__tests__/integration/capacity-caps.test.ts +13 -6
  252. package/src/session/__tests__/integration/e2e-spawn.test.ts +20 -2
  253. package/src/session/__tests__/integration/event-stream-ordering.test.ts +14 -7
  254. package/src/session/__tests__/integration/handoff-broadcast-e2e.test.ts +39 -13
  255. package/src/session/__tests__/integration/handoff-illegal-transition.test.ts +54 -19
  256. package/src/session/__tests__/integration/handoff-single-e2e.test.ts +40 -9
  257. package/src/session/__tests__/integration/hierarchy-lifecycle.test.ts +13 -10
  258. package/src/session/__tests__/integration/prev-artifact-dag.test.ts +12 -5
  259. package/src/session/__tests__/integration/retention-archive.test.ts +5 -3
  260. package/src/session/__tests__/integration/spawn-rollback.test.ts +313 -0
  261. package/src/session/__tests__/integration/summary-materialization-e2e.test.ts +4 -2
  262. package/src/session/__tests__/integration/tenant-isolation.test.ts +16 -6
  263. package/src/session/errors.ts +89 -0
  264. package/src/session/handoff/__tests__/broadcast.test.ts +56 -28
  265. package/src/session/handoff/__tests__/capacity.test.ts +26 -20
  266. package/src/session/handoff/__tests__/single.test.ts +45 -28
  267. package/src/session/handoff/assignment.ts +13 -1
  268. package/src/session/handoff/broadcast.ts +26 -1
  269. package/src/session/handoff/single.ts +23 -1
  270. package/src/session/hierarchy/__tests__/session.test.ts +9 -1
  271. package/src/session/hierarchy/index.ts +1 -0
  272. package/src/session/hierarchy/session.ts +15 -3
  273. package/src/session/hierarchy/thread.ts +55 -0
  274. package/src/session/migration/id-prefix.ts +8 -13
  275. package/src/session/retention/__tests__/archive.test.ts +5 -3
  276. package/src/session/summary/__tests__/materialize.test.ts +6 -4
  277. package/src/store/index.ts +0 -3
  278. package/src/store/session/__tests__/disk.test.ts +57 -6
  279. package/src/store/session/__tests__/memory.test.ts +84 -9
  280. package/src/store/session/disk.ts +57 -1
  281. package/src/store/session/index.ts +3 -4
  282. package/src/store/session/memory.ts +13 -1
  283. package/src/store/thread/disk.ts +261 -0
  284. package/src/store/thread/index.ts +7 -0
  285. package/src/store/thread/memory.ts +104 -0
  286. package/src/telemetry/runtime-accessors.ts +19 -0
  287. package/src/types/agent/base.ts +17 -21
  288. package/src/types/agent/factory.ts +8 -3
  289. package/src/types/agent/task.ts +19 -11
  290. package/src/types/ids/index.ts +8 -15
  291. package/src/types/rag/retrieval.ts +4 -3
  292. package/src/types/run/config.ts +6 -5
  293. package/src/types/run/metadata.ts +5 -18
  294. package/src/types/session/ids.ts +4 -15
  295. package/src/types/session/index.ts +1 -2
  296. package/src/types/session/store.ts +34 -11
  297. package/src/types/thread/index.ts +5 -0
  298. package/src/types/thread/store.ts +92 -0
  299. package/src/utils/id.ts +3 -24
  300. package/dist/provider/telemetry/setup.d.ts +0 -19
  301. package/dist/provider/telemetry/setup.d.ts.map +0 -1
  302. package/dist/provider/telemetry/setup.js +0 -102
  303. package/dist/provider/telemetry/setup.js.map +0 -1
  304. package/dist/store/conversation/memory.d.ts +0 -43
  305. package/dist/store/conversation/memory.d.ts.map +0 -1
  306. package/dist/store/conversation/memory.js +0 -108
  307. package/dist/store/conversation/memory.js.map +0 -1
  308. package/dist/telemetry/index.d.ts +0 -6
  309. package/dist/telemetry/index.d.ts.map +0 -1
  310. package/dist/telemetry/index.js +0 -4
  311. package/dist/telemetry/index.js.map +0 -1
  312. package/dist/telemetry/metrics.d.ts +0 -8
  313. package/dist/telemetry/metrics.d.ts.map +0 -1
  314. package/dist/telemetry/metrics.js +0 -53
  315. package/dist/telemetry/metrics.js.map +0 -1
  316. package/dist/types/conversation/index.d.ts +0 -14
  317. package/dist/types/conversation/index.d.ts.map +0 -1
  318. package/dist/types/conversation/index.js +0 -2
  319. package/dist/types/conversation/index.js.map +0 -1
  320. package/dist/types/telemetry/index.d.ts +0 -10
  321. package/dist/types/telemetry/index.d.ts.map +0 -1
  322. package/dist/types/telemetry/index.js +0 -2
  323. package/dist/types/telemetry/index.js.map +0 -1
  324. package/src/provider/telemetry/setup.ts +0 -125
  325. package/src/store/conversation/memory.ts +0 -144
  326. package/src/telemetry/index.ts +0 -14
  327. package/src/telemetry/metrics.ts +0 -69
  328. package/src/types/conversation/index.ts +0 -15
  329. package/src/types/telemetry/index.ts +0 -10
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Integration — AgentManager.provisionSpawn compensating rollback.
3
+ *
4
+ * Covers Codex SPAWN-ROLLBACK critique (ses_001-hierarchy-redesign Phase 2
5
+ * adversarial review, 2026-04-18). Without the try/catch wrapper around the
6
+ * createSession → updateSession → createSubSession → workspace.create
7
+ * mutation block, a failure after createSession leaves an `active` child
8
+ * session with no subsession edge — invisible to the parent, but counted
9
+ * against `maxDelegationWidth` and visible to SessionStore.listSessions
10
+ * consumers (archive/delete flows in ThreadManager).
11
+ *
12
+ * Failure modes exercised:
13
+ * A. Workspace driver throws on create. Subsession exists; must flip to
14
+ * 'failed' for audit. Child session must be hard-deleted.
15
+ * B. Subsession insert fails (store injection). No subsession recorded.
16
+ * Child session must be hard-deleted.
17
+ *
18
+ * Assertions in both cases:
19
+ * - sendMessage rejects with the underlying error.
20
+ * - SessionStore.listSessions(threadId) returns no row with the child id.
21
+ * - Parent session remains untouched (status, currentActor).
22
+ * - Fan-out cap reclaims the slot (next spawn succeeds up to the same
23
+ * width).
24
+ */
25
+
26
+ import { describe, expect, it } from 'vitest'
27
+ import { EMPTY_TOKEN_USAGE } from '../../../constants/limits.js'
28
+ import { AgentManager } from '../../../manager/agent/lifecycle.js'
29
+ import { ThreadManager } from '../../../manager/thread/lifecycle.js'
30
+ import { AgentRegistry } from '../../../registry/agent/definitions.js'
31
+ import { InMemorySessionStore } from '../../../store/session/memory.js'
32
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
33
+ import type {
34
+ AgentCapabilities,
35
+ AgentInput,
36
+ BaseAgentConfig,
37
+ BaseAgentResult,
38
+ } from '../../../types/agent/base.js'
39
+ import type { Agent } from '../../../types/agent/core.js'
40
+ import type { AgentDefinition } from '../../../types/agent/factory.js'
41
+ import type { AgentTaskContext, SendMessageOptions } from '../../../types/agent/task.js'
42
+ import type { RunId, TenantId, UserId } from '../../../types/ids/index.js'
43
+ import { createAssistantMessage } from '../../../types/message/index.js'
44
+ import type { SummaryId } from '../../../types/session/ids.js'
45
+ import { ZERO_COST } from '../../../utils/cost.js'
46
+ import { DefaultCapacityValidator } from '../../handoff/capacity.js'
47
+ import type { ActorRef } from '../../hierarchy/actor.js'
48
+ import { SessionSummaryMaterializer } from '../../summary/materialize.js'
49
+ import type {
50
+ BranchWorkspaceParams,
51
+ CreateWorkspaceParams,
52
+ WorkspaceBackendDriver,
53
+ WorkspaceInspection,
54
+ } from '../../workspace/driver.js'
55
+ import type { WorkspaceRef } from '../../workspace/ref.js'
56
+ import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
57
+
58
+ const tenant = 'tnt_alpha' as TenantId
59
+
60
+ const capabilities: AgentCapabilities = {
61
+ supportsTools: false,
62
+ supportsStreaming: false,
63
+ supportsConcurrency: false,
64
+ supportsSubAgents: false,
65
+ }
66
+
67
+ function buildAgent(id: string): Agent<BaseAgentConfig, BaseAgentResult> {
68
+ return {
69
+ type: 'reactive',
70
+ metadata: {
71
+ type: 'reactive',
72
+ id,
73
+ name: id,
74
+ version: '1.0.0',
75
+ category: 'test',
76
+ description: id,
77
+ capabilities,
78
+ },
79
+ run: async (_input: AgentInput, _config: BaseAgentConfig): Promise<BaseAgentResult> => ({
80
+ runId: 'run_child' as RunId,
81
+ status: 'completed',
82
+ usage: { ...EMPTY_TOKEN_USAGE },
83
+ cost: { ...ZERO_COST },
84
+ iterations: 1,
85
+ durationMs: 1,
86
+ messages: [createAssistantMessage('child did the work')],
87
+ result: 'child did the work',
88
+ }),
89
+ cancel: async () => undefined,
90
+ getCapabilities: () => capabilities,
91
+ }
92
+ }
93
+
94
+ function buildDefinition(agent: Agent<BaseAgentConfig, BaseAgentResult>): AgentDefinition {
95
+ return {
96
+ info: {
97
+ id: agent.metadata.id,
98
+ name: agent.metadata.name,
99
+ version: agent.metadata.version,
100
+ category: agent.metadata.category,
101
+ description: agent.metadata.description,
102
+ tools: [],
103
+ defaults: { model: 'test', tokenBudget: 1_000 },
104
+ },
105
+ typedAgent: agent,
106
+ }
107
+ }
108
+
109
+ class FailingWorkspaceDriver implements WorkspaceBackendDriver {
110
+ readonly kind = 'git-worktree' as const
111
+ createCalls = 0
112
+
113
+ async create(_params: CreateWorkspaceParams): Promise<WorkspaceRef> {
114
+ this.createCalls += 1
115
+ throw new Error('synthetic workspace backend failure')
116
+ }
117
+
118
+ async branch(_source: WorkspaceRef, _params: BranchWorkspaceParams): Promise<WorkspaceRef> {
119
+ throw new Error('unused in this test')
120
+ }
121
+
122
+ async dispose(_ref: WorkspaceRef): Promise<void> {
123
+ /* no-op */
124
+ }
125
+
126
+ async inspect(_ref: WorkspaceRef): Promise<WorkspaceInspection> {
127
+ throw new Error('unused in this test')
128
+ }
129
+ }
130
+
131
+ describe('provisionSpawn compensating rollback', () => {
132
+ it('workspace driver failure — deletes child session, marks subsession failed, leaves no orphan', async () => {
133
+ const store = new InMemorySessionStore()
134
+ const threadStore = new InMemoryThreadStore()
135
+ const project = await store.createProject(
136
+ { tenantId: tenant, name: 'rollback-project' },
137
+ tenant,
138
+ )
139
+ const thread = await threadStore.createThread(
140
+ { projectId: project.id, title: 'rollback-topic' },
141
+ tenant,
142
+ )
143
+
144
+ const userActor: ActorRef = {
145
+ kind: 'user',
146
+ userId: 'usr_root' as UserId,
147
+ tenantId: tenant,
148
+ }
149
+
150
+ const parentSession = await store.createSession(
151
+ { threadId: thread.id, projectId: project.id, currentActor: userActor },
152
+ tenant,
153
+ )
154
+ await store.updateSession({ ...parentSession, status: 'active' }, tenant)
155
+
156
+ let summaryCounter = 0
157
+ const materializer = new SessionSummaryMaterializer({
158
+ store,
159
+ generateSummaryId: () => `sum_test_${++summaryCounter}` as SummaryId,
160
+ })
161
+
162
+ const registry = new AgentRegistry()
163
+ registry.register(buildDefinition(buildAgent('worker')))
164
+
165
+ const workspaceRegistry = new WorkspaceBackendRegistry()
166
+ const failingDriver = new FailingWorkspaceDriver()
167
+ workspaceRegistry.register(failingDriver)
168
+
169
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
170
+ const manager = new AgentManager(registry, undefined, {
171
+ sessionStore: store,
172
+ summaryMaterializer: materializer,
173
+ workspaceRegistry,
174
+ capacity: new DefaultCapacityValidator(store),
175
+ threadManager,
176
+ })
177
+
178
+ const taskContext: AgentTaskContext = {
179
+ parentRunId: 'run_parent' as RunId,
180
+ parentAgentId: 'supervisor',
181
+ parentAbortController: new AbortController(),
182
+ depth: 0,
183
+ budgetTracker: { total: 100_000, remaining: 100_000 },
184
+ tenantId: tenant,
185
+ threadId: thread.id,
186
+ sessionId: parentSession.id,
187
+ projectId: project.id,
188
+ parentActor: userActor,
189
+ }
190
+
191
+ const options: SendMessageOptions = {
192
+ agentId: 'worker',
193
+ input: { messages: [], workingDirectory: '/tmp' },
194
+ parentSessionId: parentSession.id,
195
+ tenantId: tenant,
196
+ projectId: project.id,
197
+ parentActor: userActor,
198
+ workspaceBackend: 'git-worktree',
199
+ }
200
+
201
+ await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
202
+ 'synthetic workspace backend failure',
203
+ )
204
+ expect(failingDriver.createCalls).toBe(1)
205
+
206
+ // Child session is gone — archive/delete flows and fan-out caps see
207
+ // zero child attached to the thread beyond the parent.
208
+ const sessionsOnThread = await store.listSessions(thread.id, tenant)
209
+ expect(sessionsOnThread.map((s) => s.id)).toEqual([parentSession.id])
210
+
211
+ // Parent session is untouched.
212
+ const refetchedParent = await store.getSession(parentSession.id, tenant)
213
+ expect(refetchedParent?.status).toBe('active')
214
+ expect(refetchedParent?.currentActor).toEqual(userActor)
215
+
216
+ // No subsession breadcrumb — `subsession_spawned` never fired
217
+ // (provisionSpawn aborted before buildSpawnRecord), so nothing is
218
+ // expecting an audit row. Leaving a `status: 'failed'` record would
219
+ // dangle with no corresponding emission.
220
+ const subsessions = await store.getChildren(parentSession.id, tenant)
221
+ expect(subsessions).toHaveLength(0)
222
+ })
223
+
224
+ it('repeated rollback does not accumulate orphan sessions or subsessions', async () => {
225
+ const store = new InMemorySessionStore()
226
+ const threadStore = new InMemoryThreadStore()
227
+ const project = await store.createProject(
228
+ {
229
+ tenantId: tenant,
230
+ name: 'rollback-repeat-project',
231
+ },
232
+ tenant,
233
+ )
234
+ const thread = await threadStore.createThread(
235
+ { projectId: project.id, title: 'rollback-width-topic' },
236
+ tenant,
237
+ )
238
+
239
+ const userActor: ActorRef = {
240
+ kind: 'user',
241
+ userId: 'usr_root' as UserId,
242
+ tenantId: tenant,
243
+ }
244
+
245
+ const parentSession = await store.createSession(
246
+ { threadId: thread.id, projectId: project.id, currentActor: userActor },
247
+ tenant,
248
+ )
249
+ await store.updateSession({ ...parentSession, status: 'active' }, tenant)
250
+
251
+ let summaryCounter = 0
252
+ const materializer = new SessionSummaryMaterializer({
253
+ store,
254
+ generateSummaryId: () => `sum_test_${++summaryCounter}` as SummaryId,
255
+ })
256
+
257
+ const registry = new AgentRegistry()
258
+ registry.register(buildDefinition(buildAgent('worker')))
259
+
260
+ const workspaceRegistry = new WorkspaceBackendRegistry()
261
+ workspaceRegistry.register(new FailingWorkspaceDriver())
262
+
263
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
264
+ const manager = new AgentManager(registry, undefined, {
265
+ sessionStore: store,
266
+ summaryMaterializer: materializer,
267
+ workspaceRegistry,
268
+ capacity: new DefaultCapacityValidator(store),
269
+ threadManager,
270
+ })
271
+
272
+ const taskContext: AgentTaskContext = {
273
+ parentRunId: 'run_parent' as RunId,
274
+ parentAgentId: 'supervisor',
275
+ parentAbortController: new AbortController(),
276
+ depth: 0,
277
+ budgetTracker: { total: 100_000, remaining: 100_000 },
278
+ tenantId: tenant,
279
+ threadId: thread.id,
280
+ sessionId: parentSession.id,
281
+ projectId: project.id,
282
+ parentActor: userActor,
283
+ }
284
+
285
+ const options: SendMessageOptions = {
286
+ agentId: 'worker',
287
+ input: { messages: [], workingDirectory: '/tmp' },
288
+ parentSessionId: parentSession.id,
289
+ tenantId: tenant,
290
+ projectId: project.id,
291
+ parentActor: userActor,
292
+ workspaceBackend: 'git-worktree',
293
+ }
294
+
295
+ // Two consecutive failing spawns. Without rollback, two orphan
296
+ // `active` child sessions would accumulate; with rollback, each
297
+ // attempt cleans up after itself and the store stays at { parent }.
298
+ await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
299
+ 'synthetic workspace backend failure',
300
+ )
301
+ await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
302
+ 'synthetic workspace backend failure',
303
+ )
304
+
305
+ // Still no child session under the thread.
306
+ const sessionsOnThread = await store.listSessions(thread.id, tenant)
307
+ expect(sessionsOnThread.map((s) => s.id)).toEqual([parentSession.id])
308
+
309
+ // No lingering subsession rows — both attempts rolled back cleanly.
310
+ const subsessions = await store.getChildren(parentSession.id, tenant)
311
+ expect(subsessions).toHaveLength(0)
312
+ })
313
+ })
@@ -18,18 +18,20 @@
18
18
  import { describe, expect, it } from 'vitest'
19
19
  import { InMemorySessionStore } from '../../../store/session/memory.js'
20
20
  import type { SessionId } from '../../../types/ids/index.js'
21
- import type { SummaryId } from '../../../types/session/ids.js'
21
+ import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
22
22
  import { SessionSummaryMaterializer } from '../../summary/materialize.js'
23
23
  import { SessionAlreadySummarizedError } from '../../summary/ref.js'
24
24
  import { DEFAULT_TENANT, agentActor } from './_fixtures.js'
25
25
 
26
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
27
+
26
28
  async function seedActive(store: InMemorySessionStore) {
27
29
  const project = await store.createProject(
28
30
  { tenantId: DEFAULT_TENANT, name: 'summary' },
29
31
  DEFAULT_TENANT,
30
32
  )
31
33
  const session = await store.createSession(
32
- { projectId: project.id, currentActor: agentActor('agt_worker') },
34
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_worker') },
33
35
  DEFAULT_TENANT,
34
36
  )
35
37
  await store.updateSession({ ...session, status: 'active' }, DEFAULT_TENANT)
@@ -14,10 +14,12 @@
14
14
  import { describe, expect, it } from 'vitest'
15
15
  import { InMemorySessionStore } from '../../../store/session/memory.js'
16
16
  import { createUserMessage } from '../../../types/message/index.js'
17
- import type { ProjectId, SubSessionId, SummaryId } from '../../../types/session/ids.js'
17
+ import type { ProjectId, SubSessionId, SummaryId, ThreadId } from '../../../types/session/ids.js'
18
18
  import { TenantIsolationError } from '../../errors.js'
19
19
  import { DEFAULT_TENANT, OTHER_TENANT, agentActor, userActor } from './_fixtures.js'
20
20
 
21
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
22
+
21
23
  async function seedTenantAResources() {
22
24
  const store = new InMemorySessionStore()
23
25
  const project = await store.createProject(
@@ -25,11 +27,11 @@ async function seedTenantAResources() {
25
27
  DEFAULT_TENANT,
26
28
  )
27
29
  const parent = await store.createSession(
28
- { projectId: project.id, currentActor: userActor('usr_a') },
30
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
29
31
  DEFAULT_TENANT,
30
32
  )
31
33
  const child = await store.createSession(
32
- { projectId: project.id, currentActor: agentActor('agt_a') },
34
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
33
35
  DEFAULT_TENANT,
34
36
  )
35
37
  const sub = await store.createSubSession(
@@ -200,7 +202,11 @@ describe('Integration — tenant isolation', () => {
200
202
  const { store, project } = await seedTenantAResources()
201
203
  await expect(
202
204
  store.createSession(
203
- { projectId: project.id, currentActor: userActor('usr_intruder', OTHER_TENANT) },
205
+ {
206
+ threadId: TEST_THREAD_ID,
207
+ projectId: project.id,
208
+ currentActor: userActor('usr_intruder', OTHER_TENANT),
209
+ },
204
210
  OTHER_TENANT,
205
211
  ),
206
212
  ).rejects.toBeInstanceOf(TenantIsolationError)
@@ -232,7 +238,11 @@ describe('Integration — tenant isolation', () => {
232
238
  OTHER_TENANT,
233
239
  )
234
240
  const otherChild = await store.createSession(
235
- { projectId: otherProject.id, currentActor: userActor('usr_other', OTHER_TENANT) },
241
+ {
242
+ threadId: TEST_THREAD_ID,
243
+ projectId: otherProject.id,
244
+ currentActor: userActor('usr_other', OTHER_TENANT),
245
+ },
236
246
  OTHER_TENANT,
237
247
  )
238
248
 
@@ -264,7 +274,7 @@ describe('Integration — tenant isolation', () => {
264
274
  await store.createProject({ tenantId: DEFAULT_TENANT, name: 'del' }, DEFAULT_TENANT)
265
275
  ).id
266
276
  const lonely = await store.createSession(
267
- { projectId: project, currentActor: userActor('usr_lonely') },
277
+ { threadId: TEST_THREAD_ID, projectId: project, currentActor: userActor('usr_lonely') },
268
278
  DEFAULT_TENANT,
269
279
  )
270
280
  await expect(store.deleteSession(lonely.id, OTHER_TENANT)).rejects.toBeInstanceOf(
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { SessionId, TenantId } from '../types/ids/index.js'
11
+ import type { ThreadId } from '../types/session/ids.js'
12
+ import type { SessionStatus } from './hierarchy/session.js'
11
13
  import type { WorkspaceBackendKind } from './workspace/driver.js'
12
14
 
13
15
  /**
@@ -68,3 +70,90 @@ export class WorkspaceBackendError extends Error {
68
70
  this.details = details
69
71
  }
70
72
  }
73
+
74
+ /**
75
+ * Raised by {@link import('../types/thread/store.js').ThreadStore.updateThread}
76
+ * when the supplied {@link Thread.ownerVersion} does not match the persisted
77
+ * record. The caller must re-read via `getThread`, re-apply its intended
78
+ * mutation on top of the fresh record, and retry. Mirrors the Session
79
+ * handoff CAS pattern (§6.1).
80
+ */
81
+ export class StaleThreadError extends Error {
82
+ readonly details: {
83
+ threadId: ThreadId
84
+ expectedVersion: number
85
+ actualVersion: number
86
+ }
87
+
88
+ constructor(details: { threadId: ThreadId; expectedVersion: number; actualVersion: number }) {
89
+ super(
90
+ `Stale Thread ${details.threadId}: expected ownerVersion=${details.expectedVersion}, actual=${details.actualVersion}`,
91
+ )
92
+ this.name = 'StaleThreadError'
93
+ this.details = details
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Raised by the spawn path (and any caller that enforces the open-thread
99
+ * precondition) when a Thread is in `'archived'` state and would-be mutations
100
+ * require it to be `'open'`. Convention #5: deny-by-default — archival is a
101
+ * hard read-only boundary.
102
+ */
103
+ export class ThreadClosedError extends Error {
104
+ readonly details: {
105
+ threadId: ThreadId
106
+ op: string
107
+ }
108
+
109
+ constructor(details: { threadId: ThreadId; op: string }) {
110
+ super(`Thread ${details.threadId} is archived; operation '${details.op}' rejected`)
111
+ this.name = 'ThreadClosedError'
112
+ this.details = details
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Raised by {@link import('../manager/thread/lifecycle.js').ThreadManager.archive}
118
+ * and `.delete` when the Thread's session-presence precondition is violated:
119
+ *
120
+ * - `op: 'archive'` — at least one Session under the Thread is in a
121
+ * non-terminal state (`active | locked | awaiting_hitl | awaiting_merge`).
122
+ * The caller must first quiesce those sessions (let them reach `idle`,
123
+ * `failed`, or `archived`) before flipping the Thread to archived.
124
+ * - `op: 'delete'` — the Thread still has at least one attached Session.
125
+ * Callers must either archive + tombstone those sessions (`deleteSession`)
126
+ * before calling `deleteThread`, or accept that deletion is not yet safe.
127
+ *
128
+ * `blockingSessions` carries the first {@link THREAD_NOT_EMPTY_SAMPLE_LIMIT}
129
+ * offenders with their current status so operator tooling can surface an
130
+ * actionable list without unbounded error payloads on large threads.
131
+ * `totalBlockingSessions` holds the full count even when the sample is
132
+ * truncated. Convention #5: deny-by-default — no implicit cascade, no silent
133
+ * no-op.
134
+ */
135
+ export const THREAD_NOT_EMPTY_SAMPLE_LIMIT = 50
136
+
137
+ export class ThreadNotEmptyError extends Error {
138
+ readonly details: {
139
+ threadId: ThreadId
140
+ tenantId: TenantId
141
+ op: 'archive' | 'delete'
142
+ blockingSessions: ReadonlyArray<{ sessionId: SessionId; status: SessionStatus }>
143
+ totalBlockingSessions: number
144
+ }
145
+
146
+ constructor(details: {
147
+ threadId: ThreadId
148
+ tenantId: TenantId
149
+ op: 'archive' | 'delete'
150
+ blockingSessions: ReadonlyArray<{ sessionId: SessionId; status: SessionStatus }>
151
+ totalBlockingSessions: number
152
+ }) {
153
+ super(
154
+ `Thread ${details.threadId} ${details.op} blocked: ${details.totalBlockingSessions} session(s) still attached`,
155
+ )
156
+ this.name = 'ThreadNotEmptyError'
157
+ this.details = details
158
+ }
159
+ }