dsh-as-aistudio 0.5.16 → 0.5.18
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 +4 -0
- package/package.json +1 -1
- package/src/client.js +2 -2
- package/src/components.js +1 -1
- package/src/vendor/host/dsh-edit-turn.js +2 -2
- package/src/vendor/host/dsh-edit-turn.patch.js +1 -1
- package/src/vendor/host/dsh-log-trim.flatten.js +1 -1
- package/src/vendor/host/dsh-log-trim.goal-repair.js +1 -1
- package/src/vendor/host/dsh-log-trim.host-libs.mjs +1 -1
- package/src/vendor/host/dsh-log-trim.js +67 -6
- package/src/vendor/host/dsh-log-trim.ledger.js +1 -1
- package/src/vendor/host/dsh-log-trim.lossless.js +1 -1
- package/src/vendor/host/dsh-log-trim.roots.js +1 -1
- package/src/vendor/host/dsh-log-trim.vendor.compact-session.mjs +72 -5
- package/src/vendor/host/dsh-log-trim.verify-session-log.mjs +1 -1
- package/src/vendor/host/dsh-log-trim.worker.mjs +1 -1
- package/src/vendor/versions.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## 中文
|
|
6
6
|
|
|
7
|
+
**0.5.18** —— 跟进 dsh-log-trim 0.7.5(裁掉被检查点遮蔽的前缀,窗口数真的降下来)。
|
|
8
|
+
|
|
9
|
+
**0.5.17** —— 跟进 dsh-log-trim 0.7.4(启动立即扫描、最近日志优先、解锁后补裁)。
|
|
10
|
+
|
|
7
11
|
**0.5.16** —— 跟进 dsh-log-trim 0.7.3(迟到 tool/result 归位,窗口裁剪不再被校验挡住)。
|
|
8
12
|
|
|
9
13
|
**0.5.15** —— 跟进 dsh-log-trim 0.7.2(重建可用:标题 / 工具通知 / seq 引用全部重映射)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-as-aistudio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"description": "Google-AI-Studio-style turn controls for DeepSeek Harness, in one install: edit a prompt and re-run it, re-run a reply in place, delete a turn from the model context, render sent prompts as Markdown, and trim idle session logs on boot. Self-contained: the plugin list shows this one row, and the five components (four actions + one maintenance) are vendored inside it - installing the studio installs nothing else.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/client.js
CHANGED
|
@@ -9642,7 +9642,7 @@ window.__ModuleLoader__.load({
|
|
|
9642
9642
|
const react = require('react')
|
|
9643
9643
|
const { jsx, jsxs } = require('react/jsx-runtime')
|
|
9644
9644
|
|
|
9645
|
-
const PLUGIN_VERSION = '0.7.
|
|
9645
|
+
const PLUGIN_VERSION = '0.7.5'
|
|
9646
9646
|
const ROUTE_PREFIX = '/api/dsh-log-trim'
|
|
9647
9647
|
|
|
9648
9648
|
/** The knobs this card edits, in the order they are drawn. */
|
|
@@ -10168,7 +10168,7 @@ window.__ModuleLoader__.load({
|
|
|
10168
10168
|
const NS = 'dsh-as-aistudio'
|
|
10169
10169
|
|
|
10170
10170
|
/** Keep in sync with package.json and src/components.js. */
|
|
10171
|
-
const PLUGIN_VERSION = '0.5.
|
|
10171
|
+
const PLUGIN_VERSION = '0.5.18'
|
|
10172
10172
|
|
|
10173
10173
|
/** Presence marker the live verifier reads. */
|
|
10174
10174
|
const DEBUG_KEY = '__DSH_AS_AISTUDIO__'
|
package/src/components.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export const STUDIO_ID = 'dsh-as-aistudio'
|
|
27
27
|
|
|
28
28
|
/** Keep in sync with package.json and src/client.js. */
|
|
29
|
-
export const STUDIO_VERSION = '0.5.
|
|
29
|
+
export const STUDIO_VERSION = '0.5.18'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The AI Studio action strip this studio composes, in the order the user meets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-edit-turn host half (dsh-edit-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-edit-turn/lib/index.js (v0.5.
|
|
3
|
+
// Source: ../dsh-edit-turn/lib/index.js (v0.5.21, sha256 1502010fdd847526)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -85,7 +85,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
85
85
|
export const name = PLUGIN_ID
|
|
86
86
|
|
|
87
87
|
/** Keep in sync with package.json and lib/client.js. */
|
|
88
|
-
export const PLUGIN_VERSION = '0.5.
|
|
88
|
+
export const PLUGIN_VERSION = '0.5.21'
|
|
89
89
|
|
|
90
90
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
91
91
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-edit-turn host half (dsh-edit-turn.patch.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-edit-turn/lib/patch.js (v0.5.
|
|
3
|
+
// Source: ../dsh-edit-turn/lib/patch.js (v0.5.21, sha256 5e1e8cce1d168f6c)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.flatten.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/flatten.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/flatten.js (v0.7.5, sha256 3aa1fdcbb120ab06)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.goal-repair.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/goal-repair.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/goal-repair.js (v0.7.5, sha256 0801a28f35667ee7)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.host-libs.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/host-libs.mjs (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/host-libs.mjs (v0.7.5, sha256 5ebed01988b5bfcf)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/index.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/index.js (v0.7.5, sha256 a32940b526c93896)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -44,7 +44,7 @@ import { readLedger, recordFor } from './dsh-log-trim.ledger.js'
|
|
|
44
44
|
|
|
45
45
|
export const name = 'dsh-log-trim'
|
|
46
46
|
export const PLUGIN_ID = name
|
|
47
|
-
export const PLUGIN_VERSION = '0.7.
|
|
47
|
+
export const PLUGIN_VERSION = '0.7.5'
|
|
48
48
|
export const ROUTE_PREFIX = '/api/dsh-log-trim'
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -62,7 +62,12 @@ const LEGACY_MEMORY_FILE = 'dsh-session-compact/scan-memory.json'
|
|
|
62
62
|
/** Where the settings card's values are kept. */
|
|
63
63
|
const CONFIG_FILE = 'dsh-log-trim/config.json'
|
|
64
64
|
/** 开机扫描先等一下界面:第一屏还没拿到会话列表时不该跟它抢线程池。 */
|
|
65
|
-
|
|
65
|
+
// 启动扫描的延迟。worker 扫描不占主线程,所以越早越好:宿主会在启动后很快
|
|
66
|
+
// 打开上次的会话并持有 session.lock,晚一步就永远轮不到那个会话。
|
|
67
|
+
const BOOT_SCAN_DELAY_MS = 0
|
|
68
|
+
// 被锁住的会话多久后再试一次;试满 20 次(10 分钟)就放弃,等下次启动。
|
|
69
|
+
const LOCKED_RETRY_MS = 30000
|
|
70
|
+
const LOCKED_RETRIES = 20
|
|
66
71
|
/** 重命名前的目录,只读兜底。 */
|
|
67
72
|
const LEGACY_CONFIG_FILE = 'dsh-session-compact/config.json'
|
|
68
73
|
|
|
@@ -162,6 +167,50 @@ async function noteWindowTrim(logFile, result, record, beforeEvents = [], afterE
|
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 被宿主打开的会话先记下来,每隔 30 秒看一次锁还在不在;一解锁就单独裁它。
|
|
173
|
+
*
|
|
174
|
+
* 启动扫描唯一的空窗是「宿主还没打开会话」,而 DSH 会在启动后很快恢复上次的会话。
|
|
175
|
+
* 赶不上就退而求其次:等用户切走、宿主释放 session.lock 之后再补一刀。
|
|
176
|
+
*
|
|
177
|
+
* @param {object[]} locked - 带 logFile 的被锁会话。
|
|
178
|
+
* @param {object} config - 解析后的配置。
|
|
179
|
+
* @param {object} ctx - 插件上下文(只用 logger)。
|
|
180
|
+
* @param {boolean} cancelled - 启动扫描是否已被取消。
|
|
181
|
+
* @param {() => boolean} isCancelled - 重新读取取消状态。
|
|
182
|
+
*/
|
|
183
|
+
function watchUnlocked(locked, config, ctx, cancelled, isCancelled) {
|
|
184
|
+
let remaining = [...locked]
|
|
185
|
+
let attempts = 0
|
|
186
|
+
const timer = setInterval(() => {
|
|
187
|
+
attempts += 1
|
|
188
|
+
if (cancelled === true || isCancelled() === true || attempts > LOCKED_RETRIES) {
|
|
189
|
+
clearInterval(timer)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
void (async () => {
|
|
193
|
+
const next = []
|
|
194
|
+
for (const item of remaining) {
|
|
195
|
+
if (isSessionLocked(item.logFile)) {
|
|
196
|
+
next.push(item)
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
await trimOne(item.logFile, config)
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (typeof ctx.logger?.warn === 'function') {
|
|
203
|
+
ctx.logger.warn(`[dsh-log-trim] 解锁后裁剪失败(${item.sessionId}):${error instanceof Error ? error.message : String(error)}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
remaining = next
|
|
208
|
+
if (remaining.length === 0) clearInterval(timer)
|
|
209
|
+
})()
|
|
210
|
+
}, LOCKED_RETRY_MS)
|
|
211
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
212
|
+
}
|
|
213
|
+
|
|
165
214
|
export async function trimOne(logFile, config) {
|
|
166
215
|
if (isSessionLocked(logFile)) {
|
|
167
216
|
throw new Error('这个会话正被宿主打开着(session.lock)。先切到别的对话或重启 DSH,再压缩它')
|
|
@@ -921,7 +970,15 @@ export async function runScan(config, logger = {}) {
|
|
|
921
970
|
summary.roots = roots
|
|
922
971
|
const legacy = roots.flatMap((root) => listLegacy(root))
|
|
923
972
|
summary.legacy = { files: legacy.length, bytes: legacy.reduce((sum, item) => sum + item.bytes, 0) }
|
|
924
|
-
|
|
973
|
+
// 最近改过的日志先扫:宿主启动后最先打开的正是它们,早一步就有机会在
|
|
974
|
+
// session.lock 出现之前把窗口裁掉。
|
|
975
|
+
const allLogs = roots.flatMap((root) => listLogs(root)).sort((left, right) => {
|
|
976
|
+
try {
|
|
977
|
+
return fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs
|
|
978
|
+
} catch {
|
|
979
|
+
return 0
|
|
980
|
+
}
|
|
981
|
+
})
|
|
925
982
|
for (const logFile of allLogs) {
|
|
926
983
|
// 每个会话之间让出一次事件循环:宿主还要应答界面和工具调用,
|
|
927
984
|
// 一轮扫描(162 个会话)不该把它按住。
|
|
@@ -929,7 +986,7 @@ export async function runScan(config, logger = {}) {
|
|
|
929
986
|
summary.scanned += 1
|
|
930
987
|
const sessionId = path.basename(path.dirname(logFile))
|
|
931
988
|
if (isSessionLocked(logFile)) {
|
|
932
|
-
summary.skipped.push({ sessionId, reason: '正被宿主打开(session.lock)' })
|
|
989
|
+
summary.skipped.push({ sessionId, logFile, reason: '正被宿主打开(session.lock)' })
|
|
933
990
|
continue
|
|
934
991
|
}
|
|
935
992
|
// A log the host cannot load is worse than a big log: the session vanishes
|
|
@@ -1374,7 +1431,11 @@ export function apply(ctx, config = {}) {
|
|
|
1374
1431
|
// 先等界面第一屏拿完数据再干活:开机那几秒是宿主最要紧的窗口。
|
|
1375
1432
|
await new Promise((resolve) => setTimeout(resolve, BOOT_SCAN_DELAY_MS))
|
|
1376
1433
|
if (cancelled) return
|
|
1377
|
-
await run()
|
|
1434
|
+
const summary = await run()
|
|
1435
|
+
if (!cancelled && summary && Array.isArray(summary.skipped)) {
|
|
1436
|
+
const locked = summary.skipped.filter((item) => typeof item?.logFile === 'string' && String(item.reason ?? '').includes('session.lock'))
|
|
1437
|
+
if (locked.length > 0) watchUnlocked(locked, resolved, ctx, cancelled, () => cancelled)
|
|
1438
|
+
}
|
|
1378
1439
|
} catch (error) {
|
|
1379
1440
|
if (!cancelled && typeof ctx.logger?.warn === 'function') {
|
|
1380
1441
|
ctx.logger.warn(`[dsh-log-trim] 扫描失败:${error instanceof Error ? error.message : String(error)}`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.ledger.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/ledger.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/ledger.js (v0.7.5, sha256 5ca2460b42a3a776)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.lossless.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/lossless.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/lossless.js (v0.7.5, sha256 7e8478a2fd2ec10b)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.roots.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/roots.js (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/roots.js (v0.7.5, sha256 028d9c12a395aaab)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.vendor.compact-session.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/vendor.compact-session.mjs (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/vendor.compact-session.mjs (v0.7.5, sha256 f0c5dcc018253ea4)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -2142,17 +2142,70 @@ export function repairLateToolResults(events) {
|
|
|
2142
2142
|
return { events: list, moved, dropped }
|
|
2143
2143
|
}
|
|
2144
2144
|
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* 被 checkpoint 的 `replace` 区间盖住的前缀:模型与界面都看不到,留着只是占空间,
|
|
2148
|
+
* 窗口页还会把它算成一窗。
|
|
2149
|
+
*
|
|
2150
|
+
* 不逐行删(会切坏 step/turn 括号),而是把保留区改成「连续尾巴 + frame」:
|
|
2151
|
+
* 从最后一个存活 checkpoint 的 replace 末尾之后、对齐到轮次起点开始保留,
|
|
2152
|
+
* 再加系统提示词与设置事件。这样结构始终是自洽的。
|
|
2153
|
+
*
|
|
2154
|
+
* @param {object[]} events - 候选保留事件(keepLastWindows 的结果)。
|
|
2155
|
+
* @returns {{ events: object[], dropped: number }} 结果。
|
|
2156
|
+
*/
|
|
2157
|
+
function dropShadowedPrefix(events) {
|
|
2158
|
+
const checkpoints = events.filter((event) => event.type === 'user/message' && event.data?.source?.kind === 'compact-checkpoint')
|
|
2159
|
+
const last = checkpoints[checkpoints.length - 1]
|
|
2160
|
+
const shadowEnd = last !== undefined && typeof last.surfaceOp?.endSeq === 'number' ? last.surfaceOp.endSeq : null
|
|
2161
|
+
if (shadowEnd === null) return { events, dropped: 0 }
|
|
2162
|
+
const aligned = events.find((event) => event.seq > shadowEnd && event.type === 'turn/start')
|
|
2163
|
+
if (aligned === undefined) return { events, dropped: 0 }
|
|
2164
|
+
const liveStart = aligned.seq
|
|
2165
|
+
const frame = new Set()
|
|
2166
|
+
const visible = foldSurface(events).visible
|
|
2167
|
+
const system = visible.map((seq) => events.find((event) => event.seq === seq)).find((event) => event !== undefined && (event.type === 'system/message' || event.type === 'developer/message'))
|
|
2168
|
+
if (system !== undefined) frame.add(system.seq)
|
|
2169
|
+
for (const type of ['agent-preset/selected', 'permission/preset', 'sandbox/mode', 'approval/policy', 'session/title', 'session/end-seed']) {
|
|
2170
|
+
const hit = events.filter((event) => event.type === type && event.seq < liveStart).pop()
|
|
2171
|
+
if (hit !== undefined) frame.add(hit.seq)
|
|
2172
|
+
}
|
|
2173
|
+
// 结构性事件不受“前缀”限制:compaction 组、seeded 标记、inbox 拼接都是按顺序
|
|
2174
|
+
// 重放的记账,丢掉半组会让整份日志装不上;它们不占模型视图,留着也便宜。
|
|
2175
|
+
const structural = (event) =>
|
|
2176
|
+
String(event.type).startsWith('compaction/') ||
|
|
2177
|
+
String(event.type).startsWith('session/') ||
|
|
2178
|
+
event.type === 'agent/inbox/spliced'
|
|
2179
|
+
const out = events.filter((event) => event.seq >= liveStart || frame.has(event.seq) || structural(event))
|
|
2180
|
+
// compaction 是一组(start / summary / end 同一个 compactionId):end 落在尾巴里而
|
|
2181
|
+
// start/summary 落在被丢的前缀里时,宿主要求「成功的 compaction/end 必须有一条 summary」。
|
|
2182
|
+
// 只要有同组的行留下,就把整组补回来(都是记账行,不影响模型视图)。
|
|
2183
|
+
const compactionIds = new Set()
|
|
2184
|
+
for (const event of out) {
|
|
2185
|
+
if (typeof event.data?.compactionId === 'string') compactionIds.add(event.data.compactionId)
|
|
2186
|
+
}
|
|
2187
|
+
if (compactionIds.size > 0) {
|
|
2188
|
+
for (const event of events) {
|
|
2189
|
+
if (typeof event.data?.compactionId === 'string' && compactionIds.has(event.data.compactionId) && !out.includes(event)) out.push(event)
|
|
2190
|
+
}
|
|
2191
|
+
out.sort((left, right) => left.seq - right.seq)
|
|
2192
|
+
}
|
|
2193
|
+
return { events: out, dropped: events.length - out.length }
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2145
2196
|
export async function stageTrim(logFile, keep, options = {}) {
|
|
2146
2197
|
if (isSessionLocked(logFile)) {
|
|
2147
2198
|
throw new Error('会话正被宿主持有(session.lock 被打开),拒绝改动;请先关掉该会话或重启宿主')
|
|
2148
2199
|
}
|
|
2149
2200
|
const original = await readLogAsync(logFile)
|
|
2150
2201
|
const trimmed = keepLastWindows(original.events, keep)
|
|
2151
|
-
if (trimmed.cutSeq === null) throw new Error(`只有 ${trimmed.windows} 个窗口,不需要裁`)
|
|
2152
2202
|
// 旧回合的流式回放记录:与 data.message.content 逐块重复,留最近几轮就够。
|
|
2153
2203
|
const streamKeepTurns = options.streamKeepTurns === undefined ? Infinity : options.streamKeepTurns
|
|
2154
|
-
|
|
2204
|
+
// 先丢掉被 checkpoint 遮蔽的前缀(连续尾巴 + frame),再清流式回放。
|
|
2205
|
+
const shadowed = options.shadowPrefix === false ? { events: trimmed.events, dropped: 0 } : dropShadowedPrefix(trimmed.events)
|
|
2206
|
+
const stripped = stripOldStreams(shadowed.events, streamKeepTurns)
|
|
2155
2207
|
assertNoGoalEventsDropped(original.events, stripped.events)
|
|
2208
|
+
if (trimmed.cutSeq === null && shadowed.dropped === 0) throw new Error(`只有 ${trimmed.windows} 个窗口,不需要裁`)
|
|
2156
2209
|
let renamed = renumber(stripped.events)
|
|
2157
2210
|
const repaired = repairLateToolResults(renamed.events)
|
|
2158
2211
|
if (repaired.moved > 0 || repaired.dropped > 0) {
|
|
@@ -2186,7 +2239,7 @@ export async function stageTrim(logFile, keep, options = {}) {
|
|
|
2186
2239
|
backup,
|
|
2187
2240
|
before: { events: original.events.length, bytes: original.compressedBytes, windows: trimmed.windows },
|
|
2188
2241
|
after: { events: renamed.events.length, bytes: (await fs.promises.stat(staging)).size },
|
|
2189
|
-
dropped: trimmed.dropped,
|
|
2242
|
+
dropped: trimmed.dropped + shadowed.dropped,
|
|
2190
2243
|
refsFixed: renamed.refsFixed,
|
|
2191
2244
|
titlesDropped: renamed.titlesDropped,
|
|
2192
2245
|
compactionDropped: renamed.compactionDropped,
|
|
@@ -2224,7 +2277,7 @@ export async function commitTrim(staged, logFile) {
|
|
|
2224
2277
|
* @returns {Promise<object>} 结果摘要。
|
|
2225
2278
|
*/
|
|
2226
2279
|
export async function applyTrim(logFile, keep, options = {}) {
|
|
2227
|
-
|
|
2280
|
+
let staged = await stageTrim(logFile, keep, options)
|
|
2228
2281
|
if (options.verify !== false) {
|
|
2229
2282
|
let result
|
|
2230
2283
|
try {
|
|
@@ -2236,6 +2289,20 @@ export async function applyTrim(logFile, keep, options = {}) {
|
|
|
2236
2289
|
}
|
|
2237
2290
|
if (!result.ok) {
|
|
2238
2291
|
fs.rmSync(staged.staging, { force: true })
|
|
2292
|
+
// 丢掉「被 checkpoint 遮蔽的前缀」是收益、不是前提:这份日志上它可能撞到
|
|
2293
|
+
// compaction / 标题 / inbox 的结构约束。退回不丢前缀的老做法再验一次,
|
|
2294
|
+
// 两次都不过才真的失败。
|
|
2295
|
+
if (options.shadowPrefix !== false) {
|
|
2296
|
+
staged = await stageTrim(logFile, keep, { ...options, shadowPrefix: false })
|
|
2297
|
+
const retried = await (async () => {
|
|
2298
|
+
const { verifyLog } = await import('./dsh-log-trim.verify-session-log.mjs')
|
|
2299
|
+
return verifyLog(staged.staging)
|
|
2300
|
+
})()
|
|
2301
|
+
if (retried.ok) return commitTrim(staged, logFile)
|
|
2302
|
+
fs.rmSync(staged.staging, { force: true })
|
|
2303
|
+
const bad = retried.checks.filter((check) => !check.ok).map((check) => `${check.name} ${check.detail}`).join(';')
|
|
2304
|
+
throw new Error(`裁剪结果没通过宿主校验,未写入:${bad}`)
|
|
2305
|
+
}
|
|
2239
2306
|
const bad = result.checks.filter((check) => !check.ok).map((check) => `${check.name} ${check.detail}`).join(';')
|
|
2240
2307
|
throw new Error(`裁剪结果没通过宿主校验,未写入:${bad}`)
|
|
2241
2308
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.verify-session-log.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/verify-session-log.mjs (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/verify-session-log.mjs (v0.7.5, sha256 7cf835c053942961)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-log-trim host half (dsh-log-trim.worker.mjs), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-log-trim/lib/worker.mjs (v0.7.
|
|
3
|
+
// Source: ../dsh-log-trim/lib/worker.mjs (v0.7.5, sha256 675a759a767b7e38)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|