Merge remote-tracking branch 'origin/master' into worktree/2984-generic-file-upload

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	docs/persistence-catalog.i18n.yaml
#	docs/persistence-catalog.md
#	docs/persistence-catalog.zh.md
#	packages/api/session-controller/src/commands.ts
#	packages/api/session-controller/src/index.ts
#	packages/api/session-controller/tests/client-contract.client.spec.ts
#	packages/api/session-controller/tests/session-pending-submissions.client.spec.ts
#	packages/client/ui-attachment/src/AttachmentRail.module.css
#	packages/client/ui-chat/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-trajectory/README.i18n.yaml
#	packages/client/ui-trajectory/README.md
#	packages/client/ui-trajectory/README.zh.md
#	packages/extensions/cordis-client-runner/src/client/api-catalog.ts
#	packages/llm/token-meter/src/surface-fold.ts
#	packages/session-query/session-log-export/src/archive.ts
#	packages/session-query/session-log-export/tests/archive.host.spec.ts
This commit is contained in:
creatixchu
2026-09-02 10:29:53 +08:00
1010 changed files with 17789 additions and 15974 deletions
@@ -9,7 +9,7 @@
*/
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { FileUploadReceiptId, PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
@@ -131,7 +131,7 @@ export interface ISession {
* @param title - raw title text (the host normalizes acceptance).
* @returns the normalized accepted title and its event seq, or the business error.
*/
rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>>
rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>>
/**
* Extend the history window backwards (older messages pagination).
* @returns completion; failures land in snapshot.openState/loadingOlder.
@@ -145,7 +145,7 @@ export interface ISession {
* @param seq - durable event seq the window must reach (a turn's `turn/start` seq).
* @returns completion once covered, exhausted, superseded, or failed soft.
*/
loadThrough(seq: number): Promise<void>
loadThrough(seq: SessionSeq): Promise<void>
/**
* Execute one slash-command line against this session's agent — pure
* admission semantics (the host executor durably logs the lifecycle).
@@ -3,7 +3,7 @@
// List data never enters zustand; React connects via subscribe/getListSnapshot.
import type { SubagentAddress, SubagentCatalog } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionSeq, type SessionId, type SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type {
SessionControlBaseline,
@@ -27,6 +27,10 @@ import { Session } from './session.ts'
import type { SessionRemotes } from './remotes.ts'
import type { BackgroundUploadTransport } from '@deepseek-ai/dsh-client-connection/client'
function sessionSeqCursor(value: number): SessionSeqCursor {
return value === -1 ? -1 : SessionSeq(value)
}
/**
* List arrival lifecycle, orthogonal to the pull-activity `state` axis:
* `pending` (no successful pull yet — an empty items array means "nothing
@@ -501,7 +505,7 @@ export class SessionManager {
if (block === undefined) continue
const store = this.projectionStore(s.sessionId)
const values = block.values as Record<string, unknown>
for (const key of Object.keys(values)) store.apply(key, values[key], block.asOfSeq)
for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq))
}
} else {
this.listState = 'error'
@@ -593,7 +597,7 @@ export class SessionManager {
* @returns the fork result (the child session id).
*/
async fork(
opts: { sessionId: SessionId; atSeq?: number },
opts: { sessionId: SessionId; atSeq?: SessionSeq },
): Promise<RemoteResult<{ sessionId: SessionId }>> {
const source = this.summaries.find(s => s.sessionId === opts.sessionId)
const result = await this.remote.session.fork({
@@ -664,7 +668,7 @@ export class SessionManager {
return
}
if (frame.type === 'projection') {
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, frame.seq)
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq))
this.notifier.markDirty()
return
}
@@ -691,8 +695,9 @@ export class SessionManager {
for (const [sessionId, block] of Object.entries(baseline.projections)) {
const store = this.projectionStore(sessionId as SessionId)
store.truncate(block.asOfSeq)
store.seed(block)
const asOfSeq = sessionSeqCursor(block.asOfSeq)
store.truncate(asOfSeq)
store.seed({ ...block, asOfSeq })
}
for (const [sessionId, session] of this.sessions) {
session.replaceControl(this.queues.get(sessionId) ?? [])
@@ -711,7 +716,7 @@ export class SessionManager {
if (projections !== undefined) {
const store = this.projectionStore(summary.sessionId)
for (const [key, value] of Object.entries(projections.values)) {
store.apply(key, value, projections.asOfSeq)
store.apply(key, value, sessionSeqCursor(projections.asOfSeq))
}
}
if (summary.origin === 'subagent' && summary.parentSessionId !== undefined) {
@@ -9,6 +9,7 @@
* bare observable faces feed `useProjection` (ui-renderer binds them).
*/
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import { Notifier } from './notifier.ts'
@@ -46,7 +47,7 @@ export type UseProjection = {
*/
export interface ProjectionsBaseline {
/** The consistent-cut seq (equals the window tail seq by construction). */
asOfSeq: number
asOfSeq: SessionSeqCursor
/** Whole current values by key; a registered key absent here means the capability is absent. */
values: Readonly<Record<string, unknown>>
}
@@ -54,7 +55,7 @@ export interface ProjectionsBaseline {
/** One key's row: the latest finished value and the seq it is consistent with. */
interface Row {
value: unknown
seq: number
seq: SessionSeqCursor
}
/** Per-key notification channel: the bare face plus its batching notifier. */
@@ -130,7 +131,7 @@ export class ProjectionValueStore {
* @param value - whole value computed by the host unit.
* @param seq - the unit's watermark at emission.
*/
apply(key: string, value: unknown, seq: number): void {
apply(key: string, value: unknown, seq: SessionSeqCursor): void {
const row = this.rows.get(key)
if (row !== undefined && seq <= row.seq) return // higher seq wins; replays and stale frames drop
this.rows.set(key, { value, seq })
@@ -165,7 +166,7 @@ export class ProjectionValueStore {
* baseline immediately afterward.
* @param lastSeq - highest durable sequence reflected by the baseline.
*/
truncate(lastSeq: number): void {
truncate(lastSeq: SessionSeqCursor): void {
for (const [key, row] of this.rows) {
if (row.seq <= lastSeq) continue
this.rows.delete(key)
@@ -16,7 +16,7 @@
*/
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import { SESSION_SEARCH_RESULT_LIMIT } from '../../types.ts'
@@ -441,7 +441,7 @@ export class ClientSessions implements ISessions {
// Flooring lands inside the anchor's own turn (every turn opens with a
// turn/start), so the host's first-turn/end-at-or-after cut still ends
// on that turn — never clipped back to the previous one.
...(opts.atSeq === undefined ? {} : { atSeq: Math.floor(opts.atSeq) }),
...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }),
})
if (!result.ok) throw new SessionForkError(result.error, opts.sessionId)
this.projectList()
@@ -8,7 +8,7 @@ import type {
import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionEventStream } from '../transport.ts'
import type { SessionJournalChange } from '../transport.ts'
import type {
@@ -16,6 +16,7 @@ import type {
QueueAction,
SessionAddress,
SessionControlFrame,
SessionProjectionBaseline,
SessionQueuedItem,
SessionRequestId,
SessionUploadFileValue,
@@ -41,6 +42,13 @@ import type { ProjectionsBaseline } from './projection-store.ts'
import { resolvedClientTimeZone } from '../time-zone.ts'
import { SessionQueueMirror } from './queue-mirror.ts'
function projectionsBaseline(value: SessionProjectionBaseline): ProjectionsBaseline {
return {
...value,
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
}
}
/** Messages requested per history page. */
export const PAGE_MESSAGES = 50
@@ -80,7 +88,7 @@ export interface SessionOptions {
*/
export class Session implements SessionFace {
// ---- Window and derived state (all private; the snapshot is the only read API) ----
private baseSeq = 0
private baseSeq = SessionLogOffset(0)
private hasMore = false
private openState: OpenState = 'cold'
private openError: RemoteFailure | null = null
@@ -90,7 +98,7 @@ export class Session implements SessionFace {
private openGeneration = 0
private loadingOlder = false
/** Shared low-water target of the running jump loop; null when no jump is paging. */
private jumpTargetSeq: number | null = null
private jumpTargetSeq: SessionSeq | null = null
/** The running jump loop's completion, shared by retargeting callers. */
private jumpPromise: Promise<void> | null = null
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
@@ -392,10 +400,12 @@ export class Session implements SessionFace {
* @param title - raw title text (the host normalizes acceptance).
* @returns the rename result (normalized accepted title + title event seq).
*/
async rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>> {
async rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>> {
const result = await this.remote.session.rename({ sessionId: this.sessionId, title })
if (result.ok) this.projections.apply('title', result.value.title, result.value.seq)
return result
if (!result.ok) return result
const seq = SessionSeq(result.value.seq)
this.projections.apply('title', result.value.title, seq)
return { ok: true, value: { title: result.value.title, seq } }
}
/**
@@ -443,11 +453,11 @@ export class Session implements SessionFace {
}
/** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
loadThrough(seq: number): Promise<void> {
loadThrough(seq: SessionSeq): Promise<void> {
if (this.openState !== 'open' || !this.hasMore || this.baseSeq <= seq) return Promise.resolve()
if (this.jumpPromise !== null) {
// Retarget the running loop to the lowest requested seq.
this.jumpTargetSeq = Math.min(this.jumpTargetSeq ?? seq, seq)
this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq))
return this.jumpPromise
}
// A plain single-page pull owns the busy flag; the jump does not queue
@@ -500,7 +510,7 @@ export class Session implements SessionFace {
this.openPromise = null
this.openState = 'cold'
this.openError = null
this.baseSeq = 0
this.baseSeq = SessionLogOffset(0)
this.notifier.markDirty()
await this.open()
}
@@ -672,7 +682,11 @@ export class Session implements SessionFace {
private acceptEventChange(change: SessionJournalChange): void {
switch (change.type) {
case 'replace':
this.installWindow(change.entries, change.hasMore, change.page.projections)
this.installWindow(
change.entries,
change.hasMore,
change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections),
)
return
case 'prepend':
this.prependWindow(change.entries, change.hasMore)
@@ -684,7 +698,7 @@ export class Session implements SessionFace {
/** Replace the complete contiguous window and apply page-owned projection metadata. */
private installWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean, projections?: ProjectionsBaseline): void {
this.baseSeq = entries[0]?.event.seq ?? 0
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0)
this.hasMore = hasMore
if (entries.some(entry => entry.event.type === 'turn/start')) this.firstPromptPendingTurn = false
if (projections !== undefined) this.projections.seed(projections)
@@ -695,7 +709,7 @@ export class Session implements SessionFace {
/** Prepend one stream-validated history page. */
private prependWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean): void {
this.baseSeq = entries[0]?.event.seq ?? this.baseSeq
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq)
this.hasMore = hasMore
this.eventSource.prepend(entries, hasMore)
}