@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,217 @@
1
+ /**
2
+ * ThreadManager — thin orchestrator over {@link ThreadStore} and
3
+ * {@link SessionStore}.
4
+ *
5
+ * Owns user-facing lifecycle operations on the Thread topic layer plus the
6
+ * archive-gate contract enforced at session-creation ingress sites.
7
+ *
8
+ * Phase 2.6 wired `ThreadManager.requireOpen` into three ingress paths:
9
+ * - {@link AgentManager.provisionSpawn} (child session creation)
10
+ * - `executeSingleHandoff` (recipient session creation)
11
+ * - `executeBroadcastHandoff` (N recipient sessions per fan-out)
12
+ * Those call sites depend on this manager, so the one-method indirection
13
+ * stopped being "structural overhead" the moment archive/delete needed the
14
+ * session-presence cross-check anyway.
15
+ *
16
+ * Archive + delete require cross-store preconditions (session-presence
17
+ * checks) — enforced here where both stores are in scope. The stores
18
+ * themselves stay unaware of each other's layout (Convention #0), which is
19
+ * why the gate lives at the manager layer rather than as a universal store
20
+ * interceptor (see `archive` JSDoc for the direct-store-bypass boundary).
21
+ */
22
+
23
+ import {
24
+ THREAD_NOT_EMPTY_SAMPLE_LIMIT,
25
+ ThreadClosedError,
26
+ ThreadNotEmptyError,
27
+ } from '../../session/errors.js'
28
+ import type { Session, SessionStatus } from '../../session/hierarchy/session.js'
29
+ import type { Thread } from '../../session/hierarchy/thread.js'
30
+ import type { TenantId } from '../../types/ids/index.js'
31
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
32
+ import type { SessionStore } from '../../types/session/store.js'
33
+ import type { CreateThreadParams, ThreadStore } from '../../types/thread/store.js'
34
+
35
+ export interface ThreadManagerDeps {
36
+ readonly threadStore: ThreadStore
37
+ readonly sessionStore: SessionStore
38
+ }
39
+
40
+ /**
41
+ * Session statuses that block Thread archival. A session in any of these
42
+ * states has live work in-flight (mid-run, mid-handoff, blocked on human
43
+ * input, or orchestrating a broadcast merge) — freezing the Thread while any
44
+ * of them are active would strand resumable work.
45
+ *
46
+ * `idle`, `failed`, and `archived` are archival-compatible: they are
47
+ * quiescent or already-terminal, so a newly-frozen Thread can safely contain
48
+ * them. This list mirrors the `SessionStatus` discriminants that represent
49
+ * "not-yet-done" work (session-hierarchy.md §5.1).
50
+ */
51
+ const ARCHIVAL_BLOCKING_STATUSES: ReadonlySet<SessionStatus> = new Set([
52
+ 'active',
53
+ 'locked',
54
+ 'awaiting_hitl',
55
+ 'awaiting_merge',
56
+ ])
57
+
58
+ export class ThreadManager {
59
+ private readonly deps: ThreadManagerDeps
60
+
61
+ constructor(deps: ThreadManagerDeps) {
62
+ this.deps = deps
63
+ }
64
+
65
+ /** Persist a new Thread. Thin passthrough for uniformity at the manager surface. */
66
+ create(params: CreateThreadParams, tenantId: TenantId): Promise<Thread> {
67
+ return this.deps.threadStore.createThread(params, tenantId)
68
+ }
69
+
70
+ /** Read a Thread by id; returns `null` when absent for the tenant. */
71
+ get(threadId: ThreadId, tenantId: TenantId): Promise<Thread | null> {
72
+ return this.deps.threadStore.getThread(threadId, tenantId)
73
+ }
74
+
75
+ /**
76
+ * CAS update on a Thread. Propagates {@link import('../../session/errors.js').StaleThreadError}
77
+ * from the store on `ownerVersion` mismatch — callers re-read, re-apply,
78
+ * and retry.
79
+ */
80
+ update(thread: Thread, tenantId: TenantId): Promise<void> {
81
+ return this.deps.threadStore.updateThread(thread, tenantId)
82
+ }
83
+
84
+ /** List Threads under a Project, ordered by `createdAt` ascending. */
85
+ list(projectId: ProjectId, tenantId: TenantId): Promise<readonly Thread[]> {
86
+ return this.deps.threadStore.listThreads(projectId, tenantId)
87
+ }
88
+
89
+ /**
90
+ * Load a Thread and assert it is in `'open'` state. Used by the spawn path
91
+ * as a precondition — a SubSession cannot be created under an archived
92
+ * Thread. Throws on absence and on archival; returns the loaded Thread on
93
+ * success so callers can avoid the second round-trip.
94
+ *
95
+ * Convention #5: deny-by-default. A missing Thread is a hard error, not a
96
+ * silent "assume archived".
97
+ */
98
+ async requireOpen(threadId: ThreadId, tenantId: TenantId): Promise<Thread> {
99
+ const thread = await this.deps.threadStore.getThread(threadId, tenantId)
100
+ if (!thread) {
101
+ throw new Error(`Thread ${threadId} not found`)
102
+ }
103
+ if (thread.status === 'archived') {
104
+ throw new ThreadClosedError({ threadId, op: 'require-open' })
105
+ }
106
+ return thread
107
+ }
108
+
109
+ /**
110
+ * Flip a Thread to `'archived'` via CAS on {@link Thread.ownerVersion}.
111
+ *
112
+ * Preconditions (checked in order):
113
+ * 1. Thread exists for the tenant (throws on absence).
114
+ * 2. No attached Session is in a non-terminal state (see
115
+ * {@link ARCHIVAL_BLOCKING_STATUSES}). The presence check runs
116
+ * **before** the idempotent-archive short-circuit so that an already
117
+ * archived thread harboring a live session still surfaces as
118
+ * {@link ThreadNotEmptyError} rather than a silent success.
119
+ * 3. If the thread is already `'archived'` the method short-circuits
120
+ * without an `updateThread` write (idempotent re-archival). The
121
+ * returned record reflects the current persisted state.
122
+ *
123
+ * On a fresh archive transition the underlying
124
+ * {@link ThreadStore.updateThread} call commits with `ownerVersion + 1`.
125
+ * A {@link import('../../session/errors.js').StaleThreadError} from a
126
+ * concurrent writer propagates unchanged — the caller is expected to
127
+ * re-read + retry (mirrors the `updateThread` contract).
128
+ *
129
+ * Gate scope (Phase 2.6): `ThreadManager.requireOpen` is wired into
130
+ * `AgentManager.provisionSpawn` and both handoff flows, so the production
131
+ * ingress paths cannot attach new sessions under an archived thread.
132
+ * `SessionStore.createSession` / `updateSession` remain public and
133
+ * ungated at the store layer — a direct caller can still mutate a
134
+ * session after archival (the store has no `ThreadStore` handle by
135
+ * design; cross-store awareness lives in the manager). The defensive
136
+ * re-check above catches a smuggled live session on a subsequent
137
+ * archive call, but does not prevent the direct-store write from
138
+ * landing. That's an acceptable boundary — kernel callers must go
139
+ * through the ingress paths; direct store consumers are out of scope
140
+ * for the archive invariant.
141
+ */
142
+ async archive(threadId: ThreadId, tenantId: TenantId): Promise<Thread> {
143
+ const thread = await this.deps.threadStore.getThread(threadId, tenantId)
144
+ if (!thread) {
145
+ throw new Error(`Thread ${threadId} not found`)
146
+ }
147
+
148
+ // Always enforce the blocking-session invariant — even on re-archival.
149
+ // If the thread is already archived but somehow gained a live session
150
+ // (direct store mutation, concurrent spawn before a write-barrier
151
+ // existed), surfacing that via ThreadNotEmptyError is more useful to
152
+ // operators than a silent idempotent success.
153
+ const sessions = await this.deps.sessionStore.listSessions(threadId, tenantId)
154
+ const blocking = sessions.filter((s) => ARCHIVAL_BLOCKING_STATUSES.has(s.status))
155
+ if (blocking.length > 0) {
156
+ throw new ThreadNotEmptyError({
157
+ threadId,
158
+ tenantId,
159
+ op: 'archive',
160
+ blockingSessions: summarizeBlocking(blocking),
161
+ totalBlockingSessions: blocking.length,
162
+ })
163
+ }
164
+
165
+ if (thread.status === 'archived') {
166
+ // Idempotent: already archived, no live sessions attached. Skip the
167
+ // write (updateThread would still bump ownerVersion for no semantic
168
+ // change).
169
+ return thread
170
+ }
171
+
172
+ const next: Thread = { ...thread, status: 'archived' }
173
+ await this.deps.threadStore.updateThread(next, tenantId)
174
+ // updateThread advances ownerVersion + updatedAt; re-read so the returned
175
+ // record reflects the persisted state (callers rely on version monotonicity).
176
+ const reloaded = await this.deps.threadStore.getThread(threadId, tenantId)
177
+ if (!reloaded) {
178
+ throw new Error(`Thread ${threadId} vanished between archive and read-back`)
179
+ }
180
+ return reloaded
181
+ }
182
+
183
+ /**
184
+ * Hard-delete a Thread record. Rejects with {@link ThreadNotEmptyError}
185
+ * (`op: 'delete'`) when ANY Session still references the Thread —
186
+ * deletion is stricter than archival, which tolerates quiescent sessions.
187
+ * Callers must first delete or archive-and-tombstone every attached
188
+ * session (via {@link SessionStore.deleteSession}) before invoking.
189
+ *
190
+ * The session scan runs unconditionally, so orphaned sessions pointing at
191
+ * a missing thread are still detected and reject the delete. Idempotent
192
+ * for genuinely absent threads (no sessions, no thread record) — missing
193
+ * thread + empty session list is a no-op at the store layer. Convention
194
+ * #5: deny-by-default; no implicit cascade into SessionStore.
195
+ */
196
+ async delete(threadId: ThreadId, tenantId: TenantId): Promise<void> {
197
+ const sessions = await this.deps.sessionStore.listSessions(threadId, tenantId)
198
+ if (sessions.length > 0) {
199
+ throw new ThreadNotEmptyError({
200
+ threadId,
201
+ tenantId,
202
+ op: 'delete',
203
+ blockingSessions: summarizeBlocking(sessions),
204
+ totalBlockingSessions: sessions.length,
205
+ })
206
+ }
207
+ await this.deps.threadStore.deleteThread(threadId, tenantId)
208
+ }
209
+ }
210
+
211
+ function summarizeBlocking(
212
+ sessions: readonly Session[],
213
+ ): ReadonlyArray<{ sessionId: Session['id']; status: SessionStatus }> {
214
+ return sessions
215
+ .slice(0, THREAD_NOT_EMPTY_SAMPLE_LIMIT)
216
+ .map((s) => ({ sessionId: s.id, status: s.status }))
217
+ }
@@ -57,11 +57,11 @@ export class DefaultRetriever implements Retriever {
57
57
  }
58
58
 
59
59
  private expandQuery(query: RetrievalQuery): string {
60
- if (!query.threadMessages || query.threadMessages.length === 0) {
60
+ if (!query.recentMessages || query.recentMessages.length === 0) {
61
61
  return query.text
62
62
  }
63
63
 
64
- const recentContext = query.threadMessages.slice(-3).join(' ')
64
+ const recentContext = query.recentMessages.slice(-3).join(' ')
65
65
  return `${query.text}\n\nContext: ${recentContext}`
66
66
  }
67
67
 
@@ -1,7 +1,7 @@
1
1
  import { SpanStatusCode } from '@opentelemetry/api'
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema'
3
- import { getTracer } from '../../provider/telemetry/setup.js'
4
3
  import { GENAI, NAMZU, toolSpanName } from '../../telemetry/attributes.js'
4
+ import { getTracer } from '../../telemetry/runtime-accessors.js'
5
5
  import type {
6
6
  LLMToolSchema,
7
7
  ToolAvailability,
@@ -3,7 +3,7 @@ import { DefaultPathBuilder, type PathBuilder } from '../../../session/workspace
3
3
  import type { RunId, SessionId, TenantId } from '../../../types/ids/index.js'
4
4
  import type { LLMProvider } from '../../../types/provider/index.js'
5
5
  import type { AgentRunConfig } from '../../../types/run/index.js'
6
- import type { ProjectId } from '../../../types/session/ids.js'
6
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
7
7
  import { RunContextFactory } from '../context.js'
8
8
 
9
9
  function mockProvider(): LLMProvider {
@@ -16,6 +16,7 @@ function mockProvider(): LLMProvider {
16
16
 
17
17
  function buildConfig(overrides: Partial<Parameters<typeof RunContextFactory.build>[0]> = {}) {
18
18
  const sessionId = 'ses_test' as SessionId
19
+ const threadId = 'thd_test' as ThreadId
19
20
  const projectId = 'prj_test' as ProjectId
20
21
  const tenantId = 'tnt_test' as TenantId
21
22
  const runConfig: AgentRunConfig = {
@@ -31,6 +32,7 @@ function buildConfig(overrides: Partial<Parameters<typeof RunContextFactory.buil
31
32
  provider: mockProvider(),
32
33
  messages: [],
33
34
  sessionId,
35
+ threadId,
34
36
  projectId,
35
37
  tenantId,
36
38
  workingDirectory: '/tmp/run-context-test',
@@ -38,16 +40,15 @@ function buildConfig(overrides: Partial<Parameters<typeof RunContextFactory.buil
38
40
  }
39
41
  }
40
42
 
41
- describe('RunContextFactory.build — Phase 6', () => {
42
- it('requires sessionId, projectId, tenantId and returns them on the context', () => {
43
+ describe('RunContextFactory.build', () => {
44
+ it('requires sessionId, threadId, projectId, tenantId and returns them on the context', () => {
43
45
  const cfg = buildConfig()
44
46
  const ctx = RunContextFactory.build(cfg)
45
47
 
46
48
  expect(ctx.sessionId).toBe(cfg.sessionId)
49
+ expect(ctx.threadId).toBe(cfg.threadId)
47
50
  expect(ctx.projectId).toBe(cfg.projectId)
48
51
  expect(ctx.tenantId).toBe(cfg.tenantId)
49
- // threadId remains as a deprecated mirror of projectId.
50
- expect(ctx.threadId).toBe(cfg.projectId)
51
52
  })
52
53
 
53
54
  it('uses the injected PathBuilder to resolve the output dir (no hardcoded .namzu/threads)', () => {
@@ -72,17 +73,17 @@ describe('RunContextFactory.build — Phase 6', () => {
72
73
  const cfg = buildConfig()
73
74
  const ctx = RunContextFactory.build(cfg)
74
75
 
75
- // No more `/.namzu/threads/{threadId}/runs`the new layout lives under
76
- // projects/{pid}/sessions/{sid}.
76
+ // Layout lives under projects/{pid}/sessions/{sid} — no `.namzu/threads/`.
77
77
  expect(ctx.outputDir).toContain('/.namzu/projects/prj_test/sessions/ses_test')
78
78
  expect(ctx.outputDir).not.toContain('threads')
79
79
  })
80
80
 
81
- it('seeds RunPersistence with propagated sessionId/tenantId/projectId', () => {
81
+ it('seeds RunPersistence with propagated sessionId/threadId/tenantId/projectId', () => {
82
82
  const cfg = buildConfig()
83
83
  const ctx = RunContextFactory.build(cfg)
84
84
 
85
85
  expect(ctx.runMgr.sessionId).toBe(cfg.sessionId)
86
+ expect(ctx.runMgr.threadId).toBe(cfg.threadId)
86
87
  expect(ctx.runMgr.tenantId).toBe(cfg.tenantId)
87
88
  expect(ctx.runMgr.projectId).toBe(cfg.projectId)
88
89
  })
@@ -1,14 +1,14 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import type { AgentContextLevel } from '../../types/agent/factory.js'
3
- import type { ThreadId } from '../../types/ids/index.js'
4
3
  import type { AgentPersona } from '../../types/persona/index.js'
4
+ import type { ProjectId } from '../../types/session/ids.js'
5
5
  import type { Skill } from '../../types/skills/index.js'
6
6
  import type { ToolRegistryContract } from '../../types/tool/index.js'
7
7
  import { PromptBuilder, type PromptSegments } from './prompt.js'
8
8
 
9
9
  export interface ContextCacheConfig {
10
10
  agentId: string
11
- threadId: ThreadId
11
+ projectId: ProjectId
12
12
  }
13
13
 
14
14
  export interface PromptCacheInput {
@@ -21,7 +21,7 @@ export interface PromptCacheInput {
21
21
  }
22
22
 
23
23
  export class ContextCache {
24
- readonly threadId: ThreadId
24
+ readonly projectId: ProjectId
25
25
  readonly agentId: string
26
26
 
27
27
  private cachedPrompt: string | undefined
@@ -30,7 +30,7 @@ export class ContextCache {
30
30
  private cachedStaticHash: string | undefined
31
31
 
32
32
  constructor(config: ContextCacheConfig) {
33
- this.threadId = config.threadId
33
+ this.projectId = config.projectId
34
34
  this.agentId = config.agentId
35
35
  }
36
36
 
@@ -11,29 +11,27 @@ import {
11
11
  import { DefaultPathBuilder, type PathBuilder } from '../../session/workspace/path-builder.js'
12
12
  import { ActivityStore } from '../../store/activity/memory.js'
13
13
  import { type ActivityTrackingConfig, resolveActivityTracking } from '../../types/activity/index.js'
14
- import type { RunId, SessionId, TenantId, ThreadId } from '../../types/ids/index.js'
14
+ import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
15
15
  import type { Message } from '../../types/message/index.js'
16
16
  import type { PermissionMode } from '../../types/permission/index.js'
17
17
  import type { LLMProvider } from '../../types/provider/index.js'
18
18
  import type { AgentRunConfig } from '../../types/run/index.js'
19
- import type { ProjectId } from '../../types/session/ids.js'
19
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
20
20
  import type { ModelPricing } from '../../utils/cost.js'
21
21
  import { generateRunId } from '../../utils/id.js'
22
22
  import { type Logger, getRootLogger } from '../../utils/logger.js'
23
23
 
24
24
  /**
25
- * Config accepted by {@link RunContextFactory.build}. Phase 6 promotes
26
- * `sessionId`, `projectId`, and `tenantId` to required — runs are scoped
27
- * under a Session within a Project within a Tenant (session-hierarchy.md
28
- * §12.1). `threadId` is retained only as a deprecated compat alias of
29
- * `projectId` — consumers can still pass it, but no new path layout honors it.
25
+ * Config accepted by {@link RunContextFactory.build}. `sessionId`,
26
+ * `threadId`, `projectId`, and `tenantId` are required — runs carry the full
27
+ * five-layer scope (Tenant Project Thread Session → Run) per
28
+ * Convention #17.
30
29
  *
31
30
  * `pathBuilder` is optional; when absent a {@link DefaultPathBuilder} is
32
- * constructed against `{workingDirectory}/.namzu` — no more hardcoded
33
- * `.namzu/threads` path.
31
+ * constructed against `{workingDirectory}/.namzu`.
34
32
  *
35
- * Phase 7 adds `filesystemMigrator` + `migrationSink`. These are also
36
- * optional; when absent a {@link DefaultFilesystemMigrator} wired to the
33
+ * `filesystemMigrator` + `migrationSink` are optional; when absent a
34
+ * {@link DefaultFilesystemMigrator} wired to the
37
35
  * {@link NOOP_FILESYSTEM_MIGRATION_SINK} is used. Migration runs once per
38
36
  * process via {@link RunContextFactory.ensureMigrated}; the static `build`
39
37
  * method stays synchronous so existing call sites are not broken — async
@@ -51,6 +49,7 @@ export interface RunContextConfig {
51
49
  signal?: AbortSignal
52
50
 
53
51
  sessionId: SessionId
52
+ threadId: ThreadId
54
53
  projectId: ProjectId
55
54
  tenantId: TenantId
56
55
 
@@ -73,21 +72,13 @@ export interface RunContextConfig {
73
72
  depth?: number
74
73
  }
75
74
 
76
- /**
77
- * Result of {@link RunContextFactory.build}. `threadId` remains as a
78
- * deprecated read-only mirror of `projectId` for consumers still referencing
79
- * the old name — scheduled for removal in 0.3.0 (session-hierarchy.md §13.1).
80
- */
75
+ /** Result of {@link RunContextFactory.build}. */
81
76
  export interface RunContext {
82
77
  runId: RunId
83
78
  sessionId: SessionId
79
+ threadId: ThreadId
84
80
  projectId: ProjectId
85
81
  tenantId: TenantId
86
- /**
87
- * @deprecated Mirrors `projectId` — remove when callers migrate off the
88
- * legacy name.
89
- */
90
- threadId: ThreadId
91
82
  runMgr: RunPersistence
92
83
  activityStore: ActivityStore
93
84
  planManager: PlanManager
@@ -156,6 +147,7 @@ export class RunContextFactory {
156
147
  agent: config.agentName,
157
148
  runId,
158
149
  sessionId: config.sessionId,
150
+ threadId: config.threadId,
159
151
  projectId: config.projectId,
160
152
  tenantId: config.tenantId,
161
153
  })
@@ -170,6 +162,7 @@ export class RunContextFactory {
170
162
  pricing: config.pricing,
171
163
  log,
172
164
  sessionId: config.sessionId,
165
+ threadId: config.threadId,
173
166
  tenantId: config.tenantId,
174
167
  projectId: config.projectId,
175
168
  parentRunId: config.parentRunId,
@@ -183,9 +176,9 @@ export class RunContextFactory {
183
176
  return {
184
177
  runId,
185
178
  sessionId: config.sessionId,
179
+ threadId: config.threadId,
186
180
  projectId: config.projectId,
187
181
  tenantId: config.tenantId,
188
- threadId: config.projectId as ThreadId,
189
182
  runMgr,
190
183
  activityStore,
191
184
  planManager,
@@ -7,9 +7,9 @@ import {
7
7
  import { extractFromUserMessage } from '../../compaction/extractor.js'
8
8
  import { WorkingStateManager } from '../../compaction/manager.js'
9
9
  import type { CompactionConfig } from '../../config/runtime.js'
10
- import { getTracer } from '../../provider/telemetry/setup.js'
11
10
  import type { PathBuilder } from '../../session/workspace/path-builder.js'
12
11
  import { GENAI, NAMZU, agentRunSpanName } from '../../telemetry/attributes.js'
12
+ import { getTracer } from '../../telemetry/runtime-accessors.js'
13
13
  import { buildAdvisoryTools } from '../../tools/advisory/index.js'
14
14
  import { SearchToolsTool } from '../../tools/builtins/search-tools.js'
15
15
  import { buildTaskTools } from '../../tools/task/index.js'
@@ -21,7 +21,7 @@ import {
21
21
  type ResumeHandler,
22
22
  autoApproveHandler,
23
23
  } from '../../types/hitl/index.js'
24
- import type { RunId, SessionId, TenantId, ThreadId } from '../../types/ids/index.js'
24
+ import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
25
25
  import type { InvocationState } from '../../types/invocation/index.js'
26
26
  import { type Message, createSystemMessage } from '../../types/message/index.js'
27
27
  import type { AgentPersona } from '../../types/persona/index.js'
@@ -29,7 +29,7 @@ import type { LLMProvider } from '../../types/provider/index.js'
29
29
  import type { TaskRouterConfig } from '../../types/router/index.js'
30
30
  import type { AgentRun, AgentRunConfig, RunEvent, RunEventListener } from '../../types/run/index.js'
31
31
  import type { Sandbox, SandboxProvider } from '../../types/sandbox/index.js'
32
- import type { ProjectId } from '../../types/session/ids.js'
32
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
33
33
  import type { Skill } from '../../types/skills/index.js'
34
34
  import type { TaskStore } from '../../types/task/index.js'
35
35
  import type { ToolRegistryContract } from '../../types/tool/index.js'
@@ -67,30 +67,28 @@ export interface QueryParams {
67
67
  resumeHandler: ResumeHandler
68
68
  resumeFromCheckpoint?: CheckpointId
69
69
 
70
+ /** Session scope for the run. Required — every run is attributed to a Session. */
71
+ sessionId: SessionId
72
+
70
73
  /**
71
- * Session scope for the run. Required in 0.2.0 — every run is attributed to
72
- * a Session (session-hierarchy.md §12.1).
74
+ * Topic the Session lives under. Required in 0.3.0 — every run carries
75
+ * the full five-layer scope (Tenant → Project → Thread → Session →
76
+ * Run). Denormalized from `session.threadId`; callers build this
77
+ * alongside `sessionId` so the query pipeline never needs a second
78
+ * SessionStore round-trip to recover it.
73
79
  */
74
- sessionId: SessionId
80
+ threadId: ThreadId
75
81
 
76
82
  /** Long-lived goal scope for the run. Required. */
77
83
  projectId: ProjectId
78
84
 
79
- /** Isolation boundary. Required. */
85
+ /** Isolation boundary (Convention #17). Required. */
80
86
  tenantId: TenantId
81
87
 
82
- /**
83
- * @deprecated Pass `projectId` instead. When both are present, `projectId`
84
- * wins. During the 0.2.x migration window a caller supplying only
85
- * `threadId` must also supply `projectId` — the kernel no longer infers
86
- * `projectId` from a bare `threadId` on the QueryParams shape.
87
- */
88
- threadId?: ThreadId
89
-
90
88
  /**
91
89
  * Optional path layout override. Defaults to a {@link DefaultPathBuilder}
92
- * rooted at `{workingDirectory}/.namzu` (§13.4). Phase 7 wires first-call
93
- * filesystem migration onto this same entry point.
90
+ * rooted at `{workingDirectory}/.namzu`. First-call filesystem migration
91
+ * runs on this same entry point.
94
92
  */
95
93
  pathBuilder?: PathBuilder
96
94
 
@@ -158,6 +156,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Agen
158
156
  messages: params.messages,
159
157
  signal: params.signal,
160
158
  sessionId: params.sessionId,
159
+ threadId: params.threadId,
161
160
  projectId: params.projectId,
162
161
  tenantId: params.tenantId,
163
162
  pathBuilder: params.pathBuilder,
@@ -5,9 +5,9 @@ import type { WorkingStateManager } from '../../../compaction/manager.js'
5
5
  import type { CompactionConfig } from '../../../config/runtime.js'
6
6
  import type { PlanManager } from '../../../manager/plan/lifecycle.js'
7
7
  import type { RunPersistence } from '../../../manager/run/persistence.js'
8
- import { getTracer } from '../../../provider/telemetry/setup.js'
9
8
  import type { ActivityStore } from '../../../store/activity/memory.js'
10
9
  import { GENAI, NAMZU, agentIterationSpanName } from '../../../telemetry/attributes.js'
10
+ import { getTracer } from '../../../telemetry/runtime-accessors.js'
11
11
  import type { ResumeHandler } from '../../../types/hitl/index.js'
12
12
  import { createAssistantMessage, createUserMessage } from '../../../types/message/index.js'
13
13
  import type { LLMProvider } from '../../../types/provider/index.js'
@@ -15,8 +15,10 @@
15
15
  import { vi } from 'vitest'
16
16
  import { EMPTY_TOKEN_USAGE } from '../../../constants/limits.js'
17
17
  import { AgentManager } from '../../../manager/agent/lifecycle.js'
18
+ import { ThreadManager } from '../../../manager/thread/lifecycle.js'
18
19
  import { AgentRegistry } from '../../../registry/agent/definitions.js'
19
20
  import { InMemorySessionStore } from '../../../store/session/memory.js'
21
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
20
22
  import type {
21
23
  AgentCapabilities,
22
24
  AgentInput,
@@ -28,7 +30,7 @@ import type { AgentDefinition } from '../../../types/agent/factory.js'
28
30
  import type { AgentTaskContext, SendMessageOptions } from '../../../types/agent/task.js'
29
31
  import type { AgentId, RunId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
30
32
  import { createAssistantMessage } from '../../../types/message/index.js'
31
- import type { ProjectId, SummaryId } from '../../../types/session/ids.js'
33
+ import type { ProjectId, SummaryId, ThreadId } from '../../../types/session/ids.js'
32
34
  import { ZERO_COST } from '../../../utils/cost.js'
33
35
  import { DefaultCapacityValidator } from '../../handoff/capacity.js'
34
36
  import type { ActorRef } from '../../hierarchy/actor.js'
@@ -149,6 +151,8 @@ export function buildDefinition(agent: Agent<BaseAgentConfig, BaseAgentResult>):
149
151
 
150
152
  export interface IntegrationHarness {
151
153
  readonly store: InMemorySessionStore
154
+ readonly threadStore: InMemoryThreadStore
155
+ readonly threadManager: ThreadManager
152
156
  readonly registry: AgentRegistry
153
157
  readonly manager: AgentManager
154
158
  readonly materializer: SessionSummaryMaterializer
@@ -178,6 +182,7 @@ export interface IntegrationHarnessOptions {
178
182
  export function buildHarness(options: IntegrationHarnessOptions = {}): IntegrationHarness {
179
183
  const tenantId = options.tenantId ?? DEFAULT_TENANT
180
184
  const store = new InMemorySessionStore()
185
+ const threadStore = new InMemoryThreadStore()
181
186
 
182
187
  const workspaceRegistry = new WorkspaceBackendRegistry()
183
188
  if (options.withWorktreeDriver !== false) {
@@ -200,25 +205,42 @@ export function buildHarness(options: IntegrationHarnessOptions = {}): Integrati
200
205
  })
201
206
 
202
207
  const capacity = new DefaultCapacityValidator(store)
208
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
203
209
  const registry = new AgentRegistry()
204
210
  const manager = new AgentManager(registry, undefined, {
205
211
  sessionStore: store,
206
212
  summaryMaterializer: materializer,
207
213
  workspaceRegistry,
208
214
  capacity,
215
+ threadManager,
209
216
  })
210
217
 
211
- return { store, registry, manager, materializer, workspaceRegistry, capacity, tenantId }
218
+ return {
219
+ store,
220
+ threadStore,
221
+ threadManager,
222
+ registry,
223
+ manager,
224
+ materializer,
225
+ workspaceRegistry,
226
+ capacity,
227
+ tenantId,
228
+ }
212
229
  }
213
230
 
214
231
  /**
215
- * Seeds a Tenant → Project → Session triple and flips the session into
216
- * `active` so it is a legal spawn parent. Returns the project + active
217
- * session for the caller to drive spawns against.
232
+ * Seeds a Tenant → Project → Thread → Session quadruple and flips the session
233
+ * into `active` so it is a legal spawn parent. Returns the project, thread,
234
+ * and active session for the caller to drive spawns against.
218
235
  */
219
236
  export async function seedActiveParent(
220
237
  harness: IntegrationHarness,
221
- options?: { actor?: ActorRef; projectName?: string; tenantId?: TenantId },
238
+ options?: {
239
+ actor?: ActorRef
240
+ projectName?: string
241
+ tenantId?: TenantId
242
+ threadTitle?: string
243
+ },
222
244
  ) {
223
245
  const tenantId = options?.tenantId ?? harness.tenantId
224
246
  const actor: ActorRef = options?.actor ?? userActor('usr_root', tenantId)
@@ -226,12 +248,16 @@ export async function seedActiveParent(
226
248
  { tenantId, name: options?.projectName ?? 'integration-project' },
227
249
  tenantId,
228
250
  )
251
+ const thread = await harness.threadStore.createThread(
252
+ { projectId: project.id, title: options?.threadTitle ?? 'default' },
253
+ tenantId,
254
+ )
229
255
  const session = await harness.store.createSession(
230
- { projectId: project.id, currentActor: actor },
256
+ { threadId: thread.id, projectId: project.id, currentActor: actor },
231
257
  tenantId,
232
258
  )
233
259
  await harness.store.updateSession({ ...session, status: 'active' as Session['status'] }, tenantId)
234
- return { project, session, actor }
260
+ return { project, thread, session, actor }
235
261
  }
236
262
 
237
263
  /**
@@ -242,6 +268,7 @@ export async function seedActiveParent(
242
268
  export function buildTaskContext(params: {
243
269
  sessionId: SessionId
244
270
  projectId: ProjectId
271
+ threadId: ThreadId
245
272
  tenantId: TenantId
246
273
  parentActor: ActorRef
247
274
  depth?: number
@@ -258,6 +285,7 @@ export function buildTaskContext(params: {
258
285
  remaining: params.budget ?? 100_000,
259
286
  },
260
287
  tenantId: params.tenantId,
288
+ threadId: params.threadId,
261
289
  sessionId: params.sessionId,
262
290
  projectId: params.projectId,
263
291
  parentActor: params.parentActor,