@jjh32456/dsh-longlive 1.6.4 → 1.6.5
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 +14 -0
- package/QUICKSTART.md +6 -6
- package/README.en.md +2 -2
- package/README.md +3 -3
- package/SPEC.md +3 -3
- package/docs/CONFIG.md +2 -2
- package/docs/DEV.md +4 -4
- package/docs/FAQ.md +3 -3
- package/docs/FEATURES.md +4 -1
- package/docs/QUICKSTART.md +4 -4
- package/docs/README.en.md +1 -1
- package/docs/TROUBLESHOOTING.md +1 -1
- package/package.json +1 -1
- package/src/compaction/engine-core.js +45 -12
- package/test/hot-truncate.test.mjs +59 -4
- package/test/packaging.test.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
本文件从 0.7.6 开始记录;更早的版本没有随包维护变更日志(0.7.5 及以前的 tarball 里没有
|
|
4
4
|
CHANGELOG)。
|
|
5
5
|
|
|
6
|
+
## 1.6.5 — 2026-11-06
|
|
7
|
+
|
|
8
|
+
**热截断热修复:被宿主重启中断过的会话不再永久失去自动切刀。**
|
|
9
|
+
|
|
10
|
+
根因:`selectCutPlan` 把「closed-but-not-completed」(有 `turn/end`,但 reason 为
|
|
11
|
+
aborted / interrupted / truncated)的轮次当成不可跨越的屏障。一旦这类轮次落在保留窗口
|
|
12
|
+
之前,计划会一直返回 null,自动热截断从此静默失效——实测上下文涨到 50 万 tokens 也不再
|
|
13
|
+
切一刀(1.6.4 的 300k 闸门本身没问题,是计划永远为空)。
|
|
14
|
+
|
|
15
|
+
1.6.5 改为:这类轮次继续**保留不切**,但计划从它**之后**继续找连续的已完成轮,不再返回
|
|
16
|
+
null;另外修了候选段**首轮内部的 system/message**(宿主中途注入)把段截成空的问题——现在
|
|
17
|
+
该轮原地保留、段从它之后开始。新增 H3 / H3b 回归测试覆盖这两个场景。
|
|
18
|
+
`node test/run.mjs` / `node test/run-rc.mjs` 全绿。
|
|
19
|
+
|
|
6
20
|
## 1.6.4 — 2026-11-06
|
|
7
21
|
|
|
8
22
|
**热截断参数收尾 + D6 重启兼容 + 可移植性收口。** 1.6.3-rc.2 的默认窗口在长对话里几乎
|
package/QUICKSTART.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# QUICKSTART — 五步跑通(1.6.
|
|
1
|
+
# QUICKSTART — 五步跑通(1.6.5)
|
|
2
2
|
|
|
3
3
|
目标:在一台新机器上,5 步内让 `@jjh32456/dsh-longlive` 装好、能用、能回滚、能卸干净。
|
|
4
4
|
完整背景见 [README.md](README.md),逐码排错见 [docs/ERRORS.md](docs/ERRORS.md),
|
|
@@ -42,7 +42,7 @@ $DSH = '<DSH 安装目录>\resources\runtime\cli\bin\dsh.cmd'
|
|
|
42
42
|
先把包放到固定目录,核对 sha256,再装进一个**新 profile**:
|
|
43
43
|
|
|
44
44
|
```powershell
|
|
45
|
-
$TGZ = 'C:\path\to\jjh32456-dsh-longlive-1.6.
|
|
45
|
+
$TGZ = 'C:\path\to\jjh32456-dsh-longlive-1.6.5.tgz'
|
|
46
46
|
Get-FileHash -Algorithm SHA256 $TGZ # 对照同目录的 .tgz.sha256
|
|
47
47
|
|
|
48
48
|
$LLPROFILE = 'fresh0723b'
|
|
@@ -54,7 +54,7 @@ $LLPROFILE = 'fresh0723b'
|
|
|
54
54
|
& $DSH plugin --profile $LLPROFILE list --depth 0
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`list` 应出现 `@jjh32456/dsh-longlive@1.6.
|
|
57
|
+
`list` 应出现 `@jjh32456/dsh-longlive@1.6.5`。`dsh plugin --profile <新名字> add` 自己也会初始化
|
|
58
58
|
profile,但那样只带 `@deepseek-ai/dsh-base`;要网页 UI 和与本文一致的验证,先用
|
|
59
59
|
`--from-default-profile web` 建模板 profile。
|
|
60
60
|
|
|
@@ -67,8 +67,8 @@ profile,但那样只带 `@deepseek-ai/dsh-base`;要网页 UI 和与本文一
|
|
|
67
67
|
```powershell
|
|
68
68
|
Set-Location <仓库根目录>
|
|
69
69
|
& $NODE test\run.mjs
|
|
70
|
-
& $NODE scripts\pack.mjs --expect-version 1.6.
|
|
71
|
-
# 产物:dist\jjh32456-dsh-longlive-1.6.
|
|
70
|
+
& $NODE scripts\pack.mjs --expect-version 1.6.5
|
|
71
|
+
# 产物:dist\jjh32456-dsh-longlive-1.6.5.tgz(+ .sha256)
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`pack.mjs` 只负责打包,不跑测试;`--expect-version` 会在版本不匹配时拒绝出包。
|
|
@@ -79,7 +79,7 @@ Set-Location <仓库根目录>
|
|
|
79
79
|
宿主正在跑、且当前 profile 就是目标 profile 时:
|
|
80
80
|
|
|
81
81
|
```
|
|
82
|
-
plugin_manager action=install_bundle target="file:C:/path/to/jjh32456-dsh-longlive-1.6.
|
|
82
|
+
plugin_manager action=install_bundle target="file:C:/path/to/jjh32456-dsh-longlive-1.6.5.tgz"
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
目标必须是**绝对路径**,且需要上面说的权限。装完按宿主提示重载(或重启该 profile)。
|
package/README.en.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @jjh32456/dsh-longlive (Longlive)
|
|
1
|
+
# @jjh32456/dsh-longlive (Longlive)
|
|
2
2
|
|
|
3
3
|
A DeepSeek Harness plugin that keeps a long session healthy: **inspect it, back it up, and shrink it safely** before the context window fills up. No standalone UI; no network requests or telemetry in the default configuration (Router / RouterFace / embedding call the provider you configure, only after you enable them).
|
|
4
4
|
## What you get
|
|
@@ -13,7 +13,7 @@ A DeepSeek Harness plugin that keeps a long session healthy: **inspect it, back
|
|
|
13
13
|
```powershell
|
|
14
14
|
$DSH = '<DSH install>\resources\runtime\cli\bin\dsh.cmd'
|
|
15
15
|
$NODE = '<DSH install>\resources\runtime\primary-runtime\dependencies\node\bin\node.exe'
|
|
16
|
-
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.
|
|
16
|
+
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.5.tgz"
|
|
17
17
|
& $DSH plugin --profile desktop list --depth 0
|
|
18
18
|
& $NODE "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\@jjh32456\dsh-longlive\scripts\longlive-doctor.mjs" --profile desktop
|
|
19
19
|
```
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @jjh32456/dsh-longlive(久视)
|
|
1
|
+
# @jjh32456/dsh-longlive(久视)
|
|
2
2
|
|
|
3
3
|
> 1.6.0 起包名由 `dsh-session-longlive` 改为 `dsh-longlive`;1.6.4 起发布名改为 scoped 包
|
|
4
4
|
> `@jjh32456/dsh-longlive`(npm 包名),显示名仍是「久视」(英文 Longlive);
|
|
@@ -81,7 +81,7 @@ $NODE = '<DSH 安装目录>\resources\runtime\primary-runtime\dependencies\node\
|
|
|
81
81
|
|
|
82
82
|
```powershell
|
|
83
83
|
# 1. 安装本地 tgz(用 plugin add,不要用 npm install)
|
|
84
|
-
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.
|
|
84
|
+
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.5.tgz"
|
|
85
85
|
|
|
86
86
|
# 2. 确认版本
|
|
87
87
|
& $DSH plugin --profile desktop list --depth 0
|
|
@@ -129,7 +129,7 @@ $NODE = '<DSH 安装目录>\resources\runtime\primary-runtime\dependencies\node\
|
|
|
129
129
|
|
|
130
130
|
## 版本与边界
|
|
131
131
|
|
|
132
|
-
- 本套文档对应 **1.6.
|
|
132
|
+
- 本套文档对应 **1.6.5** 的本地 tgz(1.6.0 起包名由 `dsh-session-longlive` 改为 `dsh-longlive`;
|
|
133
133
|
1.6.4 起发布名改为 scoped 包 `@jjh32456/dsh-longlive`)。
|
|
134
134
|
**1.6.3 起新增热截断**:一个不调 LLM 的 cut-only 压缩引擎,通过 bundle patch 关掉宿主 stock
|
|
135
135
|
`compaction-basic` 行并插入 `compaction-hot-truncate`;只切「已完成、且早于保留窗口(默认 4 轮 /
|
package/SPEC.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# 插件规格:@jjh32456/dsh-longlive(久视)
|
|
1
|
+
# 插件规格:@jjh32456/dsh-longlive(久视)
|
|
2
2
|
|
|
3
3
|
> 命名(2026-11-06 改):发布包名 **@jjh32456/dsh-longlive**(1.6.4 起 scoped 发布名;
|
|
4
4
|
> 1.6.0–1.6.3 为 `dsh-longlive`),显示名「久视」(英文 Longlive)。原名
|
|
5
5
|
> `dsh-session-longlive`(2026-10-30 定),1.6.0 起迁移;拼写是 longlive,不是 longevity。
|
|
6
6
|
>
|
|
7
|
-
> 当前版本:**1.6.
|
|
8
|
-
> 可移植性收口;D2/D6 默认关闭;安装包 `dist/jjh32456-dsh-longlive-1.6.
|
|
7
|
+
> 当前版本:**1.6.5**(1.6.0 改名基座 + 1.6.3 热截断 + 1.6.4 的 300k 触发 / 保留 4 轮与
|
|
8
|
+
> 可移植性收口;D2/D6 默认关闭;安装包 `dist/jjh32456-dsh-longlive-1.6.5.tgz`)。
|
|
9
9
|
|
|
10
10
|
## 零、安全性承诺(2026-10-30 要求:不能出错,出错就死了)
|
|
11
11
|
|
package/docs/CONFIG.md
CHANGED
package/docs/DEV.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @jjh32456/dsh-longlive — session longevity for DSH
|
|
1
|
+
# @jjh32456/dsh-longlive — session longevity for DSH
|
|
2
2
|
|
|
3
3
|
中文名:**会话长寿**。给 DeepSeek Harness 的宿主插件。
|
|
4
4
|
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
五步版见 [QUICKSTART.md](../QUICKSTART.md)。最短路径(tgz 已在手,CLI 不需要会话权限):
|
|
12
12
|
|
|
13
13
|
```powershell
|
|
14
|
-
& $DSH plugin --profile <profile> add "file:C:/path/to/jjh32456-dsh-longlive-1.6.
|
|
15
|
-
& $DSH plugin --profile <profile> list --depth 0 # 应显示 @1.6.
|
|
14
|
+
& $DSH plugin --profile <profile> add "file:C:/path/to/jjh32456-dsh-longlive-1.6.5.tgz"
|
|
15
|
+
& $DSH plugin --profile <profile> list --depth 0 # 应显示 @1.6.5
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
`$DSH` 指 DSH 安装目录里的 `resources\runtime\cli\bin\dsh.cmd`(干净机器的 PATH 里通常没有它)。
|
|
19
|
-
从源码安装则先 `& $NODE scripts\pack.mjs --expect-version 1.6.
|
|
19
|
+
从源码安装则先 `& $NODE scripts\pack.mjs --expect-version 1.6.5`;`$NODE` 指 DSH 安装目录里的
|
|
20
20
|
`resources\runtime\primary-runtime\dependencies\node\bin\node.exe`(>=20)。
|
|
21
21
|
会话内也可以 `plugin_manager action=install_bundle target="file:<绝对路径>"`,但那需要
|
|
22
22
|
danger-full-access 或逐次批准。
|
package/docs/FAQ.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 常见问题(FAQ)
|
|
1
|
+
# 常见问题(FAQ)
|
|
2
2
|
|
|
3
3
|
**读完你会得到什么**:装完没反应、侧栏不见了、401/404、剪完变少、换电脑、断电、卸载这些最常见的情况,能自己判断「是正常、是没装好,还是该回滚」。
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
大概率是正常的。这个插件**没有自带界面**,默认 `mode: "log"` 也只会在后台写一行评估日志,不会弹窗、不会剪东西。
|
|
10
10
|
|
|
11
|
-
按顺序确认:`dsh plugin --profile desktop list --depth 0` 有 `@jjh32456/dsh-longlive@1.6.
|
|
11
|
+
按顺序确认:`dsh plugin --profile desktop list --depth 0` 有 `@jjh32456/dsh-longlive@1.6.5`;doctor 没有硬错误(退出码 0,或只有 engram 缺失导致的 1);重启后新会话里能数到 22 个工具;聊到超过阈值后,`%USERPROFILE%\.dsh\longlive-memory\.longlive\logs\auto-truncate.log` 才出现评估行(默认阈值 300000,所以短时间内没有是正常的)。
|
|
12
12
|
|
|
13
13
|
如果这四样都没有,先看宿主启动 stderr 有没有 `1 entry did not activate`。
|
|
14
14
|
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
|
|
93
93
|
四个独立证据,至少看到两个:
|
|
94
94
|
|
|
95
|
-
1. `plugin list` 显示 `@1.6.
|
|
95
|
+
1. `plugin list` 显示 `@1.6.5`;
|
|
96
96
|
2. doctor 没有硬错误(0,或只有 engram 缺失导致的 1);
|
|
97
97
|
3. 新会话能调用 `session_health`,返回里有 `sessionPath` / `events` / `visibleMessages`;
|
|
98
98
|
4. 聊到超过阈值后,`auto-truncate.log` 出现评估行(`action` 是 `planned` / `dryrun-planned` / `none` 都算插件在评估)。
|
package/docs/FEATURES.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# 1.6.
|
|
1
|
+
# 1.6.5 可选能力与开关
|
|
2
|
+
|
|
3
|
+
1.6.5 是热截断热修复:被宿主重启中断过的会话(存在 closed-but-not-completed 轮次)不会再
|
|
4
|
+
永久失去自动切刀;计划会从被中断轮次之后继续找可切的已完成轮。其余行为与 1.6.4 相同。
|
|
2
5
|
|
|
3
6
|
1.6.4 在 1.6.0 改名基座之上加入**热截断**(cut-only `CompactionEngine`:只切已完成且早于
|
|
4
7
|
保留窗口的整轮,不调 LLM,原始事件不动,只追加一条覆盖率指针;默认 `mode=cut`,自动切达到
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# 从零到第一次截断(QUICKSTART)
|
|
1
|
+
# 从零到第一次截断(QUICKSTART)
|
|
2
2
|
|
|
3
3
|
**读完你会得到什么**:从一台已经装好 DSH 的 Windows 机器出发,装好 longlive,用 `dryrun` 看它「打算怎么剪」,再切到 `request`,最后安全地完成第一次真截断,并且知道怎么退回去。
|
|
4
4
|
|
|
5
|
-
> 适合谁:第一次拿到 `jjh32456-dsh-longlive-1.6.
|
|
5
|
+
> 适合谁:第一次拿到 `jjh32456-dsh-longlive-1.6.5.tgz` 的人。全程约 20 分钟,其中真截断那一步应用会自己关闭再打开(VM 实测约 31.5 秒),请挑你不用电脑的时候做。
|
|
6
6
|
>
|
|
7
7
|
> 本文默认 DSH home 是 `%USERPROFILE%\.dsh`;如果你设过 `DSH_HOME`,把命令里的这段整体换掉。
|
|
8
8
|
|
|
@@ -25,14 +25,14 @@ $NODE = '<DSH 安装目录>\resources\runtime\primary-runtime\dependencies\node\
|
|
|
25
25
|
## 1. 装插件:三条命令
|
|
26
26
|
|
|
27
27
|
```powershell
|
|
28
|
-
$TGZ = 'C:/path/jjh32456-dsh-longlive-1.6.
|
|
28
|
+
$TGZ = 'C:/path/jjh32456-dsh-longlive-1.6.5.tgz' # 换成真实绝对路径
|
|
29
29
|
|
|
30
30
|
& $DSH plugin --profile desktop add "file:$TGZ"
|
|
31
31
|
& $DSH plugin --profile desktop list --depth 0
|
|
32
32
|
& $NODE "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\@jjh32456\dsh-longlive\scripts\longlive-doctor.mjs" --profile desktop
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
**你应该看到**:`list` 里出现 `@jjh32456/dsh-longlive@1.6.
|
|
35
|
+
**你应该看到**:`list` 里出现 `@jjh32456/dsh-longlive@1.6.5`;doctor 打印一张表,最后是「全部可选依赖已启用。」或只有 engram 缺失的提示。
|
|
36
36
|
|
|
37
37
|
**报错了怎么办**:
|
|
38
38
|
- `ENOENT` → `file:` 后面必须是**真实存在的绝对路径**,用正斜杠;不要把 `~` 写进去。
|
package/docs/README.en.md
CHANGED
|
@@ -14,7 +14,7 @@ A DeepSeek Harness plugin that keeps a long session healthy: **inspect it, back
|
|
|
14
14
|
```powershell
|
|
15
15
|
$DSH = '<DSH install>\resources\runtime\cli\bin\dsh.cmd'
|
|
16
16
|
$NODE = '<DSH install>\resources\runtime\primary-runtime\dependencies\node\bin\node.exe'
|
|
17
|
-
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.
|
|
17
|
+
& $DSH plugin --profile desktop add "file:C:/path/jjh32456-dsh-longlive-1.6.5.tgz"
|
|
18
18
|
& $DSH plugin --profile desktop list --depth 0
|
|
19
19
|
& $NODE "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\@jjh32456\dsh-longlive\scripts\longlive-doctor.mjs" --profile desktop
|
|
20
20
|
```
|
package/docs/TROUBLESHOOTING.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjh32456/dsh-longlive",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "久视(longlive):一个磁盘上的共享大脑,让每个会话都是同一个你——记忆卡、状态卡、开机卡让新会话接上同一段关系与未完成的事;配套会话卫生(热截断/备份/D2/D6 裁剪)与记忆工具(巩固/召回/程序性记忆)。1.6.4 修复记忆根可移植性(默认根 = <DSH home>/longlive-memory,一处配置贯穿全部落盘),并同步面板 0.2.5 文案与 G1-G4 修复。",
|
|
6
6
|
"keywords": [
|
|
@@ -409,26 +409,59 @@ export function selectCutPlan(session, options, measurement, helpers = {}) {
|
|
|
409
409
|
// last positional index that belongs to the prefix.
|
|
410
410
|
let endIndex = -1;
|
|
411
411
|
for (let index = 0; index < boundary; index += 1) endIndex += groups[index].seqs.length;
|
|
412
|
-
if (endIndex < startIndex) return null;
|
|
413
412
|
|
|
414
|
-
// An aborted/unfinished turn is never cut
|
|
413
|
+
// An aborted/unfinished turn is never cut — but it must not freeze every
|
|
414
|
+
// later cut either. Keep it in place and start the span after the LAST
|
|
415
|
+
// aborted/unfinished turn in the prefix, so the completed turns that follow
|
|
416
|
+
// it stay cuttable. (2026-11-06 hotfix: a restart-aborted turn used to make
|
|
417
|
+
// selectCutPlan return null forever, so automatic hot-truncate never fired
|
|
418
|
+
// again on that session.)
|
|
419
|
+
let skipPast = -1;
|
|
415
420
|
for (let index = 0; index < boundary; index += 1) {
|
|
416
421
|
const group = groups[index];
|
|
417
|
-
if (group.kind
|
|
418
|
-
|
|
419
|
-
|
|
422
|
+
if (group.kind !== 'turn' || group.turn === 0 || group.completed) continue;
|
|
423
|
+
const lastPos = surfaceNodes.indexOf(group.seqs[group.seqs.length - 1]);
|
|
424
|
+
if (lastPos > skipPast) skipPast = lastPos;
|
|
425
|
+
}
|
|
426
|
+
if (skipPast >= 0) {
|
|
427
|
+
startIndex = Math.max(startIndex, skipPast + 1);
|
|
428
|
+
while (startIndex < surfaceNodes.length) {
|
|
429
|
+
const type = table.bySeq.get(surfaceNodes[startIndex])?.type;
|
|
430
|
+
if (type === 'user/message' || type === 'assistant/message') break;
|
|
431
|
+
startIndex += 1;
|
|
420
432
|
}
|
|
421
433
|
}
|
|
434
|
+
if (endIndex < startIndex) return null;
|
|
422
435
|
|
|
423
436
|
// The host tolerates later system nodes inside a replacement range, but this
|
|
424
437
|
// engine promises never to cut one — and never to leave half a turn behind.
|
|
425
|
-
// A system node inside the candidate span
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
// A system node inside the candidate span excludes its whole turn, unless
|
|
439
|
+
// that turn sits at the head of the span: then keep it in place and start the
|
|
440
|
+
// span after it (a mid-turn system node must not freeze the plan forever).
|
|
441
|
+
for (let guard = 0; guard < 64; guard += 1) {
|
|
442
|
+
let headAfter = null;
|
|
443
|
+
let cutAt = null;
|
|
444
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
445
|
+
if (table.bySeq.get(surfaceNodes[index])?.type !== 'system/message') continue;
|
|
446
|
+
const seq = surfaceNodes[index];
|
|
447
|
+
const owner = groups.find((group) => group.seqs.includes(seq));
|
|
448
|
+
const ownerStart = owner === undefined ? index : surfaceNodes.indexOf(owner.seqs[0]);
|
|
449
|
+
const ownerLast = owner === undefined ? index : surfaceNodes.indexOf(owner.seqs[owner.seqs.length - 1]);
|
|
450
|
+
if (ownerStart <= startIndex) { headAfter = ownerLast + 1; continue; }
|
|
451
|
+
cutAt = ownerStart - 1;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
if (headAfter !== null) {
|
|
455
|
+
startIndex = headAfter;
|
|
456
|
+
while (startIndex < surfaceNodes.length) {
|
|
457
|
+
const type = table.bySeq.get(surfaceNodes[startIndex])?.type;
|
|
458
|
+
if (type === 'user/message' || type === 'assistant/message') break;
|
|
459
|
+
startIndex += 1;
|
|
460
|
+
}
|
|
461
|
+
if (endIndex < startIndex) return null;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (cutAt !== null) endIndex = Math.min(endIndex, cutAt);
|
|
432
465
|
break;
|
|
433
466
|
}
|
|
434
467
|
if (endIndex < startIndex) return null;
|
|
@@ -395,20 +395,75 @@ test('H2 retainTurns and retainTokens both gate the cut', async () => {
|
|
|
395
395
|
assert.equal(tokenDominant.surface.nodes.length, 1 + 10);
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
-
test('H3
|
|
398
|
+
test('H3 aborted turns never freeze later cuts (and are never cut themselves)', async () => {
|
|
399
399
|
const session = new MockSession();
|
|
400
400
|
for (let turn = 1; turn <= 3; turn += 1) addTurn(session, turn, { completed: false });
|
|
401
401
|
for (let turn = 4; turn <= 9; turn += 1) addTurn(session, turn, { completed: true });
|
|
402
|
-
|
|
402
|
+
addTurn(session, 10, { open: true });
|
|
403
|
+
// 2026-11-06 hotfix: a restart-aborted prefix used to make the plan null
|
|
404
|
+
// forever. The fixed planner skips past the aborted turns and still cuts the
|
|
405
|
+
// completed turns that follow (retain window keeps the last 4 completed).
|
|
406
|
+
const { engine } = engineFor({ archiveDir: caseDir('archive'), perEvent: 5000, retainTurns: 4, retainTokens: 0, autoMinTokens: 0 });
|
|
403
407
|
const result = await engine.compactIfNeeded(makeAgent(session), 'pressure', new AbortController().signal);
|
|
404
|
-
assert.
|
|
405
|
-
assert.equal(
|
|
408
|
+
assert.ok(result, 'completed turns after the aborted prefix are still cuttable');
|
|
409
|
+
assert.equal(result.shadowedSeqs.length, 4, 'only turns 4 and 5 (2 surface nodes each) are cut');
|
|
410
|
+
assert.equal(pointerEvents(session).length, 1);
|
|
411
|
+
const summary = pointerEvents(session)[0];
|
|
412
|
+
const pointer = session.events[summary.seq + 1];
|
|
413
|
+
assert.match(pointer.data.content[0].text, /第 4–5 轮/);
|
|
406
414
|
});
|
|
407
415
|
|
|
408
416
|
// ---------------------------------------------------------------------------
|
|
409
417
|
// Archive
|
|
410
418
|
// ---------------------------------------------------------------------------
|
|
411
419
|
|
|
420
|
+
test('H3b mid-turn system node at the head of the span does not freeze later cuts', async () => {
|
|
421
|
+
// 2026-11-06 second hotfix: the candidate span can start at a turn whose
|
|
422
|
+
// first surface node is an assistant message and whose later node is a
|
|
423
|
+
// system/message (host-injected). The old system-node guard trimmed the
|
|
424
|
+
// span to nothing; the fixed planner keeps that head turn in place and
|
|
425
|
+
// starts the span after it.
|
|
426
|
+
const session = new MockSession();
|
|
427
|
+
for (let turn = 1; turn <= 3; turn += 1) addTurn(session, turn, { completed: false });
|
|
428
|
+
session.append('turn/start', { turn: 4 });
|
|
429
|
+
session.append('assistant/message', {
|
|
430
|
+
turn: 4,
|
|
431
|
+
step: 1,
|
|
432
|
+
message: {
|
|
433
|
+
id: 'assistant-4',
|
|
434
|
+
role: 'assistant',
|
|
435
|
+
source: { kind: 'model', provider: 'test', model: 'test' },
|
|
436
|
+
content: [{ type: 'text', text: '第 4 轮回答(前面是重启中断)' }],
|
|
437
|
+
},
|
|
438
|
+
}, { surfaceOp: 'append' });
|
|
439
|
+
session.append('system/message', {
|
|
440
|
+
turn: 4,
|
|
441
|
+
step: 2,
|
|
442
|
+
message: {
|
|
443
|
+
id: 'system-4',
|
|
444
|
+
role: 'system',
|
|
445
|
+
source: { kind: 'system-prompt' },
|
|
446
|
+
content: [{ type: 'text', text: 'A host-injected system message mid-turn.' }],
|
|
447
|
+
},
|
|
448
|
+
}, { surfaceOp: 'append' });
|
|
449
|
+
session.append('user/message', {
|
|
450
|
+
id: 'user-4b',
|
|
451
|
+
role: 'user',
|
|
452
|
+
content: [{ type: 'text', text: '第 4 轮用户追问' }],
|
|
453
|
+
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' },
|
|
454
|
+
}, { surfaceOp: 'append' });
|
|
455
|
+
session.append('turn/end', { turn: 4, reason: { kind: 'completed' } });
|
|
456
|
+
for (let turn = 5; turn <= 10; turn += 1) addTurn(session, turn, { completed: true });
|
|
457
|
+
addTurn(session, 11, { open: true });
|
|
458
|
+
const { engine } = engineFor({ archiveDir: caseDir('archive'), perEvent: 5000, retainTurns: 4, retainTokens: 0, autoMinTokens: 0 });
|
|
459
|
+
const result = await engine.compactIfNeeded(makeAgent(session), 'pressure', new AbortController().signal);
|
|
460
|
+
assert.ok(result, 'a mid-turn system node at the span head must not freeze the plan');
|
|
461
|
+
assert.equal(result.shadowedSeqs.length, 4, 'turns 5 and 6 (2 surface nodes each) are cut');
|
|
462
|
+
const summary = pointerEvents(session)[0];
|
|
463
|
+
const pointer = session.events[summary.seq + 1];
|
|
464
|
+
assert.match(pointer.data.content[0].text, /第 5–6 轮/);
|
|
465
|
+
});
|
|
466
|
+
|
|
412
467
|
test('H4 archive: atomic JSONL, exact event count, sha256, append-only', async () => {
|
|
413
468
|
const dir = caseDir('archive');
|
|
414
469
|
const session = buildSession(9, { openLast: false });
|
package/test/packaging.test.mjs
CHANGED
|
@@ -140,17 +140,17 @@ test('只带白名单安装后:没有 prototype,所有 src 相对 import 都
|
|
|
140
140
|
assert.deepEqual(offenders, [], `存在跨出包目录/缺失的 import:\n${offenders.join('\n')}`);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
test('R2-07 发布版本面:包版本 1.6.
|
|
144
|
-
assert.equal(PKG.version, '1.6.
|
|
143
|
+
test('R2-07 发布版本面:包版本 1.6.5,文档同步 D2/D6 能力集', () => {
|
|
144
|
+
assert.equal(PKG.version, '1.6.5');
|
|
145
145
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
146
146
|
const en = read('docs/README.en.md');
|
|
147
147
|
assert.ok(en.includes('@jjh32456/dsh-longlive-panel@0.2.5'), '面板版本未统一到 0.2.5(scoped 包名)');
|
|
148
|
-
assert.ok(en.includes('jjh32456-dsh-longlive-1.6.
|
|
148
|
+
assert.ok(en.includes('jjh32456-dsh-longlive-1.6.5.tgz'), '英文文档示例 tgz 不是 scoped 的 1.6.5');
|
|
149
149
|
assert.ok(en.includes('@jjh32456/dsh-longlive'), '英文文档缺少 scoped 包名');
|
|
150
150
|
assert.equal(en.includes('dsh-longlive-panel@0.1.3'), false, '英文文档仍残留 0.1.3');
|
|
151
151
|
assert.equal(en.includes('0.9.0-rc.1'), false, '英文文档仍残留 rc.1');
|
|
152
152
|
const features = read('docs/FEATURES.md');
|
|
153
|
-
assert.ok(features.startsWith('# 1.6.
|
|
153
|
+
assert.ok(features.startsWith('# 1.6.5'), 'FEATURES.md 标题不是 1.6.5');
|
|
154
154
|
assert.equal(features.includes('0.9.0-rc.1'), false, 'FEATURES.md 仍残留 rc.1');
|
|
155
155
|
});
|
|
156
156
|
|