refactor: retire the session/title frame and the todos history rider from the wire
This commit is contained in:
@@ -10,9 +10,8 @@ import type { Context } from 'cordis'
|
||||
import type { Agent, AgentMessage, AgentMessageId, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue, Session, SessionEvent, SessionHeader, SessionId, TodoItem } from '@deepseek-ai/dsh-session'
|
||||
import type { JsonValue, Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
|
||||
import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
|
||||
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
workspaceDomainState, workspaceRecord, WorkspaceId as brandWorkspaceId,
|
||||
@@ -126,26 +125,9 @@ function frame<F>(payload: F): RpcRequest<F> {
|
||||
return { rpcId: RpcId(randomUUID()), payload }
|
||||
}
|
||||
|
||||
type SessionTitleFrame = Extract<MuxFrame, { type: 'session/title' }>
|
||||
|
||||
/** Project the latest durable title without exposing title-generation policy. */
|
||||
function titleFrame(session: Session): SessionTitleFrame | undefined {
|
||||
const title = foldSessionTitle(session.events)
|
||||
if (title === undefined) return undefined
|
||||
return {
|
||||
type: 'session/title',
|
||||
sessionId: session.id,
|
||||
title: title.title,
|
||||
eventSeq: title.eventSeq,
|
||||
updatedAt: title.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
/** Queue the subscription baseline followed by its optional title snapshot. */
|
||||
/** Queue the subscription baseline frame. */
|
||||
function subscribeSession(queue: FrameQueue<RpcRequest<MuxFrame>>, session: Session): void {
|
||||
queue.push(frame({ type: 'session/subscribed', sessionId: session.id, lastSeq: session.seq - 1 }))
|
||||
const title = titleFrame(session)
|
||||
if (title !== undefined) queue.push(frame(title))
|
||||
}
|
||||
|
||||
/** SessionSummary projection for attached (in-memory) sessions. */
|
||||
@@ -289,15 +271,6 @@ function backscanArgs(events: readonly SessionEvent[], callId: string): { name:
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Current todo projection: the latest `todo/write` over the full log (whole-list replace ⇒ last write wins); undefined when none. */
|
||||
function backscanTodos(events: readonly SessionEvent[]): TodoItem[] | undefined {
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const event = events[i]
|
||||
if (event !== undefined && event.type === 'todo/write') return event.data.todos
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The projection baseline for one history tail page: the registry's
|
||||
* watermark-cache snapshot — one fully synchronous read (no await between the
|
||||
@@ -694,18 +667,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const view = viewFor(ctx, event, callId => backscanArgs(page.events, callId))
|
||||
return { event, ...view === undefined ? {} : { view } }
|
||||
})
|
||||
// Tail page carries the session-level todo projection over the FULL
|
||||
// log (the page window may not contain the last todo/write; a paged
|
||||
// client cannot reconstruct session-level state from it).
|
||||
// TODO(gui): retire this rider onto the generic projections block.
|
||||
const todos = beforeSeq === undefined ? backscanTodos(found.agent.session.events) : undefined
|
||||
// Baseline rider: tail page only — loadOlder (beforeSeq present) is
|
||||
// the one path that never needs a fresh projection baseline.
|
||||
const projections = beforeSeq === undefined ? projectionsFor(ctx, found.agent) : undefined
|
||||
return ok(request, {
|
||||
events: entries,
|
||||
hasMore: page.hasMore,
|
||||
...todos === undefined ? {} : { todos },
|
||||
...projections === undefined ? {} : { projections },
|
||||
})
|
||||
},
|
||||
@@ -1033,10 +1000,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const view = viewFor(ctx, event, callId =>
|
||||
openCalls.get(session.id)?.get(callId) ?? backscanArgs(session.events, callId))
|
||||
queue.push(frame({ type: 'session/event', sessionId: session.id, event, ...view === undefined ? {} : { view } }))
|
||||
if (event.type === 'session/title') {
|
||||
// The accepted raw event is already in session.events, so the fold must find it.
|
||||
queue.push(frame(titleFrame(session) as SessionTitleFrame))
|
||||
}
|
||||
}),
|
||||
ctx.on('session/created', (session: Session) => {
|
||||
subscribeSession(queue, session)
|
||||
|
||||
@@ -27,7 +27,6 @@ export const askUserQuestionItemSchema = z.object({
|
||||
export const muxFrameSchema = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('session/event'), sessionId: sessionIdSchema, event: sessionEventSchema, view: toolEventViewSchema.optional() }),
|
||||
z.object({ type: z.literal('session/subscribed'), sessionId: sessionIdSchema, lastSeq: z.number().int() }),
|
||||
z.object({ type: z.literal('session/title'), sessionId: sessionIdSchema, title: z.string().min(1), eventSeq: z.number().int().nonnegative(), updatedAt: z.number() }),
|
||||
z.object({ type: z.literal('approval/requested'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, toolName: z.string(), callId: z.string().optional(), reason: z.string().optional() }),
|
||||
z.object({ type: z.literal('approval/resolved'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, outcome: z.union([z.literal('allowed-once'), z.literal('rejected'), z.literal('cancelled'), z.literal('unavailable')]) }),
|
||||
// Non-empty by wire contract: the user-interaction service rejects empty
|
||||
|
||||
@@ -35,9 +35,9 @@ export type ToolEventView =
|
||||
export interface EventsApi {
|
||||
/**
|
||||
* All-session aggregated mux stream. On open, emits a subscribed control frame for every
|
||||
* attached session followed by its optional latest title snapshot, then replays each
|
||||
* session's still-pending approval/question requested frames (rpcId reused verbatim — the
|
||||
* refresh-recovery baseline).
|
||||
* attached session, then replays each session's still-pending approval/question requested
|
||||
* frames (rpcId reused verbatim — the refresh-recovery baseline). Session titles ride the
|
||||
* generic projection pair (history-tail projections block + session/projection frames).
|
||||
* since: resume seam, unimplemented in v1 (ignored if passed); reconnection = reopen the
|
||||
* stream + refetch history.
|
||||
*/
|
||||
@@ -57,7 +57,6 @@ export interface EventsApi {
|
||||
export type MuxFrame =
|
||||
| { type: 'session/event'; sessionId: SessionId; event: SessionEvent; view?: ToolEventView }
|
||||
| { type: 'session/subscribed'; sessionId: SessionId; lastSeq: number }
|
||||
| { type: 'session/title'; sessionId: SessionId; title: string; eventSeq: number; updatedAt: number }
|
||||
| { type: 'approval/requested'; sessionId: SessionId; approvalId: ApprovalRequestId; toolName: string; callId?: CallId; reason?: string }
|
||||
| { type: 'approval/resolved'; sessionId: SessionId; approvalId: ApprovalRequestId; outcome: ApprovalOutcome }
|
||||
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
|
||||
|
||||
@@ -93,12 +93,6 @@ export const historyEntrySchema = z.object({
|
||||
view: toolEventViewSchema.optional(),
|
||||
}) satisfies z.ZodType<Wire<HistoryEntry>>
|
||||
|
||||
/** One todo item of the tail page's session-level projection (the todo/write payload shape). */
|
||||
export const todoItemSchema = z.object({
|
||||
content: z.string(),
|
||||
status: z.union([z.literal('pending'), z.literal('in_progress'), z.literal('completed')]),
|
||||
})
|
||||
|
||||
/**
|
||||
* Projection baseline passthrough: `values` stays a wide record — each value
|
||||
* was already parsed by its provider's own schema on the host side, and
|
||||
@@ -110,11 +104,10 @@ export const sessionProjectionsBlockSchema = z.object({
|
||||
values: z.record(z.string(), z.unknown()),
|
||||
}) as unknown as z.ZodType<SessionProjectionsBlock>
|
||||
|
||||
/** session.history response value (todos and projections ride the tail page only). */
|
||||
/** session.history response value (projections rides the tail page only). */
|
||||
export const sessionHistoryValueSchema = z.object({
|
||||
events: z.array(historyEntrySchema),
|
||||
hasMore: z.boolean(),
|
||||
todos: z.array(todoItemSchema).optional(),
|
||||
projections: sessionProjectionsBlockSchema.optional(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.history'>>>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { SessionEvent, SessionId, TodoItem } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
// The pure-type outlet: api/ is browser-importable, and the package root's
|
||||
// cordis Context merge (via dsh-agent) must not enter client aggregates.
|
||||
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
|
||||
@@ -97,11 +97,6 @@ export interface SessionsApi {
|
||||
* Each entry pairs the raw SessionEvent with the host-computed view (tool events whose
|
||||
* presenter produced one, evaluated against the registry at pagination time); the client
|
||||
* rebuilds the surface from the events with the shared fold.
|
||||
* The tail page (beforeSeq absent) also carries `todos` — the session's current todo
|
||||
* projection (latest `todo/write` over the FULL log, independent of the page window) —
|
||||
* so a paged client restores the plan without walking history; absent when the session
|
||||
* never wrote one. Older pages omit it (the projection is session-level, not per-page).
|
||||
* TODO(gui): the todos rider retires onto the generic projections block below.
|
||||
* The tail page — and only the tail page — additionally carries `projections`
|
||||
* when the deployment mounts the session-projection registry: every moment
|
||||
* the client needs a fresh baseline already pulls the tail page, and
|
||||
@@ -109,7 +104,7 @@ export interface SessionsApi {
|
||||
* A deployment without the registry serves histories without the block.
|
||||
*/
|
||||
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
|
||||
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean; todos?: TodoItem[]; projections?: SessionProjectionsBlock }>>
|
||||
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean; projections?: SessionProjectionsBlock }>>
|
||||
|
||||
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
|
||||
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
|
||||
|
||||
Reference in New Issue
Block a user