@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.
- package/CHANGELOG.md +53 -2
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +3 -2
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +5 -2
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/a2a/index.d.ts +1 -1
- package/dist/bridge/a2a/index.d.ts.map +1 -1
- package/dist/bridge/a2a/index.js +1 -1
- package/dist/bridge/a2a/index.js.map +1 -1
- package/dist/bridge/a2a/message.d.ts +0 -2
- package/dist/bridge/a2a/message.d.ts.map +1 -1
- package/dist/bridge/a2a/message.js +0 -26
- package/dist/bridge/a2a/message.js.map +1 -1
- package/dist/bridge/a2a/task.d.ts +4 -3
- package/dist/bridge/a2a/task.d.ts.map +1 -1
- package/dist/bridge/a2a/task.js +4 -4
- package/dist/bridge/a2a/task.js.map +1 -1
- package/dist/contracts/api.d.ts +6 -38
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/contracts/ids.d.ts +1 -1
- package/dist/contracts/ids.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +3 -5
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/schemas.d.ts +1 -31
- package/dist/contracts/schemas.d.ts.map +1 -1
- package/dist/contracts/schemas.js +1 -7
- package/dist/contracts/schemas.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/manager/agent/__tests__/lifecycle.test.js +27 -13
- package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +9 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +93 -31
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.d.ts.map +1 -1
- package/dist/manager/index.js +1 -0
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +3 -1
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +5 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/manager/thread/__tests__/lifecycle.test.d.ts +2 -0
- package/dist/manager/thread/__tests__/lifecycle.test.d.ts.map +1 -0
- package/dist/manager/thread/__tests__/lifecycle.test.js +216 -0
- package/dist/manager/thread/__tests__/lifecycle.test.js.map +1 -0
- package/dist/manager/thread/lifecycle.d.ts +105 -0
- package/dist/manager/thread/lifecycle.d.ts.map +1 -0
- package/dist/manager/thread/lifecycle.js +186 -0
- package/dist/manager/thread/lifecycle.js.map +1 -0
- package/dist/rag/retriever.js +2 -2
- package/dist/runtime/query/__tests__/context.test.js +8 -7
- package/dist/runtime/query/__tests__/context.test.js.map +1 -1
- package/dist/runtime/query/context-cache.d.ts +3 -3
- package/dist/runtime/query/context-cache.d.ts.map +1 -1
- package/dist/runtime/query/context-cache.js +2 -2
- package/dist/runtime/query/context-cache.js.map +1 -1
- package/dist/runtime/query/context.d.ts +12 -21
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +3 -1
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +13 -15
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/session/__tests__/integration/_fixtures.d.ts +11 -4
- package/dist/session/__tests__/integration/_fixtures.d.ts.map +1 -1
- package/dist/session/__tests__/integration/_fixtures.js +23 -6
- package/dist/session/__tests__/integration/_fixtures.js.map +1 -1
- package/dist/session/__tests__/integration/archive-gate.test.d.ts +15 -0
- package/dist/session/__tests__/integration/archive-gate.test.d.ts.map +1 -0
- package/dist/session/__tests__/integration/archive-gate.test.js +214 -0
- package/dist/session/__tests__/integration/archive-gate.test.js.map +1 -0
- package/dist/session/__tests__/integration/capacity-caps.test.js +13 -6
- package/dist/session/__tests__/integration/capacity-caps.test.js.map +1 -1
- package/dist/session/__tests__/integration/e2e-spawn.test.js +14 -2
- package/dist/session/__tests__/integration/e2e-spawn.test.js.map +1 -1
- package/dist/session/__tests__/integration/event-stream-ordering.test.js +14 -7
- package/dist/session/__tests__/integration/event-stream-ordering.test.js.map +1 -1
- package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js +26 -14
- package/dist/session/__tests__/integration/handoff-broadcast-e2e.test.js.map +1 -1
- package/dist/session/__tests__/integration/handoff-illegal-transition.test.js +30 -20
- package/dist/session/__tests__/integration/handoff-illegal-transition.test.js.map +1 -1
- package/dist/session/__tests__/integration/handoff-single-e2e.test.js +25 -9
- package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
- package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js +11 -10
- package/dist/session/__tests__/integration/hierarchy-lifecycle.test.js.map +1 -1
- package/dist/session/__tests__/integration/prev-artifact-dag.test.js +5 -4
- package/dist/session/__tests__/integration/prev-artifact-dag.test.js.map +1 -1
- package/dist/session/__tests__/integration/retention-archive.test.js +3 -2
- package/dist/session/__tests__/integration/retention-archive.test.js.map +1 -1
- package/dist/session/__tests__/integration/spawn-rollback.test.d.ts +26 -0
- package/dist/session/__tests__/integration/spawn-rollback.test.d.ts.map +1 -0
- package/dist/session/__tests__/integration/spawn-rollback.test.js +236 -0
- package/dist/session/__tests__/integration/spawn-rollback.test.js.map +1 -0
- package/dist/session/__tests__/integration/summary-materialization-e2e.test.js +2 -1
- package/dist/session/__tests__/integration/summary-materialization-e2e.test.js.map +1 -1
- package/dist/session/__tests__/integration/tenant-isolation.test.js +14 -5
- package/dist/session/__tests__/integration/tenant-isolation.test.js.map +1 -1
- package/dist/session/errors.d.ts +79 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +57 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/handoff/__tests__/broadcast.test.js +49 -31
- package/dist/session/handoff/__tests__/broadcast.test.js.map +1 -1
- package/dist/session/handoff/__tests__/capacity.test.js +21 -18
- package/dist/session/handoff/__tests__/capacity.test.js.map +1 -1
- package/dist/session/handoff/__tests__/single.test.js +39 -30
- package/dist/session/handoff/__tests__/single.test.js.map +1 -1
- package/dist/session/handoff/assignment.d.ts +13 -1
- package/dist/session/handoff/assignment.d.ts.map +1 -1
- package/dist/session/handoff/broadcast.d.ts +7 -0
- package/dist/session/handoff/broadcast.d.ts.map +1 -1
- package/dist/session/handoff/broadcast.js +16 -1
- package/dist/session/handoff/broadcast.js.map +1 -1
- package/dist/session/handoff/single.d.ts +7 -0
- package/dist/session/handoff/single.d.ts.map +1 -1
- package/dist/session/handoff/single.js +13 -1
- package/dist/session/handoff/single.js.map +1 -1
- package/dist/session/hierarchy/__tests__/session.test.js +2 -0
- package/dist/session/hierarchy/__tests__/session.test.js.map +1 -1
- package/dist/session/hierarchy/index.d.ts +1 -0
- package/dist/session/hierarchy/index.d.ts.map +1 -1
- package/dist/session/hierarchy/index.js.map +1 -1
- package/dist/session/hierarchy/session.d.ts +15 -3
- package/dist/session/hierarchy/session.d.ts.map +1 -1
- package/dist/session/hierarchy/session.js.map +1 -1
- package/dist/session/hierarchy/thread.d.ts +54 -0
- package/dist/session/hierarchy/thread.d.ts.map +1 -0
- package/dist/session/hierarchy/thread.js +2 -0
- package/dist/session/hierarchy/thread.js.map +1 -0
- package/dist/session/migration/id-prefix.d.ts +8 -13
- package/dist/session/migration/id-prefix.d.ts.map +1 -1
- package/dist/session/migration/id-prefix.js +8 -13
- package/dist/session/migration/id-prefix.js.map +1 -1
- package/dist/session/retention/__tests__/archive.test.js +3 -2
- package/dist/session/retention/__tests__/archive.test.js.map +1 -1
- package/dist/session/summary/__tests__/materialize.test.js +4 -3
- package/dist/session/summary/__tests__/materialize.test.js.map +1 -1
- package/dist/store/index.d.ts +0 -2
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +0 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/session/__tests__/disk.test.js +32 -5
- package/dist/store/session/__tests__/disk.test.js.map +1 -1
- package/dist/store/session/__tests__/memory.test.js +50 -9
- package/dist/store/session/__tests__/memory.test.js.map +1 -1
- package/dist/store/session/disk.d.ts +2 -1
- package/dist/store/session/disk.d.ts.map +1 -1
- package/dist/store/session/disk.js +61 -0
- package/dist/store/session/disk.js.map +1 -1
- package/dist/store/session/index.d.ts.map +1 -1
- package/dist/store/session/index.js +3 -4
- package/dist/store/session/index.js.map +1 -1
- package/dist/store/session/memory.d.ts +2 -1
- package/dist/store/session/memory.d.ts.map +1 -1
- package/dist/store/session/memory.js +13 -0
- package/dist/store/session/memory.js.map +1 -1
- package/dist/store/thread/disk.d.ts +41 -0
- package/dist/store/thread/disk.d.ts.map +1 -0
- package/dist/store/thread/disk.js +229 -0
- package/dist/store/thread/disk.js.map +1 -0
- package/dist/store/thread/index.d.ts +4 -0
- package/dist/store/thread/index.d.ts.map +1 -0
- package/dist/store/thread/index.js +6 -0
- package/dist/store/thread/index.js.map +1 -0
- package/dist/store/thread/memory.d.ts +23 -0
- package/dist/store/thread/memory.d.ts.map +1 -0
- package/dist/store/thread/memory.js +90 -0
- package/dist/store/thread/memory.js.map +1 -0
- package/dist/types/agent/base.d.ts +17 -21
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/factory.d.ts +8 -2
- package/dist/types/agent/factory.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +18 -11
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/ids/index.d.ts +5 -9
- package/dist/types/ids/index.d.ts.map +1 -1
- package/dist/types/ids/index.js +4 -4
- package/dist/types/ids/index.js.map +1 -1
- package/dist/types/rag/retrieval.d.ts +4 -3
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +6 -5
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/metadata.d.ts +5 -18
- package/dist/types/run/metadata.d.ts.map +1 -1
- package/dist/types/session/ids.d.ts +4 -13
- package/dist/types/session/ids.d.ts.map +1 -1
- package/dist/types/session/ids.js +3 -6
- package/dist/types/session/ids.js.map +1 -1
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/session/index.d.ts.map +1 -1
- package/dist/types/session/store.d.ts +32 -10
- package/dist/types/session/store.d.ts.map +1 -1
- package/dist/types/session/store.js +3 -8
- package/dist/types/session/store.js.map +1 -1
- package/dist/types/thread/index.d.ts +2 -0
- package/dist/types/thread/index.d.ts.map +1 -0
- package/dist/types/thread/index.js +5 -0
- package/dist/types/thread/index.js.map +1 -0
- package/dist/types/thread/store.d.ts +86 -0
- package/dist/types/thread/store.d.ts.map +1 -0
- package/dist/types/thread/store.js +22 -0
- package/dist/types/thread/store.js.map +1 -0
- package/dist/utils/id.d.ts +1 -12
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +3 -23
- package/dist/utils/id.js.map +1 -1
- package/package.json +6 -11
- package/src/agents/ReactiveAgent.ts +3 -2
- package/src/agents/SupervisorAgent.ts +5 -2
- package/src/bridge/a2a/index.ts +0 -1
- package/src/bridge/a2a/message.ts +0 -32
- package/src/bridge/a2a/task.ts +8 -7
- package/src/contracts/api.ts +6 -42
- package/src/contracts/ids.ts +1 -1
- package/src/contracts/index.ts +2 -8
- package/src/contracts/schemas.ts +1 -8
- package/src/index.ts +0 -4
- package/src/manager/agent/__tests__/lifecycle.test.ts +34 -13
- package/src/manager/agent/lifecycle.ts +114 -35
- package/src/manager/index.ts +3 -0
- package/src/manager/run/persistence.ts +7 -1
- package/src/manager/thread/__tests__/lifecycle.test.ts +286 -0
- package/src/manager/thread/lifecycle.ts +217 -0
- package/src/rag/retriever.ts +2 -2
- package/src/runtime/query/__tests__/context.test.ts +9 -8
- package/src/runtime/query/context-cache.ts +4 -4
- package/src/runtime/query/context.ts +15 -22
- package/src/runtime/query/index.ts +15 -16
- package/src/session/__tests__/integration/_fixtures.ts +36 -8
- package/src/session/__tests__/integration/archive-gate.test.ts +288 -0
- package/src/session/__tests__/integration/capacity-caps.test.ts +13 -6
- package/src/session/__tests__/integration/e2e-spawn.test.ts +20 -2
- package/src/session/__tests__/integration/event-stream-ordering.test.ts +14 -7
- package/src/session/__tests__/integration/handoff-broadcast-e2e.test.ts +39 -13
- package/src/session/__tests__/integration/handoff-illegal-transition.test.ts +54 -19
- package/src/session/__tests__/integration/handoff-single-e2e.test.ts +40 -9
- package/src/session/__tests__/integration/hierarchy-lifecycle.test.ts +13 -10
- package/src/session/__tests__/integration/prev-artifact-dag.test.ts +12 -5
- package/src/session/__tests__/integration/retention-archive.test.ts +5 -3
- package/src/session/__tests__/integration/spawn-rollback.test.ts +313 -0
- package/src/session/__tests__/integration/summary-materialization-e2e.test.ts +4 -2
- package/src/session/__tests__/integration/tenant-isolation.test.ts +16 -6
- package/src/session/errors.ts +89 -0
- package/src/session/handoff/__tests__/broadcast.test.ts +56 -28
- package/src/session/handoff/__tests__/capacity.test.ts +26 -20
- package/src/session/handoff/__tests__/single.test.ts +45 -28
- package/src/session/handoff/assignment.ts +13 -1
- package/src/session/handoff/broadcast.ts +26 -1
- package/src/session/handoff/single.ts +23 -1
- package/src/session/hierarchy/__tests__/session.test.ts +9 -1
- package/src/session/hierarchy/index.ts +1 -0
- package/src/session/hierarchy/session.ts +15 -3
- package/src/session/hierarchy/thread.ts +55 -0
- package/src/session/migration/id-prefix.ts +8 -13
- package/src/session/retention/__tests__/archive.test.ts +5 -3
- package/src/session/summary/__tests__/materialize.test.ts +6 -4
- package/src/store/index.ts +0 -3
- package/src/store/session/__tests__/disk.test.ts +57 -6
- package/src/store/session/__tests__/memory.test.ts +84 -9
- package/src/store/session/disk.ts +57 -1
- package/src/store/session/index.ts +3 -4
- package/src/store/session/memory.ts +13 -1
- package/src/store/thread/disk.ts +261 -0
- package/src/store/thread/index.ts +7 -0
- package/src/store/thread/memory.ts +104 -0
- package/src/types/agent/base.ts +17 -21
- package/src/types/agent/factory.ts +8 -3
- package/src/types/agent/task.ts +19 -11
- package/src/types/ids/index.ts +8 -15
- package/src/types/rag/retrieval.ts +4 -3
- package/src/types/run/config.ts +6 -5
- package/src/types/run/metadata.ts +5 -18
- package/src/types/session/ids.ts +4 -15
- package/src/types/session/index.ts +1 -2
- package/src/types/session/store.ts +34 -11
- package/src/types/thread/index.ts +5 -0
- package/src/types/thread/store.ts +92 -0
- package/src/utils/id.ts +3 -24
- package/dist/store/conversation/memory.d.ts +0 -43
- package/dist/store/conversation/memory.d.ts.map +0 -1
- package/dist/store/conversation/memory.js +0 -108
- package/dist/store/conversation/memory.js.map +0 -1
- package/dist/types/conversation/index.d.ts +0 -14
- package/dist/types/conversation/index.d.ts.map +0 -1
- package/dist/types/conversation/index.js +0 -2
- package/dist/types/conversation/index.js.map +0 -1
- package/src/store/conversation/memory.ts +0 -144
- package/src/types/conversation/index.ts +0 -15
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, expect, it, vi } from 'vitest'
|
|
16
|
+
import { ThreadManager } from '../../../manager/thread/lifecycle.js'
|
|
16
17
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
18
|
+
import { InMemoryThreadStore } from '../../../store/thread/memory.js'
|
|
17
19
|
import type { TenantId } from '../../../types/ids/index.js'
|
|
18
20
|
import { generateHandoffId } from '../../../utils/id.js'
|
|
19
21
|
import { TenantIsolationError } from '../../errors.js'
|
|
@@ -26,7 +28,10 @@ import { GitWorktreeDriver } from '../../workspace/git-worktree.js'
|
|
|
26
28
|
import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
|
|
27
29
|
import { DEFAULT_TENANT, OTHER_TENANT, okExec, stubLogger, userActor } from './_fixtures.js'
|
|
28
30
|
|
|
29
|
-
function buildHandoffDeps(
|
|
31
|
+
function buildHandoffDeps(
|
|
32
|
+
store: InMemorySessionStore,
|
|
33
|
+
threadStore: InMemoryThreadStore,
|
|
34
|
+
): {
|
|
30
35
|
deps: SingleHandoffDeps
|
|
31
36
|
updateCalls: Array<{ status?: string; ownerVersion?: number }>
|
|
32
37
|
} {
|
|
@@ -52,12 +57,14 @@ function buildHandoffDeps(store: InMemorySessionStore): {
|
|
|
52
57
|
return originalUpdate(session, tenantId)
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
const threadManager = new ThreadManager({ threadStore, sessionStore: store })
|
|
55
61
|
return {
|
|
56
62
|
deps: {
|
|
57
63
|
store,
|
|
58
64
|
workspaceRegistry,
|
|
59
65
|
capacity: new DefaultCapacityValidator(store),
|
|
60
66
|
events: sink,
|
|
67
|
+
threadManager,
|
|
61
68
|
},
|
|
62
69
|
updateCalls,
|
|
63
70
|
}
|
|
@@ -66,23 +73,29 @@ function buildHandoffDeps(store: InMemorySessionStore): {
|
|
|
66
73
|
describe('Integration — single-recipient handoff E2E', () => {
|
|
67
74
|
it('idle → locked → commit: source previousActors grew + ownerVersion bumped atomically', async () => {
|
|
68
75
|
const store = new InMemorySessionStore()
|
|
76
|
+
const threadStore = new InMemoryThreadStore()
|
|
69
77
|
const project = await store.createProject(
|
|
70
78
|
{ tenantId: DEFAULT_TENANT, name: 'ho' },
|
|
71
79
|
DEFAULT_TENANT,
|
|
72
80
|
)
|
|
81
|
+
const thread = await threadStore.createThread(
|
|
82
|
+
{ projectId: project.id, title: 'ho' },
|
|
83
|
+
DEFAULT_TENANT,
|
|
84
|
+
)
|
|
73
85
|
const sourceActor = userActor('usr_source')
|
|
74
86
|
const recipientActor = userActor('usr_target')
|
|
75
87
|
const session = await store.createSession(
|
|
76
|
-
{ projectId: project.id, currentActor: sourceActor },
|
|
88
|
+
{ threadId: thread.id, projectId: project.id, currentActor: sourceActor },
|
|
77
89
|
DEFAULT_TENANT,
|
|
78
90
|
)
|
|
79
91
|
|
|
80
|
-
const { deps, updateCalls } = buildHandoffDeps(store)
|
|
92
|
+
const { deps, updateCalls } = buildHandoffDeps(store, threadStore)
|
|
81
93
|
const assignment: HandoffAssignment = {
|
|
82
94
|
id: generateHandoffId(),
|
|
83
95
|
mode: 'single',
|
|
84
96
|
sourceSessionId: session.id,
|
|
85
97
|
tenantId: DEFAULT_TENANT,
|
|
98
|
+
threadId: thread.id,
|
|
86
99
|
projectId: project.id,
|
|
87
100
|
sourceActor,
|
|
88
101
|
recipientActor,
|
|
@@ -115,21 +128,27 @@ describe('Integration — single-recipient handoff E2E', () => {
|
|
|
115
128
|
|
|
116
129
|
it('cross-tenant assignment rejects at entry (TenantIsolationError)', async () => {
|
|
117
130
|
const store = new InMemorySessionStore()
|
|
131
|
+
const threadStore = new InMemoryThreadStore()
|
|
118
132
|
const project = await store.createProject(
|
|
119
133
|
{ tenantId: DEFAULT_TENANT, name: 'ct' },
|
|
120
134
|
DEFAULT_TENANT,
|
|
121
135
|
)
|
|
136
|
+
const thread = await threadStore.createThread(
|
|
137
|
+
{ projectId: project.id, title: 'ct' },
|
|
138
|
+
DEFAULT_TENANT,
|
|
139
|
+
)
|
|
122
140
|
const session = await store.createSession(
|
|
123
|
-
{ projectId: project.id, currentActor: userActor('usr_source') },
|
|
141
|
+
{ threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
|
|
124
142
|
DEFAULT_TENANT,
|
|
125
143
|
)
|
|
126
144
|
|
|
127
|
-
const { deps } = buildHandoffDeps(store)
|
|
145
|
+
const { deps } = buildHandoffDeps(store, threadStore)
|
|
128
146
|
const assignment: HandoffAssignment = {
|
|
129
147
|
id: generateHandoffId(),
|
|
130
148
|
mode: 'single',
|
|
131
149
|
sourceSessionId: session.id,
|
|
132
150
|
tenantId: OTHER_TENANT,
|
|
151
|
+
threadId: thread.id,
|
|
133
152
|
projectId: project.id,
|
|
134
153
|
sourceActor: userActor('usr_source', OTHER_TENANT),
|
|
135
154
|
recipientActor: userActor('usr_target', OTHER_TENANT),
|
|
@@ -144,21 +163,27 @@ describe('Integration — single-recipient handoff E2E', () => {
|
|
|
144
163
|
|
|
145
164
|
it('source-owned workspace provisioned for recipient', async () => {
|
|
146
165
|
const store = new InMemorySessionStore()
|
|
166
|
+
const threadStore = new InMemoryThreadStore()
|
|
147
167
|
const project = await store.createProject(
|
|
148
168
|
{ tenantId: DEFAULT_TENANT, name: 'wsp' },
|
|
149
169
|
DEFAULT_TENANT,
|
|
150
170
|
)
|
|
171
|
+
const thread = await threadStore.createThread(
|
|
172
|
+
{ projectId: project.id, title: 'wsp' },
|
|
173
|
+
DEFAULT_TENANT,
|
|
174
|
+
)
|
|
151
175
|
const source = await store.createSession(
|
|
152
|
-
{ projectId: project.id, currentActor: userActor('usr_source') },
|
|
176
|
+
{ threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
|
|
153
177
|
DEFAULT_TENANT,
|
|
154
178
|
)
|
|
155
179
|
|
|
156
|
-
const { deps } = buildHandoffDeps(store)
|
|
180
|
+
const { deps } = buildHandoffDeps(store, threadStore)
|
|
157
181
|
const assignment: HandoffAssignment = {
|
|
158
182
|
id: generateHandoffId(),
|
|
159
183
|
mode: 'single',
|
|
160
184
|
sourceSessionId: source.id,
|
|
161
185
|
tenantId: DEFAULT_TENANT,
|
|
186
|
+
threadId: thread.id,
|
|
162
187
|
projectId: project.id,
|
|
163
188
|
sourceActor: userActor('usr_source'),
|
|
164
189
|
recipientActor: userActor('usr_target'),
|
|
@@ -190,21 +215,27 @@ describe('Integration — single-recipient handoff E2E', () => {
|
|
|
190
215
|
it('denormalized tenantId stamped on Session + SubSession records', async () => {
|
|
191
216
|
const _tenantType: TenantId = DEFAULT_TENANT
|
|
192
217
|
const store = new InMemorySessionStore()
|
|
218
|
+
const threadStore = new InMemoryThreadStore()
|
|
193
219
|
const project = await store.createProject(
|
|
194
220
|
{ tenantId: DEFAULT_TENANT, name: 'denorm' },
|
|
195
221
|
DEFAULT_TENANT,
|
|
196
222
|
)
|
|
223
|
+
const thread = await threadStore.createThread(
|
|
224
|
+
{ projectId: project.id, title: 'denorm' },
|
|
225
|
+
DEFAULT_TENANT,
|
|
226
|
+
)
|
|
197
227
|
const source = await store.createSession(
|
|
198
|
-
{ projectId: project.id, currentActor: userActor('usr_source') },
|
|
228
|
+
{ threadId: thread.id, projectId: project.id, currentActor: userActor('usr_source') },
|
|
199
229
|
DEFAULT_TENANT,
|
|
200
230
|
)
|
|
201
|
-
const { deps } = buildHandoffDeps(store)
|
|
231
|
+
const { deps } = buildHandoffDeps(store, threadStore)
|
|
202
232
|
|
|
203
233
|
const assignment: HandoffAssignment = {
|
|
204
234
|
id: generateHandoffId(),
|
|
205
235
|
mode: 'single',
|
|
206
236
|
sourceSessionId: source.id,
|
|
207
237
|
tenantId: DEFAULT_TENANT,
|
|
238
|
+
threadId: thread.id,
|
|
208
239
|
projectId: project.id,
|
|
209
240
|
sourceActor: userActor('usr_source'),
|
|
210
241
|
recipientActor: userActor('usr_target'),
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import type { ThreadId } from '../../../types/session/ids.js'
|
|
13
14
|
import { TenantIsolationError } from '../../errors.js'
|
|
14
15
|
import { DEFAULT_TENANT, agentActor, buildHarness, userActor } from './_fixtures.js'
|
|
15
16
|
|
|
17
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
18
|
+
|
|
16
19
|
describe('Integration — hierarchy lifecycle', () => {
|
|
17
20
|
it('creates Tenant → Project → Session → SubSession with properly branded IDs', async () => {
|
|
18
21
|
const { store } = buildHarness()
|
|
@@ -23,7 +26,7 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
23
26
|
expect(project.tenantId.startsWith('tnt_')).toBe(true)
|
|
24
27
|
|
|
25
28
|
const session = await store.createSession(
|
|
26
|
-
{ projectId: project.id, currentActor: userActor('usr_a') },
|
|
29
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
|
|
27
30
|
tenant,
|
|
28
31
|
)
|
|
29
32
|
expect(session.id.startsWith('ses_')).toBe(true)
|
|
@@ -34,7 +37,7 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
34
37
|
expect(session.previousActors).toEqual([])
|
|
35
38
|
|
|
36
39
|
const childSession = await store.createSession(
|
|
37
|
-
{ projectId: project.id, currentActor: agentActor('agt_worker') },
|
|
40
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_worker') },
|
|
38
41
|
tenant,
|
|
39
42
|
)
|
|
40
43
|
const subSession = await store.createSubSession(
|
|
@@ -59,15 +62,15 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
59
62
|
|
|
60
63
|
const project = await store.createProject({ tenantId: tenant, name: 'drill' }, tenant)
|
|
61
64
|
const parent = await store.createSession(
|
|
62
|
-
{ projectId: project.id, currentActor: userActor('usr_root') },
|
|
65
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_root') },
|
|
63
66
|
tenant,
|
|
64
67
|
)
|
|
65
68
|
const childA = await store.createSession(
|
|
66
|
-
{ projectId: project.id, currentActor: agentActor('agt_a') },
|
|
69
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
|
|
67
70
|
tenant,
|
|
68
71
|
)
|
|
69
72
|
const childB = await store.createSession(
|
|
70
|
-
{ projectId: project.id, currentActor: agentActor('agt_b') },
|
|
73
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_b') },
|
|
71
74
|
tenant,
|
|
72
75
|
)
|
|
73
76
|
await store.createSubSession(
|
|
@@ -120,7 +123,7 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
120
123
|
const userC = userActor('usr_c')
|
|
121
124
|
|
|
122
125
|
const session = await store.createSession(
|
|
123
|
-
{ projectId: project.id, currentActor: userA },
|
|
126
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userA },
|
|
124
127
|
tenant,
|
|
125
128
|
)
|
|
126
129
|
|
|
@@ -154,11 +157,11 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
154
157
|
|
|
155
158
|
const project = await store.createProject({ tenantId: tenant, name: 'cycle' }, tenant)
|
|
156
159
|
const sA = await store.createSession(
|
|
157
|
-
{ projectId: project.id, currentActor: userActor('usr_a') },
|
|
160
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
|
|
158
161
|
tenant,
|
|
159
162
|
)
|
|
160
163
|
const sB = await store.createSession(
|
|
161
|
-
{ projectId: project.id, currentActor: userActor('usr_b') },
|
|
164
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_b') },
|
|
162
165
|
tenant,
|
|
163
166
|
)
|
|
164
167
|
|
|
@@ -195,11 +198,11 @@ describe('Integration — hierarchy lifecycle', () => {
|
|
|
195
198
|
|
|
196
199
|
const project = await store.createProject({ tenantId: tenant, name: 'lifecycle' }, tenant)
|
|
197
200
|
const parent = await store.createSession(
|
|
198
|
-
{ projectId: project.id, currentActor: userActor('usr_a') },
|
|
201
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
|
|
199
202
|
tenant,
|
|
200
203
|
)
|
|
201
204
|
const child = await store.createSession(
|
|
202
|
-
{ projectId: project.id, currentActor: agentActor('agt_a') },
|
|
205
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
|
|
203
206
|
tenant,
|
|
204
207
|
)
|
|
205
208
|
const sub = await store.createSubSession(
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
import { describe, expect, it } from 'vitest'
|
|
15
15
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
16
16
|
import type { SessionId } from '../../../types/ids/index.js'
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
DeliverableId,
|
|
19
|
+
SubSessionId,
|
|
20
|
+
SummaryId,
|
|
21
|
+
ThreadId,
|
|
22
|
+
} from '../../../types/session/ids.js'
|
|
18
23
|
import {
|
|
19
24
|
ArtifactRefCycleError,
|
|
20
25
|
type InterventionChainLoader,
|
|
@@ -24,6 +29,8 @@ import {
|
|
|
24
29
|
import type { DeliverableRef, SessionSummaryDeliverable } from '../../summary/deliverable.js'
|
|
25
30
|
import { DEFAULT_TENANT, agentActor, userActor } from './_fixtures.js'
|
|
26
31
|
|
|
32
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
33
|
+
|
|
27
34
|
/**
|
|
28
35
|
* Build a live loader pointing at a real InMemorySessionStore. Each node
|
|
29
36
|
* resolves via `findParentSubSession`-style lookup on the store's sub-session
|
|
@@ -68,7 +75,7 @@ async function buildLinearChain(
|
|
|
68
75
|
let previous: SessionId | null = null
|
|
69
76
|
for (let i = 0; i < length; i++) {
|
|
70
77
|
const s = await store.createSession(
|
|
71
|
-
{ projectId: project.id, currentActor: agentActor(`agt_${i}`) },
|
|
78
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(`agt_${i}`) },
|
|
72
79
|
DEFAULT_TENANT,
|
|
73
80
|
)
|
|
74
81
|
if (previous) {
|
|
@@ -260,15 +267,15 @@ describe('Integration — prevArtifactRef DAG against real store', () => {
|
|
|
260
267
|
DEFAULT_TENANT,
|
|
261
268
|
)
|
|
262
269
|
const sA = await store.createSession(
|
|
263
|
-
{ projectId: project.id, currentActor: agentActor('agt_a') },
|
|
270
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_a') },
|
|
264
271
|
DEFAULT_TENANT,
|
|
265
272
|
)
|
|
266
273
|
const sB = await store.createSession(
|
|
267
|
-
{ projectId: project.id, currentActor: agentActor('agt_b') },
|
|
274
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_b') },
|
|
268
275
|
DEFAULT_TENANT,
|
|
269
276
|
)
|
|
270
277
|
const sC = await store.createSession(
|
|
271
|
-
{ projectId: project.id, currentActor: agentActor('agt_c') },
|
|
278
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_c') },
|
|
272
279
|
DEFAULT_TENANT,
|
|
273
280
|
)
|
|
274
281
|
|
|
@@ -15,24 +15,26 @@ import { join } from 'node:path'
|
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
16
16
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
17
17
|
import { createUserMessage } from '../../../types/message/index.js'
|
|
18
|
-
import type { WorkspaceId } from '../../../types/session/ids.js'
|
|
18
|
+
import type { ThreadId, WorkspaceId } from '../../../types/session/ids.js'
|
|
19
19
|
import { ArchivalManager, ArchiveNotConfiguredError } from '../../retention/archive.js'
|
|
20
20
|
import { DiskArchiveBackend } from '../../retention/disk-backend.js'
|
|
21
21
|
import type { WorkspaceRef } from '../../workspace/ref.js'
|
|
22
22
|
import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
|
|
23
23
|
import { DEFAULT_TENANT, agentActor, userActor } from './_fixtures.js'
|
|
24
24
|
|
|
25
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
26
|
+
|
|
25
27
|
async function seedIdleSubSession(store: InMemorySessionStore) {
|
|
26
28
|
const project = await store.createProject(
|
|
27
29
|
{ tenantId: DEFAULT_TENANT, name: 'archive' },
|
|
28
30
|
DEFAULT_TENANT,
|
|
29
31
|
)
|
|
30
32
|
const parent = await store.createSession(
|
|
31
|
-
{ projectId: project.id, currentActor: userActor('usr_a') },
|
|
33
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor('usr_a') },
|
|
32
34
|
DEFAULT_TENANT,
|
|
33
35
|
)
|
|
34
36
|
const child = await store.createSession(
|
|
35
|
-
{ projectId: project.id, currentActor: agentActor('agt_w') },
|
|
37
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_w') },
|
|
36
38
|
DEFAULT_TENANT,
|
|
37
39
|
)
|
|
38
40
|
const sub = await store.createSubSession(
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration — AgentManager.provisionSpawn compensating rollback.
|
|
3
|
+
*
|
|
4
|
+
* Covers Codex SPAWN-ROLLBACK critique (ses_001-hierarchy-redesign Phase 2
|
|
5
|
+
* adversarial review, 2026-04-18). Without the try/catch wrapper around the
|
|
6
|
+
* createSession → updateSession → createSubSession → workspace.create
|
|
7
|
+
* mutation block, a failure after createSession leaves an `active` child
|
|
8
|
+
* session with no subsession edge — invisible to the parent, but counted
|
|
9
|
+
* against `maxDelegationWidth` and visible to SessionStore.listSessions
|
|
10
|
+
* consumers (archive/delete flows in ThreadManager).
|
|
11
|
+
*
|
|
12
|
+
* Failure modes exercised:
|
|
13
|
+
* A. Workspace driver throws on create. Subsession exists; must flip to
|
|
14
|
+
* 'failed' for audit. Child session must be hard-deleted.
|
|
15
|
+
* B. Subsession insert fails (store injection). No subsession recorded.
|
|
16
|
+
* Child session must be hard-deleted.
|
|
17
|
+
*
|
|
18
|
+
* Assertions in both cases:
|
|
19
|
+
* - sendMessage rejects with the underlying error.
|
|
20
|
+
* - SessionStore.listSessions(threadId) returns no row with the child id.
|
|
21
|
+
* - Parent session remains untouched (status, currentActor).
|
|
22
|
+
* - Fan-out cap reclaims the slot (next spawn succeeds up to the same
|
|
23
|
+
* width).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, expect, it } from 'vitest'
|
|
27
|
+
import { EMPTY_TOKEN_USAGE } from '../../../constants/limits.js'
|
|
28
|
+
import { AgentManager } from '../../../manager/agent/lifecycle.js'
|
|
29
|
+
import { ThreadManager } from '../../../manager/thread/lifecycle.js'
|
|
30
|
+
import { AgentRegistry } from '../../../registry/agent/definitions.js'
|
|
31
|
+
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
32
|
+
import { InMemoryThreadStore } from '../../../store/thread/memory.js'
|
|
33
|
+
import type {
|
|
34
|
+
AgentCapabilities,
|
|
35
|
+
AgentInput,
|
|
36
|
+
BaseAgentConfig,
|
|
37
|
+
BaseAgentResult,
|
|
38
|
+
} from '../../../types/agent/base.js'
|
|
39
|
+
import type { Agent } from '../../../types/agent/core.js'
|
|
40
|
+
import type { AgentDefinition } from '../../../types/agent/factory.js'
|
|
41
|
+
import type { AgentTaskContext, SendMessageOptions } from '../../../types/agent/task.js'
|
|
42
|
+
import type { RunId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
43
|
+
import { createAssistantMessage } from '../../../types/message/index.js'
|
|
44
|
+
import type { SummaryId } from '../../../types/session/ids.js'
|
|
45
|
+
import { ZERO_COST } from '../../../utils/cost.js'
|
|
46
|
+
import { DefaultCapacityValidator } from '../../handoff/capacity.js'
|
|
47
|
+
import type { ActorRef } from '../../hierarchy/actor.js'
|
|
48
|
+
import { SessionSummaryMaterializer } from '../../summary/materialize.js'
|
|
49
|
+
import type {
|
|
50
|
+
BranchWorkspaceParams,
|
|
51
|
+
CreateWorkspaceParams,
|
|
52
|
+
WorkspaceBackendDriver,
|
|
53
|
+
WorkspaceInspection,
|
|
54
|
+
} from '../../workspace/driver.js'
|
|
55
|
+
import type { WorkspaceRef } from '../../workspace/ref.js'
|
|
56
|
+
import { WorkspaceBackendRegistry } from '../../workspace/registry.js'
|
|
57
|
+
|
|
58
|
+
const tenant = 'tnt_alpha' as TenantId
|
|
59
|
+
|
|
60
|
+
const capabilities: AgentCapabilities = {
|
|
61
|
+
supportsTools: false,
|
|
62
|
+
supportsStreaming: false,
|
|
63
|
+
supportsConcurrency: false,
|
|
64
|
+
supportsSubAgents: false,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildAgent(id: string): Agent<BaseAgentConfig, BaseAgentResult> {
|
|
68
|
+
return {
|
|
69
|
+
type: 'reactive',
|
|
70
|
+
metadata: {
|
|
71
|
+
type: 'reactive',
|
|
72
|
+
id,
|
|
73
|
+
name: id,
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
category: 'test',
|
|
76
|
+
description: id,
|
|
77
|
+
capabilities,
|
|
78
|
+
},
|
|
79
|
+
run: async (_input: AgentInput, _config: BaseAgentConfig): Promise<BaseAgentResult> => ({
|
|
80
|
+
runId: 'run_child' as RunId,
|
|
81
|
+
status: 'completed',
|
|
82
|
+
usage: { ...EMPTY_TOKEN_USAGE },
|
|
83
|
+
cost: { ...ZERO_COST },
|
|
84
|
+
iterations: 1,
|
|
85
|
+
durationMs: 1,
|
|
86
|
+
messages: [createAssistantMessage('child did the work')],
|
|
87
|
+
result: 'child did the work',
|
|
88
|
+
}),
|
|
89
|
+
cancel: async () => undefined,
|
|
90
|
+
getCapabilities: () => capabilities,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildDefinition(agent: Agent<BaseAgentConfig, BaseAgentResult>): AgentDefinition {
|
|
95
|
+
return {
|
|
96
|
+
info: {
|
|
97
|
+
id: agent.metadata.id,
|
|
98
|
+
name: agent.metadata.name,
|
|
99
|
+
version: agent.metadata.version,
|
|
100
|
+
category: agent.metadata.category,
|
|
101
|
+
description: agent.metadata.description,
|
|
102
|
+
tools: [],
|
|
103
|
+
defaults: { model: 'test', tokenBudget: 1_000 },
|
|
104
|
+
},
|
|
105
|
+
typedAgent: agent,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class FailingWorkspaceDriver implements WorkspaceBackendDriver {
|
|
110
|
+
readonly kind = 'git-worktree' as const
|
|
111
|
+
createCalls = 0
|
|
112
|
+
|
|
113
|
+
async create(_params: CreateWorkspaceParams): Promise<WorkspaceRef> {
|
|
114
|
+
this.createCalls += 1
|
|
115
|
+
throw new Error('synthetic workspace backend failure')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async branch(_source: WorkspaceRef, _params: BranchWorkspaceParams): Promise<WorkspaceRef> {
|
|
119
|
+
throw new Error('unused in this test')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async dispose(_ref: WorkspaceRef): Promise<void> {
|
|
123
|
+
/* no-op */
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async inspect(_ref: WorkspaceRef): Promise<WorkspaceInspection> {
|
|
127
|
+
throw new Error('unused in this test')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('provisionSpawn compensating rollback', () => {
|
|
132
|
+
it('workspace driver failure — deletes child session, marks subsession failed, leaves no orphan', async () => {
|
|
133
|
+
const store = new InMemorySessionStore()
|
|
134
|
+
const threadStore = new InMemoryThreadStore()
|
|
135
|
+
const project = await store.createProject(
|
|
136
|
+
{ tenantId: tenant, name: 'rollback-project' },
|
|
137
|
+
tenant,
|
|
138
|
+
)
|
|
139
|
+
const thread = await threadStore.createThread(
|
|
140
|
+
{ projectId: project.id, title: 'rollback-topic' },
|
|
141
|
+
tenant,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const userActor: ActorRef = {
|
|
145
|
+
kind: 'user',
|
|
146
|
+
userId: 'usr_root' as UserId,
|
|
147
|
+
tenantId: tenant,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parentSession = await store.createSession(
|
|
151
|
+
{ threadId: thread.id, projectId: project.id, currentActor: userActor },
|
|
152
|
+
tenant,
|
|
153
|
+
)
|
|
154
|
+
await store.updateSession({ ...parentSession, status: 'active' }, tenant)
|
|
155
|
+
|
|
156
|
+
let summaryCounter = 0
|
|
157
|
+
const materializer = new SessionSummaryMaterializer({
|
|
158
|
+
store,
|
|
159
|
+
generateSummaryId: () => `sum_test_${++summaryCounter}` as SummaryId,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const registry = new AgentRegistry()
|
|
163
|
+
registry.register(buildDefinition(buildAgent('worker')))
|
|
164
|
+
|
|
165
|
+
const workspaceRegistry = new WorkspaceBackendRegistry()
|
|
166
|
+
const failingDriver = new FailingWorkspaceDriver()
|
|
167
|
+
workspaceRegistry.register(failingDriver)
|
|
168
|
+
|
|
169
|
+
const threadManager = new ThreadManager({ threadStore, sessionStore: store })
|
|
170
|
+
const manager = new AgentManager(registry, undefined, {
|
|
171
|
+
sessionStore: store,
|
|
172
|
+
summaryMaterializer: materializer,
|
|
173
|
+
workspaceRegistry,
|
|
174
|
+
capacity: new DefaultCapacityValidator(store),
|
|
175
|
+
threadManager,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const taskContext: AgentTaskContext = {
|
|
179
|
+
parentRunId: 'run_parent' as RunId,
|
|
180
|
+
parentAgentId: 'supervisor',
|
|
181
|
+
parentAbortController: new AbortController(),
|
|
182
|
+
depth: 0,
|
|
183
|
+
budgetTracker: { total: 100_000, remaining: 100_000 },
|
|
184
|
+
tenantId: tenant,
|
|
185
|
+
threadId: thread.id,
|
|
186
|
+
sessionId: parentSession.id,
|
|
187
|
+
projectId: project.id,
|
|
188
|
+
parentActor: userActor,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const options: SendMessageOptions = {
|
|
192
|
+
agentId: 'worker',
|
|
193
|
+
input: { messages: [], workingDirectory: '/tmp' },
|
|
194
|
+
parentSessionId: parentSession.id,
|
|
195
|
+
tenantId: tenant,
|
|
196
|
+
projectId: project.id,
|
|
197
|
+
parentActor: userActor,
|
|
198
|
+
workspaceBackend: 'git-worktree',
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
|
|
202
|
+
'synthetic workspace backend failure',
|
|
203
|
+
)
|
|
204
|
+
expect(failingDriver.createCalls).toBe(1)
|
|
205
|
+
|
|
206
|
+
// Child session is gone — archive/delete flows and fan-out caps see
|
|
207
|
+
// zero child attached to the thread beyond the parent.
|
|
208
|
+
const sessionsOnThread = await store.listSessions(thread.id, tenant)
|
|
209
|
+
expect(sessionsOnThread.map((s) => s.id)).toEqual([parentSession.id])
|
|
210
|
+
|
|
211
|
+
// Parent session is untouched.
|
|
212
|
+
const refetchedParent = await store.getSession(parentSession.id, tenant)
|
|
213
|
+
expect(refetchedParent?.status).toBe('active')
|
|
214
|
+
expect(refetchedParent?.currentActor).toEqual(userActor)
|
|
215
|
+
|
|
216
|
+
// No subsession breadcrumb — `subsession_spawned` never fired
|
|
217
|
+
// (provisionSpawn aborted before buildSpawnRecord), so nothing is
|
|
218
|
+
// expecting an audit row. Leaving a `status: 'failed'` record would
|
|
219
|
+
// dangle with no corresponding emission.
|
|
220
|
+
const subsessions = await store.getChildren(parentSession.id, tenant)
|
|
221
|
+
expect(subsessions).toHaveLength(0)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('repeated rollback does not accumulate orphan sessions or subsessions', async () => {
|
|
225
|
+
const store = new InMemorySessionStore()
|
|
226
|
+
const threadStore = new InMemoryThreadStore()
|
|
227
|
+
const project = await store.createProject(
|
|
228
|
+
{
|
|
229
|
+
tenantId: tenant,
|
|
230
|
+
name: 'rollback-repeat-project',
|
|
231
|
+
},
|
|
232
|
+
tenant,
|
|
233
|
+
)
|
|
234
|
+
const thread = await threadStore.createThread(
|
|
235
|
+
{ projectId: project.id, title: 'rollback-width-topic' },
|
|
236
|
+
tenant,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const userActor: ActorRef = {
|
|
240
|
+
kind: 'user',
|
|
241
|
+
userId: 'usr_root' as UserId,
|
|
242
|
+
tenantId: tenant,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parentSession = await store.createSession(
|
|
246
|
+
{ threadId: thread.id, projectId: project.id, currentActor: userActor },
|
|
247
|
+
tenant,
|
|
248
|
+
)
|
|
249
|
+
await store.updateSession({ ...parentSession, status: 'active' }, tenant)
|
|
250
|
+
|
|
251
|
+
let summaryCounter = 0
|
|
252
|
+
const materializer = new SessionSummaryMaterializer({
|
|
253
|
+
store,
|
|
254
|
+
generateSummaryId: () => `sum_test_${++summaryCounter}` as SummaryId,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const registry = new AgentRegistry()
|
|
258
|
+
registry.register(buildDefinition(buildAgent('worker')))
|
|
259
|
+
|
|
260
|
+
const workspaceRegistry = new WorkspaceBackendRegistry()
|
|
261
|
+
workspaceRegistry.register(new FailingWorkspaceDriver())
|
|
262
|
+
|
|
263
|
+
const threadManager = new ThreadManager({ threadStore, sessionStore: store })
|
|
264
|
+
const manager = new AgentManager(registry, undefined, {
|
|
265
|
+
sessionStore: store,
|
|
266
|
+
summaryMaterializer: materializer,
|
|
267
|
+
workspaceRegistry,
|
|
268
|
+
capacity: new DefaultCapacityValidator(store),
|
|
269
|
+
threadManager,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const taskContext: AgentTaskContext = {
|
|
273
|
+
parentRunId: 'run_parent' as RunId,
|
|
274
|
+
parentAgentId: 'supervisor',
|
|
275
|
+
parentAbortController: new AbortController(),
|
|
276
|
+
depth: 0,
|
|
277
|
+
budgetTracker: { total: 100_000, remaining: 100_000 },
|
|
278
|
+
tenantId: tenant,
|
|
279
|
+
threadId: thread.id,
|
|
280
|
+
sessionId: parentSession.id,
|
|
281
|
+
projectId: project.id,
|
|
282
|
+
parentActor: userActor,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const options: SendMessageOptions = {
|
|
286
|
+
agentId: 'worker',
|
|
287
|
+
input: { messages: [], workingDirectory: '/tmp' },
|
|
288
|
+
parentSessionId: parentSession.id,
|
|
289
|
+
tenantId: tenant,
|
|
290
|
+
projectId: project.id,
|
|
291
|
+
parentActor: userActor,
|
|
292
|
+
workspaceBackend: 'git-worktree',
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Two consecutive failing spawns. Without rollback, two orphan
|
|
296
|
+
// `active` child sessions would accumulate; with rollback, each
|
|
297
|
+
// attempt cleans up after itself and the store stays at { parent }.
|
|
298
|
+
await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
|
|
299
|
+
'synthetic workspace backend failure',
|
|
300
|
+
)
|
|
301
|
+
await expect(manager.sendMessage(options, taskContext)).rejects.toThrow(
|
|
302
|
+
'synthetic workspace backend failure',
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Still no child session under the thread.
|
|
306
|
+
const sessionsOnThread = await store.listSessions(thread.id, tenant)
|
|
307
|
+
expect(sessionsOnThread.map((s) => s.id)).toEqual([parentSession.id])
|
|
308
|
+
|
|
309
|
+
// No lingering subsession rows — both attempts rolled back cleanly.
|
|
310
|
+
const subsessions = await store.getChildren(parentSession.id, tenant)
|
|
311
|
+
expect(subsessions).toHaveLength(0)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -18,18 +18,20 @@
|
|
|
18
18
|
import { describe, expect, it } from 'vitest'
|
|
19
19
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
20
20
|
import type { SessionId } from '../../../types/ids/index.js'
|
|
21
|
-
import type { SummaryId } from '../../../types/session/ids.js'
|
|
21
|
+
import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
|
|
22
22
|
import { SessionSummaryMaterializer } from '../../summary/materialize.js'
|
|
23
23
|
import { SessionAlreadySummarizedError } from '../../summary/ref.js'
|
|
24
24
|
import { DEFAULT_TENANT, agentActor } from './_fixtures.js'
|
|
25
25
|
|
|
26
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
27
|
+
|
|
26
28
|
async function seedActive(store: InMemorySessionStore) {
|
|
27
29
|
const project = await store.createProject(
|
|
28
30
|
{ tenantId: DEFAULT_TENANT, name: 'summary' },
|
|
29
31
|
DEFAULT_TENANT,
|
|
30
32
|
)
|
|
31
33
|
const session = await store.createSession(
|
|
32
|
-
{ projectId: project.id, currentActor: agentActor('agt_worker') },
|
|
34
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor('agt_worker') },
|
|
33
35
|
DEFAULT_TENANT,
|
|
34
36
|
)
|
|
35
37
|
await store.updateSession({ ...session, status: 'active' }, DEFAULT_TENANT)
|