Files
deepseek-harness/packages/api/session-controller/src/client/transport.ts
T
imccyu 674a1e95a3 fix(api): rename the failure marker, harden cross-realm discrimination, and mark the packed-record violation
- protocol: isDSHRemoteGatewayError -> isDSHRemoteError (the class is
  protocol-wide, not Gateway-specific); remoteErrorOf drops its
  instanceof Error precondition and tests the marker plus a string code
  structurally, so a marked failure from another realm no longer reads
  as a local defect.
- session-controller: the live-follow packed-record protocol violation
  now throws a marked RemoteError('gateway/internal'), landing the
  session in openState=error instead of an unhandled rejection;
  pinned at the transport and session levels.
- util-time: correct the invariant companion's @module name.
- regenerate the tool-cordis catalog for the marker rename.
2026-08-29 03:12:43 +08:00

214 lines
7.1 KiB
TypeScript

/** Session-specific adapters for Gateway-owned Remote stream lifecycles. */
import type {} from '@deepseek-ai/dsh-api-session-controller/remote'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import {
RemoteJournalStream,
RemoteSnapshotStream,
RemoteStreamCarrierError,
type ClientRemote,
type RemoteJournalChange,
type RemoteJournalFrame,
} from '@deepseek-ai/dsh-api-gateway/client'
import type {
SessionAddress,
SessionControlFrame,
SessionHistoryRecord,
SessionPage,
SessionPageRequest,
SessionProjectionBaseline,
} from '../types.ts'
import {
historyEntries,
historyRecordFirstSeq,
historyRecordLastSeq,
} from './sessions/history-records.ts'
import type { SessionEventLikeEntry, SessionLiveEventEntry } from './contract/events.ts'
import type { SessionRemotes } from './sessions/remotes.ts'
export {
SESSION_SEARCH_RESULT_LIMIT,
SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS,
} from '../types.ts'
/** Pagination fields bound to an already-addressed Session journal. */
export type ClientSessionPageRequest = Omit<SessionPageRequest, 'address' | 'throughSeq'>
/** Complete generated `ctx.remote.session` namespace. */
export type SessionRemote = ClientRemote['session']
/** Opening metadata carried only by a follow snapshot, never by loadOlder pages. */
interface SessionJournalPage extends SessionPage {
readonly projections?: SessionProjectionBaseline
}
/** One complete publication from the Session journal stream. */
export type SessionJournalChange =
| {
readonly type: 'replace' | 'prepend'
readonly page: SessionJournalPage
readonly entries: readonly SessionEventLikeEntry[]
readonly hasMore: boolean
}
| { readonly type: 'append'; readonly entry: SessionLiveEventEntry }
function toSessionJournalChange(
change: RemoteJournalChange<SessionJournalPage, SessionHistoryRecord>,
): SessionJournalChange {
switch (change.type) {
case 'replace':
case 'prepend':
return { ...change, entries: historyEntries(change.entries) }
case 'append': {
if (change.entry.type !== 'event') {
throw new RemoteError(
'gateway/internal',
'session live stream emitted a packed history record',
{},
)
}
return {
type: 'append',
entry: change.entry as unknown as SessionLiveEventEntry,
}
}
}
}
type SessionControlBaselineFrame = Extract<SessionControlFrame, { type: 'baseline' }>
type SessionControlDeltaFrame = Exclude<SessionControlFrame, SessionControlBaselineFrame>
/** Gateway-owned control snapshot stream configured for Session frames. */
export type SessionControlStream = RemoteSnapshotStream<
SessionControlBaselineFrame,
SessionControlDeltaFrame
>
/** Domain sinks used by the Host-wide Session control stream. */
export interface SessionControlStreamOptions {
/** Apply a complete baseline or one later update. */
readonly accept: (frame: SessionControlFrame) => void
/** Observe a retryable carrier loss before reconnection. */
readonly carrierFailed?: (error: RemoteStreamCarrierError) => void
/** Publish a terminal business or protocol failure. */
readonly failed: (error: unknown) => void
}
/** Domain sinks used by one addressed Session event journal. */
export interface SessionEventStreamOptions {
/** Apply one complete event-window change. */
readonly publish: (change: SessionJournalChange) => void
/** Observe a retryable carrier loss before reconnection. */
readonly carrierFailed?: (error: RemoteStreamCarrierError) => void
/** Publish a terminal stream, page, or protocol failure after opening. */
readonly failed: (error: unknown) => void
}
/**
* Create the Host-wide Session control snapshot stream.
* @param remote - generated Session namespace and Gateway stream factory.
* @param options - Session state destinations.
* @returns an unstarted stream owned by the Client Session runtime.
*/
export function createSessionControlStream(
remote: SessionRemotes,
options: SessionControlStreamOptions,
): SessionControlStream {
const stream = remote.$stream<SessionControlFrame>({
name: 'session control stream',
open: signal => remote.session.control(signal),
ended: accepted => accepted
? new RemoteStreamCarrierError('session control stream ended without a terminal result')
: new Error('session control stream ended before its opening snapshot'),
...(options.carrierFailed === undefined ? {} : { carrierFailed: options.carrierFailed }),
})
return new RemoteSnapshotStream(stream, {
name: 'session control stream',
isSnapshot: (frame): frame is SessionControlBaselineFrame => frame.type === 'baseline',
replace: options.accept,
update: options.accept,
failed: options.failed,
})
}
/** Gateway-owned event journal bound to one ordinary or direct-subagent Session address. */
export class SessionEventStream extends RemoteJournalStream<
SessionJournalPage,
SessionHistoryRecord,
number,
ClientSessionPageRequest
> {
/**
* @param remote - generated Session namespace and Gateway stream factory.
* @param address - durable ordinary-Session or direct-subagent address.
* @param options - Session event-window destinations.
*/
constructor(
private readonly remote: SessionRemotes,
private readonly address: SessionAddress,
options: SessionEventStreamOptions,
) {
super(remote, {
name: 'session event stream',
emptyCursor: -1,
entries: page => page.records,
hasMore: page => page.hasMore,
first: historyRecordFirstSeq,
last: historyRecordLastSeq,
compare: (left, right) => left - right,
follows: (left, right) => right === left + 1,
publish: (change) => { options.publish(toSessionJournalChange(change)) },
...(options.carrierFailed === undefined
? {}
: { carrierFailed: options.carrierFailed }),
failed: options.failed,
})
}
/** @inheritdoc */
protected override async * follow(
request: ClientSessionPageRequest,
signal: AbortSignal,
): AsyncIterable<RemoteJournalFrame<SessionHistoryRecord, number, SessionJournalPage>> {
for await (const frame of this.remote.session.follow({
address: this.address,
...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
}, signal)) {
if (frame.type === 'snapshot') {
yield {
type: 'opened',
cursor: frame.cursor,
page: {
records: frame.records,
hasMore: frame.hasMore,
projections: frame.projections,
},
}
continue
}
yield { type: 'entry', entry: frame }
}
}
/** @inheritdoc */
protected override async readPage(
request: ClientSessionPageRequest,
throughSeq: number,
signal: AbortSignal,
): Promise<SessionJournalPage> {
const result = await this.remote.session.page(
{ address: this.address, throughSeq, ...request },
signal,
)
if (!result.ok) throw result.error
return result.value
}
/** @inheritdoc */
protected override repairRequest(
request: ClientSessionPageRequest,
): ClientSessionPageRequest {
return request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }
}
}