dsh-ops 0.2.4 → 0.2.5-beta.1
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/CHANGELOG.md +9 -0
- package/README.en.md +20 -0
- package/README.md +17 -6
- package/README.zh.md +17 -6
- package/cordis.patch.yml +9 -2
- package/docs/auto-compact.md +102 -0
- package/dsh-plugin.json +6 -1
- package/lib/profile-install.js +1 -1
- package/node_modules/@dsh-ops/auto-compact/LICENSE +30 -0
- package/node_modules/@dsh-ops/auto-compact/README.md +23 -0
- package/node_modules/@dsh-ops/auto-compact/lib/client.js +79 -0
- package/node_modules/@dsh-ops/auto-compact/lib/config.js +60 -0
- package/node_modules/@dsh-ops/auto-compact/lib/index.js +132 -0
- package/node_modules/@dsh-ops/auto-compact/locale/en.json +6 -0
- package/node_modules/@dsh-ops/auto-compact/locale/zh.json +6 -0
- package/node_modules/@dsh-ops/auto-compact/package.json +31 -0
- package/package.json +11 -3
- package/packages/README.md +87 -0
- package/packages/auto-compact/LICENSE +30 -0
- package/packages/auto-compact/README.md +23 -0
- package/packages/auto-compact/lib/client.js +79 -0
- package/packages/auto-compact/lib/config.js +60 -0
- package/packages/auto-compact/lib/index.js +132 -0
- package/packages/auto-compact/locale/en.json +6 -0
- package/packages/auto-compact/locale/zh.json +6 -0
- package/packages/auto-compact/package.json +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this package are recorded here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
6
|
this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.5-beta.1] — Automatic context compaction (prerelease candidate)
|
|
9
|
+
|
|
10
|
+
- Add the fourth independent marketplace row, `@dsh-ops/auto-compact`, enabled by default; background remains opt-in. Bundle its internal companion package with dsh-ops so rc.2 discovers its browser face through a package-root row, with no separate publishing requirement.
|
|
11
|
+
- Add a settings control on this row in the existing Plugins component region: live threshold (50%), cooldown (120 seconds) and compaction deadline (120 seconds), with revision-fenced profile writes and restore defaults.
|
|
12
|
+
- Use the Web context-pressure projection and the same public `compactNow` API as `/compact`, only after a non-cancelled turn becomes idle; do not publish a model-callable compaction tool or modify the host's own automatic policy.
|
|
13
|
+
- Serialize this component's compactions, suppress unchanged surfaces and self-generated replacements, back off failures, suspend sessions after commit/persistence failures, and abort/drain owned work on disable.
|
|
14
|
+
- Keep host activation free of host value imports: a dependency-free Standard Schema descriptor and a scoped update hook retain validated live references. No Schemastery vendor impersonation.
|
|
15
|
+
- Add targeted safety/lifecycle/schema/browser-registration regressions. Full historical gates still contain stale pre-0.2.4 expectations; no claim of a fully green legacy suite or live Desktop acceptance.
|
|
16
|
+
|
|
8
17
|
## [0.2.4] — Independently toggled components
|
|
9
18
|
|
|
10
19
|
- Split the market bundle into shell (on), file (on), and background (off) rows with exported per-component titles, descriptions, icons and locales.
|
package/README.en.md
CHANGED
|
@@ -8,6 +8,16 @@ Routing: **file tools → bash → PowerShell 7**. Prefer bash for general comma
|
|
|
8
8
|
use pwsh only for necessary Windows-native operations. Fix bash errors in bash.
|
|
9
9
|
Windows x64 only; target DSH `0.2.0-rc.2`. [中文](README.md)
|
|
10
10
|
|
|
11
|
+
## Beta candidate
|
|
12
|
+
|
|
13
|
+
`0.2.5-beta.1` adds the fourth component and its row configuration page. It is
|
|
14
|
+
intended for the npm `beta` tag, without changing `latest`. Registry metadata is
|
|
15
|
+
the authority for publication status. Once published, explicitly install
|
|
16
|
+
`dsh-ops@0.2.5-beta.1` from the official npm registry. Exact version selection does
|
|
17
|
+
not bypass pnpm's minimum release age: wait or narrowly exempt this version,
|
|
18
|
+
never delete the lockfile or disable global policy. Fully quit/restart the app
|
|
19
|
+
and verify the installed main version and four components.
|
|
20
|
+
|
|
11
21
|
## Install, update and remove
|
|
12
22
|
|
|
13
23
|
In the official marketplace, enter **`dsh-ops`**, install, then enable. This installs
|
|
@@ -69,6 +79,7 @@ Lost ownership after disconnect and permission downgrade do not guarantee termin
|
|
|
69
79
|
| dsh-ops-bash & powershell 7 | On | Prefer ops_bash for general commands; bundled PowerShell 7 for Windows-native work |
|
|
70
80
|
| dsh-ops-file | On | Rust batch reads, search, path discovery and replacement |
|
|
71
81
|
| dsh-ops-background | Off | Scientific simulations, model training and other long-running tasks |
|
|
82
|
+
| Automatic context compaction | On | Idle-only host /compact service above a configurable Web context threshold; no additional model-facing tool |
|
|
72
83
|
|
|
73
84
|
Each row owns its tools and runtime prompt section; OFF withdraws both. Nothing is
|
|
74
85
|
appended to AGENTS.md. Shell ON adds a runtime-only configuration overlay for the
|
|
@@ -82,6 +93,15 @@ PTC invokes the tools through its generated SDK with unchanged permission checks
|
|
|
82
93
|
|
|
83
94
|
## Configuration and tools
|
|
84
95
|
|
|
96
|
+
**Beta candidate feature:** the fourth row's configuration control is in
|
|
97
|
+
Plugins → dsh-ops → Included components. Configure the threshold (default **50%**),
|
|
98
|
+
cooldown (120 seconds) and timeout (120 seconds). The policy uses the Web meter,
|
|
99
|
+
requires strictly more than the threshold, and waits for a non-cancelled idle turn.
|
|
100
|
+
It uses revision-fenced host profile persistence and does not interrupt active tools.
|
|
101
|
+
Summarization may call your model and incur cost. Disabling aborts/drains only owned
|
|
102
|
+
compaction. Installed npm `0.2.4` does not yet include this feature. See
|
|
103
|
+
[safety boundaries and validation](docs/auto-compact.md).
|
|
104
|
+
|
|
85
105
|
Edit the corresponding dsh-ops/shell, dsh-ops/file or dsh-ops/background row's config.
|
|
86
106
|
Unknown keys fail; normal component enablement is available in the marketplace.
|
|
87
107
|
|
package/README.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# dsh-ops
|
|
2
2
|
|
|
3
|
-
解决 Windows 下 DSH 反复使用 PowerShell 报错造成的 token
|
|
3
|
+
解决 Windows 下 DSH 反复使用 PowerShell 报错造成的 token、时间和模型注意力浪费。本插件的三个主要功能提供:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
- 独立 **PowerShell 7.6.6**,用于必要的 Windows 原生操作,沿用宿主 `pwsh` 工具。
|
|
7
|
-
- Rust
|
|
5
|
+
- 提供独立 **bash 5.3.15** 并优先使用bash,让你的AI不再和PowerShell打架
|
|
6
|
+
- 独立 **PowerShell 7.6.6**,用于必要的 Windows 原生操作,沿用宿主 `pwsh` 工具。PowerShell 7 比自带的 PowerShell 5 也有不少优化
|
|
7
|
+
- 一些 Rust 编写的性能更好的,更快的读取、查找、替换工具,加速任务运行,减少不必要的输入输出。由于部分工具不经过 DSH 的受限终端后端,命令与后台任务工具只在**完全权限**时启用。
|
|
8
8
|
|
|
9
9
|
执行顺序:**工具包 → bash → PowerShell 7**。bash 报错在 bash 内修正,不随意换 shell,不混用两套语法。
|
|
10
10
|
|
|
11
11
|
仅支持 **Windows x64**;目标宿主 DSH `0.2.0-rc.2`。[English](README.en.md)
|
|
12
12
|
|
|
13
|
+
## Beta 测试版候选
|
|
14
|
+
|
|
15
|
+
`0.2.5-beta.1` 增加「上下文自动压缩」及其组件配置入口,默认阈值 50%。它仅计划发布到 npm 的 `beta` 标签;`latest` 保持正式版,不自动给普通用户升级。发布状态以 registry 查询为准,发布前不能据此认定包已在线。
|
|
16
|
+
|
|
17
|
+
发布后在插件市场指定 **`dsh-ops@0.2.5-beta.1`**、选择 npm 官方源。精确指定版本**不会绕过** pnpm 的发布冷却策略;如被拦截,应等待冷却期结束或由用户对该精确版本设置豁免,不删除锁文件、不关闭全局供应链检查。安装后完全退出并重启应用,确认主包版本与四个组件。
|
|
18
|
+
|
|
13
19
|
## 安装、更新、卸载
|
|
14
20
|
|
|
15
21
|
### 官方插件市场
|
|
16
22
|
|
|
17
|
-
在目标 DSH 应用的插件市场中输入 npm 包名 **`dsh-ops`**,安装后选择启用。安装归属于当前运行的 profile,桌面端为 `desktop`。发布包依赖完整 FastCtx、bash 和 PowerShell 7 载荷;不使用安装脚本下载,不改系统 PATH。
|
|
23
|
+
在目标 DSH 应用的插件市场中输入 npm 包名 **`dsh-ops@0.2.4`**,安装后选择启用。安装归属于当前运行的 profile,桌面端为 `desktop`。发布包依赖完整 FastCtx、bash 和 PowerShell 7 载荷;不使用安装脚本下载,不改系统 PATH。
|
|
24
|
+
|
|
25
|
+
注:新发布版本暂时明确写 dsh-ops@0.2.4,避免 pnpm 安全冷却期使裸包名选择旧版本。
|
|
18
26
|
|
|
19
27
|
### npm 一键安装
|
|
20
28
|
|
|
@@ -55,13 +63,14 @@ npx --yes dsh-ops@latest uninstall --profile desktop
|
|
|
55
63
|
|
|
56
64
|
## 组件开关与提示词
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
官方市场显示四个组件,分别启停,首次安装默认开启 shell、file 和自动压缩:
|
|
59
67
|
|
|
60
68
|
| 组件 | 介绍 | 默认 | 发布的工具 |
|
|
61
69
|
| --- | --- | --- | --- |
|
|
62
70
|
| `dsh-ops-bash & powershell 7` | 将默认终端改为bash,以及bash覆盖不到时提供powershell7 | 开 | `ops_bash`;宿主 `pwsh` 使用随包 PowerShell 7 |
|
|
63
71
|
| `dsh-ops-file` | 更快、更高性能、输出更精简更省token的rust文件检索 | 开 | 四个文件工具 |
|
|
64
72
|
| `dsh-ops-background` | 为科研仿真、模型训练及其他长进程后台任务提供托管 | 关 | 启动、输出、列出、终止后台任务 |
|
|
73
|
+
| `上下文自动压缩` | Web 占用高于可设置阈值时,空闲后调用宿主 `/compact` 服务 | 开 | 自动策略,不新增模型工具 |
|
|
65
74
|
|
|
66
75
|
“默认终端改为 bash”指模型提示优先使用 `ops_bash`,**不修改系统默认终端、PATH 或宿主原有 shell 工具名称**。Shell 组件开时,通过官方配置生命周期临时指定 `pwsh-sandbox` 的 PowerShell 7 路径;关时按执行器最新原配置恢复,不写 profile 配置。执行器配置切换不代表取消已在运行的宿主命令。
|
|
67
76
|
|
|
@@ -69,6 +78,8 @@ npx --yes dsh-ops@latest uninstall --profile desktop
|
|
|
69
78
|
|
|
70
79
|
## 配置方式与工具列表
|
|
71
80
|
|
|
81
|
+
**Beta 测试版新增组件**:在上面的「包含的组件」中打开「上下文自动压缩」的配置入口,设置阈值(默认 **50%**)、冷却(120 秒)、超时(120 秒)。严格高于阈值、正常会话进入空闲后才触发,不打断运行中的工具;保存使用宿主 profile 持久化和版本冲突检查。压缩可能调用模型并产生费用。关闭组件会取消并等待它自己启动的压缩,不取消用户任务。该组件属于 `0.2.5-beta.1` 测试版候选,已安装的正式版 npm `0.2.4` 不含它;是否已发布请查 npm。详见[安全边界与验证记录](docs/auto-compact.md)。
|
|
82
|
+
|
|
72
83
|
在 DSH 的配置编辑器中找到对应的 `dsh-ops/shell`、`dsh-ops/file` 或 `dsh-ops/background` 行,调整 `config` 后保存;未知配置键会点名报错。也可直接在市场切换组件。主要配置:
|
|
73
84
|
|
|
74
85
|
```yaml
|
package/README.zh.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# dsh-ops
|
|
2
2
|
|
|
3
|
-
解决 Windows 下 DSH 反复使用 PowerShell 报错造成的 token
|
|
3
|
+
解决 Windows 下 DSH 反复使用 PowerShell 报错造成的 token、时间和模型注意力浪费。本插件的三个主要功能提供:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
- 独立 **PowerShell 7.6.6**,用于必要的 Windows 原生操作,沿用宿主 `pwsh` 工具。
|
|
7
|
-
- Rust
|
|
5
|
+
- 提供独立 **bash 5.3.15** 并优先使用bash,让你的AI不再和PowerShell打架
|
|
6
|
+
- 独立 **PowerShell 7.6.6**,用于必要的 Windows 原生操作,沿用宿主 `pwsh` 工具。PowerShell 7 比自带的 PowerShell 5 也有不少优化
|
|
7
|
+
- 一些 Rust 编写的性能更好的,更快的读取、查找、替换工具,加速任务运行,减少不必要的输入输出。由于部分工具不经过 DSH 的受限终端后端,命令与后台任务工具只在**完全权限**时启用。
|
|
8
8
|
|
|
9
9
|
执行顺序:**工具包 → bash → PowerShell 7**。bash 报错在 bash 内修正,不随意换 shell,不混用两套语法。
|
|
10
10
|
|
|
11
11
|
仅支持 **Windows x64**;目标宿主 DSH `0.2.0-rc.2`。[English](README.en.md)
|
|
12
12
|
|
|
13
|
+
## Beta 测试版候选
|
|
14
|
+
|
|
15
|
+
`0.2.5-beta.1` 增加「上下文自动压缩」及其组件配置入口,默认阈值 50%。它仅计划发布到 npm 的 `beta` 标签;`latest` 保持正式版,不自动给普通用户升级。发布状态以 registry 查询为准,发布前不能据此认定包已在线。
|
|
16
|
+
|
|
17
|
+
发布后在插件市场指定 **`dsh-ops@0.2.5-beta.1`**、选择 npm 官方源。精确指定版本**不会绕过** pnpm 的发布冷却策略;如被拦截,应等待冷却期结束或由用户对该精确版本设置豁免,不删除锁文件、不关闭全局供应链检查。安装后完全退出并重启应用,确认主包版本与四个组件。
|
|
18
|
+
|
|
13
19
|
## 安装、更新、卸载
|
|
14
20
|
|
|
15
21
|
### 官方插件市场
|
|
16
22
|
|
|
17
|
-
在目标 DSH 应用的插件市场中输入 npm 包名 **`dsh-ops`**,安装后选择启用。安装归属于当前运行的 profile,桌面端为 `desktop`。发布包依赖完整 FastCtx、bash 和 PowerShell 7 载荷;不使用安装脚本下载,不改系统 PATH。
|
|
23
|
+
在目标 DSH 应用的插件市场中输入 npm 包名 **`dsh-ops@0.2.4`**,安装后选择启用。安装归属于当前运行的 profile,桌面端为 `desktop`。发布包依赖完整 FastCtx、bash 和 PowerShell 7 载荷;不使用安装脚本下载,不改系统 PATH。
|
|
24
|
+
|
|
25
|
+
注:新发布版本暂时明确写 dsh-ops@0.2.4,避免 pnpm 安全冷却期使裸包名选择旧版本。
|
|
18
26
|
|
|
19
27
|
### npm 一键安装
|
|
20
28
|
|
|
@@ -55,13 +63,14 @@ npx --yes dsh-ops@latest uninstall --profile desktop
|
|
|
55
63
|
|
|
56
64
|
## 组件开关与提示词
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
官方市场显示四个组件,分别启停,首次安装默认开启 shell、file 和自动压缩:
|
|
59
67
|
|
|
60
68
|
| 组件 | 介绍 | 默认 | 发布的工具 |
|
|
61
69
|
| --- | --- | --- | --- |
|
|
62
70
|
| `dsh-ops-bash & powershell 7` | 将默认终端改为bash,以及bash覆盖不到时提供powershell7 | 开 | `ops_bash`;宿主 `pwsh` 使用随包 PowerShell 7 |
|
|
63
71
|
| `dsh-ops-file` | 更快、更高性能、输出更精简更省token的rust文件检索 | 开 | 四个文件工具 |
|
|
64
72
|
| `dsh-ops-background` | 为科研仿真、模型训练及其他长进程后台任务提供托管 | 关 | 启动、输出、列出、终止后台任务 |
|
|
73
|
+
| `上下文自动压缩` | Web 占用高于可设置阈值时,空闲后调用宿主 `/compact` 服务 | 开 | 自动策略,不新增模型工具 |
|
|
65
74
|
|
|
66
75
|
“默认终端改为 bash”指模型提示优先使用 `ops_bash`,**不修改系统默认终端、PATH 或宿主原有 shell 工具名称**。Shell 组件开时,通过官方配置生命周期临时指定 `pwsh-sandbox` 的 PowerShell 7 路径;关时按执行器最新原配置恢复,不写 profile 配置。执行器配置切换不代表取消已在运行的宿主命令。
|
|
67
76
|
|
|
@@ -69,6 +78,8 @@ npx --yes dsh-ops@latest uninstall --profile desktop
|
|
|
69
78
|
|
|
70
79
|
## 配置方式与工具列表
|
|
71
80
|
|
|
81
|
+
**Beta 测试版新增组件**:在上面的「包含的组件」中打开「上下文自动压缩」的配置入口,设置阈值(默认 **50%**)、冷却(120 秒)、超时(120 秒)。严格高于阈值、正常会话进入空闲后才触发,不打断运行中的工具;保存使用宿主 profile 持久化和版本冲突检查。压缩可能调用模型并产生费用。关闭组件会取消并等待它自己启动的压缩,不取消用户任务。该组件属于 `0.2.5-beta.1` 测试版候选,已安装的正式版 npm `0.2.4` 不含它;是否已发布请查 npm。详见[安全边界与验证记录](docs/auto-compact.md)。
|
|
82
|
+
|
|
72
83
|
在 DSH 的配置编辑器中找到对应的 `dsh-ops/shell`、`dsh-ops/file` 或 `dsh-ops/background` 行,调整 `config` 后保存;未知配置键会点名报错。也可直接在市场切换组件。主要配置:
|
|
73
84
|
|
|
74
85
|
```yaml
|
package/cordis.patch.yml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Shell and
|
|
1
|
+
# Four independent market components; no unconditional host executor override.
|
|
2
|
+
# Shell, file and idle-only automatic compaction are enabled by default.
|
|
3
|
+
# Background remains explicitly opt-in.
|
|
3
4
|
- insert:
|
|
4
5
|
- id: dsh-ops-shell
|
|
5
6
|
name: dsh-ops/shell
|
|
@@ -23,3 +24,9 @@
|
|
|
23
24
|
enableShellTools: true
|
|
24
25
|
required: false
|
|
25
26
|
toolCallTimeoutMs: 300000
|
|
27
|
+
- id: dsh-ops-auto-compact
|
|
28
|
+
name: '@dsh-ops/auto-compact'
|
|
29
|
+
config:
|
|
30
|
+
thresholdPercent: 50
|
|
31
|
+
cooldownSeconds: 120
|
|
32
|
+
timeoutSeconds: 120
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 上下文自动压缩 / Automatic context compaction
|
|
2
|
+
|
|
3
|
+
## 界面位置
|
|
4
|
+
|
|
5
|
+
本地开发版在 **插件列表 → dsh-ops → 包含的组件** 中新增第四项「上下文自动压缩」
|
|
6
|
+
(`dsh-ops-auto-compact` / `@dsh-ops/auto-compact`)。使用它旁边的配置入口进入设置,
|
|
7
|
+
不是全局设置中的独立页面。新组件默认启用;原有 background 仍默认停用。
|
|
8
|
+
`0.2.5-beta.1` 是待发布测试版候选;正式版 npm 0.2.4 不含此功能。是否发布以 registry 为准。
|
|
9
|
+
|
|
10
|
+
| 配置 | 默认 | 范围 |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `thresholdPercent` | 50 | 1–99,整数 |
|
|
13
|
+
| `cooldownSeconds` | 120 | 30–3600,整数 |
|
|
14
|
+
| `timeoutSeconds` | 120 | 10–600,整数 |
|
|
15
|
+
|
|
16
|
+
设置调用现有宿主表单的 revision-fenced mutate,保存到当前 profile 的
|
|
17
|
+
`cordis.patch.yml`,可恢复默认,不使用 localStorage。启用组件且当前客户端具有
|
|
18
|
+
宿主设置权限时才可保存;停用组件不伪装成可配置。修改即时影响下一次判定,
|
|
19
|
+
不重挂载或中断正在运行的压缩;压缩已经开始时沿用当次捕获的截止时间。
|
|
20
|
+
|
|
21
|
+
## 触发与安全
|
|
22
|
+
|
|
23
|
+
- 使用与 Web 一致的 `contextPressure` 投影:`projectedTokens ?? pressureTokens`,
|
|
24
|
+
除以 `contextWindow`,四舍五入并上限 100。显示值 **严格高于** 阈值才满足条件;
|
|
25
|
+
等于 50% 不触发。无样本或容量、异常值时跳过,不猜模型容量。
|
|
26
|
+
- 不按计时器轮询。正常 turn 进入 idle 时判定,热启用时检查已加载的 idle agent。
|
|
27
|
+
用户取消的 turn 不自动启动维护。长时间持续运行的 turn 不会在中途压缩;
|
|
28
|
+
原有宿主 pressure/overflow 自动策略保持不动。
|
|
29
|
+
- 调用宿主 `/compact` 同一个 `compaction.compactNow(agent, signal)` 服务,不向
|
|
30
|
+
模型发送 `/compact` 文本,不增加模型可调用工具或额外提示词。
|
|
31
|
+
- idle 状态本身不构成互斥:由宿主 runMaintenance admission 和 durable compaction
|
|
32
|
+
marker 执行真实串行化;busy 是正常竞态,跳过而非循环重试。
|
|
33
|
+
- 一个组件实例同时只运行一个压缩,跨会话共享冷却期;不干扰宿主自己的压缩器。
|
|
34
|
+
同一 surface 在没有新内容/阈值改变时不重复尝试,自己的摘要替换也不会形成循环。
|
|
35
|
+
无可压缩历史、失败都受冷却和 surface 门禁控制;无定时后台重试。
|
|
36
|
+
- commit/persistence 故障会暂停该会话的本组件自动压缩,直到重新加载组件。
|
|
37
|
+
应先检查会话状态,不直接重试。其它故障只记录固定脱敏消息。
|
|
38
|
+
- deadline 是 AbortSignal 取消,不是强制销毁:依赖宿主/模型适配器响应 abort。
|
|
39
|
+
即使适配器延迟退出,本组件也不开放第二个任务;关闭组件会 abort 自己的压缩并
|
|
40
|
+
await allSettled,不调用 agent.cancel,不丢用户 inbox,不杀后台任务。
|
|
41
|
+
- 摘要使用宿主配置的模型,可能产生调用费用;摘要会改变 model-visible surface,
|
|
42
|
+
原始历史/标记与持久化由宿主负责。压缩不是无损保证,不宣称百分比是未来 provider
|
|
43
|
+
请求的精确计费。文档与截图不能替代真实 Desktop 验收。
|
|
44
|
+
|
|
45
|
+
## English
|
|
46
|
+
|
|
47
|
+
The fourth component is in **Plugins → dsh-ops → Included components**. Its row
|
|
48
|
+
configuration page uses the existing host forms, revision-fenced profile writes,
|
|
49
|
+
and restore-default operations. The new component defaults to ON; background stays OFF.
|
|
50
|
+
Only integer settings in the table above are accepted. This feature belongs to the
|
|
51
|
+
0.2.5-beta.1 prerelease candidate, not npm 0.2.4. Check the registry for publication status.
|
|
52
|
+
|
|
53
|
+
The policy matches the rounded Web context meter and requires a value strictly
|
|
54
|
+
above the threshold. It calls the same public idle-maintenance service as `/compact`
|
|
55
|
+
after a non-cancelled turn, never during active tools. Missing samples skip work.
|
|
56
|
+
No model-callable tool, polling, invented model capacity, host policy mutation, or
|
|
57
|
+
shared registry rewrite is involved. One owned operation, global cooldown,
|
|
58
|
+
unchanged-surface suppression, abort/drain teardown, and commit/save-failure session
|
|
59
|
+
suspension bound work. New user messages are queued by the host's maintenance seam.
|
|
60
|
+
A timeout requests cooperative cancellation, not forced quiescence. Summarization
|
|
61
|
+
may incur model cost and is not guaranteed lossless.
|
|
62
|
+
|
|
63
|
+
## 内部分发
|
|
64
|
+
|
|
65
|
+
宿主 Web module discovery 只接受 package-root row,不接受 `dsh-ops/*` 子入口。
|
|
66
|
+
因此此组件是 `packages/auto-compact/` 内的伴随包,声明 file dependency 并通过
|
|
67
|
+
`bundleDependencies` 随主包携带,无单独 npm 发布或安装要求。旧三入口不变,列表
|
|
68
|
+
仍为四组件。宿主从所选 bundle 的 dependency closure 解析这个嵌套依赖。
|
|
69
|
+
`npm pack` 后在隔离目录离线安装,已从主包锚点确认宿主入口/浏览器入口可解析;
|
|
70
|
+
从 consumer 根直接 import 嵌套伴随包失败是预期的 npm 非提升布局,不假称根可解析。
|
|
71
|
+
|
|
72
|
+
## 验证记录
|
|
73
|
+
|
|
74
|
+
Target: DSH 0.2.0-rc.2. Runtime packages and vendored Rust are unchanged.
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
node test/run.mjs auto-compact
|
|
78
|
+
node --check packages/auto-compact/lib/client.js
|
|
79
|
+
node --check packages/auto-compact/lib/index.js
|
|
80
|
+
git diff --check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Targeted regressions cover schema rehydration with the real host library, real
|
|
84
|
+
Cordis mounting/live update/unloading, strict bounds and occupancy, cancellation,
|
|
85
|
+
serialization, no-op/self-replacement suppression, deadline/drain, errors and row
|
|
86
|
+
registration. The compaction backend and browser rendering are test seams, not a
|
|
87
|
+
real provider-backed Desktop E2E. Manual acceptance still required:
|
|
88
|
+
|
|
89
|
+
1. Install a local development artifact in an isolated profile, reload the app,
|
|
90
|
+
confirm four components and this row's configuration control.
|
|
91
|
+
2. Save 60%, reload page/app, confirm profile persistence; restore 50%.
|
|
92
|
+
3. Grow a conversation past the threshold, confirm one compaction after idle;
|
|
93
|
+
inspect its durable marker and summary; queue input during compaction.
|
|
94
|
+
4. Cancel a turn: no fresh automatic compaction. Disable during a summary: owned
|
|
95
|
+
abort/drain, no user/tool cancellation. Existing tools and background remain live.
|
|
96
|
+
5. Missing model capacity, busy/manual compaction, provider failure and persistence
|
|
97
|
+
failure: no spin/no duplicate summary; inspect warnings and original session log.
|
|
98
|
+
|
|
99
|
+
Full historical `npm run verify` is currently blocked by a pre-existing SVG-only
|
|
100
|
+
icon expectation while the package intentionally ships PNG. Running the old suites
|
|
101
|
+
also reveals earlier ladder/tool/metadata expectations incompatible with 0.2.4;
|
|
102
|
+
no unrelated legacy migration is included in this change.
|
package/dsh-plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifestVersion": "0.15",
|
|
4
4
|
"id": "dsh-ops.fastctx",
|
|
5
5
|
"name": "dsh-ops",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.5-beta.1",
|
|
7
7
|
"license": "MIT AND Apache-2.0",
|
|
8
8
|
"facets": {
|
|
9
9
|
"host": {
|
|
@@ -109,6 +109,11 @@
|
|
|
109
109
|
"id": "dsh-ops-background",
|
|
110
110
|
"entry": "dsh-ops/background",
|
|
111
111
|
"defaultEnabled": false
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "dsh-ops-auto-compact",
|
|
115
|
+
"entry": "@dsh-ops/auto-compact",
|
|
116
|
+
"defaultEnabled": true
|
|
112
117
|
}
|
|
113
118
|
]
|
|
114
119
|
},
|
package/lib/profile-install.js
CHANGED
|
@@ -50,7 +50,7 @@ export function profileCommand(operation, { profile, dshCli, dryRun = false }) {
|
|
|
50
50
|
if (/\.cmd$/i.test(command)) {
|
|
51
51
|
// cmd wrappers are Windows-owned launchers; reject shell metacharacters in caller-supplied paths.
|
|
52
52
|
if (/["\r\n&|<>^%!]/.test(command)) throw new Error('Unsafe CLI path; use a path without cmd metacharacters.')
|
|
53
|
-
result = spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `""${command}" ${args.join(' ')}"`], { stdio: 'inherit', windowsHide: true })
|
|
53
|
+
result = spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `""${command}" ${args.join(' ')}"`], { stdio: 'inherit', windowsHide: true, windowsVerbatimArguments: true })
|
|
54
54
|
} else result = spawnSync(command, [...prefix, ...args], { stdio: 'inherit', windowsHide: true })
|
|
55
55
|
if (result.error) throw result.error
|
|
56
56
|
if (result.status !== 0) return result.status ?? 1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 T-Auto and the dsh-ops contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
-------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
This MIT license covers the dsh-ops plugin: everything in this repository
|
|
26
|
+
except `vendor/fastctx/`, which is FastCtx under the Apache License 2.0. The
|
|
27
|
+
distribution as a whole is therefore licensed under the composite
|
|
28
|
+
`MIT AND Apache-2.0`. See `NOTICE` for the full statement and the attribution
|
|
29
|
+
FastCtx requires, and `vendor/fastctx/LICENSE-APACHE` for the Apache License
|
|
30
|
+
2.0 text.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @dsh-ops/auto-compact
|
|
2
|
+
|
|
3
|
+
Internal, bundled companion of dsh-ops. It adds one independently toggled row to
|
|
4
|
+
**Plugins → dsh-ops → Included components**, not a second marketplace bundle.
|
|
5
|
+
The package-root entry is required by DSH rc.2's browser module discovery; plugin
|
|
6
|
+
subpaths are not browser entry points. No separate installation is required.
|
|
7
|
+
|
|
8
|
+
Defaults: threshold 50% (integer 1–99), cooldown 120s (30–3600), timeout 120s
|
|
9
|
+
(10–600). Configuration belongs to this row and uses host profile settings with
|
|
10
|
+
revision fencing and restore defaults. The policy calls the same public idle
|
|
11
|
+
maintenance API as `/compact` only above the rounded Web context percentage and
|
|
12
|
+
after a non-cancelled turn. Running tools are not interrupted. No new model-facing
|
|
13
|
+
tool, polling, or host auto-policy modification is introduced.
|
|
14
|
+
|
|
15
|
+
One owned operation at a time, cooldown, unchanged-surface suppression and
|
|
16
|
+
abort/drain teardown bound work. Commit/save failures suspend this session until
|
|
17
|
+
component reload and require inspection. Timeouts rely on cooperative cancellation.
|
|
18
|
+
Summarization may call the host's configured model and incur cost; it is not
|
|
19
|
+
lossless. Missing samples/capacity and busy admission skip work.
|
|
20
|
+
|
|
21
|
+
This companion is carried by the dsh-ops 0.2.5-beta.1 prerelease candidate, not the
|
|
22
|
+
stable 0.2.4. Check registry metadata for publication status. See the main repository's
|
|
23
|
+
`docs/auto-compact.md` for safety limits and manual acceptance requirements.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Browser artifact in the host's closure-factory module format; no globals except its loader. */
|
|
2
|
+
window.__ModuleLoader__.load({ id: '@dsh-ops/auto-compact', factory: require => {
|
|
3
|
+
const { createElement: h, useState, useEffect, useRef } = require('react')
|
|
4
|
+
const NS = 'dshOps.autoCompact'
|
|
5
|
+
const dictionaries = {
|
|
6
|
+
en: {
|
|
7
|
+
title: 'Settings', threshold: 'Context threshold (%)', cooldown: 'Cooldown (seconds)', timeout: 'Compaction timeout (seconds)',
|
|
8
|
+
save: 'Save', reset: 'Restore defaults', saved: 'Saved', failed: 'Not saved. Configuration may have changed; reload and retry.',
|
|
9
|
+
unavailable: 'Enable this component to configure it. This client must have host settings write access.',
|
|
10
|
+
hint: 'When the Web context meter is strictly above the threshold, compact at the next idle boundary using the same service as /compact. Running tools are not interrupted. Summarization may call your configured model and incur cost. No extra model-facing tool is added.',
|
|
11
|
+
invalid: 'Enter integers: threshold 1–99, cooldown 30–3600, timeout 10–600.',
|
|
12
|
+
},
|
|
13
|
+
zh: {
|
|
14
|
+
title: '设置', threshold: '上下文阈值(%)', cooldown: '冷却时间(秒)', timeout: '压缩超时(秒)',
|
|
15
|
+
save: '保存', reset: '恢复默认', saved: '已保存', failed: '未保存,配置可能已变化,请重新加载后重试。',
|
|
16
|
+
unavailable: '请先启用此组件;当前客户端还需具有宿主设置写入权限。',
|
|
17
|
+
hint: 'Web 上下文占用严格高于阈值后,在下一次空闲边界调用与 /compact 相同的服务;不会打断正在运行的工具。摘要可能调用已配置模型并产生费用,不额外增加模型可调用工具。',
|
|
18
|
+
invalid: '请输入整数:阈值 1–99,冷却 30–3600 秒,超时 10–600 秒。',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
const defaults = { thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 }
|
|
22
|
+
const fields = [
|
|
23
|
+
['thresholdPercent', 'threshold', 1, 99],
|
|
24
|
+
['cooldownSeconds', 'cooldown', 30, 3600],
|
|
25
|
+
['timeoutSeconds', 'timeout', 10, 600],
|
|
26
|
+
]
|
|
27
|
+
function Settings({ view, form, t }) {
|
|
28
|
+
const [draft, setDraft] = useState(defaults)
|
|
29
|
+
const [revision, setRevision] = useState(undefined)
|
|
30
|
+
const [busy, setBusy] = useState(false)
|
|
31
|
+
const [message, setMessage] = useState('')
|
|
32
|
+
const alive = useRef(false)
|
|
33
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false } }, [])
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (form?.state.status === 'ready') {
|
|
36
|
+
setDraft({ ...defaults, ...form.state.value })
|
|
37
|
+
setRevision(form.state.revision)
|
|
38
|
+
}
|
|
39
|
+
}, [form?.state.revision, form?.state.status])
|
|
40
|
+
if (view === 'summary') return t('hint')
|
|
41
|
+
const writable = form?.state.status === 'ready' && form.state.writable && form.state.mode === 'host'
|
|
42
|
+
async function save(reset) {
|
|
43
|
+
if (!writable || busy || revision === undefined) return
|
|
44
|
+
if (!reset && fields.some(([key, , min, max]) => !Number.isInteger(Number(draft[key])) || String(draft[key]).trim() === '' || Number(draft[key]) < min || Number(draft[key]) > max)) {
|
|
45
|
+
setMessage(t('invalid')); return
|
|
46
|
+
}
|
|
47
|
+
setBusy(true); setMessage('')
|
|
48
|
+
try {
|
|
49
|
+
const ops = fields.map(([key]) => reset
|
|
50
|
+
? { op: 'unset', path: [key] }
|
|
51
|
+
: { op: 'set', path: [key], value: Number(draft[key]) })
|
|
52
|
+
const ok = await form.mutate(ops, revision)
|
|
53
|
+
if (alive.current) setMessage(t(ok ? 'saved' : 'failed'))
|
|
54
|
+
} catch { if (alive.current) setMessage(t('failed')) }
|
|
55
|
+
finally { if (alive.current) setBusy(false) }
|
|
56
|
+
}
|
|
57
|
+
return h('section', { 'data-dsh-ops-auto-compact-settings': true },
|
|
58
|
+
h('h4', null, t('title')),
|
|
59
|
+
h('p', null, t('hint')),
|
|
60
|
+
!writable ? h('p', { role: 'status' }, t('unavailable')) : null,
|
|
61
|
+
...fields.map(([key, label, min, max]) => h('label', { key, style: { display: 'block', marginBottom: 12 } },
|
|
62
|
+
t(label), ' ', h('input', {
|
|
63
|
+
type: 'number', min, max, step: 1, value: draft[key], disabled: !writable || busy,
|
|
64
|
+
onChange: event => setDraft(current => ({ ...current, [key]: event.target.value })),
|
|
65
|
+
}))),
|
|
66
|
+
h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(false) }, t('save')), ' ',
|
|
67
|
+
h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(true) }, t('reset')),
|
|
68
|
+
h('p', { role: 'status' }, message))
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
inject: ['slots', 'locale'],
|
|
72
|
+
apply(ctx) {
|
|
73
|
+
ctx.effect(() => ctx.get('locale').register(NS, dictionaries), 'dsh-ops settings locales')
|
|
74
|
+
ctx.get('slots').inject('plugins.row.config', () => ctx.get('slots').register({
|
|
75
|
+
name: 'plugins.row.config', key: 'dsh-ops#dsh-ops-auto-compact', locale: NS,
|
|
76
|
+
}, Settings))
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
} })
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Dependency-free strict settings and structural host form descriptor. */
|
|
2
|
+
export const AUTO_COMPACT_DEFAULTS = Object.freeze({ thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 })
|
|
3
|
+
const LIMITS = Object.freeze({ thresholdPercent: [1, 99], cooldownSeconds: [30, 3600], timeoutSeconds: [10, 600] })
|
|
4
|
+
const WRITE = Symbol.for('cosmokit.volatile.write')
|
|
5
|
+
|
|
6
|
+
export function resolveAutoCompactConfig(input = {}) {
|
|
7
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)
|
|
8
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(input))) {
|
|
9
|
+
throw new TypeError('dsh-ops auto-compact config must be a plain object')
|
|
10
|
+
}
|
|
11
|
+
for (const key of Object.keys(input)) {
|
|
12
|
+
if (!Object.hasOwn(LIMITS, key)) throw new TypeError(`dsh-ops auto-compact: unknown config key ${key}`)
|
|
13
|
+
}
|
|
14
|
+
return Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, fallback]) => {
|
|
15
|
+
const raw = input[key]
|
|
16
|
+
const value = raw !== null && typeof raw === 'object' && WRITE in raw ? raw.get() : raw === undefined ? fallback : raw
|
|
17
|
+
const [min, max] = LIMITS[key]
|
|
18
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
19
|
+
throw new TypeError(`dsh-ops auto-compact: ${key} must be an integer between ${min} and ${max}`)
|
|
20
|
+
}
|
|
21
|
+
return [key, value]
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard Schema validation plus a serializable structural descriptor, not a
|
|
27
|
+
* Schemastery instance or vendor impersonation. The host rehydrates with its OWN
|
|
28
|
+
* library. No loading-time host values are imported. Live references implement
|
|
29
|
+
* the shared cosmokit.volatile.write protocol and are validated before updates.
|
|
30
|
+
*/
|
|
31
|
+
export function createAutoCompactConfigSchema() {
|
|
32
|
+
const dict = Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, value]) => [key, {
|
|
33
|
+
type: 'number',
|
|
34
|
+
meta: { default: value, min: LIMITS[key][0], max: LIMITS[key][1], step: 1, volatile: true },
|
|
35
|
+
toJSON() { return { type: this.type, meta: { ...this.meta } } },
|
|
36
|
+
}]))
|
|
37
|
+
return {
|
|
38
|
+
type: 'object', meta: { default: {} }, dict,
|
|
39
|
+
'~standard': {
|
|
40
|
+
version: 1, vendor: 'dsh-ops',
|
|
41
|
+
validate(input) {
|
|
42
|
+
try {
|
|
43
|
+
const value = Object.fromEntries(Object.entries(resolveAutoCompactConfig(input)).map(([key, initial]) => {
|
|
44
|
+
let current = initial
|
|
45
|
+
return [key, Object.freeze({ get: () => current, [WRITE]: next => { current = next } })]
|
|
46
|
+
}))
|
|
47
|
+
return { value }
|
|
48
|
+
} catch (error) { return { issues: [{ message: error.message }] } }
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
toJSON() {
|
|
52
|
+
const refs = { 0: { type: 'object', meta: { default: {} }, dict: {} } }
|
|
53
|
+
Object.entries(dict).forEach(([key, node], index) => {
|
|
54
|
+
refs[0].dict[key] = index + 1
|
|
55
|
+
refs[index + 1] = { type: node.type, meta: { ...node.meta } }
|
|
56
|
+
})
|
|
57
|
+
return { uid: 0, refs }
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Idle-only policy over the same public compaction seam as /compact. */
|
|
2
|
+
import { createAutoCompactConfigSchema, resolveAutoCompactConfig } from './config.js'
|
|
3
|
+
|
|
4
|
+
export const name = 'dsh-ops-auto-compact'
|
|
5
|
+
export const inject = ['compaction', 'sessionProjections']
|
|
6
|
+
export const Config = createAutoCompactConfigSchema()
|
|
7
|
+
|
|
8
|
+
/** Match the Web context meter; absent/invalid samples never trigger work. */
|
|
9
|
+
export function contextPercent(pressure) {
|
|
10
|
+
const used = pressure?.projectedTokens ?? pressure?.pressureTokens
|
|
11
|
+
const capacity = pressure?.contextWindow
|
|
12
|
+
if (!Number.isFinite(used) || used < 0 || !Number.isFinite(capacity) || capacity <= 0) return null
|
|
13
|
+
return Math.min(100, Math.round(used / capacity * 100))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Surface changes, not compaction bookkeeping, permit a fresh attempt. */
|
|
17
|
+
function surfaceKey(session) {
|
|
18
|
+
return JSON.stringify([session.surface.contentGeneration, session.surface.nodes])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Bounded, event-driven supervisor. It never publishes a model-callable tool. */
|
|
22
|
+
export function createAutoCompactor(ctx, rawConfig, {
|
|
23
|
+
now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const states = new WeakMap()
|
|
26
|
+
const active = new Map()
|
|
27
|
+
const disposedAgents = new WeakSet()
|
|
28
|
+
let closed = false
|
|
29
|
+
// One summary at a time for this component, across all sessions.
|
|
30
|
+
let globalNextAt = 0
|
|
31
|
+
|
|
32
|
+
function check(agent) {
|
|
33
|
+
if (closed || disposedAgents.has(agent) || agent.status !== 'idle' || active.size > 0) return
|
|
34
|
+
let config, percent, key
|
|
35
|
+
try {
|
|
36
|
+
config = resolveAutoCompactConfig(rawConfig)
|
|
37
|
+
percent = contextPercent(ctx.get('sessionProjections').snapshot(agent.session, ['contextPressure']).values.contextPressure)
|
|
38
|
+
key = surfaceKey(agent.session)
|
|
39
|
+
} catch {
|
|
40
|
+
// No guessed capacity, no admission when a required service disappears.
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (percent === null || percent <= config.thresholdPercent) return
|
|
44
|
+
const state = states.get(agent.session)
|
|
45
|
+
const time = now()
|
|
46
|
+
if (time < globalNextAt || time < (state?.nextAt ?? 0)
|
|
47
|
+
|| state?.blocked
|
|
48
|
+
|| state?.key === key && state.threshold === config.thresholdPercent) return
|
|
49
|
+
|
|
50
|
+
const controller = new AbortController()
|
|
51
|
+
const slot = { controller, done: undefined }
|
|
52
|
+
// Install before calling compactNow: service admission may emit synchronously.
|
|
53
|
+
active.set(agent, slot)
|
|
54
|
+
states.set(agent.session, { key, threshold: config.thresholdPercent, nextAt: time + config.cooldownSeconds * 1000 })
|
|
55
|
+
globalNextAt = time + config.cooldownSeconds * 1000
|
|
56
|
+
const timer = setTimer(() => controller.abort(new Error('Automatic compaction timed out')), config.timeoutSeconds * 1000)
|
|
57
|
+
timer.unref?.()
|
|
58
|
+
let blocked = false
|
|
59
|
+
slot.done = (async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await ctx.get('compaction').compactNow(agent, controller.signal)
|
|
62
|
+
if (result !== null) ctx.logger.info('dsh-ops auto-compact: compacted idle session history')
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const code = error?.code
|
|
65
|
+
if (code === 'commit' || code === 'persistence') {
|
|
66
|
+
blocked = true
|
|
67
|
+
ctx.logger.warn('dsh-ops auto-compact: session requires inspection after a commit/save failure; automatic compaction is suspended until this component is reloaded')
|
|
68
|
+
} else if (!controller.signal.aborted && code !== 'busy' && code !== 'cancelled') {
|
|
69
|
+
// Do not log history, provider errors, paths or credentials.
|
|
70
|
+
ctx.logger.warn('dsh-ops auto-compact: compaction failed; automatic retry is backed off')
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimer(timer)
|
|
74
|
+
// Suppress retries against our own replacement or bookkeeping records.
|
|
75
|
+
try {
|
|
76
|
+
states.set(agent.session, {
|
|
77
|
+
key: surfaceKey(agent.session), threshold: config.thresholdPercent, blocked,
|
|
78
|
+
nextAt: now() + config.cooldownSeconds * 1000,
|
|
79
|
+
})
|
|
80
|
+
} catch { /* disposed session */ }
|
|
81
|
+
globalNextAt = now() + config.cooldownSeconds * 1000
|
|
82
|
+
if (active.get(agent) === slot) active.delete(agent)
|
|
83
|
+
}
|
|
84
|
+
})()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function retire(agent) {
|
|
88
|
+
disposedAgents.add(agent)
|
|
89
|
+
active.get(agent)?.controller.abort(new Error('Agent disposed'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function dispose() {
|
|
93
|
+
closed = true
|
|
94
|
+
const slots = [...active.values()]
|
|
95
|
+
for (const slot of slots) slot.controller.abort(new Error('Automatic compaction component disabled'))
|
|
96
|
+
await Promise.allSettled(slots.map(slot => slot.done))
|
|
97
|
+
}
|
|
98
|
+
return { check, retire, dispose }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Keep this plugin's validated live references stable through Fiber.update. */
|
|
102
|
+
export function installLiveConfigUpdates(ctx, config) {
|
|
103
|
+
ctx.on('internal/update', (candidate, _noSave, _next) => {
|
|
104
|
+
const values = resolveAutoCompactConfig(candidate)
|
|
105
|
+
const write = Symbol.for('cosmokit.volatile.write')
|
|
106
|
+
for (const [key, value] of Object.entries(values)) config[key][write](value)
|
|
107
|
+
try { ctx.emit('loader/volatile-update', Object.keys(values).map(key => [key])) }
|
|
108
|
+
catch { ctx.logger.warn('dsh-ops auto-compact: live config notification failed') }
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Scoped listeners, abort and drain follow the host's teardown lifecycle. */
|
|
113
|
+
export function apply(ctx, config = {}, { liveUpdates = true } = {}) {
|
|
114
|
+
resolveAutoCompactConfig(config)
|
|
115
|
+
if (liveUpdates) installLiveConfigUpdates(ctx, config)
|
|
116
|
+
const policy = createAutoCompactor(ctx, config)
|
|
117
|
+
const lastTurnSignals = new WeakMap()
|
|
118
|
+
ctx.effect(function* () {
|
|
119
|
+
yield () => policy.dispose()
|
|
120
|
+
yield ctx.on('agent/status', ({ agent, status }) => {
|
|
121
|
+
if (status === 'idle' && !lastTurnSignals.get(agent)?.aborted) policy.check(agent)
|
|
122
|
+
})
|
|
123
|
+
yield ctx.on('agent/turn-stopping', ({ agent, signal }) => {
|
|
124
|
+
lastTurnSignals.set(agent, signal)
|
|
125
|
+
})
|
|
126
|
+
yield ctx.on('agent/disposed', ({ agent }) => policy.retire(agent))
|
|
127
|
+
// Hot-enabling also covers already loaded idle sessions, without waking them.
|
|
128
|
+
ctx.inject(['agents'], inner => {
|
|
129
|
+
for (const agent of inner.get('agents').list()) policy.check(agent)
|
|
130
|
+
})
|
|
131
|
+
}, 'dsh-ops auto-compact lifecycle')
|
|
132
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-ops/auto-compact",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Idle-only automatic context compaction and row settings for dsh-ops.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./locale/*.json": "./locale/*.json"
|
|
13
|
+
},
|
|
14
|
+
"dsh": {
|
|
15
|
+
"client": {
|
|
16
|
+
"platform": "web",
|
|
17
|
+
"inject": [
|
|
18
|
+
"@deepseek-ai/dsh-client-locale",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": ["lib", "locale", "LICENSE", "README.md"],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/T-Auto/dsh-ops.git",
|
|
28
|
+
"directory": "packages/auto-compact"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": { "access": "public" }
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-beta.1",
|
|
4
4
|
"publishConfig": {
|
|
5
|
-
"access": "public"
|
|
5
|
+
"access": "public",
|
|
6
|
+
"tag": "beta"
|
|
6
7
|
},
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
},
|
|
67
68
|
"files": [
|
|
68
69
|
"lib",
|
|
70
|
+
"packages/auto-compact",
|
|
69
71
|
"bin",
|
|
70
72
|
"cordis.patch.yml",
|
|
71
73
|
"dsh-plugin.json",
|
|
@@ -112,8 +114,8 @@
|
|
|
112
114
|
}
|
|
113
115
|
},
|
|
114
116
|
"optionalDependencies": {
|
|
115
|
-
"@dsh-ops/fastctx-win32-x64": "0.2.1",
|
|
116
117
|
"@dsh-ops/bash-win32-x64": "0.2.1",
|
|
118
|
+
"@dsh-ops/fastctx-win32-x64": "0.2.1",
|
|
117
119
|
"@dsh-ops/pwsh-win32-x64": "0.2.1"
|
|
118
120
|
},
|
|
119
121
|
"devDependencies": {
|
|
@@ -127,5 +129,11 @@
|
|
|
127
129
|
},
|
|
128
130
|
"engines": {
|
|
129
131
|
"node": "^22.19 || >=24"
|
|
132
|
+
},
|
|
133
|
+
"bundleDependencies": [
|
|
134
|
+
"@dsh-ops/auto-compact"
|
|
135
|
+
],
|
|
136
|
+
"dependencies": {
|
|
137
|
+
"@dsh-ops/auto-compact": "file:packages/auto-compact"
|
|
130
138
|
}
|
|
131
139
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Platform package skeletons
|
|
2
|
+
|
|
3
|
+
The three directories here are the **skeletons** of the platform packages this
|
|
4
|
+
plugin publishes, one per package, named `<kind>-<platform>-<arch>`. They are
|
|
5
|
+
not the same kind of package:
|
|
6
|
+
|
|
7
|
+
| Skeleton | Published as | What it carries |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `fastctx-win32-x64/` | the FastCtx package for Windows x64 | a payload: `bin/fastctx.exe`, built from `vendor/fastctx`, plus the Apache-2.0 licence and notice documents |
|
|
10
|
+
| `bash-win32-x64/` | the bash pin package for Windows x64 | metadata only: a pin on Git for Windows PortableGit |
|
|
11
|
+
| `pwsh-win32-x64/` | the pwsh pin package for Windows x64 | metadata only: a pin on PowerShell 7 |
|
|
12
|
+
|
|
13
|
+
Each skeleton holds only what is committed by hand: the package metadata, this
|
|
14
|
+
documentation, and — for the FastCtx package — the licence and notice documents
|
|
15
|
+
that must travel with the binary. **No binary is ever committed here.**
|
|
16
|
+
|
|
17
|
+
## What the build puts in each package
|
|
18
|
+
|
|
19
|
+
`scripts/build-platform-packages.mjs` writes every package to
|
|
20
|
+
`<dist>/stage/<kind>-<platform>-<arch>/`.
|
|
21
|
+
|
|
22
|
+
The **payload** package:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
fastctx-win32-x64/
|
|
26
|
+
├── package.json name and version injected, the rest from the skeleton
|
|
27
|
+
├── provenance.json source, toolchain, upstream revision, payload digest
|
|
28
|
+
├── bin/fastctx.exe the payload
|
|
29
|
+
├── LICENSE, NOTICE, FORK.md, carried verbatim from vendor/fastctx, as Apache-2.0
|
|
30
|
+
├── UPSTREAM.md, requires of a redistributor
|
|
31
|
+
│ THIRD_PARTY_LICENSES*.md
|
|
32
|
+
└── README.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The **pin** packages:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
bash-win32-x64/ (and pwsh-win32-x64/)
|
|
39
|
+
├── package.json name and version injected, the rest from the skeleton
|
|
40
|
+
├── provenance.json the pin: repository, tag, asset name, URL, sha256,
|
|
41
|
+
│ bytes, the measured unpacked size (files and bytes),
|
|
42
|
+
│ unpacked layout, entry point and its digest, licence,
|
|
43
|
+
│ copyright, and the statement that this package
|
|
44
|
+
│ forwards no upstream bytes
|
|
45
|
+
└── README.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Two rules keep a pin a pin, and both are enforced rather than asserted:
|
|
49
|
+
|
|
50
|
+
- **The stage may contain nothing else.** After assembly the script lists every
|
|
51
|
+
file in a pin package's stage and fails if any file is not one of
|
|
52
|
+
`package.json`, `provenance.json`, `README.md`. `--check` re-runs the same
|
|
53
|
+
test on the existing stages, so an upstream byte cannot slip into a pin
|
|
54
|
+
without failing a gate.
|
|
55
|
+
- **The pin is data, not a copy of the digests in prose.** `provenance.json` is
|
|
56
|
+
generated from the pin record — today read from `lib/shells.js` when that
|
|
57
|
+
module exports a pin table or pin function, and otherwise from the measured
|
|
58
|
+
fallback inside the build script, which the script announces on every run and
|
|
59
|
+
`--check` prints. `--verify-upstream` re-derives each asset digest from the
|
|
60
|
+
live upstream release and deletes the download again, so a stale pin is one
|
|
61
|
+
command away from being caught.
|
|
62
|
+
|
|
63
|
+
## Where the names come from
|
|
64
|
+
|
|
65
|
+
**The package name lives in exactly one module.** `lib/shells.js`
|
|
66
|
+
(`PLATFORM_PACKAGES`) owns the shell names and therefore the scope; the FastCtx
|
|
67
|
+
name comes from `lib/binary.js` (`opsFastctxPackage`). The build script imports
|
|
68
|
+
them and never spells a name out, which is why the skeleton `package.json` files
|
|
69
|
+
carry neither `name` nor `version` — a re-scoped release is one edit in `lib/`,
|
|
70
|
+
not a search-and-replace here.
|
|
71
|
+
|
|
72
|
+
The version of a platform package is always the version of the main package
|
|
73
|
+
(`package.json`), injected at build time so the two can never disagree.
|
|
74
|
+
|
|
75
|
+
## Only the payload package is injected
|
|
76
|
+
|
|
77
|
+
The main package's published copy declares the FastCtx platform package as an
|
|
78
|
+
`optionalDependency` — injected by the build script, never in the committed
|
|
79
|
+
manifest, where a version nobody has published yet would make every
|
|
80
|
+
contributor's `npm install` a 404.
|
|
81
|
+
|
|
82
|
+
The two shell pins are published but **never injected**, because installing one
|
|
83
|
+
alone cannot work: a pin carries no shell, so a dependency on it would install
|
|
84
|
+
metadata where a deployment expects a binary. They exist for a deployment that
|
|
85
|
+
deliberately resolves a shell — the plugin's own provisioning path, or an
|
|
86
|
+
explicit install — and an install on a platform they do not cover skips them
|
|
87
|
+
instead of failing.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 T-Auto and the dsh-ops contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
-------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
This MIT license covers the dsh-ops plugin: everything in this repository
|
|
26
|
+
except `vendor/fastctx/`, which is FastCtx under the Apache License 2.0. The
|
|
27
|
+
distribution as a whole is therefore licensed under the composite
|
|
28
|
+
`MIT AND Apache-2.0`. See `NOTICE` for the full statement and the attribution
|
|
29
|
+
FastCtx requires, and `vendor/fastctx/LICENSE-APACHE` for the Apache License
|
|
30
|
+
2.0 text.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @dsh-ops/auto-compact
|
|
2
|
+
|
|
3
|
+
Internal, bundled companion of dsh-ops. It adds one independently toggled row to
|
|
4
|
+
**Plugins → dsh-ops → Included components**, not a second marketplace bundle.
|
|
5
|
+
The package-root entry is required by DSH rc.2's browser module discovery; plugin
|
|
6
|
+
subpaths are not browser entry points. No separate installation is required.
|
|
7
|
+
|
|
8
|
+
Defaults: threshold 50% (integer 1–99), cooldown 120s (30–3600), timeout 120s
|
|
9
|
+
(10–600). Configuration belongs to this row and uses host profile settings with
|
|
10
|
+
revision fencing and restore defaults. The policy calls the same public idle
|
|
11
|
+
maintenance API as `/compact` only above the rounded Web context percentage and
|
|
12
|
+
after a non-cancelled turn. Running tools are not interrupted. No new model-facing
|
|
13
|
+
tool, polling, or host auto-policy modification is introduced.
|
|
14
|
+
|
|
15
|
+
One owned operation at a time, cooldown, unchanged-surface suppression and
|
|
16
|
+
abort/drain teardown bound work. Commit/save failures suspend this session until
|
|
17
|
+
component reload and require inspection. Timeouts rely on cooperative cancellation.
|
|
18
|
+
Summarization may call the host's configured model and incur cost; it is not
|
|
19
|
+
lossless. Missing samples/capacity and busy admission skip work.
|
|
20
|
+
|
|
21
|
+
This companion is carried by the dsh-ops 0.2.5-beta.1 prerelease candidate, not the
|
|
22
|
+
stable 0.2.4. Check registry metadata for publication status. See the main repository's
|
|
23
|
+
`docs/auto-compact.md` for safety limits and manual acceptance requirements.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Browser artifact in the host's closure-factory module format; no globals except its loader. */
|
|
2
|
+
window.__ModuleLoader__.load({ id: '@dsh-ops/auto-compact', factory: require => {
|
|
3
|
+
const { createElement: h, useState, useEffect, useRef } = require('react')
|
|
4
|
+
const NS = 'dshOps.autoCompact'
|
|
5
|
+
const dictionaries = {
|
|
6
|
+
en: {
|
|
7
|
+
title: 'Settings', threshold: 'Context threshold (%)', cooldown: 'Cooldown (seconds)', timeout: 'Compaction timeout (seconds)',
|
|
8
|
+
save: 'Save', reset: 'Restore defaults', saved: 'Saved', failed: 'Not saved. Configuration may have changed; reload and retry.',
|
|
9
|
+
unavailable: 'Enable this component to configure it. This client must have host settings write access.',
|
|
10
|
+
hint: 'When the Web context meter is strictly above the threshold, compact at the next idle boundary using the same service as /compact. Running tools are not interrupted. Summarization may call your configured model and incur cost. No extra model-facing tool is added.',
|
|
11
|
+
invalid: 'Enter integers: threshold 1–99, cooldown 30–3600, timeout 10–600.',
|
|
12
|
+
},
|
|
13
|
+
zh: {
|
|
14
|
+
title: '设置', threshold: '上下文阈值(%)', cooldown: '冷却时间(秒)', timeout: '压缩超时(秒)',
|
|
15
|
+
save: '保存', reset: '恢复默认', saved: '已保存', failed: '未保存,配置可能已变化,请重新加载后重试。',
|
|
16
|
+
unavailable: '请先启用此组件;当前客户端还需具有宿主设置写入权限。',
|
|
17
|
+
hint: 'Web 上下文占用严格高于阈值后,在下一次空闲边界调用与 /compact 相同的服务;不会打断正在运行的工具。摘要可能调用已配置模型并产生费用,不额外增加模型可调用工具。',
|
|
18
|
+
invalid: '请输入整数:阈值 1–99,冷却 30–3600 秒,超时 10–600 秒。',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
const defaults = { thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 }
|
|
22
|
+
const fields = [
|
|
23
|
+
['thresholdPercent', 'threshold', 1, 99],
|
|
24
|
+
['cooldownSeconds', 'cooldown', 30, 3600],
|
|
25
|
+
['timeoutSeconds', 'timeout', 10, 600],
|
|
26
|
+
]
|
|
27
|
+
function Settings({ view, form, t }) {
|
|
28
|
+
const [draft, setDraft] = useState(defaults)
|
|
29
|
+
const [revision, setRevision] = useState(undefined)
|
|
30
|
+
const [busy, setBusy] = useState(false)
|
|
31
|
+
const [message, setMessage] = useState('')
|
|
32
|
+
const alive = useRef(false)
|
|
33
|
+
useEffect(() => { alive.current = true; return () => { alive.current = false } }, [])
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (form?.state.status === 'ready') {
|
|
36
|
+
setDraft({ ...defaults, ...form.state.value })
|
|
37
|
+
setRevision(form.state.revision)
|
|
38
|
+
}
|
|
39
|
+
}, [form?.state.revision, form?.state.status])
|
|
40
|
+
if (view === 'summary') return t('hint')
|
|
41
|
+
const writable = form?.state.status === 'ready' && form.state.writable && form.state.mode === 'host'
|
|
42
|
+
async function save(reset) {
|
|
43
|
+
if (!writable || busy || revision === undefined) return
|
|
44
|
+
if (!reset && fields.some(([key, , min, max]) => !Number.isInteger(Number(draft[key])) || String(draft[key]).trim() === '' || Number(draft[key]) < min || Number(draft[key]) > max)) {
|
|
45
|
+
setMessage(t('invalid')); return
|
|
46
|
+
}
|
|
47
|
+
setBusy(true); setMessage('')
|
|
48
|
+
try {
|
|
49
|
+
const ops = fields.map(([key]) => reset
|
|
50
|
+
? { op: 'unset', path: [key] }
|
|
51
|
+
: { op: 'set', path: [key], value: Number(draft[key]) })
|
|
52
|
+
const ok = await form.mutate(ops, revision)
|
|
53
|
+
if (alive.current) setMessage(t(ok ? 'saved' : 'failed'))
|
|
54
|
+
} catch { if (alive.current) setMessage(t('failed')) }
|
|
55
|
+
finally { if (alive.current) setBusy(false) }
|
|
56
|
+
}
|
|
57
|
+
return h('section', { 'data-dsh-ops-auto-compact-settings': true },
|
|
58
|
+
h('h4', null, t('title')),
|
|
59
|
+
h('p', null, t('hint')),
|
|
60
|
+
!writable ? h('p', { role: 'status' }, t('unavailable')) : null,
|
|
61
|
+
...fields.map(([key, label, min, max]) => h('label', { key, style: { display: 'block', marginBottom: 12 } },
|
|
62
|
+
t(label), ' ', h('input', {
|
|
63
|
+
type: 'number', min, max, step: 1, value: draft[key], disabled: !writable || busy,
|
|
64
|
+
onChange: event => setDraft(current => ({ ...current, [key]: event.target.value })),
|
|
65
|
+
}))),
|
|
66
|
+
h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(false) }, t('save')), ' ',
|
|
67
|
+
h('button', { type: 'button', disabled: !writable || busy, onClick: () => save(true) }, t('reset')),
|
|
68
|
+
h('p', { role: 'status' }, message))
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
inject: ['slots', 'locale'],
|
|
72
|
+
apply(ctx) {
|
|
73
|
+
ctx.effect(() => ctx.get('locale').register(NS, dictionaries), 'dsh-ops settings locales')
|
|
74
|
+
ctx.get('slots').inject('plugins.row.config', () => ctx.get('slots').register({
|
|
75
|
+
name: 'plugins.row.config', key: 'dsh-ops#dsh-ops-auto-compact', locale: NS,
|
|
76
|
+
}, Settings))
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
} })
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Dependency-free strict settings and structural host form descriptor. */
|
|
2
|
+
export const AUTO_COMPACT_DEFAULTS = Object.freeze({ thresholdPercent: 50, cooldownSeconds: 120, timeoutSeconds: 120 })
|
|
3
|
+
const LIMITS = Object.freeze({ thresholdPercent: [1, 99], cooldownSeconds: [30, 3600], timeoutSeconds: [10, 600] })
|
|
4
|
+
const WRITE = Symbol.for('cosmokit.volatile.write')
|
|
5
|
+
|
|
6
|
+
export function resolveAutoCompactConfig(input = {}) {
|
|
7
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)
|
|
8
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(input))) {
|
|
9
|
+
throw new TypeError('dsh-ops auto-compact config must be a plain object')
|
|
10
|
+
}
|
|
11
|
+
for (const key of Object.keys(input)) {
|
|
12
|
+
if (!Object.hasOwn(LIMITS, key)) throw new TypeError(`dsh-ops auto-compact: unknown config key ${key}`)
|
|
13
|
+
}
|
|
14
|
+
return Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, fallback]) => {
|
|
15
|
+
const raw = input[key]
|
|
16
|
+
const value = raw !== null && typeof raw === 'object' && WRITE in raw ? raw.get() : raw === undefined ? fallback : raw
|
|
17
|
+
const [min, max] = LIMITS[key]
|
|
18
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
19
|
+
throw new TypeError(`dsh-ops auto-compact: ${key} must be an integer between ${min} and ${max}`)
|
|
20
|
+
}
|
|
21
|
+
return [key, value]
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard Schema validation plus a serializable structural descriptor, not a
|
|
27
|
+
* Schemastery instance or vendor impersonation. The host rehydrates with its OWN
|
|
28
|
+
* library. No loading-time host values are imported. Live references implement
|
|
29
|
+
* the shared cosmokit.volatile.write protocol and are validated before updates.
|
|
30
|
+
*/
|
|
31
|
+
export function createAutoCompactConfigSchema() {
|
|
32
|
+
const dict = Object.fromEntries(Object.entries(AUTO_COMPACT_DEFAULTS).map(([key, value]) => [key, {
|
|
33
|
+
type: 'number',
|
|
34
|
+
meta: { default: value, min: LIMITS[key][0], max: LIMITS[key][1], step: 1, volatile: true },
|
|
35
|
+
toJSON() { return { type: this.type, meta: { ...this.meta } } },
|
|
36
|
+
}]))
|
|
37
|
+
return {
|
|
38
|
+
type: 'object', meta: { default: {} }, dict,
|
|
39
|
+
'~standard': {
|
|
40
|
+
version: 1, vendor: 'dsh-ops',
|
|
41
|
+
validate(input) {
|
|
42
|
+
try {
|
|
43
|
+
const value = Object.fromEntries(Object.entries(resolveAutoCompactConfig(input)).map(([key, initial]) => {
|
|
44
|
+
let current = initial
|
|
45
|
+
return [key, Object.freeze({ get: () => current, [WRITE]: next => { current = next } })]
|
|
46
|
+
}))
|
|
47
|
+
return { value }
|
|
48
|
+
} catch (error) { return { issues: [{ message: error.message }] } }
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
toJSON() {
|
|
52
|
+
const refs = { 0: { type: 'object', meta: { default: {} }, dict: {} } }
|
|
53
|
+
Object.entries(dict).forEach(([key, node], index) => {
|
|
54
|
+
refs[0].dict[key] = index + 1
|
|
55
|
+
refs[index + 1] = { type: node.type, meta: { ...node.meta } }
|
|
56
|
+
})
|
|
57
|
+
return { uid: 0, refs }
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Idle-only policy over the same public compaction seam as /compact. */
|
|
2
|
+
import { createAutoCompactConfigSchema, resolveAutoCompactConfig } from './config.js'
|
|
3
|
+
|
|
4
|
+
export const name = 'dsh-ops-auto-compact'
|
|
5
|
+
export const inject = ['compaction', 'sessionProjections']
|
|
6
|
+
export const Config = createAutoCompactConfigSchema()
|
|
7
|
+
|
|
8
|
+
/** Match the Web context meter; absent/invalid samples never trigger work. */
|
|
9
|
+
export function contextPercent(pressure) {
|
|
10
|
+
const used = pressure?.projectedTokens ?? pressure?.pressureTokens
|
|
11
|
+
const capacity = pressure?.contextWindow
|
|
12
|
+
if (!Number.isFinite(used) || used < 0 || !Number.isFinite(capacity) || capacity <= 0) return null
|
|
13
|
+
return Math.min(100, Math.round(used / capacity * 100))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Surface changes, not compaction bookkeeping, permit a fresh attempt. */
|
|
17
|
+
function surfaceKey(session) {
|
|
18
|
+
return JSON.stringify([session.surface.contentGeneration, session.surface.nodes])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Bounded, event-driven supervisor. It never publishes a model-callable tool. */
|
|
22
|
+
export function createAutoCompactor(ctx, rawConfig, {
|
|
23
|
+
now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const states = new WeakMap()
|
|
26
|
+
const active = new Map()
|
|
27
|
+
const disposedAgents = new WeakSet()
|
|
28
|
+
let closed = false
|
|
29
|
+
// One summary at a time for this component, across all sessions.
|
|
30
|
+
let globalNextAt = 0
|
|
31
|
+
|
|
32
|
+
function check(agent) {
|
|
33
|
+
if (closed || disposedAgents.has(agent) || agent.status !== 'idle' || active.size > 0) return
|
|
34
|
+
let config, percent, key
|
|
35
|
+
try {
|
|
36
|
+
config = resolveAutoCompactConfig(rawConfig)
|
|
37
|
+
percent = contextPercent(ctx.get('sessionProjections').snapshot(agent.session, ['contextPressure']).values.contextPressure)
|
|
38
|
+
key = surfaceKey(agent.session)
|
|
39
|
+
} catch {
|
|
40
|
+
// No guessed capacity, no admission when a required service disappears.
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (percent === null || percent <= config.thresholdPercent) return
|
|
44
|
+
const state = states.get(agent.session)
|
|
45
|
+
const time = now()
|
|
46
|
+
if (time < globalNextAt || time < (state?.nextAt ?? 0)
|
|
47
|
+
|| state?.blocked
|
|
48
|
+
|| state?.key === key && state.threshold === config.thresholdPercent) return
|
|
49
|
+
|
|
50
|
+
const controller = new AbortController()
|
|
51
|
+
const slot = { controller, done: undefined }
|
|
52
|
+
// Install before calling compactNow: service admission may emit synchronously.
|
|
53
|
+
active.set(agent, slot)
|
|
54
|
+
states.set(agent.session, { key, threshold: config.thresholdPercent, nextAt: time + config.cooldownSeconds * 1000 })
|
|
55
|
+
globalNextAt = time + config.cooldownSeconds * 1000
|
|
56
|
+
const timer = setTimer(() => controller.abort(new Error('Automatic compaction timed out')), config.timeoutSeconds * 1000)
|
|
57
|
+
timer.unref?.()
|
|
58
|
+
let blocked = false
|
|
59
|
+
slot.done = (async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await ctx.get('compaction').compactNow(agent, controller.signal)
|
|
62
|
+
if (result !== null) ctx.logger.info('dsh-ops auto-compact: compacted idle session history')
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const code = error?.code
|
|
65
|
+
if (code === 'commit' || code === 'persistence') {
|
|
66
|
+
blocked = true
|
|
67
|
+
ctx.logger.warn('dsh-ops auto-compact: session requires inspection after a commit/save failure; automatic compaction is suspended until this component is reloaded')
|
|
68
|
+
} else if (!controller.signal.aborted && code !== 'busy' && code !== 'cancelled') {
|
|
69
|
+
// Do not log history, provider errors, paths or credentials.
|
|
70
|
+
ctx.logger.warn('dsh-ops auto-compact: compaction failed; automatic retry is backed off')
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimer(timer)
|
|
74
|
+
// Suppress retries against our own replacement or bookkeeping records.
|
|
75
|
+
try {
|
|
76
|
+
states.set(agent.session, {
|
|
77
|
+
key: surfaceKey(agent.session), threshold: config.thresholdPercent, blocked,
|
|
78
|
+
nextAt: now() + config.cooldownSeconds * 1000,
|
|
79
|
+
})
|
|
80
|
+
} catch { /* disposed session */ }
|
|
81
|
+
globalNextAt = now() + config.cooldownSeconds * 1000
|
|
82
|
+
if (active.get(agent) === slot) active.delete(agent)
|
|
83
|
+
}
|
|
84
|
+
})()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function retire(agent) {
|
|
88
|
+
disposedAgents.add(agent)
|
|
89
|
+
active.get(agent)?.controller.abort(new Error('Agent disposed'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function dispose() {
|
|
93
|
+
closed = true
|
|
94
|
+
const slots = [...active.values()]
|
|
95
|
+
for (const slot of slots) slot.controller.abort(new Error('Automatic compaction component disabled'))
|
|
96
|
+
await Promise.allSettled(slots.map(slot => slot.done))
|
|
97
|
+
}
|
|
98
|
+
return { check, retire, dispose }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Keep this plugin's validated live references stable through Fiber.update. */
|
|
102
|
+
export function installLiveConfigUpdates(ctx, config) {
|
|
103
|
+
ctx.on('internal/update', (candidate, _noSave, _next) => {
|
|
104
|
+
const values = resolveAutoCompactConfig(candidate)
|
|
105
|
+
const write = Symbol.for('cosmokit.volatile.write')
|
|
106
|
+
for (const [key, value] of Object.entries(values)) config[key][write](value)
|
|
107
|
+
try { ctx.emit('loader/volatile-update', Object.keys(values).map(key => [key])) }
|
|
108
|
+
catch { ctx.logger.warn('dsh-ops auto-compact: live config notification failed') }
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Scoped listeners, abort and drain follow the host's teardown lifecycle. */
|
|
113
|
+
export function apply(ctx, config = {}, { liveUpdates = true } = {}) {
|
|
114
|
+
resolveAutoCompactConfig(config)
|
|
115
|
+
if (liveUpdates) installLiveConfigUpdates(ctx, config)
|
|
116
|
+
const policy = createAutoCompactor(ctx, config)
|
|
117
|
+
const lastTurnSignals = new WeakMap()
|
|
118
|
+
ctx.effect(function* () {
|
|
119
|
+
yield () => policy.dispose()
|
|
120
|
+
yield ctx.on('agent/status', ({ agent, status }) => {
|
|
121
|
+
if (status === 'idle' && !lastTurnSignals.get(agent)?.aborted) policy.check(agent)
|
|
122
|
+
})
|
|
123
|
+
yield ctx.on('agent/turn-stopping', ({ agent, signal }) => {
|
|
124
|
+
lastTurnSignals.set(agent, signal)
|
|
125
|
+
})
|
|
126
|
+
yield ctx.on('agent/disposed', ({ agent }) => policy.retire(agent))
|
|
127
|
+
// Hot-enabling also covers already loaded idle sessions, without waking them.
|
|
128
|
+
ctx.inject(['agents'], inner => {
|
|
129
|
+
for (const agent of inner.get('agents').list()) policy.check(agent)
|
|
130
|
+
})
|
|
131
|
+
}, 'dsh-ops auto-compact lifecycle')
|
|
132
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-ops/auto-compact",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Idle-only automatic context compaction and row settings for dsh-ops.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./locale/*.json": "./locale/*.json"
|
|
13
|
+
},
|
|
14
|
+
"dsh": {
|
|
15
|
+
"client": {
|
|
16
|
+
"platform": "web",
|
|
17
|
+
"inject": [
|
|
18
|
+
"@deepseek-ai/dsh-client-locale",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": ["lib", "locale", "LICENSE", "README.md"],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/T-Auto/dsh-ops.git",
|
|
28
|
+
"directory": "packages/auto-compact"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": { "access": "public" }
|
|
31
|
+
}
|