Files
deepseek-harness/packages/api/workspace-controller/src/commands.ts
T

197 lines
6.6 KiB
TypeScript

/** Workspace command implementation and stable Remote failure mapping. */
import type { Context } from '@deepseek-ai/cordis'
import type { Workspace } from '@deepseek-ai/dsh-workspace'
import {
WorkspaceId,
WorkspaceMoveInvalidError,
WorkspaceOrderInvalidError,
WorkspaceUnknownSessionError,
} from '@deepseek-ai/dsh-workspace'
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import { workspaceView } from './feed.ts'
import type {
WorkspaceArchiveSessionRequest,
WorkspaceArchiveValue,
WorkspaceCreateRequest,
WorkspaceCreateValue,
WorkspaceDeleteRequest,
WorkspaceDeleteValue,
WorkspaceInsertBeforeRequest,
WorkspaceInsertSessionBeforeRequest,
WorkspaceOrderValue,
WorkspaceRenameRequest,
WorkspaceValue,
} from './types.ts'
/** Implements Workspace mutations against the authoritative registry. */
export class WorkspaceCommands {
private operationTail = Promise.resolve()
/** @param ctx - Host context containing the Workspace registry. */
constructor(private readonly ctx: Context) {}
/**
* Create or resolve one Workspace over an existing directory.
* @param request - directory path to register.
* @returns the Workspace and whether this call created it.
*/
create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue> {
return this.enqueue(async () => {
try {
const existing = await this.ctx.workspaceRegistry.resolveByPath(request.path)
if (existing !== undefined) {
return { workspace: workspaceView(existing), created: false }
}
const workspace = await this.ctx.workspaceRegistry.create(request.path)
return { workspace: workspaceView(workspace), created: true }
} catch (error) {
if (error instanceof TypertRemoteFailure) throw error
throw failure(
'workspace-invalid-path',
`cannot create a Workspace at "${request.path}": ${errorMessage(error)}`,
{ path: request.path },
)
}
})
}
/**
* Rename one Workspace after serializing title ownership checks.
* @param request - Workspace identity and proposed title.
* @returns the updated Workspace projection.
*/
rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue> {
const title = request.title.trim()
if (title === '') {
return Promise.reject(failure(
'bad-request',
'Workspace rename requires a non-blank title',
{},
))
}
return this.enqueue(async () => {
const workspace = this.requireWorkspace(request.workspaceId)
if (title !== workspace.title) {
if (this.ctx.workspaceRegistry.list().some(candidate =>
candidate.id !== workspace.id && candidate.title === title)) {
throw failure(
'workspace-name-conflict',
`Workspace name '${title}' is already in use`,
{ name: title },
)
}
await workspace.setTitle(title)
}
return { workspace: workspaceView(workspace) }
})
}
/**
* Delete one Workspace registration without deleting its directory or Sessions.
* @param request - Workspace identity to remove.
* @returns deletion confirmation.
*/
delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue> {
return this.enqueue(async () => {
if (!await this.ctx.workspaceRegistry.delete(WorkspaceId(request.workspaceId))) {
throw workspaceNotFound(request.workspaceId)
}
return { deleted: true }
})
}
/**
* Move one Workspace within the durable registry order.
* @param request - moved Workspace and optional anchor.
* @returns the complete resulting Workspace order.
*/
async insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue> {
try {
const workspaceIds = await this.ctx.workspaceRegistry.insertBefore(
WorkspaceId(request.workspaceId),
request.beforeWorkspaceId === undefined
? undefined
: WorkspaceId(request.beforeWorkspaceId),
)
return { workspaceIds: [...workspaceIds] }
} catch (error) {
if (!(error instanceof WorkspaceOrderInvalidError)) throw error
throw workspaceNotFound(error.workspaceId)
}
}
/**
* Move one accounted Session within a Workspace's manual order.
* @param request - Workspace, Session, and optional anchor identities.
* @returns the updated Workspace projection.
*/
async insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue> {
const workspace = this.requireWorkspace(request.workspaceId)
try {
await workspace.insertSessionBefore(request.sessionId, request.beforeSessionId)
} catch (error) {
if (!(error instanceof WorkspaceMoveInvalidError)) throw error
throw failure(
'workspace-move-invalid',
error.message,
{
workspaceId: request.workspaceId,
sessionId: request.sessionId,
...request.beforeSessionId === undefined
? {}
: { beforeSessionId: request.beforeSessionId },
},
)
}
return { workspace: workspaceView(workspace) }
}
/**
* Add one known Session to the registry-global archive set.
* @param request - Session identity to archive.
* @returns the complete resulting archive set.
*/
async archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue> {
try {
await this.ctx.workspaceRegistry.archiveSession(request.sessionId)
} catch (error) {
if (!(error instanceof WorkspaceUnknownSessionError)) throw error
throw failure('session-not-found', error.message, { sessionId: request.sessionId })
}
return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] }
}
private requireWorkspace(workspaceId: WorkspaceId): Workspace {
const workspace = this.ctx.workspaceRegistry.get(WorkspaceId(workspaceId))
if (workspace === undefined) throw workspaceNotFound(workspaceId)
return workspace
}
private enqueue<T>(operation: () => Promise<T>): Promise<T> {
const result = this.operationTail.then(operation)
this.operationTail = result.then(() => undefined, () => undefined)
return result
}
}
function workspaceNotFound(workspaceId: WorkspaceId): TypertRemoteFailure {
return failure(
'workspace-not-found',
`Workspace "${workspaceId}" not found`,
{ workspaceId },
)
}
function failure(
code: string,
message: string,
details: object,
): TypertRemoteFailure {
return new TypertRemoteFailure({ code, message, details })
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}