mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-13 04:03:30 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user