@namzu/sdk 0.2.0 → 0.3.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 (297) hide show
  1. package/CHANGELOG.md +53 -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 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -3
  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/runtime/query/__tests__/context.test.js +8 -7
  60. package/dist/runtime/query/__tests__/context.test.js.map +1 -1
  61. package/dist/runtime/query/context-cache.d.ts +3 -3
  62. package/dist/runtime/query/context-cache.d.ts.map +1 -1
  63. package/dist/runtime/query/context-cache.js +2 -2
  64. package/dist/runtime/query/context-cache.js.map +1 -1
  65. package/dist/runtime/query/context.d.ts +12 -21
  66. package/dist/runtime/query/context.d.ts.map +1 -1
  67. package/dist/runtime/query/context.js +3 -1
  68. package/dist/runtime/query/context.js.map +1 -1
  69. package/dist/runtime/query/index.d.ts +13 -15
  70. package/dist/runtime/query/index.d.ts.map +1 -1
  71. package/dist/runtime/query/index.js +1 -0
  72. package/dist/runtime/query/index.js.map +1 -1
  73. package/dist/session/__tests__/integration/_fixtures.d.ts +11 -4
  74. package/dist/session/__tests__/integration/_fixtures.d.ts.map +1 -1
  75. package/dist/session/__tests__/integration/_fixtures.js +23 -6
  76. package/dist/session/__tests__/integration/_fixtures.js.map +1 -1
  77. package/dist/session/__tests__/integration/archive-gate.test.d.ts +15 -0
  78. package/dist/session/__tests__/integration/archive-gate.test.d.ts.map +1 -0
  79. package/dist/session/__tests__/integration/archive-gate.test.js +214 -0
  80. package/dist/session/__tests__/integration/archive-gate.test.js.map +1 -0
  81. package/dist/session/__tests__/integration/capacity-caps.test.js +13 -6
  82. package/dist/session/__tests__/integration/capacity-caps.test.js.map +1 -1
  83. package/dist/session/__tests__/integration/e2e-spawn.test.js +14 -2
  84. package/dist/session/__tests__/integration/e2e-spawn.test.js.map +1 -1
  85. package/dist/session/__tests__/integration/event-stream-ordering.test.js +14 -7
  86. package/dist/session/__tests__/integration/event-stream-ordering.test.js.map +1 -1
  87. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js +26 -14
  88. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js.map +1 -1
  89. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js +30 -20
  90. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js.map +1 -1
  91. package/dist/session/__tests__/integration/handoff-single-e2e.test.js +25 -9
  92. package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
  93. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js +11 -10
  94. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js.map +1 -1
  95. package/dist/session/__tests__/integration/prev-artifact-dag.test.js +5 -4
  96. package/dist/session/__tests__/integration/prev-artifact-dag.test.js.map +1 -1
  97. package/dist/session/__tests__/integration/retention-archive.test.js +3 -2
  98. package/dist/session/__tests__/integration/retention-archive.test.js.map +1 -1
  99. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts +26 -0
  100. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts.map +1 -0
  101. package/dist/session/__tests__/integration/spawn-rollback.test.js +236 -0
  102. package/dist/session/__tests__/integration/spawn-rollback.test.js.map +1 -0
  103. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js +2 -1
  104. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js.map +1 -1
  105. package/dist/session/__tests__/integration/tenant-isolation.test.js +14 -5
  106. package/dist/session/__tests__/integration/tenant-isolation.test.js.map +1 -1
  107. package/dist/session/errors.d.ts +79 -0
  108. package/dist/session/errors.d.ts.map +1 -1
  109. package/dist/session/errors.js +57 -0
  110. package/dist/session/errors.js.map +1 -1
  111. package/dist/session/handoff/__tests__/broadcast.test.js +49 -31
  112. package/dist/session/handoff/__tests__/broadcast.test.js.map +1 -1
  113. package/dist/session/handoff/__tests__/capacity.test.js +21 -18
  114. package/dist/session/handoff/__tests__/capacity.test.js.map +1 -1
  115. package/dist/session/handoff/__tests__/single.test.js +39 -30
  116. package/dist/session/handoff/__tests__/single.test.js.map +1 -1
  117. package/dist/session/handoff/assignment.d.ts +13 -1
  118. package/dist/session/handoff/assignment.d.ts.map +1 -1
  119. package/dist/session/handoff/broadcast.d.ts +7 -0
  120. package/dist/session/handoff/broadcast.d.ts.map +1 -1
  121. package/dist/session/handoff/broadcast.js +16 -1
  122. package/dist/session/handoff/broadcast.js.map +1 -1
  123. package/dist/session/handoff/single.d.ts +7 -0
  124. package/dist/session/handoff/single.d.ts.map +1 -1
  125. package/dist/session/handoff/single.js +13 -1
  126. package/dist/session/handoff/single.js.map +1 -1
  127. package/dist/session/hierarchy/__tests__/session.test.js +2 -0
  128. package/dist/session/hierarchy/__tests__/session.test.js.map +1 -1
  129. package/dist/session/hierarchy/index.d.ts +1 -0
  130. package/dist/session/hierarchy/index.d.ts.map +1 -1
  131. package/dist/session/hierarchy/index.js.map +1 -1
  132. package/dist/session/hierarchy/session.d.ts +15 -3
  133. package/dist/session/hierarchy/session.d.ts.map +1 -1
  134. package/dist/session/hierarchy/session.js.map +1 -1
  135. package/dist/session/hierarchy/thread.d.ts +54 -0
  136. package/dist/session/hierarchy/thread.d.ts.map +1 -0
  137. package/dist/session/hierarchy/thread.js +2 -0
  138. package/dist/session/hierarchy/thread.js.map +1 -0
  139. package/dist/session/migration/id-prefix.d.ts +8 -13
  140. package/dist/session/migration/id-prefix.d.ts.map +1 -1
  141. package/dist/session/migration/id-prefix.js +8 -13
  142. package/dist/session/migration/id-prefix.js.map +1 -1
  143. package/dist/session/retention/__tests__/archive.test.js +3 -2
  144. package/dist/session/retention/__tests__/archive.test.js.map +1 -1
  145. package/dist/session/summary/__tests__/materialize.test.js +4 -3
  146. package/dist/session/summary/__tests__/materialize.test.js.map +1 -1
  147. package/dist/store/index.d.ts +0 -2
  148. package/dist/store/index.d.ts.map +1 -1
  149. package/dist/store/index.js +0 -1
  150. package/dist/store/index.js.map +1 -1
  151. package/dist/store/session/__tests__/disk.test.js +32 -5
  152. package/dist/store/session/__tests__/disk.test.js.map +1 -1
  153. package/dist/store/session/__tests__/memory.test.js +50 -9
  154. package/dist/store/session/__tests__/memory.test.js.map +1 -1
  155. package/dist/store/session/disk.d.ts +2 -1
  156. package/dist/store/session/disk.d.ts.map +1 -1
  157. package/dist/store/session/disk.js +61 -0
  158. package/dist/store/session/disk.js.map +1 -1
  159. package/dist/store/session/index.d.ts.map +1 -1
  160. package/dist/store/session/index.js +3 -4
  161. package/dist/store/session/index.js.map +1 -1
  162. package/dist/store/session/memory.d.ts +2 -1
  163. package/dist/store/session/memory.d.ts.map +1 -1
  164. package/dist/store/session/memory.js +13 -0
  165. package/dist/store/session/memory.js.map +1 -1
  166. package/dist/store/thread/disk.d.ts +41 -0
  167. package/dist/store/thread/disk.d.ts.map +1 -0
  168. package/dist/store/thread/disk.js +229 -0
  169. package/dist/store/thread/disk.js.map +1 -0
  170. package/dist/store/thread/index.d.ts +4 -0
  171. package/dist/store/thread/index.d.ts.map +1 -0
  172. package/dist/store/thread/index.js +6 -0
  173. package/dist/store/thread/index.js.map +1 -0
  174. package/dist/store/thread/memory.d.ts +23 -0
  175. package/dist/store/thread/memory.d.ts.map +1 -0
  176. package/dist/store/thread/memory.js +90 -0
  177. package/dist/store/thread/memory.js.map +1 -0
  178. package/dist/types/agent/base.d.ts +17 -21
  179. package/dist/types/agent/base.d.ts.map +1 -1
  180. package/dist/types/agent/factory.d.ts +8 -2
  181. package/dist/types/agent/factory.d.ts.map +1 -1
  182. package/dist/types/agent/task.d.ts +18 -11
  183. package/dist/types/agent/task.d.ts.map +1 -1
  184. package/dist/types/ids/index.d.ts +5 -9
  185. package/dist/types/ids/index.d.ts.map +1 -1
  186. package/dist/types/ids/index.js +4 -4
  187. package/dist/types/ids/index.js.map +1 -1
  188. package/dist/types/rag/retrieval.d.ts +4 -3
  189. package/dist/types/rag/retrieval.d.ts.map +1 -1
  190. package/dist/types/run/config.d.ts +6 -5
  191. package/dist/types/run/config.d.ts.map +1 -1
  192. package/dist/types/run/metadata.d.ts +5 -18
  193. package/dist/types/run/metadata.d.ts.map +1 -1
  194. package/dist/types/session/ids.d.ts +4 -13
  195. package/dist/types/session/ids.d.ts.map +1 -1
  196. package/dist/types/session/ids.js +3 -6
  197. package/dist/types/session/ids.js.map +1 -1
  198. package/dist/types/session/index.d.ts +1 -1
  199. package/dist/types/session/index.d.ts.map +1 -1
  200. package/dist/types/session/store.d.ts +32 -10
  201. package/dist/types/session/store.d.ts.map +1 -1
  202. package/dist/types/session/store.js +3 -8
  203. package/dist/types/session/store.js.map +1 -1
  204. package/dist/types/thread/index.d.ts +2 -0
  205. package/dist/types/thread/index.d.ts.map +1 -0
  206. package/dist/types/thread/index.js +5 -0
  207. package/dist/types/thread/index.js.map +1 -0
  208. package/dist/types/thread/store.d.ts +86 -0
  209. package/dist/types/thread/store.d.ts.map +1 -0
  210. package/dist/types/thread/store.js +22 -0
  211. package/dist/types/thread/store.js.map +1 -0
  212. package/dist/utils/id.d.ts +1 -12
  213. package/dist/utils/id.d.ts.map +1 -1
  214. package/dist/utils/id.js +3 -23
  215. package/dist/utils/id.js.map +1 -1
  216. package/package.json +6 -11
  217. package/src/agents/ReactiveAgent.ts +3 -2
  218. package/src/agents/SupervisorAgent.ts +5 -2
  219. package/src/bridge/a2a/index.ts +0 -1
  220. package/src/bridge/a2a/message.ts +0 -32
  221. package/src/bridge/a2a/task.ts +8 -7
  222. package/src/contracts/api.ts +6 -42
  223. package/src/contracts/ids.ts +1 -1
  224. package/src/contracts/index.ts +2 -8
  225. package/src/contracts/schemas.ts +1 -8
  226. package/src/index.ts +0 -4
  227. package/src/manager/agent/__tests__/lifecycle.test.ts +34 -13
  228. package/src/manager/agent/lifecycle.ts +114 -35
  229. package/src/manager/index.ts +3 -0
  230. package/src/manager/run/persistence.ts +7 -1
  231. package/src/manager/thread/__tests__/lifecycle.test.ts +286 -0
  232. package/src/manager/thread/lifecycle.ts +217 -0
  233. package/src/rag/retriever.ts +2 -2
  234. package/src/runtime/query/__tests__/context.test.ts +9 -8
  235. package/src/runtime/query/context-cache.ts +4 -4
  236. package/src/runtime/query/context.ts +15 -22
  237. package/src/runtime/query/index.ts +15 -16
  238. package/src/session/__tests__/integration/_fixtures.ts +36 -8
  239. package/src/session/__tests__/integration/archive-gate.test.ts +288 -0
  240. package/src/session/__tests__/integration/capacity-caps.test.ts +13 -6
  241. package/src/session/__tests__/integration/e2e-spawn.test.ts +20 -2
  242. package/src/session/__tests__/integration/event-stream-ordering.test.ts +14 -7
  243. package/src/session/__tests__/integration/handoff-broadcast-e2e.test.ts +39 -13
  244. package/src/session/__tests__/integration/handoff-illegal-transition.test.ts +54 -19
  245. package/src/session/__tests__/integration/handoff-single-e2e.test.ts +40 -9
  246. package/src/session/__tests__/integration/hierarchy-lifecycle.test.ts +13 -10
  247. package/src/session/__tests__/integration/prev-artifact-dag.test.ts +12 -5
  248. package/src/session/__tests__/integration/retention-archive.test.ts +5 -3
  249. package/src/session/__tests__/integration/spawn-rollback.test.ts +313 -0
  250. package/src/session/__tests__/integration/summary-materialization-e2e.test.ts +4 -2
  251. package/src/session/__tests__/integration/tenant-isolation.test.ts +16 -6
  252. package/src/session/errors.ts +89 -0
  253. package/src/session/handoff/__tests__/broadcast.test.ts +56 -28
  254. package/src/session/handoff/__tests__/capacity.test.ts +26 -20
  255. package/src/session/handoff/__tests__/single.test.ts +45 -28
  256. package/src/session/handoff/assignment.ts +13 -1
  257. package/src/session/handoff/broadcast.ts +26 -1
  258. package/src/session/handoff/single.ts +23 -1
  259. package/src/session/hierarchy/__tests__/session.test.ts +9 -1
  260. package/src/session/hierarchy/index.ts +1 -0
  261. package/src/session/hierarchy/session.ts +15 -3
  262. package/src/session/hierarchy/thread.ts +55 -0
  263. package/src/session/migration/id-prefix.ts +8 -13
  264. package/src/session/retention/__tests__/archive.test.ts +5 -3
  265. package/src/session/summary/__tests__/materialize.test.ts +6 -4
  266. package/src/store/index.ts +0 -3
  267. package/src/store/session/__tests__/disk.test.ts +57 -6
  268. package/src/store/session/__tests__/memory.test.ts +84 -9
  269. package/src/store/session/disk.ts +57 -1
  270. package/src/store/session/index.ts +3 -4
  271. package/src/store/session/memory.ts +13 -1
  272. package/src/store/thread/disk.ts +261 -0
  273. package/src/store/thread/index.ts +7 -0
  274. package/src/store/thread/memory.ts +104 -0
  275. package/src/types/agent/base.ts +17 -21
  276. package/src/types/agent/factory.ts +8 -3
  277. package/src/types/agent/task.ts +19 -11
  278. package/src/types/ids/index.ts +8 -15
  279. package/src/types/rag/retrieval.ts +4 -3
  280. package/src/types/run/config.ts +6 -5
  281. package/src/types/run/metadata.ts +5 -18
  282. package/src/types/session/ids.ts +4 -15
  283. package/src/types/session/index.ts +1 -2
  284. package/src/types/session/store.ts +34 -11
  285. package/src/types/thread/index.ts +5 -0
  286. package/src/types/thread/store.ts +92 -0
  287. package/src/utils/id.ts +3 -24
  288. package/dist/store/conversation/memory.d.ts +0 -43
  289. package/dist/store/conversation/memory.d.ts.map +0 -1
  290. package/dist/store/conversation/memory.js +0 -108
  291. package/dist/store/conversation/memory.js.map +0 -1
  292. package/dist/types/conversation/index.d.ts +0 -14
  293. package/dist/types/conversation/index.d.ts.map +0 -1
  294. package/dist/types/conversation/index.js +0 -2
  295. package/dist/types/conversation/index.js.map +0 -1
  296. package/src/store/conversation/memory.ts +0 -144
  297. package/src/types/conversation/index.ts +0 -15
@@ -13,7 +13,9 @@
13
13
  */
14
14
 
15
15
  import { describe, expect, it, vi } from 'vitest'
16
+ import { ThreadManager } from '../../../manager/thread/lifecycle.js'
16
17
  import { InMemorySessionStore } from '../../../store/session/memory.js'
18
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
17
19
  import type { TenantId } from '../../../types/ids/index.js'
18
20
  import { generateHandoffId } from '../../../utils/id.js'
19
21
  import { TenantIsolationError } from '../../errors.js'
@@ -26,7 +28,10 @@ import { GitWorktreeDriver } from '../../workspace/git-worktree.js'
26
28
  import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
27
29
  import { DEFAULT_TENANT, OTHER_TENANT, okExec, stubLogger, userActor } from './_fixtures.js'
28
30
 
29
- function buildHandoffDeps(store: InMemorySessionStore): {
31
+ function buildHandoffDeps(
32
+ store: InMemorySessionStore,
33
+ threadStore: InMemoryThreadStore,
34
+ ): {
30
35
  deps: SingleHandoffDeps
31
36
  updateCalls: Array<{ status?: string; ownerVersion?: number }>
32
37
  } {
@@ -52,12 +57,14 @@ function buildHandoffDeps(store: InMemorySessionStore): {
52
57
  return originalUpdate(session, tenantId)
53
58
  }
54
59
 
60
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
55
61
  return {
56
62
  deps: {
57
63
  store,
58
64
  workspaceRegistry,
59
65
  capacity: new DefaultCapacityValidator(store),
60
66
  events: sink,
67
+ threadManager,
61
68
  },
62
69
  updateCalls,
63
70
  }
@@ -66,23 +73,29 @@ function buildHandoffDeps(store: InMemorySessionStore): {
66
73
  describe('Integration — single-recipient handoff E2E', () => {
67
74
  it('idle → locked → commit: source previousActors grew + ownerVersion bumped atomically', async () => {
68
75
  const store = new InMemorySessionStore()
76
+ const threadStore = new InMemoryThreadStore()
69
77
  const project = await store.createProject(
70
78
  { tenantId: DEFAULT_TENANT, name: 'ho' },
71
79
  DEFAULT_TENANT,
72
80
  )
81
+ const thread = await threadStore.createThread(
82
+ { projectId: project.id, title: 'ho' },
83
+ DEFAULT_TENANT,
84
+ )
73
85
  const sourceActor = userActor('usr_source')
74
86
  const recipientActor = userActor('usr_target')
75
87
  const session = await store.createSession(
76
- { projectId: project.id, currentActor: sourceActor },
88
+ { threadId: thread.id, projectId: project.id, currentActor: sourceActor },
77
89
  DEFAULT_TENANT,
78
90
  )
79
91
 
80
- const { deps, updateCalls } = buildHandoffDeps(store)
92
+ const { deps, updateCalls } = buildHandoffDeps(store, threadStore)
81
93
  const assignment: HandoffAssignment = {
82
94
  id: generateHandoffId(),
83
95
  mode: 'single',
84
96
  sourceSessionId: session.id,
85
97
  tenantId: DEFAULT_TENANT,
98
+ threadId: thread.id,
86
99
  projectId: project.id,
87
100
  sourceActor,
88
101
  recipientActor,
@@ -115,21 +128,27 @@ describe('Integration — single-recipient handoff E2E', () => {
115
128
 
116
129
  it('cross-tenant assignment rejects at entry (TenantIsolationError)', async () => {
117
130
  const store = new InMemorySessionStore()
131
+ const threadStore = new InMemoryThreadStore()
118
132
  const project = await store.createProject(
119
133
  { tenantId: DEFAULT_TENANT, name: 'ct' },
120
134
  DEFAULT_TENANT,
121
135
  )
136
+ const thread = await threadStore.createThread(
137
+ { projectId: project.id, title: 'ct' },
138
+ DEFAULT_TENANT,
139
+ )
122
140
  const session = await store.createSession(
123
- { projectId: project.id, currentActor: userActor('usr_source') },
141
+ { threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
124
142
  DEFAULT_TENANT,
125
143
  )
126
144
 
127
- const { deps } = buildHandoffDeps(store)
145
+ const { deps } = buildHandoffDeps(store, threadStore)
128
146
  const assignment: HandoffAssignment = {
129
147
  id: generateHandoffId(),
130
148
  mode: 'single',
131
149
  sourceSessionId: session.id,
132
150
  tenantId: OTHER_TENANT,
151
+ threadId: thread.id,
133
152
  projectId: project.id,
134
153
  sourceActor: userActor('usr_source', OTHER_TENANT),
135
154
  recipientActor: userActor('usr_target', OTHER_TENANT),
@@ -144,21 +163,27 @@ describe('Integration — single-recipient handoff E2E', () => {
144
163
 
145
164
  it('source-owned workspace provisioned for recipient', async () => {
146
165
  const store = new InMemorySessionStore()
166
+ const threadStore = new InMemoryThreadStore()
147
167
  const project = await store.createProject(
148
168
  { tenantId: DEFAULT_TENANT, name: 'wsp' },
149
169
  DEFAULT_TENANT,
150
170
  )
171
+ const thread = await threadStore.createThread(
172
+ { projectId: project.id, title: 'wsp' },
173
+ DEFAULT_TENANT,
174
+ )
151
175
  const source = await store.createSession(
152
- { projectId: project.id, currentActor: userActor('usr_source') },
176
+ { threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
153
177
  DEFAULT_TENANT,
154
178
  )
155
179
 
156
- const { deps } = buildHandoffDeps(store)
180
+ const { deps } = buildHandoffDeps(store, threadStore)
157
181
  const assignment: HandoffAssignment = {
158
182
  id: generateHandoffId(),
159
183
  mode: 'single',
160
184
  sourceSessionId: source.id,
161
185
  tenantId: DEFAULT_TENANT,
186
+ threadId: thread.id,
162
187
  projectId: project.id,
163
188
  sourceActor: userActor('usr_source'),
164
189
  recipientActor: userActor('usr_target'),
@@ -190,21 +215,27 @@ describe('Integration — single-recipient handoff E2E', () => {
190
215
  it('denormalized tenantId stamped on Session + SubSession records', async () => {
191
216
  const _tenantType: TenantId = DEFAULT_TENANT
192
217
  const store = new InMemorySessionStore()
218
+ const threadStore = new InMemoryThreadStore()
193
219
  const project = await store.createProject(
194
220
  { tenantId: DEFAULT_TENANT, name: 'denorm' },
195
221
  DEFAULT_TENANT,
196
222
  )
223
+ const thread = await threadStore.createThread(
224
+ { projectId: project.id, title: 'denorm' },
225
+ DEFAULT_TENANT,
226
+ )
197
227
  const source = await store.createSession(
198
- { projectId: project.id, currentActor: userActor('usr_source') },
228
+ { threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
199
229
  DEFAULT_TENANT,
200
230
  )
201
- const { deps } = buildHandoffDeps(store)
231
+ const { deps } = buildHandoffDeps(store, threadStore)
202
232
 
203
233
  const assignment: HandoffAssignment = {
204
234
  id: generateHandoffId(),
205
235
  mode: 'single',
206
236
  sourceSessionId: source.id,
207
237
  tenantId: DEFAULT_TENANT,
238
+ threadId: thread.id,
208
239
  projectId: project.id,
209
240
  sourceActor: userActor('usr_source'),
210
241
  recipientActor: userActor('usr_target'),
@@ -10,9 +10,12 @@
10
10
  */
11
11
 
12
12
  import { describe, expect, it } from 'vitest'
13
+ import type { ThreadId } from '../../../types/session/ids.js'
13
14
  import { TenantIsolationError } from '../../errors.js'
14
15
  import { DEFAULT_TENANT, agentActor, buildHarness, userActor } from './_fixtures.js'
15
16
 
17
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
18
+
16
19
  describe('Integration — hierarchy lifecycle', () => {
17
20
  it('creates Tenant → Project → Session → SubSession with properly branded IDs', async () => {
18
21
  const { store } = buildHarness()
@@ -23,7 +26,7 @@ describe('Integration — hierarchy lifecycle', () => {
23
26
  expect(project.tenantId.startsWith('tnt_')).toBe(true)
24
27
 
25
28
  const session = await store.createSession(
26
- { projectId: project.id, currentActor: userActor('usr_a') },
29
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
27
30
  tenant,
28
31
  )
29
32
  expect(session.id.startsWith('ses_')).toBe(true)
@@ -34,7 +37,7 @@ describe('Integration — hierarchy lifecycle', () => {
34
37
  expect(session.previousActors).toEqual([])
35
38
 
36
39
  const childSession = await store.createSession(
37
- { projectId: project.id, currentActor: agentActor('agt_worker') },
40
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_worker') },
38
41
  tenant,
39
42
  )
40
43
  const subSession = await store.createSubSession(
@@ -59,15 +62,15 @@ describe('Integration — hierarchy lifecycle', () => {
59
62
 
60
63
  const project = await store.createProject({ tenantId: tenant, name: 'drill' }, tenant)
61
64
  const parent = await store.createSession(
62
- { projectId: project.id, currentActor: userActor('usr_root') },
65
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_root') },
63
66
  tenant,
64
67
  )
65
68
  const childA = await store.createSession(
66
- { projectId: project.id, currentActor: agentActor('agt_a') },
69
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
67
70
  tenant,
68
71
  )
69
72
  const childB = await store.createSession(
70
- { projectId: project.id, currentActor: agentActor('agt_b') },
73
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_b') },
71
74
  tenant,
72
75
  )
73
76
  await store.createSubSession(
@@ -120,7 +123,7 @@ describe('Integration — hierarchy lifecycle', () => {
120
123
  const userC = userActor('usr_c')
121
124
 
122
125
  const session = await store.createSession(
123
- { projectId: project.id, currentActor: userA },
126
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userA },
124
127
  tenant,
125
128
  )
126
129
 
@@ -154,11 +157,11 @@ describe('Integration — hierarchy lifecycle', () => {
154
157
 
155
158
  const project = await store.createProject({ tenantId: tenant, name: 'cycle' }, tenant)
156
159
  const sA = await store.createSession(
157
- { projectId: project.id, currentActor: userActor('usr_a') },
160
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
158
161
  tenant,
159
162
  )
160
163
  const sB = await store.createSession(
161
- { projectId: project.id, currentActor: userActor('usr_b') },
164
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_b') },
162
165
  tenant,
163
166
  )
164
167
 
@@ -195,11 +198,11 @@ describe('Integration — hierarchy lifecycle', () => {
195
198
 
196
199
  const project = await store.createProject({ tenantId: tenant, name: 'lifecycle' }, tenant)
197
200
  const parent = await store.createSession(
198
- { projectId: project.id, currentActor: userActor('usr_a') },
201
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
199
202
  tenant,
200
203
  )
201
204
  const child = await store.createSession(
202
- { projectId: project.id, currentActor: agentActor('agt_a') },
205
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
203
206
  tenant,
204
207
  )
205
208
  const sub = await store.createSubSession(
@@ -14,7 +14,12 @@
14
14
  import { describe, expect, it } from 'vitest'
15
15
  import { InMemorySessionStore } from '../../../store/session/memory.js'
16
16
  import type { SessionId } from '../../../types/ids/index.js'
17
- import type { DeliverableId, SubSessionId, SummaryId } from '../../../types/session/ids.js'
17
+ import type {
18
+ DeliverableId,
19
+ SubSessionId,
20
+ SummaryId,
21
+ ThreadId,
22
+ } from '../../../types/session/ids.js'
18
23
  import {
19
24
  ArtifactRefCycleError,
20
25
  type InterventionChainLoader,
@@ -24,6 +29,8 @@ import {
24
29
  import type { DeliverableRef, SessionSummaryDeliverable } from '../../summary/deliverable.js'
25
30
  import { DEFAULT_TENANT, agentActor, userActor } from './_fixtures.js'
26
31
 
32
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
33
+
27
34
  /**
28
35
  * Build a live loader pointing at a real InMemorySessionStore. Each node
29
36
  * resolves via `findParentSubSession`-style lookup on the store's sub-session
@@ -68,7 +75,7 @@ async function buildLinearChain(
68
75
  let previous: SessionId | null = null
69
76
  for (let i = 0; i < length; i++) {
70
77
  const s = await store.createSession(
71
- { projectId: project.id, currentActor: agentActor(`agt_${i}`) },
78
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(`agt_${i}`) },
72
79
  DEFAULT_TENANT,
73
80
  )
74
81
  if (previous) {
@@ -260,15 +267,15 @@ describe('Integration — prevArtifactRef DAG against real store', () => {
260
267
  DEFAULT_TENANT,
261
268
  )
262
269
  const sA = await store.createSession(
263
- { projectId: project.id, currentActor: agentActor('agt_a') },
270
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
264
271
  DEFAULT_TENANT,
265
272
  )
266
273
  const sB = await store.createSession(
267
- { projectId: project.id, currentActor: agentActor('agt_b') },
274
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_b') },
268
275
  DEFAULT_TENANT,
269
276
  )
270
277
  const sC = await store.createSession(
271
- { projectId: project.id, currentActor: agentActor('agt_c') },
278
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_c') },
272
279
  DEFAULT_TENANT,
273
280
  )
274
281
 
@@ -15,24 +15,26 @@ import { join } from 'node:path'
15
15
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
16
16
  import { InMemorySessionStore } from '../../../store/session/memory.js'
17
17
  import { createUserMessage } from '../../../types/message/index.js'
18
- import type { WorkspaceId } from '../../../types/session/ids.js'
18
+ import type { ThreadId, WorkspaceId } from '../../../types/session/ids.js'
19
19
  import { ArchivalManager, ArchiveNotConfiguredError } from '../../retention/archive.js'
20
20
  import { DiskArchiveBackend } from '../../retention/disk-backend.js'
21
21
  import type { WorkspaceRef } from '../../workspace/ref.js'
22
22
  import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
23
23
  import { DEFAULT_TENANT, agentActor, userActor } from './_fixtures.js'
24
24
 
25
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
26
+
25
27
  async function seedIdleSubSession(store: InMemorySessionStore) {
26
28
  const project = await store.createProject(
27
29
  { tenantId: DEFAULT_TENANT, name: 'archive' },
28
30
  DEFAULT_TENANT,
29
31
  )
30
32
  const parent = await store.createSession(
31
- { projectId: project.id, currentActor: userActor('usr_a') },
33
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
32
34
  DEFAULT_TENANT,
33
35
  )
34
36
  const child = await store.createSession(
35
- { projectId: project.id, currentActor: agentActor('agt_w') },
37
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_w') },
36
38
  DEFAULT_TENANT,
37
39
  )
38
40
  const sub = await store.createSubSession(
@@ -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)