mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
fix(inspector): reject source-frame limits without a Cordis snapshot budget
Share the topic length limit with the state record overhead and fail option resolution when maxSourceFrameBytes leaves no state payload budget.
This commit is contained in:
@@ -21,8 +21,8 @@
|
||||
en: 7db0c435e07d5005
|
||||
zh: 1573972326c33893
|
||||
/deepseek-ai-dsh-experimental-inspector/configuration:
|
||||
en: d094842893700446
|
||||
zh: 7a7f880ba795948f
|
||||
en: 8da502df555ab5b6
|
||||
zh: 849c39827b9b8365
|
||||
/deepseek-ai-dsh-experimental-inspector/observation-api:
|
||||
en: d32f994e6c9d0c4a
|
||||
zh: ac2f35f5799aeae0
|
||||
|
||||
@@ -78,7 +78,7 @@ The Host plugin injects `webServer` and `connection` and accepts these fields:
|
||||
| `maxBodyChunkBytes` | 48 KiB | Raw bytes carried by one body record before base64 encoding |
|
||||
| `maxJournalBytes` | 256 MiB | Worker-retained request and response body bytes |
|
||||
| `maxRetainedRequests` | `2000` | Active and completed requests retained by the Worker |
|
||||
| `maxSourceFrameBytes` | 128 KiB | Encoded source-frame limit |
|
||||
| `maxSourceFrameBytes` | 128 KiB | Encoded source-frame limit; values below 5121 bytes fail at load because they leave no room for the Cordis tree snapshot |
|
||||
| `maxSourceRecordsPerFrame` | `128` | Records in one source batch |
|
||||
| `maxQueuedRecords` | `2048` | Per-producer records waiting for transport |
|
||||
| `maxQueuedBytes` | 16 MiB | Per-producer queued encoded bytes |
|
||||
|
||||
@@ -78,7 +78,7 @@ Host 插件注入 `webServer` 和 `connection`,接受以下字段:
|
||||
| `maxBodyChunkBytes` | 48 KiB | base64 编码前一条 body 记录携带的原始字节数 |
|
||||
| `maxJournalBytes` | 256 MiB | Worker 保留的请求与响应 body 总字节数 |
|
||||
| `maxRetainedRequests` | `2000` | Worker 保留的进行中与已完成请求总数 |
|
||||
| `maxSourceFrameBytes` | 128 KiB | 编码后的 source frame 上限 |
|
||||
| `maxSourceFrameBytes` | 128 KiB | 编码后的 source frame 上限;小于 5121 字节时加载失败,因为容纳不下 Cordis 树快照 |
|
||||
| `maxSourceRecordsPerFrame` | `128` | 每个 source batch 的记录数 |
|
||||
| `maxQueuedRecords` | `2048` | 每个 producer 等待发送的记录数 |
|
||||
| `maxQueuedBytes` | 16 MiB | 每个 producer 等待发送的编码字节数 |
|
||||
|
||||
@@ -4,6 +4,7 @@ import { randomBytes, randomUUID } from 'node:crypto'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { MessageChannel, Worker, type MessagePort, type WorkerOptions } from 'node:worker_threads'
|
||||
import type { InspectorClientBootstrap, InspectorWorkerBoot, InspectorWorkerConfig } from '../../shared/bridge/messages/control.ts'
|
||||
import { maxStatePayloadBytes } from '../../shared/bridge/buffer.ts'
|
||||
import { INSPECTOR_PROTOCOL_VERSION } from '../../shared/bridge/version.ts'
|
||||
import type { InspectorConnection } from '../../shared/bridge/publisher.ts'
|
||||
import { installFetchObserver, NETWORK_TOPICS, type FetchObserver } from '../inspection/network.ts'
|
||||
@@ -51,7 +52,7 @@ export interface InspectorOptions {
|
||||
readonly maxJournalBytes?: number
|
||||
/** Maximum active and completed fetch requests retained by the Worker. */
|
||||
readonly maxRetainedRequests?: number
|
||||
/** Maximum encoded bytes accepted in one source transport frame. */
|
||||
/** Maximum encoded bytes accepted in one source transport frame; values below 5121 leave no Cordis snapshot budget and fail. */
|
||||
readonly maxSourceFrameBytes?: number
|
||||
/** Maximum observation records accepted in one source batch. */
|
||||
readonly maxSourceRecordsPerFrame?: number
|
||||
@@ -168,6 +169,9 @@ export function resolveInspectorOptions(options: InspectorOptions = {}): Inspect
|
||||
if (largestEncodedChunk > spec.maxSourceFrameBytes) {
|
||||
throw new Error('inspector: maxSourceFrameBytes cannot carry one base64 body chunk')
|
||||
}
|
||||
if (maxStatePayloadBytes(spec.maxSourceFrameBytes) <= 0) {
|
||||
throw new Error('inspector: maxSourceFrameBytes cannot carry a Cordis tree snapshot')
|
||||
}
|
||||
if (spec.clientReconnectMaxMs < spec.clientReconnectBaseMs) {
|
||||
throw new Error('inspector: clientReconnectMaxMs must be at least clientReconnectBaseMs')
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import type { InspectorRecordInput, SourceAppendFrame, SourceReplaceFrame } from
|
||||
import { INSPECTOR_PROTOCOL_VERSION } from './version.ts'
|
||||
|
||||
const SOURCE_FRAME_OVERHEAD_BYTES = 4_096
|
||||
/** Upper bound on the JSON around one replacement state payload: array, `monotonicMs`, and a 128-code-unit escaped topic. */
|
||||
const MAX_TOPIC_CODE_UNITS = 128
|
||||
/**
|
||||
* Upper bound on the JSON around one replacement state payload: 38 bytes of punctuation and keys,
|
||||
* at most 24 for `monotonicMs`, and at most 6 per escaped topic code unit plus quotes (770).
|
||||
*/
|
||||
const STATE_RECORD_OVERHEAD_BYTES = 1_024
|
||||
|
||||
/**
|
||||
@@ -140,8 +144,8 @@ export class InspectorSourceBuffer {
|
||||
}
|
||||
|
||||
private record(topic: string, payload: InspectorJsonValue, monotonicMs: number): InspectorRecordInput {
|
||||
if (topic.length === 0 || topic.length > 128) {
|
||||
throw new Error('inspector: topic must contain 1 to 128 characters')
|
||||
if (topic.length === 0 || topic.length > MAX_TOPIC_CODE_UNITS) {
|
||||
throw new Error(`inspector: topic must contain 1 to ${String(MAX_TOPIC_CODE_UNITS)} characters`)
|
||||
}
|
||||
if (!this.options.topics.includes('*') && !this.options.topics.includes(topic)) {
|
||||
throw new Error(`inspector: source does not declare topic ${JSON.stringify(topic)}`)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { MessageChannel } from 'node:worker_threads'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveInspectorOptions } from '../src/host/bridge/controller.ts'
|
||||
import { HostBridgePublisher } from '../src/host/bridge/publisher.ts'
|
||||
import { inspectorId } from '../src/shared/bridge/ids.ts'
|
||||
import { InspectorSourceBuffer, maxStatePayloadBytes, type InspectorSourceBufferOptions } from '../src/shared/bridge/buffer.ts'
|
||||
@@ -91,6 +92,14 @@ describe('Inspector source buffer', () => {
|
||||
expect(new TextEncoder().encode(frame).byteLength).toBeLessThanOrEqual(maxFrameBytes)
|
||||
})
|
||||
|
||||
it('rejects a source-frame limit that leaves no state payload budget at option resolution', () => {
|
||||
const smallest = 5_121
|
||||
expect(maxStatePayloadBytes(smallest)).toBe(1)
|
||||
expect(resolveInspectorOptions({ maxSourceFrameBytes: smallest, maxBodyChunkBytes: 1 }).maxSourceFrameBytes).toBe(smallest)
|
||||
expect(() => resolveInspectorOptions({ maxSourceFrameBytes: smallest - 1, maxBodyChunkBytes: 1 }))
|
||||
.toThrow('maxSourceFrameBytes cannot carry a Cordis tree snapshot')
|
||||
})
|
||||
|
||||
it('retains every pruned Cordis snapshot whose collector uses the state budget', async (test) => {
|
||||
const root = new Context()
|
||||
test.onTestFinished(async () => { await root.fiber.dispose() })
|
||||
|
||||
Reference in New Issue
Block a user