feat(apiproxy): workspace.archiveSession RPC and archived-sessions frame

workspace.archiveSession answers the full updated archive set;
workspace.list carries the set as the reconnect baseline; the host
stream pushes host/archived-sessions-changed full snapshots from the
domain/changed global-put branch (same posture as workspace-changed).
Unknown sessions map to the existing session-not-found code.
This commit is contained in:
imccyu
2026-07-31 02:40:03 +08:00
committed by imccyu
parent b00cd0cd8c
commit 53ceab53b8
15 changed files with 139 additions and 13 deletions

View File

@@ -21,7 +21,7 @@ import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-se
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
import {
workspaceDomainState, workspaceRecord, WorkspaceId as brandWorkspaceId,
WorkspaceMoveInvalidError, WorkspaceNameConflictError,
WorkspaceMoveInvalidError, WorkspaceNameConflictError, WorkspaceUnknownSessionError,
} from '@deepseek-ai/dsh-workspace'
// Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters).
import type {} from '@deepseek-ai/dsh-tools'
@@ -1582,7 +1582,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
workspace: {
list(request) {
return Promise.resolve(ok(request, { items: ctx.workspace.list().map(workspaceView) }))
return Promise.resolve(ok(request, {
items: ctx.workspace.list().map(workspaceView),
archivedSessionIds: [...ctx.workspace.archivedSessionIds],
}))
},
// Exactly one of path/name arrives (schema refine). Existing-folder
@@ -1698,6 +1701,23 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
}
return ok(request, { workspace: workspaceView(workspace) })
},
async archiveSession(request) {
const { sessionId } = request.payload
try {
await ctx.workspace.archiveSession(sessionId)
} catch (error: unknown) {
// Only the registry's unknown-session rejection is the business
// code; storage/durability failures propagate as internal errors.
if (!(error instanceof WorkspaceUnknownSessionError)) throw error
return err(request, {
code: 'session-not-found',
message: error.message,
details: { sessionId },
})
}
return ok(request, { archivedSessionIds: [...ctx.workspace.archivedSessionIds] })
},
},
host: {
@@ -2109,6 +2129,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
const committedWorkspaceIds = new Set(
ctx.workspace.list().map(workspace => String(workspace.id)),
)
// Frame-dedup baseline, same posture as committedWorkspaceIds: the
// stream opens against the current set; workspace.list re-baselines
// reconnecting clients, so only later changes need frames.
let archivedSessionIds = ctx.workspace.archivedSessionIds
const disposers = [
ctx.on('session/created', (session: Session) => {
queue.push(frame({
@@ -2145,6 +2169,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
committedWorkspaceIds.add(workspaceId)
queue.push(frame({ type: 'host/workspace-changed', workspace: workspaceView(workspace) }))
}
if (state.archivedSessionIds.length !== archivedSessionIds.length
|| state.archivedSessionIds.some((id, index) => id !== archivedSessionIds[index])) {
archivedSessionIds = state.archivedSessionIds
queue.push(frame({
type: 'host/archived-sessions-changed',
archivedSessionIds: [...state.archivedSessionIds],
}))
}
return
}
if (change.table !== 'workspaces') return