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:
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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in new issue
Block a user