dsh-retrace 0.4.11 → 0.4.13
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/bin/retrace.mjs +0 -0
- package/lib/badge.js +65 -0
- package/lib/client.bundle.js +58 -3
- package/lib/client.js +20 -1
- package/lib/dynamic-client.js +58 -3
- package/lib/dynamic-host.js +4 -0
- package/lib/host-core.js +4 -0
- package/lib/http.js +2 -2
- package/lib/index.js +12 -0
- package/lib/interrupt-guard.js +89 -0
- package/lib/prewrite-guard.js +61 -1
- package/package.json +10 -8
package/bin/retrace.mjs
CHANGED
|
File without changes
|
package/lib/badge.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-retrace — lib/badge.js
|
|
3
|
+
*
|
|
4
|
+
* 会话短码(identity badge)——2026-08-31 用户指定的小功能。
|
|
5
|
+
*
|
|
6
|
+
* 需求:与人交互时需要稳定的会话识别符(标题会变、session id 太长)。
|
|
7
|
+
* 解法:**从 session id(唯一值)确定性导出短码**——不依赖全量扫描、
|
|
8
|
+
* 不依赖外部短码表、插件运行时自算即可。同一 session id 永远得到同一短码。
|
|
9
|
+
*
|
|
10
|
+
* 与 `会话短码表.json`(工作区+序号语义格式 dx023dx001)的关系:
|
|
11
|
+
* - 外部表 = 人工维护的语义短码(线名/父子关系可读),由修复线维护;
|
|
12
|
+
* - 本模块 = 插件自算的确定性短码(人机交互兜底),任何环境可复现;
|
|
13
|
+
* - 两者并存:铭牌优先显示外部语义短码(若可查),否则用本模块兜底。
|
|
14
|
+
*
|
|
15
|
+
* 算法:FNV-1a 64-bit(uuid 段)→ base36 → 固定 10 位(补 0)。
|
|
16
|
+
* - 10 位纯字母数字(36^10 ≈ 3.6e15 空间,会话规模下碰撞可忽略);
|
|
17
|
+
* - 只依赖 id 本身,无状态、无 IO、纯函数(可测试、可在 client/host 复用)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 提取 id 中的 uuid 段(hex),无匹配则退回 hex 清洗。 */
|
|
21
|
+
export function uuidOf(id) {
|
|
22
|
+
const text = String(id ?? '')
|
|
23
|
+
const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)
|
|
24
|
+
if (m) return m[0].replace(/-/g, '').toLowerCase()
|
|
25
|
+
const cleaned = text.replace(/[^0-9a-f]/gi, '').toLowerCase()
|
|
26
|
+
return cleaned.length >= 8 ? cleaned.slice(0, 32) : ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** FNV-1a 64-bit(BigInt,避免精度丢失)。 */
|
|
30
|
+
export function fnv1a64(text) {
|
|
31
|
+
let hash = 0xcbf29ce484222325n
|
|
32
|
+
const prime = 0x100000001b3n
|
|
33
|
+
const mask = 0xffffffffffffffffn
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
hash ^= BigInt(text.charCodeAt(i))
|
|
36
|
+
hash = (hash * prime) & mask
|
|
37
|
+
}
|
|
38
|
+
return hash
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
42
|
+
|
|
43
|
+
/** BigInt → base36 字符串。 */
|
|
44
|
+
export function toBase36(value) {
|
|
45
|
+
if (value < 0n) value = -value
|
|
46
|
+
if (value === 0n) return '0'
|
|
47
|
+
let out = ''
|
|
48
|
+
let v = value
|
|
49
|
+
while (v > 0n) {
|
|
50
|
+
out = BASE36[Number(v % 36n)] + out
|
|
51
|
+
v /= 36n
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 会话短码:uuid 段 → FNV-1a 64 → base36 → 固定 10 位(补 0 截断)。
|
|
58
|
+
* @param {string} sessionId - 会话 id(`session-<uuid>` 或裸 uuid)。
|
|
59
|
+
* @returns {string} 10 位短码;无法提取 uuid 时返回空串(调用方自行兜底)。
|
|
60
|
+
*/
|
|
61
|
+
export function sessionBadge(sessionId) {
|
|
62
|
+
const uuid = uuidOf(sessionId)
|
|
63
|
+
if (uuid.length === 0) return ''
|
|
64
|
+
return toBase36(fnv1a64(uuid)).padStart(10, '0').slice(0, 10)
|
|
65
|
+
}
|
package/lib/client.bundle.js
CHANGED
|
@@ -32,6 +32,44 @@ __export(client_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(client_exports);
|
|
34
34
|
var import_react = require("react");
|
|
35
|
+
|
|
36
|
+
// lib/badge.js
|
|
37
|
+
function uuidOf(id) {
|
|
38
|
+
const text = String(id ?? "");
|
|
39
|
+
const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
40
|
+
if (m) return m[0].replace(/-/g, "").toLowerCase();
|
|
41
|
+
const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
|
|
42
|
+
return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
|
|
43
|
+
}
|
|
44
|
+
function fnv1a64(text) {
|
|
45
|
+
let hash = 0xcbf29ce484222325n;
|
|
46
|
+
const prime = 0x100000001b3n;
|
|
47
|
+
const mask = 0xffffffffffffffffn;
|
|
48
|
+
for (let i = 0; i < text.length; i++) {
|
|
49
|
+
hash ^= BigInt(text.charCodeAt(i));
|
|
50
|
+
hash = hash * prime & mask;
|
|
51
|
+
}
|
|
52
|
+
return hash;
|
|
53
|
+
}
|
|
54
|
+
var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
55
|
+
function toBase36(value) {
|
|
56
|
+
if (value < 0n) value = -value;
|
|
57
|
+
if (value === 0n) return "0";
|
|
58
|
+
let out = "";
|
|
59
|
+
let v = value;
|
|
60
|
+
while (v > 0n) {
|
|
61
|
+
out = BASE36[Number(v % 36n)] + out;
|
|
62
|
+
v /= 36n;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function sessionBadge(sessionId) {
|
|
67
|
+
const uuid = uuidOf(sessionId);
|
|
68
|
+
if (uuid.length === 0) return "";
|
|
69
|
+
return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// lib/client.js
|
|
35
73
|
var name = "dsh-retrace";
|
|
36
74
|
var inject = ["slots", "locale", "conversationEvents"];
|
|
37
75
|
var NS = "retrace";
|
|
@@ -139,7 +177,9 @@ var zh = {
|
|
|
139
177
|
"fork.lineageThis": "\u5F53\u524D\u4F1A\u8BDD",
|
|
140
178
|
"fork.lineageParent": "\u7236\u4F1A\u8BDD",
|
|
141
179
|
"fork.lineageRoot": "\u6839\u4F1A\u8BDD",
|
|
142
|
-
"fork.lineageEmpty": "\u672C\u4F1A\u8BDD\u6CA1\u6709\u7236\u4F1A\u8BDD\uFF08\u72EC\u7ACB\u6839\u4F1A\u8BDD\uFF09\u3002"
|
|
180
|
+
"fork.lineageEmpty": "\u672C\u4F1A\u8BDD\u6CA1\u6709\u7236\u4F1A\u8BDD\uFF08\u72EC\u7ACB\u6839\u4F1A\u8BDD\uFF09\u3002",
|
|
181
|
+
"fork.badge": "\u4F1A\u8BDD\u77ED\u7801",
|
|
182
|
+
"fork.badgeHint": "\u77ED\u7801\u7531\u4F1A\u8BDD\u552F\u4E00 id \u786E\u5B9A\u6027\u5BFC\u51FA\uFF0C\u6C38\u4E0D\u53D8\uFF08\u6807\u9898\u4F1A\u53D8\uFF09\u3002"
|
|
143
183
|
};
|
|
144
184
|
var en = {
|
|
145
185
|
"action.edit": "Edit",
|
|
@@ -233,7 +273,9 @@ var en = {
|
|
|
233
273
|
"fork.lineageThis": "This session",
|
|
234
274
|
"fork.lineageParent": "Parent session",
|
|
235
275
|
"fork.lineageRoot": "Root session",
|
|
236
|
-
"fork.lineageEmpty": "This session has no parent (standalone root)."
|
|
276
|
+
"fork.lineageEmpty": "This session has no parent (standalone root).",
|
|
277
|
+
"fork.badge": "Session badge",
|
|
278
|
+
"fork.badgeHint": "A stable shortcode derived from the session id (titles change, badges never do)."
|
|
237
279
|
};
|
|
238
280
|
var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
|
|
239
281
|
function isReplacementSurfaceEvent(event) {
|
|
@@ -415,7 +457,9 @@ var recallMarkerDefinition = {
|
|
|
415
457
|
// the action-row suppression check (useShadowed) but empty for legacy
|
|
416
458
|
// so no row is hidden and no action row is suppressed for legacy
|
|
417
459
|
// markers (rename must never make visible content disappear).
|
|
418
|
-
|
|
460
|
+
// 2026-09-01:优先读 data.shadowedSeqs(host 冗余写入),fallback 顶层
|
|
461
|
+
// sourceEventSeqs——部分客户端事件管道剥离顶层字段导致隐藏失效。
|
|
462
|
+
shadowedSeqs: legacy ? [] : Array.isArray(event.data?.shadowedSeqs) ? event.data.shadowedSeqs.slice() : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
|
|
419
463
|
targetSeq: event.data?.editor?.targetSeq,
|
|
420
464
|
text: event.data?.editor?.text
|
|
421
465
|
};
|
|
@@ -931,6 +975,10 @@ var CSS = `
|
|
|
931
975
|
.dsh-rt-fork-lineage-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
932
976
|
.dsh-rt-fork-lineage-arrow{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);flex:none}
|
|
933
977
|
.dsh-rt-fork-lineage-empty{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;flex:none}
|
|
978
|
+
.dsh-rt-fork-badge{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated);border-radius:8px;padding:6px 8px;flex:none;min-width:0}
|
|
979
|
+
.dsh-rt-fork-badge-tag{font-size:11px;font-weight:500;line-height:16px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
980
|
+
.dsh-rt-fork-badge-code{font-family:var(--dsw-font-mono);font-size:12px;font-weight:600;line-height:16px;color:var(--dsw-alias-state-info-primary);flex:none}
|
|
981
|
+
.dsh-rt-fork-badge-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
934
982
|
`;
|
|
935
983
|
var JUMP_PAGE_BUDGET = 24;
|
|
936
984
|
function switchToViewTab(viewId) {
|
|
@@ -1316,6 +1364,12 @@ function ForkView({ sessionId, useProjection, t, actions, store }) {
|
|
|
1316
1364
|
onClick: refresh
|
|
1317
1365
|
}, t("fork.refresh"))
|
|
1318
1366
|
]),
|
|
1367
|
+
// 会话铭牌(2026-08-31):短码 = 从 session id 确定性导出,永不变(标题会变)。
|
|
1368
|
+
sessionId && (0, import_react.createElement)("div", { key: "badge", className: "dsh-rt-fork-badge", title: t("fork.badgeHint") }, [
|
|
1369
|
+
(0, import_react.createElement)("span", { key: "tag", className: "dsh-rt-fork-badge-tag" }, t("fork.badge")),
|
|
1370
|
+
(0, import_react.createElement)("code", { key: "code", className: "dsh-rt-fork-badge-code" }, `[${sessionBadge(sessionId)}]`),
|
|
1371
|
+
(0, import_react.createElement)("span", { key: "id", className: "dsh-rt-fork-badge-id" }, sessionId)
|
|
1372
|
+
]),
|
|
1319
1373
|
Array.isArray(lineage) && lineage.length > 1 && (0, import_react.createElement)("div", { key: "lineage", className: "dsh-rt-fork-lineage" }, [
|
|
1320
1374
|
(0, import_react.createElement)("div", { key: "lt", className: "dsh-rt-fork-spine-label" }, t("fork.lineage")),
|
|
1321
1375
|
lineage.map((hop, i) => (0, import_react.createElement)("div", {
|
|
@@ -1327,6 +1381,7 @@ function ForkView({ sessionId, useProjection, t, actions, store }) {
|
|
|
1327
1381
|
{ key: "tag", className: "dsh-rt-fork-lineage-tag" },
|
|
1328
1382
|
i === 0 ? t("fork.lineageThis") : i === lineage.length - 1 ? t("fork.lineageRoot") : t("fork.lineageParent")
|
|
1329
1383
|
),
|
|
1384
|
+
(0, import_react.createElement)("code", { key: "badge", className: "dsh-rt-fork-badge-code" }, `[${sessionBadge(hop.id)}]`),
|
|
1330
1385
|
(0, import_react.createElement)("span", { key: "id", className: "dsh-rt-fork-lineage-id" }, hop.id),
|
|
1331
1386
|
i < lineage.length - 1 && (0, import_react.createElement)("span", { key: "arrow", className: "dsh-rt-fork-lineage-arrow" }, "\u2190")
|
|
1332
1387
|
]))
|
package/lib/client.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* `/api/plugins/retrace/*` registered by the Host half.
|
|
17
17
|
*/
|
|
18
18
|
import { createElement, useEffect, useState } from 'react'
|
|
19
|
+
import { sessionBadge } from './badge.js'
|
|
19
20
|
|
|
20
21
|
export const name = 'dsh-retrace'
|
|
21
22
|
export const inject = ['slots', 'locale', 'conversationEvents']
|
|
@@ -148,6 +149,8 @@ const zh = {
|
|
|
148
149
|
'fork.lineageParent': '父会话',
|
|
149
150
|
'fork.lineageRoot': '根会话',
|
|
150
151
|
'fork.lineageEmpty': '本会话没有父会话(独立根会话)。',
|
|
152
|
+
'fork.badge': '会话短码',
|
|
153
|
+
'fork.badgeHint': '短码由会话唯一 id 确定性导出,永不变(标题会变)。',
|
|
151
154
|
}
|
|
152
155
|
/** English dictionary, checked complete against the zh key set. */
|
|
153
156
|
const en = {
|
|
@@ -243,6 +246,8 @@ const en = {
|
|
|
243
246
|
'fork.lineageParent': 'Parent session',
|
|
244
247
|
'fork.lineageRoot': 'Root session',
|
|
245
248
|
'fork.lineageEmpty': 'This session has no parent (standalone root).',
|
|
249
|
+
'fork.badge': 'Session badge',
|
|
250
|
+
'fork.badgeHint': 'A stable shortcode derived from the session id (titles change, badges never do).',
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
// ---------------------------------------------------------------------------
|
|
@@ -503,7 +508,9 @@ const recallMarkerDefinition = {
|
|
|
503
508
|
// the action-row suppression check (useShadowed) but empty for legacy
|
|
504
509
|
// so no row is hidden and no action row is suppressed for legacy
|
|
505
510
|
// markers (rename must never make visible content disappear).
|
|
506
|
-
|
|
511
|
+
// 2026-09-01:优先读 data.shadowedSeqs(host 冗余写入),fallback 顶层
|
|
512
|
+
// sourceEventSeqs——部分客户端事件管道剥离顶层字段导致隐藏失效。
|
|
513
|
+
shadowedSeqs: legacy ? [] : (Array.isArray(event.data?.shadowedSeqs) ? event.data.shadowedSeqs.slice() : (Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [])),
|
|
507
514
|
targetSeq: event.data?.editor?.targetSeq,
|
|
508
515
|
text: event.data?.editor?.text,
|
|
509
516
|
}
|
|
@@ -1164,6 +1171,10 @@ const CSS = `
|
|
|
1164
1171
|
.dsh-rt-fork-lineage-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1165
1172
|
.dsh-rt-fork-lineage-arrow{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);flex:none}
|
|
1166
1173
|
.dsh-rt-fork-lineage-empty{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;flex:none}
|
|
1174
|
+
.dsh-rt-fork-badge{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated);border-radius:8px;padding:6px 8px;flex:none;min-width:0}
|
|
1175
|
+
.dsh-rt-fork-badge-tag{font-size:11px;font-weight:500;line-height:16px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
1176
|
+
.dsh-rt-fork-badge-code{font-family:var(--dsw-font-mono);font-size:12px;font-weight:600;line-height:16px;color:var(--dsw-alias-state-info-primary);flex:none}
|
|
1177
|
+
.dsh-rt-fork-badge-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1167
1178
|
`
|
|
1168
1179
|
|
|
1169
1180
|
const JUMP_PAGE_BUDGET = 24
|
|
@@ -1726,6 +1737,13 @@ function ForkView({ sessionId, useProjection, t, actions, store }) {
|
|
|
1726
1737
|
}, t('fork.refresh')),
|
|
1727
1738
|
]),
|
|
1728
1739
|
|
|
1740
|
+
// 会话铭牌(2026-08-31):短码 = 从 session id 确定性导出,永不变(标题会变)。
|
|
1741
|
+
sessionId && createElement('div', { key: 'badge', className: 'dsh-rt-fork-badge', title: t('fork.badgeHint') }, [
|
|
1742
|
+
createElement('span', { key: 'tag', className: 'dsh-rt-fork-badge-tag' }, t('fork.badge')),
|
|
1743
|
+
createElement('code', { key: 'code', className: 'dsh-rt-fork-badge-code' }, `[${sessionBadge(sessionId)}]`),
|
|
1744
|
+
createElement('span', { key: 'id', className: 'dsh-rt-fork-badge-id' }, sessionId),
|
|
1745
|
+
]),
|
|
1746
|
+
|
|
1729
1747
|
Array.isArray(lineage) && lineage.length > 1 && createElement('div', { key: 'lineage', className: 'dsh-rt-fork-lineage' }, [
|
|
1730
1748
|
createElement('div', { key: 'lt', className: 'dsh-rt-fork-spine-label' }, t('fork.lineage')),
|
|
1731
1749
|
lineage.map((hop, i) => createElement('div', {
|
|
@@ -1734,6 +1752,7 @@ function ForkView({ sessionId, useProjection, t, actions, store }) {
|
|
|
1734
1752
|
}, [
|
|
1735
1753
|
createElement('span', { key: 'tag', className: 'dsh-rt-fork-lineage-tag' },
|
|
1736
1754
|
i === 0 ? t('fork.lineageThis') : (i === lineage.length - 1 ? t('fork.lineageRoot') : t('fork.lineageParent'))),
|
|
1755
|
+
createElement('code', { key: 'badge', className: 'dsh-rt-fork-badge-code' }, `[${sessionBadge(hop.id)}]`),
|
|
1737
1756
|
createElement('span', { key: 'id', className: 'dsh-rt-fork-lineage-id' }, hop.id),
|
|
1738
1757
|
i < lineage.length - 1 && createElement('span', { key: 'arrow', className: 'dsh-rt-fork-lineage-arrow' }, '←'),
|
|
1739
1758
|
])),
|
package/lib/dynamic-client.js
CHANGED
|
@@ -38,6 +38,44 @@ return {
|
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(client_exports);
|
|
40
40
|
var import_react = require("react");
|
|
41
|
+
|
|
42
|
+
// lib/badge.js
|
|
43
|
+
function uuidOf(id) {
|
|
44
|
+
const text = String(id ?? "");
|
|
45
|
+
const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
46
|
+
if (m) return m[0].replace(/-/g, "").toLowerCase();
|
|
47
|
+
const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
|
|
48
|
+
return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
|
|
49
|
+
}
|
|
50
|
+
function fnv1a64(text) {
|
|
51
|
+
let hash = 0xcbf29ce484222325n;
|
|
52
|
+
const prime = 0x100000001b3n;
|
|
53
|
+
const mask = 0xffffffffffffffffn;
|
|
54
|
+
for (let i = 0; i < text.length; i++) {
|
|
55
|
+
hash ^= BigInt(text.charCodeAt(i));
|
|
56
|
+
hash = hash * prime & mask;
|
|
57
|
+
}
|
|
58
|
+
return hash;
|
|
59
|
+
}
|
|
60
|
+
var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
61
|
+
function toBase36(value) {
|
|
62
|
+
if (value < 0n) value = -value;
|
|
63
|
+
if (value === 0n) return "0";
|
|
64
|
+
let out = "";
|
|
65
|
+
let v = value;
|
|
66
|
+
while (v > 0n) {
|
|
67
|
+
out = BASE36[Number(v % 36n)] + out;
|
|
68
|
+
v /= 36n;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function sessionBadge(sessionId) {
|
|
73
|
+
const uuid = uuidOf(sessionId);
|
|
74
|
+
if (uuid.length === 0) return "";
|
|
75
|
+
return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// lib/client.js
|
|
41
79
|
var name = "dsh-retrace";
|
|
42
80
|
var inject = ["slots", "locale", "conversationEvents"];
|
|
43
81
|
var NS = "retrace";
|
|
@@ -145,7 +183,9 @@ return {
|
|
|
145
183
|
"fork.lineageThis": "\u5F53\u524D\u4F1A\u8BDD",
|
|
146
184
|
"fork.lineageParent": "\u7236\u4F1A\u8BDD",
|
|
147
185
|
"fork.lineageRoot": "\u6839\u4F1A\u8BDD",
|
|
148
|
-
"fork.lineageEmpty": "\u672C\u4F1A\u8BDD\u6CA1\u6709\u7236\u4F1A\u8BDD\uFF08\u72EC\u7ACB\u6839\u4F1A\u8BDD\uFF09\u3002"
|
|
186
|
+
"fork.lineageEmpty": "\u672C\u4F1A\u8BDD\u6CA1\u6709\u7236\u4F1A\u8BDD\uFF08\u72EC\u7ACB\u6839\u4F1A\u8BDD\uFF09\u3002",
|
|
187
|
+
"fork.badge": "\u4F1A\u8BDD\u77ED\u7801",
|
|
188
|
+
"fork.badgeHint": "\u77ED\u7801\u7531\u4F1A\u8BDD\u552F\u4E00 id \u786E\u5B9A\u6027\u5BFC\u51FA\uFF0C\u6C38\u4E0D\u53D8\uFF08\u6807\u9898\u4F1A\u53D8\uFF09\u3002"
|
|
149
189
|
};
|
|
150
190
|
var en = {
|
|
151
191
|
"action.edit": "Edit",
|
|
@@ -239,7 +279,9 @@ return {
|
|
|
239
279
|
"fork.lineageThis": "This session",
|
|
240
280
|
"fork.lineageParent": "Parent session",
|
|
241
281
|
"fork.lineageRoot": "Root session",
|
|
242
|
-
"fork.lineageEmpty": "This session has no parent (standalone root)."
|
|
282
|
+
"fork.lineageEmpty": "This session has no parent (standalone root).",
|
|
283
|
+
"fork.badge": "Session badge",
|
|
284
|
+
"fork.badgeHint": "A stable shortcode derived from the session id (titles change, badges never do)."
|
|
243
285
|
};
|
|
244
286
|
var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
|
|
245
287
|
function isReplacementSurfaceEvent(event) {
|
|
@@ -421,7 +463,9 @@ return {
|
|
|
421
463
|
// the action-row suppression check (useShadowed) but empty for legacy
|
|
422
464
|
// so no row is hidden and no action row is suppressed for legacy
|
|
423
465
|
// markers (rename must never make visible content disappear).
|
|
424
|
-
|
|
466
|
+
// 2026-09-01:优先读 data.shadowedSeqs(host 冗余写入),fallback 顶层
|
|
467
|
+
// sourceEventSeqs——部分客户端事件管道剥离顶层字段导致隐藏失效。
|
|
468
|
+
shadowedSeqs: legacy ? [] : Array.isArray(event.data?.shadowedSeqs) ? event.data.shadowedSeqs.slice() : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
|
|
425
469
|
targetSeq: event.data?.editor?.targetSeq,
|
|
426
470
|
text: event.data?.editor?.text
|
|
427
471
|
};
|
|
@@ -937,6 +981,10 @@ return {
|
|
|
937
981
|
.dsh-rt-fork-lineage-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
938
982
|
.dsh-rt-fork-lineage-arrow{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);flex:none}
|
|
939
983
|
.dsh-rt-fork-lineage-empty{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;flex:none}
|
|
984
|
+
.dsh-rt-fork-badge{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated);border-radius:8px;padding:6px 8px;flex:none;min-width:0}
|
|
985
|
+
.dsh-rt-fork-badge-tag{font-size:11px;font-weight:500;line-height:16px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
986
|
+
.dsh-rt-fork-badge-code{font-family:var(--dsw-font-mono);font-size:12px;font-weight:600;line-height:16px;color:var(--dsw-alias-state-info-primary);flex:none}
|
|
987
|
+
.dsh-rt-fork-badge-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
940
988
|
`;
|
|
941
989
|
var JUMP_PAGE_BUDGET = 24;
|
|
942
990
|
function switchToViewTab(viewId) {
|
|
@@ -1322,6 +1370,12 @@ return {
|
|
|
1322
1370
|
onClick: refresh
|
|
1323
1371
|
}, t("fork.refresh"))
|
|
1324
1372
|
]),
|
|
1373
|
+
// 会话铭牌(2026-08-31):短码 = 从 session id 确定性导出,永不变(标题会变)。
|
|
1374
|
+
sessionId && (0, import_react.createElement)("div", { key: "badge", className: "dsh-rt-fork-badge", title: t("fork.badgeHint") }, [
|
|
1375
|
+
(0, import_react.createElement)("span", { key: "tag", className: "dsh-rt-fork-badge-tag" }, t("fork.badge")),
|
|
1376
|
+
(0, import_react.createElement)("code", { key: "code", className: "dsh-rt-fork-badge-code" }, `[${sessionBadge(sessionId)}]`),
|
|
1377
|
+
(0, import_react.createElement)("span", { key: "id", className: "dsh-rt-fork-badge-id" }, sessionId)
|
|
1378
|
+
]),
|
|
1325
1379
|
Array.isArray(lineage) && lineage.length > 1 && (0, import_react.createElement)("div", { key: "lineage", className: "dsh-rt-fork-lineage" }, [
|
|
1326
1380
|
(0, import_react.createElement)("div", { key: "lt", className: "dsh-rt-fork-spine-label" }, t("fork.lineage")),
|
|
1327
1381
|
lineage.map((hop, i) => (0, import_react.createElement)("div", {
|
|
@@ -1333,6 +1387,7 @@ return {
|
|
|
1333
1387
|
{ key: "tag", className: "dsh-rt-fork-lineage-tag" },
|
|
1334
1388
|
i === 0 ? t("fork.lineageThis") : i === lineage.length - 1 ? t("fork.lineageRoot") : t("fork.lineageParent")
|
|
1335
1389
|
),
|
|
1390
|
+
(0, import_react.createElement)("code", { key: "badge", className: "dsh-rt-fork-badge-code" }, `[${sessionBadge(hop.id)}]`),
|
|
1336
1391
|
(0, import_react.createElement)("span", { key: "id", className: "dsh-rt-fork-lineage-id" }, hop.id),
|
|
1337
1392
|
i < lineage.length - 1 && (0, import_react.createElement)("span", { key: "arrow", className: "dsh-rt-fork-lineage-arrow" }, "\u2190")
|
|
1338
1393
|
]))
|
package/lib/dynamic-host.js
CHANGED
|
@@ -142,6 +142,10 @@ return {
|
|
|
142
142
|
}
|
|
143
143
|
const surfaceOp = { op: 'replace', start: span.start, end: span.end }
|
|
144
144
|
const sourceEventSeqs = span.shadowedSeqs.slice()
|
|
145
|
+
// 2026-09-01:把遮蔽 seq 冗余进 data.shadowedSeqs——部分客户端事件管道会剥离
|
|
146
|
+
// event 顶层 sourceEventSeqs(0.4.12 实测:撤回/编辑后消息不隐藏 = client 拿不到
|
|
147
|
+
// shadowedSeqs)。data 里的字段随事件体持久化,client 读 data 更稳(fallback 顶层)。
|
|
148
|
+
data.shadowedSeqs = sourceEventSeqs
|
|
145
149
|
if (typeof validate === 'function') {
|
|
146
150
|
const result = await validate(session, { type: 'assistant/message', data, surfaceOp, sourceEventSeqs })
|
|
147
151
|
if (result && result.t1Ok === false) {
|
package/lib/host-core.js
CHANGED
|
@@ -133,6 +133,10 @@ export async function appendEditorMarker(session, span, op, targetSeq, originalT
|
|
|
133
133
|
}
|
|
134
134
|
const surfaceOp = { op: 'replace', start: span.start, end: span.end }
|
|
135
135
|
const sourceEventSeqs = span.shadowedSeqs.slice()
|
|
136
|
+
// 2026-09-01:把遮蔽 seq 冗余进 data.shadowedSeqs——部分客户端事件管道会剥离
|
|
137
|
+
// event 顶层 sourceEventSeqs(0.4.12 实测:撤回/编辑后消息不隐藏 = client 拿不到
|
|
138
|
+
// shadowedSeqs)。data 里的字段随事件体持久化,client 读 data 更稳(fallback 顶层)。
|
|
139
|
+
data.shadowedSeqs = sourceEventSeqs
|
|
136
140
|
if (typeof validate === 'function') {
|
|
137
141
|
const result = await validate(session, { type: 'assistant/message', data, surfaceOp, sourceEventSeqs })
|
|
138
142
|
if (result && result.t1Ok === false) {
|
package/lib/http.js
CHANGED
|
@@ -81,8 +81,8 @@ function sendError(res, error) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** Route one request. `seam` is the versioning seam (lib/versioning.js). */
|
|
84
|
-
export function createRetraceHttpHandler(ctx, { sessions, agents, seam, rollback, log = () => {} }) {
|
|
85
|
-
const api = createEditorApi(ctx, sessions, agents, log)
|
|
84
|
+
export function createRetraceHttpHandler(ctx, { sessions, agents, seam, rollback, hooks = {}, log = () => {} }) {
|
|
85
|
+
const api = createEditorApi(ctx, sessions, agents, log, hooks)
|
|
86
86
|
|
|
87
87
|
function handleVersions(req, res, sessionId, config) {
|
|
88
88
|
seam.setConfig(sessionId, config)
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,8 @@ import { createVersioningSeam } from './versioning.js'
|
|
|
25
25
|
import { createRollbackExecutor } from './rollback.js'
|
|
26
26
|
import { createMarkerGuard } from './prewrite-guard.js'
|
|
27
27
|
import { createWatchdog } from './watchdog.js'
|
|
28
|
+
import { attachExitWarning } from './interrupt-guard.js'
|
|
29
|
+
import { sessionBadge } from './badge.js'
|
|
28
30
|
|
|
29
31
|
export const name = 'dsh-retrace'
|
|
30
32
|
export const inject = ['sessions', 'agents', 'webServer', 'fs', 'subprocess', 'sandboxPolicy']
|
|
@@ -52,6 +54,7 @@ export function apply(ctx) {
|
|
|
52
54
|
agents: ctx.agents,
|
|
53
55
|
seam,
|
|
54
56
|
rollback,
|
|
57
|
+
hooks,
|
|
55
58
|
log,
|
|
56
59
|
})
|
|
57
60
|
|
|
@@ -78,6 +81,11 @@ export function apply(ctx) {
|
|
|
78
81
|
harness.handle('retrace.recall', (args) => api.recall(args)),
|
|
79
82
|
harness.handle('retrace.editAndResend', (args) => api.editAndResend(args)),
|
|
80
83
|
harness.handle('retrace.regenerate', (args) => api.regenerate(args)),
|
|
84
|
+
// 会话短码(2026-08-31):session id → 确定性 10 位短码,人机交互识别用。
|
|
85
|
+
harness.handle('retrace.sessionBadge', (args) => {
|
|
86
|
+
const sessionId = String(args?.sessionId ?? '')
|
|
87
|
+
return { ok: true, value: { sessionId, badge: sessionBadge(sessionId) } }
|
|
88
|
+
}),
|
|
81
89
|
]
|
|
82
90
|
return () => disposers.forEach((dispose) => dispose())
|
|
83
91
|
})()
|
|
@@ -85,9 +93,13 @@ export function apply(ctx) {
|
|
|
85
93
|
// R1 实时看门狗:轮询文件尾部 seq vs 内存长度,捕获并发写入/旧光标回放现场。
|
|
86
94
|
const watchdog = createWatchdog(ctx, log)
|
|
87
95
|
|
|
96
|
+
// R4 中断轮次治理:退出/重载时对未闭合 turn 会话提示(只检测不写事件)。
|
|
97
|
+
const warnUnclosed = attachExitWarning(ctx, log)
|
|
98
|
+
|
|
88
99
|
ctx.effect(() => () => {
|
|
89
100
|
disposeRoute()
|
|
90
101
|
disposeHarness()
|
|
91
102
|
watchdog.dispose()
|
|
103
|
+
warnUnclosed()
|
|
92
104
|
}, 'dsh-retrace: transports')
|
|
93
105
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-retrace · lib/interrupt-guard.js
|
|
3
|
+
*
|
|
4
|
+
* R4 中断轮次治理(2026-08-30 任务书 §4)。
|
|
5
|
+
*
|
|
6
|
+
* 目标:退出/关闭会话前对「未闭合 turn」的会话提示用户,避免误以为任务
|
|
7
|
+
* 已干净结束。
|
|
8
|
+
*
|
|
9
|
+
* 设计(按实现方案 §R4,铁律「先核实官方 turn 关闭路径」):
|
|
10
|
+
* - 官方 `dsh-agent-loop/lib/index.js:620` 在 finally 块保证 `turn/end`
|
|
11
|
+
* 必然写入(含 reason.kind='error' 的中断)——正常退出/中断路径不会
|
|
12
|
+
* 留下未闭合 turn;
|
|
13
|
+
* - 未闭合 turn 只出现在「崩溃 / 强杀 / 断电」场景:有 turn/start 无
|
|
14
|
+
* turn/end(interrupted/aborted 是官方正常闭合,不在此列);
|
|
15
|
+
* - 因此本守卫**只检测 + 提示,绝不自动写 turn/end**(与官方持久化竞态
|
|
16
|
+
* 会引入双写;且插件不应在运行期补写持久化事件——铁律「插件内不要
|
|
17
|
+
* 删除/重编号持久化事件」的同类约束)。
|
|
18
|
+
*
|
|
19
|
+
* 检测函数:
|
|
20
|
+
* unclosedTurns(session) → { turn, state: 'open' }[]
|
|
21
|
+
* 扫描 session.events:turn/start 开、turn/end 关;尾部仍开 = 未闭合。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 扫描会话,找未闭合的轮次。
|
|
26
|
+
* @param {object} session - DSH session 对象(session.events 为事件数组)。
|
|
27
|
+
* @returns {Array<{turn: number, state: 'open'}>}
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ 判定(2026-08-31 独立审查修正):只报**真正未闭合**的轮次——有
|
|
30
|
+
* turn/start 且尾部没有对应 turn/end(崩溃/强杀现场)。
|
|
31
|
+
* turn/end 的 reason.kind 为 'interrupted'/'aborted' 是官方**正常闭合**
|
|
32
|
+
* (用户主动中断,agent-loop finally 保证写入),archive 实测 69 会话中
|
|
33
|
+
* 666 aborted + 126 interrupted 全是正常闭合、真 open 只 10 个——把它们
|
|
34
|
+
* 当"未闭合"会在每次退出对 91% 会话打误导性 warning。
|
|
35
|
+
*/
|
|
36
|
+
export function unclosedTurns(session) {
|
|
37
|
+
const events = Array.isArray(session?.events) ? session.events : []
|
|
38
|
+
const found = []
|
|
39
|
+
let openTurn = null
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
if (!event || typeof event !== 'object') continue
|
|
42
|
+
if (event.type === 'turn/start') {
|
|
43
|
+
openTurn = event.data?.turn
|
|
44
|
+
} else if (event.type === 'turn/end') {
|
|
45
|
+
openTurn = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 尾部仍有未闭合 turn(崩溃/强杀现场)
|
|
49
|
+
if (openTurn !== null) {
|
|
50
|
+
found.push({ turn: openTurn, state: 'open' })
|
|
51
|
+
}
|
|
52
|
+
return found
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 挂载退出前提示:在插件 dispose 时对「当前 attach 且有未闭合轮次」的会话
|
|
57
|
+
* 记录一条 warning(非阻断)。调用方(apply 的 ctx.effect 清理)负责在
|
|
58
|
+
* 退出/重载时触发。
|
|
59
|
+
*
|
|
60
|
+
* 不做客户端弹窗(非阻断提示由宿主 UI 在会话内呈现——本插件只负责检测与
|
|
61
|
+
* 日志;客户端时间线渲染未闭合轮次的提示属 UI 层,后续可扩展)。
|
|
62
|
+
* @param {object} ctx - cordis ctx。
|
|
63
|
+
* @param {(line: string) => void} log - 日志函数。
|
|
64
|
+
* @param {() => Array<object>} [sessionList] - 活跃会话列表(默认 ctx.sessions)。
|
|
65
|
+
*/
|
|
66
|
+
export function attachExitWarning(ctx, log, sessionList) {
|
|
67
|
+
return () => {
|
|
68
|
+
const sessions = typeof sessionList === 'function' ? sessionList() : ctx?.sessions
|
|
69
|
+
if (!sessions || typeof sessions !== 'object') return
|
|
70
|
+
const ids = typeof sessions.keys === 'function' ? [...sessions.keys()] : Object.keys(sessions)
|
|
71
|
+
for (const id of ids) {
|
|
72
|
+
const session = typeof sessions.get === 'function' ? sessions.get(id) : sessions[id]
|
|
73
|
+
if (!session) continue
|
|
74
|
+
const unclosed = unclosedTurns(session)
|
|
75
|
+
if (unclosed.length === 0) continue
|
|
76
|
+
const summary = unclosed.map((u) => `turn ${u.turn} (${u.state})`).join(', ')
|
|
77
|
+
log(`retrace-interrupt-guard: 会话 ${id} 存在未闭合轮次 ${summary}——中断未干净收尾,重启后可能触发旧光标回放;建议退出前先体检(dsh-log-contract check)`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 在给定会话上运行检测(供 CLI/测试直接调用)。
|
|
84
|
+
* @param {object} session - 会话对象或 { events: [...] }。
|
|
85
|
+
* @returns {Array<{turn: number, state: string}>}
|
|
86
|
+
*/
|
|
87
|
+
export function detectUnclosed(session) {
|
|
88
|
+
return unclosedTurns(session)
|
|
89
|
+
}
|
package/lib/prewrite-guard.js
CHANGED
|
@@ -32,6 +32,45 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { editorError } from './host-core.js'
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 回档幅度阈值(快照点守卫,2026-08-31 c2d05ce9 事故闭环)。
|
|
37
|
+
*
|
|
38
|
+
* 业务语义(生产级运行保障,见 工程-生产级运行时/快照点与回档机制-设计-20260831.md):
|
|
39
|
+
* - 一次编辑/撤回/重发遮蔽的 surface 节点占比 > 本阈值 = **回档请求**——
|
|
40
|
+
* 等于把生产基线大幅改写 → 拒绝落盘,引导从快照点创建官方分支;
|
|
41
|
+
* - 与 client 侧 SHADOW_SAFETY_RATIO(0.4)同源:UI 降级判定与写前拦截共用同一阈值;
|
|
42
|
+
* - 阈值可通过 createMarkerGuard({ rollbackRatio }) 覆盖(测试/未来配置)。
|
|
43
|
+
*/
|
|
44
|
+
export const ROLLBACK_RATIO = 0.4
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 短会话豁免阈值(2026-08-31 独立审查问题 B):surface 节点 ≤ 6(≈ 3 轮)
|
|
48
|
+
* 的会话不做回档幅度拦截——否则 1-2 轮新会话编辑/撤回/重发 100% 被拒,
|
|
49
|
+
* 现有功能回归。事故防御针对大会话的「编辑早期消息遮蔽几十轮」,短会话无此风险。
|
|
50
|
+
*/
|
|
51
|
+
export const ROLLBACK_MIN_SURFACE = 6
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 该 marker 是否来自 rollback(restore op)——rollback 本身就是「从快照点
|
|
55
|
+
* 回档」的官方机制,不应再被回档幅度守卫拦截(否则大范围回档永远不可用,
|
|
56
|
+
* 且错误文案引导「打分支」而插件内无此操作 = 死路)。2026-08-31 独立审查问题 A。
|
|
57
|
+
*/
|
|
58
|
+
export function isRestoreMarker(envelope) {
|
|
59
|
+
const id = envelope?.data?.message?.id
|
|
60
|
+
return typeof id === 'string' && id.startsWith('retrace-restore-')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 计算一次 replace 的遮蔽占比(0..1)。无 surface/无 sourceEventSeqs/短会话 → 0(不拦截)。 */
|
|
64
|
+
export function rollbackShareOf(session, envelope) {
|
|
65
|
+
if (!session || !envelope) return 0
|
|
66
|
+
const shadowed = Array.isArray(envelope.sourceEventSeqs) ? envelope.sourceEventSeqs.length : 0
|
|
67
|
+
if (shadowed <= 0) return 0
|
|
68
|
+
const surfaceNodes = session.surface?.nodes
|
|
69
|
+
const total = Array.isArray(surfaceNodes) ? surfaceNodes.length : 0
|
|
70
|
+
if (total <= ROLLBACK_MIN_SURFACE) return 0 // 短会话豁免(问题 B)
|
|
71
|
+
return shadowed / total
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
/** R2 —— T1 折叠自检:与 dsh-token-meter/_foldEvent 及 checks.js T1 同语义。 */
|
|
36
75
|
export function tokenMeterFoldOk(events) {
|
|
37
76
|
// 与 checks.js:372 同语义:无任何 step/start 的日志(极早期格式/简化夹具)
|
|
@@ -61,7 +100,7 @@ export function tokenMeterFoldOk(events) {
|
|
|
61
100
|
* @param {(sessionId: string) => boolean} [options.enabled] 门控(默认恒 true)。
|
|
62
101
|
* @returns {{ validateMarkerAppend(session, envelope): Promise<void> }}
|
|
63
102
|
*/
|
|
64
|
-
export function createMarkerGuard({ log = () => {}, prewriterFactory, enabled = () => true } = {}) {
|
|
103
|
+
export function createMarkerGuard({ log = () => {}, prewriterFactory, enabled = () => true, rollbackRatio = ROLLBACK_RATIO } = {}) {
|
|
65
104
|
let factory = prewriterFactory ?? null
|
|
66
105
|
return {
|
|
67
106
|
/**
|
|
@@ -72,6 +111,27 @@ export function createMarkerGuard({ log = () => {}, prewriterFactory, enabled =
|
|
|
72
111
|
*/
|
|
73
112
|
async validateMarkerAppend(session, envelope) {
|
|
74
113
|
if (typeof enabled === 'function' && enabled(session?.id) === false) return { t1Ok: true }
|
|
114
|
+
// 快照点守卫(2026-08-31 c2d05ce9 事故闭环):回档请求(遮蔽 > 阈值)
|
|
115
|
+
// 拒绝落盘——生产基线不支持原地大幅改写,引导从快照点创建官方分支。
|
|
116
|
+
// 在契约校验之前独立检查(业务规则,与合法性正交);host 层强制,UI 绕过也拦。
|
|
117
|
+
// 豁免(2026-08-31 独立审查):restore(rollback 回档本身合法)与短会话(≤3 轮)。
|
|
118
|
+
if (
|
|
119
|
+
envelope?.surfaceOp?.op === 'replace' &&
|
|
120
|
+
Array.isArray(envelope.sourceEventSeqs) &&
|
|
121
|
+
!isRestoreMarker(envelope)
|
|
122
|
+
) {
|
|
123
|
+
const share = rollbackShareOf(session, envelope)
|
|
124
|
+
if (share > rollbackRatio) {
|
|
125
|
+
const pct = Math.round(share * 100)
|
|
126
|
+
log(
|
|
127
|
+
`retrace: rollback guard — replace shadows ${envelope.sourceEventSeqs.length}/${session?.surface?.nodes?.length ?? '?'} surface nodes (${pct}% > ${Math.round(rollbackRatio * 100)}%); refusing in-place rewrite; guide user to fork a branch from the snapshot point`,
|
|
128
|
+
)
|
|
129
|
+
throw editorError(
|
|
130
|
+
'rollback-guide',
|
|
131
|
+
`此操作将回档到较早的快照点(遮蔽 ${pct}% 的对话)。生产基线不支持原地大幅改写;请从快照点创建会话分支,在新分支中继续——原会话完整保留。`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
75
135
|
if (factory === null) {
|
|
76
136
|
try {
|
|
77
137
|
factory = (await import('dsh-log-contract')).createPreWriter
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-retrace",
|
|
3
3
|
"description": "Retrace · 回溯 — Recall, edit-and-resend, regenerate, and conversation/artifact versioning (timeline, rollback, fork map) for DeepSeek Harness — Web and Desktop",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.13",
|
|
5
|
+
"packageManager": "pnpm@11.7.0",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "lib/index.js",
|
|
7
8
|
"types": "lib/types/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/generate-dynamic.mjs && node scripts/build-client.mjs",
|
|
11
|
+
"check": "node scripts/check-syntax.mjs && node scripts/check-dynamic.mjs",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"prepublishOnly": "pnpm check && pnpm build && pnpm test"
|
|
14
|
+
},
|
|
8
15
|
"exports": {
|
|
9
16
|
".": {
|
|
10
17
|
"default": "./lib/index.js"
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
"react": "^18.2.0"
|
|
79
86
|
},
|
|
80
87
|
"dependencies": {
|
|
81
|
-
"dsh-log-contract": "^0.3.
|
|
88
|
+
"dsh-log-contract": "^0.3.7",
|
|
82
89
|
"zod": "^4.4.3"
|
|
83
90
|
},
|
|
84
91
|
"repository": {
|
|
@@ -95,10 +102,5 @@
|
|
|
95
102
|
},
|
|
96
103
|
"bin": {
|
|
97
104
|
"retrace": "bin/retrace.mjs"
|
|
98
|
-
},
|
|
99
|
-
"scripts": {
|
|
100
|
-
"build": "node scripts/generate-dynamic.mjs && node scripts/build-client.mjs",
|
|
101
|
-
"check": "node scripts/check-syntax.mjs && node scripts/check-dynamic.mjs",
|
|
102
|
-
"test": "vitest run"
|
|
103
105
|
}
|
|
104
|
-
}
|
|
106
|
+
}
|