@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.
- package/CHANGELOG.md +74 -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 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- 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/registry/tool/execute.js +1 -1
- package/dist/registry/tool/execute.js.map +1 -1
- 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 +2 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +1 -1
- package/dist/runtime/query/iteration/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/telemetry/runtime-accessors.d.ts +4 -0
- package/dist/telemetry/runtime-accessors.d.ts.map +1 -0
- package/dist/telemetry/runtime-accessors.js +17 -0
- package/dist/telemetry/runtime-accessors.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 +11 -20
- 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 +3 -15
- 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/registry/tool/execute.ts +1 -1
- 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 +16 -17
- package/src/runtime/query/iteration/index.ts +1 -1
- 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/telemetry/runtime-accessors.ts +19 -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/provider/telemetry/setup.d.ts +0 -19
- package/dist/provider/telemetry/setup.d.ts.map +0 -1
- package/dist/provider/telemetry/setup.js +0 -102
- package/dist/provider/telemetry/setup.js.map +0 -1
- 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/telemetry/index.d.ts +0 -6
- package/dist/telemetry/index.d.ts.map +0 -1
- package/dist/telemetry/index.js +0 -4
- package/dist/telemetry/index.js.map +0 -1
- package/dist/telemetry/metrics.d.ts +0 -8
- package/dist/telemetry/metrics.d.ts.map +0 -1
- package/dist/telemetry/metrics.js +0 -53
- package/dist/telemetry/metrics.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/dist/types/telemetry/index.d.ts +0 -10
- package/dist/types/telemetry/index.d.ts.map +0 -1
- package/dist/types/telemetry/index.js +0 -2
- package/dist/types/telemetry/index.js.map +0 -1
- package/src/provider/telemetry/setup.ts +0 -125
- package/src/store/conversation/memory.ts +0 -144
- package/src/telemetry/index.ts +0 -14
- package/src/telemetry/metrics.ts +0 -69
- package/src/types/conversation/index.ts +0 -15
- package/src/types/telemetry/index.ts +0 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ThreadManager } from '../../../manager/thread/lifecycle.js'
|
|
2
3
|
import type { ActorRef } from '../../../session/hierarchy/actor.js'
|
|
3
4
|
import {
|
|
4
5
|
type ExecFile,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
} from '../../../session/workspace/git-worktree.js'
|
|
8
9
|
import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js'
|
|
9
10
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
11
|
+
import { InMemoryThreadStore } from '../../../store/thread/memory.js'
|
|
10
12
|
import type { SessionId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
11
13
|
import type { ProjectId } from '../../../types/session/ids.js'
|
|
12
14
|
import { generateHandoffId } from '../../../utils/id.js'
|
|
@@ -56,7 +58,11 @@ interface DepsBundle {
|
|
|
56
58
|
events: MockedHandoffEventSink
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function buildDeps(
|
|
61
|
+
function buildDeps(
|
|
62
|
+
store: InMemorySessionStore,
|
|
63
|
+
threadStore: InMemoryThreadStore,
|
|
64
|
+
execOverride?: ExecFile,
|
|
65
|
+
): DepsBundle {
|
|
60
66
|
const exec: ExecFile = execOverride ? execOverride : async (_file, _args) => okExec()
|
|
61
67
|
const driver = new GitWorktreeDriver({
|
|
62
68
|
repoRoot: '/repo',
|
|
@@ -73,29 +79,36 @@ function buildDeps(store: InMemorySessionStore, execOverride?: ExecFile): DepsBu
|
|
|
73
79
|
onBroadcastRollback: vi.fn<(ev: HandoffBroadcastRollbackEvent) => void>(),
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
const threadManager = new ThreadManager({ threadStore, sessionStore: store })
|
|
76
83
|
return {
|
|
77
84
|
deps: {
|
|
78
85
|
store,
|
|
79
86
|
workspaceRegistry: registry,
|
|
80
87
|
capacity: new DefaultCapacityValidator(store),
|
|
81
88
|
events,
|
|
89
|
+
threadManager,
|
|
82
90
|
},
|
|
83
91
|
events,
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
async function seedIdle(store: InMemorySessionStore) {
|
|
95
|
+
async function seedIdle(store: InMemorySessionStore, threadStore: InMemoryThreadStore) {
|
|
88
96
|
const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
|
|
97
|
+
const thread = await threadStore.createThread(
|
|
98
|
+
{ projectId: project.id, title: 'handoff-broadcast-test' },
|
|
99
|
+
tenant,
|
|
100
|
+
)
|
|
89
101
|
const session = await store.createSession(
|
|
90
|
-
{ projectId: project.id, currentActor: user('usr_source') },
|
|
102
|
+
{ threadId: thread.id, projectId: project.id, currentActor: user('usr_source') },
|
|
91
103
|
tenant,
|
|
92
104
|
)
|
|
93
|
-
return { project, session }
|
|
105
|
+
return { project, thread, session }
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
function buildAssignments(
|
|
97
109
|
sourceSessionId: SessionId,
|
|
98
110
|
projectId: ProjectId,
|
|
111
|
+
threadId: Awaited<ReturnType<InMemoryThreadStore['createThread']>>['id'],
|
|
99
112
|
expectedOwnerVersion: number,
|
|
100
113
|
recipients: ActorRef[],
|
|
101
114
|
broadcastId = 'bc_1',
|
|
@@ -105,6 +118,7 @@ function buildAssignments(
|
|
|
105
118
|
mode: 'broadcast' as const,
|
|
106
119
|
sourceSessionId,
|
|
107
120
|
tenantId: tenant,
|
|
121
|
+
threadId,
|
|
108
122
|
projectId,
|
|
109
123
|
sourceActor: user('usr_source'),
|
|
110
124
|
recipientActor,
|
|
@@ -116,16 +130,18 @@ function buildAssignments(
|
|
|
116
130
|
|
|
117
131
|
describe('executeBroadcastHandoff', () => {
|
|
118
132
|
let store: InMemorySessionStore
|
|
133
|
+
let threadStore: InMemoryThreadStore
|
|
119
134
|
|
|
120
135
|
beforeEach(() => {
|
|
121
136
|
store = new InMemorySessionStore()
|
|
137
|
+
threadStore = new InMemoryThreadStore()
|
|
122
138
|
})
|
|
123
139
|
|
|
124
140
|
it('happy path: 3 recipients → source ends in awaiting_merge with 3 new children', async () => {
|
|
125
|
-
const { project, session } = await seedIdle(store)
|
|
126
|
-
const { deps, events } = buildDeps(store)
|
|
141
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
142
|
+
const { deps, events } = buildDeps(store, threadStore)
|
|
127
143
|
|
|
128
|
-
const assignments = buildAssignments(session.id, project.id, 0, [
|
|
144
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
|
|
129
145
|
user('usr_bob'),
|
|
130
146
|
user('usr_carol'),
|
|
131
147
|
user('usr_dan'),
|
|
@@ -149,7 +165,7 @@ describe('executeBroadcastHandoff', () => {
|
|
|
149
165
|
})
|
|
150
166
|
|
|
151
167
|
it('rollback on mid-fan-out failure (2nd recipient worktree add fails): source reverts, rollback emits accurate partialState', async () => {
|
|
152
|
-
const { project, session } = await seedIdle(store)
|
|
168
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
153
169
|
|
|
154
170
|
let addCount = 0
|
|
155
171
|
const exec: ExecFile = async (_file, args) => {
|
|
@@ -159,9 +175,9 @@ describe('executeBroadcastHandoff', () => {
|
|
|
159
175
|
}
|
|
160
176
|
return okExec()
|
|
161
177
|
}
|
|
162
|
-
const { deps, events } = buildDeps(store, exec)
|
|
178
|
+
const { deps, events } = buildDeps(store, threadStore, exec)
|
|
163
179
|
|
|
164
|
-
const assignments = buildAssignments(session.id, project.id, 0, [
|
|
180
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
|
|
165
181
|
user('usr_b'),
|
|
166
182
|
user('usr_c'),
|
|
167
183
|
user('usr_d'),
|
|
@@ -196,7 +212,7 @@ describe('executeBroadcastHandoff', () => {
|
|
|
196
212
|
})
|
|
197
213
|
|
|
198
214
|
it('rollback performs full cleanup via deleteSubSession/deleteSession (no status-flip stopgap)', async () => {
|
|
199
|
-
const { project, session } = await seedIdle(store)
|
|
215
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
200
216
|
|
|
201
217
|
let addCount = 0
|
|
202
218
|
const exec: ExecFile = async (_file, args) => {
|
|
@@ -206,9 +222,12 @@ describe('executeBroadcastHandoff', () => {
|
|
|
206
222
|
}
|
|
207
223
|
return okExec()
|
|
208
224
|
}
|
|
209
|
-
const { deps } = buildDeps(store, exec)
|
|
225
|
+
const { deps } = buildDeps(store, threadStore, exec)
|
|
210
226
|
|
|
211
|
-
const assignments = buildAssignments(session.id, project.id, 0, [
|
|
227
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
|
|
228
|
+
user('usr_b'),
|
|
229
|
+
user('usr_c'),
|
|
230
|
+
])
|
|
212
231
|
|
|
213
232
|
await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow()
|
|
214
233
|
|
|
@@ -224,7 +243,7 @@ describe('executeBroadcastHandoff', () => {
|
|
|
224
243
|
})
|
|
225
244
|
|
|
226
245
|
it('rollback idempotency: worktree dispose throwing during rollback does not bubble a secondary failure', async () => {
|
|
227
|
-
const { project, session } = await seedIdle(store)
|
|
246
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
228
247
|
|
|
229
248
|
let addCount = 0
|
|
230
249
|
let removeCount = 0
|
|
@@ -242,9 +261,12 @@ describe('executeBroadcastHandoff', () => {
|
|
|
242
261
|
}
|
|
243
262
|
return okExec()
|
|
244
263
|
}
|
|
245
|
-
const { deps, events } = buildDeps(store, exec)
|
|
264
|
+
const { deps, events } = buildDeps(store, threadStore, exec)
|
|
246
265
|
|
|
247
|
-
const assignments = buildAssignments(session.id, project.id, 0, [
|
|
266
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
|
|
267
|
+
user('usr_b'),
|
|
268
|
+
user('usr_c'),
|
|
269
|
+
])
|
|
248
270
|
|
|
249
271
|
// Outer failure is the PRIMARY one — the secondary dispose failure is
|
|
250
272
|
// swallowed. Primary wraps in WorkspaceBackendError (create op).
|
|
@@ -262,11 +284,15 @@ describe('executeBroadcastHandoff', () => {
|
|
|
262
284
|
})
|
|
263
285
|
|
|
264
286
|
it('dedupe: two assignments targeting same recipient → rejected pre-lock (no side effects)', async () => {
|
|
265
|
-
const { project, session } = await seedIdle(store)
|
|
266
|
-
const { deps, events } = buildDeps(store)
|
|
287
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
288
|
+
const { deps, events } = buildDeps(store, threadStore)
|
|
267
289
|
|
|
268
290
|
const bob = user('usr_bob')
|
|
269
|
-
const assignments = buildAssignments(session.id, project.id, 0, [
|
|
291
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [
|
|
292
|
+
bob,
|
|
293
|
+
bob,
|
|
294
|
+
user('usr_dan'),
|
|
295
|
+
])
|
|
270
296
|
|
|
271
297
|
await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
|
|
272
298
|
/duplicate recipient/,
|
|
@@ -281,11 +307,11 @@ describe('executeBroadcastHandoff', () => {
|
|
|
281
307
|
})
|
|
282
308
|
|
|
283
309
|
it('width cap: 9 recipients exceeds default maxWidth=8 → rejected before source lock', async () => {
|
|
284
|
-
const { project, session } = await seedIdle(store)
|
|
285
|
-
const { deps, events } = buildDeps(store)
|
|
310
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
311
|
+
const { deps, events } = buildDeps(store, threadStore)
|
|
286
312
|
|
|
287
313
|
const recipients = Array.from({ length: 9 }, (_, i) => user(`usr_${i}`))
|
|
288
|
-
const assignments = buildAssignments(session.id, project.id, 0, recipients)
|
|
314
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, recipients)
|
|
289
315
|
|
|
290
316
|
await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
|
|
291
317
|
/Delegation capacity exceeded/,
|
|
@@ -298,12 +324,13 @@ describe('executeBroadcastHandoff', () => {
|
|
|
298
324
|
})
|
|
299
325
|
|
|
300
326
|
it('concurrent broadcast on same source: second attempt rejected with HandoffVersionConflict', async () => {
|
|
301
|
-
const { project, session } = await seedIdle(store)
|
|
302
|
-
const { deps } = buildDeps(store)
|
|
327
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
328
|
+
const { deps } = buildDeps(store, threadStore)
|
|
303
329
|
|
|
304
330
|
const firstAssignments = buildAssignments(
|
|
305
331
|
session.id,
|
|
306
332
|
project.id,
|
|
333
|
+
thread.id,
|
|
307
334
|
0,
|
|
308
335
|
[user('usr_b'), user('usr_c')],
|
|
309
336
|
'bc_1',
|
|
@@ -322,6 +349,7 @@ describe('executeBroadcastHandoff', () => {
|
|
|
322
349
|
const second = buildAssignments(
|
|
323
350
|
session.id,
|
|
324
351
|
project.id,
|
|
352
|
+
thread.id,
|
|
325
353
|
0, // stale — actual is 1
|
|
326
354
|
[user('usr_d'), user('usr_e')],
|
|
327
355
|
'bc_2',
|
|
@@ -332,16 +360,16 @@ describe('executeBroadcastHandoff', () => {
|
|
|
332
360
|
})
|
|
333
361
|
|
|
334
362
|
it('empty assignments → throws a descriptive error', async () => {
|
|
335
|
-
const { deps } = buildDeps(store)
|
|
363
|
+
const { deps } = buildDeps(store, threadStore)
|
|
336
364
|
await expect(executeBroadcastHandoff(deps, [], tenant)).rejects.toThrow(
|
|
337
365
|
/assignments must not be empty/,
|
|
338
366
|
)
|
|
339
367
|
})
|
|
340
368
|
|
|
341
369
|
it('single-row broadcast → rejected (caller must use executeSingleHandoff)', async () => {
|
|
342
|
-
const { project, session } = await seedIdle(store)
|
|
343
|
-
const { deps } = buildDeps(store)
|
|
344
|
-
const assignments = buildAssignments(session.id, project.id, 0, [user('usr_b')])
|
|
370
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
371
|
+
const { deps } = buildDeps(store, threadStore)
|
|
372
|
+
const assignments = buildAssignments(session.id, project.id, thread.id, 0, [user('usr_b')])
|
|
345
373
|
|
|
346
374
|
await expect(executeBroadcastHandoff(deps, assignments, tenant)).rejects.toThrow(
|
|
347
375
|
/single-recipient handoffs must use executeSingleHandoff/,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { ActorRef } from '../../../session/hierarchy/actor.js'
|
|
3
3
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
4
|
+
import { InMemoryThreadStore } from '../../../store/thread/memory.js'
|
|
4
5
|
import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
6
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
5
7
|
import { DefaultCapacityValidator, DelegationCapacityExceeded } from '../capacity.js'
|
|
6
8
|
|
|
7
9
|
const tenant = 'tnt_alpha' as TenantId
|
|
@@ -16,18 +18,22 @@ function agent(): ActorRef {
|
|
|
16
18
|
|
|
17
19
|
async function seedProject(store: InMemorySessionStore) {
|
|
18
20
|
const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
const threadStore = new InMemoryThreadStore()
|
|
22
|
+
const thread = await threadStore.createThread({ projectId: project.id, title: 'default' }, tenant)
|
|
23
|
+
const root = await store.createSession(
|
|
24
|
+
{ threadId: thread.id, projectId: project.id, currentActor: user() },
|
|
25
|
+
tenant,
|
|
26
|
+
)
|
|
27
|
+
return { project, thread, root }
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
async function spawnChild(
|
|
24
31
|
store: InMemorySessionStore,
|
|
25
32
|
parentId: SessionId,
|
|
26
|
-
projectId:
|
|
27
|
-
|
|
28
|
-
: Parameters<InMemorySessionStore['createSession']>[0]['projectId'],
|
|
33
|
+
projectId: ProjectId,
|
|
34
|
+
threadId: ThreadId,
|
|
29
35
|
): Promise<{ childId: SessionId }> {
|
|
30
|
-
const child = await store.createSession({ projectId, currentActor: user() }, tenant)
|
|
36
|
+
const child = await store.createSession({ threadId, projectId, currentActor: user() }, tenant)
|
|
31
37
|
await store.createSubSession(
|
|
32
38
|
{
|
|
33
39
|
parentSessionId: parentId,
|
|
@@ -51,11 +57,11 @@ describe('DefaultCapacityValidator', () => {
|
|
|
51
57
|
|
|
52
58
|
it('depth: chain of 4 (root→c1→c2→c3→c4) allows a 5th (depth 5) to pass when limit = 5', async () => {
|
|
53
59
|
const store = new InMemorySessionStore()
|
|
54
|
-
const { project, root } = await seedProject(store)
|
|
55
|
-
const c1 = await spawnChild(store, root.id, project.id)
|
|
56
|
-
const c2 = await spawnChild(store, c1.childId, project.id)
|
|
57
|
-
const c3 = await spawnChild(store, c2.childId, project.id)
|
|
58
|
-
const c4 = await spawnChild(store, c3.childId, project.id)
|
|
60
|
+
const { project, thread, root } = await seedProject(store)
|
|
61
|
+
const c1 = await spawnChild(store, root.id, project.id, thread.id)
|
|
62
|
+
const c2 = await spawnChild(store, c1.childId, project.id, thread.id)
|
|
63
|
+
const c3 = await spawnChild(store, c2.childId, project.id, thread.id)
|
|
64
|
+
const c4 = await spawnChild(store, c3.childId, project.id, thread.id)
|
|
59
65
|
|
|
60
66
|
const validator = new DefaultCapacityValidator(store)
|
|
61
67
|
// Ancestry of c4: root→c1→c2→c3→c4 = length 5. Spawning under c4 = depth 5.
|
|
@@ -64,11 +70,11 @@ describe('DefaultCapacityValidator', () => {
|
|
|
64
70
|
|
|
65
71
|
it('depth: over-limit throws DelegationCapacityExceeded with dimension=depth', async () => {
|
|
66
72
|
const store = new InMemorySessionStore()
|
|
67
|
-
const { project, root } = await seedProject(store)
|
|
68
|
-
const c1 = await spawnChild(store, root.id, project.id)
|
|
69
|
-
const c2 = await spawnChild(store, c1.childId, project.id)
|
|
70
|
-
const c3 = await spawnChild(store, c2.childId, project.id)
|
|
71
|
-
const c4 = await spawnChild(store, c3.childId, project.id)
|
|
73
|
+
const { project, thread, root } = await seedProject(store)
|
|
74
|
+
const c1 = await spawnChild(store, root.id, project.id, thread.id)
|
|
75
|
+
const c2 = await spawnChild(store, c1.childId, project.id, thread.id)
|
|
76
|
+
const c3 = await spawnChild(store, c2.childId, project.id, thread.id)
|
|
77
|
+
const c4 = await spawnChild(store, c3.childId, project.id, thread.id)
|
|
72
78
|
|
|
73
79
|
const validator = new DefaultCapacityValidator(store)
|
|
74
80
|
try {
|
|
@@ -94,9 +100,9 @@ describe('DefaultCapacityValidator', () => {
|
|
|
94
100
|
|
|
95
101
|
it('width: existing 5 + pending 3 = 8 passes exactly at the limit', async () => {
|
|
96
102
|
const store = new InMemorySessionStore()
|
|
97
|
-
const { project, root } = await seedProject(store)
|
|
103
|
+
const { project, thread, root } = await seedProject(store)
|
|
98
104
|
for (let i = 0; i < 5; i++) {
|
|
99
|
-
await spawnChild(store, root.id, project.id)
|
|
105
|
+
await spawnChild(store, root.id, project.id, thread.id)
|
|
100
106
|
}
|
|
101
107
|
const validator = new DefaultCapacityValidator(store)
|
|
102
108
|
await expect(validator.validateWidth(root.id, 3, 8, tenant)).resolves.toBeUndefined()
|
|
@@ -104,9 +110,9 @@ describe('DefaultCapacityValidator', () => {
|
|
|
104
110
|
|
|
105
111
|
it('width: existing 6 + pending 3 = 9 exceeds 8, throws dimension=width', async () => {
|
|
106
112
|
const store = new InMemorySessionStore()
|
|
107
|
-
const { project, root } = await seedProject(store)
|
|
113
|
+
const { project, thread, root } = await seedProject(store)
|
|
108
114
|
for (let i = 0; i < 6; i++) {
|
|
109
|
-
await spawnChild(store, root.id, project.id)
|
|
115
|
+
await spawnChild(store, root.id, project.id, thread.id)
|
|
110
116
|
}
|
|
111
117
|
const validator = new DefaultCapacityValidator(store)
|
|
112
118
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ThreadManager } from '../../../manager/thread/lifecycle.js'
|
|
2
3
|
import { TenantIsolationError } from '../../../session/errors.js'
|
|
3
4
|
import type { ActorRef } from '../../../session/hierarchy/actor.js'
|
|
4
5
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
} from '../../../session/workspace/git-worktree.js'
|
|
9
10
|
import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js'
|
|
10
11
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
12
|
+
import { InMemoryThreadStore } from '../../../store/thread/memory.js'
|
|
11
13
|
import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
12
14
|
import { generateHandoffId } from '../../../utils/id.js'
|
|
13
15
|
import type { HandoffAssignment } from '../assignment.js'
|
|
@@ -59,6 +61,7 @@ interface MockedHandoffEventSink extends HandoffEventSink {
|
|
|
59
61
|
|
|
60
62
|
function buildDeps(
|
|
61
63
|
store: InMemorySessionStore,
|
|
64
|
+
threadStore: InMemoryThreadStore,
|
|
62
65
|
execOverride?: ExecFile,
|
|
63
66
|
runResolver?: RunStatusResolver,
|
|
64
67
|
): { deps: SingleHandoffDeps; events: MockedHandoffEventSink; execCalls: string[] } {
|
|
@@ -84,29 +87,36 @@ function buildDeps(
|
|
|
84
87
|
onBroadcastRollback: vi.fn<(ev: HandoffBroadcastRollbackEvent) => void>(),
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
const threadManager = new ThreadManager({ threadStore, sessionStore: store })
|
|
87
91
|
const deps: SingleHandoffDeps = {
|
|
88
92
|
store,
|
|
89
93
|
workspaceRegistry: registry,
|
|
90
94
|
capacity: new DefaultCapacityValidator(store),
|
|
91
95
|
events,
|
|
96
|
+
threadManager,
|
|
92
97
|
...(runResolver !== undefined && { runStatus: runResolver }),
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
return { deps, events, execCalls }
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
async function seedIdle(store: InMemorySessionStore) {
|
|
103
|
+
async function seedIdle(store: InMemorySessionStore, threadStore: InMemoryThreadStore) {
|
|
99
104
|
const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
|
|
105
|
+
const thread = await threadStore.createThread(
|
|
106
|
+
{ projectId: project.id, title: 'handoff-single-test' },
|
|
107
|
+
tenant,
|
|
108
|
+
)
|
|
100
109
|
const session = await store.createSession(
|
|
101
|
-
{ projectId: project.id, currentActor: user('usr_source') },
|
|
110
|
+
{ threadId: thread.id, projectId: project.id, currentActor: user('usr_source') },
|
|
102
111
|
tenant,
|
|
103
112
|
)
|
|
104
|
-
return { project, session }
|
|
113
|
+
return { project, thread, session }
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
function buildAssignment(
|
|
108
117
|
sourceSessionId: SessionId,
|
|
109
118
|
projectId: Awaited<ReturnType<InMemorySessionStore['createProject']>>['id'],
|
|
119
|
+
threadId: Awaited<ReturnType<InMemoryThreadStore['createThread']>>['id'],
|
|
110
120
|
expectedOwnerVersion: number,
|
|
111
121
|
recipient: ActorRef = user('usr_target'),
|
|
112
122
|
): HandoffAssignment {
|
|
@@ -115,6 +125,7 @@ function buildAssignment(
|
|
|
115
125
|
mode: 'single',
|
|
116
126
|
sourceSessionId,
|
|
117
127
|
tenantId: tenant,
|
|
128
|
+
threadId,
|
|
118
129
|
projectId,
|
|
119
130
|
sourceActor: user('usr_source'),
|
|
120
131
|
recipientActor: recipient,
|
|
@@ -125,16 +136,18 @@ function buildAssignment(
|
|
|
125
136
|
|
|
126
137
|
describe('executeSingleHandoff', () => {
|
|
127
138
|
let store: InMemorySessionStore
|
|
139
|
+
let threadStore: InMemoryThreadStore
|
|
128
140
|
|
|
129
141
|
beforeEach(() => {
|
|
130
142
|
store = new InMemorySessionStore()
|
|
143
|
+
threadStore = new InMemoryThreadStore()
|
|
131
144
|
})
|
|
132
145
|
|
|
133
146
|
it('happy path: idle source → lock → commit → outcome populated + source mutated', async () => {
|
|
134
|
-
const { project, session } = await seedIdle(store)
|
|
135
|
-
const { deps, events } = buildDeps(store)
|
|
147
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
148
|
+
const { deps, events } = buildDeps(store, threadStore)
|
|
136
149
|
|
|
137
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
150
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
138
151
|
const outcome = await executeSingleHandoff(deps, assignment, tenant)
|
|
139
152
|
|
|
140
153
|
expect(outcome.assignmentId).toBe(assignment.id)
|
|
@@ -155,11 +168,11 @@ describe('executeSingleHandoff', () => {
|
|
|
155
168
|
})
|
|
156
169
|
|
|
157
170
|
it('rejects when source session is non-idle (active → HandoffLockRejected with active_run)', async () => {
|
|
158
|
-
const { project, session } = await seedIdle(store)
|
|
171
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
159
172
|
await store.updateSession({ ...session, status: 'active' }, tenant)
|
|
160
173
|
|
|
161
|
-
const { deps } = buildDeps(store)
|
|
162
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
174
|
+
const { deps } = buildDeps(store, threadStore)
|
|
175
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
163
176
|
|
|
164
177
|
try {
|
|
165
178
|
await executeSingleHandoff(deps, assignment, tenant)
|
|
@@ -171,14 +184,14 @@ describe('executeSingleHandoff', () => {
|
|
|
171
184
|
})
|
|
172
185
|
|
|
173
186
|
it('rejects when Run resolver reports pending_hitl', async () => {
|
|
174
|
-
const { project, session } = await seedIdle(store)
|
|
187
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
175
188
|
const resolver: RunStatusResolver = {
|
|
176
189
|
async blockingRun() {
|
|
177
190
|
return { reason: 'pending_hitl' }
|
|
178
191
|
},
|
|
179
192
|
}
|
|
180
|
-
const { deps } = buildDeps(store, undefined, resolver)
|
|
181
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
193
|
+
const { deps } = buildDeps(store, threadStore, undefined, resolver)
|
|
194
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
182
195
|
|
|
183
196
|
try {
|
|
184
197
|
await executeSingleHandoff(deps, assignment, tenant)
|
|
@@ -190,11 +203,11 @@ describe('executeSingleHandoff', () => {
|
|
|
190
203
|
})
|
|
191
204
|
|
|
192
205
|
it('rejects on tenant mismatch (TenantIsolationError)', async () => {
|
|
193
|
-
const { project, session } = await seedIdle(store)
|
|
194
|
-
const { deps } = buildDeps(store)
|
|
206
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
207
|
+
const { deps } = buildDeps(store, threadStore)
|
|
195
208
|
// Assignment tenant differs from the call-site tenant.
|
|
196
209
|
const assignment: HandoffAssignment = {
|
|
197
|
-
...buildAssignment(session.id, project.id, 0),
|
|
210
|
+
...buildAssignment(session.id, project.id, thread.id, 0),
|
|
198
211
|
tenantId: otherTenant,
|
|
199
212
|
}
|
|
200
213
|
await expect(executeSingleHandoff(deps, assignment, otherTenant)).rejects.toBeInstanceOf(
|
|
@@ -203,13 +216,13 @@ describe('executeSingleHandoff', () => {
|
|
|
203
216
|
})
|
|
204
217
|
|
|
205
218
|
it('rejects on CAS mismatch (HandoffVersionConflict)', async () => {
|
|
206
|
-
const { project, session } = await seedIdle(store)
|
|
207
|
-
const { deps } = buildDeps(store)
|
|
219
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
220
|
+
const { deps } = buildDeps(store, threadStore)
|
|
208
221
|
|
|
209
222
|
// Simulate a concurrent bump: move ownerVersion to 1 before the assignment
|
|
210
223
|
// with expectedOwnerVersion=0 is executed.
|
|
211
224
|
await store.updateSession({ ...session, ownerVersion: 1 }, tenant)
|
|
212
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
225
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
213
226
|
|
|
214
227
|
try {
|
|
215
228
|
await executeSingleHandoff(deps, assignment, tenant)
|
|
@@ -224,18 +237,22 @@ describe('executeSingleHandoff', () => {
|
|
|
224
237
|
it('depth cap enforcement rejects with DelegationCapacityExceeded (dimension=depth)', async () => {
|
|
225
238
|
// Build a chain so the handoff source already sits at max depth.
|
|
226
239
|
const project = await store.createProject({ tenantId: tenant, name: 'p' }, tenant)
|
|
240
|
+
const thread = await threadStore.createThread(
|
|
241
|
+
{ projectId: project.id, title: 'depth-cap' },
|
|
242
|
+
tenant,
|
|
243
|
+
)
|
|
227
244
|
// Set a tight limit on the project via a second createProject? — no, the
|
|
228
245
|
// store hardcodes defaults {4,8,10}. Build a depth-4 chain then attempt
|
|
229
246
|
// handoff on depth-4 node (ancestry length 5 > 4).
|
|
230
247
|
const root = await store.createSession(
|
|
231
|
-
{ projectId: project.id, currentActor: user('usr_source') },
|
|
248
|
+
{ threadId: thread.id, projectId: project.id, currentActor: user('usr_source') },
|
|
232
249
|
tenant,
|
|
233
250
|
)
|
|
234
251
|
let parent = root.id
|
|
235
252
|
let tail: SessionId = root.id
|
|
236
253
|
for (let i = 0; i < 4; i++) {
|
|
237
254
|
const child = await store.createSession(
|
|
238
|
-
{ projectId: project.id, currentActor: user(`usr_${i}`) },
|
|
255
|
+
{ threadId: thread.id, projectId: project.id, currentActor: user(`usr_${i}`) },
|
|
239
256
|
tenant,
|
|
240
257
|
)
|
|
241
258
|
await store.createSubSession(
|
|
@@ -252,15 +269,15 @@ describe('executeSingleHandoff', () => {
|
|
|
252
269
|
}
|
|
253
270
|
|
|
254
271
|
// Source is `tail` at ancestry length 5 → depth-capacity with limit 4 rejects.
|
|
255
|
-
const { deps } = buildDeps(store)
|
|
256
|
-
const assignment = buildAssignment(tail, project.id, 0)
|
|
272
|
+
const { deps } = buildDeps(store, threadStore)
|
|
273
|
+
const assignment = buildAssignment(tail, project.id, thread.id, 0)
|
|
257
274
|
await expect(executeSingleHandoff(deps, assignment, tenant)).rejects.toBeInstanceOf(
|
|
258
275
|
DelegationCapacityExceeded,
|
|
259
276
|
)
|
|
260
277
|
})
|
|
261
278
|
|
|
262
279
|
it('compensating revert: workspace provisioning failure reverts source to idle, version unchanged, onUnlocked fires', async () => {
|
|
263
|
-
const { project, session } = await seedIdle(store)
|
|
280
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
264
281
|
|
|
265
282
|
// Fail only on `worktree add` but pass for everything else. Here we fail
|
|
266
283
|
// the single worktree add.
|
|
@@ -270,8 +287,8 @@ describe('executeSingleHandoff', () => {
|
|
|
270
287
|
}
|
|
271
288
|
return okExec()
|
|
272
289
|
}
|
|
273
|
-
const { deps, events } = buildDeps(store, exec)
|
|
274
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
290
|
+
const { deps, events } = buildDeps(store, threadStore, exec)
|
|
291
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
275
292
|
|
|
276
293
|
await expect(executeSingleHandoff(deps, assignment, tenant)).rejects.toThrow(
|
|
277
294
|
/Workspace backend git-worktree failed on create/,
|
|
@@ -290,7 +307,7 @@ describe('executeSingleHandoff', () => {
|
|
|
290
307
|
})
|
|
291
308
|
|
|
292
309
|
it('compensating revert: store.createSubSession failure still reverts + archives partial recipient', async () => {
|
|
293
|
-
const { project, session } = await seedIdle(store)
|
|
310
|
+
const { project, thread, session } = await seedIdle(store, threadStore)
|
|
294
311
|
|
|
295
312
|
// Monkey-patch createSubSession on the store to throw.
|
|
296
313
|
const original = store.createSubSession.bind(store)
|
|
@@ -298,8 +315,8 @@ describe('executeSingleHandoff', () => {
|
|
|
298
315
|
throw new Error('simulated createSubSession failure')
|
|
299
316
|
}
|
|
300
317
|
|
|
301
|
-
const { deps, events } = buildDeps(store)
|
|
302
|
-
const assignment = buildAssignment(session.id, project.id, 0)
|
|
318
|
+
const { deps, events } = buildDeps(store, threadStore)
|
|
319
|
+
const assignment = buildAssignment(session.id, project.id, thread.id, 0)
|
|
303
320
|
|
|
304
321
|
await expect(executeSingleHandoff(deps, assignment, tenant)).rejects.toThrow(
|
|
305
322
|
/createSubSession failure/,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { SessionId, TenantId } from '../../types/ids/index.js'
|
|
12
|
-
import type { HandoffId, ProjectId, WorkspaceId } from '../../types/session/ids.js'
|
|
12
|
+
import type { HandoffId, ProjectId, ThreadId, WorkspaceId } from '../../types/session/ids.js'
|
|
13
13
|
import type { ActorRef } from '../hierarchy/actor.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -32,6 +32,18 @@ export interface HandoffAssignment {
|
|
|
32
32
|
mode: HandoffMode
|
|
33
33
|
sourceSessionId: SessionId
|
|
34
34
|
tenantId: TenantId
|
|
35
|
+
/**
|
|
36
|
+
* Topic-layer scope the source session belongs to. Handoff recipients
|
|
37
|
+
* always land on the same Thread (cross-thread handoff is forbidden —
|
|
38
|
+
* a new actor taking over a conversation stays on the same topic).
|
|
39
|
+
* Validated against `source.threadId` at execute time.
|
|
40
|
+
*/
|
|
41
|
+
threadId: ThreadId
|
|
42
|
+
/**
|
|
43
|
+
* Denormalized from the owning Thread. Kept alongside `threadId` as the
|
|
44
|
+
* Session record itself carries both (see `Session` JSDoc). Consistency
|
|
45
|
+
* validated against `source.projectId` at execute time.
|
|
46
|
+
*/
|
|
35
47
|
projectId: ProjectId
|
|
36
48
|
/** The actor initiating the handoff (must be the source's current owner). */
|
|
37
49
|
sourceActor: ActorRef
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import type { ThreadManager } from '../../manager/thread/lifecycle.js'
|
|
24
25
|
import type { SessionId, TenantId } from '../../types/ids/index.js'
|
|
25
26
|
import type { SubSessionId } from '../../types/session/ids.js'
|
|
26
27
|
import type { SessionStore } from '../../types/session/store.js'
|
|
@@ -41,6 +42,12 @@ export interface BroadcastHandoffDeps {
|
|
|
41
42
|
capacity: CapacityValidator
|
|
42
43
|
events: HandoffEventSink
|
|
43
44
|
runStatus?: RunStatusResolver
|
|
45
|
+
/**
|
|
46
|
+
* Gate every recipient-session creation on the Thread being `'open'`.
|
|
47
|
+
* Added in Phase 2.6; checked once per broadcast (all recipients share
|
|
48
|
+
* a threadId by the fan-out invariant validated above).
|
|
49
|
+
*/
|
|
50
|
+
threadManager: ThreadManager
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/**
|
|
@@ -108,6 +115,9 @@ export async function executeBroadcastHandoff(
|
|
|
108
115
|
if (a.expectedOwnerVersion !== first.expectedOwnerVersion) {
|
|
109
116
|
throw new Error('executeBroadcastHandoff: all assignments must share expectedOwnerVersion')
|
|
110
117
|
}
|
|
118
|
+
if (a.threadId !== first.threadId) {
|
|
119
|
+
throw new Error('executeBroadcastHandoff: all assignments must share threadId')
|
|
120
|
+
}
|
|
111
121
|
if (a.projectId !== first.projectId) {
|
|
112
122
|
throw new Error('executeBroadcastHandoff: all assignments must share projectId')
|
|
113
123
|
}
|
|
@@ -130,6 +140,12 @@ export async function executeBroadcastHandoff(
|
|
|
130
140
|
seen.add(key)
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
// Thread archive gate (Phase 2.6) — runs BEFORE source load/capacity so an
|
|
144
|
+
// archived thread fails fastest with `ThreadClosedError`. All assignments
|
|
145
|
+
// share `threadId` by the shape validation above. Runs BEFORE the CAS
|
|
146
|
+
// lock so a denied fan-out leaves the source session untouched.
|
|
147
|
+
await deps.threadManager.requireOpen(first.threadId, tenantId)
|
|
148
|
+
|
|
133
149
|
// 3. Load source + tenant check.
|
|
134
150
|
const source = await deps.store.getSession(first.sourceSessionId, tenantId)
|
|
135
151
|
if (!source) {
|
|
@@ -141,6 +157,11 @@ export async function executeBroadcastHandoff(
|
|
|
141
157
|
resource: `session(${source.id})`,
|
|
142
158
|
})
|
|
143
159
|
}
|
|
160
|
+
if (source.threadId !== first.threadId) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Assignment threadId ${first.threadId} does not match source threadId ${source.threadId}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
144
165
|
if (source.projectId !== first.projectId) {
|
|
145
166
|
throw new Error(
|
|
146
167
|
`Assignment projectId ${first.projectId} does not match source projectId ${source.projectId}`,
|
|
@@ -220,7 +241,11 @@ export async function executeBroadcastHandoff(
|
|
|
220
241
|
worktreesProvisioned += 1
|
|
221
242
|
|
|
222
243
|
const childSession = await deps.store.createSession(
|
|
223
|
-
{
|
|
244
|
+
{
|
|
245
|
+
threadId: source.threadId,
|
|
246
|
+
projectId: source.projectId,
|
|
247
|
+
currentActor: assignment.recipientActor,
|
|
248
|
+
},
|
|
224
249
|
tenantId,
|
|
225
250
|
)
|
|
226
251
|
partial.createdSessionId = childSession.id
|