Conversation
用户可以在**单个任务**上选择工作上下文窗口(tokens),与全局的自动压缩阈值 百分比正交:档位是分母/容量,百分比是触发点。分档计费的模型据此落在不同价带 (档位旁直接标出该档输入单价),不分档计费的模型通过压小运行期上下文降低每轮 输入成本。 - 新增 sessions.context_window_budget(nullable)。NULL = 跟随模型路由默认, 老任务零行为变化;不触碰 context_window / context_window_runtime 快照口径。 - 有效窗口 = min(任务预算 ?? 模型级上限, 模型级上限, 目录 contextWindowMax), 由 resolveConfiguredContextWindow 解析;任务预算不得越过目录物理上限。 - 下发:prepareStartOptions 统一收敛后经 StartSessionOptions.contextWindowBudget 传给三引擎(CC 的 AUTO_COMPACT_WINDOW、Pi 的 working window/reserveTokens、 Codex 的 model_context_window);切模时按目标路由重新收敛;fork 继承(继承前归一化)。 - 生效:空闲任务关 handle、下一条消息冷重建;回合中登记 pending、回合结束生效; 并发改档位以 DB 终值为准(锁内重读会话路由 + 预算值 + 同值去重)。 - renderer:任务底栏的「上下文窗口」档位 chip。档位固定三档 250K / 500K / 1M, 选中路由默认档上报 null,不把当前默认值快照进任务。 - 档位锚点按**路由**解析(唯一候选;歧义时只允许收紧),Pi/Codex 不回落扁平表; 同路由的模型级上限会成为档位表上界。 - device-link 远程任务:写 channel maker:set-context-window-budget(进 allowlist, 带范围校验,与本地入口同口径);控制端经同一个只读 handler/channel 读被控端该路由的 窗口边界(按需拉取 + 60s TTL + 开菜单刷新),据此在远程任务上也能选择更大的档位; 拿不到(老被控端 CHANNEL_NOT_ALLOWED / 失败 / 畸形返回)时退回「只允许收紧」并如实说明。 多轮对抗性 review 共处置 34 项发现(29 修、5 记为取舍/已记录不阻塞),明细见 PR 描述。 用户验收反馈:菜单文案收短、档位固定为 250K/500K/1M 三档。 验证:pnpm test:unit:related 全绿(apps/desktop、apps/mobile、packages/device-link、 packages/lizi-mcps、packages/maker-core、packages/orca-workflow);desktop / @cindy/maker-core / @cindy/device-link typecheck;pnpm --filter desktop db:validate 与 test:migration-replay。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
|
本 PR 命中维护者确认门(产品 / UI、规则文档、架构),已开讨论 issue,请维护者在本 PR 上 Approve 放行;需要修改请 Request Changes。 讨论 issue:#4601 |
- 4 个手写 CREATE TABLE sessions fixture 少了 `context_window_budget` (CI companion 步骤 botCanonicalSession.test.ts 198 例因此挂掉); - statusBarCards 改为按**实例**登记(分屏/Orca 同时挂载两份底栏时, 按种类登记会互相顶掉关闭回调,两张档位卡能同时亮着);注销后的句柄 不再有协调权限; - 档位行补 roving tabindex + 方向键/Home/End 焦点移动(只移焦点不提交, 避免扫过一遍档位连写多次库),并补对应单测。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review — 请基于当前 head 上一轮 3 条意见的处置(逐条已在对应 thread 回复):
本轮其它变化: CI 首轮红的根因是 4 个手写 |
|
@codex review |
同步上游 main(d1fd9c91e),解决与任务级上下文窗口档位的冲突: - 迁移编号冲突:上游已占用 0108/0109,本功能的 `0108_loose_puppet_master.sql` 顺延为 `0110_green_unus.sql`, 并重生成 0110 快照与 journal 条目(`db:validate` 校验通过、 迁移回放 10/10 通过)。 - 其余冲突按「上游结构 + 本功能 hunk」合并:本分支相对上游 main 的差异仍恰好只有本功能的 51 个文件。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
关于本轮 证据(该 job 日志):
本机全量 desktop 单测上还观察到两个同类上游 flake(两轮各挂 1 / 2 例,且两轮互不相同),均为上游 main 的既有测试、
这两处不属于本 PR 的改动范围,本 PR 未改动上游测试。 |
Upstream-PR: makecindy#4598 Upstream-PR-Head: 6e5c18d Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
本地构建曾把 makecindy#4598 的「任务级上下文窗口档位」迁移落成 0108_loose_puppet_master.sql; 之后该分支合入上游时,主干已在 0108 放了 bot 私聊迁移(0108_lowly_scarlet_witch.sql), 同一 schema 意图被重编号到 0110_green_unus.sql。于是「用旧包升级过、又换新包」的库会在 prepareMigrationRuntimeManifest 处以 `applied migration runtime identity changed at seq 108 (0108_loose_puppet_master.sql)` 失败关闭——app 起不来,而且重装官方版也救不回:判定依据是 userData 里的数据库与 migration-runtime manifest,不属于安装包。 修法(只认登记在册、内容指纹完全一致的重编号,其余仍失败关闭): - migrationRunner: 新增 KNOWN_RENUMBERED_APPLIED_MIGRATIONS 登记表与 resolveCanonicalAppliedIdentity;已落库旧身份能解析到 canonical 时重写 manifest, 并把它腾出的旧 seq 一并销账;「canonical 未执行」的缺条目检查只查到已落库前缀为止。 - 0108 重放守卫:重编号血统的 bot_direct_messages 缺 sender_name/recipient_name 时, 在该迁移事务内把表收敛回 0108 执行后的等价结构(列序/索引对齐,先自查无外部外键引用)。 - migrate: up-to-date 与 replay 两条启动路径都调用 reconcileRenumberedAppliedSchema, 保证「旧 checkout 血统」的 schema 收敛不会被 pending 为空短路掉。 测试:migrationCompatibility / migrationReplay 新增重编号修复与重放守卫用例; 四个引用 migrationRunner/migrate 的测试文件 39 通过,tsc 通过。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn> (cherry picked from commit 194299a) (cherry picked from commit d697139)
Upstream-PR: makecindy#4598 Upstream-PR-Head: 6e5c18d Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
本地构建曾把 makecindy#4598 的「任务级上下文窗口档位」迁移落成 0108_loose_puppet_master.sql; 之后该分支合入上游时,主干已在 0108 放了 bot 私聊迁移(0108_lowly_scarlet_witch.sql), 同一 schema 意图被重编号到 0110_green_unus.sql。于是「用旧包升级过、又换新包」的库会在 prepareMigrationRuntimeManifest 处以 `applied migration runtime identity changed at seq 108 (0108_loose_puppet_master.sql)` 失败关闭——app 起不来,而且重装官方版也救不回:判定依据是 userData 里的数据库与 migration-runtime manifest,不属于安装包。 修法(只认登记在册、内容指纹完全一致的重编号,其余仍失败关闭): - migrationRunner: 新增 KNOWN_RENUMBERED_APPLIED_MIGRATIONS 登记表与 resolveCanonicalAppliedIdentity;已落库旧身份能解析到 canonical 时重写 manifest, 并把它腾出的旧 seq 一并销账;「canonical 未执行」的缺条目检查只查到已落库前缀为止。 - 0108 重放守卫:重编号血统的 bot_direct_messages 缺 sender_name/recipient_name 时, 在该迁移事务内把表收敛回 0108 执行后的等价结构(列序/索引对齐,先自查无外部外键引用)。 - migrate: up-to-date 与 replay 两条启动路径都调用 reconcileRenumberedAppliedSchema, 保证「旧 checkout 血统」的 schema 收敛不会被 pending 为空短路掉。 测试:migrationCompatibility / migrationReplay 新增重编号修复与重放守卫用例; 四个引用 migrationRunner/migrate 的测试文件 39 通过,tsc 通过。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn> (cherry picked from commit 194299a) (cherry picked from commit d697139)
同步上游 main(3e238a8fa,132 个提交)。 本轮唯一真冲突在 `apps/desktop/src/main/localDb/mapper.ts`:两侧都在 `sessionCreateToRow` 同一区域加字段/类型成员,rerere 复用了上一轮的解法, 逐行核对确认该文件相对上游只多出本功能的 12 行、没有丢上游内容。 合并结果 = 上游 main + 本功能原样保留(逐项核验): - 相对上游 main 的差异仍恰好是 51 文件 / +9077-43; - 23 个「两侧都改过」的文件,其新增行与删除行集合与合并前逐行一致; - 上游未占用新迁移号(本功能仍是 0110),`db:validate` 通过、 migration replay 10/10; - desktop typecheck 通过;本功能 11 个测试文件 516 例、device-link allowlist 49 例全过;`pnpm test:runner` 全绿;`check:i18n` 五语言 10042 key 一致。 本机重门禁(`pnpm test:unit:related`)两轮都因共享测试锁被其它 worktree 占满而排队超时(exit 75,测试未跑),已用上述定向用例与 test:runner 覆盖,并由 CI 的全量分片做最终判定。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
同步上游 main(773631f91)。上游已发布到 0113(v0.1.88),其中 `0110_abandoned_scarlet_witch` 占用了本功能此前顺延到的 0110 号。 - 迁移按主干最新链用 drizzle-kit 重新生成:`0114_blue_harrier.sql`,并按仓库 规则改成「SQL 置空 + 同名 companion 用 PRAGMA 守卫」的幂等形态,使列已存在时 重放为 no-op;新增回放用例覆盖该守卫。 - 其余冲突仍按「上游结构 + 本功能 hunk」合并:相对上游 main 的差异只有本功能 自己(53 文件 / +9252-43,含生成的 snapshot 与 journal 条目)。 - 校验:`db:validate`(0000..0114、journal aligned、no schema drift)、migration replay 12/12、desktop typecheck、本功能 11 个测试文件 516 例、device-link allowlist 49 例、`pnpm test:runner`(本机需把 rustc 放进 PATH)。 - 本机重门禁(`pnpm test:unit:related`)在上一版合并态已通过(EXIT 0);本次 合并态由 CI 全量分片做最终判定。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
补充说明这次同步主干带来的迁移调整(不改功能语义,只改迁移身份与幂等形态): 1. 迁移号
|
|
@greptileai review — 本轮两条: |
用户远程端实测(2026-09-22):自定义连接未声明窗口 + 模型级上限 1M 时,档位卡只剩 「20% · 200K」与「100% · 1M 模型默认」两行,25%/50% 缺失。 上一轮只把行内百分比的分母改成 tierBase(max(申报上限, 默认档)),档位表却仍按 effectiveMaxWindow 生成:被控端申报的上限(老 main 会把未核实路由的兜底默认 200K 当 上限发过来)低于它自己解析出的默认档 1M 时,基准被抬到 1M,而 25%/50% 按 200K 生成 又低于 200K 地板被过滤 —— 显示与可选档位分叉。 改为新端直接拿 tierBase 当档位上限(显示与档位同源);老被控端维持原口径:只允许收紧, 基准就是那个保守上限,不拿默认档去抬(避免重新引入 800K 档那类 P1)。 用例:远程形态固化为一条回归(20%/25%/50%/100% 四行),并保留老被控端不出现 800K 的断言。验证:tsc 通过;相关 6 文件 111 例全过;pnpm test:unit:related 全绿 (apps/desktop unit 488.6s PASS)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review |
bb35729 把档位表也换成 tierBase(它含 max(…, 默认档) 的抬升),于是目录**声明了** contextWindowMax=200K、模型级逃生上限 1M 的未核实路由会给出 250K/500K 档;但 main 对 **显式**预算无条件按声明上限夹一次,这两档运行期只会得到 200K —— 窗口与输入价带一起 说谎。默认档写 null 走模型级逃生口、真的生效在 1M,那条不抬上限就成立。 改为两条口径各归各位: - 显式比例档的上限 = min(申报上限, 模型级上限),即 main 对显式预算会夹的那个数; - 行内百分比分母 = max(该上限, 默认档),默认档高于上限时它自己那行仍落在 100%; - 老被控端维持「只允许收紧」,不拿默认档去抬。 两条 P1 同时成立:不再出现运行期达不到的档位,也不再出现 500% · 1M 模型默认。 用例:未声明上限(用户实测形态,含已存 200K 任务档)→ 20%/25%/50%/100% 四行; 声明上限 200K + 模型级 1M → 只有 20%/100%,断言不出现 250000/500000;老被控端 不出现 800K、无上限只显绝对值等既有用例保持。 验证:tsc 通过;chip 42 例全过;pnpm test:unit:related 全绿(apps/desktop unit 568.0s PASS;首轮 piEnvironment 一条为本机环境抖动,单跑 23/23 通过)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review |
用户实测:底栏左边「本任务 ¥…」卡悬浮展开后点击不会收起,本卡却会 —— 两张卡交互 不一致。 与 TodaySpendChip 同款做法:onClick 里 preventDefault 阻止 Radix 把点击解释成开关, 展开/收起只由 hover、焦点与 outside-click 驱动;点击回到「保持展开」,来源按 指针/键盘区分(指针路径继续不抢焦点,鼠标用户不会看到蓝框)。 用例:悬浮展开后点击 → 卡片与档位行仍在,且不会排出下一次关闭。 验证:tsc 通过;chip 43 例全过;pnpm test:unit:related 全绿(apps/desktop unit 610.7s PASS)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
用户实测:Tab 到达底栏用量卡后再按多少次 Tab 都到不了本卡。根因是触发器用了原生
`disabled`(调用点 disabled={readOnly || !session})—— 原生禁用会把元素整个移出
Tab 序列,而左边用量卡没有这道门。
改为不用原生 disabled:
- `aria-disabled` 暴露状态,元素保持可聚焦、可 hover、可展开 —— 只读任务也能看一眼
当前档位与价带;
- 「不可改」由档位行自己 disabled 拦住(写入路径不受影响),并在卡内补一句
`readOnlyHint`(五语言)说明原因,不再让行默默变灰;
- 外观改为按 isDisabled 条件加 `cursor-default opacity-50`(原来的 `disabled:` 变体
在没有原生 disabled 后已失效)。
用例:只读任务下 trigger.disabled === false / aria-disabled === true、可展开、档位行
全部禁用、卡内有只读说明。
验证:tsc 通过;chip 44 例全过;check:i18n 通过(10527 key 五语言一致);
pnpm test:unit:related 全绿(apps/desktop unit 545.3s PASS)。
Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
用户实测:Tab 到左边用量卡后按多少次都到不了右边的任务窗口卡。CDP 实测定位到两个叠加原因: 1. 两张底栏卡片的浮层都 portal 到 body 末尾 —— 文档内卡片之后再没有下一个可 Tab 节点,焦点一进卡就离开了底栏(表观「卡住」)。改为把浮层挂到触发器**紧跟其后**的 宿主节点(`ui/popover.tsx` 新增可选 `portalContainer`,缺省仍是 body),DOM 顺序 = 视觉顺序:底栏实测可 Tab 顺序为 用量触发器 → 用量卡 → 任务窗口触发器 → 圆环。 2. 用量卡关闭路径无条件 `restoreQuotaPopoverFocus()` 把焦点抢回触发器:focus-outside 触发的关闭比浏览器的 Tab 目标更早执行,正好把 Tab 要去的那枚 chip 顶掉,于是焦点在 trigger 与卡片之间反复弹。给归还加保护:焦点已交给卡片外**仍然存在**的控件时不抢回; 焦点在卡片里(Escape)或卡片卸载中焦点即将掉到 body(切换计费形态)时照旧归还。 同时保留既有键盘语义:键盘聚焦仍进卡(卡片滚动区 / 看板按钮可达),点击/悬停行为不变。 用例:本卡新增「浮层必须挂在触发器之后、不在 body 上」的结构不变量;用量卡既有 4 组 焦点用例全绿(未改变语义)。 验证:tsc 通过;4 个底栏单测文件 166 例全过(本卡 45 例);pnpm test:unit:related 全绿 (apps/desktop unit 470.3s PASS)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review |
用户实测(附定位):Tab 能到 1(工作目录)、2(用量卡),卡片段落展开后 Tab 永远 出不去。CDP 焦点仿真 + 真实 Tab 复现:焦点进卡后永远环回卡内 —— 根因是 Radix PopoverContentImpl 给 FocusScope 写死 `loop: true`(loop 在卡内没有可 Tab 边缘时把 Tab 还给自身,我们的卡在最后一档亦然),Tab 的默认穿越动作被环回逻辑吃掉、 且无 preventDefault 可循。 Radix 不透传 loop,改为在它之前接管两个 Tab 边缘(新增 lib/focusTraversal.ts): - Tab 在卡内**最后一个**可 Tab 元素上 → 焦点交给卡外 DOM 序的下一个(右边那枚 chip); - Shift+Tab 在卡内**第一个**可 Tab 元素上 → 交给卡外上一个(回到触发器方向); - 卡内其余 Tab/方向键行为不变。接线:两张底栏卡片(任务窗口卡 + 用量卡)的 onKeyDownCapture,先于 Radix 的 bubble handler,接管即 preventDefault + stopPropagation。 可 Tab 集合的可见性判据与 Radix FocusScope getTabbableCandidates 同源(checkVisibility 缺失时按 visibility/display祖先链判定),避免与 Radix 行为分叉。 验证:官方确认卡片段落展开后 Tab 可继续(CDP 真实 Tab:用量卡 → 任务窗口卡 → 圆环); tsc 通过;新增 focusTraversal 4 例;chip 45 例与用量卡 48 例全过; pnpm test:unit:related 13 workspace 全绿(apps/desktop unit 740.1s PASS)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
底栏卡片键盘可达性收口(本轮四条交互修复)用户目测阶段的四条交互问题,全部收到并修复,累计 5 个提交:
实测(CDP 焦点仿真 + 真实 Tab 按键):用量卡展开后 Tab 依次是 → 验证:tsc 通过;focusTraversal 4 例 + 底栏 4 个测试文件全过; |
|
@greptileai review — 本轮追加四处底栏交互修复(点击不收卡 / 只读可达性 / portal 顺序 / loop 焦点困锁),见上一条评论。 |
Upstream-PR: makecindy#4598 Upstream-PR-Head: c58ffd1 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
Upstream-PR: makecindy#4598 Upstream-PR-Head: c58ffd1 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
Upstream-PR: makecindy#4598 Upstream-PR-Head: c58ffd1 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
用户实测:Orca worker 的 tester 任务窗口预算调到 50% 后,上下文跑到 110% 也不自动压缩。 取证(安装版 0.1.93 + 磁盘 Pi 实例): - 偏好文件里该会话的 500000 一直在,会话列 context_window=500000,档位卡按 50% 显示; - 但 Pi 实例 settings.json 的 compaction.reserveTokens 是 50000(= 无预算 + pct95 的 公式),阈值退回满窗 950K ⇒ 预算窗口 500K 内永远不会触发; - 用生产同一支函数复现:resolveConfiguredContextWindow(500K)=500000、reserveTokens 应为 550000(阈值 450000)⇒ 收敛与换算都没问题,**预算没进到引擎进程**。 断链点:resumeOrcaWorkerSessionIfMissing 自己拼 opts 且不带 contextWindowBudget。 它经 bootstrapSession → maker.createSession,理论上会被 prepareStartOptions 兜住, 但这条路径自己拼 opts、不依赖那条钩子;引擎进程重启后预算即静默丢失(UI 仍显示 50%)。 修法: - 把预算收敛抽成 applySessionContextWindowBudgetToCreateOpts(model-context-settings), prepareStartOptions 与 worker 唤醒路径共用同一口径,不许分叉; - worker 唤醒在 bootstrapSession 之前显式调用它; - 未定型路由(无 model)时不透传未收敛的值。 用例:共享函数 4 例(读存档位/显式值收敛/未自定义不留字段/无 model 不透传,写盘隔离到 临时目录)+ worker 唤醒路径源码契约(预算注入必须在 bootstrapSession 之前)。 验证:tsc 通过;相关 62 例全过;pnpm test:unit:related 全绿(apps/desktop unit 546.4s PASS)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
用户实测报障修复:任务窗口预算在 worker 唤醒后失效(跑到 100% 以上也不压缩)用户实测:Orca worker(tester)把任务级上下文窗口调到 50% 后,上下文跑到 110% 仍不自动压缩。 取证(安装版 0.1.93 + 磁盘 Pi 实例)
用生产同一支函数复现(真实目录事实: ⇒ 收敛与换算都没问题,预算没进到引擎进程。 断链点
顺带澄清另一条时间线:09-30 13:26 那次"给 tester 改预算"实际落在了 reviewer(tester 当天没有任何预算写入),reviewer 因此被重建、阈值下移,所以它压缩正常;tester 是重启后预算丢失的那一个。 修法
顺带核过其它启动入口:worker 首次创建( 用例与验证
|
上游前进 485 个提交后的例行同步。三处冲突由 rerere 重放(register.ts / runtimeSetModel.ts / packages/maker-core/src/agents/pi/index.ts),已按符号级核对确认 任务级窗口预算的读取与下发链路完整保留;上游新增迁移 0115-0120 编号与本 PR 无冲突, 未改动任何既有迁移编号。 pnpm --filter desktop run typecheck: PASS Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review |
单个冲突:worker 唤醒路径的预算注入(我方)与上游新增的 assertCurrent 调用,取并集 —— 保持上游的新顺序(assertCurrent 在 ensureRemoteReadyForSessionStart 之前),预算 注入仍在 bootstrapSession 之前。 顺带修掉一个由合并暴露的真实问题:SET_CONTEXT_WINDOW_BUDGET / GET_CONTEXT_WINDOW_BOUNDS 两个 handler 落在 pluginWriteAccess.test.ts 的源码切片区间内(该测试按字面量把 SET_PERMISSION_MODE → SET_PLAN_MODE → EXPORT_SESSION_HTML 三段源码切出来在沙箱执行), 沙箱没有 normalizeSessionContextWindowBoundsRoute,报 ReferenceError 并连带打挂 6 条 Plan 权限用例。把两个 handler 整块移到 SET_PERMISSION_MODE 之前,使三个切片区间各自 只含自己的 handler。 验证:tsc 通过;25 个 register.ts 源码切片类测试文件 1667 例全绿;pnpm test:unit:related 全绿(apps/desktop unit 571.4s PASS,0 失败)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
|
@greptileai review |
Upstream-PR: makecindy#4598 Upstream-PR-Head: 397d3e8 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
…断结构)
union 把 PR 侧预检塞进 remoteHostId/sdkSessionId 嵌套 if,与 HEAD 侧 if/else{rehydrate} 重复保留并丢掉闭合括号。改为对齐 local/v0.1.96 的已交付结构:预检声明在 runtimeAgentKind==='pi' 公共位置,三段式判定取代 HEAD 的 if/else 分支,保留 makecindy#4598 的 sessionContextWindowBudget 口径。另修上游新增测试 harness 缺的 statDirectory mock 与 makerTransport mock 的 canStopAgentTask 重复键。
Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
(cherry picked from commit af0ddbe6fbddeaf608bdbfe60dcf9c0e0f7161ed)
四处冲突,全部按并集/语义合并: - maker-host/index.ts(import):上游新增 resolveDesktopModelEfforts,与我的预算相关 import 合并为一条(并集去重)。 - model-context-settings.ts:上游在同区域新增 resolveDesktopModelEfforts,与我的预算 函数族并存;补上被并集切掉的闭合括号。 - claude-code/index.ts(2 处):上游给 sdkEffortForModel 加了 providerId 参数、给 setModel 加了 effort 选项;与我的 contextWindowBudget 声明/参数合并,两边语义都保留。 - pi/index.ts:上游删除了 projectedReserve 预演块,取我方(含预算优先的 nextWorkingContextWindow)。 顺带:合并带进上游新依赖 @noble/curves(apps/mobile),本地 pnpm install 补齐。 门禁取证(两轮 RELATED_EXIT=1 的唯一失败均为 pi-package-store-security.test.ts): - 纯净上游 worktree 单跑该文件:194 例全绿 - 本分支单跑该文件连续 3 次:均 194 例全绿 - 仅在门禁 45000+ 用例并发下偶发,且每轮失败的是不同用例 ⇒ 并发时序抖动, 非本 PR 回归;其余 55 个 workspace 全绿。 tsc(desktop + maker-core)通过;预算链路六项与上游新语义逐项核在位。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
Upstream-PR: makecindy#4598 Upstream-PR-Head: 884c63f Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
…断结构)
union 把 PR 侧预检塞进 remoteHostId/sdkSessionId 嵌套 if,与 HEAD 侧 if/else{rehydrate} 重复保留并丢掉闭合括号。改为对齐 local/v0.1.96 的已交付结构:预检声明在 runtimeAgentKind==='pi' 公共位置,三段式判定取代 HEAD 的 if/else 分支,保留 makecindy#4598 的 sessionContextWindowBudget 口径。另修上游新增测试 harness 缺的 statDirectory mock 与 makerTransport mock 的 canStopAgentTask 重复键。
Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn>
(cherry picked from commit af0ddbe6fbddeaf608bdbfe60dcf9c0e0f7161ed)
该测试切源码 + new Function 只截 resumeOrcaWorkerSessionIfMissing,而 makecindy#4598 让该函数依赖 applySessionContextWindowBudgetToCreateOpts/getActiveCatalog,isOrcaWorkerSessionResumable 是同文件兄弟函数也未随之截取 → ReferenceError。补进 bindings(状态读取复用测试自己的 readStatus 以保留 post-bootstrap 复核语义)。 Signed-off-by: Zhang Yunjin <zhangyunjin@zju.edu.cn> (cherry picked from commit cbc37924199f7cd9223f2d99f2b6f6f4881b43ed)
feat(desktop): 任务级上下文窗口档位
---HEAD: feat/session-context-window
这次改了什么
摘要
给单个任务加了「工作上下文窗口档位」:用户可以在任务里选择这个任务最多用多大的上下文窗口(tokens),与设置页的自动压缩阈值百分比正交——档位决定分母/容量,百分比决定触发点。
动机有两类:
关联 Issue:#3464(「按工种/角色预设上下文档位」,维护者分析结论:产品缺口是任务级策略与入口,
contextWindow必须继续来自目录,不能由 session 数据库改写)。本 PR 落的是任务级入口本身,且从不超过目录声明的contextWindowMax。菜单能给到什么范围(统一口径;此前正文这里有自相矛盾的两处表述,已按实现改正):
min(目录物理上限 contextWindowMax, 用户在同路由设过的模型级「上下文上限」);比例档取基准的 25% / 50% / 100%,所以当有效上限高于模型默认窗口时,菜单里会出现比默认窗口更大的档位(算术示例:基准 1.05M、默认 272K → 默认 / 50% · 525K / 100% · 1.05M 三档)。contextWindowMax;contextWindowMax原始字段不进 renderer 投影(不包含项见下),但 main / 被控端解析出的有效上限会随maker:get-context-window-bounds下发给 chip,作为档位基准。变更类型
feat新功能范围
session-context-budget-prefs.json,形如{ budgets: { "<sessionId>": tokens } },null / 缺条目 = 跟随模型默认),读写集中在session-context-budget-store.ts;不新增数据库迁移、不改 schema、不写会话行 —— DB 模式停在官方0113;sessions.context_window_budget+ 新迁移。那会改写迁移链身份:库一旦记上本分支独有的迁移,任何不带该迁移的包(例如官方包)启动时会以applied migration runtime identity changed at seq ...拒绝启动,反向亦然 —— 同一份用户库无法在官方包与本分支包之间切换。任务级窗口是纯客户端偏好,不该换来不可逆的库状态;pnpm --filter desktop db:validate现在报0000..0113、no schema drift;min(任务预算 ?? 模型级上限, 模型级上限, 目录 contextWindowMax);maker:set-context-window-budget(进 allowlist),控制端经隧道让被控端写入自己的偏好文件并应用;老被控端回CHANNEL_NOT_ALLOWED→ 提示「该设备版本不支持」。ImDefaultSettingsSection加;contextWindowMax原始字段的 renderer 投影(第 4 轮 review 时已撤掉端到端透传):renderer 不直接读目录字段,档位基准一律走 main / 被控端解析出的有效上限。这不等于「只能调小」——有效上限高于默认窗口时菜单就会给出更大的档位(见上面的统一口径);/cc-agent/new是 transient draft(没有后端 session,ChatInput的sessionId为undefined),而任务级窗口是按会话存的,所以第一条消息按模型默认窗口跑;任务创建后底栏 chip 立即可调,改动从下一条消息生效(与卡片文案一致)。想「这条路由默认就别跑满」走
设置 → 模型 → 高级 → 「上下文上限」(全局,同时是档位表的基准)。草稿期选择(把预算存进草稿、
createSession时落库)是独立增量,单独一版做。
context_window/context_window_runtime快照口径不变。语义与生效时机(如实说明)
null,目录默认变化能继续跟随(符合configuration-and-overrides.md的 override 纪律)。UI 变化
docs/design-rules/DESIGN.md§2(颜色一律走语义 token:chip 用--msg-tool-card-chevron/--text-secondary/--warning-fg,与货币 chip、「用量明细」卡同源)、§4(复用标准Popover原语——与「用量明细」卡同一个浮层,不新造几何)、§5(chip 是按钮 → 胶囊,不新造外层卡片)、§10(Light/Dark 都由 token 覆盖,无单模式补丁)、§11(微文案:说明生效时机与「档位 ≠ 压缩目标」)。min(目录物理上限, 该路由模型级上限))25%/50%/100% 的比例档」,最多三档,超出时优先保留大比例;比例档值按round(基准 × 百分比)精确换算(1,048,576 的 50% 就是 524,288,不贴 256K/512K 网格);低于MIN_PERCENT_TIER_TOKENS(200K)或与默认档重合的不出现;100% 档只在有效上限高于默认窗口时出现(保证能开满窗口:基准 1.05M 而默认 272K 时,三档是 默认 / 50% · 525K / 100% · 1.05M;这条方向由keeps the 100% tier so a lowered default can still reach the physical max与 chip 的offers the route max tier so a session can raise the window again两个用例守着);显示为25% · 250K(K 段一位小数、M 段最多两位,落库仍是精确整数);每一行都是「百分比 · 绝对值」(含默认档与旧值补的当前档:默认档 =100% · 1.05M 模型默认,Codex 形态默认档 =26% · 272K 模型默认),不再有「默认档不带百分比」的例外。aria-label(triggerLabel)与卡片行。标记用↕(U+2195)而不是括号/图标 —— 括号语义不明确。字号text-11、字重/颜色与货币 chip 的¥同一 token(--msg-tool-card-chevron)并同样opacity-60;text-11是按墨迹选的:12px 下↕墨迹 12px(上缘 11/下缘 1)比¥(9/0)和圆环1%(9/1)高 2px,11px 下 ≈10.1/0.9,三者的墨迹高度与视觉中心都在亚像素级。lib/statusBarCards.ts,与TodaySpendChip同值),不需要点击;键盘仍可 Enter/Esc,且键盘路径保留 focus 指示(指针路径关闭时不还焦点,避免 Chrome 判成:focus-visible出现蓝圈)。卡片改用与 ¥ 卡同一个Popover原语(Radix 菜单展开会抢焦点,做不了悬浮卡),档位行是role="radio"+aria-checked。renderer/lib/statusBarCards.ts协调器,两张底栏卡片互相注册 —— 一张展开时另一张立刻收起(实测四个方向都只有一张卡)。选中档位后卡片不自动关(勾选看得见),鼠标移开才收。--text-secondary(即「用量明细」卡里说明文字的同一档灰,实测rgb(111,111,111));字号/字重也与该卡同套:标题 12/500、说明 12/400、档位行 13/500、注解 12/400。chipLabel/percentBasis两个词条已从 5 语言删除。100% · 1.05M 模型默认),百分比与绝对值同色同节点,不再出现「默认档绝对值在前 / 比例档百分比单独上色」的两种行。.github/pr-assets/):¥ ↕ 100% ◯ 1%—— 标记与货币符号同色同透明度、与圆环并列同基线↕自动展开,无需点击):档位行25% · 262.1K/50% · 524.3K/⦿100% · 1.05M 模型默认Review 轮次修复(对抗性 review 第 1 轮,4 个 P1)
本 PR 在本地经过一轮对抗性 review(模型:
z-ai/glm-5.3-flash,范围 = 单 commit vsupstream/main)。四条 P1 均已复核为真并修复:applyRuntimeSetModelChange,而PendingCredentialSwitchService.register是整体替换 —— 用户先切模型 B、再调档位,pending 就从 B 变成 A(且无任何提示)。修复:与目录刷新共用hasPendingRuntimeSelection守卫(跨引擎切换意图 / 显式路由变更 / 凭证切换),有待定选择时只落库不动 pending;pending 自己的收口重建会经prepareStartOptions读到最新预算。requiresModelSwitchRebuild在预算模式下对任意目标模型都返回「预算 == live env」,探测不到失配;宿主侧窗口保护闸也只按用量判危,用量不高时不会拦。修复:宿主在切模时比较目标路由收敛后窗口与当前窗口,不同即强制重建(不依赖引擎自检)。resolveConfiguredContextWindow的 unverified 回退分支直接用预算值,而目录里真实存在「contextWindowVerified=false+contextWindowMax有值」的形态(发现流程只拿到max_context_window时)。修复:任务预算即使未核实也先按目录声明的上限夹紧(模型级上限保留既有「路由配错时用户可强行解开」的逃生口语义);同时修掉启动时非法显式预算透传给引擎的漏斗缺口。buildContextWindowBudgetOptions的上限档分支在 renderer 侧拿不到contextWindowMax(能力投影里没有),等于死代码。修复:contextWindowMax进availableModels展示投影(catalog-to-descriptors→ maker-coreModelDescriptor→ rendererModelDefinition),chip 用目录上限推导上限档;标注为纯展示元数据,运行期收敛仍按会话实际路由。同时补测:未核实路由夹紧、
contextWindowBudgetChangesEffectiveWindow(applier 的「是否真的变了」判定,原先无覆盖)。Review 轮次修复(对抗性 review 第 2 轮,6 个 P1)
第 2 轮换模型(
meta/muse-spark-1.3-contributor,providercommandcode)独立重审,结论仍是「不建议按当前 commit 推送」,报 6 条 P1。处置如下:useMemo缺maxWindow依赖 → 目录上限变化后上限档永久过期[budget, effectiveDefaultWindow, maxWindow, modelLimit, ceiling])availableModels扁平表 → 同 id 跨 provider 时可能显示另一条路由的上限resolveRouteContextWindowMax(providers, agent, providerId, model)(要求唯一候选,歧义时只允许收紧),并给「歧义 → 不出现上限档」补测useModelContextLimit(route),档位表与「有效默认档」都按min(目录默认, 目录上限, 模型级上限)收敛;补测sessionRuntimeControl的独立 store,收口边界两套状态同时生效,登记重建 pending 不覆盖轴意图)forceSessionRebuild(凭证 pending),收口关会话 → 下一条消息按 DB 新预算重建;仅轴/runtime-control pending 落到原有登记路径,保证收口一定重建target !== verified恒真 → 同窗切模也被强制重建catalogCurrentWindowForBudget),避免不必要的破坏性重建第 2 轮同时确认了第 1 轮 4 处修复中 2 处完全成立、2 处部分成立(部分成立的部分即上表的第 2/3 行,已按上表处置)。
Review 轮次修复(对抗性 review 第 3 轮,7 个 P1)
第 3 轮仍用
meta/muse-spark-1.3-contributor独立重审(commit29eeefa8d),报 7 条 P1、0 条 P0。处置:deviceId)不再使用本机providers解析路由:默认值退回被控端能力缓存、不给上限档、不读本机模型级上限(只允许收紧),并补测resolveSessionContextWindow对 pi/codex 恒 null)resolveRouteContextWindowBounds(路由唯一候选 →{providerId, defaultWindow, maxWindow},不借用 pi/codex 的运行时快照口径),chip 的默认档与上限档都用它,补 Pi 用例providerId缺失时 chip 放弃模型级上限,与 host 收敛分叉resolveRouteContextWindowBounds回传路由解出的providerId,据此读同一路由的模型级上限,补测withSendToSessionLock之外先读后写,可冲掉期间新选的模型agentSwitchPending直接 return → 切换被取消时预算永不生效maker:set-context-window-budget无范围校验:非法值静默清预算或静默夹紧[MIN_CONTEXT_WINDOW_BUDGET, MAX_CONTEXT_WINDOW_BUDGET]整数值校验,越界抛INVALID_PARAMSforceSessionRebuild判据在「当前窗不可知」时恒为真 → 无辜同窗切模也破坏性重建budgetCompareWindow = verified ?? 有效窗口 ?? 当前路由收敛值;三者都不可知时不强制重建(fail-safe)git checkout恢复的是已含改动的 HEAD,实际没撤)ModelDescriptor/ModelDefinition/capabilities/catalog-to-descriptors/useAgentCapabilities上的contextWindowMax透传(抽屉用的是CatalogModel原生字段,不受影响)Review 轮次修复(对抗性 review 第 4 轮,6 个 P1)
第 4 轮(
meta/muse-spark-1.3-contributor,对 commita20aee2f1)报 6 条 P1、0 条 P0。处置:forceSessionRebuild(不再按agentSwitchPending分叉);无凭证 pending 时才用 live 路由登记sessions:update静默 normalizeupdateSessionInDb入口加同口径校验(null 或[1000, 1e8]整数,越界INVALID_PARAMS),两条入口行为一致;补 2 例测试next(有效窗口比对也移到锁内),让 live 与落库终值对齐;chip 加同步在飞守卫(useRef)避免同帧连发两次写menuHint写「调大适合长文档」→ 误导remoteTightenOnly文案并在远程任务菜单里展示,如实说明「只能调小」;补断言(注:后续版本补上被控端边界只读查询后,这条限制只在拿不到边界时生效,见下方「远程任务」一节)Review 轮次修复(对抗性 review 第 5 轮,5 个 P1)
第 5 轮(
meta/muse-spark-1.3-contributor,对 commitc44b4661a)报 5 条 P1、0 条 P0。处置:useRef在飞守卫会吞掉合法连点(第 4 轮引入)disabled(可见禁用而非静默丢弃);跨窗口并发由 main 侧会话锁 + 终值比对收敛。测试同步撤掉latestBudget但沿用 staleprevious→ 同一终值连续重建两次persistSessionFields(镜像回流)/sessionCreateToRow/fork 与新校验三口径分裂,可静默清预算updateSessionInDb同口径)远程任务:按需向被控端拉取窗口边界(可放大)
上一版把远程档位收紧成「只出不大于被控端默认值的档」,原因是控制端拿不到被控端该路由的
物理上限与模型级上限(本地目录可能属于另一条路由)。这版补上事实来源:
maker:get-context-window-bounds(进 allowlist,无 sender 依赖、不写任何存储):被控端按会话实际路由解析
{providerId, defaultWindow, maxWindow, modelLimit}—— 与运行期收敛同一套口径(同一份目录 + 同一份模型级上限 store,含隐式来源解析)。
设备+会话+模型缓存,提交成功后失效),拿到边界才放出「更大」的档位;拿不到(老版本 →
CHANNEL_NOT_ALLOWED,或拉取失败)退回只允许收紧并如实说明,绝不拿本机目录的值冒充被控端。
第 6 轮 review 修复(4 个 P1)
maker:get-context-window-bounds(与远程同一 handler/口径,preload 暴露getSessionContextWindowBounds),chip 本地/远程都读它 —— 跨 provider 同 id 时 renderer 解不出来源、给不出模型级上限,会给出 main 一定夹掉的档;现在这类分叉连同前几轮记的「已知限制」一起消失。第 6 轮 P2 的收敛(非阻塞项,能低成本修的都修了)
(设备, 会话, 模型)在渲染期取缓存值派生,不必等 effect。getSessionProvider ?? row.providerId),deferred 切模期间不再按旧路由回答。第 7 轮 review 结论与未修项(P2,非阻塞,已记录)
第 7 轮(
meta/muse-spark-1.3-contributor,对 commit42dd47ebd)结论:0 个 P0、0 个 P1,可按现状推送/开 PR;另报 5 条 P2(按仓库口径不作为阻塞项),其中 4 条已在本轮改前顺手收敛(渲染期换锚、内存 provider 覆盖、会话语义清理、共享形状收敛测试),剩余记录如下:appliedContextWindowBudgets只在「取消切换 / 活实例消失」时清;删会话/归档不经过那两条路径 → 条目常驻到进程结束(UUID 主键,有界、不丢失数据)。另有一条窄分叉:预算应用后关掉会话 → 关闭期间改预算(applier 因无活实例 early-return,表不更新)→ 重开后再改回旧值会被去重表挡住一次,直到下次改动自愈。属「下次重建自愈」类,不阻塞。INVALID_PARAMS拒绝并 toast 失败);运行时不受影响(tighterBudget内会 round),仅脏数据可达。activeKeyRef.current = key写在渲染期:本例有两重守卫(cancelled+ key 比对),无用户可见错误,但属 React 反模式,后续加逻辑易踩。dev 实机验证发现的问题与修复(第 8 轮,用户实测报障)
sessions.model还是旧模型」。原maker:get-context-window-bounds只带sessionId,main/被控端按会话行(旧路由)回答 —— 262K 的千问切到 1M 的 deepseek 后,chip 与档位表都停在262.1K,且该答案会按目标模型作为键进入 60s 缓存,切模落库后仍显示旧值。{ agent, providerId, model },renderer 与 device-link 远程同一形状),main/被控端按该路由解析(与运行期收敛同口径);未携带或畸形时整份退回「按会话行回答」(老调用方/老被控端行为不变);缓存键补providerId(同一模型跨来源的上限可能不同)。新增normalizeSessionContextWindowBoundsRoute与「按显示路由回答」「跨来源重键」两组回归用例。1cab679d会话切到千问 3.8 27B → chip 立刻262.1K、菜单只剩「262.1K 模型默认」;切回 DeepSeek V4.1 Flash → 立刻回1M+ 25%/50%/默认三档。main 侧 A/B 直测:不带路由 →1,000,000(会话行),带Qwen/Qwen3.8-27B路由 →262,144,畸形路由 → 退回会话行。怎么验证的
自动验证
pnpm test:unit:related:全绿(含新增的statusBarCards7 例与 chip 键盘导航 3 例;最近一次 desktop 420s / mobile / device-link / maker-core / lizi-mcps / orca-workflow / test:runner 全 PASS)(apps/desktop full、packages/lizi-mcps full、packages/maker-core related、packages/orca-workflow related)。pnpm --filter desktop run typecheck、pnpm --filter @cindy/maker-core run build(tsc --noEmit):通过。pnpm --filter desktop db:validate+test:migration-replay:通过(0000..0113、journal/snapshot 对齐、no schema drift;drizzle 目录 /schema.ts/mapper.ts与上游逐字节一致)。sessionContextWindowBudget(档位推导/取值收敛)、sessionContextBudgetStore(偏好文件读写、外部编辑生效、越界/非整数拒绝、fork 复制、任务删除后清理)、modelContextSettings.sessionBudget(预算 vs 模型级上限 vs 目录上限,含未核实路由夹紧与有效窗口变化判定)、contextWindowBudgetChip(31 例:按显示路由查边界、跨来源缓存重键、路由级上限档、模型级上限压档、跨 provider 歧义时不给上限档、Pi 走路由锚点、远程只允许收紧 + 远程文案、providerId 缺失时仍读到模型级上限、提交值、价带、缩档提示、隧道降级与失败回显)、sessionsUpdate(预算字段两条入口同口径:越界/非整数拒绝,null 与合法值写入偏好)、resolveSessionContextWindowBounds(被控端边界的隐式来源解析与不可用情形)、device-link allowlist(只读查询放行、通用写入口不放开)、chip 用例(本地读 main 权威边界并按其夹紧档位、远程拿到边界→上限档出现、拿不到→只收紧 + 说明)。手工验证
session-context-window-7f7ffc):chip 渲染与档位集合、切模时档位表跟随显示路由、选档写入偏好文件并由maker:get-context-window-bounds回带、下一条消息后context_window/context_window_runtime收敛、远程边界查询参数形状。92b55311d):Linux/Windows unit tests (1/2)的 companion 步骤(botCanonicalSession.test.ts等 localDb 用例不在 unit tier,本地test:unit:related的vitest related选不中它们)挂在no such column: "context_window_budget"—— 根因是 4 个手写CREATE TABLE sessionsfixture 没跟着新列同步(botCanonicalSession/sessionAutoTitlePersist/orcaTeamStore/drizzleProxy),当时补了context_window_budget INTEGER并逐个跑通(247 + 17 + 11 + 4 例)。最新一版把存储换成偏好文件后,这些 fixture 与schema.ts已整体回到上游,本地不再有任何 DB 侧改动。list_preview等列,以及 worktree 锁 /dbClient.tx的 mock 问题),这些文件不在本 PR 的 diff 里,未改动。maker:set-permission-mode/maker:set-extra-dirs/maker:send等既有 channel 同一套门禁且能力更强,仓库里也不存在"未向控制端开放的任务"模型,已在对应 thread 逐条说明;2 条 P2(同类卡片无法互斥 / 单选组缺键盘导航)成立并已修,见上。未执行的验证
429 ExceededBudget(账号预算为 0,与本改动无关)导致一次 Pi 端到端调用未能闭环,已如实记录。风险
风险分类
影响与回滚
影响面:只有显式选过档位的任务受影响(偏好文件里有条目);未选过的任务逐字节保持原判定。
回滚:
git revert即可;偏好文件里的条目无人读取,选过档位的任务退回「跟随模型默认」。数据库完全不受影响(无 schema 改动、无迁移记录),因此新老包可以自由来回切换。偏好文件:路径走 owner 隔离 userData(
ownerScopedUserDataPath),与「单模型上下文上限」同一套写入约定(临时文件 + 原子 rename);写入失败会向上抛错、不回滚成静默成功。系统提示词 / 协议 / 原生层:不涉及。
跨平台:不涉及平台相关分支。
跨端协议:新增
maker:set-context-window-budget到 device-link allowlist(单向新增,老控制端不认识该 channel 不会调)。老被控端不认识新 channel 时回CHANNEL_NOT_ALLOWED,控制端提示版本不支持并保留原档位;无 schema 变更、无重连/握手变化。行为风险:把档位压到低于任务基线(system prompt + MCP 工具 schema)会频繁压缩;这与「把 pct 调到很低」同源,靠 CC 自带 thrashing 检测、host rollover 与 chip 的缩档提示兜底,动态地板作为后续工作。
Codex 特例:带显式档位的任务会走 Codex 的 custom-context host 路径(与用户设过「上下文上限」的行为一致),不影响不带档位的任务。