Merge remote-tracking branch 'origin/master' into fix/stats

This commit is contained in:
07akioni
2026-08-12 21:22:05 +08:00
465 changed files with 11350 additions and 1149 deletions
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/connection/README.md
README.md: a82bb55ab65df2732ad16248d2cc9aa15b60e94d
README.zh.md: d4c19db8e74e3562ec55b5ef60675aef79b82008
README.md: d3727df981ecbc022345def48b38fbc879e29cb2
README.zh.md: 69af4cdba553399c0db0e0587e540c2a7f5375d2
+1
View File
@@ -23,3 +23,4 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **History resumes an unattached session** — opening history may create the host-side agent and add latency to the first open; there is no persistence-only read path.
- **The `/api` bridge buffers each request body in memory** — `maxRequestBodyBytes` (default 160 MiB, sized for the default 100 MiB aggregate image limit after base64 expansion plus envelope headroom) is therefore also the per-request resident bound; a streaming body path would be needed to lower it without shrinking the image limits.
+1
View File
@@ -23,3 +23,4 @@ node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-r
## 已知限制与暂缓事项
- **History 会恢复未附加的会话**:打开 history 可能创建宿主侧 agent,并增加首次打开的延迟;没有仅从持久化读取的路径。
- **`/api` 桥把每个请求体整体缓冲在内存里**:`maxRequestBodyBytes`(默认 160 MiB,按默认 100 MiB 图片总量上限经 base64 膨胀加信封余量得出)因此同时是单请求的驻留内存上界;要降低它而不缩小图片限额,需要流式请求体路径。
@@ -353,7 +353,7 @@ function fixtureUsage(turn: number, step: number): TokenUsage {
}
}
/** fx-alpha history script: 74 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50),
/** fx-alpha history script: 75 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50),
* mixing reasoning blocks / tool call+result / context. */
function buildAlphaLog(): SessionEvent[] {
const events: Record<string, unknown>[] = []
@@ -489,7 +489,7 @@ function buildAlphaLog(): SessionEvent[] {
push({ type: 'step/end', data: { turn, step: 0 } })
push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
}
// Turn 73: todo_write sample — the TodoRow toolview in the flow plus the
// Turn 74: todo_write sample — the TodoRow toolview in the flow plus the
// todo/write snapshot event feeding the TodoPanel plan strip. Two items are
// in_progress: this fixture chooses the parallel policy, so both surfaces
// must render a parallel plan rather than the first active item alone.
@@ -548,20 +548,35 @@ function buildAlphaLog(): SessionEvent[] {
toolTurn(70, 'web_search', '{"query":"deepseek harness architecture"}', 'Search results for deepseek harness architecture.')
toolTurn(71, 'web_fetch', '{"url":"https://www.deepseek.com/blog/harness-architecture"}', '# Harness architecture\n\nEverything is a plugin.')
// Turn 72: user and assistant images share one durable fixture object.
// The todo turn remains last so its standing projection stays visible.
// Turn 72: max-tokens sample — the provider ends the turn at its output cap
// mid-sentence, so the chat flow must render the turn-max-tokens notice
// instead of ending silently. Ordered before the todo turn for the same
// standing-plan reason the bash turn is.
push({ type: 'turn/start', data: { turn: 72 } })
push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text('问题 72:请完整列出全部一百条条目。')) })
push({ type: 'step/start', data: { turn: 72, step: 0 } })
push({
type: 'assistant/message',
surfaceOp: 'append',
data: { turn: 72, step: 0, message: assistantMessage(text('条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被')) },
})
push({ type: 'step/end', data: { turn: 72, step: 0 } })
push({ type: 'turn/end', data: { turn: 72, reason: { kind: 'max-tokens' } } })
// Turn 73: user and assistant images share one durable fixture object.
// The todo turn remains last so its standing projection stays visible.
push({ type: 'turn/start', data: { turn: 73 } })
push({
type: 'user/message',
surfaceOp: 'append',
data: userMessage([{ type: 'image', attachment: FIXTURE_IMAGE_REF }, ...text('历史用户图片')]),
})
push({ type: 'step/start', data: { turn: 72, step: 0 } })
push({ type: 'step/start', data: { turn: 73, step: 0 } })
push({
type: 'assistant/message',
surfaceOp: 'append',
data: {
turn: 72,
turn: 73,
step: 0,
message: assistantMessage(
[...text('结构化模型图片:'), { type: 'image', attachment: FIXTURE_IMAGE_REF }],
@@ -569,11 +584,11 @@ function buildAlphaLog(): SessionEvent[] {
),
},
})
push({ type: 'step/end', data: { turn: 72, step: 0 } })
push({ type: 'turn/end', data: { turn: 72, reason: { kind: 'completed' } } })
push({ type: 'step/end', data: { turn: 73, step: 0 } })
push({ type: 'turn/end', data: { turn: 73, reason: { kind: 'completed' } } })
const todoArgs = JSON.stringify({ todos: fixtureTodos })
toolTurn(73, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 2 in progress, 1 completed.')
toolTurn(74, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 2 in progress, 1 completed.')
// The real tool appends the snapshot mid-execution — between tool/call and
// tool/result — so the fixture reproduces that exact ordering (the last
// toolTurn events run ... tool/call, tool/result, step/end, turn/end).
@@ -1052,6 +1067,18 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
values['contextBreakdown'] = contextBreakdownOf(log)
// Always present (session-stats unit composed): whole-log turn/step counts.
values['sessionStats'] = sessionStatsOf(log)
// Always present (attachment service composed): the deployment image
// limits, constant per boot (mirrors the attachment-local defaults).
// Deliberate host divergence: the real gateway never pushes an imageLimits
// change frame (constant unit), but the fixture's uniform baseline replay
// frames every key here, incidentally exercising higher-seq-wins.
values['imageLimits'] = {
maxImageBytes: 5 * 1024 * 1024,
maxImagesPerMessage: 20,
maxMessageImageBytes: 100 * 1024 * 1024,
maxImagePixels: 40_000_000,
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
}
return values
}
@@ -1516,7 +1543,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
['my-agent', { trust: 'user', content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n" }],
])
let fixtureDefaultPreset = 'standard'
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 74]])
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 75]])
let nextSession = 1
let nextRpc = 1
let attachedSessions = options.empty ? 0 : 1
@@ -5,6 +5,12 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
/** Default carrier cap for all HTTP RPC bodies: sized for the default
* aggregate image limit (100 MiB) after base64 expansion plus envelope
* headroom (~134.3 MiB required), rounded up for slack. The bridge buffers
* each body in memory, so this cap is also the per-request resident bound. */
export const DEFAULT_MAX_REQUEST_BODY_BYTES = 160 * 1024 * 1024
/** Transport-independent request handler consumed by the Host HTTP bridge. */
export interface FetchHandler {
/**
@@ -27,7 +33,7 @@ export async function bridge(
req: IncomingMessage,
res: ServerResponse,
apiHandler: FetchHandler,
maxRequestBodyBytes = 32 * 1024 * 1024,
maxRequestBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES,
): Promise<void> {
const abort = new AbortController()
// Client-disconnect detection MUST hang off the response, not the request:
+1 -3
View File
@@ -6,7 +6,7 @@ import type {} from '@deepseek-ai/dsh-attachment'
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
import { API_PATH, HOST_EVENTS_PATH, MUX_EVENTS_PATH } from './api-path.ts'
import { bridge } from './http-bridge.ts'
import { bridge, DEFAULT_MAX_REQUEST_BODY_BYTES } from './http-bridge.ts'
import { assertTrustedAuthority, isTrustedApiRequest } from './api-request-trust.ts'
import { HostConnectionService } from './rpc-host.ts'
import { rejectWebSocketUpgrade, WebSocketDownlinks } from './websocket-downlink.ts'
@@ -42,8 +42,6 @@ function assertImageBodyCapacity(ctx: Context, maxRequestBodyBytes: number): voi
)
}
}
/** Default carrier cap for all HTTP RPC bodies. */
const DEFAULT_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024
/** Services required before providing Connection; API Proxy is an optional `/api` fallback. */
export const inject = ['httpServer']
@@ -168,6 +168,13 @@ describe('createFixtureApi', () => {
sessionStats: {
turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0,
},
imageLimits: {
maxImageBytes: 5 * 1024 * 1024,
maxImagesPerMessage: 20,
maxMessageImageBytes: 100 * 1024 * 1024,
maxImagePixels: 40_000_000,
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
},
} },
})
})
@@ -357,7 +364,7 @@ describe('createFixtureApi', () => {
const envelopes: RpcRequest<MuxFrame>[] = []
for await (const envelope of api.events.mux(req({}), abort.signal)) {
envelopes.push(envelope)
if (envelopes.length >= 12) abort.abort()
if (envelopes.length >= 13) abort.abort()
}
return envelopes
}
@@ -380,10 +387,14 @@ describe('createFixtureApi', () => {
expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0)
expect(first[9]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'sessionStats' })
expect((first[9]?.payload as { value: { turns: number; steps: number } }).value.steps).toBeGreaterThan(0)
expect(first[10]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[10]?.rpcId).toBe(first[10]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[11]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[11]?.rpcId).toBe(first[11]?.rpcId)
expect(first[10]?.payload).toMatchObject({
type: 'session/projection', sessionId: 'fx-alpha', key: 'imageLimits',
value: { maxImagesPerMessage: 20, maxImageBytes: 5 * 1024 * 1024 },
})
expect(first[11]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[11]?.rpcId).toBe(first[11]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[12]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[12]?.rpcId).toBe(first[12]?.rpcId)
})
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {