@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
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { ThreadManager } from '../../../manager/thread/lifecycle.js'
2
3
  import type { ActorRef } from '../../../session/hierarchy/actor.js'
3
4
  import {
4
5
  type ExecFile,
@@ -7,6 +8,7 @@ import {
7
8
  } from '../../../session/workspace/git-worktree.js'
8
9
  import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js'
9
10
  import { InMemorySessionStore } from '../../../store/session/memory.js'
11
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
10
12
  import type { SessionId, TenantId, UserId } from '../../../types/ids/index.js'
11
13
  import type { ProjectId } from '../../../types/session/ids.js'
12
14
  import { generateHandoffId } from '../../../utils/id.js'
@@ -56,7 +58,11 @@ interface DepsBundle {
56
58
  events: MockedHandoffEventSink
57
59
  }
58
60
 
59
- function buildDeps(store: InMemorySessionStore, execOverride?: ExecFile): DepsBundle {
61
+ function buildDeps(
62
+ store: InMemorySessionStore,
63
+ threadStore: InMemoryThreadStore,
64
+ execOverride?: ExecFile,
65
+ ): DepsBundle {
60
66
  const exec: ExecFile = execOverride ? execOverride : async (_file, _args) => okExec()
61
67
  const driver = new GitWorktreeDriver({
62
68
  repoRoot: '/repo',
@@ -73,29 +79,36 @@ function buildDeps(store: InMemorySessionStore, execOverride?: ExecFile): DepsBu
73
79
  onBroadcastRollback: vi.fn<(ev: HandoffBroadcastRollbackEvent) => void>(),
74
80
  }
75
81
 
82
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
76
83
  return {
77
84
  deps: {
78
85
  store,
79
86
  workspaceRegistry: registry,
80
87
  capacity: new DefaultCapacityValidator(store),
81
88
  events,
89
+ threadManager,
82
90
  },
83
91
  events,
84
92
  }
85
93
  }
86
94
 
87
- async function seedIdle(store: InMemorySessionStore) {
95
+ async function seedIdle(store: InMemorySessionStore, threadStore: InMemoryThreadStore) {
88
96
  const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
97
+ const thread = await threadStore.createThread(
98
+ { projectId: project.id, title: 'handoff-broadcast-test' },
99
+ tenant,
100
+ )
89
101
  const session = await store.createSession(
90
- { projectId: project.id, currentActor: user('usr_source') },
102
+ { threadId: thread.id, projectId: project.id, currentActor: user('usr_source') },
91
103
  tenant,
92
104
  )
93
- return { project, session }
105
+ return { project, thread, session }
94
106
  }
95
107
 
96
108
  function buildAssignments(
97
109
  sourceSessionId: SessionId,
98
110
  projectId: ProjectId,
111
+ threadId: Awaited<ReturnType<InMemoryThreadStore['createThread']>>['id'],
99
112
  expectedOwnerVersion: number,
100
113
  recipients: ActorRef[],
101
114
  broadcastId = 'bc_1',
@@ -105,6 +118,7 @@ function buildAssignments(
105
118
  mode: 'broadcast' as const,
106
119
  sourceSessionId,
107
120
  tenantId: tenant,
121
+ threadId,
108
122
  projectId,
109
123
  sourceActor: user('usr_source'),
110
124
  recipientActor,
@@ -116,16 +130,18 @@ function buildAssignments(
116
130
 
117
131
  describe('executeBroadcastHandoff', () => {
118
132
  let store: InMemorySessionStore
133
+ let threadStore: InMemoryThreadStore
119
134
 
120
135
  beforeEach(() => {
121
136
  store = new InMemorySessionStore()
137
+ threadStore = new InMemoryThreadStore()
122
138
  })
123
139
 
124
140
  it('happy path: 3 recipients → source ends in awaiting_merge with 3 new children', async () => {
125
- const { project, session } = await seedIdle(store)
126
- const { deps, events } = buildDeps(store)
141
+ const { project, thread, session } = await seedIdle(store, threadStore)
142
+ const { deps, events } = buildDeps(store, threadStore)
127
143
 
128
- const assignments = buildAssignments(session.id, project.id, 0, [
144
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
129
145
  user('usr_bob'),
130
146
  user('usr_carol'),
131
147
  user('usr_dan'),
@@ -149,7 +165,7 @@ describe('executeBroadcastHandoff', () => {
149
165
  })
150
166
 
151
167
  it('rollback on mid-fan-out failure (2nd recipient worktree add fails): source reverts, rollback emits accurate partialState', async () => {
152
- const { project, session } = await seedIdle(store)
168
+ const { project, thread, session } = await seedIdle(store, threadStore)
153
169
 
154
170
  let addCount = 0
155
171
  const exec: ExecFile = async (_file, args) => {
@@ -159,9 +175,9 @@ describe('executeBroadcastHandoff', () => {
159
175
  }
160
176
  return okExec()
161
177
  }
162
- const { deps, events } = buildDeps(store, exec)
178
+ const { deps, events } = buildDeps(store, threadStore, exec)
163
179
 
164
- const assignments = buildAssignments(session.id, project.id, 0, [
180
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
165
181
  user('usr_b'),
166
182
  user('usr_c'),
167
183
  user('usr_d'),
@@ -196,7 +212,7 @@ describe('executeBroadcastHandoff', () => {
196
212
  })
197
213
 
198
214
  it('rollback performs full cleanup via deleteSubSession/deleteSession (no status-flip stopgap)', async () => {
199
- const { project, session } = await seedIdle(store)
215
+ const { project, thread, session } = await seedIdle(store, threadStore)
200
216
 
201
217
  let addCount = 0
202
218
  const exec: ExecFile = async (_file, args) => {
@@ -206,9 +222,12 @@ describe('executeBroadcastHandoff', () => {
206
222
  }
207
223
  return okExec()
208
224
  }
209
- const { deps } = buildDeps(store, exec)
225
+ const { deps } = buildDeps(store, threadStore, exec)
210
226
 
211
- const assignments = buildAssignments(session.id, project.id, 0, [user('usr_b'), user('usr_c')])
227
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
228
+ user('usr_b'),
229
+ user('usr_c'),
230
+ ])
212
231
 
213
232
  await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow()
214
233
 
@@ -224,7 +243,7 @@ describe('executeBroadcastHandoff', () => {
224
243
  })
225
244
 
226
245
  it('rollback idempotency: worktree dispose throwing during rollback does not bubble a secondary failure', async () => {
227
- const { project, session } = await seedIdle(store)
246
+ const { project, thread, session } = await seedIdle(store, threadStore)
228
247
 
229
248
  let addCount = 0
230
249
  let removeCount = 0
@@ -242,9 +261,12 @@ describe('executeBroadcastHandoff', () => {
242
261
  }
243
262
  return okExec()
244
263
  }
245
- const { deps, events } = buildDeps(store, exec)
264
+ const { deps, events } = buildDeps(store, threadStore, exec)
246
265
 
247
- const assignments = buildAssignments(session.id, project.id, 0, [user('usr_b'), user('usr_c')])
266
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
267
+ user('usr_b'),
268
+ user('usr_c'),
269
+ ])
248
270
 
249
271
  // Outer failure is the PRIMARY one — the secondary dispose failure is
250
272
  // swallowed. Primary wraps in WorkspaceBackendError (create op).
@@ -262,11 +284,15 @@ describe('executeBroadcastHandoff', () => {
262
284
  })
263
285
 
264
286
  it('dedupe: two assignments targeting same recipient → rejected pre-lock (no side effects)', async () => {
265
- const { project, session } = await seedIdle(store)
266
- const { deps, events } = buildDeps(store)
287
+ const { project, thread, session } = await seedIdle(store, threadStore)
288
+ const { deps, events } = buildDeps(store, threadStore)
267
289
 
268
290
  const bob = user('usr_bob')
269
- const assignments = buildAssignments(session.id, project.id, 0, [bob, bob, user('usr_dan')])
291
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
292
+ bob,
293
+ bob,
294
+ user('usr_dan'),
295
+ ])
270
296
 
271
297
  await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
272
298
  /duplicate recipient/,
@@ -281,11 +307,11 @@ describe('executeBroadcastHandoff', () => {
281
307
  })
282
308
 
283
309
  it('width cap: 9 recipients exceeds default maxWidth=8 → rejected before source lock', async () => {
284
- const { project, session } = await seedIdle(store)
285
- const { deps, events } = buildDeps(store)
310
+ const { project, thread, session } = await seedIdle(store, threadStore)
311
+ const { deps, events } = buildDeps(store, threadStore)
286
312
 
287
313
  const recipients = Array.from({ length: 9 }, (_, i) => user(`usr_${i}`))
288
- const assignments = buildAssignments(session.id, project.id, 0, recipients)
314
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, recipients)
289
315
 
290
316
  await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
291
317
  /Delegation capacity exceeded/,
@@ -298,12 +324,13 @@ describe('executeBroadcastHandoff', () => {
298
324
  })
299
325
 
300
326
  it('concurrent broadcast on same source: second attempt rejected with HandoffVersionConflict', async () => {
301
- const { project, session } = await seedIdle(store)
302
- const { deps } = buildDeps(store)
327
+ const { project, thread, session } = await seedIdle(store, threadStore)
328
+ const { deps } = buildDeps(store, threadStore)
303
329
 
304
330
  const firstAssignments = buildAssignments(
305
331
  session.id,
306
332
  project.id,
333
+ thread.id,
307
334
  0,
308
335
  [user('usr_b'), user('usr_c')],
309
336
  'bc_1',
@@ -322,6 +349,7 @@ describe('executeBroadcastHandoff', () => {
322
349
  const second = buildAssignments(
323
350
  session.id,
324
351
  project.id,
352
+ thread.id,
325
353
  0, // stale — actual is 1
326
354
  [user('usr_d'), user('usr_e')],
327
355
  'bc_2',
@@ -332,16 +360,16 @@ describe('executeBroadcastHandoff', () => {
332
360
  })
333
361
 
334
362
  it('empty assignments → throws a descriptive error', async () => {
335
- const { deps } = buildDeps(store)
363
+ const { deps } = buildDeps(store, threadStore)
336
364
  await expect(executeBroadcastHandoff(deps, [], tenant)).rejects.toThrow(
337
365
  /assignments must not be empty/,
338
366
  )
339
367
  })
340
368
 
341
369
  it('single-row broadcast → rejected (caller must use executeSingleHandoff)', async () => {
342
- const { project, session } = await seedIdle(store)
343
- const { deps } = buildDeps(store)
344
- const assignments = buildAssignments(session.id, project.id, 0, [user('usr_b')])
370
+ const { project, thread, session } = await seedIdle(store, threadStore)
371
+ const { deps } = buildDeps(store, threadStore)
372
+ const assignments = buildAssignments(session.id, project.id, thread.id, 0, [user('usr_b')])
345
373
 
346
374
  await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
347
375
  /single-recipient handoffs must use executeSingleHandoff/,
@@ -1,7 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { ActorRef } from '../../../session/hierarchy/actor.js'
3
3
  import { InMemorySessionStore } from '../../../store/session/memory.js'
4
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
4
5
  import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
6
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
5
7
  import { DefaultCapacityValidator, DelegationCapacityExceeded } from '../capacity.js'
6
8
 
7
9
  const tenant = 'tnt_alpha' as TenantId
@@ -16,18 +18,22 @@ function agent(): ActorRef {
16
18
 
17
19
  async function seedProject(store: InMemorySessionStore) {
18
20
  const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
19
- const root = await store.createSession({ projectId: project.id, currentActor: user() }, tenant)
20
- return { project, root }
21
+ const threadStore = new InMemoryThreadStore()
22
+ const thread = await threadStore.createThread({ projectId: project.id, title: 'default' }, tenant)
23
+ const root = await store.createSession(
24
+ { threadId: thread.id, projectId: project.id, currentActor: user() },
25
+ tenant,
26
+ )
27
+ return { project, thread, root }
21
28
  }
22
29
 
23
30
  async function spawnChild(
24
31
  store: InMemorySessionStore,
25
32
  parentId: SessionId,
26
- projectId: ReturnType<typeof String> extends never
27
- ? never
28
- : Parameters<InMemorySessionStore['createSession']>[0]['projectId'],
33
+ projectId: ProjectId,
34
+ threadId: ThreadId,
29
35
  ): Promise<{ childId: SessionId }> {
30
- const child = await store.createSession({ projectId, currentActor: user() }, tenant)
36
+ const child = await store.createSession({ threadId, projectId, currentActor: user() }, tenant)
31
37
  await store.createSubSession(
32
38
  {
33
39
  parentSessionId: parentId,
@@ -51,11 +57,11 @@ describe('DefaultCapacityValidator', () => {
51
57
 
52
58
  it('depth: chain of 4 (root→c1→c2→c3→c4) allows a 5th (depth 5) to pass when limit = 5', async () => {
53
59
  const store = new InMemorySessionStore()
54
- const { project, root } = await seedProject(store)
55
- const c1 = await spawnChild(store, root.id, project.id)
56
- const c2 = await spawnChild(store, c1.childId, project.id)
57
- const c3 = await spawnChild(store, c2.childId, project.id)
58
- const c4 = await spawnChild(store, c3.childId, project.id)
60
+ const { project, thread, root } = await seedProject(store)
61
+ const c1 = await spawnChild(store, root.id, project.id, thread.id)
62
+ const c2 = await spawnChild(store, c1.childId, project.id, thread.id)
63
+ const c3 = await spawnChild(store, c2.childId, project.id, thread.id)
64
+ const c4 = await spawnChild(store, c3.childId, project.id, thread.id)
59
65
 
60
66
  const validator = new DefaultCapacityValidator(store)
61
67
  // Ancestry of c4: root→c1→c2→c3→c4 = length 5. Spawning under c4 = depth 5.
@@ -64,11 +70,11 @@ describe('DefaultCapacityValidator', () => {
64
70
 
65
71
  it('depth: over-limit throws DelegationCapacityExceeded with dimension=depth', async () => {
66
72
  const store = new InMemorySessionStore()
67
- const { project, root } = await seedProject(store)
68
- const c1 = await spawnChild(store, root.id, project.id)
69
- const c2 = await spawnChild(store, c1.childId, project.id)
70
- const c3 = await spawnChild(store, c2.childId, project.id)
71
- const c4 = await spawnChild(store, c3.childId, project.id)
73
+ const { project, thread, root } = await seedProject(store)
74
+ const c1 = await spawnChild(store, root.id, project.id, thread.id)
75
+ const c2 = await spawnChild(store, c1.childId, project.id, thread.id)
76
+ const c3 = await spawnChild(store, c2.childId, project.id, thread.id)
77
+ const c4 = await spawnChild(store, c3.childId, project.id, thread.id)
72
78
 
73
79
  const validator = new DefaultCapacityValidator(store)
74
80
  try {
@@ -94,9 +100,9 @@ describe('DefaultCapacityValidator', () => {
94
100
 
95
101
  it('width: existing 5 + pending 3 = 8 passes exactly at the limit', async () => {
96
102
  const store = new InMemorySessionStore()
97
- const { project, root } = await seedProject(store)
103
+ const { project, thread, root } = await seedProject(store)
98
104
  for (let i = 0; i < 5; i++) {
99
- await spawnChild(store, root.id, project.id)
105
+ await spawnChild(store, root.id, project.id, thread.id)
100
106
  }
101
107
  const validator = new DefaultCapacityValidator(store)
102
108
  await expect(validator.validateWidth(root.id, 3, 8, tenant)).resolves.toBeUndefined()
@@ -104,9 +110,9 @@ describe('DefaultCapacityValidator', () => {
104
110
 
105
111
  it('width: existing 6 + pending 3 = 9 exceeds 8, throws dimension=width', async () => {
106
112
  const store = new InMemorySessionStore()
107
- const { project, root } = await seedProject(store)
113
+ const { project, thread, root } = await seedProject(store)
108
114
  for (let i = 0; i < 6; i++) {
109
- await spawnChild(store, root.id, project.id)
115
+ await spawnChild(store, root.id, project.id, thread.id)
110
116
  }
111
117
  const validator = new DefaultCapacityValidator(store)
112
118
  try {