feat(web): re-land the goals wire domain as mutation-only ref acknowledgements

Six mutation RPCs (create/edit/pause/resume/complete/clear) move into
dsh-host-apiproxy (the PR's host/runtime carrier is gone): goalService()
resolves ctx.get('goals') with a loud absence error, mutateGoal() resolves
the session's agent (agentFor, the command.* implicit-resume precedent) and
acknowledges with the new CAS ref only. GoalError codes ride err.details.

goal.get and the wire GoalView/goalViewSchema are gone: the read side is the
'goal' session projection (whole values on the history tail page and
session/projection frames), so responses never feed client state — the rule
whose absence forced the original PR's write-revision fences.
This commit is contained in:
imccyu
2026-07-28 21:05:43 +08:00
parent cbab62bdea
commit 1c2923c84f
13 changed files with 149 additions and 146 deletions

View File

@@ -1,10 +1,12 @@
/**
* goals domain zod schemas.
* goals domain zod schemas. Mutation-only shapes: every value schema is a
* `{ ref }` acknowledgement (clear: `{ cleared }`) — the current goal state
* travels exclusively on the 'goal' session projection.
*/
import { z } from 'zod'
import type { Wire } from './rpc.schema.ts'
import type { GoalRef, GoalView, RequestPayload, ResponseValue } from './index.ts'
import type { GoalRef, RequestPayload, ResponseValue } from './index.ts'
/** GoalRef schema. */
export const goalRefSchema = z.object({
@@ -12,25 +14,8 @@ export const goalRefSchema = z.object({
revision: z.number().int().positive(),
}) as unknown as z.ZodType<Wire<GoalRef>>
/** Goal block reason schema. */
export const goalBlockReasonSchema = z.object({
code: z.string(),
message: z.string(),
})
/** GoalView schema. */
export const goalViewSchema = z.object({
id: z.string(),
revision: z.number().int().positive(),
objective: z.string(),
phase: z.union([z.literal('active'), z.literal('paused'), z.literal('blocked'), z.literal('complete')]),
blockedReason: goalBlockReasonSchema.optional(),
maxGoalRounds: z.number().int().positive(),
roundsStarted: z.number().int().nonnegative(),
createdAt: z.number(),
updatedAt: z.number(),
activation: z.union([z.literal('armed'), z.literal('disarmed')]),
}) as unknown as z.ZodType<Wire<GoalView>>
/** Shared `{ ref }` acknowledgement value of every non-clear mutation. */
const goalRefValueSchema = z.object({ ref: goalRefSchema })
/** goal.create request payload. */
export const goalCreateRequestSchema = z.object({
@@ -40,9 +25,7 @@ export const goalCreateRequestSchema = z.object({
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.create'>>>
/** goal.create response value. */
export const goalCreateValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.create'>>>
export const goalCreateValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.create'>>>
/** goal.edit request payload. */
export const goalEditRequestSchema = z.object({
@@ -55,9 +38,7 @@ export const goalEditRequestSchema = z.object({
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.edit'>>>
/** goal.edit response value. */
export const goalEditValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.edit'>>>
export const goalEditValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.edit'>>>
/** goal.pause request payload. */
export const goalPauseRequestSchema = z.object({
@@ -66,9 +47,7 @@ export const goalPauseRequestSchema = z.object({
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.pause'>>>
/** goal.pause response value. */
export const goalPauseValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.pause'>>>
export const goalPauseValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.pause'>>>
/** goal.resume request payload. */
export const goalResumeRequestSchema = z.object({
@@ -77,9 +56,7 @@ export const goalResumeRequestSchema = z.object({
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.resume'>>>
/** goal.resume response value. */
export const goalResumeValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.resume'>>>
export const goalResumeValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.resume'>>>
/** goal.complete request payload. */
export const goalCompleteRequestSchema = z.object({
@@ -88,9 +65,7 @@ export const goalCompleteRequestSchema = z.object({
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.complete'>>>
/** goal.complete response value. */
export const goalCompleteValueSchema = z.object({
goal: goalViewSchema,
}) as unknown as z.ZodType<Wire<ResponseValue<'goal.complete'>>>
export const goalCompleteValueSchema = goalRefValueSchema as unknown as z.ZodType<Wire<ResponseValue<'goal.complete'>>>
/** goal.clear request payload. */
export const goalClearRequestSchema = z.object({

View File

@@ -1,6 +1,12 @@
/**
* goals domain contract. Method signatures are the source of truth:
* unary methods take the RpcRequest<P> narrow form and the impl echoes rpcId.
*
* Mutations only: the read side is the 'goal' session projection (history
* tail-page projections block + session/projection frames), so there is no
* goal.get and no wire goal view — responses acknowledge with the new CAS
* ref and never feed client state (the committed goal/change event reaches
* every client through the mux stream carrying the same whole value).
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
@@ -16,69 +22,27 @@ export interface GoalRef {
readonly revision: number
}
/** Durable continuation phase. */
export type GoalPhase =
| 'active'
| 'paused'
| 'blocked'
| 'complete'
/** Machine-routable and human-readable explanation for a blocked goal. */
export interface GoalBlockReason {
readonly code: string
readonly message: string
}
/** Whether this live process may automatically continue an active goal. */
export type GoalActivation = 'armed' | 'disarmed'
/** Current goal projection, including values derived from the session log. */
export interface GoalView {
readonly id: GoalId
readonly revision: number
readonly objective: string
readonly phase: GoalPhase
readonly blockedReason?: GoalBlockReason
readonly maxGoalRounds: number
readonly roundsStarted: number
readonly createdAt: number
readonly updatedAt: number
readonly activation: GoalActivation
}
/** Input whose omitted round cap is resolved by the service configuration. */
export interface CreateGoalRequest {
readonly objective: string
readonly maxGoalRounds?: number
}
/** Fields changed by an edit; at least one must be present. */
export interface EditGoalRequest {
readonly objective?: string
readonly maxGoalRounds?: number
}
/** Goal-domain unary methods (mutations only: the read side is the 'goal' session projection). */
/** Goal-domain unary methods (every mutation resolves the session's agent and applies one CAS-guarded verb). */
export interface GoalsApi {
/** Create and arm a goal. */
create(request: RpcRequest<{ sessionId: SessionId; objective: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ goal: GoalView }>>
Promise<RpcResponse<{ ref: GoalRef }>>
/** Edit objective and/or round cap without changing phase. */
edit(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef; objective?: string; maxGoalRounds?: number }>):
Promise<RpcResponse<{ goal: GoalView }>>
Promise<RpcResponse<{ ref: GoalRef }>>
/** Pause an active goal and disarm automatic continuation. */
pause(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
Promise<RpcResponse<{ ref: GoalRef }>>
/** Resume and arm a stopped goal. */
resume(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
Promise<RpcResponse<{ ref: GoalRef }>>
/** Mark a current non-complete goal complete and disarm it. */
complete(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
Promise<RpcResponse<{ goal: GoalView }>>
Promise<RpcResponse<{ ref: GoalRef }>>
/** Clear the current goal while retaining a durable tombstone and history. */
clear(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):

View File

@@ -36,7 +36,7 @@ export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
export type { CommandsApi, CommandDescriptor } from './commands.ts'
export type { SkillsApi, SkillEntry } from './skills.ts'
export type { EventsApi, MuxFrame, HostFrame, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
export type { GoalsApi, GoalView, GoalRef, GoalPhase, GoalBlockReason, CreateGoalRequest, EditGoalRequest } from './goals.ts'
export type { GoalsApi, GoalId, GoalRef } from './goals.ts'
export type { ApprovalResponsePayload } from './approvals.ts'
export type { QuestionResponsePayload } from './questions.ts'