Merge remote-tracking branch 'origin/master' into codex/pr-3111-review

# Conflicts:
#	packages/api/session-controller/src/client/contract/snapshot.ts
#	packages/test-support/client-runtime/src/sessions.ts
This commit is contained in:
creatixchu
2026-08-26 15:34:18 +08:00
167 changed files with 2711 additions and 1425 deletions
@@ -1,10 +1,11 @@
/** Client operation results spanning Session Remote calls and the legacy subagent carrier. */
/** Client operation results spanning the Session and subagent Remote calls. */
import type { RpcError } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentControlError } from '@deepseek-ai/dsh-subagent/client'
import type { SessionError } from '../../types.ts'
/** Failure surfaced by the Client Session object layer. */
export type ClientFailure = RpcError | SessionError
export type ClientFailure = RpcError | SessionError | SubagentControlError
/** Success or failure returned by a Client Session operation. */
export type ClientResult<T> =
@@ -13,7 +14,7 @@ export type ClientResult<T> =
/**
* Fold a rejected carrier operation into the Client Session failure vocabulary.
* @param error - rejection from a legacy subagent or local carrier call.
* @param error - rejection from a Remote or local carrier call.
* @returns the failure branch of a Client Session result.
*/
export function transportResult<T>(error: unknown): ClientResult<T> {
@@ -5,7 +5,7 @@
* explicit act of widening what features may do to the sessions domain.
*/
import type { Context } from '@deepseek-ai/cordis'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type { AgentContext } from '../scope.ts'
@@ -2,7 +2,7 @@
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SubagentAddress } from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionRequestId } from '../../types.ts'
import type { ClientFailure } from './result.ts'
@@ -82,6 +82,7 @@ export const inject = [
'remote',
'remote.commands',
'remote.session',
'remote.subagents',
]
/**
@@ -91,7 +92,7 @@ export const inject = [
export function apply(ctx: Context): void {
const connection = ctx.get('connection') as ConnectionHandle
const remotes = ctx.remote as unknown as SessionRemotes
const sessions = new ClientSessions(ctx, connection.api, remotes)
const sessions = new ClientSessions(ctx, remotes)
ctx.remote.$on('api-session/added', (summary) => { sessions.handleSessionAdded(summary) })
ctx.remote.$on('api-session/removed', (sessionId) => { sessions.handleSessionRemoved(sessionId) })
ctx.remote.$on('api-session/status', (sessionId, running) => {
@@ -2,9 +2,7 @@
// dispatch entry + list state, constructed and held by ClientSessions (one per browser client).
// List data never enters zustand; React connects via subscribe/getListSnapshot.
import type {
IApiClient, SubagentAddress, SubagentCatalog,
} from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress, SubagentCatalog } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type {
@@ -145,11 +143,10 @@ export class SessionManager {
})
/**
* @param api - shared wire client.
* @param remote - generated Remote namespaces the Session cluster calls.
* @param restoredSelection - persisted real-Session selection candidate.
*/
constructor(
private readonly api: IApiClient,
private readonly remote: SessionRemotes,
restoredSelection?: SessionId,
restoredAddress?: SubagentAddress,
@@ -321,7 +318,7 @@ export class SessionManager {
const parentAvailable = address === undefined
? undefined
: this.catalogs.get(address.parentSessionId)?.parentAvailable
return new Session(sessionId, this.api, this.remote, {
return new Session(sessionId, this.remote, {
...(address === undefined ? {} : {
address,
...catalogAvailability(parentAvailable),
@@ -369,7 +366,7 @@ export class SessionManager {
this.notifier.markDirty()
const operation = (async () => {
try {
const { result } = await this.api.subagents.list({ parentSessionId })
const result = toSessionResult(await this.remote.subagents.list(parentSessionId))
if (result.ok) {
const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
?? result.value.parentAvailable
@@ -8,6 +8,9 @@
import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types'
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest,
} from '@deepseek-ai/dsh-subagent/client'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { SessionRemote } from '../transport.ts'
@@ -21,9 +24,24 @@ export interface SessionCommandsRemote {
): Promise<RemoteResult<object | undefined>>
}
/** Narrow subagent namespace consumed by a Client Session and its manager. */
export interface SessionSubagentsRemote {
list(parentSessionId: SessionId, signal?: AbortSignal): Promise<RemoteResult<SubagentCatalog>>
prompt(
request: SubagentPromptRequest,
signal?: AbortSignal,
): Promise<RemoteResult<SubagentPromptReceipt>>
interruptByParent(
childSessionId: SessionId,
parentSessionId: SessionId,
mode: 'continuable',
): Promise<RemoteResult<SubagentInterruptReceipt>>
}
/** Generated Remote namespaces consumed by the Client Session object layer. */
export interface SessionRemotes {
readonly $stream: ClientRemote['$stream']
readonly commands: SessionCommandsRemote
readonly session: SessionRemote
readonly subagents: SessionSubagentsRemote
}
@@ -15,9 +15,7 @@
* survives frozen (read-only view) until the stage moves on.
*/
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type {
IApiClient, SubagentAddress,
} from '@deepseek-ai/dsh-client-connection/client'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import 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'
@@ -218,12 +216,10 @@ export class ClientSessions implements ISessions {
/**
* @param ctx - client root context (scope fibers mount under it).
* @param api - wire client shared with every Session.
* @param remote - generated Remote namespaces shared with every Session.
*/
constructor(
private readonly rootCtx: Context,
api: IApiClient,
remote: SessionRemotes,
) {
this.selection = createSnapshotStore<SessionSelection>(
@@ -231,7 +227,6 @@ export class ClientSessions implements ISessions {
{ persist: { name: 'dsh.sessions.current' } })
const restored = this.selection.getSnapshot()
this.manager = new SessionManager(
api,
remote,
restored.sessionId,
restored.subagentAddress,
@@ -3,9 +3,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
IApiClient, SubagentAddress,
} from '@deepseek-ai/dsh-client-connection/client'
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 {
@@ -143,13 +141,11 @@ export class Session implements SessionFace {
/**
* @param sessionId - Host session identity (client sessions are always Host-born).
* @param api - shared wire client.
* @param remote - generated Remote namespaces this session calls.
* @param options - optional manager-owned state observers.
*/
constructor(
readonly sessionId: SessionId,
private readonly api: IApiClient,
private readonly remote: SessionRemotes,
private readonly options: SessionOptions = {},
) {
@@ -258,13 +254,16 @@ export class Session implements SessionFace {
},
}
} else {
const routed = (await this.api.subagents.prompt({
...this.address,
const routed = toSessionResult(await this.remote.subagents.prompt({
requestId: randomUUID() as SessionRequestId,
parentSessionId: this.address.parentSessionId,
childSessionId: this.address.childSessionId,
mode: this.address.mode,
content: content.flatMap(part => part.type === 'text'
? [{ type: 'text' as const, text: part.text }]
: []),
clientTimeZone: resolvedClientTimeZone(),
}, signal)).result
}, signal))
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
}
}
@@ -327,7 +326,7 @@ export class Session implements SessionFace {
/**
* Stop the active turn while the Host preserves pending inbox work; failures
* land in promptError (same error-strip display slot). A continuable
* subagent address routes through `subagent.interrupt`, whose durable
* subagent address routes through `subagents.interruptByParent`, whose durable
* parent-address authority works without a live parent Agent; a one-shot
* address stays uncancellable (the UI offers no stop action, so this arm is
* defensive).
@@ -351,7 +350,11 @@ export class Session implements SessionFace {
let result: ClientResult<{ accepted: true }>
try {
result = address !== undefined
? (await this.api.subagents.interrupt(address)).result
? toSessionResult(await this.remote.subagents.interruptByParent(
address.childSessionId,
address.parentSessionId,
address.mode,
))
: toSessionResult(await this.remote.session.cancel({ sessionId: this.sessionId }))
} catch (error) {
result = transportResult(error)