@namzu/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +3 -2
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +5 -2
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/bridge/a2a/index.d.ts +1 -1
  9. package/dist/bridge/a2a/index.d.ts.map +1 -1
  10. package/dist/bridge/a2a/index.js +1 -1
  11. package/dist/bridge/a2a/index.js.map +1 -1
  12. package/dist/bridge/a2a/message.d.ts +0 -2
  13. package/dist/bridge/a2a/message.d.ts.map +1 -1
  14. package/dist/bridge/a2a/message.js +0 -26
  15. package/dist/bridge/a2a/message.js.map +1 -1
  16. package/dist/bridge/a2a/task.d.ts +4 -3
  17. package/dist/bridge/a2a/task.d.ts.map +1 -1
  18. package/dist/bridge/a2a/task.js +4 -4
  19. package/dist/bridge/a2a/task.js.map +1 -1
  20. package/dist/contracts/api.d.ts +6 -38
  21. package/dist/contracts/api.d.ts.map +1 -1
  22. package/dist/contracts/ids.d.ts +1 -1
  23. package/dist/contracts/ids.d.ts.map +1 -1
  24. package/dist/contracts/index.d.ts +3 -5
  25. package/dist/contracts/index.d.ts.map +1 -1
  26. package/dist/contracts/index.js +1 -1
  27. package/dist/contracts/index.js.map +1 -1
  28. package/dist/contracts/schemas.d.ts +1 -31
  29. package/dist/contracts/schemas.d.ts.map +1 -1
  30. package/dist/contracts/schemas.js +1 -7
  31. package/dist/contracts/schemas.js.map +1 -1
  32. package/dist/index.d.ts +2 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/manager/agent/__tests__/lifecycle.test.js +27 -13
  37. package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
  38. package/dist/manager/agent/lifecycle.d.ts +9 -0
  39. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  40. package/dist/manager/agent/lifecycle.js +93 -31
  41. package/dist/manager/agent/lifecycle.js.map +1 -1
  42. package/dist/manager/index.d.ts +2 -0
  43. package/dist/manager/index.d.ts.map +1 -1
  44. package/dist/manager/index.js +1 -0
  45. package/dist/manager/index.js.map +1 -1
  46. package/dist/manager/run/persistence.d.ts +3 -1
  47. package/dist/manager/run/persistence.d.ts.map +1 -1
  48. package/dist/manager/run/persistence.js +5 -0
  49. package/dist/manager/run/persistence.js.map +1 -1
  50. package/dist/manager/thread/__tests__/lifecycle.test.d.ts +2 -0
  51. package/dist/manager/thread/__tests__/lifecycle.test.d.ts.map +1 -0
  52. package/dist/manager/thread/__tests__/lifecycle.test.js +216 -0
  53. package/dist/manager/thread/__tests__/lifecycle.test.js.map +1 -0
  54. package/dist/manager/thread/lifecycle.d.ts +105 -0
  55. package/dist/manager/thread/lifecycle.d.ts.map +1 -0
  56. package/dist/manager/thread/lifecycle.js +186 -0
  57. package/dist/manager/thread/lifecycle.js.map +1 -0
  58. package/dist/rag/retriever.js +2 -2
  59. package/dist/runtime/query/__tests__/context.test.js +8 -7
  60. package/dist/runtime/query/__tests__/context.test.js.map +1 -1
  61. package/dist/runtime/query/context-cache.d.ts +3 -3
  62. package/dist/runtime/query/context-cache.d.ts.map +1 -1
  63. package/dist/runtime/query/context-cache.js +2 -2
  64. package/dist/runtime/query/context-cache.js.map +1 -1
  65. package/dist/runtime/query/context.d.ts +12 -21
  66. package/dist/runtime/query/context.d.ts.map +1 -1
  67. package/dist/runtime/query/context.js +3 -1
  68. package/dist/runtime/query/context.js.map +1 -1
  69. package/dist/runtime/query/index.d.ts +13 -15
  70. package/dist/runtime/query/index.d.ts.map +1 -1
  71. package/dist/runtime/query/index.js +1 -0
  72. package/dist/runtime/query/index.js.map +1 -1
  73. package/dist/session/__tests__/integration/_fixtures.d.ts +11 -4
  74. package/dist/session/__tests__/integration/_fixtures.d.ts.map +1 -1
  75. package/dist/session/__tests__/integration/_fixtures.js +23 -6
  76. package/dist/session/__tests__/integration/_fixtures.js.map +1 -1
  77. package/dist/session/__tests__/integration/archive-gate.test.d.ts +15 -0
  78. package/dist/session/__tests__/integration/archive-gate.test.d.ts.map +1 -0
  79. package/dist/session/__tests__/integration/archive-gate.test.js +214 -0
  80. package/dist/session/__tests__/integration/archive-gate.test.js.map +1 -0
  81. package/dist/session/__tests__/integration/capacity-caps.test.js +13 -6
  82. package/dist/session/__tests__/integration/capacity-caps.test.js.map +1 -1
  83. package/dist/session/__tests__/integration/e2e-spawn.test.js +14 -2
  84. package/dist/session/__tests__/integration/e2e-spawn.test.js.map +1 -1
  85. package/dist/session/__tests__/integration/event-stream-ordering.test.js +14 -7
  86. package/dist/session/__tests__/integration/event-stream-ordering.test.js.map +1 -1
  87. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js +26 -14
  88. package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js.map +1 -1
  89. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js +30 -20
  90. package/dist/session/__tests__/integration/handoff-illegal-transition.test.js.map +1 -1
  91. package/dist/session/__tests__/integration/handoff-single-e2e.test.js +25 -9
  92. package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
  93. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js +11 -10
  94. package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js.map +1 -1
  95. package/dist/session/__tests__/integration/prev-artifact-dag.test.js +5 -4
  96. package/dist/session/__tests__/integration/prev-artifact-dag.test.js.map +1 -1
  97. package/dist/session/__tests__/integration/retention-archive.test.js +3 -2
  98. package/dist/session/__tests__/integration/retention-archive.test.js.map +1 -1
  99. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts +26 -0
  100. package/dist/session/__tests__/integration/spawn-rollback.test.d.ts.map +1 -0
  101. package/dist/session/__tests__/integration/spawn-rollback.test.js +236 -0
  102. package/dist/session/__tests__/integration/spawn-rollback.test.js.map +1 -0
  103. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js +2 -1
  104. package/dist/session/__tests__/integration/summary-materialization-e2e.test.js.map +1 -1
  105. package/dist/session/__tests__/integration/tenant-isolation.test.js +14 -5
  106. package/dist/session/__tests__/integration/tenant-isolation.test.js.map +1 -1
  107. package/dist/session/errors.d.ts +79 -0
  108. package/dist/session/errors.d.ts.map +1 -1
  109. package/dist/session/errors.js +57 -0
  110. package/dist/session/errors.js.map +1 -1
  111. package/dist/session/handoff/__tests__/broadcast.test.js +49 -31
  112. package/dist/session/handoff/__tests__/broadcast.test.js.map +1 -1
  113. package/dist/session/handoff/__tests__/capacity.test.js +21 -18
  114. package/dist/session/handoff/__tests__/capacity.test.js.map +1 -1
  115. package/dist/session/handoff/__tests__/single.test.js +39 -30
  116. package/dist/session/handoff/__tests__/single.test.js.map +1 -1
  117. package/dist/session/handoff/assignment.d.ts +13 -1
  118. package/dist/session/handoff/assignment.d.ts.map +1 -1
  119. package/dist/session/handoff/broadcast.d.ts +7 -0
  120. package/dist/session/handoff/broadcast.d.ts.map +1 -1
  121. package/dist/session/handoff/broadcast.js +16 -1
  122. package/dist/session/handoff/broadcast.js.map +1 -1
  123. package/dist/session/handoff/single.d.ts +7 -0
  124. package/dist/session/handoff/single.d.ts.map +1 -1
  125. package/dist/session/handoff/single.js +13 -1
  126. package/dist/session/handoff/single.js.map +1 -1
  127. package/dist/session/hierarchy/__tests__/session.test.js +2 -0
  128. package/dist/session/hierarchy/__tests__/session.test.js.map +1 -1
  129. package/dist/session/hierarchy/index.d.ts +1 -0
  130. package/dist/session/hierarchy/index.d.ts.map +1 -1
  131. package/dist/session/hierarchy/index.js.map +1 -1
  132. package/dist/session/hierarchy/session.d.ts +15 -3
  133. package/dist/session/hierarchy/session.d.ts.map +1 -1
  134. package/dist/session/hierarchy/session.js.map +1 -1
  135. package/dist/session/hierarchy/thread.d.ts +54 -0
  136. package/dist/session/hierarchy/thread.d.ts.map +1 -0
  137. package/dist/session/hierarchy/thread.js +2 -0
  138. package/dist/session/hierarchy/thread.js.map +1 -0
  139. package/dist/session/migration/id-prefix.d.ts +8 -13
  140. package/dist/session/migration/id-prefix.d.ts.map +1 -1
  141. package/dist/session/migration/id-prefix.js +8 -13
  142. package/dist/session/migration/id-prefix.js.map +1 -1
  143. package/dist/session/retention/__tests__/archive.test.js +3 -2
  144. package/dist/session/retention/__tests__/archive.test.js.map +1 -1
  145. package/dist/session/summary/__tests__/materialize.test.js +4 -3
  146. package/dist/session/summary/__tests__/materialize.test.js.map +1 -1
  147. package/dist/store/index.d.ts +0 -2
  148. package/dist/store/index.d.ts.map +1 -1
  149. package/dist/store/index.js +0 -1
  150. package/dist/store/index.js.map +1 -1
  151. package/dist/store/session/__tests__/disk.test.js +32 -5
  152. package/dist/store/session/__tests__/disk.test.js.map +1 -1
  153. package/dist/store/session/__tests__/memory.test.js +50 -9
  154. package/dist/store/session/__tests__/memory.test.js.map +1 -1
  155. package/dist/store/session/disk.d.ts +2 -1
  156. package/dist/store/session/disk.d.ts.map +1 -1
  157. package/dist/store/session/disk.js +61 -0
  158. package/dist/store/session/disk.js.map +1 -1
  159. package/dist/store/session/index.d.ts.map +1 -1
  160. package/dist/store/session/index.js +3 -4
  161. package/dist/store/session/index.js.map +1 -1
  162. package/dist/store/session/memory.d.ts +2 -1
  163. package/dist/store/session/memory.d.ts.map +1 -1
  164. package/dist/store/session/memory.js +13 -0
  165. package/dist/store/session/memory.js.map +1 -1
  166. package/dist/store/thread/disk.d.ts +41 -0
  167. package/dist/store/thread/disk.d.ts.map +1 -0
  168. package/dist/store/thread/disk.js +229 -0
  169. package/dist/store/thread/disk.js.map +1 -0
  170. package/dist/store/thread/index.d.ts +4 -0
  171. package/dist/store/thread/index.d.ts.map +1 -0
  172. package/dist/store/thread/index.js +6 -0
  173. package/dist/store/thread/index.js.map +1 -0
  174. package/dist/store/thread/memory.d.ts +23 -0
  175. package/dist/store/thread/memory.d.ts.map +1 -0
  176. package/dist/store/thread/memory.js +90 -0
  177. package/dist/store/thread/memory.js.map +1 -0
  178. package/dist/types/agent/base.d.ts +17 -21
  179. package/dist/types/agent/base.d.ts.map +1 -1
  180. package/dist/types/agent/factory.d.ts +8 -2
  181. package/dist/types/agent/factory.d.ts.map +1 -1
  182. package/dist/types/agent/task.d.ts +18 -11
  183. package/dist/types/agent/task.d.ts.map +1 -1
  184. package/dist/types/ids/index.d.ts +5 -9
  185. package/dist/types/ids/index.d.ts.map +1 -1
  186. package/dist/types/ids/index.js +4 -4
  187. package/dist/types/ids/index.js.map +1 -1
  188. package/dist/types/rag/retrieval.d.ts +4 -3
  189. package/dist/types/rag/retrieval.d.ts.map +1 -1
  190. package/dist/types/run/config.d.ts +6 -5
  191. package/dist/types/run/config.d.ts.map +1 -1
  192. package/dist/types/run/metadata.d.ts +5 -18
  193. package/dist/types/run/metadata.d.ts.map +1 -1
  194. package/dist/types/session/ids.d.ts +4 -13
  195. package/dist/types/session/ids.d.ts.map +1 -1
  196. package/dist/types/session/ids.js +3 -6
  197. package/dist/types/session/ids.js.map +1 -1
  198. package/dist/types/session/index.d.ts +1 -1
  199. package/dist/types/session/index.d.ts.map +1 -1
  200. package/dist/types/session/store.d.ts +32 -10
  201. package/dist/types/session/store.d.ts.map +1 -1
  202. package/dist/types/session/store.js +3 -8
  203. package/dist/types/session/store.js.map +1 -1
  204. package/dist/types/thread/index.d.ts +2 -0
  205. package/dist/types/thread/index.d.ts.map +1 -0
  206. package/dist/types/thread/index.js +5 -0
  207. package/dist/types/thread/index.js.map +1 -0
  208. package/dist/types/thread/store.d.ts +86 -0
  209. package/dist/types/thread/store.d.ts.map +1 -0
  210. package/dist/types/thread/store.js +22 -0
  211. package/dist/types/thread/store.js.map +1 -0
  212. package/dist/utils/id.d.ts +1 -12
  213. package/dist/utils/id.d.ts.map +1 -1
  214. package/dist/utils/id.js +3 -23
  215. package/dist/utils/id.js.map +1 -1
  216. package/package.json +6 -11
  217. package/src/agents/ReactiveAgent.ts +3 -2
  218. package/src/agents/SupervisorAgent.ts +5 -2
  219. package/src/bridge/a2a/index.ts +0 -1
  220. package/src/bridge/a2a/message.ts +0 -32
  221. package/src/bridge/a2a/task.ts +8 -7
  222. package/src/contracts/api.ts +6 -42
  223. package/src/contracts/ids.ts +1 -1
  224. package/src/contracts/index.ts +2 -8
  225. package/src/contracts/schemas.ts +1 -8
  226. package/src/index.ts +0 -4
  227. package/src/manager/agent/__tests__/lifecycle.test.ts +34 -13
  228. package/src/manager/agent/lifecycle.ts +114 -35
  229. package/src/manager/index.ts +3 -0
  230. package/src/manager/run/persistence.ts +7 -1
  231. package/src/manager/thread/__tests__/lifecycle.test.ts +286 -0
  232. package/src/manager/thread/lifecycle.ts +217 -0
  233. package/src/rag/retriever.ts +2 -2
  234. package/src/runtime/query/__tests__/context.test.ts +9 -8
  235. package/src/runtime/query/context-cache.ts +4 -4
  236. package/src/runtime/query/context.ts +15 -22
  237. package/src/runtime/query/index.ts +15 -16
  238. package/src/session/__tests__/integration/_fixtures.ts +36 -8
  239. package/src/session/__tests__/integration/archive-gate.test.ts +288 -0
  240. package/src/session/__tests__/integration/capacity-caps.test.ts +13 -6
  241. package/src/session/__tests__/integration/e2e-spawn.test.ts +20 -2
  242. package/src/session/__tests__/integration/event-stream-ordering.test.ts +14 -7
  243. package/src/session/__tests__/integration/handoff-broadcast-e2e.test.ts +39 -13
  244. package/src/session/__tests__/integration/handoff-illegal-transition.test.ts +54 -19
  245. package/src/session/__tests__/integration/handoff-single-e2e.test.ts +40 -9
  246. package/src/session/__tests__/integration/hierarchy-lifecycle.test.ts +13 -10
  247. package/src/session/__tests__/integration/prev-artifact-dag.test.ts +12 -5
  248. package/src/session/__tests__/integration/retention-archive.test.ts +5 -3
  249. package/src/session/__tests__/integration/spawn-rollback.test.ts +313 -0
  250. package/src/session/__tests__/integration/summary-materialization-e2e.test.ts +4 -2
  251. package/src/session/__tests__/integration/tenant-isolation.test.ts +16 -6
  252. package/src/session/errors.ts +89 -0
  253. package/src/session/handoff/__tests__/broadcast.test.ts +56 -28
  254. package/src/session/handoff/__tests__/capacity.test.ts +26 -20
  255. package/src/session/handoff/__tests__/single.test.ts +45 -28
  256. package/src/session/handoff/assignment.ts +13 -1
  257. package/src/session/handoff/broadcast.ts +26 -1
  258. package/src/session/handoff/single.ts +23 -1
  259. package/src/session/hierarchy/__tests__/session.test.ts +9 -1
  260. package/src/session/hierarchy/index.ts +1 -0
  261. package/src/session/hierarchy/session.ts +15 -3
  262. package/src/session/hierarchy/thread.ts +55 -0
  263. package/src/session/migration/id-prefix.ts +8 -13
  264. package/src/session/retention/__tests__/archive.test.ts +5 -3
  265. package/src/session/summary/__tests__/materialize.test.ts +6 -4
  266. package/src/store/index.ts +0 -3
  267. package/src/store/session/__tests__/disk.test.ts +57 -6
  268. package/src/store/session/__tests__/memory.test.ts +84 -9
  269. package/src/store/session/disk.ts +57 -1
  270. package/src/store/session/index.ts +3 -4
  271. package/src/store/session/memory.ts +13 -1
  272. package/src/store/thread/disk.ts +261 -0
  273. package/src/store/thread/index.ts +7 -0
  274. package/src/store/thread/memory.ts +104 -0
  275. package/src/types/agent/base.ts +17 -21
  276. package/src/types/agent/factory.ts +8 -3
  277. package/src/types/agent/task.ts +19 -11
  278. package/src/types/ids/index.ts +8 -15
  279. package/src/types/rag/retrieval.ts +4 -3
  280. package/src/types/run/config.ts +6 -5
  281. package/src/types/run/metadata.ts +5 -18
  282. package/src/types/session/ids.ts +4 -15
  283. package/src/types/session/index.ts +1 -2
  284. package/src/types/session/store.ts +34 -11
  285. package/src/types/thread/index.ts +5 -0
  286. package/src/types/thread/store.ts +92 -0
  287. package/src/utils/id.ts +3 -24
  288. package/dist/store/conversation/memory.d.ts +0 -43
  289. package/dist/store/conversation/memory.d.ts.map +0 -1
  290. package/dist/store/conversation/memory.js +0 -108
  291. package/dist/store/conversation/memory.js.map +0 -1
  292. package/dist/types/conversation/index.d.ts +0 -14
  293. package/dist/types/conversation/index.d.ts.map +0 -1
  294. package/dist/types/conversation/index.js +0 -2
  295. package/dist/types/conversation/index.js.map +0 -1
  296. package/src/store/conversation/memory.ts +0 -144
  297. package/src/types/conversation/index.ts +0 -15
@@ -10,6 +10,7 @@ import type { DeliverableRef } from '../../../session/summary/deliverable.js'
10
10
  import { SessionSummaryMaterializer } from '../../../session/summary/materialize.js'
11
11
  import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js'
12
12
  import { InMemorySessionStore } from '../../../store/session/memory.js'
13
+ import { InMemoryThreadStore } from '../../../store/thread/memory.js'
13
14
  import type {
14
15
  AgentCapabilities,
15
16
  AgentInput,
@@ -23,8 +24,9 @@ import type { AgentTaskContext, SendMessageOptions } from '../../../types/agent/
23
24
  import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
24
25
  import { createAssistantMessage } from '../../../types/message/index.js'
25
26
  import type { RunEvent } from '../../../types/run/events.js'
26
- import type { SummaryId } from '../../../types/session/ids.js'
27
+ import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
27
28
  import { ZERO_COST } from '../../../utils/cost.js'
29
+ import { ThreadManager } from '../../thread/lifecycle.js'
28
30
  import { AgentManager } from '../lifecycle.js'
29
31
 
30
32
  const tenant = 'tnt_alpha' as TenantId
@@ -113,10 +115,13 @@ function agentActor(id: string, tid: TenantId = tenant): ActorRef {
113
115
 
114
116
  interface Harness {
115
117
  store: InMemorySessionStore
118
+ threadStore: InMemoryThreadStore
119
+ threadManager: ThreadManager
116
120
  materializer: SessionSummaryMaterializer
117
121
  manager: AgentManager
118
122
  parentSession: Awaited<ReturnType<InMemorySessionStore['createSession']>>
119
123
  projectId: import('../../../types/session/ids.js').ProjectId
124
+ threadId: ThreadId
120
125
  registry: AgentRegistry
121
126
  }
122
127
 
@@ -125,9 +130,15 @@ async function buildHarness(
125
130
  tenantId: TenantId = tenant,
126
131
  ): Promise<Harness> {
127
132
  const store = new InMemorySessionStore()
133
+ const threadStore = new InMemoryThreadStore()
134
+ const threadManager = new ThreadManager({ threadStore, sessionStore: store })
128
135
  const project = await store.createProject({ tenantId, name: 'p1' }, tenantId)
136
+ const thread = await threadStore.createThread(
137
+ { projectId: project.id, title: 'lifecycle-test' },
138
+ tenantId,
139
+ )
129
140
  const parentSession = await store.createSession(
130
- { projectId: project.id, currentActor: user(tenantId) },
141
+ { threadId: thread.id, projectId: project.id, currentActor: user(tenantId) },
131
142
  tenantId,
132
143
  )
133
144
  // Parent runs kick the session into 'active' so the materializer can
@@ -148,14 +159,18 @@ async function buildHarness(
148
159
  summaryMaterializer: materializer,
149
160
  workspaceRegistry: new WorkspaceBackendRegistry(),
150
161
  capacity: new DefaultCapacityValidator(store),
162
+ threadManager,
151
163
  })
152
164
 
153
165
  return {
154
166
  store,
167
+ threadStore,
168
+ threadManager,
155
169
  materializer,
156
170
  manager,
157
171
  parentSession: { ...parentSession, status: 'active' },
158
172
  projectId: project.id,
173
+ threadId: thread.id,
159
174
  registry,
160
175
  }
161
176
  }
@@ -163,6 +178,7 @@ async function buildHarness(
163
178
  function buildContext(
164
179
  parentSessionId: SessionId,
165
180
  projectId: import('../../../types/session/ids.js').ProjectId,
181
+ threadId: ThreadId,
166
182
  tenantId: TenantId = tenant,
167
183
  depth = 0,
168
184
  ): AgentTaskContext {
@@ -173,6 +189,7 @@ function buildContext(
173
189
  depth,
174
190
  budgetTracker: { total: 100_000, remaining: 100_000 },
175
191
  tenantId,
192
+ threadId,
176
193
  sessionId: parentSessionId,
177
194
  projectId,
178
195
  parentActor: user(tenantId),
@@ -214,7 +231,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
214
231
 
215
232
  const task = await harness.manager.sendMessage(
216
233
  buildOptions('child-1', harness.parentSession.id, harness.projectId),
217
- buildContext(harness.parentSession.id, harness.projectId),
234
+ buildContext(harness.parentSession.id, harness.projectId, harness.threadId),
218
235
  listener,
219
236
  )
220
237
  await waitForTask(harness.manager, task.taskId)
@@ -259,7 +276,11 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
259
276
  // Pre-fill 8 direct sub-sessions under the parent, up to the default width cap.
260
277
  for (let i = 0; i < 8; i++) {
261
278
  const sibling = await harness.store.createSession(
262
- { projectId: harness.projectId, currentActor: agentActor('sibling') },
279
+ {
280
+ threadId: harness.threadId,
281
+ projectId: harness.projectId,
282
+ currentActor: agentActor('sibling'),
283
+ },
263
284
  tenant,
264
285
  )
265
286
  await harness.store.createSubSession(
@@ -276,7 +297,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
276
297
  await expect(
277
298
  harness.manager.sendMessage(
278
299
  buildOptions('child-1', harness.parentSession.id, harness.projectId),
279
- buildContext(harness.parentSession.id, harness.projectId),
300
+ buildContext(harness.parentSession.id, harness.projectId, harness.threadId),
280
301
  ),
281
302
  ).rejects.toBeInstanceOf(DelegationCapacityExceeded)
282
303
  })
@@ -290,7 +311,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
290
311
  let parentId: SessionId = harness.parentSession.id
291
312
  for (let i = 0; i < 4; i++) {
292
313
  const child = await harness.store.createSession(
293
- { projectId: harness.projectId, currentActor: agentActor('c') },
314
+ { threadId: harness.threadId, projectId: harness.projectId, currentActor: agentActor('c') },
294
315
  tenant,
295
316
  )
296
317
  await harness.store.createSubSession(
@@ -308,7 +329,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
308
329
  await expect(
309
330
  harness.manager.sendMessage(
310
331
  buildOptions('child-1', parentId, harness.projectId),
311
- buildContext(parentId, harness.projectId, tenant, 0),
332
+ buildContext(parentId, harness.projectId, harness.threadId, tenant, 0),
312
333
  ),
313
334
  ).rejects.toBeInstanceOf(DelegationCapacityExceeded)
314
335
  })
@@ -319,7 +340,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
319
340
 
320
341
  const task = await harness.manager.sendMessage(
321
342
  buildOptions('child-fail', harness.parentSession.id, harness.projectId),
322
- buildContext(harness.parentSession.id, harness.projectId),
343
+ buildContext(harness.parentSession.id, harness.projectId, harness.threadId),
323
344
  )
324
345
  await waitForTask(harness.manager, task.taskId)
325
346
 
@@ -342,7 +363,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
342
363
 
343
364
  const task = await harness.manager.sendMessage(
344
365
  buildOptions('child-msgs', harness.parentSession.id, harness.projectId),
345
- buildContext(harness.parentSession.id, harness.projectId),
366
+ buildContext(harness.parentSession.id, harness.projectId, harness.threadId),
346
367
  )
347
368
  await waitForTask(harness.manager, task.taskId)
348
369
 
@@ -364,7 +385,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
364
385
 
365
386
  // Seed c1 under parentSession, c2 under c1.
366
387
  const c1 = await harness.store.createSession(
367
- { projectId: harness.projectId, currentActor: agentActor('c1') },
388
+ { threadId: harness.threadId, projectId: harness.projectId, currentActor: agentActor('c1') },
368
389
  tenant,
369
390
  )
370
391
  await harness.store.createSubSession(
@@ -377,7 +398,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
377
398
  tenant,
378
399
  )
379
400
  const c2 = await harness.store.createSession(
380
- { projectId: harness.projectId, currentActor: agentActor('c2') },
401
+ { threadId: harness.threadId, projectId: harness.projectId, currentActor: agentActor('c2') },
381
402
  tenant,
382
403
  )
383
404
  await harness.store.createSubSession(
@@ -393,7 +414,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
393
414
  const events: RunEvent[] = []
394
415
  const task = await harness.manager.sendMessage(
395
416
  buildOptions('grandchild', c2.id, harness.projectId),
396
- buildContext(c2.id, harness.projectId),
417
+ buildContext(c2.id, harness.projectId, harness.threadId),
397
418
  (e) => {
398
419
  events.push(e)
399
420
  },
@@ -440,7 +461,7 @@ describe('AgentManager.sendMessage — Phase 6 SubSession spawn', () => {
440
461
  await expect(
441
462
  harness.manager.sendMessage(
442
463
  mismatchedOptions,
443
- buildContext(harness.parentSession.id, harness.projectId, tenant),
464
+ buildContext(harness.parentSession.id, harness.projectId, harness.threadId, tenant),
444
465
  ),
445
466
  ).rejects.toThrow(/Tenant mismatch/)
446
467
  })
@@ -35,6 +35,7 @@ import { ZERO_COST } from '../../utils/cost.js'
35
35
  import { toErrorMessage } from '../../utils/error.js'
36
36
  import { generateTaskId } from '../../utils/id.js'
37
37
  import { type Logger, getRootLogger } from '../../utils/logger.js'
38
+ import type { ThreadManager } from '../thread/lifecycle.js'
38
39
 
39
40
  /**
40
41
  * Dependencies threaded into {@link AgentManager}. Phase 6 promoted the
@@ -60,6 +61,14 @@ export interface AgentManagerDeps {
60
61
  readonly workspaceRegistry: WorkspaceBackendRegistry
61
62
  readonly summaryMaterializer: SessionSummaryMaterializer
62
63
  readonly capacity: CapacityValidator
64
+ /**
65
+ * Gate session creation on the parent Thread being `'open'` via
66
+ * {@link ThreadManager.requireOpen}. Added in Phase 2.6 to close the
67
+ * archive-gate gap flagged by the Phase 2.5 commit: without this,
68
+ * `ThreadManager.archive` was best-effort because spawn could still
69
+ * attach a live session under an archived Thread.
70
+ */
71
+ readonly threadManager: ThreadManager
63
72
  }
64
73
 
65
74
  interface ChildSpawnRecord {
@@ -146,6 +155,7 @@ export class AgentManager {
146
155
  budgetTracker: context.budgetTracker,
147
156
  factoryOptions: context.factoryOptions,
148
157
  tenantId: context.tenantId,
158
+ threadId: context.threadId,
149
159
  sessionId: spawnRecord.childSessionId,
150
160
  projectId: context.projectId,
151
161
  parentActor: childParentActor,
@@ -203,12 +213,16 @@ export class AgentManager {
203
213
 
204
214
  const definition = this.registry.getOrThrow(options.agentId)
205
215
  let childConfig: BaseAgentConfig
206
- if (definition.configBuilder && context.factoryOptions) {
216
+ if (definition.configBuilder) {
217
+ // Call the configBuilder regardless of whether factoryOptions were
218
+ // supplied. BYO-provider flows (Bedrock IAM, custom ProviderRegistry)
219
+ // commonly omit factoryOptions because the provider resolves its own
220
+ // credentials; the builder still needs to run to wire provider+tools.
221
+ // Defaults: empty factoryOptions when omitted; configOverrides win.
207
222
  childConfig = await definition.configBuilder({
208
- ...context.factoryOptions,
223
+ ...(context.factoryOptions ?? {}),
209
224
  tokenBudget: allocatedTokens,
210
225
  timeoutMs: options.budgetAllocation?.timeoutMs ?? context.budgetTracker.remaining,
211
- threadId: context.projectId as string,
212
226
  parentRunId: context.parentRunId as string | undefined,
213
227
  depth: context.depth + 1,
214
228
  ...options.configOverrides,
@@ -222,10 +236,11 @@ export class AgentManager {
222
236
  // configBuilder may not have been updated to emit these yet; we
223
237
  // stamp them here so query() sees them regardless.
224
238
  childConfig.sessionId = spawnRecord?.childSessionId ?? context.sessionId
239
+ childConfig.threadId = context.threadId
225
240
  childConfig.projectId = context.projectId
226
241
  childConfig.tenantId = context.tenantId
227
242
  } else {
228
- this.log.warn('No configBuilder or factoryOptions, using bare config', {
243
+ this.log.warn('No configBuilder, using bare config', {
229
244
  agentId: options.agentId,
230
245
  })
231
246
  childConfig = {
@@ -236,8 +251,8 @@ export class AgentManager {
236
251
  maxIterations: options.configOverrides?.maxIterations,
237
252
  maxResponseTokens: options.configOverrides?.maxResponseTokens,
238
253
  env: options.configOverrides?.env,
239
- threadId: context.projectId,
240
254
  sessionId: spawnRecord.childSessionId,
255
+ threadId: context.threadId,
241
256
  projectId: context.projectId,
242
257
  tenantId: context.tenantId,
243
258
  parentRunId: context.parentRunId,
@@ -367,6 +382,42 @@ export class AgentManager {
367
382
  // partial/legacy path).
368
383
  const store = this.deps.sessionStore
369
384
 
385
+ // Thread archive gate — runs FIRST so an archived thread fails fastest
386
+ // with the correct error (not DelegationCapacityExceeded or a project
387
+ // lookup error). Phase 2.6 closes the gap the Phase 2.5 commit
388
+ // flagged: without it, `ThreadManager.archive` could be undermined by
389
+ // a concurrent spawn landing a live session post-archival.
390
+ // Scope: this gate enforces the archive invariant at the production
391
+ // ingress path (AgentManager.sendMessage + handoff flows). Direct
392
+ // callers of `SessionStore.createSession` bypass it — the store layer
393
+ // is intentionally unaware of thread status to preserve its
394
+ // single-responsibility boundary.
395
+ await this.deps.threadManager.requireOpen(context.threadId, context.tenantId)
396
+
397
+ // Parent session cross-check: validate that `options.parentSessionId`
398
+ // exists for this tenant AND lives under the same thread as the
399
+ // context. A mismatched `context.threadId` would otherwise attach the
400
+ // child's sub-session edge to a parent in a different thread —
401
+ // corrupting the hierarchy invariant (cross-thread spawn is forbidden
402
+ // by design). Mirrors the `source.threadId === assignment.threadId`
403
+ // check in handoff (Phase 2.4).
404
+ const parentSession = await store.getSession(options.parentSessionId, context.tenantId)
405
+ if (!parentSession) {
406
+ throw new Error(
407
+ `Parent session ${options.parentSessionId} not found for tenant ${context.tenantId} — spawn rejected`,
408
+ )
409
+ }
410
+ if (parentSession.threadId !== context.threadId) {
411
+ throw new Error(
412
+ `Thread mismatch on spawn: parent session ${parentSession.id} is on thread ${parentSession.threadId}, but context.threadId=${context.threadId}. Cross-thread spawn is forbidden (session-hierarchy.md §6.3).`,
413
+ )
414
+ }
415
+ if (parentSession.projectId !== context.projectId) {
416
+ throw new Error(
417
+ `Project mismatch on spawn: parent session ${parentSession.id} is on project ${parentSession.projectId}, but context.projectId=${context.projectId}.`,
418
+ )
419
+ }
420
+
370
421
  const project = await store.getProject(context.projectId, context.tenantId)
371
422
  if (!project) {
372
423
  throw new Error(
@@ -401,45 +452,73 @@ export class AgentManager {
401
452
  parentActor: context.parentActor,
402
453
  }
403
454
 
455
+ // Child session inherits the parent's threadId verbatim (cross-thread
456
+ // spawn is forbidden by design — a delegated sub-agent stays on the
457
+ // same topic). Phase 2.6 elides the previous parent-session read by
458
+ // carrying `threadId` on `AgentTaskContext`.
404
459
  const childSession = await store.createSession(
405
- { projectId: context.projectId, currentActor: childActor },
406
- context.tenantId,
407
- )
408
-
409
- // Flip to 'active' so the materializer's atomic write + status flip
410
- // lands on terminal — §5.3: pending→active→idle.
411
- await store.updateSession({ ...childSession, status: 'active' }, context.tenantId)
412
-
413
- const subSession = await store.createSubSession(
414
460
  {
415
- parentSessionId: options.parentSessionId,
416
- childSessionId: childSession.id,
417
- kind: 'agent_spawn',
418
- spawnedBy: context.parentActor,
419
- failureMode: 'delegate',
420
- completionMode: 'summary_ref',
461
+ threadId: context.threadId,
462
+ projectId: context.projectId,
463
+ currentActor: childActor,
421
464
  },
422
465
  context.tenantId,
423
466
  )
424
467
 
425
- // Workspace provisioning best-effort. When the requested backend is
426
- // registered we create a new workspace for the child; failures surface
427
- // as WorkspaceBackendError and abort the spawn (Convention #0: no
428
- // silent fallback). Pattern doc §7.1 allows lazy provisioning: an
429
- // unregistered backend leaves `workspaceRef: undefined` on the spawn
430
- // record, not a hard error the registry is the capability surface.
468
+ // Compensating rollback wraps every mutation after createSession so a
469
+ // mid-flight failure (status flip, subsession insert, workspace driver)
470
+ // leaves no orphan child session. Codex SPAWN-ROLLBACK critique (Phase
471
+ // 2 review, 2026-04-18): without this, `workspaceRegistry.get().create`
472
+ // throwing or a concurrent `updateSession` race strands an
473
+ // `active` child session with no subsession edge, invisible to the
474
+ // parent but counted against `maxDelegationWidth`.
475
+ let subSession: Awaited<ReturnType<typeof store.createSubSession>> | undefined
431
476
  let workspaceRef: WorkspaceRef | undefined
432
- const backend = options.workspaceBackend ?? 'git-worktree'
433
- if (this.deps.workspaceRegistry.has(backend)) {
434
- const driver = this.deps.workspaceRegistry.get(backend)
435
- try {
477
+ try {
478
+ // Flip to 'active' so the materializer's atomic write + status flip
479
+ // lands on terminal — §5.3: pending→active→idle.
480
+ await store.updateSession({ ...childSession, status: 'active' }, context.tenantId)
481
+
482
+ subSession = await store.createSubSession(
483
+ {
484
+ parentSessionId: options.parentSessionId,
485
+ childSessionId: childSession.id,
486
+ kind: 'agent_spawn',
487
+ spawnedBy: context.parentActor,
488
+ failureMode: 'delegate',
489
+ completionMode: 'summary_ref',
490
+ },
491
+ context.tenantId,
492
+ )
493
+
494
+ // Workspace provisioning — best-effort. When the requested backend
495
+ // is registered we create a new workspace for the child; failures
496
+ // surface as WorkspaceBackendError and abort the spawn (Convention
497
+ // #0: no silent fallback). Pattern doc §7.1 allows lazy
498
+ // provisioning: an unregistered backend leaves `workspaceRef:
499
+ // undefined` on the spawn record, not a hard error — the registry
500
+ // is the capability surface.
501
+ const backend = options.workspaceBackend ?? 'git-worktree'
502
+ if (this.deps.workspaceRegistry.has(backend)) {
503
+ const driver = this.deps.workspaceRegistry.get(backend)
436
504
  workspaceRef = await driver.create({ label: subSession.id })
437
- } catch (err) {
438
- // Surface the failure — the subsession record exists but is
439
- // unusable without a workspace. Dispose any partial state.
440
- await store.updateSubSession({ ...subSession, status: 'failed' }, context.tenantId)
441
- throw err
442
505
  }
506
+ } catch (err) {
507
+ // Compensating rollback order is mandated by the store's
508
+ // deny-by-default cascade policy (Convention #5): `deleteSession`
509
+ // throws when any subsession still references it, so the subsession
510
+ // record must be removed first. No failed-subsession audit row is
511
+ // kept — the `subsession_spawned` run event never fired (we aborted
512
+ // before `buildSpawnRecord`), so no observer is expecting one, and
513
+ // leaving a `status: 'failed'` breadcrumb would be a dangling
514
+ // record with no corresponding emission. The original `err` is the
515
+ // caller-visible signal; cleanup errors are swallowed so they
516
+ // cannot mask it.
517
+ if (subSession !== undefined) {
518
+ await store.deleteSubSession(subSession.id, context.tenantId).catch(() => undefined)
519
+ }
520
+ await store.deleteSession(childSession.id, context.tenantId).catch(() => undefined)
521
+ throw err
443
522
  }
444
523
 
445
524
  return {
@@ -16,4 +16,7 @@ export type {
16
16
  export { PlanManager } from './plan/lifecycle.js'
17
17
  export type { PlanEvent, PlanEventListener, PlanApprovalHandler } from './plan/lifecycle.js'
18
18
 
19
+ export { ThreadManager } from './thread/lifecycle.js'
20
+ export type { ThreadManagerDeps } from './thread/lifecycle.js'
21
+
19
22
  export { AgentManager } from './agent/lifecycle.js'
@@ -5,7 +5,7 @@ import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
5
5
  import type { AssistantMessage, Message } from '../../types/message/index.js'
6
6
  import type { EmergencySaveData } from '../../types/run/emergency.js'
7
7
  import type { AgentRun, RunPersistenceConfig, StopReason } from '../../types/run/index.js'
8
- import type { ProjectId } from '../../types/session/ids.js'
8
+ import type { ProjectId, ThreadId } from '../../types/session/ids.js'
9
9
  import { type ModelPricing, ZERO_COST, accumulateCost } from '../../utils/cost.js'
10
10
  import { generateEmergencySaveId } from '../../utils/id.js'
11
11
  import type { Logger } from '../../utils/logger.js'
@@ -16,6 +16,7 @@ export class RunPersistence {
16
16
  private pricing?: ModelPricing
17
17
  private log: Logger
18
18
  private readonly _sessionId: SessionId
19
+ private readonly _threadId: ThreadId
19
20
  private readonly _tenantId: TenantId
20
21
  private readonly _projectId: ProjectId
21
22
 
@@ -23,6 +24,7 @@ export class RunPersistence {
23
24
  this.pricing = config.pricing
24
25
  this.log = config.log
25
26
  this._sessionId = config.sessionId
27
+ this._threadId = config.threadId
26
28
  this._tenantId = config.tenantId
27
29
  this._projectId = config.projectId
28
30
 
@@ -58,6 +60,10 @@ export class RunPersistence {
58
60
  return this._sessionId
59
61
  }
60
62
 
63
+ get threadId(): ThreadId {
64
+ return this._threadId
65
+ }
66
+
61
67
  get tenantId(): TenantId {
62
68
  return this._tenantId
63
69
  }