dsh-context-domains 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +122 -4
- package/README.md +32 -6
- package/SPEC.md +10 -0
- package/client.js +583 -132
- package/lib/compaction.js +262 -0
- package/lib/http.js +30 -2
- package/lib/runtime.js +12 -5
- package/lib/store.js +10 -1
- package/package.json +1 -1
package/DESIGN.md
CHANGED
|
@@ -599,10 +599,13 @@ UI:成员面板里每行一个 switch;关掉的成员进 `excluded`。这样
|
|
|
599
599
|
|
|
600
600
|
一页两块(`settings.section`, id `context-domains`, order 35):
|
|
601
601
|
|
|
602
|
-
1.
|
|
603
|
-
2.
|
|
604
|
-
3.
|
|
605
|
-
4.
|
|
602
|
+
1. **左列 200px · 领域清单**:48px 表头(`n 个领域` + `+ 新建` + 紧邻的 `✦` AI 图标按钮)、每行 `<名称> / <总数> 项 [默认] / <技能>/<工具>`,选中行左侧 2px accent 条,底栏「未归入领域 · n skill · m tool」。
|
|
603
|
+
2. **右列 · 编辑器**:`名称` 单行、`说明` 两行 textarea(label 在上)、`默认启用` 真开关行(上下 1px 分隔线)、成员区、底部 `删除领域`(危险色描边)。
|
|
604
|
+
3. **成员区**:`搜索(右端内嵌 已选 n 项)→ 一行 [全部|技能|工具] + [显示:全部成员|已选|未选] → 260px 双列成员表`。类型页签只决定看哪一类;显示开关只筛视图,不改成员(勾选框才是加/删)。列表扁平、技能在前工具在后,类型由页签承担,空态给「清除筛选」。
|
|
605
|
+
4. **AI 归类弹窗**:`✦` 打开 520px 居中卡片,含说明、等宽 prompt(**点一下就复制**,hover 出「点击复制」、复制后变成功色的「已复制」)与底部 `复制 prompt`。prompt 不常驻右栏——它是低频操作,长期占用编辑区只会挤掉真正天天用的成员列表。归类是判断活,交给 AI 比自己一条条勾更省事,也比插件内置启发式更准(内置只按前缀建议,见 §4.3)。
|
|
606
|
+
5. 视觉稿:OpenDesign 项目 `cdx-settings-v2`(`index.html`,深/浅两套 `--cdx-*` 变量)。插件里颜色一律换成宿主 token(`--dsw-alias-*`),间距 8px 栅格、圆角 6/8/10/14、字号 11/12/13/15。
|
|
607
|
+
6. 页脚提示:「保存后:新会话立即生效;已有会话在下一轮生效(进行中的调用不受影响)」。
|
|
608
|
+
7. 首次进入空态:扫描现有 skill 的 `provider` 与 tool 前缀,给出 3~5 个建议领域(飞书 / 记忆 / 逆向 / 办公…),点一下即创建。这是解决“从零建领域太累”的关键一步。
|
|
606
609
|
|
|
607
610
|
设置页数据走 `GET /context-domains/api/catalog` + `POST /domains`;保存后广播,所有已开会话的芯片带随之刷新。
|
|
608
611
|
|
|
@@ -955,3 +958,118 @@ manageUnknownPresets: false
|
|
|
955
958
|
|
|
956
959
|
结论:本插件与 archify 的冲突已消除(`managed: false`,完全不介入它的 preset);剩下的失败是
|
|
957
960
|
「模型没写文件」与「重启打断」,与领域机制无关。
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
## 17. 压缩阈值:运行期接管 compaction-basic 的压力线(v0.4)
|
|
965
|
+
|
|
966
|
+
### 17.1 需求与约束
|
|
967
|
+
|
|
968
|
+
技能与工具把每轮的固定开销抬高,所以「什么时候压缩」不该由模型窗口的固定比例决定,
|
|
969
|
+
而该由用户按「模型能力 / token 花费 / 任务性质 / 会话现状」现场调。约束三条:
|
|
970
|
+
|
|
971
|
+
1. 调完**不重启 dsh**立刻生效;
|
|
972
|
+
2. 与模型无关(换模型不用重新调);
|
|
973
|
+
3. 界面只是顶栏最右边一个 `− 数字 +`(单位 10 万,最低 2)。
|
|
974
|
+
|
|
975
|
+
### 17.2 静态改阈值为什么不行
|
|
976
|
+
|
|
977
|
+
`compaction-basic` 的判定链只有一句:
|
|
978
|
+
|
|
979
|
+
```js
|
|
980
|
+
thresholdTokens = floor(min(contextWindow * thresholdRatio, contextWindow - reservedOutput - headroomTokens))
|
|
981
|
+
if (measurement.totalTokens < thresholdTokens) return null // 不压
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
它的 config 在 `resolveConfig()` 里被 `deepFreeze`,而且这行挂在 preset
|
|
985
|
+
(`@deepseek-ai/dsh-agent-preset` → registry `mountPreset`)的 `isolate: { compaction: true }`
|
|
986
|
+
组里 —— profile 的 `cordis.patch.yml` 只能按 id 命中顶层或 `group: true` 节点,够不到 preset
|
|
987
|
+
内部子节点(同 id 的顶层节点是 web-app 关掉的那份副本,写上去静默无效)。所以静态改阈值
|
|
988
|
+
只能改 preset 声明文件,然后重启;`~/.dsh/compaction-threshold.py` 就是那条路。
|
|
989
|
+
|
|
990
|
+
### 17.3 运行期接管:把预算写成一条精确的 modelPolicies 覆盖
|
|
991
|
+
|
|
992
|
+
关键事实:`modelPolicies` 不是装载时算完就固化的 —— `compactPressure()` 每次都调
|
|
993
|
+
`resolveTargetPolicy(this.config, target)` 现查。于是只要重写引擎实例的 `config`:
|
|
994
|
+
|
|
995
|
+
| 字段 | 取值 | 解得 |
|
|
996
|
+
|---|---|---|
|
|
997
|
+
| `thresholdRatio` | `1` | `window × 1 = window` |
|
|
998
|
+
| `headroomTokens` | `window - reservedOutput - budget`(下限 0) | `window - out - headroom = budget` |
|
|
999
|
+
| `retainTokens` | `budget × 0.25`(必须 < 阈值,原生会抛) | 压缩后保留的原文尾巴 |
|
|
1000
|
+
|
|
1001
|
+
`min(window, budget)` 就是预算本身;预算超过 `window - 输出预留` 时自然贴住物理上限。
|
|
1002
|
+
`reservedOutput` 取 `header.config.maxTokens ?? info.defaultMaxTokens`,与原生同一口径
|
|
1003
|
+
(这里用 `defaultMaxTokens` 近似:会话级 `maxTokens` 极少被显式设过,设了就少算这几十 k,
|
|
1004
|
+
不影响「到数就压」的语义)。
|
|
1005
|
+
|
|
1006
|
+
### 17.4 引擎怎么找到(isolate realm)
|
|
1007
|
+
|
|
1008
|
+
preset 是**按需挂载**的:一个 preset 定义挂一次(`createScope(owner, key)`),所有选它的会话共用
|
|
1009
|
+
同一个引擎实例。引擎在 isolate realm 的符号里,`ctx.get('compaction')` 从宿主平面拿不到
|
|
1010
|
+
(realm 私有符号),所以按服务表扫:
|
|
1011
|
+
|
|
1012
|
+
```js
|
|
1013
|
+
for (const key of Object.getOwnPropertySymbols(ctx.reflect.store)) {
|
|
1014
|
+
const impl = bag[key]
|
|
1015
|
+
if (impl?.name !== 'compaction') continue
|
|
1016
|
+
if (impl.ctx?.fiber?.state !== 2) continue // cordis 的「已激活」
|
|
1017
|
+
impl.value.compactPressure // 就是引擎
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
`impl.value` 只在该 fiber 激活后才有值,所以扫描天然跳过未挂载/已卸载的行。
|
|
1022
|
+
`ctx.reflect.store` 是整个 runtime 共用的一张表(`ReflectService` 只在 root 构造,子 ctx 原型继承),
|
|
1023
|
+
isolate realm 只是不同的符号键 —— 这也是 registry 自己的 `leakedServices()` 用的同一套读法。
|
|
1024
|
+
|
|
1025
|
+
### 17.5 为什么包 `compactPressure` 而不是换引擎
|
|
1026
|
+
|
|
1027
|
+
引擎被三处调用:`agent/pre-step`(自动压缩)、`agent/request`(模型切换,`requireBelowThreshold = true`)、
|
|
1028
|
+
`compactIfNeeded`(overflow 恢复)。三条都走 `this.compactPressure(...)`,包一层全覆盖;
|
|
1029
|
+
而且这是唯一能拿到 `header.config`(真实路由的 provider/model)的地方 —— 模型的
|
|
1030
|
+
`contextWindow` 只能在走到这一步时才知道,所以「按目标解析窗口」这个动作只能放在这里:
|
|
1031
|
+
|
|
1032
|
+
```
|
|
1033
|
+
compactPressure(agent, header, signal, requireBelowThreshold)
|
|
1034
|
+
→ 第一次见到该 target:await llm.resolveModelInfo(),学窗口,重写所有引擎的 config(同步)
|
|
1035
|
+
→ 原方法照跑
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
窗口学到之后进缓存,后续调用零开销;模型切换那条路也照样走(阈值同样被钉住,所以切到小窗口
|
|
1039
|
+
模型时会按该模型的上限先压,安全语义不变)。
|
|
1040
|
+
|
|
1041
|
+
### 17.6 降级矩阵
|
|
1042
|
+
|
|
1043
|
+
| 情形 | 行为 | 用户看到 |
|
|
1044
|
+
|---|---|---|
|
|
1045
|
+
| 冷启动、还没有会话 | 没有引擎可接管 | `degraded: 没有在线的 compaction 引擎(preset 还没挂载)`,数字琥珀色 |
|
|
1046
|
+
| 模型没声明 `contextWindow` | 该 target 回落原生判定 | `degraded: 模型没有声明 contextWindow` |
|
|
1047
|
+
| `llm` 服务不可见 | 不接管 | `degraded: llm 服务不可见,预算未接管` |
|
|
1048
|
+
| 预算 > 模型窗口 | 贴住 `window - 输出预留` | `targets[].clamped = true`,悬停提示「实际按窗口上限压」 |
|
|
1049
|
+
| 预算 < 原生比例阈值 | 照样提前压 | 无(这是正常路径) |
|
|
1050
|
+
|
|
1051
|
+
### 17.7 真机验证(独立实例,不碰在跑的 dsh)
|
|
1052
|
+
|
|
1053
|
+
在 `/tmp/dshprobe` 起了一个独立 dsh 实例(`--port 13999`,profile 用 `link:` 指到本仓库,
|
|
1054
|
+
LLM patch 抄在跑的 profile),实测:
|
|
1055
|
+
|
|
1056
|
+
| 观察 | 结果 |
|
|
1057
|
+
|---|---|
|
|
1058
|
+
| 引擎扫描 | `engines: 4`(web-app 的 standard / minimal / ptc / cordis 各挂一个) |
|
|
1059
|
+
| 目标解析 | `zifei/deepseek-v4.1-flash` → `contextWindow 1000000`、`defaultMaxTokens 64000` |
|
|
1060
|
+
| `POST {tokens:3}` | 每个引擎 `modelPolicies`: `{thresholdRatio:1, headroomTokens:636000, retainTokens:75000}` → `thresholdTokens = 300000` |
|
|
1061
|
+
| `POST {tokens:12}` | `headroomTokens:0, retainTokens:234000` → `thresholdTokens = 936000`,`clamped: true` |
|
|
1062
|
+
| `POST {tokens:1}` | 收成 200000(下限);`{tokens:"x"}` → 400 bad-request |
|
|
1063
|
+
| 持久化 | 预算写进 `state.json` 的 `compaction`,重启后仍在 |
|
|
1064
|
+
| 降级 | 适配器未注册时 `degraded: [{provider, model, reason: 'no adapter registered for provider "zifei"'}]`,配置保持原生 |
|
|
1065
|
+
|
|
1066
|
+
没验到的只有「阈值到点真的压了一次」:那需要一个真实会话跑满预算,属于模型/网络侧的动作,
|
|
1067
|
+
换算链已由上面 + `test/compaction.test.js` 覆盖。
|
|
1068
|
+
|
|
1069
|
+
### 17.8 落点
|
|
1070
|
+
|
|
1071
|
+
- `lib/compaction.js`:控制器(扫描 / 包装 / 换算 / 状态)。
|
|
1072
|
+
- `lib/runtime.js`:`budget.start()`(`ctx.effect` 持有,卸载时还原原生 config、清掉定时器)。
|
|
1073
|
+
- `lib/http.js`:`POST /compaction`,`/snapshot`、`/health` 多一个 `compaction` 块。
|
|
1074
|
+
- `lib/store.js`:`compaction = { budgetTokens, at }`(读侧缺省 50 万,等于原来 0.5 × 1M 的口径)。
|
|
1075
|
+
- `client.js`:`CompactionCounter`,渲染在 `.cdx-bar` 最右侧(`chips` 之后)。
|
package/README.md
CHANGED
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
(skill 从模型目录里摘掉、tool 从工具表里移除)。
|
|
13
13
|
- **领域条**:会话里、会话头正下方一行,`● 名称`,绿点=已加载、灰点=未加载,点一下开关;
|
|
14
14
|
悬停展开成员(技能/工具两段,名字用顿号连起来)。新会话与对话是同一个位置。
|
|
15
|
-
-
|
|
15
|
+
- **压缩阈值计数器**:领域条最右边 `− 5 +`,中间是数字、单位 10 万 token(2 = 20 万,最低值)。
|
|
16
|
+
它就是上下文压缩的触发线:到数就压,**改完下一轮生效,不用重启 dsh**。
|
|
17
|
+
- **设置 → 上下文领域**:左列领域清单,右侧 `搜索 → 技能/工具页签 + 显示开关 → 成员列表`,
|
|
18
|
+
显示开关可切「全部成员 / 已选 / 未选」;新建领域默认勾上「默认启用」。不想一条条勾时,
|
|
19
|
+
点左列 `+ 新建` 旁的 `✦`,弹窗里是给 AI 的归类 prompt,一键复制。
|
|
16
20
|
|
|
17
|
-

|
|
21
|
+

|
|
18
22
|
|
|
19
23
|
---
|
|
20
24
|
|
|
@@ -28,13 +32,13 @@
|
|
|
28
32
|
|
|
29
33
|
**新会话也一样** —— 开会话前就能定好这次要哪些能力:
|
|
30
34
|
|
|
31
|
-

|
|
35
|
+

|
|
32
36
|
|
|
33
37
|
**一个领域里同时装技能和工具** —— 悬停展开,名字用顿号连起来(技能 3 项、工具 4 项):
|
|
34
38
|
|
|
35
39
|

|
|
36
40
|
|
|
37
|
-
**设置 → 上下文领域** ——
|
|
41
|
+
**设置 → 上下文领域** —— 左边领域清单;右边搜索、技能/工具页签、显示开关三行分明:
|
|
38
42
|
|
|
39
43
|

|
|
40
44
|
|
|
@@ -70,12 +74,34 @@ dsh plugin --profile web add link:/workspace/dsh-context-domains
|
|
|
70
74
|
|
|
71
75
|
会话级选择存在 `$DSH_HOME/context-domains/state.json`(原子写、串行写、坏文件自动备份)。
|
|
72
76
|
数据结构:`Domain { id, name, icon?, description?, defaultOn, members[{type,name}] }`、
|
|
73
|
-
`sessions[id] = { domains: string[], excluded: {skills,tools} }
|
|
77
|
+
`sessions[id] = { domains: string[], excluded: {skills,tools} }`、
|
|
78
|
+
`compaction = { budgetTokens, at }`;v0.2 的 `skills/tools` 两串会在读取时自动合并。
|
|
79
|
+
|
|
80
|
+
## 压缩阈值(条最右边那个计数器)
|
|
81
|
+
|
|
82
|
+
上下文压缩由 `compaction-basic` 决定,判定只有一句:`measurement.totalTokens >= thresholdTokens`,
|
|
83
|
+
而 `thresholdTokens = floor(min(window × thresholdRatio, window − 输出预留 − headroomTokens))`。
|
|
84
|
+
它的 config 装载时就被 deepFreeze,又挂在 preset 的 isolate 组里(profile 的 `cordis.patch.yml`
|
|
85
|
+
够不到),所以静态改阈值两条路都得重启。
|
|
86
|
+
|
|
87
|
+
这里改成运行期接管:给每个 `(provider, model)` 造一条精确的 `modelPolicies` 覆盖
|
|
88
|
+
(`thresholdRatio: 1`、`headroomTokens = 窗口 − 输出预留 − 预算`),解出来阈值就等于预算本身;
|
|
89
|
+
`modelPolicies` 是每次 `compactPressure` 现查的,所以重写引擎实例的 `config` 立刻生效。
|
|
90
|
+
|
|
91
|
+
- **引擎怎么找**:preset 是按需挂载的,引擎在 isolate realm 里,`ctx.get('compaction')` 拿不到。
|
|
92
|
+
按 `ctx.reflect.store` 扫符号,取 `name === 'compaction'` 且 fiber 已激活的实现。
|
|
93
|
+
- **多引擎**:每个挂着的 preset(standard / ptc / minimal / 自定义)各有一个引擎,全部接管,同一个预算。
|
|
94
|
+
- **会话无关**:预算是全局的(引擎按 preset 一个、多会话共用),条在哪个会话上都改同一个数。
|
|
95
|
+
- **降级**:模型没声明 `contextWindow`、`llm` 服务不可见、还没有引擎挂载(冷启动尚无会话)时,
|
|
96
|
+
阈值回落到该模型的原生判定,并在 `snapshot.compaction.degraded` 里如实列出原因(条上的数字变琥珀色)。
|
|
97
|
+
- **预算高于模型窗口**:会贴住物理上限(`window − 输出预留`),`snapshot` 里 `clamped: true`;
|
|
98
|
+
真超了仍由原生的 context-overflow 恢复兜底。
|
|
74
99
|
|
|
75
100
|
## API
|
|
76
101
|
|
|
77
|
-
`/context-domains/api/{health,snapshot,catalog,domains,toggle,exclude,probe}`,全部经 `requestRejection` 栅栏。
|
|
102
|
+
`/context-domains/api/{health,snapshot,catalog,domains,toggle,exclude,probe,compaction}`,全部经 `requestRejection` 栅栏。
|
|
78
103
|
`POST /toggle` 只需要 `{ sessionId, domainId, enabled }`(v0.3 起没有 `kind`)。
|
|
104
|
+
`POST /compaction` 收 `{ tokens }`(单位 10 万,最低 2)或 `{ budgetTokens }`(绝对值,收进 20 万~1000 万)。
|
|
79
105
|
|
|
80
106
|
## 配置
|
|
81
107
|
|
package/SPEC.md
CHANGED
|
@@ -52,3 +52,13 @@ Domain { id, name, icon, description, defaultOn, members: [{ type: 'skill'|'tool
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
改动下一轮组装提示词时生效;运行中的调用不打断;芯片点色只看实际生效集合。
|
|
55
|
+
|
|
56
|
+
## v0.4 增补:顶栏压缩阈值计数器
|
|
57
|
+
|
|
58
|
+
会话顶栏最右边加一个计数器:`− 数字 +`,中间是数字、**单位 10 万 token**,最低 2(= 20 万),
|
|
59
|
+
每格 10 万。它控制的是上下文压缩的触发线(到数就压),不是模型参数:
|
|
60
|
+
|
|
61
|
+
- **全局值**:压缩引擎按 preset 挂一个、多会话共用,所以这个数是全局的;任何会话上改都立刻对全场生效。
|
|
62
|
+
- **改完不重启**:运行期重写 compaction 引擎的 `config.modelPolicies`(详见 DESIGN.md §17)。
|
|
63
|
+
- **与会话无关、与模型无关**:换模型不改数;模型窗口比数小时贴住窗口上限。
|
|
64
|
+
- **降级要说出来**:没挂引擎 / 模型没声明 window / llm 不可见,数字转琥珀色,原因在悬停里。
|