@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
|
@@ -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
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 9. Emit `onCommitted` with the new version.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import type { ThreadManager } from '../../manager/thread/lifecycle.js'
|
|
23
24
|
import type { SessionId, TenantId } from '../../types/ids/index.js'
|
|
24
25
|
import type { SessionStore } from '../../types/session/store.js'
|
|
25
26
|
import { TenantIsolationError } from '../errors.js'
|
|
@@ -64,6 +65,12 @@ export interface SingleHandoffDeps {
|
|
|
64
65
|
capacity: CapacityValidator
|
|
65
66
|
events: HandoffEventSink
|
|
66
67
|
runStatus?: RunStatusResolver
|
|
68
|
+
/**
|
|
69
|
+
* Gate the recipient-session creation on the Thread being `'open'`.
|
|
70
|
+
* Added in Phase 2.6 to mirror spawn — a handoff into an archived
|
|
71
|
+
* Thread would otherwise undermine `ThreadManager.archive`.
|
|
72
|
+
*/
|
|
73
|
+
threadManager: ThreadManager
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/**
|
|
@@ -85,6 +92,12 @@ export async function executeSingleHandoff(
|
|
|
85
92
|
})
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
// Thread archive gate (Phase 2.6) — runs FIRST so an archived thread
|
|
96
|
+
// fails fastest with `ThreadClosedError` rather than a lock rejection or
|
|
97
|
+
// capacity error. Checked BEFORE the CAS lock so a denied handoff leaves
|
|
98
|
+
// the source session untouched.
|
|
99
|
+
await deps.threadManager.requireOpen(assignment.threadId, tenantId)
|
|
100
|
+
|
|
88
101
|
// 1. Load source session + tenant check.
|
|
89
102
|
const source = await deps.store.getSession(assignment.sourceSessionId, tenantId)
|
|
90
103
|
if (!source) {
|
|
@@ -96,6 +109,11 @@ export async function executeSingleHandoff(
|
|
|
96
109
|
resource: `session(${source.id})`,
|
|
97
110
|
})
|
|
98
111
|
}
|
|
112
|
+
if (source.threadId !== assignment.threadId) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Assignment threadId ${assignment.threadId} does not match source session threadId ${source.threadId}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
99
117
|
if (source.projectId !== assignment.projectId) {
|
|
100
118
|
throw new Error(
|
|
101
119
|
`Assignment projectId ${assignment.projectId} does not match source session projectId ${source.projectId}`,
|
|
@@ -157,7 +175,11 @@ export async function executeSingleHandoff(
|
|
|
157
175
|
provisionedWorkspace = await driver.create({ label: `handoff-${assignment.id}` })
|
|
158
176
|
|
|
159
177
|
const recipientSession = await deps.store.createSession(
|
|
160
|
-
{
|
|
178
|
+
{
|
|
179
|
+
threadId: source.threadId,
|
|
180
|
+
projectId: source.projectId,
|
|
181
|
+
currentActor: assignment.recipientActor,
|
|
182
|
+
},
|
|
161
183
|
tenantId,
|
|
162
184
|
)
|
|
163
185
|
createdSessionId = recipientSession.id
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { RunStatus } from '../../../types/run/status.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ProjectId,
|
|
5
|
+
SessionId,
|
|
6
|
+
TenantId,
|
|
7
|
+
ThreadId,
|
|
8
|
+
UserId,
|
|
9
|
+
} from '../../../types/session/ids.js'
|
|
4
10
|
import type { ActorRef } from '../actor.js'
|
|
5
11
|
import { type Session, type SessionStatus, deriveStatus } from '../session.js'
|
|
6
12
|
|
|
7
13
|
const tenant = 'tnt_a' as TenantId
|
|
8
14
|
const project = 'prj_a' as ProjectId
|
|
15
|
+
const thread = 'thd_a' as ThreadId
|
|
9
16
|
|
|
10
17
|
function user(): ActorRef {
|
|
11
18
|
return { kind: 'user', userId: 'usr_a' as UserId, tenantId: tenant }
|
|
@@ -14,6 +21,7 @@ function user(): ActorRef {
|
|
|
14
21
|
function makeSession(status: SessionStatus): Session {
|
|
15
22
|
return {
|
|
16
23
|
id: 'ses_a' as SessionId,
|
|
24
|
+
threadId: thread,
|
|
17
25
|
projectId: project,
|
|
18
26
|
tenantId: tenant,
|
|
19
27
|
status,
|
|
@@ -5,6 +5,7 @@ export type { ActorRef, SystemRoleId } from './actor.js'
|
|
|
5
5
|
export type { Lineage } from './lineage.js'
|
|
6
6
|
export type { Tenant } from './tenant.js'
|
|
7
7
|
export type { Project, ProjectConfig } from './project.js'
|
|
8
|
+
export type { Thread, ThreadStatus } from './thread.js'
|
|
8
9
|
export type { Session, SessionStatus } from './session.js'
|
|
9
10
|
export { deriveStatus } from './session.js'
|
|
10
11
|
export type {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionId, TenantId } from '../../types/ids/index.js'
|
|
2
2
|
import type { RunStatus } from '../../types/run/status.js'
|
|
3
|
-
import type { ProjectId, WorkspaceId } from '../../types/session/ids.js'
|
|
3
|
+
import type { ProjectId, ThreadId, WorkspaceId } from '../../types/session/ids.js'
|
|
4
4
|
import type { ActorRef } from './actor.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -21,15 +21,27 @@ export type SessionStatus =
|
|
|
21
21
|
/**
|
|
22
22
|
* Multi-turn work unit owned by exactly one {@link ActorRef} at a time.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* Scope identifiers:
|
|
25
|
+
* - `threadId` — the topic-level {@link Thread} this Session lives under.
|
|
26
|
+
* Set at creation, immutable; Sessions never move threads.
|
|
27
|
+
* - `projectId` — the {@link Project} the owning Thread belongs to.
|
|
28
|
+
* **Denormalized** from `thread.projectId` at creation time; immutable.
|
|
29
|
+
* Kept on the Session record for ergonomic access (Project-scoped
|
|
30
|
+
* consumers — handoff validators, archival, retention — would otherwise
|
|
31
|
+
* need a second round-trip to ThreadStore on every read). This is NOT
|
|
32
|
+
* a deprecated mirror of a fading field; it is a deliberate
|
|
33
|
+
* denormalization of structurally-immutable derived data.
|
|
34
|
+
*
|
|
35
|
+
* Other invariants (session-hierarchy.md §4.3):
|
|
25
36
|
* - `previousActors` is append-only and publicly read-only; previous
|
|
26
|
-
* owners cannot write to the session again
|
|
37
|
+
* owners cannot write to the session again.
|
|
27
38
|
* - `ownerVersion` is the CAS counter for handoff (§6.1 / §6.2 / §6.4).
|
|
28
39
|
* - `workspaceId` is nullable for sessions whose workspace has not yet
|
|
29
40
|
* been provisioned (or has been torn down during archival).
|
|
30
41
|
*/
|
|
31
42
|
export interface Session {
|
|
32
43
|
id: SessionId
|
|
44
|
+
threadId: ThreadId
|
|
33
45
|
projectId: ProjectId
|
|
34
46
|
tenantId: TenantId
|
|
35
47
|
status: SessionStatus
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { TenantId } from '../../types/ids/index.js'
|
|
2
|
+
import type { ProjectId, ThreadId } from '../../types/session/ids.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle state of a Thread.
|
|
6
|
+
*
|
|
7
|
+
* - `open` — accepts new Sessions and new Runs under existing Sessions.
|
|
8
|
+
* - `archived` — read-only tombstone. No new Sessions may be created; existing
|
|
9
|
+
* Sessions remain navigable. Transitioning `open → archived` requires that
|
|
10
|
+
* no Session under the Thread is in a non-terminal state (guarded at the
|
|
11
|
+
* store level by listing + status fan-in).
|
|
12
|
+
*
|
|
13
|
+
* There is no `active` variant — Thread does NOT derive status from its child
|
|
14
|
+
* Sessions the way a Session does from its Runs. Thread is a pure container
|
|
15
|
+
* (Phase 0 decision B.1: Thread is container-only, no message stream, no
|
|
16
|
+
* fan-in). Its status is an explicit owner action.
|
|
17
|
+
*/
|
|
18
|
+
export type ThreadStatus = 'open' | 'archived'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Topic-level container sitting between {@link ProjectId Project} and
|
|
22
|
+
* {@link import('../../types/session/ids.js').SessionId Session} in the
|
|
23
|
+
* five-layer hierarchy (Project → Thread → Session → SubSession → Run).
|
|
24
|
+
*
|
|
25
|
+
* A Thread groups together many Sessions that address the same coherent
|
|
26
|
+
* topic or line-of-work within a Project (e.g. "auth refactor", "billing
|
|
27
|
+
* incident"). Sessions under the same Thread share Project-level shared
|
|
28
|
+
* resources (memory, vaults, knowledge bases) but have independent actor
|
|
29
|
+
* state, handoff history, and Run streams.
|
|
30
|
+
*
|
|
31
|
+
* Design §4 (`docs.local/sessions/ses_001-hierarchy-redesign/design.md`):
|
|
32
|
+
* - Container only. No own message stream, no own Run stream. Messages
|
|
33
|
+
* live in Sessions (Phase 0 decision B.1).
|
|
34
|
+
* - `title` is a user-facing label. **Titles are NOT unique within a
|
|
35
|
+
* Project.** Callers disambiguate by {@link ThreadId}; the title is
|
|
36
|
+
* freeform display text. If a product surface needs uniqueness (e.g.
|
|
37
|
+
* a human-typed slug), that constraint lives at the API layer, not in
|
|
38
|
+
* the kernel.
|
|
39
|
+
* - `ownerVersion` is the CAS counter for mutations — `updateThread` and
|
|
40
|
+
* archival transitions require a matching version and reject
|
|
41
|
+
* {@link StaleThreadError} on mismatch. Mirrors the
|
|
42
|
+
* {@link import('./session.js').Session} handoff CAS pattern (§6.1).
|
|
43
|
+
* - No fan-in `deriveStatus()` helper — status is owner-managed, not
|
|
44
|
+
* Run-derived. This is the Thread-vs-Session contract boundary.
|
|
45
|
+
*/
|
|
46
|
+
export interface Thread {
|
|
47
|
+
id: ThreadId
|
|
48
|
+
projectId: ProjectId
|
|
49
|
+
tenantId: TenantId
|
|
50
|
+
title: string
|
|
51
|
+
status: ThreadStatus
|
|
52
|
+
ownerVersion: number
|
|
53
|
+
createdAt: Date
|
|
54
|
+
updatedAt: Date
|
|
55
|
+
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ID-prefix migration window — read-side compat for legacy `thd_*` IDs.
|
|
3
3
|
*
|
|
4
|
-
* Phase 1 already ships {@link parseThreadId} in `utils/id.ts` that accepts
|
|
5
|
-
* either `thd_*` or `prj_*` silently. This module formalises the warning
|
|
6
|
-
* emission path called out in session-hierarchy.md §13.3.1:
|
|
7
|
-
*
|
|
8
|
-
* | Version | Reader accepts | Writer emits | Legacy read behaviour |
|
|
9
|
-
* |---------|---------------------|--------------|-------------------------------|
|
|
10
|
-
* | 0.2.x | `thd_*` AND `prj_*` | `prj_*` only | emits `MigrationWarning` once |
|
|
11
|
-
* | 0.3.x | `prj_*` only | `prj_*` only | rejects `StalePrefixError` |
|
|
12
|
-
*
|
|
13
4
|
* Consumers that touch raw legacy IDs (filesystem migrator, wire decoders,
|
|
14
5
|
* CLI imports) route through {@link acceptLegacyThreadId} so the warning
|
|
15
6
|
* signal is structured rather than ad-hoc console output (Convention #18).
|
|
16
7
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
8
|
+
* | Window state | Reader accepts | Writer emits | Legacy read behaviour |
|
|
9
|
+
* |--------------|---------------------|--------------|-------------------------------|
|
|
10
|
+
* | OPEN (now) | `thd_*` AND `prj_*` | `prj_*` only | emits `MigrationWarning` once |
|
|
11
|
+
* | CLOSED | `prj_*` only | `prj_*` only | rejects `StalePrefixError` |
|
|
12
|
+
*
|
|
13
|
+
* The {@link WINDOW_OPEN} constant is the single switch that flips this
|
|
14
|
+
* module from soft-accept to hard-reject. Convention #0: no silent
|
|
15
|
+
* long-lived compat — the window is explicit and fails closed when closed.
|
|
21
16
|
*/
|
|
22
17
|
|
|
23
18
|
import type { ProjectId } from '../../types/session/ids.js'
|
|
@@ -10,7 +10,7 @@ import { WorkspaceBackendRegistry } from '../../../session/workspace/registry.js
|
|
|
10
10
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
11
11
|
import type { AgentId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
12
12
|
import { createUserMessage } from '../../../types/message/index.js'
|
|
13
|
-
import type { WorkspaceId } from '../../../types/session/ids.js'
|
|
13
|
+
import type { ThreadId, WorkspaceId } from '../../../types/session/ids.js'
|
|
14
14
|
import {
|
|
15
15
|
ArchivalManager,
|
|
16
16
|
ArchiveNotConfiguredError,
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
} from '../archive.js'
|
|
19
19
|
import { DiskArchiveBackend } from '../disk-backend.js'
|
|
20
20
|
|
|
21
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
22
|
+
|
|
21
23
|
const tenantA = 'tnt_alpha' as TenantId
|
|
22
24
|
|
|
23
25
|
function stubLogger() {
|
|
@@ -43,11 +45,11 @@ function agentActor(tenantId: TenantId): ActorRef {
|
|
|
43
45
|
async function seedIdleSubSession(store: InMemorySessionStore) {
|
|
44
46
|
const project = await store.createProject({ tenantId: tenantA, name: 'p' }, tenantA)
|
|
45
47
|
const parent = await store.createSession(
|
|
46
|
-
{ projectId: project.id, currentActor: userActor(tenantA) },
|
|
48
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantA) },
|
|
47
49
|
tenantA,
|
|
48
50
|
)
|
|
49
51
|
const child = await store.createSession(
|
|
50
|
-
{ projectId: project.id, currentActor: agentActor(tenantA) },
|
|
52
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
|
|
51
53
|
tenantA,
|
|
52
54
|
)
|
|
53
55
|
const sub = await store.createSubSession(
|
|
@@ -3,7 +3,7 @@ import { TenantIsolationError } from '../../../session/errors.js'
|
|
|
3
3
|
import type { ActorRef } from '../../../session/hierarchy/actor.js'
|
|
4
4
|
import { InMemorySessionStore } from '../../../store/session/memory.js'
|
|
5
5
|
import type { AgentId, SessionId, TenantId, UserId } from '../../../types/ids/index.js'
|
|
6
|
-
import type { SummaryId } from '../../../types/session/ids.js'
|
|
6
|
+
import type { SummaryId, ThreadId } from '../../../types/session/ids.js'
|
|
7
7
|
import type { DeliverableRef } from '../deliverable.js'
|
|
8
8
|
import { SessionSummaryMaterializer } from '../materialize.js'
|
|
9
9
|
import {
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
SessionAlreadySummarizedError,
|
|
13
13
|
} from '../ref.js'
|
|
14
14
|
|
|
15
|
+
const TEST_THREAD_ID = 'thd_test' as ThreadId
|
|
16
|
+
|
|
15
17
|
const tenantA = 'tnt_alpha' as TenantId
|
|
16
18
|
const tenantB = 'tnt_beta' as TenantId
|
|
17
19
|
|
|
@@ -31,7 +33,7 @@ function makeSummaryIdGenerator(): () => SummaryId {
|
|
|
31
33
|
async function seedActiveSession(store: InMemorySessionStore, tenantId: TenantId) {
|
|
32
34
|
const project = await store.createProject({ tenantId, name: 'p1' }, tenantId)
|
|
33
35
|
const session = await store.createSession(
|
|
34
|
-
{ projectId: project.id, currentActor: agentActor(tenantId) },
|
|
36
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantId) },
|
|
35
37
|
tenantId,
|
|
36
38
|
)
|
|
37
39
|
// Put the session into `active` so the materializer's status-flip behavior
|
|
@@ -232,7 +234,7 @@ describe('SessionSummaryMaterializer.materialize', () => {
|
|
|
232
234
|
const store = new InMemorySessionStore()
|
|
233
235
|
const project = await store.createProject({ tenantId: tenantA, name: 'p1' }, tenantA)
|
|
234
236
|
const session = await store.createSession(
|
|
235
|
-
{ projectId: project.id, currentActor: userActor(tenantA) },
|
|
237
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: userActor(tenantA) },
|
|
236
238
|
tenantA,
|
|
237
239
|
)
|
|
238
240
|
// session.status defaults to 'idle'
|
|
@@ -255,7 +257,7 @@ describe('SessionSummaryMaterializer.materialize', () => {
|
|
|
255
257
|
const store = new InMemorySessionStore()
|
|
256
258
|
const project = await store.createProject({ tenantId: tenantA, name: 'p1' }, tenantA)
|
|
257
259
|
const session = await store.createSession(
|
|
258
|
-
{ projectId: project.id, currentActor: agentActor(tenantA) },
|
|
260
|
+
{ threadId: TEST_THREAD_ID, projectId: project.id, currentActor: agentActor(tenantA) },
|
|
259
261
|
tenantA,
|
|
260
262
|
)
|
|
261
263
|
await store.updateSession({ ...session, status: 'failed' }, tenantA)
|
package/src/store/index.ts
CHANGED
|
@@ -10,9 +10,6 @@ export { InMemoryTaskStore } from './task/memory.js'
|
|
|
10
10
|
export { DiskTaskStore } from './task/disk.js'
|
|
11
11
|
export type { DiskTaskStoreConfig } from './task/disk.js'
|
|
12
12
|
|
|
13
|
-
export { InMemoryConversationStore } from './conversation/memory.js'
|
|
14
|
-
export type { InMemoryConversationStoreConfig } from './conversation/memory.js'
|
|
15
|
-
|
|
16
13
|
export { InMemoryMemoryIndex } from './memory/index.js'
|
|
17
14
|
export { InMemoryMemoryStore } from './memory/memory.js'
|
|
18
15
|
export { DiskMemoryStore } from './memory/disk.js'
|