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.
This commit is contained in:
imccyu committed 2026-08-29 03:12:43 +08:00
1 parent 02a542f49f
commit 674a1e95a3
7 files changed
+45 -10

No files matched your search

@@ -108,7 +108,7 @@ describe('RemoteStream', () => {
], carrierFailed)
await expect(stream[Symbol.asyncIterator]().next()).rejects.toMatchObject({
isDSHRemoteGatewayError: true,
isDSHRemoteError: true,
code: 'gateway/internal',
message: 'isolated retry failed',
details: {},
@@ -130,7 +130,7 @@ describe('RemoteStream', () => {
})
await expect(stream[Symbol.asyncIterator]().next()).rejects.toMatchObject({
isDSHRemoteGatewayError: true,
isDSHRemoteError: true,
code: 'gateway/internal',
message: 'generation exploded',
})
@@ -1,6 +1,7 @@
/** 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,
@@ -60,7 +61,11 @@ function toSessionJournalChange(
return { ...change, entries: historyEntries(change.entries) }
case 'append': {
if (change.entry.type !== 'event') {
throw new Error('session live stream emitted a packed history record')
throw new RemoteError(
'gateway/internal',
'session live stream emitted a packed history record',
{},
)
}
return {
type: 'append',
@@ -99,6 +99,29 @@ describe('Session open', () => {
expect(api.followStarts).toHaveLength(2)
})
it('lands a packed live record in openState=error instead of crashing the stream loop', async () => {
const { api, session } = makeSession()
api.onHistory = () => histResponse(plainTurn(0, 0, 'a', 'b'))
await session.open()
expect(session.getSnapshot().openState).toBe('open')
// The live tail may carry only events; a packed record breaks that contract.
await api.pushFollow(SID, {
type: 'chunks',
event: {
type: 'chunkrow/text-chunks',
seq: 6,
time: 6,
data: { turn: 1, step: 1, index: 0, texts: ['a'], dt: [] },
},
} as never)
await vi.waitFor(() => { expect(session.getSnapshot().openState).toBe('error') })
expect(session.getSnapshot().openError).toMatchObject({
code: 'gateway/internal', message: 'session live stream emitted a packed history record',
})
})
it('lands a Gateway-marked stream failure in openState=error', async () => {
const { api, session } = makeSession()
api.onHistory = () => Promise.reject(new Error('socket died'))
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import {
isRemoteFailure,
RemoteStream,
RemoteStreamCarrierError,
type RemoteStreamOptions,
@@ -181,7 +182,10 @@ describe('Session Client stream adapters', () => {
await stream.open({})
await vi.waitFor(() => { expect(failed).toHaveBeenCalledOnce() })
expect(failed.mock.calls[0]?.[0]).toMatchObject({
const violation: unknown = failed.mock.calls[0]?.[0]
expect(isRemoteFailure(violation)).toBe(true)
expect(violation).toMatchObject({
code: 'gateway/internal',
message: 'session live stream emitted a packed history record',
})
await stream.dispose()
@@ -4608,7 +4608,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'RemoteError',
declaration: 'export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends Error {\n readonly isDSHRemoteGatewayError: true;\n constructor(readonly code: Code, message: string, readonly details: RemoteErrorDetailsMap[Code], options?: ErrorOptions);\n}',
declaration: 'export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends Error {\n readonly isDSHRemoteError: true;\n constructor(readonly code: Code, message: string, readonly details: RemoteErrorDetailsMap[Code], options?: ErrorOptions);\n}',
},
{
name: 'RemoteErrorCode',
+6 -3
View File
@@ -11,7 +11,7 @@ import type { RemoteErrorCode, RemoteErrorDetailsMap, RemoteFailure } from './ty
*/
export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends Error {
/** Structural marker: cross-realm/bundle identification never uses instanceof. */
readonly isDSHRemoteGatewayError: true = true
readonly isDSHRemoteError: true = true
/**
* @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
@@ -38,8 +38,11 @@ export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends
* @returns the failure when the marker matches, otherwise undefined.
*/
export function remoteErrorOf(value: unknown): RemoteFailure | undefined {
if (value instanceof Error
&& (value as { isDSHRemoteGatewayError?: unknown }).isDSHRemoteGatewayError === true) {
// Structural, not instanceof: an Error thrown in another realm (iframe, VM)
// fails instanceof Error here, so the marker plus the code field is the test.
if (typeof value === 'object' && value !== null
&& (value as { isDSHRemoteError?: unknown }).isDSHRemoteError === true
&& typeof (value as { code?: unknown }).code === 'string') {
return value as unknown as RemoteFailure
}
return undefined
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-time`.
* @module @deepseek-ai/dsh-time/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-util-time`.
* @module @deepseek-ai/dsh-util-time/invariant
*/
/* jscpd:ignore-start */