Merge origin/master (goal line #527) into web-permission-sandbox-merge-master

Shared-surface conflicts resolve as unions: the fixture serves all four
projection keys (title/todos/permissions/goal) with both the /permission
and /goal command mirrors (the goal-fixture placeholder retires with
master), apps/cli carries both lines' dependency additions, and the README
Model Experience allowlist keeps both entries. The connection specs assert
the four-key baseline and the shifted approval/question replay indices;
the module graph regenerates over the merged dependency set.
This commit is contained in:
imccyu
2026-07-29 02:27:11 +08:00
69 changed files with 2240 additions and 177 deletions

View File

@@ -24,7 +24,7 @@ import {
// Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters).
import type {} from '@deepseek-ai/dsh-tools'
import type {
ApiProxy, HistoryEntry, HostFrame, ModelCatalogFailure, ModelProviderGroup, ModelReasoning,
ApiProxy, GoalRef, HistoryEntry, HostFrame, ModelCatalogFailure, ModelProviderGroup, ModelReasoning,
MuxFrame, QuestionResponsePayload, SessionProjectionsBlock, SessionSummary, ToolEventView,
WorkspaceId, WorkspaceView,
} from './api/index.ts'
@@ -32,6 +32,9 @@ import type {
import type {} from '@deepseek-ai/dsh-session-projection'
// Type-only: resolves `ctx.get('sessionProjectionCache')` (the cold listing column).
import type {} from '@deepseek-ai/dsh-session-projection-cache'
// GoalError narrows domain rejections to their stable codes at the wire boundary.
import { GoalError } from '@deepseek-ai/dsh-goal'
import type { GoalRef as CoreGoalRef } from '@deepseek-ai/dsh-goal'
// Type-only edges: resolve `ctx.get('commands')`, the `commands/change` event, and `ctx.get('skills')`.
import type {} from '@deepseek-ai/dsh-commands'
import type {} from '@deepseek-ai/dsh-skill'
@@ -773,6 +776,38 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
return operation
}
/** Resolve the goal service; absent = the deployment did not compose @deepseek-ai/dsh-goal. */
function goalService(): NonNullable<ReturnType<typeof ctx.get<'goals'>>> | { error: RpcError } {
const goals = ctx.get('goals')
if (goals === undefined) {
return { error: { code: 'internal', message: 'goal service is absent: this deployment does not mount @deepseek-ai/dsh-goal in its composition (cordis.yml or explicit assembly)', details: {} } }
}
return goals
}
/** Map one goal-domain rejection to the wire error (stable GoalError codes ride in details). */
function goalError(request: RpcRequest<unknown>, error: unknown): RpcResponse<never> {
const details = error instanceof GoalError ? { goalCode: error.code } : {}
return err(request, { code: 'internal', message: String(error), details })
}
/** Resolve a session's agent, apply one goal mutation, and acknowledge with the new CAS ref. */
async function mutateGoal(
request: RpcRequest<{ sessionId: SessionId }>,
mutation: (goals: NonNullable<ReturnType<typeof ctx.get<'goals'>>>, agent: Agent) => CoreGoalRef,
): Promise<RpcResponse<{ ref: GoalRef }>> {
const goals = goalService()
if ('error' in goals) return err(request, goals.error)
const found = await agentFor(request.payload.sessionId)
if ('error' in found) return err(request, found.error)
try {
const ref = mutation(goals, found.agent)
return ok(request, { ref: { id: ref.id, revision: ref.revision } })
} catch (error: unknown) {
return goalError(request, error)
}
}
return {
sessions: {
// Attached sessions summarize from memory; persisted-but-unattached (cold)
@@ -1230,6 +1265,54 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
},
},
goals: {
// Mutations only — the read side is the 'goal' session projection.
// Every verb resolves the session's agent (agentFor: implicit cold
// resume, the command.* precedent) and acknowledges with the new CAS
// ref; the committed goal/change event carries the whole value to every
// client through the projection frames.
async create(request) {
const { objective, maxGoalRounds } = request.payload
return mutateGoal(request, (goals, agent) => goals.create(agent, {
objective,
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
}))
},
async edit(request) {
const { ref, objective, maxGoalRounds } = request.payload
return mutateGoal(request, (goals, agent) => goals.edit(agent, ref, {
...(objective !== undefined ? { objective } : {}),
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
}))
},
async pause(request) {
return mutateGoal(request, (goals, agent) => goals.pause(agent, request.payload.ref))
},
async resume(request) {
return mutateGoal(request, (goals, agent) => goals.resume(agent, request.payload.ref))
},
async complete(request) {
return mutateGoal(request, (goals, agent) => goals.complete(agent, request.payload.ref))
},
async clear(request) {
const goals = goalService()
if ('error' in goals) return err(request, goals.error)
const found = await agentFor(request.payload.sessionId)
if ('error' in found) return err(request, found.error)
try {
goals.clear(found.agent, request.payload.ref)
return ok(request, { cleared: true as const })
} catch (error: unknown) {
return goalError(request, error)
}
},
},
skills: {
// Skill lookup never touches the Agent registry: the session address
// resolves to a canonical cwd from the host-resident session header, so