fix(apiproxy,client): the list projection column becomes a seedable watermarked block

Review finding (PR #791): the column carried bare values (no seq), so the
client could not seed its value store without risking a stale list block
outranking newer push frames — and nothing consumed the column at all,
leaving cold titles absent after a restart. SessionSummary.projections is
now the same SessionProjectionsBlock as the history tail (values +
asOfSeq; attached rows cut the live registry, cold rows serve the cache's
identity-checked cachedSnapshot whose asOfSeq is the lowest served-row
watermark). SessionManager.refreshList seeds each row's block into the
per-session projection store via per-key apply — partial-baseline
semantics: an absent key never clears, and higher-seq-wins keeps stale
list blocks beneath push frames and tail baselines — so cold titles
surface in the sidebar without opening a session.
This commit is contained in:
imccyu
2026-07-28 12:10:04 +08:00
parent 27198d3091
commit c46419cf5c
6 changed files with 75 additions and 42 deletions
@@ -7,7 +7,6 @@
import { z } from 'zod'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import type {
@@ -38,16 +37,7 @@ export const sessionEventSchema = z.object({
surfaceOp: z.unknown().optional(),
}) as unknown as z.ZodType<SessionEvent>
/**
* Projection-values passthrough (same posture as
* {@link sessionProjectionsBlockSchema}): each value already passed its
* unit's own schema on the host side; deep-validating here would import
* every domain's schema into the carrier.
*/
const projectionValuesSchema =
z.record(z.string(), z.unknown()) as unknown as z.ZodType<Partial<SessionProjectionMap>>
/** SessionSummary row of session.list. */
/** SessionSummary row of session.list (`projections` reuses the history block's shape and schema). */
export const sessionSummarySchema = z.object({
sessionId: sessionIdSchema,
updatedAt: z.number(),
@@ -55,8 +45,8 @@ export const sessionSummarySchema = z.object({
blank: z.boolean(),
parentSessionId: sessionIdSchema.optional(),
cwd: z.string().optional(),
projections: projectionValuesSchema.optional(),
}) satisfies z.ZodType<Wire<SessionSummary>>
projections: z.lazy(() => sessionProjectionsBlockSchema).optional(),
}) as unknown as z.ZodType<Wire<SessionSummary>>
/** session.list request payload (cursor is a reserved seat, unimplemented in v1). */
export const sessionListRequestSchema = z.object({
@@ -64,9 +54,9 @@ export const sessionListRequestSchema = z.object({
}) satisfies z.ZodType<Wire<RequestPayload<'session.list'>>>
/** session.list response value. */
export const sessionListValueSchema = z.object({
export const sessionListValueSchema: z.ZodType<Wire<ResponseValue<'session.list'>>> = z.object({
items: z.array(sessionSummarySchema),
}) satisfies z.ZodType<Wire<ResponseValue<'session.list'>>>
})
/** session.create request payload (at most one of workspaceId / cwd). */
export const sessionCreateRequestSchema = z.object({
+10 -9
View File
@@ -144,16 +144,17 @@ export interface SessionSummary {
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
cwd?: string
/**
* Whole current value per projection key, with zero log loads: attached
* sessions read the registry's live watermark cut; cold sessions read the
* persisted projection cache's stored rows — as stale as that session's
* last durable checkpoint, never wrong, superseded by the history tail
* baseline the moment the session is opened. Absent when no value is
* available (no registry, no cache row for a cold session, or a fail-soft
* cache read miss); a listing client treats absence as "no title yet",
* exactly like a blank session.
* Projection baseline for this row, with zero log loads: attached sessions
* read the registry's live watermark cut; cold sessions read the persisted
* projection cache's stored rows — as stale as that session's last durable
* checkpoint (`asOfSeq` says exactly how stale), never wrong, and directly
* seedable into the client's per-session value store under its
* higher-seq-wins rule (a list baseline can never overwrite a newer push
* frame). Absent when no value is available (no registry, no cache row for
* a cold session, or a fail-soft cache read miss); a listing client treats
* absence as "no title yet", exactly like a blank session.
*/
projections?: Partial<SessionProjectionMap>
projections?: SessionProjectionsBlock
}
/** Session-domain unary methods (the map keys session.* of RpcMethodMap). */