@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
@@ -20,6 +20,7 @@
20
20
  * 9. Emit `onCommitted` with the new version.
21
21
  */
22
22
 
23
+ import type { ThreadManager } from '../../manager/thread/lifecycle.js'
23
24
  import type { SessionId, TenantId } from '../../types/ids/index.js'
24
25
  import type { SessionStore } from '../../types/session/store.js'
25
26
  import { TenantIsolationError } from '../errors.js'
@@ -64,6 +65,12 @@ export interface SingleHandoffDeps {
64
65
  capacity: CapacityValidator
65
66
  events: HandoffEventSink
66
67
  runStatus?: RunStatusResolver
68
+ /**
69
+ * Gate the recipient-session creation on the Thread being `'open'`.
70
+ * Added in Phase 2.6 to mirror spawn — a handoff into an archived
71
+ * Thread would otherwise undermine `ThreadManager.archive`.
72
+ */
73
+ threadManager: ThreadManager
67
74
  }
68
75
 
69
76
  /**
@@ -85,6 +92,12 @@ export async function executeSingleHandoff(
85
92
  })
86
93
  }
87
94
 
95
+ // Thread archive gate (Phase 2.6) — runs FIRST so an archived thread
96
+ // fails fastest with `ThreadClosedError` rather than a lock rejection or
97
+ // capacity error. Checked BEFORE the CAS lock so a denied handoff leaves
98
+ // the source session untouched.
99
+ await deps.threadManager.requireOpen(assignment.threadId, tenantId)
100
+
88
101
  // 1. Load source session + tenant check.
89
102
  const source = await deps.store.getSession(assignment.sourceSessionId, tenantId)
90
103
  if (!source) {
@@ -96,6 +109,11 @@ export async function executeSingleHandoff(
96
109
  resource: `session(${source.id})`,
97
110
  })
98
111
  }
112
+ if (source.threadId !== assignment.threadId) {
113
+ throw new Error(
114
+ `Assignment threadId ${assignment.threadId} does not match source session threadId ${source.threadId}`,
115
+ )
116
+ }
99
117
  if (source.projectId !== assignment.projectId) {
100
118
  throw new Error(
101
119
  `Assignment projectId ${assignment.projectId} does not match source session projectId ${source.projectId}`,
@@ -157,7 +175,11 @@ export async function executeSingleHandoff(
157
175
  provisionedWorkspace = await driver.create({ label: `handoff-${assignment.id}` })
158
176
 
159
177
  const recipientSession = await deps.store.createSession(
160
- { projectId: source.projectId, currentActor: assignment.recipientActor },
178
+ {
179
+ threadId: source.threadId,
180
+ projectId: source.projectId,
181
+ currentActor: assignment.recipientActor,
182
+ },
161
183
  tenantId,
162
184
  )
163
185
  createdSessionId = recipientSession.id
@@ -1,11 +1,18 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { RunStatus } from '../../../types/run/status.js'
3
- import type { ProjectId, SessionId, TenantId, UserId } from '../../../types/session/ids.js'
3
+ import type {
4
+ ProjectId,
5
+ SessionId,
6
+ TenantId,
7
+ ThreadId,
8
+ UserId,
9
+ } from '../../../types/session/ids.js'
4
10
  import type { ActorRef } from '../actor.js'
5
11
  import { type Session, type SessionStatus, deriveStatus } from '../session.js'
6
12
 
7
13
  const tenant = 'tnt_a' as TenantId
8
14
  const project = 'prj_a' as ProjectId
15
+ const thread = 'thd_a' as ThreadId
9
16
 
10
17
  function user(): ActorRef {
11
18
  return { kind: 'user', userId: 'usr_a' as UserId, tenantId: tenant }
@@ -14,6 +21,7 @@ function user(): ActorRef {
14
21
  function makeSession(status: SessionStatus): Session {
15
22
  return {
16
23
  id: 'ses_a' as SessionId,
24
+ threadId: thread,
17
25
  projectId: project,
18
26
  tenantId: tenant,
19
27
  status,
@@ -5,6 +5,7 @@ export type { ActorRef, SystemRoleId } from './actor.js'
5
5
  export type { Lineage } from './lineage.js'
6
6
  export type { Tenant } from './tenant.js'
7
7
  export type { Project, ProjectConfig } from './project.js'
8
+ export type { Thread, ThreadStatus } from './thread.js'
8
9
  export type { Session, SessionStatus } from './session.js'
9
10
  export { deriveStatus } from './session.js'
10
11
  export type {
@@ -1,6 +1,6 @@
1
1
  import type { SessionId, TenantId } from '../../types/ids/index.js'
2
2
  import type { RunStatus } from '../../types/run/status.js'
3
- import type { ProjectId, WorkspaceId } from '../../types/session/ids.js'
3
+ import type { ProjectId, ThreadId, WorkspaceId } from '../../types/session/ids.js'
4
4
  import type { ActorRef } from './actor.js'
5
5
 
6
6
  /**
@@ -21,15 +21,27 @@ export type SessionStatus =
21
21
  /**
22
22
  * Multi-turn work unit owned by exactly one {@link ActorRef} at a time.
23
23
  *
24
- * Fields derived from session-hierarchy.md §4.3:
24
+ * Scope identifiers:
25
+ * - `threadId` — the topic-level {@link Thread} this Session lives under.
26
+ * Set at creation, immutable; Sessions never move threads.
27
+ * - `projectId` — the {@link Project} the owning Thread belongs to.
28
+ * **Denormalized** from `thread.projectId` at creation time; immutable.
29
+ * Kept on the Session record for ergonomic access (Project-scoped
30
+ * consumers — handoff validators, archival, retention — would otherwise
31
+ * need a second round-trip to ThreadStore on every read). This is NOT
32
+ * a deprecated mirror of a fading field; it is a deliberate
33
+ * denormalization of structurally-immutable derived data.
34
+ *
35
+ * Other invariants (session-hierarchy.md §4.3):
25
36
  * - `previousActors` is append-only and publicly read-only; previous
26
- * owners cannot write to the session again (Decision #3).
37
+ * owners cannot write to the session again.
27
38
  * - `ownerVersion` is the CAS counter for handoff (§6.1 / §6.2 / §6.4).
28
39
  * - `workspaceId` is nullable for sessions whose workspace has not yet
29
40
  * been provisioned (or has been torn down during archival).
30
41
  */
31
42
  export interface Session {
32
43
  id: SessionId
44
+ threadId: ThreadId
33
45
  projectId: ProjectId
34
46
  tenantId: TenantId
35
47
  status: SessionStatus
@@ -0,0 +1,55 @@
1
+ import type { TenantId } from '../../types/ids/index.js'
2
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
3
+
4
+ /**
5
+ * Lifecycle state of a Thread.
6
+ *
7
+ * - `open` — accepts new Sessions and new Runs under existing Sessions.
8
+ * - `archived` — read-only tombstone. No new Sessions may be created; existing
9
+ * Sessions remain navigable. Transitioning `open → archived` requires that
10
+ * no Session under the Thread is in a non-terminal state (guarded at the
11
+ * store level by listing + status fan-in).
12
+ *
13
+ * There is no `active` variant — Thread does NOT derive status from its child
14
+ * Sessions the way a Session does from its Runs. Thread is a pure container
15
+ * (Phase 0 decision B.1: Thread is container-only, no message stream, no
16
+ * fan-in). Its status is an explicit owner action.
17
+ */
18
+ export type ThreadStatus = 'open' | 'archived'
19
+
20
+ /**
21
+ * Topic-level container sitting between {@link ProjectId Project} and
22
+ * {@link import('../../types/session/ids.js').SessionId Session} in the
23
+ * five-layer hierarchy (Project → Thread → Session → SubSession → Run).
24
+ *
25
+ * A Thread groups together many Sessions that address the same coherent
26
+ * topic or line-of-work within a Project (e.g. "auth refactor", "billing
27
+ * incident"). Sessions under the same Thread share Project-level shared
28
+ * resources (memory, vaults, knowledge bases) but have independent actor
29
+ * state, handoff history, and Run streams.
30
+ *
31
+ * Design §4 (`docs.local/sessions/ses_001-hierarchy-redesign/design.md`):
32
+ * - Container only. No own message stream, no own Run stream. Messages
33
+ * live in Sessions (Phase 0 decision B.1).
34
+ * - `title` is a user-facing label. **Titles are NOT unique within a
35
+ * Project.** Callers disambiguate by {@link ThreadId}; the title is
36
+ * freeform display text. If a product surface needs uniqueness (e.g.
37
+ * a human-typed slug), that constraint lives at the API layer, not in
38
+ * the kernel.
39
+ * - `ownerVersion` is the CAS counter for mutations — `updateThread` and
40
+ * archival transitions require a matching version and reject
41
+ * {@link StaleThreadError} on mismatch. Mirrors the
42
+ * {@link import('./session.js').Session} handoff CAS pattern (§6.1).
43
+ * - No fan-in `deriveStatus()` helper — status is owner-managed, not
44
+ * Run-derived. This is the Thread-vs-Session contract boundary.
45
+ */
46
+ export interface Thread {
47
+ id: ThreadId
48
+ projectId: ProjectId
49
+ tenantId: TenantId
50
+ title: string
51
+ status: ThreadStatus
52
+ ownerVersion: number
53
+ createdAt: Date
54
+ updatedAt: Date
55
+ }
@@ -1,23 +1,18 @@
1
1
  /**
2
2
  * ID-prefix migration window — read-side compat for legacy `thd_*` IDs.
3
3
  *
4
- * Phase 1 already ships {@link parseThreadId} in `utils/id.ts` that accepts
5
- * either `thd_*` or `prj_*` silently. This module formalises the warning
6
- * emission path called out in session-hierarchy.md §13.3.1:
7
- *
8
- * | Version | Reader accepts | Writer emits | Legacy read behaviour |
9
- * |---------|---------------------|--------------|-------------------------------|
10
- * | 0.2.x | `thd_*` AND `prj_*` | `prj_*` only | emits `MigrationWarning` once |
11
- * | 0.3.x | `prj_*` only | `prj_*` only | rejects `StalePrefixError` |
12
- *
13
4
  * Consumers that touch raw legacy IDs (filesystem migrator, wire decoders,
14
5
  * CLI imports) route through {@link acceptLegacyThreadId} so the warning
15
6
  * signal is structured rather than ad-hoc console output (Convention #18).
16
7
  *
17
- * The `WINDOW_OPEN` constant is the single switch that flips this module
18
- * from soft-accept to hard-reject when 0.3.0 cuts. Convention #0: no silent
19
- * long-lived compat the window is explicit, dated, and fails closed when
20
- * the clock runs out.
8
+ * | Window state | Reader accepts | Writer emits | Legacy read behaviour |
9
+ * |--------------|---------------------|--------------|-------------------------------|
10
+ * | OPEN (now) | `thd_*` AND `prj_*` | `prj_*` only | emits `MigrationWarning` once |
11
+ * | CLOSED | `prj_*` only | `prj_*` only | rejects `StalePrefixError` |
12
+ *
13
+ * The {@link WINDOW_OPEN} constant is the single switch that flips this
14
+ * module from soft-accept to hard-reject. Convention #0: no silent
15
+ * long-lived compat — the window is explicit and fails closed when closed.
21
16
  */
22
17
 
23
18
  import type { ProjectId } from '../../types/session/ids.js'
@@ -10,7 +10,7 @@ import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js
10
10
  import { InMemorySessionStore } from '../../../store/session/memory.js'
11
11
  import type { AgentId, TenantId, UserId } from '../../../types/ids/index.js'
12
12
  import { createUserMessage } from '../../../types/message/index.js'
13
- import type { WorkspaceId } from '../../../types/session/ids.js'
13
+ import type { ThreadId, WorkspaceId } from '../../../types/session/ids.js'
14
14
  import {
15
15
  ArchivalManager,
16
16
  ArchiveNotConfiguredError,
@@ -18,6 +18,8 @@ import {
18
18
  } from '../archive.js'
19
19
  import { DiskArchiveBackend } from '../disk-backend.js'
20
20
 
21
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
22
+
21
23
  const tenantA = 'tnt_alpha' as TenantId
22
24
 
23
25
  function stubLogger() {
@@ -43,11 +45,11 @@ function agentActor(tenantId: TenantId): ActorRef {
43
45
  async function seedIdleSubSession(store: InMemorySessionStore) {
44
46
  const project = await store.createProject({ tenantId: tenantA, name: 'p' }, tenantA)
45
47
  const parent = await store.createSession(
46
- { projectId: project.id, currentActor: userActor(tenantA) },
48
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantA) },
47
49
  tenantA,
48
50
  )
49
51
  const child = await store.createSession(
50
- { projectId: project.id, currentActor: agentActor(tenantA) },
52
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
51
53
  tenantA,
52
54
  )
53
55
  const sub = await store.createSubSession(
@@ -3,7 +3,7 @@ import { TenantIsolationError } from '../../../session/errors.js'
3
3
  import type { ActorRef } from '../../../session/hierarchy/actor.js'
4
4
  import { InMemorySessionStore } from '../../../store/session/memory.js'
5
5
  import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
6
- import type { SummaryId } from '../../../types/session/ids.js'
6
+ import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
7
7
  import type { DeliverableRef } from '../deliverable.js'
8
8
  import { SessionSummaryMaterializer } from '../materialize.js'
9
9
  import {
@@ -12,6 +12,8 @@ import {
12
12
  SessionAlreadySummarizedError,
13
13
  } from '../ref.js'
14
14
 
15
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
16
+
15
17
  const tenantA = 'tnt_alpha' as TenantId
16
18
  const tenantB = 'tnt_beta' as TenantId
17
19
 
@@ -31,7 +33,7 @@ function makeSummaryIdGenerator(): () => SummaryId {
31
33
  async function seedActiveSession(store: InMemorySessionStore, tenantId: TenantId) {
32
34
  const project = await store.createProject({ tenantId, name: 'p1' }, tenantId)
33
35
  const session = await store.createSession(
34
- { projectId: project.id, currentActor: agentActor(tenantId) },
36
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantId) },
35
37
  tenantId,
36
38
  )
37
39
  // Put the session into `active` so the materializer's status-flip behavior
@@ -232,7 +234,7 @@ describe('SessionSummaryMaterializer.materialize', () => {
232
234
  const store = new InMemorySessionStore()
233
235
  const project = await store.createProject({ tenantId: tenantA, name: 'p1' }, tenantA)
234
236
  const session = await store.createSession(
235
- { projectId: project.id, currentActor: userActor(tenantA) },
237
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantA) },
236
238
  tenantA,
237
239
  )
238
240
  // session.status defaults to 'idle'
@@ -255,7 +257,7 @@ describe('SessionSummaryMaterializer.materialize', () => {
255
257
  const store = new InMemorySessionStore()
256
258
  const project = await store.createProject({ tenantId: tenantA, name: 'p1' }, tenantA)
257
259
  const session = await store.createSession(
258
- { projectId: project.id, currentActor: agentActor(tenantA) },
260
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
259
261
  tenantA,
260
262
  )
261
263
  await store.updateSession({ ...session, status: 'failed' }, tenantA)
@@ -10,9 +10,6 @@ export { InMemoryTaskStore } from './task/memory.js'
10
10
  export { DiskTaskStore } from './task/disk.js'
11
11
  export type { DiskTaskStoreConfig } from './task/disk.js'
12
12
 
13
- export { InMemoryConversationStore } from './conversation/memory.js'
14
- export type { InMemoryConversationStoreConfig } from './conversation/memory.js'
15
-
16
13
  export { InMemoryMemoryIndex } from './memory/index.js'
17
14
  export { InMemoryMemoryStore } from './memory/memory.js'
18
15
  export { DiskMemoryStore } from './memory/disk.js'
@@ -23,7 +23,7 @@ function agentActor(tenantId: TenantId): ActorRef {
23
23
  async function seed(store: DiskSessionStore, tenantId: TenantId) {
24
24
  const project = await store.createProject({ tenantId, name: 'p1' }, tenantId)
25
25
  const session = await store.createSession(
26
- { projectId: project.id, currentActor: userActor(tenantId) },
26
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantId) },
27
27
  tenantId,
28
28
  )
29
29
  return { project, session }
@@ -45,7 +45,7 @@ describe('DiskSessionStore', () => {
45
45
  it('writes the canonical directory layout (projects/.../sessions/.../subsessions)', async () => {
46
46
  const { project, session } = await seed(store, tenantA)
47
47
  const child = await store.createSession(
48
- { projectId: project.id, currentActor: agentActor(tenantA) },
48
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
49
49
  tenantA,
50
50
  )
51
51
  const sub = await store.createSubSession(
@@ -143,7 +143,7 @@ describe('DiskSessionStore', () => {
143
143
  it('drill returns children and ancestry after a cold reload', async () => {
144
144
  const { project, session: root } = await seed(store, tenantA)
145
145
  const child = await store.createSession(
146
- { projectId: project.id, currentActor: agentActor(tenantA) },
146
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
147
147
  tenantA,
148
148
  )
149
149
  await store.createSubSession(
@@ -278,7 +278,7 @@ describe('DiskSessionStore', () => {
278
278
  it('deleteSession rejects if sub-sessions are still attached', async () => {
279
279
  const { project, session: root } = await seed(store, tenantA)
280
280
  const child = await store.createSession(
281
- { projectId: project.id, currentActor: agentActor(tenantA) },
281
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
282
282
  tenantA,
283
283
  )
284
284
  await store.createSubSession(
@@ -297,7 +297,7 @@ describe('DiskSessionStore', () => {
297
297
  it('deleteSubSession removes the sub-session directory and is idempotent', async () => {
298
298
  const { project, session: root } = await seed(store, tenantA)
299
299
  const child = await store.createSession(
300
- { projectId: project.id, currentActor: agentActor(tenantA) },
300
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
301
301
  tenantA,
302
302
  )
303
303
  const sub = await store.createSubSession(
@@ -338,9 +338,60 @@ describe('DiskSessionStore', () => {
338
338
 
339
339
  await expect(store.getSummary(session.id, tenantB)).rejects.toBeInstanceOf(TenantIsolationError)
340
340
  })
341
+
342
+ describe('listSessions(threadId, tenantId)', () => {
343
+ const threadX = 'thd_x' as ThreadId
344
+ const threadY = 'thd_y' as ThreadId
345
+
346
+ it('returns [] when the projects root is empty', async () => {
347
+ // Fresh temp root — no projects directory yet.
348
+ expect(await store.listSessions(threadX, tenantA)).toEqual([])
349
+ })
350
+
351
+ it('filters by threadId and tenant; orders by createdAt ascending', async () => {
352
+ const project = await store.createProject({ tenantId: tenantA, name: 'p' }, tenantA)
353
+
354
+ const first = await store.createSession(
355
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
356
+ tenantA,
357
+ )
358
+ await new Promise((r) => setTimeout(r, 2))
359
+ const second = await store.createSession(
360
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
361
+ tenantA,
362
+ )
363
+ // Same project, different thread — must not appear.
364
+ await store.createSession(
365
+ { threadId: threadY, projectId: project.id, currentActor: userActor(tenantA) },
366
+ tenantA,
367
+ )
368
+
369
+ const listed = await store.listSessions(threadX, tenantA)
370
+ expect(listed.map((s) => s.id)).toEqual([first.id, second.id])
371
+ })
372
+
373
+ it('skips cross-tenant sessions even when threadId matches', async () => {
374
+ const pA = await store.createProject({ tenantId: tenantA, name: 'pa' }, tenantA)
375
+ const pB = await store.createProject({ tenantId: tenantB, name: 'pb' }, tenantB)
376
+
377
+ const own = await store.createSession(
378
+ { threadId: threadX, projectId: pA.id, currentActor: userActor(tenantA) },
379
+ tenantA,
380
+ )
381
+ await store.createSession(
382
+ { threadId: threadX, projectId: pB.id, currentActor: userActor(tenantB) },
383
+ tenantB,
384
+ )
385
+
386
+ const listed = await store.listSessions(threadX, tenantA)
387
+ expect(listed.map((s) => s.id)).toEqual([own.id])
388
+ })
389
+ })
341
390
  })
342
391
 
343
392
  import type { SessionSummaryRef } from '../../../session/summary/ref.js'
344
393
  // Import after use so tests are self-contained w.r.t. types we already use.
345
394
  import type { SessionId } from '../../../types/ids/index.js'
346
- import type { SummaryId } from '../../../types/session/ids.js'
395
+ import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
396
+
397
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
@@ -4,8 +4,11 @@ import type { ActorRef } from '../../../session/hierarchy/actor.js'
4
4
  import type { SubSession } from '../../../session/hierarchy/sub-session.js'
5
5
  import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
6
6
  import { createUserMessage } from '../../../types/message/index.js'
7
+ import type { ThreadId } from '../../../types/session/ids.js'
7
8
  import { InMemorySessionStore } from '../memory.js'
8
9
 
10
+ const TEST_THREAD_ID = 'thd_test' as ThreadId
11
+
9
12
  function userActor(tenantId: TenantId): ActorRef {
10
13
  return { kind: 'user', userId: 'usr_a' as UserId, tenantId }
11
14
  }
@@ -25,7 +28,7 @@ const tenantB = 'tnt_beta' as TenantId
25
28
  async function seed(store: InMemorySessionStore, tenantId: TenantId) {
26
29
  const project = await store.createProject({ tenantId, name: 'p1' }, tenantId)
27
30
  const session = await store.createSession(
28
- { projectId: project.id, currentActor: userActor(tenantId) },
31
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantId) },
29
32
  tenantId,
30
33
  )
31
34
  return { project, session }
@@ -84,7 +87,7 @@ describe('InMemorySessionStore', () => {
84
87
 
85
88
  // Create a child session + link via sub-session.
86
89
  const child = await store.createSession(
87
- { projectId: project.id, currentActor: agentActor(tenantA) },
90
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
88
91
  tenantA,
89
92
  )
90
93
  const sub = await store.createSubSession(
@@ -139,7 +142,7 @@ describe('InMemorySessionStore', () => {
139
142
  const { project: pB, session: rootB } = await seed(store, tenantB)
140
143
 
141
144
  const childA = await store.createSession(
142
- { projectId: pA.id, currentActor: agentActor(tenantA) },
145
+ { threadId: TEST_THREAD_ID, projectId: pA.id, currentActor: agentActor(tenantA) },
143
146
  tenantA,
144
147
  )
145
148
  await store.createSubSession(
@@ -153,7 +156,7 @@ describe('InMemorySessionStore', () => {
153
156
  )
154
157
 
155
158
  const childB = await store.createSession(
156
- { projectId: pB.id, currentActor: agentActor(tenantB) },
159
+ { threadId: TEST_THREAD_ID, projectId: pB.id, currentActor: agentActor(tenantB) },
157
160
  tenantB,
158
161
  )
159
162
  await store.createSubSession(
@@ -186,7 +189,7 @@ describe('InMemorySessionStore', () => {
186
189
  const store = new InMemorySessionStore()
187
190
  const { project, session: rootA } = await seed(store, tenantA)
188
191
  const sessionB = await store.createSession(
189
- { projectId: project.id, currentActor: agentActor(tenantA) },
192
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
190
193
  tenantA,
191
194
  )
192
195
 
@@ -235,7 +238,7 @@ describe('InMemorySessionStore', () => {
235
238
  const store = new InMemorySessionStore()
236
239
  const { project, session: root } = await seed(store, tenantA)
237
240
  const child = await store.createSession(
238
- { projectId: project.id, currentActor: agentActor(tenantA) },
241
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
239
242
  tenantA,
240
243
  )
241
244
  await store.createSubSession(
@@ -265,7 +268,7 @@ describe('InMemorySessionStore', () => {
265
268
  const store = new InMemorySessionStore()
266
269
  const { project, session: root } = await seed(store, tenantA)
267
270
  const child = await store.createSession(
268
- { projectId: project.id, currentActor: agentActor(tenantA) },
271
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
269
272
  tenantA,
270
273
  )
271
274
  const sub = await store.createSubSession(
@@ -293,11 +296,11 @@ describe('InMemorySessionStore', () => {
293
296
  const { project, session: root } = await seed(store, tenantA)
294
297
 
295
298
  const c1 = await store.createSession(
296
- { projectId: project.id, currentActor: agentActor(tenantA) },
299
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
297
300
  tenantA,
298
301
  )
299
302
  const c2 = await store.createSession(
300
- { projectId: project.id, currentActor: agentActor(tenantA) },
303
+ { threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
301
304
  tenantA,
302
305
  )
303
306
 
@@ -324,4 +327,76 @@ describe('InMemorySessionStore', () => {
324
327
  const ids = children.map((s: SubSession) => s.id)
325
328
  expect(new Set(ids)).toEqual(new Set([s1.id, s2.id]))
326
329
  })
330
+
331
+ describe('listSessions(threadId, tenantId)', () => {
332
+ const threadX = 'thd_x' as ThreadId
333
+ const threadY = 'thd_y' as ThreadId
334
+
335
+ it('returns [] when the thread has no sessions', async () => {
336
+ const store = new InMemorySessionStore()
337
+ expect(await store.listSessions(threadX, tenantA)).toEqual([])
338
+ })
339
+
340
+ it('returns only sessions whose threadId matches, for the caller tenant', async () => {
341
+ const store = new InMemorySessionStore()
342
+ const project = await store.createProject({ tenantId: tenantA, name: 'p' }, tenantA)
343
+
344
+ const sX1 = await store.createSession(
345
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
346
+ tenantA,
347
+ )
348
+ const sX2 = await store.createSession(
349
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
350
+ tenantA,
351
+ )
352
+ // Same project, different thread — must not appear.
353
+ await store.createSession(
354
+ { threadId: threadY, projectId: project.id, currentActor: userActor(tenantA) },
355
+ tenantA,
356
+ )
357
+
358
+ const listed = await store.listSessions(threadX, tenantA)
359
+ expect(listed.map((s) => s.id).sort()).toEqual([sX1.id, sX2.id].sort())
360
+ })
361
+
362
+ it('orders results by createdAt ascending', async () => {
363
+ const store = new InMemorySessionStore()
364
+ const project = await store.createProject({ tenantId: tenantA, name: 'p' }, tenantA)
365
+
366
+ const first = await store.createSession(
367
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
368
+ tenantA,
369
+ )
370
+ // Nudge clock for deterministic ordering; in-memory uses `new Date()`.
371
+ await new Promise((r) => setTimeout(r, 2))
372
+ const second = await store.createSession(
373
+ { threadId: threadX, projectId: project.id, currentActor: userActor(tenantA) },
374
+ tenantA,
375
+ )
376
+
377
+ const listed = await store.listSessions(threadX, tenantA)
378
+ expect(listed.map((s) => s.id)).toEqual([first.id, second.id])
379
+ })
380
+
381
+ it('silently skips cross-tenant sessions sharing the same threadId', async () => {
382
+ // Thread ids are tenant-scoped in practice but nothing at the type
383
+ // level prevents the same string identifier being reused across
384
+ // tenants — the listing must filter by tenant without erroring.
385
+ const store = new InMemorySessionStore()
386
+ const pA = await store.createProject({ tenantId: tenantA, name: 'pa' }, tenantA)
387
+ const pB = await store.createProject({ tenantId: tenantB, name: 'pb' }, tenantB)
388
+
389
+ const own = await store.createSession(
390
+ { threadId: threadX, projectId: pA.id, currentActor: userActor(tenantA) },
391
+ tenantA,
392
+ )
393
+ await store.createSession(
394
+ { threadId: threadX, projectId: pB.id, currentActor: userActor(tenantB) },
395
+ tenantB,
396
+ )
397
+
398
+ const listed = await store.listSessions(threadX, tenantA)
399
+ expect(listed.map((s) => s.id)).toEqual([own.id])
400
+ })
401
+ })
327
402
  })