Merge latest master into PR 555

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/runtime/README.i18n.yaml
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
creatixchu
2026-07-31 19:39:22 +08:00
187 files changed
+5806 -657

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ export type {
AssistantTiming, CodeSubCall, CommandNode, CompactionSummaryNode, ComposerPhase,
ContextMessageNode, ConversationNode, ConversationSnapshot, ModelRetryNode, QueuedMessage,
RunningToolCall,
SteeringMessageNode, TodoItem, ToolResultNode, UnknownSurfaceNode, UserMessageNode,
SteeringMessageNode, TodoItem, ToolResultNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode,
} from './sessions/conversation.ts'
export type {
ConversationContext, ConversationContextOriginKind,
@@ -138,6 +138,19 @@ export type ModelRetryNode = LlmRetryEventData & {
retryState: 'scheduled' | 'started' | 'cancelled'
}
/** Durable terminal failure for a turn that has no scheduled retry. */
export interface TurnErrorNode {
kind: 'turn-error'
/** Seq of the owning turn/end event. */
seq: number
/** Unix epoch ms from the turn/end event. */
time: number
turn: number
step: number
message: string
code?: string
}
/** A tool result paired (when in-window) with its call head. */
export interface ToolResultNode {
kind: 'tool-result'
@@ -226,6 +239,7 @@ export type ConversationNode =
| SteeringMessageNode
| ContextMessageNode
| ModelRetryNode
| TurnErrorNode
| ToolResultNode
| CommandNode
| CompactionSummaryNode
@@ -0,0 +1,10 @@
/**
* Convert a durable failure into copy that is safe to expose in the GUI.
* @param failure - Structured failure preserved by the session event.
* @returns Display-safe copy for client projections.
*/
export function displayFailureMessage(failure: { code?: string; message: string }): string {
// Provider AUTH messages may echo a masked or partially preserved credential.
// Keep the raw diagnostic in the session log, but never project it into UI state.
return failure.code === 'AUTH' ? 'API key is invalid' : failure.message
}
@@ -8,6 +8,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {
AssistantProvenanceView, AssistantRequestConfig,
} from './conversation.ts'
import { displayFailureMessage } from './failure-display.ts'
export type {
AssistantProvenanceView, AssistantRequestConfig,
@@ -35,40 +36,55 @@ export interface RequestPromptChange {
previous?: ConversationPromptSnapshot
}
/** One provider request reconstructed from durable request lifecycle events. */
export interface RequestView {
/** Request category; compaction is a purpose, not a separate projection. */
purpose: 'assistant' | 'compaction'
/** Lifecycle fields shared by ordinary generation and compaction requests. */
interface RequestViewBase {
/** Sequence that opened the operation represented by this request. */
startSeq: number
turn: number
/** Agent-loop step, or zero for a direct compaction request. */
step: number
startedAt: number
completedAt: number | null
status: 'running' | 'complete' | 'error'
error?: string
/** Effective ordinary request input, inherited until a later header changes it. */
prompt?: ConversationPromptSnapshot
/** Prompt change logged while preparing this request. */
promptChange?: RequestPromptChange
provenance?: AssistantProvenanceView
requestConfig?: AssistantRequestConfig
usage?: unknown
/** Assistant message or compaction summary sequence produced by this request. */
resultSeq?: number
}
/** One ordinary assistant generation reconstructed from durable request events. */
interface AssistantRequestView extends RequestViewBase {
purpose: 'assistant'
turn: number
/** Agent-loop step that issued this request. */
step: number
/** Effective ordinary request input, inherited until a later header changes it. */
prompt?: ConversationPromptSnapshot
/** Prompt change logged while preparing this request. */
promptChange?: RequestPromptChange
/** Retry ordinal scheduled after a failed ordinary request. */
retry?: number
maxRetries?: number
retryDelayMs?: number
}
/** One compaction provider request, either turn-owned or standalone between turns. */
interface CompactionRequestView extends RequestViewBase {
purpose: 'compaction'
/** Owning turn, or `null` when manual compaction ran between turns. */
turn: number | null
/** Direct compaction requests do not consume an agent-loop step. */
step: 0
/** Compaction replacement message sequence, when one was committed. */
replacementSeq?: number
/** Safe compaction summary projection. */
summary?: readonly ContentBlock[]
/** Complete compaction provider output before the safe projection. */
rawOutput?: readonly ContentBlock[]
/** Retry ordinal scheduled after a failed ordinary request. */
retry?: number
maxRetries?: number
retryDelayMs?: number
}
/** One provider request reconstructed from durable request lifecycle events. */
export type RequestView = AssistantRequestView | CompactionRequestView
/** Immutable request-centric projection derived from one history window. */
export interface RequestInspectionSnapshot {
requests: readonly RequestView[]
@@ -110,7 +126,7 @@ interface CompactionStartEvent {
type: 'compact/start'
seq: number
time: number
data: { turn: number }
data: { turn: number | null }
}
interface CompactionSummaryEvent {
@@ -131,7 +147,7 @@ interface CompactionEndEvent {
type: 'compact/end'
seq: number
time: number
data: { turn: number; error?: string }
data: { turn: number | null; error?: string }
}
function requestKey(turn: number, step: number): string {
@@ -228,10 +244,21 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
let activePrompt: ConversationPromptSnapshot | undefined
let activeCompaction: number | undefined
const update = (index: number | undefined, change: Partial<RequestView>): void => {
const updateAssistant = (
index: number | undefined,
change: Partial<Omit<AssistantRequestView, 'purpose'>>,
): void => {
if (index === undefined) return
const request = requests[index]
if (request !== undefined) requests[index] = { ...request, ...change }
if (request?.purpose === 'assistant') requests[index] = { ...request, ...change }
}
const updateCompaction = (
index: number | undefined,
change: Partial<Omit<CompactionRequestView, 'purpose'>>,
): void => {
if (index === undefined) return
const request = requests[index]
if (request?.purpose === 'compaction') requests[index] = { ...request, ...change }
}
for (const sourceEvent of events) {
@@ -263,7 +290,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
}
const change = promptChange(activePrompt, prompt, sourceEvent)
activePrompt = prompt
update(activeStep === undefined ? undefined : ordinaryByStep.get(activeStep), {
updateAssistant(activeStep === undefined ? undefined : ordinaryByStep.get(activeStep), {
prompt,
requestConfig: prompt.config,
...(change === undefined ? {} : { promptChange: change }),
@@ -278,8 +305,11 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
requestKey(sourceEvent.data.turn, sourceEvent.data.step),
)
const request = index === undefined ? undefined : requests[index]
update(index, {
usage: addTokenUsage(request?.usage, sourceEvent.data.chunk.usage),
updateAssistant(index, {
usage: addTokenUsage(
request?.purpose === 'assistant' ? request.usage : undefined,
sourceEvent.data.chunk.usage,
),
})
continue
}
@@ -288,7 +318,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
requestKey(sourceEvent.data.turn, sourceEvent.data.step),
)
const request = index === undefined ? undefined : requests[index]
update(index, {
updateAssistant(index, {
completedAt: sourceEvent.time,
status: 'complete',
resultSeq: sourceEvent.seq,
@@ -296,7 +326,9 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
provider: sourceEvent.data.message.source.provider,
model: sourceEvent.data.message.source.model,
},
...(request?.usage !== undefined || sourceEvent.data.usage === undefined
...(request?.purpose === 'assistant'
&& request.usage !== undefined
|| sourceEvent.data.usage === undefined
? {}
: { usage: sourceEvent.data.usage }),
})
@@ -306,8 +338,8 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
const key = requestKey(sourceEvent.data.turn, sourceEvent.data.step)
const index = ordinaryByStep.get(key)
const request = index === undefined ? undefined : requests[index]
if (request?.status === 'running') {
update(index, {
if (request?.purpose === 'assistant' && request.status === 'running') {
updateAssistant(index, {
completedAt: sourceEvent.time,
status: 'error',
})
@@ -317,9 +349,9 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
}
if ((sourceEvent.type as string) === 'llm/retry') {
const event = sourceEvent as unknown as RetryEvent
update(ordinaryByStep.get(requestKey(event.data.turn, event.data.step)), {
updateAssistant(ordinaryByStep.get(requestKey(event.data.turn, event.data.step)), {
status: 'error',
error: event.data.failure.message,
error: displayFailureMessage(event.data.failure),
retry: event.data.retry,
maxRetries: event.data.maxRetries,
retryDelayMs: event.data.delayMs,
@@ -328,14 +360,23 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
}
if (sourceEvent.type === 'turn/end' && sourceEvent.data.reason.kind === 'error') {
const reason = sourceEvent.data.reason
update(ordinaryByStep.get(requestKey(sourceEvent.data.turn, reason.step)), {
updateAssistant(ordinaryByStep.get(requestKey(sourceEvent.data.turn, reason.step)), {
status: 'error',
error: 'failure' in reason ? reason.failure.message : reason.message,
error: displayFailureMessage('failure' in reason ? reason.failure : reason),
})
continue
}
const type = sourceEvent.type as string
if (type === 'session/end-seed' && activeCompaction !== undefined) {
updateCompaction(activeCompaction, {
completedAt: sourceEvent.time,
status: 'error',
error: 'Compaction was interrupted before completion.',
})
activeCompaction = undefined
continue
}
if (type === 'compact/start') {
const event = sourceEvent as unknown as CompactionStartEvent
activeCompaction = requests.length
@@ -352,7 +393,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
}
if (type === 'compact/summary' && activeCompaction !== undefined) {
const event = sourceEvent as unknown as CompactionSummaryEvent
update(activeCompaction, {
updateCompaction(activeCompaction, {
resultSeq: event.seq,
summary: event.data.summary,
...(event.data.rawOutput === undefined ? {} : { rawOutput: event.data.rawOutput }),
@@ -375,12 +416,12 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[]
&& activeCompaction !== undefined
&& isCompactionSource(sourceEvent.data.source)
) {
update(activeCompaction, { replacementSeq: sourceEvent.seq })
updateCompaction(activeCompaction, { replacementSeq: sourceEvent.seq })
continue
}
if (type !== 'compact/end' || activeCompaction === undefined) continue
const event = sourceEvent as unknown as CompactionEndEvent
update(activeCompaction, {
updateCompaction(activeCompaction, {
completedAt: event.time,
status: event.data.error === undefined ? 'complete' : 'error',
...(event.data.error === undefined ? {} : { error: event.data.error }),
@@ -20,6 +20,7 @@ import type {
import type { PendingInteraction } from './pending.ts'
import { PendingWait } from './pending.ts'
import { TranscriptAdapter } from './transcript-adapter.ts'
import { displayFailureMessage } from './failure-display.ts'
import { Notifier } from './notifier.ts'
import { PartialAccumulator } from './partial.ts'
import { ProjectionValueStore } from './projection-store.ts'
@@ -772,6 +773,22 @@ export class Session implements SessionFace {
if (event.data.reason.kind === 'aborted' || event.data.reason.kind === 'disposed') {
this.settleScheduledRetry('cancelled', event.data.turn)
}
if (
event.data.reason.kind === 'error'
&& !this.derivedNodes.some(node => node.kind === 'model-retry' && node.turn === event.data.turn)
) {
const failure = 'failure' in event.data.reason ? event.data.reason.failure : event.data.reason
this.derivedNodes.push({
kind: 'turn-error',
seq: event.seq,
time: event.time,
turn: event.data.turn,
step: event.data.reason.step,
message: displayFailureMessage(failure),
...(failure.code === undefined ? {} : { code: failure.code }),
})
this.derivedRev++
}
// Aborted turns never finalize. The accumulated partial is VALUE, not residue: freeze it
// into an interrupted terminal node (pulse stops, text survives) instead of deleting it.
// Shared by live and window-replay paths, so a refresh reconstructs the same frozen node