Merge remote-tracking branch 'origin/worktree/session-format-04-live-assistant-stream' into worktree/session-format-05-v1-v2-chunk-migration

This commit is contained in:
Tianyi Cui
2026-09-02 21:52:34 +08:00
29 changed files with 268 additions and 489 deletions
+3 -16
View File
@@ -17,11 +17,7 @@ import { SessionCommandController } from './commands.ts'
import { SessionControlController } from './control.ts'
import { SessionHistoryController } from './history.ts'
import { SessionFileReferences } from './file-references.ts'
import {
ApiSessionList,
DEFAULT_COLD_BLANK_PROBE_MAX_BYTES,
DEFAULT_COLD_BLANK_PROBE_MAX_EVENTS,
} from './list.ts'
import { ApiSessionList } from './list.ts'
import { buildModelCatalog } from './catalog.ts'
import { installModelSelectionProjection } from './model-selection-projection.ts'
import { SessionSkillCatalog } from './skill-catalog.ts'
@@ -70,10 +66,6 @@ declare module '@deepseek-ai/cordis' {
/** Session Controller deployment policy. */
export interface Config {
/** Maximum stat-reported event count eligible for one full cold projection observation; `0` disables the event-count gate. */
readonly coldBlankProbeMaxEvents?: number
/** Maximum stat-reported artifact byte size eligible for one full cold projection observation; `0` disables the byte-size gate. */
readonly coldBlankProbeMaxBytes?: number
/** Override platform desktop-opener detection. */
readonly nativeOpen?: boolean
}
@@ -101,8 +93,6 @@ export class SessionController extends TypertRemoteService {
]
static Config: z<Config> = z.object({
coldBlankProbeMaxEvents: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_EVENTS),
coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES),
nativeOpen: z.boolean(),
})
@@ -117,7 +107,7 @@ export class SessionController extends TypertRemoteService {
/**
* @param ctx - Host context containing the Session capability assembly.
* @param config - cold-list observation and native-opener deployment policy.
* @param config - native-opener deployment policy.
* @param internals - host integrations replaceable by direct unit tests.
*/
constructor(ctx: Context, config: Config, internals: SessionControllerInternals = {}) {
@@ -132,10 +122,7 @@ export class SessionController extends TypertRemoteService {
await Promise.allSettled([...this.promotions])
}, 'session-controller.promotions')
this.history = new SessionHistoryController(ctx, (observation) => { this.promote(observation) })
this.listState = new ApiSessionList(ctx, {
coldBlankProbeMaxEvents: config.coldBlankProbeMaxEvents ?? DEFAULT_COLD_BLANK_PROBE_MAX_EVENTS,
coldBlankProbeMaxBytes: config.coldBlankProbeMaxBytes ?? DEFAULT_COLD_BLANK_PROBE_MAX_BYTES,
})
this.listState = new ApiSessionList(ctx)
this.openPath = internals.openPath ?? openNativePath
this.canOpenPath = internals.canOpenPath
?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath()))
+8 -88
View File
@@ -19,21 +19,6 @@ import type {
SessionSearchValue, SessionSummary,
} from './types.ts'
/** Default maximum stat-reported event count eligible for one cold projection observation. */
export const DEFAULT_COLD_BLANK_PROBE_MAX_EVENTS = 16
/** Default maximum stat-reported artifact size eligible for one cold projection observation. */
export const DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024
/** Resolved cold-blank probe policy: each threshold gates its stat metric; `0` disables that gate. */
export interface ColdBlankProbePolicy {
/** Maximum stat-reported `eventCount` eligible for a full observation. */
readonly coldBlankProbeMaxEvents: number
/** Maximum stat-reported `sizeBytes` eligible for a full observation. */
readonly coldBlankProbeMaxBytes: number
}
const COLD_SUMMARY_BATCH_SIZE = 16
const SEARCH_PROVIDER_CALL_LIMIT = 100
const SESSION_SEARCH_QUERY_MAX_CHARS = 500
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message'])
@@ -90,14 +75,8 @@ export function truncateUnicodeCodePoints(value: string, maximum: number): strin
/** Owns list projection registration, bounded cold summaries, and authorized search. */
export class ApiSessionList {
/**
* @param ctx - Host context carrying Session, query, persistence, and projection services.
* @param probe - stat-metadata thresholds gating a full cold observation.
*/
constructor(
private readonly ctx: Context,
private readonly probe: ColdBlankProbePolicy,
) {
/** @param ctx - Host context carrying Session, query, persistence, and projection services. */
constructor(private readonly ctx: Context) {
ctx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({
key: 'sessionListMetadata',
stateSchema: sessionListMetadataSchema,
@@ -159,26 +138,13 @@ export class ApiSessionList {
if (record.header.cwd === undefined) continue
cold.push(record.header)
}
for (let offset = 0; offset < cold.length; offset += COLD_SUMMARY_BATCH_SIZE) {
const settled = await Promise.allSettled(cold.slice(offset, offset + COLD_SUMMARY_BATCH_SIZE)
.map(header => this.summarizeCold(header, signal)))
for (const result of settled) {
if (result.status === 'rejected') throw result.reason
items.push(result.value)
}
}
for (const header of cold) items.push(this.summarizeCold(header))
items.sort((left, right) => right.updatedAt - left.updatedAt)
return items
}
private async summarizeCold(
header: SessionHeader,
signal: AbortSignal | undefined,
): Promise<SessionSummary> {
const cached = this.projectionsFor(header, undefined)
const projections = cached?.values.sessionListMetadata?.blank === false
? cached
: await this.probeSmallCold(header, signal) ?? cached
private summarizeCold(header: SessionHeader): SessionSummary {
const projections = this.projectionsFor(header, undefined)
const raced = this.ctx.sessions.get(header.id)
if (raced !== undefined) return this.summaryFor(raced)
const metadata = projections?.values.sessionListMetadata
@@ -193,54 +159,6 @@ export class ApiSessionList {
}
}
private async probeSmallCold(
header: SessionHeader,
signal: AbortSignal | undefined,
): Promise<SessionProjectionHints | undefined> {
const { coldBlankProbeMaxEvents, coldBlankProbeMaxBytes } = this.probe
if (coldBlankProbeMaxEvents === 0 && coldBlankProbeMaxBytes === 0) return undefined
const persistence = this.ctx.get('sessionPersistence')
if (persistence === undefined) return undefined
signal?.throwIfAborted()
let snapshot: Awaited<ReturnType<typeof persistence.stat>>
try {
snapshot = await persistence.stat(header.id, signal === undefined ? {} : { signal })
} catch (error: unknown) {
// An unreadable single session degrades to unknown state instead of
// failing the whole list request.
signal?.throwIfAborted()
this.ctx.logger.warn(
`api-session.list: cold stat for "${header.id}" failed; serving it as visible: ${String(error)}`,
)
return undefined
}
if (snapshot === undefined) return undefined
if (snapshot.eventCount !== undefined) {
if (coldBlankProbeMaxEvents === 0 || snapshot.eventCount > coldBlankProbeMaxEvents) return undefined
} else if (snapshot.sizeBytes !== undefined) {
if (coldBlankProbeMaxBytes === 0 || snapshot.sizeBytes > coldBlankProbeMaxBytes) return undefined
} else {
// The backend offers no cheap size hint, so a full observation is unbounded work.
return undefined
}
try {
using observation = await this.ctx.sessionQuery.observeSession(header.id, {
...(signal === undefined ? {} : { signal }),
projectionMode: 'all',
})
const block = observation.projections
return block === undefined
? undefined
: { asOfSeq: block.asOfSeq, values: block.values as SessionProjectionValues }
} catch (error: unknown) {
signal?.throwIfAborted()
this.ctx.logger.warn(
`api-session.list: small cold observation for "${header.id}" failed; serving it as visible: ${String(error)}`,
)
return undefined
}
}
/**
* Search current visible message content without activating any matching Session.
* @param query - literal message-content query.
@@ -354,10 +272,12 @@ export class ApiSessionList {
session: Session | undefined,
): SessionProjectionHints | undefined {
try {
const cache = this.ctx.get('sessionProjectionCache')
const block = session === undefined
? header.isSeeded
? undefined
: this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header, SessionLogOffset(0))
: cache?.cachedSnapshot(header, SessionLogOffset(0))
?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0))
: this.ctx.sessionProjections.cachedSnapshot(session)
return block !== undefined && Object.keys(block.values).length > 0
? {