dsh-instruction-memory 1.3.0 → 1.3.2
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 +55 -0
- package/lib/client.js +38 -9
- package/lib/index.js +58 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@
|
|
|
7
7
|
> 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
|
|
8
8
|
> 这些历史 tag **不应回填**——npm 上的版本号已被占用。
|
|
9
9
|
|
|
10
|
+
## [1.3.2] — 2026-10-16
|
|
11
|
+
|
|
12
|
+
1.3.1 发布后的两轮全量体检产出:CI 测试基建根治、client/demo 边界修复与宿主生命周期加固。
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- **CI 测试基建:路由等待按墙钟计时**:contract/ui 的 rawRoute 原以固定迭代数轮询
|
|
17
|
+
(约 2s 上限),高负载 runner 上真实磁盘写路径(锁、双备份、rename、历史快照)可能偶发
|
|
18
|
+
超限,导致整套测试崩溃(`route never responded`),且崩溃无断言痕迹。CI 偶发红
|
|
19
|
+
(1.2.0 时代即有先例)的根因即此。现改为墙钟 15s 上限,并给每个套件装崩溃钩子
|
|
20
|
+
(失败时输出 GitHub `::error::` 注解,含断言与堆栈),verify 的固定 60ms boot 等待改为
|
|
21
|
+
轮询到条件成立。仅测试基建,不影响已发布包内容。
|
|
22
|
+
- **CI 假绿窗口收口**:套件尾部 `process.exit(failures===0?0:1)` 会覆盖崩溃钩子设置的
|
|
23
|
+
exitCode——最后一次校验之后的浮游 rejection 可能被吞成「ALL PASS + exit 0」。现在退出码
|
|
24
|
+
尊重钩子状态;ui-test 的 scratch 清理移到退出码捕获之前;fetch 包装器在路由超时抛出时
|
|
25
|
+
递减 inFlight(否则后续每次 settle 都烧满等待窗口)。
|
|
26
|
+
- **测试 scratch 目录自清理**:contract/verify/ui 现在都注册按 PID 的退出钩子,任何退出路径
|
|
27
|
+
(崩溃、抛错、失败)都删掉本进程的家目录;此前每次跑测试都会在仓库里留下
|
|
28
|
+
`.test-dsh-home-*` 目录(实测累积到 39 个)。仅测试基建。
|
|
29
|
+
- **verify 三条恒真断言修正**:F17 相关性分词的三个 check 把比较器写在了 detail 位、
|
|
30
|
+
ok 位是恒真值,实际从未断言(c74bfcc 引入的既有问题,本轮审阅发现);改为真布尔断言。
|
|
31
|
+
- **demo 桩 `sets.total` 计数错误**:scoped 数组覆盖克隆后才读计数,「全部配置集合计」显示的是
|
|
32
|
+
当前集合数而非全库数;改为覆盖前从原库统计。
|
|
33
|
+
- **client:`data:null` 快照穿透 normalizer**:absorb 读取 `data.budgetChars` 抛错,初始加载
|
|
34
|
+
会永久卡在加载态且无错误提示(当前宿主不产生该形状,防御性收口,verify 钉住)。
|
|
35
|
+
- **client:重进源码模式不再静默丢弃未保存草稿**:源码草稿与最近一次读/存的基线比对,有改动
|
|
36
|
+
走标准弃用确认(基线随读取与保存推进)。
|
|
37
|
+
- **client:F17 本地预演不再给已停用条目标注命中**(它们永远不会被注入,此前会误导)。
|
|
38
|
+
- **host 生命周期三处加固**:初始 `state.data` 字面量补齐 `disabledUntil` 键(watcher 自写
|
|
39
|
+
检测在新进程首次保存时因键形状不一致必然失配,产生一次多余重载与冗余 SSE 帧,探针实证);
|
|
40
|
+
卸载栅栏(in-flight 操作完成后不再重注册注入段/重启 watcher/推送快照);`state` 路由的
|
|
41
|
+
首次物化纳入磁盘队列(与 readSource 一致)。
|
|
42
|
+
|
|
43
|
+
### 变更
|
|
44
|
+
|
|
45
|
+
- **暂停倒计时改用渲染时时钟**:剩余时间在渲染时直接取 `Date.now()` 计算,1 秒 interval
|
|
46
|
+
仅作重渲染触发器。此前 nowTick 在两次暂停之间可能陈旧任意久(如隔天再暂停),首帧
|
|
47
|
+
剩余时间会虚大;原描述「最多偏大 1 秒」低估了陈旧量。仅展示层。
|
|
48
|
+
|
|
49
|
+
## [1.3.1] — 2026-10-15
|
|
50
|
+
|
|
51
|
+
1.3.0 发布当天的评审跟进:一个披露计数回归与三个展示层小修正。
|
|
52
|
+
|
|
53
|
+
### 修复
|
|
54
|
+
|
|
55
|
+
- **ui-test 去竞态**:F7 真实定时器冒烟中「暂停后立刻断言注入已静音」与 50ms 定时器存在调度竞态
|
|
56
|
+
(高负载 runner 上定时器可能已过期),改为确定性地断言到期时刻已写入存储文件,并把到期等待
|
|
57
|
+
从 300ms 放宽到 1s。仅测试基建,不影响已发布的包内容。
|
|
58
|
+
- **导入回退去重后的计数披露**:「新增指令 N 条 / 新增用户形象 N 个」按实际落库数(staged)
|
|
59
|
+
统计,不再包含被回退去重跳过的项;截断披露同样只统计实际落库项。a2b885b 引入的口径回归,
|
|
60
|
+
contract-test 新增断言钉住「2 通过 − 1 回退重复 = 新增指令 1 条」。
|
|
61
|
+
- **暂停倒计时与复制路径的三个小修正**:新起暂停时先取当前时刻再起秒表(否则长开面板的
|
|
62
|
+
首帧剩余时间可能偏大最多 1 秒,下一秒自愈);他窗发起暂停时复位暂停菜单展开态;
|
|
63
|
+
剪贴板 execCommand 抛错路径也移除游离的辅助 textarea。均为 1.3.0 评审发现,仅展示层。
|
|
64
|
+
|
|
10
65
|
## [1.3.0] — 2026-10-15
|
|
11
66
|
|
|
12
67
|
两个审查修复与两项体验增强(F12 / F7)。
|
package/lib/client.js
CHANGED
|
@@ -924,7 +924,9 @@ window.__ModuleLoader__.load({
|
|
|
924
924
|
function normalizeResult(result) {
|
|
925
925
|
if (result === null || typeof result !== 'object') return null
|
|
926
926
|
const snapshot = result.snapshot !== undefined && result.snapshot !== null ? result.snapshot : result
|
|
927
|
-
|
|
927
|
+
// data === null passes a typeof check but poisons every consumer that
|
|
928
|
+
// reads data.budgetChars; it is exactly as unusable as a missing data.
|
|
929
|
+
if (snapshot === null || typeof snapshot !== 'object' || snapshot.data === undefined || snapshot.data === null) return null
|
|
928
930
|
return {
|
|
929
931
|
snapshot,
|
|
930
932
|
ok: result.ok !== false,
|
|
@@ -1530,6 +1532,10 @@ window.__ModuleLoader__.load({
|
|
|
1530
1532
|
// The body ceiling for the live check, mirrored into a ref so the
|
|
1531
1533
|
// debounced effect never has to depend on the snapshot object.
|
|
1532
1534
|
const sourceCeilingRef = React.useRef(0)
|
|
1535
|
+
// The source text as of the last read/save: openSource compares the
|
|
1536
|
+
// live draft against this to decide whether re-entering source mode
|
|
1537
|
+
// needs a discard confirmation.
|
|
1538
|
+
const sourceBaselineRef = React.useRef('')
|
|
1533
1539
|
// F17: the local relevance preview.
|
|
1534
1540
|
const [simOpen, setSimOpen] = React.useState(false)
|
|
1535
1541
|
const [simQuery, setSimQuery] = React.useState('')
|
|
@@ -1595,11 +1601,16 @@ window.__ModuleLoader__.load({
|
|
|
1595
1601
|
// clears it the moment the snapshot says otherwise. This effect sits
|
|
1596
1602
|
// BEFORE the early returns (a hook skipped by a conditional return would
|
|
1597
1603
|
// desync the hook order); it reads the snapshot through `view` because
|
|
1598
|
-
// the derived `injection` does not exist yet up here.
|
|
1604
|
+
// the derived `injection` does not exist yet up here. The tick value
|
|
1605
|
+
// itself is only a re-render trigger; the rendered remaining time is
|
|
1606
|
+
// computed from Date.now() at render time (see below).
|
|
1599
1607
|
const mutedNow = view !== null && view.injection !== null && typeof view.injection === 'object'
|
|
1600
1608
|
&& view.injection.muted === true
|
|
1601
1609
|
React.useEffect(() => {
|
|
1602
1610
|
if (!mutedNow) return undefined
|
|
1611
|
+
// A pause started from ANOTHER window replaces the menu row; the open
|
|
1612
|
+
// flag must not resurface when the row returns after expiry.
|
|
1613
|
+
setPauseMenuOpen(false)
|
|
1603
1614
|
const tick = setInterval(() => setNowTick(Date.now()), 1000)
|
|
1604
1615
|
return () => clearInterval(tick)
|
|
1605
1616
|
}, [mutedNow])
|
|
@@ -1874,6 +1885,12 @@ window.__ModuleLoader__.load({
|
|
|
1874
1885
|
setError(t('source.editorOpen'))
|
|
1875
1886
|
return
|
|
1876
1887
|
}
|
|
1888
|
+
// Re-entering source mode must not silently discard an unsaved source
|
|
1889
|
+
// draft: sourceText survives 退出源码模式 and reloadSource, and the
|
|
1890
|
+
// button stays reachable while source mode is open. The baseline is
|
|
1891
|
+
// the text as of the last read/save, so an untouched draft reopens
|
|
1892
|
+
// without friction.
|
|
1893
|
+
if (sourceText !== sourceBaselineRef.current && !confirmDiscard()) return
|
|
1877
1894
|
try {
|
|
1878
1895
|
const result = await callHost('read-source', null)
|
|
1879
1896
|
if (result === null || typeof result !== 'object' || typeof result.source !== 'string') {
|
|
@@ -1886,6 +1903,7 @@ window.__ModuleLoader__.load({
|
|
|
1886
1903
|
return
|
|
1887
1904
|
}
|
|
1888
1905
|
setSourceText(result.source)
|
|
1906
|
+
sourceBaselineRef.current = result.source
|
|
1889
1907
|
setSourceBaseRev(revOfFrom(result.snapshot))
|
|
1890
1908
|
setSourceVerdict(null)
|
|
1891
1909
|
setSourceMode(true)
|
|
@@ -1911,6 +1929,7 @@ window.__ModuleLoader__.load({
|
|
|
1911
1929
|
// on must move with it — otherwise the very next save would be
|
|
1912
1930
|
// refused as a conflict against our own write.
|
|
1913
1931
|
setSourceBaseRev(revOfFrom(result.snapshot))
|
|
1932
|
+
sourceBaselineRef.current = sourceText
|
|
1914
1933
|
void loadHistory()
|
|
1915
1934
|
})
|
|
1916
1935
|
}
|
|
@@ -1984,8 +2003,14 @@ window.__ModuleLoader__.load({
|
|
|
1984
2003
|
const host = document.body || document.head
|
|
1985
2004
|
if (host && typeof host.appendChild === 'function') host.appendChild(area)
|
|
1986
2005
|
if (typeof area.select === 'function') area.select()
|
|
1987
|
-
|
|
1988
|
-
|
|
2006
|
+
// execCommand may THROW (not merely return false); the detached
|
|
2007
|
+
// helper must leave the DOM on every path.
|
|
2008
|
+
let ok = false
|
|
2009
|
+
try {
|
|
2010
|
+
ok = typeof document.execCommand === 'function' && document.execCommand('copy') === true
|
|
2011
|
+
} finally {
|
|
2012
|
+
if (host && typeof host.removeChild === 'function' && area.parentNode === host) host.removeChild(area)
|
|
2013
|
+
}
|
|
1989
2014
|
if (ok) {
|
|
1990
2015
|
setNotice(t('diag.copied'))
|
|
1991
2016
|
return
|
|
@@ -2201,7 +2226,9 @@ window.__ModuleLoader__.load({
|
|
|
2201
2226
|
: data.entries.length
|
|
2202
2227
|
const activeSetName = (setsList.find((set) => set.id === setsActiveId) || {}).name || ''
|
|
2203
2228
|
// F17: the local preview scores the ACTIVE set's on-demand entries.
|
|
2204
|
-
|
|
2229
|
+
// Disabled entries can never be injected, so scoring them as 预演 hits
|
|
2230
|
+
// would mislead — they are excluded from the preview entirely.
|
|
2231
|
+
const autoEntries = data.entries.filter((entry) => entry.mode === 'auto' && entry.enabled !== false)
|
|
2205
2232
|
const sim = relevancePreview(autoEntries, simQuery)
|
|
2206
2233
|
const simById = new Map(sim.results.map((hit) => [hit.id, hit]))
|
|
2207
2234
|
const omittedById = new Map()
|
|
@@ -2258,14 +2285,16 @@ window.__ModuleLoader__.load({
|
|
|
2258
2285
|
? view.diagnostics.rev
|
|
2259
2286
|
: (data.rev || 0)
|
|
2260
2287
|
// F7: the pause decision itself belongs to the host (injection.muted);
|
|
2261
|
-
// the countdown below is display-only.
|
|
2262
|
-
// the
|
|
2263
|
-
//
|
|
2288
|
+
// the countdown below is display-only. The remaining time is computed
|
|
2289
|
+
// from the clock AT RENDER TIME — a stale tick value could be days old
|
|
2290
|
+
// (the interval only runs while a pause is live), which would inflate
|
|
2291
|
+
// the first frame of a later pause. The 1s interval exists purely to
|
|
2292
|
+
// trigger re-renders; at zero the bar yields back to the normal control.
|
|
2264
2293
|
const muted = injection.muted === true
|
|
2265
2294
|
const disabledUntil = typeof data.disabledUntil === 'number' && Number.isFinite(data.disabledUntil)
|
|
2266
2295
|
? data.disabledUntil
|
|
2267
2296
|
: 0
|
|
2268
|
-
const muteRemainingMs = muted ? Math.max(0, disabledUntil -
|
|
2297
|
+
const muteRemainingMs = muted ? Math.max(0, disabledUntil - Date.now()) : 0
|
|
2269
2298
|
const muteRemainingText = (ms) => {
|
|
2270
2299
|
const totalSec = Math.max(0, Math.round(ms / 1000))
|
|
2271
2300
|
const hh = Math.floor(totalSec / 3600)
|
package/lib/index.js
CHANGED
|
@@ -1255,6 +1255,11 @@ export function apply(ctx) {
|
|
|
1255
1255
|
version: FORMAT_VERSION,
|
|
1256
1256
|
rev: 0,
|
|
1257
1257
|
enabled: true,
|
|
1258
|
+
// Must match normalizeData's output shape KEY FOR KEY: the watcher's
|
|
1259
|
+
// self-write detection compares JSON strings, and a missing key here
|
|
1260
|
+
// made every fresh process's first save look like an external change
|
|
1261
|
+
// (one redundant reload + frame).
|
|
1262
|
+
disabledUntil: 0,
|
|
1258
1263
|
budgetChars: DEFAULT_BUDGET,
|
|
1259
1264
|
backupDir: '',
|
|
1260
1265
|
activeSetId: DEFAULT_SET_ID,
|
|
@@ -1313,6 +1318,13 @@ export function apply(ctx) {
|
|
|
1313
1318
|
* (from syncSection) and cleared in the effect cleanup.
|
|
1314
1319
|
*/
|
|
1315
1320
|
let muteTimer = null
|
|
1321
|
+
/**
|
|
1322
|
+
* Set by the effect cleanup: after unmount, an in-flight queued op may
|
|
1323
|
+
* still complete its disk write (the data must land), but it must not
|
|
1324
|
+
* re-register the prompt section, restart the watcher, or push snapshots
|
|
1325
|
+
* into a torn-down fiber.
|
|
1326
|
+
*/
|
|
1327
|
+
let disposed = false
|
|
1316
1328
|
|
|
1317
1329
|
/**
|
|
1318
1330
|
* A first run deliberately writes NOTHING (see readFromDisk step 3). This flag
|
|
@@ -1471,6 +1483,7 @@ export function apply(ctx) {
|
|
|
1471
1483
|
* leave the prompt byte-identical to a deployment without this plugin.
|
|
1472
1484
|
*/
|
|
1473
1485
|
const syncSection = () => {
|
|
1486
|
+
if (disposed) return
|
|
1474
1487
|
const wanted = sectionText() !== ''
|
|
1475
1488
|
if (wanted && sectionDispose === null) {
|
|
1476
1489
|
try {
|
|
@@ -1504,6 +1517,7 @@ export function apply(ctx) {
|
|
|
1504
1517
|
}
|
|
1505
1518
|
|
|
1506
1519
|
ctx.effect(() => () => {
|
|
1520
|
+
disposed = true
|
|
1507
1521
|
if (sectionDispose !== null) {
|
|
1508
1522
|
const dispose = sectionDispose
|
|
1509
1523
|
sectionDispose = null
|
|
@@ -1534,7 +1548,7 @@ export function apply(ctx) {
|
|
|
1534
1548
|
let watchDebounce = null
|
|
1535
1549
|
|
|
1536
1550
|
const pushSnapshot = () => {
|
|
1537
|
-
if (sseStreams.size === 0) return
|
|
1551
|
+
if (disposed || sseStreams.size === 0) return
|
|
1538
1552
|
const payload = 'data: ' + JSON.stringify(snapshot()) + '\n\n'
|
|
1539
1553
|
for (const [send, cleanup] of Array.from(sseStreams)) {
|
|
1540
1554
|
try {
|
|
@@ -1606,7 +1620,7 @@ export function apply(ctx) {
|
|
|
1606
1620
|
* yet) throws and is retried by the first successful write.
|
|
1607
1621
|
*/
|
|
1608
1622
|
const startWatching = () => {
|
|
1609
|
-
if (storeWatcher !== null) return
|
|
1623
|
+
if (disposed || storeWatcher !== null) return
|
|
1610
1624
|
try {
|
|
1611
1625
|
storeWatcher = watch(dirname(storePath), { persistent: false }, (event, filename) => {
|
|
1612
1626
|
// Only events naming memory.json matter; the .tmp/.bak/.lock churn of
|
|
@@ -3372,8 +3386,6 @@ export function apply(ctx) {
|
|
|
3372
3386
|
const seenIds = new Set(existing.map((entry) => entry.setId + '\u0000' + entry.id))
|
|
3373
3387
|
const seenBodies = new Set(existing.map((entry) => bodyKey(entry.setId, entry.title, entry.content)))
|
|
3374
3388
|
const accepted = []
|
|
3375
|
-
const cutFields = new Set()
|
|
3376
|
-
let truncatedCount = 0
|
|
3377
3389
|
let skipped = 0
|
|
3378
3390
|
for (const candidate of (rawEntries || [])) {
|
|
3379
3391
|
const entry = sanitizeEntry(candidate)
|
|
@@ -3390,17 +3402,12 @@ export function apply(ctx) {
|
|
|
3390
3402
|
skipped += 1
|
|
3391
3403
|
continue
|
|
3392
3404
|
}
|
|
3405
|
+
// Truncation travels per item and is only REPORTED for items that are
|
|
3406
|
+
// actually staged: an item later skipped as a fallback duplicate must
|
|
3407
|
+
// not inflate the truncation disclosure (or the inserted-count).
|
|
3393
3408
|
const cut = truncatedFields(candidate)
|
|
3394
|
-
if (cut.length > 0) {
|
|
3395
|
-
// sanitizeEntry enforces the limits silently, which is fine for the
|
|
3396
|
-
// editor (its inputs carry the same maxLength) but wrong here: an
|
|
3397
|
-
// imported 7,000-character instruction would come back as 6,000 with
|
|
3398
|
-
// nothing said about the loss.
|
|
3399
|
-
truncatedCount += 1
|
|
3400
|
-
for (const field of cut) cutFields.add(field)
|
|
3401
|
-
}
|
|
3402
3409
|
entry.updatedAt = Date.now()
|
|
3403
|
-
accepted.push({ entry, wanted })
|
|
3410
|
+
accepted.push({ entry, wanted, cut })
|
|
3404
3411
|
seenIds.add(wanted + '\u0000' + entry.id)
|
|
3405
3412
|
seenBodies.add(bodyKey(wanted, entry.title, entry.content))
|
|
3406
3413
|
}
|
|
@@ -3411,8 +3418,6 @@ export function apply(ctx) {
|
|
|
3411
3418
|
const seenProfileIds = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.id))
|
|
3412
3419
|
const seenProfileBodies = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.name + '\u0000' + profile.content))
|
|
3413
3420
|
const acceptedProfiles = []
|
|
3414
|
-
const cutProfileFields = new Set()
|
|
3415
|
-
let truncatedProfileCount = 0
|
|
3416
3421
|
let skippedProfiles = 0
|
|
3417
3422
|
for (const candidate of (rawProfiles || [])) {
|
|
3418
3423
|
const profile = sanitizeProfile(candidate)
|
|
@@ -3431,12 +3436,8 @@ export function apply(ctx) {
|
|
|
3431
3436
|
continue
|
|
3432
3437
|
}
|
|
3433
3438
|
const cut = truncatedProfileFields(candidate)
|
|
3434
|
-
if (cut.length > 0) {
|
|
3435
|
-
truncatedProfileCount += 1
|
|
3436
|
-
for (const field of cut) cutProfileFields.add(field)
|
|
3437
|
-
}
|
|
3438
3439
|
profile.updatedAt = Date.now()
|
|
3439
|
-
acceptedProfiles.push({ profile, wanted })
|
|
3440
|
+
acceptedProfiles.push({ profile, wanted, cut })
|
|
3440
3441
|
seenProfileIds.add(wanted + '\u0000' + profile.id)
|
|
3441
3442
|
seenProfileBodies.add(wanted + '\u0000' + profile.name + '\u0000' + profile.content)
|
|
3442
3443
|
}
|
|
@@ -3499,7 +3500,7 @@ export function apply(ctx) {
|
|
|
3499
3500
|
finalSeenBodies.add(bodyKey(finalId, item.entry.title, item.entry.content))
|
|
3500
3501
|
}
|
|
3501
3502
|
item.entry.setId = finalId
|
|
3502
|
-
stagedEntries.push(item
|
|
3503
|
+
stagedEntries.push(item)
|
|
3503
3504
|
}
|
|
3504
3505
|
const finalSeenProfileIds = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.id))
|
|
3505
3506
|
const finalSeenProfileBodies = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.name + '\u0000' + profile.content))
|
|
@@ -3522,7 +3523,7 @@ export function apply(ctx) {
|
|
|
3522
3523
|
finalSeenProfileBodies.add(finalId + '\u0000' + item.profile.name + '\u0000' + item.profile.content)
|
|
3523
3524
|
}
|
|
3524
3525
|
item.profile.setId = finalId
|
|
3525
|
-
stagedProfiles.push(item
|
|
3526
|
+
stagedProfiles.push(item)
|
|
3526
3527
|
}
|
|
3527
3528
|
}
|
|
3528
3529
|
if (stagedEntries.length === 0 && stagedProfiles.length === 0) {
|
|
@@ -3538,8 +3539,8 @@ export function apply(ctx) {
|
|
|
3538
3539
|
}
|
|
3539
3540
|
const saved = await commit(() => {
|
|
3540
3541
|
state.data.sets.push(...createdSets)
|
|
3541
|
-
for (const
|
|
3542
|
-
for (const
|
|
3542
|
+
for (const item of stagedEntries) state.data.entries.push(item.entry)
|
|
3543
|
+
for (const item of stagedProfiles) state.data.profiles.push(item.profile)
|
|
3543
3544
|
})
|
|
3544
3545
|
const parts = []
|
|
3545
3546
|
if (createdSets.length > 0) {
|
|
@@ -3551,19 +3552,38 @@ export function apply(ctx) {
|
|
|
3551
3552
|
if (dedupedOnFallback > 0) {
|
|
3552
3553
|
parts.push('另有 ' + dedupedOnFallback + ' 项因回退到当前配置集后与已有内容重复被跳过')
|
|
3553
3554
|
}
|
|
3554
|
-
if (
|
|
3555
|
-
|
|
3556
|
-
|
|
3555
|
+
if (stagedEntries.length > 0) {
|
|
3556
|
+
// Counts quote what actually LANDED: staged ⊆ accepted, and the gap is
|
|
3557
|
+
// the fallback duplicates already disclosed on their own line — an
|
|
3558
|
+
// accepted-based count would claim inserts that were skipped.
|
|
3559
|
+
const stagedCut = new Set()
|
|
3560
|
+
let stagedTruncated = 0
|
|
3561
|
+
for (const item of stagedEntries) {
|
|
3562
|
+
if (item.cut.length > 0) {
|
|
3563
|
+
stagedTruncated += 1
|
|
3564
|
+
for (const field of item.cut) stagedCut.add(field)
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
const truncated = stagedTruncated > 0
|
|
3568
|
+
? ';其中 ' + stagedTruncated + ' 条超过长度上限,' + [...stagedCut].join('、') + '已被截断'
|
|
3557
3569
|
: ''
|
|
3558
|
-
parts.push('新增指令 ' +
|
|
3570
|
+
parts.push('新增指令 ' + stagedEntries.length + ' 条,跳过 ' + skipped + ' 条' + truncated)
|
|
3559
3571
|
} else if (skipped > 0) {
|
|
3560
3572
|
parts.push('指令跳过 ' + skipped + ' 条(已存在或无效)')
|
|
3561
3573
|
}
|
|
3562
|
-
if (
|
|
3563
|
-
const
|
|
3564
|
-
|
|
3574
|
+
if (stagedProfiles.length > 0) {
|
|
3575
|
+
const stagedCut = new Set()
|
|
3576
|
+
let stagedTruncated = 0
|
|
3577
|
+
for (const item of stagedProfiles) {
|
|
3578
|
+
if (item.cut.length > 0) {
|
|
3579
|
+
stagedTruncated += 1
|
|
3580
|
+
for (const field of item.cut) stagedCut.add(field)
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
const truncated = stagedTruncated > 0
|
|
3584
|
+
? ';其中 ' + stagedTruncated + ' 个超过长度上限,' + [...stagedCut].join('、') + '已被截断'
|
|
3565
3585
|
: ''
|
|
3566
|
-
parts.push('新增用户形象 ' +
|
|
3586
|
+
parts.push('新增用户形象 ' + stagedProfiles.length + ' 个,跳过 ' + skippedProfiles + ' 个' + truncated)
|
|
3567
3587
|
} else if (skippedProfiles > 0) {
|
|
3568
3588
|
parts.push('用户形象跳过 ' + skippedProfiles + ' 个(已存在或无效)')
|
|
3569
3589
|
}
|
|
@@ -3579,7 +3599,12 @@ export function apply(ctx) {
|
|
|
3579
3599
|
// A first run writes nothing at boot (readFromDisk step 3), so the
|
|
3580
3600
|
// materialisation happens here, on a deliberate user action, instead of
|
|
3581
3601
|
// as a side effect of the host merely starting up.
|
|
3582
|
-
|
|
3602
|
+
// A first run writes nothing at boot (readFromDisk step 3), so the
|
|
3603
|
+
// materialisation happens here, on a deliberate user action, instead of
|
|
3604
|
+
// as a side effect of the host merely starting up. It goes through the
|
|
3605
|
+
// same disk queue as every other disk access, so it can never
|
|
3606
|
+
// interleave with an in-flight write (readSource already did this).
|
|
3607
|
+
const materialized = await enqueue(() => ensureStoreMaterialized())
|
|
3583
3608
|
// Same envelope as every other method. One response shape means the
|
|
3584
3609
|
// Client can never again be handed a payload it silently ignores.
|
|
3585
3610
|
// `saved` is true only on the first run, when materialisation actually
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "User-only instruction memory for DSH: long-term instructions maintained in the settings UI and auto-injected into every conversation — the model has no write access. 指令记忆:只由用户在设置页维护的长期指令,自动注入此后每轮对话;模型没有任何写入口。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|