Merge remote-tracking branch 'origin/master' into docs/post-v3-release-proofreading

# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
#	.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md
#	README.i18n.yaml
#	README.zh.md
#	scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
xjt
2026-08-12 16:45:48 +08:00
254 changed files with 5437 additions and 517 deletions

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: cc9e4e01bb0dec278cf162ae442154d11c76aa85
README.md: d3727df981ecbc022345def48b38fbc879e29cb2
README.zh.md: 5e632bf6d7135bda60178f0699691aff2d5623db

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.

View File

@@ -23,3 +23,4 @@ node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-r
## 已知限制与暂缓事项
- **History 会恢复未附加的会话**:打开 history 可能创建宿主侧 agent,并增加首次打开的延迟;没有仅从持久化读取的路径。
- **`/api` 桥把每个请求体整体缓冲在内存里**:`maxRequestBodyBytes`(默认 160 MiB,按默认 100 MiB 图片总量上限经 base64 膨胀加信封余量得出)因此同时是单请求的驻留内存上界;要降低它而不缩小图片限额,需要流式请求体路径。

View File

@@ -979,6 +979,18 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
values['contextPressure'] = contextPressureOf(log)
// Always present (token-meter composed): heuristic request composition.
values['contextBreakdown'] = contextBreakdownOf(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
}

View File

@@ -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:

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']

View File

@@ -164,6 +164,13 @@ describe('createFixtureApi', () => {
toolsTokens: 0,
messageTokens: 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'],
},
} },
})
})
@@ -374,10 +381,14 @@ describe('createFixtureApi', () => {
value: { systemTokens: 0, toolsTokens: 0 },
})
expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0)
expect(first[9]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[9]?.rpcId).toBe(first[9]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[10]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[10]?.rpcId).toBe(first[10]?.rpcId)
expect(first[9]?.payload).toMatchObject({
type: 'session/projection', sessionId: 'fx-alpha', key: 'imageLimits',
value: { maxImagesPerMessage: 20, maxImageBytes: 5 * 1024 * 1024 },
})
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)
})
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {