From fe9cd80b288ff6613c3d983adcae10ceb3e5b2fb Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sat, 31 Oct 2026 10:40:31 +0800 Subject: [PATCH] revert: remove follow-up CI repairs --- ...backend-continuation-performance.i18n.yaml | 3 - ...-09-06-backend-continuation-performance.md | 18 ------ ...-06-backend-continuation-performance.zh.md | 18 ------ ...-06-frontend-performance-budgets.i18n.yaml | 7 +-- ...2026-09-06-frontend-performance-budgets.md | 22 ++----- ...6-09-06-frontend-performance-budgets.zh.md | 22 ++----- .github/workflows/ci.yml | 4 +- apps/web/tests/README.i18n.yaml | 4 +- apps/web/tests/README.md | 2 +- apps/web/tests/README.zh.md | 2 +- apps/web/tests/goal-multi-turn-actions.e2e.ts | 7 +-- .../conversation-fold.bench.client.ts | 22 ++----- .../client/ui-sidebar-files/README.i18n.yaml | 4 +- packages/client/ui-sidebar-files/README.md | 2 - packages/client/ui-sidebar-files/README.zh.md | 2 - .../src/client/FilesBody.module.css | 7 +-- .../ui-sidebar-files/src/client/face.ts | 7 +-- .../tests/navigation.client.spec.tsx | 29 +--------- packages/llm/llm/README.i18n.yaml | 4 +- packages/llm/llm/README.md | 2 - packages/llm/llm/README.zh.md | 2 - packages/llm/llm/src/content.ts | 11 +--- packages/llm/llm/src/index.ts | 30 ++++------ packages/llm/llm/tests/content.spec.ts | 13 +---- packages/llm/llm/tests/service.spec.ts | 57 +++++++++---------- .../ptc-runtime-node/tests/runtime.spec.ts | 8 +-- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 2 +- python/sdk-runtime/README.zh.md | 2 +- scripts/build-exe-for-python-sdk.spec.ts | 1 - scripts/build-exe-for-python-sdk.ts | 2 +- scripts/run-bench-suite.ts | 8 +-- vitest.bench.config.ts | 9 +-- 33 files changed, 95 insertions(+), 242 deletions(-) diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml index 3ca97fe5cf..fe9f28b0f2 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml @@ -17,9 +17,6 @@ /agent-note-performance-baselines-for-tool-heavy-backend-continuation/calibration-evidence/sdk-minimal-checkpoint-calibration: en: f4b52851e35cec56 zh: b419546dda8abf21 -/agent-note-performance-baselines-for-tool-heavy-backend-continuation/calibration-evidence/unchanged-request-projection: - en: 10711f81b5eda63f - zh: 210ce3ffaee986f5 /agent-note-performance-baselines-for-tool-heavy-backend-continuation/alternatives-considered: en: 3a61dff7c926eb22 zh: 8f2633b16be78233 diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md index fe768d4ea5..e1de8eec76 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md @@ -70,24 +70,6 @@ The upper observed run median is 5,432.567 ms; the slowest single sample is 5,94 This calibration accepts the measured cost of the stronger durability policy; it is not a performance improvement. Local on/off diagnosis attributes most additional persistence time to frequent file synchronization. The browser input-overlap failures in attempts 1 and 4 are separate failures and receive no exemption from this budget. Deterministic controls use the measured-verdict assertion: the recorded upper-median sample group fails the old 4,250 ms ceiling and passes 7,000 ms, while a synthetic 7,800 ms median remains rejected. -### Unchanged request projection - -Developer-message filtering and foreign replay-state removal inspect every request, allocating output arrays only after a message changes. Unchanged histories retain their array identity, and retained messages keep their identities. This removes discarded full-history arrays and a final equality scan without caching mutable inputs or adapter ownership. - -On macOS arm64 with Node 24.22.2, the compiled continuation worker uses five fresh processes per variant, each copying the same seeded history into a private temporary root. Every sample completes 40 requests and 13,925 events over 800 historical turns. The table reports total elapsed milliseconds, rounded to three decimal places; fixture setup, library builds and a separate CPU profile are outside these measurements. - -| Implementation | Five request-history totals (ms) | Median (ms) | -|---|---|---:| -| Original | 55.750, 52.999, 54.812, 53.478, 55.106 | 54.812 | -| Developer filtering only | 49.431, 49.826, 51.421, 50.679, 49.494 | 49.826 | -| Replay-state stripping only | 54.444, 53.043, 52.007, 52.776, 53.101 | 53.043 | -| Combined | 47.814, 47.326, 49.925, 47.867, 48.722 | 47.867 | -| Original restored | 55.670, 55.497, 56.265, 54.353, 58.718 | 55.670 | -| Combined restored | 48.423, 48.717, 48.199, 48.534, 50.129 | 48.534 | -| Combined, for-of traversal | 48.280, 48.631, 51.909, 48.966, 54.301 | 48.966 | - -The final total median is 12.0% below the restored original. Median process CPU time falls from 79.751 to 72.009 ms, while post-GC retained heap remains about 1.35 MiB. Whole-process maximum RSS is 201.61/202.52 MiB for the restored original/final samples. Source restoration and rebuilding isolate the removed cost; both local implementations remain below the unchanged 297 ms CI budget, so this comparison is not a budget-rejection control or proof of hosted-runner stability. Functional tests cover unchanged-array identity, leading/trailing developer removal, mixed replay-state ownership, retained message identity and frozen projection output. - ## Alternatives considered **Repeat existing migration and first-open variants.** Rejected: those twelve cases already distinguish read-only preparation from writable publication. These cases use the current generation and begin or continue actual model work, or enumerate a corpus rather than open one Session. diff --git a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md index ac4c313762..cc96d63e95 100644 --- a/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md +++ b/.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md @@ -70,24 +70,6 @@ SDK-minimal profile 在 step、模型请求和顶层工具执行前等待检查 此校准接受更强持久化策略的实测成本,不表示性能已改善。本地开关对照将额外持久化时间的大部分归因于频繁的文件同步。第 1、4 次尝试中的浏览器输入重叠失败属于独立问题,不因本预算而豁免。确定性对照与实测判定使用同一断言:记录的最高中位数样本组被旧 4,250 ms 上限拒绝,在 7,000 ms 下通过;合成的 7,800 ms 中位数仍被拒绝。 -### 未改变的请求投影 - -Developer 消息过滤和跨适配器 replay-state 移除检查每次请求,只在消息改变后分配输出数组。未改变的历史保留数组身份,保留的消息维持对象身份。这移除了被丢弃的完整历史数组和最后的相等性扫描,不缓存可变输入或适配器归属。 - -在 macOS arm64、Node 24.22.2 上,编译后的续聊 worker 对每种变体运行五个全新进程,每次将相同的种子历史复制到独立临时根目录。每个样本基于 800 个历史轮次完成 40 次请求和 13,925 个事件。下表记录总耗时毫秒数,四舍五入至小数点后三位;夹具准备、library 构建和单独的 CPU profile 不计入这些测量。 - -| 实现 | 五次请求历史总耗时(ms) | 中位数(ms) | -|---|---|---:| -| 原实现 | 55.750, 52.999, 54.812, 53.478, 55.106 | 54.812 | -| 仅 developer 过滤 | 49.431, 49.826, 51.421, 50.679, 49.494 | 49.826 | -| 仅 replay-state 移除 | 54.444, 53.043, 52.007, 52.776, 53.101 | 53.043 | -| 组合 | 47.814, 47.326, 49.925, 47.867, 48.722 | 47.867 | -| 恢复原实现 | 55.670, 55.497, 56.265, 54.353, 58.718 | 55.670 | -| 恢复组合 | 48.423, 48.717, 48.199, 48.534, 50.129 | 48.534 | -| 组合(for-of 遍历) | 48.280, 48.631, 51.909, 48.966, 54.301 | 48.966 | - -最终总耗时中位数比恢复后的原实现低 12.0%。进程 CPU 时间中位数从 79.751 降至 72.009 ms,GC 后保留堆内存仍约为 1.35 MiB。恢复后的原实现/最终样本的全进程最大 RSS 为 201.61/202.52 MiB。恢复源码并重建隔离了被移除的成本;两个本地实现都低于未改变的 297 ms CI 预算,因此该比较不是预算拒绝对照,也不能证明托管运行器稳定性。功能测试覆盖未改变数组的身份、首尾 developer 消息移除、混合 replay-state 归属、保留消息身份和冻结投影输出。 - ## 考虑过的替代方案 **重复现有迁移和首次打开变体。** 拒绝:现有十二个用例已经区分只读准备与可写发布。这些用例使用当前代际并开始或继续实际模型工作,或者列举语料集合而不是打开单个 Session。 diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml index 1dcfbd8888..31c799468d 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml @@ -29,12 +29,9 @@ /agent-note-frontend-large-session-performance-budgets/calibration/history-selector-invalidation: en: 2d2f78f4cf6aad8a zh: aec6af5f7c66de16 -/agent-note-frontend-large-session-performance-budgets/calibration/navigator-stylesheet-scope: - en: 61ca5a8e0b052b22 - zh: f0392463611351f6 /agent-note-frontend-large-session-performance-budgets/calibration/streaming-assistant-visibility: - en: 0f49a122a43f9ac5 - zh: 017910642ee123a6 + en: 5dc3c84b80be0e8e + zh: 4bbd16e1f455ef85 /agent-note-frontend-large-session-performance-budgets/alternatives-considered: en: 4d2a4a64ccd14507 zh: 8a4aaf4d4abea13d diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md index 97d65b99f7..50cc863788 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md @@ -102,32 +102,22 @@ These local runs both failed the independent open budget: their open medians wer Post-GC browser heap was 80.70/80.71 MiB. GNU time reported maximum RSS of 535.78/561.80 MiB for each benchmark command and its children; this is not input-phase memory or an aggregate browser-memory measurement. Moving the composer's selection reveal into an animation frame was rejected: profiling showed the style flush moved to another geometry read, without improving typing. The product fix removes the invalidation source instead. -### Navigator stylesheet scope - -The files navigator marks its actual DOM parent during the existing drawing lifetime. Covered-content selectors require that marker before evaluating `:has(> .navigator…)`; replacement and disposal remove it from the previous parent. An unconstrained `:has()` subject made Chromium invalidate unrelated Chat flow subtrees even while no file navigator was mounted. A temporary invalidation trace attributes 1,893 flow-subtree invalidations to these two rules, with three style recalculations covering about 17,700 elements each. Tracing used 3× CPU throttling and adds observer overhead; these counts identify the cause, not endpoint latency. - -Separate unchanged three-sample runs on macOS arm64, Node 26.8.2 and Chromium 149.0.7827.55 measure the following original/scoped medians: first reply 690.56/460.95 ms, draft typing 161.97/115.71 ms, and stream main-thread task 1403.75/1174.59 ms. Before the source fix, a CSSOM-only scoped-selector control on the same built artifacts measures 480.46/121.68/1181.03 ms respectively, supporting the selector diagnosis. All samples retain 23,980 DOM nodes, preserve trusted input overlap, reject post-DONE input, and pass the unchanged endpoint budgets. Post-GC heap remains approximately 80.8 MiB. These local measurements do not establish a hosted CI verdict. - -The navigator lifetime regression covers repeated registration, parent replacement, independent panes and removal; removing the parent mark makes that regression fail. Browser file-navigation coverage retains the filled and fullscreen layouts, covered-content focus handoff, Escape and pane moves. Replay pacing, workload, input scheduling and benchmark assertions remain unchanged. - ### Streaming Assistant visibility Chat retains visibility per Assistant block. Same-kind text and reasoning appends inspect only the incoming delta until the block becomes visible, then reuse that result. This avoids repeated `trim()` calls on accumulated strings without changing whitespace, first-token timing, or visible anchors. Replacements recalculate visibility, retry resets it, and final messages rebuild it. Copying the block array remains proportional to its length; this does not make every Assembler operation constant-time. The [conversation-fold worker](../../../../benchmarks/conversation-fold/conversation-fold.worker.client.ts) measures a single 256 KiB block split into 4,096 or 16,384 deltas, using ordinary text or reasoning whose first half is whitespace. Five fresh compiled Node children per case reuse the existing benchmark registrations, production Assembler and Chat builder. Timing includes every append, one final flush, and reading every output character. Fixture preparation and GC are excluded; retained heap is the post-GC increment with the Assembler, live entries and published state still reachable. -The reference allowances remain 12/32 ms, but this endpoint has its own hosted expectations of 34/104 ms. Five-process medians on Node 24.22.1, Linux x64, four-vCPU Blacksmith runners ranged from 25.13 to 32.48 ms for 4,096 deltas and 65.56 to 101.90 ms for 16,384 deltas across run 41083633701 (jobs 124158958438 and 124166293761) and run 41088281578 (job 124175117777). These measurements execute the same streaming implementation; the latency-sensitive phase is serial in each run. Applying only the existing 1.25 variance headroom to the hosted expectations gives 43/130 ms limits. The global machine scale, five fresh children, workload, median verdict and heap limit are unchanged; these numbers are calibration, not a product speedup. - -Local macOS arm64 measurements on Node 24.22.2 use five fresh compiled children per case. The negative control replaces delta-local visibility checks with accumulated-string trimming while retaining every append, final publication and complete text read: +Local macOS arm64 measurements on Node 26.8.2 give these elapsed medians. The time limits use reference allowances of 12/32 ms and the existing shared scale and headroom; they are not new hosted-runner calibration. | Workload | Deltas | Repeated accumulated trim | Cached visibility | Time limit | |---|---:|---:|---:|---:| -| Ordinary text | 4,096 | 51.61 ms | 7.33 ms | 43 ms | -| Half-leading-whitespace reasoning | 4,096 | 151.14 ms | 7.62 ms | 43 ms | -| Ordinary text | 16,384 | 207.85 ms | 21.77 ms | 130 ms | -| Half-leading-whitespace reasoning | 16,384 | 573.18 ms | 22.06 ms | 130 ms | +| Ordinary text | 4,096 | 42.25 ms | 7.32 ms | 30 ms | +| Half-leading-whitespace reasoning | 4,096 | 252.75 ms | 7.96 ms | 30 ms | +| Ordinary text | 16,384 | 206.71 ms | 23.06 ms | 80 ms | +| Half-leading-whitespace reasoning | 16,384 | 1114.18 ms | 23.08 ms | 80 ms | -All four measured negative controls exceed the calibrated limits. Deterministic gate controls also reject the measured 32.48/101.90 ms hosted medians at the historical 30/80 ms limits, accept them at the calibrated limits, and reject a duration above either limit. Retained heap remains approximately 2.8/9.9 MiB for the two fragment counts, below the 20 MiB limit derived from a 16 MiB allowance. Maximum worker RSS across the negative/current samples is 163.78/99.88 MiB; it is a whole-process diagnostic, not aggregate application memory. This endpoint measures local assembly with one final publication, excluding network, browser rendering and Trajectory. Separate browser regressions cover settlement, cancellation, retry, reload, reopening and blank reasoning without asserting a browser speedup. +All four cases reject accumulated trimming and pass with cached visibility. Retained heap remains approximately 2.8/9.9 MiB for the two fragment counts, below the 20 MiB limit derived from a 16 MiB allowance. Maximum worker RSS across these samples falls from 185.70 to 89.36 MiB; it is a whole-process diagnostic, not aggregate application memory. This endpoint measures local assembly with one final publication, excluding network, browser rendering and Trajectory. Separate browser regressions cover settlement, cancellation, retry, reload, reopening and blank reasoning without asserting a browser speedup. ## Alternatives considered diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md index 08371ab56f..1c469a7768 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md @@ -102,32 +102,22 @@ Trajectory 激活在动画帧上采样可见搜索框与 `[data-trajectory-scrol 强制 GC 后的浏览器 heap 为 80.70/80.71 MiB。GNU time 为两次基准命令及其子进程报告的最大 RSS 为 535.78/561.80 MiB;这不是输入阶段内存,也不是浏览器总内存测量。曾尝试将输入框的选区显示推迟到动画帧,但分析显示样式刷新转移到另一处几何读取,没有改善键入,因此未采用。产品修复直接移除样式失效的来源。 -### 导航栏样式作用域 - -文件导航栏在现有绘制生命周期内标记其实际 DOM 父元素。覆盖内容选择器要求该标记存在,再判断 `:has(> .navigator…)`;替换和释放绘制时移除旧父元素上的标记。未限定主体的 `:has()` 会让 Chromium 在未挂载文件导航栏时,也使无关 Chat 流子树失效。临时失效追踪将 1,893 次流子树失效归因于这两条规则,其中三次样式重算各涉及约 17,700 个元素。追踪使用 3 倍 CPU 节流,并增加观察器开销;这些计数定位原因,不代表终点延迟。 - -在 macOS arm64、Node 26.8.2 和 Chromium 149.0.7827.55 上,分别运行未改变的三样本基准,原始/限定作用域的中位数为:首次回复 690.56/460.95 ms、草稿键入 161.97/115.71 ms、流式主线程任务 1403.75/1174.59 ms。源码修复前,使用相同构建产物且仅通过 CSSOM 限定选择器的对照,分别测得 480.46/121.68/1181.03 ms,支持选择器诊断。所有样本保留 23,980 个 DOM 节点,保持受信任输入重叠,拒绝 DONE 后输入,并通过未修改的终点预算。GC 后 heap 仍约为 80.8 MiB。这些本地测量不能证明托管 CI 已通过。 - -导航栏生命周期回归覆盖重复注册、父元素替换、独立分栏与移除;移除父元素标记会使该回归失败。文件导航浏览器覆盖保留铺满与全屏布局、被覆盖内容的焦点移交、Escape 及分栏移动。回放节奏、工作负载、输入调度和基准断言均不变。 - ### 流式 Assistant 可见性 Chat 保存每个 Assistant 块的可见状态。同类型文本和推理追加只检查新增片段,直到该块可见,之后复用结果。这避免对累计字符串重复调用 `trim()`,同时保留空白、首 token 时间及可见锚点。块替换重新计算可见性,重试清空状态,最终消息重建状态。块数组复制仍与其长度成正比,因此并非每次 Assembler 操作都是常数时间。 [conversation-fold worker](../../../../benchmarks/conversation-fold/conversation-fold.worker.client.ts) 测量单个 256 KiB 块,拆成 4,096 或 16,384 个片段,内容为普通文本或前半段为空白的推理。每个用例使用五个全新的已编译 Node 子进程,复用现有基准注册、生产 Assembler 和 Chat builder。计时包含每次追加、一次最终 flush 及逐字符读取输出。夹具准备和 GC 不计时;保留堆内存为 GC 后的增量,此时 Assembler、实时条目和已发布状态仍可达。 -参考额度仍为 12/32 ms,但此终点采用单独的 34/104 ms 托管运行器预期。在 Node 24.22.1、Linux x64、四 vCPU Blacksmith 运行器上,run 41083633701(job 124158958438 和 124166293761)及 run 41088281578(job 124175117777) 的五进程中位数范围为:4,096 片段 25.13–32.48 ms,16,384 片段 65.56–101.90 ms。这些测量执行相同的流式实现,各轮延迟敏感阶段均为串行。托管预期只乘现有 1.25 波动余量,得到 43/130 ms 上限。全局机器倍率、五个全新子进程、负载、中位数判定和堆内存上限保持不变;这是校准,不代表产品提速。 - -macOS arm64、Node 24.22.2 上的本地测量每个用例使用五个全新编译子进程。负向控制将新增片段的可见性检查替换为累计字符串 trim,保留每次追加、最终发布及完整文本读取: +macOS arm64、Node 26.8.2 上的本地测量得到以下耗时中位数。时间上限使用 12/32 ms 参考额度及现有共享缩放与余量,不代表新的托管运行器校准。 | 工作负载 | 片段数 | 累计文本重复 trim | 缓存可见性 | 时间上限 | |---|---:|---:|---:|---:| -| 普通文本 | 4,096 | 51.61 ms | 7.33 ms | 43 ms | -| 前半段为空白的推理 | 4,096 | 151.14 ms | 7.62 ms | 43 ms | -| 普通文本 | 16,384 | 207.85 ms | 21.77 ms | 130 ms | -| 前半段为空白的推理 | 16,384 | 573.18 ms | 22.06 ms | 130 ms | +| 普通文本 | 4,096 | 42.25 ms | 7.32 ms | 30 ms | +| 前半段为空白的推理 | 4,096 | 252.75 ms | 7.96 ms | 30 ms | +| 普通文本 | 16,384 | 206.71 ms | 23.06 ms | 80 ms | +| 前半段为空白的推理 | 16,384 | 1114.18 ms | 23.08 ms | 80 ms | -四个实测负向控制均超过校准后的上限。确定性门禁控制还验证:历史 30/80 ms 上限拒绝实测的 32.48/101.90 ms 托管中位数,校准后的上限接受这些中位数,并拒绝超过各自上限的耗时。两个片段规模的保留堆内存仍约为 2.8/9.9 MiB,低于 16 MiB 额度经余量计算后的 20 MiB 上限。负向/当前样本中的 worker 最大 RSS 为 163.78/99.88 MiB;它是整个进程的诊断值,不是应用总内存。该终点只测量一次最终发布的本地组装,不包含网络、浏览器渲染和 Trajectory。独立浏览器回归覆盖结算、取消、重试、刷新、重开及空白推理,不据此声称浏览器提速。 +四个用例都拒绝累计文本重复 trim,并在缓存可见性后通过。两个片段规模的保留堆内存仍约为 2.8/9.9 MiB,低于 16 MiB 额度经余量计算后的 20 MiB 上限。这些样本中的 worker 最大 RSS 从 185.70 降至 89.36 MiB;它是整个进程的诊断值,不是应用总内存。该终点只测量一次最终发布的本地组装,不包含网络、浏览器渲染和 Trajectory。独立浏览器回归覆盖结算、取消、重试、刷新、重开及空白推理,不据此声称浏览器提速。 ## 考虑过的替代方案 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3da0bda3e..916971cfa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,8 +274,8 @@ jobs: || 'ubuntu-24.04' }} name: node 24 / benchmarks # Wall-clock budgets need an otherwise idle runner, so this job runs the - # benchmark lane alone instead of joining a concurrent gate aggregate, with - # one measured file at a time inside that runner. The self-hosted + # benchmark lane alone instead of joining a concurrent gate aggregate, and + # keeps its own two-file parallelism inside that runner. The self-hosted # failover pool is shared, so the benchmarks stay on hosted Linux there. timeout-minutes: 20 steps: diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index b9ef470416..e314a707ba 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/web/tests/README.md /apps-web-browser-e2e: - en: fb3ce9b97f794a5d - zh: 1b042c0e6ac4bb6a + en: 2deef0df72a6465c + zh: b24989ffb2d8586a /apps-web-browser-e2e/completion-observations: en: 8c139966983959cc zh: 54f09f9b7ac84cda diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 0b52f08aee..969cf3a965 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -14,7 +14,7 @@ On Linux, `--with-deps` installs dependencies through the system package manager Ordinary scenarios begin with no registered Workspace or Session and a durable marker recording a removed default Workspace, so explicit folder-selection scenarios retain control of their cwd. `launchWebScaffold({ firstUse: true })` leaves initialization eligible for startup scenarios. -Same-day message goldens use the shared `WEB_FIXTURE_TIME`: seeded transcripts pass it as `createdAt` and pin the browser calendar with `pinBrowserClock`; scenarios that submit live messages, including autonomous Goal rounds, also use `pinHostClock` and restore it after teardown. These clocks advance with real elapsed time while keeping native timers intact, so midnight does not change a scenario's date labels. +Same-day message goldens use the shared `WEB_FIXTURE_TIME`: seeded transcripts pass it as `createdAt` and pin the browser calendar with `pinBrowserClock`; scenarios that submit live messages also use `pinHostClock` and restore it after teardown. These clocks advance with real elapsed time while keeping native timers intact, so midnight does not change a scenario's date labels. ## Completion observations diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index 82b4e06232..29234f33c6 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -14,7 +14,7 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps 普通场景以没有已登记 Workspace 或 Session、但持久化标记记录默认 Workspace 已被删除的状态启动,使显式文件夹选择场景自行决定 cwd。`launchWebScaffold({ firstUse: true })` 保留初始化资格,供启动场景使用。 -当天消息的 golden 使用共享的 `WEB_FIXTURE_TIME`:预置会话将它传为 `createdAt`,并通过 `pinBrowserClock` 固定浏览器日期;提交实时消息的场景(包括自主 Goal 回合)还使用 `pinHostClock`,在清理后恢复。固定后的时钟随真实耗时前进,保留原生定时器,因此跨午夜不会改变场景的日期标签。 +当天消息的 golden 使用共享的 `WEB_FIXTURE_TIME`:预置会话将它传为 `createdAt`,并通过 `pinBrowserClock` 固定浏览器日期;提交实时消息的场景还使用 `pinHostClock`,在清理后恢复。固定后的时钟随真实耗时前进,保留原生定时器,因此跨午夜不会改变场景的日期标签。 ## 完成状态观察 diff --git a/apps/web/tests/goal-multi-turn-actions.e2e.ts b/apps/web/tests/goal-multi-turn-actions.e2e.ts index 72f3e5400d..4a1c6e8710 100644 --- a/apps/web/tests/goal-multi-turn-actions.e2e.ts +++ b/apps/web/tests/goal-multi-turn-actions.e2e.ts @@ -14,7 +14,7 @@ import { assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { connectFreshWorkspace, newEnglishPage, pinBrowserClock, pinHostClock, saveFailureShot } from './support.ts' +import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/goal-multi-turn-actions', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl') @@ -95,7 +95,6 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = let page: Page let tripwire: ReturnType let sessionEvents: SessionEvent[] - let hostClock: ReturnType | undefined afterEach(async () => { const failures: unknown[] = [] @@ -104,8 +103,6 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = const closing = scaffold scaffold = undefined await closing?.close().catch((error: unknown) => failures.push(error)) - hostClock?.mockRestore() - hostClock = undefined if (failures.length === 1) throw failures[0] if (failures.length > 1) throw new AggregateError(failures, 'goal-multi-turn-actions teardown failed') }) @@ -113,7 +110,6 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = /** Boot the real Web composition and connect a fresh package fixture workspace. */ async function launch(): Promise { sessionEvents = [] - if (MODE !== 'record') hostClock = pinHostClock() scaffold = await launchWebScaffold( MODE === 'record' ? {} : { replayFixture: FIXTURE, replayOverride: OVERRIDE }, ) @@ -121,7 +117,6 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) }) browser = await chromium.launch() page = await newEnglishPage(browser) - if (MODE !== 'record') await pinBrowserClock(page) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) diff --git a/benchmarks/conversation-fold/conversation-fold.bench.client.ts b/benchmarks/conversation-fold/conversation-fold.bench.client.ts index 31e6a03ef2..3ab528a616 100644 --- a/benchmarks/conversation-fold/conversation-fold.bench.client.ts +++ b/benchmarks/conversation-fold/conversation-fold.bench.client.ts @@ -56,10 +56,10 @@ const PREPARING_CASES = [ const STREAMING_CHARACTERS = 256 * 1024 const STREAMING_SAMPLES = 5 -/** Reference allowances and separately measured hosted expectations; CI uses only variance headroom. */ +/** Reference-machine expectations; shared CI scaling applies only to elapsed time. */ const STREAMING_WORKLOADS = [ - { fragments: 4_096, expectedMs: 12, expectedCiMs: 34 }, - { fragments: 16_384, expectedMs: 32, expectedCiMs: 104 }, + { fragments: 4_096, expectedMs: 12 }, + { fragments: 16_384, expectedMs: 32 }, ] as const const STREAMING_CASES = STREAMING_WORKLOADS.flatMap(workload => [ { ...workload, blockType: 'text', pattern: 'ordinary' }, @@ -143,18 +143,6 @@ describe('preparing tool arguments', () => { }) describe('live Assistant text publication', () => { - it.each([ - { workload: STREAMING_WORKLOADS[0], observedMs: 32.475685, regressionMs: 51.607958, expectedBudget: 43 }, - { workload: STREAMING_WORKLOADS[1], observedMs: 101.9018, regressionMs: 207.845958, expectedBudget: 130 }, - ])('accepts hosted $workload.fragments-delta samples and rejects accumulated trimming', ({ workload, observedMs, regressionMs, expectedBudget }) => { - const budgetMs = Math.ceil(workload.expectedCiMs * PERFORMANCE_BUDGET_HEADROOM) - expect(budgetMs).toBe(expectedBudget) - expect(observedMs).toBeGreaterThan(ciTimeBudget(workload.expectedMs)) - expect(observedMs).toBeLessThanOrEqual(budgetMs) - expect(() => expect(regressionMs).toBeLessThanOrEqual(budgetMs)).toThrow() - expect(() => expect(budgetMs + 1).toBeLessThanOrEqual(budgetMs)).toThrow() - }) - it.each(STREAMING_CASES)('publishes $fragments $blockType deltas with $pattern content within bounded CPU and heap costs', async (workload) => { const samples: StreamingAssistantWorkerReport[] = [] for (let index = 0; index < STREAMING_SAMPLES; index++) { @@ -173,14 +161,14 @@ describe('live Assistant text publication', () => { } const median = samples.toSorted((a, b) => a.elapsedMs - b.elapsedMs)[Math.floor(STREAMING_SAMPLES / 2)]! const retainedMb = samples.map(sample => sample.retainedMb).toSorted((a, b) => a - b)[Math.floor(STREAMING_SAMPLES / 2)]! - const budgetMs = Math.ceil(workload.expectedCiMs * PERFORMANCE_BUDGET_HEADROOM) + const budgetMs = ciTimeBudget(workload.expectedMs) const budgetMb = EXPECTED_STREAMING_RETAINED_MB * PERFORMANCE_BUDGET_HEADROOM const benchmarkCase: BenchmarkCase = { id: `conversation-fold/streaming-${workload.blockType}-${workload.pattern}-${String(workload.fragments)}`, measures: `Client append, final Chat publication, and complete text read for ${String(STREAMING_CHARACTERS / 1024)} KiB in ${String(workload.fragments)} ${workload.blockType} deltas with ${workload.pattern} content.`, affects: 'Local processing of a long active model response; excludes model pacing, network transport, and browser rendering.', } - console.log(JSON.stringify({ benchmark: benchmarkCase.id, samples, medianMs: median.elapsedMs, retainedMb, referenceMs: workload.expectedMs, expectedCiMs: workload.expectedCiMs, budgetMs, budgetMb })) + console.log(JSON.stringify({ benchmark: benchmarkCase.id, samples, medianMs: median.elapsedMs, retainedMb, budgetMs, budgetMb })) recordTimings(benchmarkCase, { medianMs: { ms: median.elapsedMs, cpuMs: median.cpuUserMs + median.cpuSystemMs }, }, { medianMs: budgetMs }) diff --git a/packages/client/ui-sidebar-files/README.i18n.yaml b/packages/client/ui-sidebar-files/README.i18n.yaml index e5d9114e94..d420350ecc 100644 --- a/packages/client/ui-sidebar-files/README.i18n.yaml +++ b/packages/client/ui-sidebar-files/README.i18n.yaml @@ -18,8 +18,8 @@ en: 2b620997cf39cac8 zh: ddfaa2da1c1bcd55 /deepseek-ai-dsh-client-ui-sidebar-files/the-navigator: - en: 656c9a3639594a33 - zh: 05e7ee796bc23c9a + en: 803d10142360264b + zh: 042568a8ba293037 /deepseek-ai-dsh-client-ui-sidebar-files/the-tree: en: c6d5d2d068bdec7b zh: dd41bcfa3e9080ad diff --git a/packages/client/ui-sidebar-files/README.md b/packages/client/ui-sidebar-files/README.md index 27862d7a0b..7443828930 100644 --- a/packages/client/ui-sidebar-files/README.md +++ b/packages/client/ui-sidebar-files/README.md @@ -44,8 +44,6 @@ Render `sidebar.files.navigator` with `FileNavigatorInput`: `{ useTabInfo, page, The navigator is drawn below the preview's header, which spans the pane, at the right of the row it shares with the body. Beside a preview, its pane's record decides whether it shows: a pane starts with it hidden, and the choice holds for every file tab of the pane until the toggle, Escape, the workspace command, or navigation to a covered preview changes it. A pane without a record of its own, such as a floating panel a tab was dragged out into, starts hidden at the default width. A click outside the tree does not hide it, and opening content in another pane does not hide it either. On the files page the tree fills the page below the header, docked or floating, without a separator, and always shows: the page draws no toggle and Escape leaves it. A file the page opens starts with the destination pane's own choice, hidden for a pane that has made none. -The navigator's drawing lifetime marks its DOM parent for the covered-content stylesheet and removes that mark when the drawing ends or moves. These selectors remain confined to navigator parents, so unrelated conversation mutations do not invalidate their descendants. - Its width is the pane's preference, 260px until the reader drags the separator on its left edge with the primary button or moves it with the arrow keys, Home (narrowest), or End (widest), clamped to the row: at least 180px when the row allows it, and at most the row's width less 336px where the navigator sits beside the body and less 120px where it floats. Beside a preview in a viewport at least 768px wide, the row's width, measured by the face, picks one of three layouts. From 560px on, the navigator is a raised panel beside the body, 8px inside the row's top, right, and bottom edges and 8px clear of the body, which narrows by the panel and its margins but keeps at least 320px. From 400px up to 560px, the same panel floats over the body's right part, so the body keeps its full width and the part the panel leaves uncovered stays scrollable and interactive; the panel covers the body's scrollbar and a renderer's sticky toolbar where they run beneath it. Both panels have rounded corners, the floating dock panel's surface and shadow in light mode, and the compact tree's dark background with a soft white shadow in dark mode. The 560px threshold depends on the row alone, so resizing the tree never moves it between the two layouts. In a row narrower than 400px, or a viewport narrower than 768px, where the right Sidebar presents its panel fullscreen, the navigator fills the row below the header without the separator, and navigation to that preview hides the tree so the file shows. While filled, the covered body is hidden from keyboard navigation, including embedded frames. If the body holds focus when the row or viewport enters this layout, including the tree's first drawing, focus moves into the tree without changing the pane's open choice. The browser may blur the hidden body before the new width is reported; that focus also moves into the tree unless the reader moves the keyboard or the pointer, or the window loses focus, before the report. Docked and floating panes follow the same rules; the floating pane's resize handle remains reachable with the tree open in every layout. Beside a preview, an unmodified Escape that reaches the page's document from the body or the tree in the row closes the navigator at any row width and moves keyboard focus to the toggle; a key pressed while an embedded frame's document holds focus does not bubble to the page's document, so it leaves the navigator open. Escape is heard on the document after the row's own handlers, so a handler in the row that prevents it keeps it. The preview's `onOpen` passes [`replaceTab: true`](../ui-sidebar-right/README.md#ctxsidebarright); the files page passes `{ paneId: panel.id }` when docked and omits placement when floating. The right Sidebar owns replacement, default docked placement, and reveal. A replacement retains the pane's tree. When the tree holds keyboard focus and an open replaces or switches the tab in the same pane, the next drawing focuses the current file's row; revealing another pane leaves the initiating tree and its focus unchanged. diff --git a/packages/client/ui-sidebar-files/README.zh.md b/packages/client/ui-sidebar-files/README.zh.md index 9550766e16..69e40033ca 100644 --- a/packages/client/ui-sidebar-files/README.zh.md +++ b/packages/client/ui-sidebar-files/README.zh.md @@ -44,8 +44,6 @@ kind: "package-reference" 导航栏画在横跨分栏的预览标题行下方,位于它与正文共用的那一行的右侧。在预览旁,是否显示由其分栏的记录决定:分栏默认隐藏,这一选择对分栏内的每个文件 tab 都有效,直到开关、Escape、工作区命令,或导航到被目录树遮住的预览改变它。自身没有记录的分栏(例如 tab 拖出后所在的浮窗)从隐藏、默认宽度开始。点击目录树以外的地方不会隐藏它;在另一分栏打开内容也不会隐藏它。在文件页上,目录树铺满标题行下方的整页,停靠与浮动时都一样,不显示分隔条,且始终显示:文件页不画开关,Escape 也不关闭它。文件页打开的文件沿用落定分栏自己的选择,尚未做过选择的分栏为隐藏。 -导航栏的绘制生命周期为其 DOM 父元素添加覆盖内容样式的作用域标记,在绘制结束或移动时移除。相关选择器仅作用于导航栏父元素,不会因无关会话内容变化而使其后代样式失效。 - 其宽度是分栏的偏好,在读者用主键拖动左缘分隔条,或用方向键、Home(最窄)、End(最宽)调整之前为 260px,并限制在这一行内:行宽允许时至少 180px;与正文并排时至多为行宽减去 336px,悬浮时至多为行宽减去 120px。在预览旁、视口至少 768px 时,face 测得的行宽决定三种布局之一。行宽至少 560px 时,导航栏是正文旁的浮起面板,距行的上、右、下边缘各 8px,与正文相隔 8px;正文让出面板及其外边距的宽度而变窄,但至少保留 320px。行宽在 400px 到 560px 之间时,同样的面板悬浮在正文右侧之上,因此正文保持完整宽度,面板未覆盖的部分仍可滚动、可交互;正文的滚动条和渲染器的吸顶工具栏在面板下方的部分会被它遮住。两种面板都带圆角:浅色模式采用浮动停靠面板的背景与阴影,深色模式采用与紧凑目录树一致的深背景和柔和白色阴影。560px 这一界限只取决于行宽,因此调整目录树宽度不会让它在两种布局之间切换。在窄于 400px 的行中,或窄于 768px、右侧 Sidebar 以全屏呈现面板的视口中,导航栏铺满标题行下方的整行,不显示分隔条,导航到该预览后会隐藏目录树,让文件显示出来。铺满时,被覆盖的正文及其内嵌框架会隐藏,不能通过键盘进入。若行宽或视口进入此布局时正文持有焦点(包括目录树首次绘制时),焦点移到目录树内,不改变分栏的开关选择。浏览器可能在新宽度报告之前就让被隐藏的正文失焦;除非读者在报告之前移动了键盘焦点或指针,或窗口失去焦点,这一焦点同样移到目录树内。停靠与浮动分栏遵循相同规则;目录树开启时,在每种布局下浮窗的缩放手柄都仍可操作。在预览旁,从这一行内的正文或目录树传到页面 document 的不带修饰键的 Escape,在任何行宽下都会关闭导航栏,并把键盘焦点移到开关上;内嵌框架的 document 持有焦点时,按键不会冒泡到页面 document,因此导航栏保持打开。Escape 在 document 上接收,晚于这一行自己的处理器,因此行内处理器若阻止了默认行为,这次按键就留给它。 预览的 `onOpen` 传入 [`replaceTab: true`](../ui-sidebar-right/README.zh.md#ctxsidebarright);文件页在停靠时传入 `{ paneId: panel.id }`,浮动时省略落位。右侧 Sidebar 负责原位替换、默认停靠落位与揭示。原位替换保留分栏的目录树。目录树持有键盘焦点时,若打开操作在同一分栏内替换或切换 tab,下次绘制会聚焦当前文件所在行;揭示其他分栏时,发起侧的目录树与焦点保持不变。 diff --git a/packages/client/ui-sidebar-files/src/client/FilesBody.module.css b/packages/client/ui-sidebar-files/src/client/FilesBody.module.css index 7fadea8828..c250c5d4ae 100644 --- a/packages/client/ui-sidebar-files/src/client/FilesBody.module.css +++ b/packages/client/ui-sidebar-files/src/client/FilesBody.module.css @@ -72,15 +72,14 @@ border-left: 0; } -/* Only the drawn navigator row hides covered content; unscoped :has() - would invalidate unrelated conversation subtrees on each streamed update. */ -:global([data-files-navigator-row]):has(> .navigator.fill) > :not(.navigator) { +/* Covered content stays laid out but cannot receive keyboard focus. */ +:has(> .navigator.fill) > :not(.navigator) { visibility: hidden; } /* navigator-layout `FULLSCREEN_QUERY`: every layout fills the row. */ @media (width < 768px) { - :global([data-files-navigator-row]):has(> .navigator:not(.wholePage)) > :not(.navigator) { + :has(> .navigator:not(.wholePage)) > :not(.navigator) { visibility: hidden; } diff --git a/packages/client/ui-sidebar-files/src/client/face.ts b/packages/client/ui-sidebar-files/src/client/face.ts index 3a8714f532..0957339723 100644 --- a/packages/client/ui-sidebar-files/src/client/face.ts +++ b/packages/client/ui-sidebar-files/src/client/face.ts @@ -190,8 +190,7 @@ export interface FilesInjected { readonly currentDrawn: (paneId: PaneId, row: HTMLElement | null) => void /** * Register a navigator's element, or with `null` as gone. While it is - * drawn, the face marks its parent for covered-content CSS, measures the - * row it is laid out in and, when `closable`, + * drawn, the face measures the row it is laid out in and, when `closable`, * closes the navigator on Escape in that row. When a new width leaves the * focused resize edge removed or hidden, the tree takes keyboard focus. * When a closable navigator starts filling the row, including its first @@ -653,11 +652,8 @@ export function filesFace( drop(paneId) return } - const parent = navigator.parentElement - if (parent === null) return const row = layoutParent(navigator) if (row === null) return - parent.setAttribute('data-files-navigator-row', '') const document = row.ownerDocument // The resize edge while it holds the keyboard. A width at which the // tree fills the row removes it, and the fullscreen viewport hides it, @@ -745,7 +741,6 @@ export function filesFace( navigator.addEventListener('focusin', focusin) navigator.addEventListener('focusout', focusout) pane.unobserve = () => { - parent.removeAttribute('data-files-navigator-row') observer.disconnect() viewport?.removeEventListener('change', measure) document.removeEventListener('keydown', close) diff --git a/packages/client/ui-sidebar-files/tests/navigation.client.spec.tsx b/packages/client/ui-sidebar-files/tests/navigation.client.spec.tsx index cc007ccbf7..7aee76c1fc 100644 --- a/packages/client/ui-sidebar-files/tests/navigation.client.spec.tsx +++ b/packages/client/ui-sidebar-files/tests/navigation.client.spec.tsx @@ -143,28 +143,6 @@ function harness() { } describe('files navigator target navigation', () => { - it('scopes covered-content styling to each current navigator parent and releases old drawings', () => { - const h = harness() - const first = h.draw(LEFT, h.tab(A), 399) - const second = h.draw(RIGHT, h.tab(B), 399) - const marked = (row: Element): boolean => row.hasAttribute('data-files-navigator-row') - expect(marked(first.row)).toBe(true) - expect(marked(second.row)).toBe(true) - h.face.navigatorDrawn(LEFT, first.navigator, true) - expect(marked(first.row)).toBe(true) - const parent = document.createElement('div') - first.row.append(parent) - parent.append(first.navigator) - h.face.navigatorDrawn(LEFT, first.navigator, true) - expect(marked(first.row)).toBe(false) - expect(marked(parent)).toBe(true) - first.remove() - expect(marked(parent)).toBe(false) - expect(marked(second.row)).toBe(true) - second.remove() - expect(marked(second.row)).toBe(false) - }) - it.each([ ['restored revision zero', 0, true], ['newly opened revision one', 1, false], @@ -358,17 +336,12 @@ describe('files navigator target navigation', () => { await Promise.resolve() expect(document.activeElement).toBe(drawing.file) }) - it.each([false, true])('waits for a navigator without a containing row with a contents wrapper %j', (wrapped) => { + it('waits for a navigator without a containing row before answering navigation', () => { const h = harness() const tab = h.tab(B) h.attach(RIGHT, tab) h.shown(RIGHT, true) const navigator = document.createElement('aside') - if (wrapped) { - const parent = document.createElement('div') - parent.style.display = 'contents' - parent.append(navigator) - } const body = navigator.appendChild(document.createElement('div')) h.face.bodyDrawn(RIGHT, tab.address, body) h.face.navigatorDrawn(RIGHT, navigator, true) diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index f7a69ffba7..dc90c35071 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -39,8 +39,8 @@ en: ae1618577f4e8364 zh: a6e8ab1563c08dfb /deepseek-ai-dsh-llm/understand-the-implementation/main-flow: - en: d141f1b3c38c7352 - zh: 7f6f472490054e47 + en: c95d4947745e39bb + zh: ec37b969f27629e2 /deepseek-ai-dsh-llm/understand-the-implementation/invariants: en: 0163a050239248e5 zh: 885c95bedcbe2ff4 diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index bbe607cf85..e2c319f53d 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -130,8 +130,6 @@ A request is validated against its exact model's capability — context window, File detection reads current content, including tool-role result content, on every request without caching message identities or freeze state. The [archived file-scan decision](../../../.agents/notes/archived/simplification/2026-09-07-file-content-scan.md) records the measured traversal cost. -Developer-message filtering and replay-state stripping reuse unchanged request histories. A new history array is allocated only when a message is removed or replaced; retained messages keep their identities, and projected frozen requests remain frozen. Each dispatch reads the current messages and adapter ownership. - ### Invariants - **Model-visible ⟺ logged** — callers must keep every provider request's model-visible input reconstructable from the session log; loop-built requests arrive deep-frozen and cannot be rewritten. diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index e07f0530c6..2fd7de3175 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -130,8 +130,6 @@ const configure = (maxTokens: number): ConfigureCall => 文件检测在每次请求时读取当前内容,包括 tool-role 结果内容,不缓存消息身份或冻结状态。[已归档的文件扫描决策](../../../.agents/notes/archived/simplification/2026-09-07-file-content-scan.md)记录了实测遍历成本。 -Developer 消息过滤和 replay-state 移除复用未改变的请求历史。只有删除或替换消息时才分配新的历史数组;保留的消息维持对象身份,已冻结请求的投影仍保持冻结。每次分发都读取当前消息和适配器归属。 - ### 不变式 - **模型可见 ⟺ 已记录**——调用方必须确保每个提供方请求中的模型可见输入都可以从会话日志重建;loop 构建的请求以深度冻结状态到达,不可改写。 diff --git a/packages/llm/llm/src/content.ts b/packages/llm/llm/src/content.ts index 17230bef0b..cecbfba938 100644 --- a/packages/llm/llm/src/content.ts +++ b/packages/llm/llm/src/content.ts @@ -374,14 +374,8 @@ export interface ProjectedToolUpdates { } function withoutDeveloperMessages(messages: readonly RequestMessage[]): readonly RequestMessage[] { - let retained: RequestMessage[] | undefined - let index = 0 - for (const message of messages) { - if (message.role === 'developer') retained ??= messages.slice(0, index) - else retained?.push(message) - index++ - } - return retained ?? messages + const retained = messages.filter(message => message.role !== 'developer') + return retained.length === messages.length ? messages : retained } function toolDeclarations( @@ -421,7 +415,6 @@ function toolDeclarations( * Construct provider declarations from session-folded history without changing logged active tools. * Unsupported routes and incomplete history use current declarations without developer updates. * Explicitly deferred baseline tools become available only after their first retained addition. - * Histories without removed or replaced messages retain their original array identity. * @param messages - complete request inputs, or the prefix selected for an auxiliary call. * @param tools - currently active tool schemas. * @param toolUpdate - the resolved route's update mode. diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index ffd23c41ad..14be9c87f8 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -1023,25 +1023,19 @@ export class LlmRuntime extends TypertRemoteService { return registration } - /** Remove replay state owned by another adapter, retaining unchanged requests and message identities. */ + /** Remove replay state whose historical route is owned by another adapter. */ private forAdapter(options: GenerateOptions, adapter: LlmAdapter): GenerateOptions { - let messages: RequestMessage[] | undefined - let index = 0 - for (const message of options.messages) { - if (message.role === 'assistant' && message.source.replayState !== undefined - && this.adapters.get(message.source.provider)?.adapter !== adapter) { - const source = message.source - messages ??= options.messages.slice(0, index) - messages.push(freezeMessage({ - ...message, - source: { kind: 'model', provider: source.provider, model: source.model }, - })) - } else { - messages?.push(message) - } - index++ - } - if (messages === undefined) return options + const messages: RequestMessage[] = options.messages.map((message) => { + if (message.role !== 'assistant') return message + const source = message.source + if (source.replayState === undefined) return message + if (this.adapters.get(source.provider)?.adapter === adapter) return message + return freezeMessage({ + ...message, + source: { kind: 'model', provider: source.provider, model: source.model }, + }) + }) + if (messages.every((message, index) => message === options.messages[index])) return options const filtered = { ...options, messages } return Object.isFrozen(options) ? deepFreeze(filtered) : filtered } diff --git a/packages/llm/llm/tests/content.spec.ts b/packages/llm/llm/tests/content.spec.ts index f226da9007..19bb84e460 100644 --- a/packages/llm/llm/tests/content.spec.ts +++ b/packages/llm/llm/tests/content.spec.ts @@ -410,7 +410,7 @@ describe('projectToolUpdates', () => { const roles = (messages: readonly RequestMessage[]) => messages.filter(message => message.role === 'developer').map(message => message.content) it('returns the input history and tools by identity when nothing is projected', () => { - const messages = Object.freeze([prompt]) + const messages = [prompt] const tools = [search] const projected = projectToolUpdates(messages, tools, 'in-history', { tools, updates: [] }) expect(projected.messages).toBe(messages) @@ -418,17 +418,6 @@ describe('projectToolUpdates', () => { expect(projectToolUpdates(messages, tools, undefined).messages).toBe(messages) }) - it.each(['first', 'last', 'all'] as const)('filters developer messages at %s positions without replacing retained messages', (position) => { - const added = developer([{ type: 'tool-addition', toolName: 'fetch' }]) - const messages = Object.freeze(position === 'first' ? [added, prompt] - : position === 'last' ? [prompt, added] : [added, added]) - const projected = projectToolUpdates(messages, [search], undefined) - expect(projected.messages).toEqual(position === 'all' ? [] : [prompt]) - expect(projected.messages).not.toBe(messages) - if (position !== 'all') expect(projected.messages[0]).toBe(prompt) - expect(messages).toHaveLength(2) - }) - it('strips deferred loading and developer updates when the route declares no mode', () => { const added = developer([{ type: 'tool-addition', toolName: 'fetch' }]) const history: ToolHistory = { tools: [search], updates: [{ messageId: added.id, additions: [fetch] }] } diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index d8e6880499..a5d8be4413 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -345,7 +345,6 @@ describe('LlmRuntime', () => { expect(projected, fixture.name).toMatchObject({ type: 'text' }) if (projected?.type !== 'text') throw new Error(`expected projected text for ${fixture.name}`) expect(projected.text, fixture.name).toContain(fixture.expected) - expect(ctx.llm.fileRequestText(attachment), fixture.name).toBe(projected.text) if (fixture.fs !== undefined) { expect(projected.text, fixture.name).toContain('include this saved path in the delegation prompt') } @@ -1501,58 +1500,56 @@ describe('LlmRuntime', () => { expect(assistant.source.replayState).toEqual({ private: 'state' }) }) - it('keeps history by identity when historical and target providers belong to the same adapter instance', async () => { + it('keeps replay state when historical and target providers belong to the same adapter instance', async () => { const ctx = new Context() await ctx.plugin(LlmRuntime) const adapter = new RecordingAdapter(SCRIPT) ctx.llm.registerAdapter(['historical', 'target'], adapter) const replayState = { private: 'state' } - const messages = [createMessage({ - role: 'assistant', - content: [{ type: 'text', text: 'old response' }], - source: { kind: 'model', provider: 'historical', model: 'old-model', replayState }, - })] for await (const _chunk of ctx.llm.stream({ - provider: 'target', model: 'new-model', messages, + provider: 'target', + model: 'new-model', + messages: [createMessage({ + role: 'assistant', + content: [{ type: 'text', text: 'old response' }], + source: { + kind: 'model', + ...{ provider: 'historical', model: 'old-model', replayState }, + }, + })], })) { /* drain */ } - expect(adapter.lastOptions?.messages).toBe(messages) expect(adapter.lastOptions?.messages[0]?.source).toEqual({ kind: 'model', provider: 'historical', model: 'old-model', replayState, }) }) - it('strips foreign replay state while retaining prefix, intervening and suffix message identities', async () => { + it('strips replay state but preserves provider and model when the target uses a different adapter instance', async () => { const ctx = new Context() await ctx.plugin(LlmRuntime) ctx.llm.registerAdapter(['historical'], new RecordingAdapter(SCRIPT)) const target = new RecordingAdapter(SCRIPT) ctx.llm.registerAdapter(['target'], target) - const prompt = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: 'continue' }] }) - const assistant = (provider: string) => createMessage({ - role: 'assistant', - content: [{ type: 'text', text: 'old response' }], - source: { kind: 'model', provider, model: 'old-model', replayState: { private: 'state' } }, - }) - const foreign = assistant('historical') - const owned = assistant('target') - const messages = [prompt, foreign, owned, foreign, prompt] - Object.freeze(messages) for await (const _chunk of ctx.llm.stream({ - provider: 'target', model: 'new-model', messages, + provider: 'target', + model: 'new-model', + messages: [createMessage({ + role: 'assistant', + content: [{ type: 'text', text: 'old response' }], + source: { + kind: 'model', + ...{ provider: 'historical', model: 'old-model', replayState: { private: 'state' } }, + }, + })], })) { /* drain */ } - expect(target.lastOptions?.messages).not.toBe(messages) - expect(target.lastOptions?.messages).toHaveLength(5) - for (const index of [0, 2, 4]) expect(target.lastOptions?.messages[index]).toBe(messages[index]) - for (const index of [1, 3]) { - expect(target.lastOptions?.messages[index]).toEqual({ - ...foreign, source: { kind: 'model', provider: 'historical', model: 'old-model' }, - }) - } - expect(foreign.source.replayState).toEqual({ private: 'state' }) + expect(target.lastOptions?.messages[0]?.source).toEqual({ + kind: 'model', + provider: 'historical', + model: 'old-model', + }) }) it('preserves immutability while stripping replay state from frozen requests', async () => { diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts index 6028e5fd52..03688e1ae4 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts @@ -182,11 +182,11 @@ describe('Node program process', () => { }) it.each([{}, { timeoutMs: null }] as const)('cancels a live program and closes its managed process with %j', async (timing) => { - const { run, cwd } = await setup() + const { run } = await setup() + const entered = Promise.withResolvers() const controller = new AbortController() - // Synchronous readiness cannot remain queued on the control pipe behind the busy loop. - const active = run({ ...timing, program: 'const fs = await import("node:fs"); fs.writeFileSync("started", "ready"); for (;;) {}', signal: controller.signal, bindings: [] }) - await expect.poll(() => readFile(join(cwd, 'started'), 'utf8'), { timeout: 30_000 }).toBe('ready') + const active = run({ ...timing, program: 'void tools.enter({}); for (;;) {}', signal: controller.signal, bindings: bindings({ enter: async () => { entered.resolve(undefined); return null } }) }) + await entered.promise controller.abort('stop') expect((await active).error).toEqual({ kind: 'abort', message: 'stop' }) }) diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index 86a351e7db..b7f6458756 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -15,5 +15,5 @@ en: 0f476fb87a1a3349 zh: 359dbd9b24b18b46 /deepseek-harness-runtime-bin/build-and-distribution: - en: 80e861713ec5fb8b - zh: e2dd11e70d748dcf + en: a41e8eb7e99b9532 + zh: 2789d8f43b1b54be diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 72df976d7e..346813cb1c 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -62,7 +62,7 @@ External profile management uses `dsh plugin --profile ...`. After explic ## Build and distribution -Production deployment permits unused workspace patches for packages outside the runtime closure; patches for included packages must still apply successfully. This exception is confined to the deploy command; repository installation still rejects unused patches. Development dependency restoration keeps the lockfile frozen and prefers cached packages, but permits fetching missing registry metadata required by pnpm supply-chain checks. +Production deployment permits unused workspace patches for packages outside the runtime closure; patches for included packages must still apply successfully. This exception is confined to the deploy command; repository installation still rejects unused patches. From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. The executable stores embedded files with Brotli compression. Only the target platform’s PTY prebuilds are packaged. Package README, CHANGELOG, HISTORY Markdown, source maps, and TypeScript declarations outside runtime assets are omitted. Deployment disables workspace hoisting, and packaging excludes checkout dependencies and workspace source trees outside the deployment so dependency scans cannot add development packages to the payload. Staging shares root workspace packages with nested consumers of the same pnpm dependency identity, preserving singleton services while retaining different peer resolutions. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version. diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index 724ce7aa56..0339c1020b 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -62,7 +62,7 @@ Python 与打包的 bootstrap 共享资源布局:`//office/` ## 构建与分发 -生产部署允许工作区中不属于运行时闭包的补丁保持未使用;闭包内包的补丁仍必须成功应用。此例外仅用于部署命令,仓库安装仍拒绝未使用的补丁。恢复开发依赖时保持锁文件冻结并优先使用缓存包,但允许获取 pnpm 供应链检查所需的缺失 registry 元数据。 +生产部署允许工作区中不属于运行时闭包的补丁保持未使用;闭包内包的补丁仍必须成功应用。此例外仅用于部署命令,仓库安装仍拒绝未使用的补丁。 在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。可执行文件使用 Brotli 压缩存储内嵌文件。打包时只保留目标平台的 PTY 预构建文件,并排除运行时资源目录之外的包内 README、CHANGELOG、HISTORY Markdown 文档、source map 和 TypeScript 声明。部署会关闭工作区提升,打包时还会排除部署目录之外的检出依赖和工作区源码树,防止依赖扫描把开发包加入载荷。暂存步骤让使用相同 pnpm 依赖标识的嵌套消费者共享根目录的工作区包,保留服务单例,同时保留不同的对等依赖解析。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。 diff --git a/scripts/build-exe-for-python-sdk.spec.ts b/scripts/build-exe-for-python-sdk.spec.ts index 9ffbc7352d..fdaf7202a7 100644 --- a/scripts/build-exe-for-python-sdk.spec.ts +++ b/scripts/build-exe-for-python-sdk.spec.ts @@ -41,7 +41,6 @@ describe('Python runtime executable builder CLI', () => { expect(commands[0]).toMatch(/ run verify-runtime-closure$/) expect(commands[1]?.endsWith(' ' + command)).toBe(true) expect(commands[2]).toContain(' --filter dsh-python-runtime-closure deploy ') - expect(commands[3]).toMatch(/ install --prefer-offline --frozen-lockfile --prod=false --ignore-scripts$/) }) it('rejects simultaneous build modes before invoking pnpm', () => { diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 03c6c48b34..447c98ff14 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -319,7 +319,7 @@ class SingleExeBuild { this.staging, ]) } finally { // Legacy deploy records production-only workspace state; restore the development installation before exec. - await this.runPnpm('restore development dependencies', ['install', '--prefer-offline', '--frozen-lockfile', '--prod=false', '--ignore-scripts']) + await this.runPnpm('restore development dependencies', ['install', '--offline', '--frozen-lockfile', '--prod=false', '--ignore-scripts']) } await this.restoreLegacyHoists() await this.materializeStagedLinks() diff --git a/scripts/run-bench-suite.ts b/scripts/run-bench-suite.ts index 38d18986a0..bb6b789a8f 100644 --- a/scripts/run-bench-suite.ts +++ b/scripts/run-bench-suite.ts @@ -1,11 +1,11 @@ -/** Run corpus and latency-sensitive benchmarks in separate reporting phases. */ +/** Run the benchmark suite in the two phases its measurements tolerate. */ import { resolve } from 'node:path' import { runPnpmCommand } from './compile-referenced-projects.ts' /** * Cases whose wall-clock endpoints cannot absorb a sibling: they drive a * browser page, compare animation frames or paced streams, or spawn fresh - * children and budget their startup. + * children and budget their startup. Corpus cases tolerate a sibling. */ export const SERIAL_BENCH_FILES = [ 'benchmarks/long-session-browser/long-session.bench.ts', @@ -25,8 +25,8 @@ const CONFIG = ['--config', 'vitest.bench.config.ts'] as const /** * Run one phase of the suite. * @param root - Repository root containing the Vitest configuration. - * @param phase - `first` clears the report and runs corpus cases; - * `last` runs latency-sensitive cases and renders the report. + * @param phase - `first` clears the report and runs the CPU cases two at a time; + * `last` runs the browser cases one at a time and renders the report. * @throws If the phase fails. */ export function runBenchPhase(root: string, phase: 'first' | 'last'): void { diff --git a/vitest.bench.config.ts b/vitest.bench.config.ts index 8978ba2789..053b7a2c73 100644 --- a/vitest.bench.config.ts +++ b/vitest.bench.config.ts @@ -6,8 +6,9 @@ import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts' * CI performance gate. Node CPU cases use compiled plain-Node workers under * `benchmarks/.dsh-build/`; browser cases drive built Client artifacts through * the shared shipped-composition Web scaffold. - * Files run one at a time so measured workers do not share CPU and memory - * bandwidth with another benchmark's workers or fixture preparation. + * Two files run at a time so the four-vCPU CI runner keeps both cores busy. + * Budgets keep their reviewed CI scale and headroom; a pairing that inflates an + * endpoint surfaces as a failing budget and is never loosened to accept it. */ export default defineConfig({ plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] }), standardDecoratorPlugin()], @@ -20,8 +21,8 @@ export default defineConfig({ 'benchmarks/**/*.bench.ts', 'benchmarks/**/*.bench.client.ts', ], - fileParallelism: false, - maxWorkers: 1, + fileParallelism: true, + maxWorkers: 2, testTimeout: 600_000, hookTimeout: 120_000, disableConsoleIntercept: true,