@namzu/sdk 13.1.0 → 14.0.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 +122 -0
- package/dist/agents/runAgent.d.ts +9 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js +101 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts +10 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +16 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/stdio.d.ts +12 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +33 -1
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +2 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +13 -1
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts +2 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js +73 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js.map +1 -0
- package/dist/contracts/schemas.d.ts +15 -0
- package/dist/contracts/schemas.d.ts.map +1 -1
- package/dist/contracts/schemas.js +18 -1
- package/dist/contracts/schemas.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +6 -4
- 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/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts +2 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts.map +1 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js +208 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js.map +1 -0
- package/dist/manager/project/lifecycle.d.ts +66 -0
- package/dist/manager/project/lifecycle.d.ts.map +1 -0
- package/dist/manager/project/lifecycle.js +125 -0
- package/dist/manager/project/lifecycle.js.map +1 -0
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/session/errors.d.ts +71 -1
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +50 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/handoff/broadcast.d.ts.map +1 -1
- package/dist/session/handoff/broadcast.js +4 -5
- package/dist/session/handoff/broadcast.js.map +1 -1
- package/dist/session/handoff/single.d.ts.map +1 -1
- package/dist/session/handoff/single.js +5 -4
- package/dist/session/handoff/single.js.map +1 -1
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts +2 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts.map +1 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js +75 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js.map +1 -0
- package/dist/session/migration/filesystem.d.ts.map +1 -1
- package/dist/session/migration/filesystem.js +24 -0
- package/dist/session/migration/filesystem.js.map +1 -1
- package/dist/store/session/disk.d.ts +3 -1
- package/dist/store/session/disk.d.ts.map +1 -1
- package/dist/store/session/disk.js +64 -1
- package/dist/store/session/disk.js.map +1 -1
- package/dist/store/session/memory.d.ts +3 -1
- package/dist/store/session/memory.d.ts.map +1 -1
- package/dist/store/session/memory.js +39 -1
- package/dist/store/session/memory.js.map +1 -1
- package/dist/types/project/entity.d.ts +17 -0
- package/dist/types/project/entity.d.ts.map +1 -1
- package/dist/types/session/store.d.ts +28 -1
- package/dist/types/session/store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/runAgent.ts +9 -0
- package/src/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.ts +127 -0
- package/src/connector/mcp/http-sse.ts +17 -0
- package/src/connector/mcp/stdio.ts +34 -1
- package/src/connector/mcp/streamable-http.ts +15 -1
- package/src/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.ts +84 -0
- package/src/contracts/schemas.ts +18 -1
- package/src/manager/agent/lifecycle.ts +6 -6
- package/src/manager/index.ts +3 -0
- package/src/manager/project/__tests__/a-closed-workspace-takes-no-work.test.ts +268 -0
- package/src/manager/project/lifecycle.ts +160 -0
- package/src/public-runtime.ts +9 -0
- package/src/session/errors.ts +85 -1
- package/src/session/handoff/broadcast.ts +4 -5
- package/src/session/handoff/single.ts +5 -4
- package/src/session/migration/__tests__/a-migrated-project-id-is-an-id.test.ts +92 -0
- package/src/session/migration/filesystem.ts +28 -0
- package/src/store/session/disk.ts +79 -2
- package/src/store/session/memory.ts +49 -2
- package/src/types/project/entity.ts +18 -0
- package/src/types/session/store.ts +35 -1
|
@@ -248,6 +248,34 @@ export class DefaultFilesystemMigrator implements FilesystemMigrator {
|
|
|
248
248
|
|
|
249
249
|
const legacyThreadId = entry
|
|
250
250
|
const suffix = legacyThreadId.slice('thd_'.length)
|
|
251
|
+
|
|
252
|
+
// The suffix comes off the filesystem, and `startsWith('thd_')`
|
|
253
|
+
// is the only thing it has passed. A folder named `thd_Not An
|
|
254
|
+
// Id` mints `prj_legacy_Not An Id`: structurally a `ProjectId`,
|
|
255
|
+
// accepted by no validator, and now a directory name in the new
|
|
256
|
+
// layout. The migration is the one place a project id is built
|
|
257
|
+
// from data rather than generated, so it is the one place the
|
|
258
|
+
// id contract has to be checked.
|
|
259
|
+
//
|
|
260
|
+
// (Not a containment check — it cannot be one. `readdir` yields
|
|
261
|
+
// path components, which hold no separator, and `..` is never
|
|
262
|
+
// among them; `prj_legacy_..` is a literal name, not a parent
|
|
263
|
+
// reference. Nothing here can leave the root.)
|
|
264
|
+
//
|
|
265
|
+
// It refuses rather than skipping. A skipped folder is a legacy
|
|
266
|
+
// thread whose runs are still on disk and no longer addressable,
|
|
267
|
+
// with nothing in the result saying so — and the marker would be
|
|
268
|
+
// written as if the migration were complete.
|
|
269
|
+
if (!/^[a-z0-9]+$/.test(suffix)) {
|
|
270
|
+
throw new FilesystemMigrationError({
|
|
271
|
+
op: 'validate_thread_id',
|
|
272
|
+
path: join(threadsDir, legacyThreadId),
|
|
273
|
+
cause: new Error(
|
|
274
|
+
`Legacy thread folder '${legacyThreadId}' is not a thread id: expected 'thd_' followed by lowercase alphanumerics. It is refused rather than migrated because the name is used as a directory name.`,
|
|
275
|
+
),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
251
279
|
const newProjectId = `${LEGACY_DEFAULT_PROJECT_PREFIX}${suffix}` as ProjectId
|
|
252
280
|
|
|
253
281
|
const legacyRunsDir = join(threadsDir, legacyThreadId, 'runs')
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
|
|
24
24
|
import { appendFile, mkdir, readFile, readdir, rm } from 'node:fs/promises'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
|
-
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
26
|
+
import { StaleProjectError, StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
27
27
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
28
28
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
29
29
|
import type { Message } from '../../types/message/index.js'
|
|
30
|
-
import type { Project } from '../../types/project/entity.js'
|
|
30
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
31
31
|
import type { Session } from '../../types/session/entity.js'
|
|
32
32
|
import type { ProjectId, SubSessionId, SummaryId, ThreadId } from '../../types/session/ids.js'
|
|
33
33
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -80,6 +80,10 @@ interface PersistedProject {
|
|
|
80
80
|
tenantId: TenantId
|
|
81
81
|
name: string
|
|
82
82
|
config: Project['config']
|
|
83
|
+
/** Absent in files written before the workspace gained a status. */
|
|
84
|
+
status?: ProjectStatus
|
|
85
|
+
/** Absent in files written before the workspace gained a CAS counter. */
|
|
86
|
+
ownerVersion?: number
|
|
83
87
|
createdAt: string
|
|
84
88
|
updatedAt: string
|
|
85
89
|
}
|
|
@@ -198,6 +202,8 @@ export class DiskSessionStore implements SessionStore {
|
|
|
198
202
|
maxDelegationWidth: params.config?.maxDelegationWidth ?? 8,
|
|
199
203
|
maxInterventionDepth: 10,
|
|
200
204
|
},
|
|
205
|
+
status: 'open',
|
|
206
|
+
ownerVersion: 0,
|
|
201
207
|
createdAt: now,
|
|
202
208
|
updatedAt: now,
|
|
203
209
|
}
|
|
@@ -245,6 +251,35 @@ export class DiskSessionStore implements SessionStore {
|
|
|
245
251
|
return project
|
|
246
252
|
}
|
|
247
253
|
|
|
254
|
+
async setProjectStatus(
|
|
255
|
+
projectId: ProjectId,
|
|
256
|
+
status: ProjectStatus,
|
|
257
|
+
tenantId: TenantId,
|
|
258
|
+
expectedOwnerVersion: number,
|
|
259
|
+
): Promise<Project | null> {
|
|
260
|
+
const existing = await this.getProject(projectId, tenantId)
|
|
261
|
+
if (!existing) return null
|
|
262
|
+
// Against the version on disk, not the caller's copy of it.
|
|
263
|
+
if (existing.ownerVersion !== expectedOwnerVersion) {
|
|
264
|
+
throw new StaleProjectError({
|
|
265
|
+
projectId,
|
|
266
|
+
expectedOwnerVersion,
|
|
267
|
+
actualOwnerVersion: existing.ownerVersion,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
const project: Project = {
|
|
271
|
+
...existing,
|
|
272
|
+
status,
|
|
273
|
+
ownerVersion: existing.ownerVersion + 1,
|
|
274
|
+
updatedAt: new Date(),
|
|
275
|
+
}
|
|
276
|
+
await atomicWriteJson(
|
|
277
|
+
join(this.projectDir(projectId), 'project.json'),
|
|
278
|
+
serializeProject(project),
|
|
279
|
+
)
|
|
280
|
+
return project
|
|
281
|
+
}
|
|
282
|
+
|
|
248
283
|
async listProjects(tenantId: TenantId): Promise<readonly Project[]> {
|
|
249
284
|
// Read from the directory rather than the lazily-built index: the index
|
|
250
285
|
// only knows about projects this instance has already touched, so a
|
|
@@ -369,6 +404,40 @@ export class DiskSessionStore implements SessionStore {
|
|
|
369
404
|
return results
|
|
370
405
|
}
|
|
371
406
|
|
|
407
|
+
async listSessionsByProject(
|
|
408
|
+
projectId: ProjectId,
|
|
409
|
+
tenantId: TenantId,
|
|
410
|
+
): Promise<readonly Session[]> {
|
|
411
|
+
// One directory, not the whole root: sessions live under their project,
|
|
412
|
+
// so this is the cheap direction. `listSessions` has to scan every
|
|
413
|
+
// project precisely because `threadId` is denormalised onto the record
|
|
414
|
+
// rather than expressed in the layout.
|
|
415
|
+
const sessionsRoot = join(this.projectDir(projectId), 'sessions')
|
|
416
|
+
let sessionDirs: string[]
|
|
417
|
+
try {
|
|
418
|
+
sessionDirs = await readdir(sessionsRoot)
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
421
|
+
if (code === 'ENOENT') return []
|
|
422
|
+
throw err
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const results: Session[] = []
|
|
426
|
+
for (const rawSessionId of sessionDirs) {
|
|
427
|
+
if (!rawSessionId.startsWith('ses_')) continue
|
|
428
|
+
const path = join(sessionsRoot, rawSessionId)
|
|
429
|
+
const raw = await readJson<PersistedSession>(join(path, 'session.json'))
|
|
430
|
+
if (!raw) continue
|
|
431
|
+
if (raw.tenantId !== tenantId) continue
|
|
432
|
+
results.push(deserializeSession(raw))
|
|
433
|
+
this.sessionIndex.set(raw.id, { sessionId: raw.id, projectId, path })
|
|
434
|
+
}
|
|
435
|
+
results.sort(
|
|
436
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id),
|
|
437
|
+
)
|
|
438
|
+
return results
|
|
439
|
+
}
|
|
440
|
+
|
|
372
441
|
async updateSession(
|
|
373
442
|
session: Session,
|
|
374
443
|
tenantId: TenantId,
|
|
@@ -910,6 +979,8 @@ function serializeProject(p: Project): PersistedProject {
|
|
|
910
979
|
tenantId: p.tenantId,
|
|
911
980
|
name: p.name,
|
|
912
981
|
config: p.config,
|
|
982
|
+
status: p.status,
|
|
983
|
+
ownerVersion: p.ownerVersion,
|
|
913
984
|
createdAt: p.createdAt.toISOString(),
|
|
914
985
|
updatedAt: p.updatedAt.toISOString(),
|
|
915
986
|
}
|
|
@@ -921,6 +992,12 @@ function deserializeProject(p: PersistedProject): Project {
|
|
|
921
992
|
tenantId: p.tenantId,
|
|
922
993
|
name: p.name,
|
|
923
994
|
config: p.config,
|
|
995
|
+
// A project.json written before these fields existed reads as an open
|
|
996
|
+
// workspace at version 0, which is what it was. Leaving `ownerVersion`
|
|
997
|
+
// undefined would be worse than a wrong default: every compare-and-set
|
|
998
|
+
// against it would fail, so an existing store could never be closed.
|
|
999
|
+
status: p.status ?? 'open',
|
|
1000
|
+
ownerVersion: p.ownerVersion ?? 0,
|
|
924
1001
|
createdAt: new Date(p.createdAt),
|
|
925
1002
|
updatedAt: new Date(p.updatedAt),
|
|
926
1003
|
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* (Convention #5 deny-by-default, session-hierarchy.md §12.2).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
11
|
+
import { StaleProjectError, StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
12
12
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
13
13
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
14
14
|
import type { Message } from '../../types/message/index.js'
|
|
15
|
-
import type { Project } from '../../types/project/entity.js'
|
|
15
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
16
16
|
import type { Session } from '../../types/session/entity.js'
|
|
17
17
|
import type { ProjectId, SubSessionId, ThreadId } from '../../types/session/ids.js'
|
|
18
18
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -93,6 +93,8 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
93
93
|
maxDelegationWidth: params.config?.maxDelegationWidth ?? 8,
|
|
94
94
|
maxInterventionDepth: 10,
|
|
95
95
|
},
|
|
96
|
+
status: 'open',
|
|
97
|
+
ownerVersion: 0,
|
|
96
98
|
createdAt: now,
|
|
97
99
|
updatedAt: now,
|
|
98
100
|
}
|
|
@@ -134,6 +136,35 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
134
136
|
return project
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
async setProjectStatus(
|
|
140
|
+
projectId: ProjectId,
|
|
141
|
+
status: ProjectStatus,
|
|
142
|
+
tenantId: TenantId,
|
|
143
|
+
expectedOwnerVersion: number,
|
|
144
|
+
): Promise<Project | null> {
|
|
145
|
+
const record = this.projects.get(projectId)
|
|
146
|
+
if (!record) return null
|
|
147
|
+
this.assertTenant(record.tenantId, tenantId, `project(${projectId})`)
|
|
148
|
+
// Against the STORED version, not the caller's copy of it — comparing a
|
|
149
|
+
// value against itself is the shape the session CAS was written wrong in
|
|
150
|
+
// the first time.
|
|
151
|
+
if (record.project.ownerVersion !== expectedOwnerVersion) {
|
|
152
|
+
throw new StaleProjectError({
|
|
153
|
+
projectId,
|
|
154
|
+
expectedOwnerVersion,
|
|
155
|
+
actualOwnerVersion: record.project.ownerVersion,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
const project: Project = {
|
|
159
|
+
...record.project,
|
|
160
|
+
status,
|
|
161
|
+
ownerVersion: record.project.ownerVersion + 1,
|
|
162
|
+
updatedAt: new Date(),
|
|
163
|
+
}
|
|
164
|
+
this.projects.set(projectId, { tenantId, project })
|
|
165
|
+
return project
|
|
166
|
+
}
|
|
167
|
+
|
|
137
168
|
async listProjects(tenantId: TenantId): Promise<readonly Project[]> {
|
|
138
169
|
const matches: Project[] = []
|
|
139
170
|
for (const record of this.projects.values()) {
|
|
@@ -195,6 +226,22 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
195
226
|
return matches
|
|
196
227
|
}
|
|
197
228
|
|
|
229
|
+
async listSessionsByProject(
|
|
230
|
+
projectId: ProjectId,
|
|
231
|
+
tenantId: TenantId,
|
|
232
|
+
): Promise<readonly Session[]> {
|
|
233
|
+
const matches: Session[] = []
|
|
234
|
+
for (const record of this.sessions.values()) {
|
|
235
|
+
if (record.tenantId !== tenantId) continue
|
|
236
|
+
if (record.session.projectId !== projectId) continue
|
|
237
|
+
matches.push(record.session)
|
|
238
|
+
}
|
|
239
|
+
matches.sort(
|
|
240
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id),
|
|
241
|
+
)
|
|
242
|
+
return matches
|
|
243
|
+
}
|
|
244
|
+
|
|
198
245
|
async updateSession(
|
|
199
246
|
session: Session,
|
|
200
247
|
tenantId: TenantId,
|
|
@@ -27,15 +27,33 @@ export interface ProjectConfig {
|
|
|
27
27
|
retentionPolicy?: RetentionPolicy
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Whether the workspace accepts new work.
|
|
32
|
+
*
|
|
33
|
+
* Owner-managed, like the status it replaces: a project does not derive
|
|
34
|
+
* `archived` from having no live sessions, because "empty right now" and
|
|
35
|
+
* "closed" are different facts and only one of them is a decision.
|
|
36
|
+
*/
|
|
37
|
+
export type ProjectStatus = 'open' | 'archived'
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Long-lived goal scope that owns shared memory, vaults, knowledge bases,
|
|
32
41
|
* and deliverables across sessions. See session-hierarchy.md §4.2.
|
|
42
|
+
*
|
|
43
|
+
* `status` is the gate the ingress paths read: an archived project accepts no
|
|
44
|
+
* new session and no handoff. It lives here rather than on Thread because the
|
|
45
|
+
* project is the thing a tenant actually closes — a workspace with its own
|
|
46
|
+
* limits, its own environment, and its own memory — and closing it has to mean
|
|
47
|
+
* something to the code, not only to a listing.
|
|
33
48
|
*/
|
|
34
49
|
export interface Project {
|
|
35
50
|
id: ProjectId
|
|
36
51
|
tenantId: TenantId
|
|
37
52
|
name: string
|
|
38
53
|
config: ProjectConfig
|
|
54
|
+
status: ProjectStatus
|
|
55
|
+
/** CAS counter for status transitions. Mirrors `Session.ownerVersion`. */
|
|
56
|
+
ownerVersion: number
|
|
39
57
|
createdAt: Date
|
|
40
58
|
updatedAt: Date
|
|
41
59
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* API — extensions land alongside their consumers.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { Project } from '../../types/project/entity.js'
|
|
9
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
10
10
|
import type { ActorRef } from '../../types/session/actor.js'
|
|
11
11
|
import type { Session } from '../../types/session/entity.js'
|
|
12
12
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -159,6 +159,29 @@ export interface SessionStore {
|
|
|
159
159
|
*/
|
|
160
160
|
listProjects?(tenantId: TenantId): Promise<readonly Project[]>
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Open or close a workspace. OPTIONAL, same reasoning as the two above.
|
|
164
|
+
*
|
|
165
|
+
* Compare-and-set on {@link Project.ownerVersion}: pass the version you
|
|
166
|
+
* read, and a concurrent writer makes this throw `StaleProjectError`
|
|
167
|
+
* instead of silently winning. On success the stored version is bumped.
|
|
168
|
+
*
|
|
169
|
+
* Both directions, because a workspace is long-lived and closing one by
|
|
170
|
+
* mistake should not be permanent — unlike the Thread status this replaces,
|
|
171
|
+
* which only ever went one way. Returns `null` if the project does not
|
|
172
|
+
* exist; writing to another tenant's project throws.
|
|
173
|
+
*
|
|
174
|
+
* This is the store-level write. The precondition that no live session is
|
|
175
|
+
* attached belongs to {@link import('../../manager/project/lifecycle.js').ProjectManager},
|
|
176
|
+
* because the store deliberately holds no view of what is running.
|
|
177
|
+
*/
|
|
178
|
+
setProjectStatus?(
|
|
179
|
+
projectId: ProjectId,
|
|
180
|
+
status: ProjectStatus,
|
|
181
|
+
tenantId: TenantId,
|
|
182
|
+
expectedOwnerVersion: number,
|
|
183
|
+
): Promise<Project | null>
|
|
184
|
+
|
|
162
185
|
// Session CRUD ------------------------------------------------------------
|
|
163
186
|
|
|
164
187
|
createSession(params: CreateSessionParams, tenantId: TenantId): Promise<Session>
|
|
@@ -213,6 +236,17 @@ export interface SessionStore {
|
|
|
213
236
|
*/
|
|
214
237
|
listSessions(threadId: ThreadId, tenantId: TenantId): Promise<readonly Session[]>
|
|
215
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Every Session attached to a workspace, oldest first. OPTIONAL.
|
|
241
|
+
*
|
|
242
|
+
* The project-scoped sibling of {@link SessionStore.listSessions}, and the
|
|
243
|
+
* one the archive precondition reads: closing a workspace has to know what
|
|
244
|
+
* is still running in it, and "what is running in this thread" was never
|
|
245
|
+
* the question — a project can hold sessions across many threads, and after
|
|
246
|
+
* the Thread level is removed it is the only grouping left.
|
|
247
|
+
*/
|
|
248
|
+
listSessionsByProject?(projectId: ProjectId, tenantId: TenantId): Promise<readonly Session[]>
|
|
249
|
+
|
|
216
250
|
/**
|
|
217
251
|
* Hard-delete a session. Idempotent — absent sessions succeed as a no-op.
|
|
218
252
|
* Rejects with `TenantIsolationError` on cross-tenant access.
|