dsh-instruction-memory 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@
7
7
  > 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
8
8
  > 这些历史 tag **不应回填**——npm 上的版本号已被占用。
9
9
 
10
+ ## [1.3.1] — 2026-10-15
11
+
12
+ 1.3.0 发布当天的评审跟进:一个披露计数回归与三个展示层小修正。
13
+
14
+ ### 修复
15
+
16
+ - **ui-test 去竞态**:F7 真实定时器冒烟中「暂停后立刻断言注入已静音」与 50ms 定时器存在调度竞态
17
+ (高负载 runner 上定时器可能已过期),改为确定性地断言到期时刻已写入存储文件,并把到期等待
18
+ 从 300ms 放宽到 1s。仅测试基建,不影响已发布的包内容。
19
+ - **导入回退去重后的计数披露**:「新增指令 N 条 / 新增用户形象 N 个」按实际落库数(staged)
20
+ 统计,不再包含被回退去重跳过的项;截断披露同样只统计实际落库项。80f94f5 引入的口径回归,
21
+ contract-test 新增断言钉住「2 通过 − 1 回退重复 = 新增指令 1 条」。
22
+ - **暂停倒计时与复制路径的三个小修正**:新起暂停时先取当前时刻再起秒表(否则长开面板的
23
+ 首帧剩余时间可能偏大最多 1 秒,下一秒自愈);他窗发起暂停时复位暂停菜单展开态;
24
+ 剪贴板 execCommand 抛错路径也移除游离的辅助 textarea。均为 1.3.0 评审发现,仅展示层。
25
+
10
26
  ## [1.3.0] — 2026-10-15
11
27
 
12
28
  两个审查修复与两项体验增强(F12 / F7)。
package/lib/client.js CHANGED
@@ -1600,6 +1600,13 @@ window.__ModuleLoader__.load({
1600
1600
  && view.injection.muted === true
1601
1601
  React.useEffect(() => {
1602
1602
  if (!mutedNow) return undefined
1603
+ // A pause started from ANOTHER window replaces the menu row; the open
1604
+ // flag must not resurface when the row returns after expiry.
1605
+ setPauseMenuOpen(false)
1606
+ // Snapshot the clock the moment the pause lands: nowTick may be up to
1607
+ // a second stale from a PREVIOUS pause, which would render the first
1608
+ // frame with an inflated remaining time.
1609
+ setNowTick(Date.now())
1603
1610
  const tick = setInterval(() => setNowTick(Date.now()), 1000)
1604
1611
  return () => clearInterval(tick)
1605
1612
  }, [mutedNow])
@@ -1984,8 +1991,14 @@ window.__ModuleLoader__.load({
1984
1991
  const host = document.body || document.head
1985
1992
  if (host && typeof host.appendChild === 'function') host.appendChild(area)
1986
1993
  if (typeof area.select === 'function') area.select()
1987
- const ok = typeof document.execCommand === 'function' && document.execCommand('copy') === true
1988
- if (host && typeof host.removeChild === 'function' && area.parentNode === host) host.removeChild(area)
1994
+ // execCommand may THROW (not merely return false); the detached
1995
+ // helper must leave the DOM on every path.
1996
+ let ok = false
1997
+ try {
1998
+ ok = typeof document.execCommand === 'function' && document.execCommand('copy') === true
1999
+ } finally {
2000
+ if (host && typeof host.removeChild === 'function' && area.parentNode === host) host.removeChild(area)
2001
+ }
1989
2002
  if (ok) {
1990
2003
  setNotice(t('diag.copied'))
1991
2004
  return
package/lib/index.js CHANGED
@@ -3372,8 +3372,6 @@ export function apply(ctx) {
3372
3372
  const seenIds = new Set(existing.map((entry) => entry.setId + '\u0000' + entry.id))
3373
3373
  const seenBodies = new Set(existing.map((entry) => bodyKey(entry.setId, entry.title, entry.content)))
3374
3374
  const accepted = []
3375
- const cutFields = new Set()
3376
- let truncatedCount = 0
3377
3375
  let skipped = 0
3378
3376
  for (const candidate of (rawEntries || [])) {
3379
3377
  const entry = sanitizeEntry(candidate)
@@ -3390,17 +3388,12 @@ export function apply(ctx) {
3390
3388
  skipped += 1
3391
3389
  continue
3392
3390
  }
3391
+ // Truncation travels per item and is only REPORTED for items that are
3392
+ // actually staged: an item later skipped as a fallback duplicate must
3393
+ // not inflate the truncation disclosure (or the inserted-count).
3393
3394
  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
3395
  entry.updatedAt = Date.now()
3403
- accepted.push({ entry, wanted })
3396
+ accepted.push({ entry, wanted, cut })
3404
3397
  seenIds.add(wanted + '\u0000' + entry.id)
3405
3398
  seenBodies.add(bodyKey(wanted, entry.title, entry.content))
3406
3399
  }
@@ -3411,8 +3404,6 @@ export function apply(ctx) {
3411
3404
  const seenProfileIds = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.id))
3412
3405
  const seenProfileBodies = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.name + '\u0000' + profile.content))
3413
3406
  const acceptedProfiles = []
3414
- const cutProfileFields = new Set()
3415
- let truncatedProfileCount = 0
3416
3407
  let skippedProfiles = 0
3417
3408
  for (const candidate of (rawProfiles || [])) {
3418
3409
  const profile = sanitizeProfile(candidate)
@@ -3431,12 +3422,8 @@ export function apply(ctx) {
3431
3422
  continue
3432
3423
  }
3433
3424
  const cut = truncatedProfileFields(candidate)
3434
- if (cut.length > 0) {
3435
- truncatedProfileCount += 1
3436
- for (const field of cut) cutProfileFields.add(field)
3437
- }
3438
3425
  profile.updatedAt = Date.now()
3439
- acceptedProfiles.push({ profile, wanted })
3426
+ acceptedProfiles.push({ profile, wanted, cut })
3440
3427
  seenProfileIds.add(wanted + '\u0000' + profile.id)
3441
3428
  seenProfileBodies.add(wanted + '\u0000' + profile.name + '\u0000' + profile.content)
3442
3429
  }
@@ -3499,7 +3486,7 @@ export function apply(ctx) {
3499
3486
  finalSeenBodies.add(bodyKey(finalId, item.entry.title, item.entry.content))
3500
3487
  }
3501
3488
  item.entry.setId = finalId
3502
- stagedEntries.push(item.entry)
3489
+ stagedEntries.push(item)
3503
3490
  }
3504
3491
  const finalSeenProfileIds = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.id))
3505
3492
  const finalSeenProfileBodies = new Set(existingProfiles.map((profile) => profile.setId + '\u0000' + profile.name + '\u0000' + profile.content))
@@ -3522,7 +3509,7 @@ export function apply(ctx) {
3522
3509
  finalSeenProfileBodies.add(finalId + '\u0000' + item.profile.name + '\u0000' + item.profile.content)
3523
3510
  }
3524
3511
  item.profile.setId = finalId
3525
- stagedProfiles.push(item.profile)
3512
+ stagedProfiles.push(item)
3526
3513
  }
3527
3514
  }
3528
3515
  if (stagedEntries.length === 0 && stagedProfiles.length === 0) {
@@ -3538,8 +3525,8 @@ export function apply(ctx) {
3538
3525
  }
3539
3526
  const saved = await commit(() => {
3540
3527
  state.data.sets.push(...createdSets)
3541
- for (const entry of stagedEntries) state.data.entries.push(entry)
3542
- for (const profile of stagedProfiles) state.data.profiles.push(profile)
3528
+ for (const item of stagedEntries) state.data.entries.push(item.entry)
3529
+ for (const item of stagedProfiles) state.data.profiles.push(item.profile)
3543
3530
  })
3544
3531
  const parts = []
3545
3532
  if (createdSets.length > 0) {
@@ -3551,19 +3538,38 @@ export function apply(ctx) {
3551
3538
  if (dedupedOnFallback > 0) {
3552
3539
  parts.push('另有 ' + dedupedOnFallback + ' 项因回退到当前配置集后与已有内容重复被跳过')
3553
3540
  }
3554
- if (accepted.length > 0) {
3555
- const truncated = truncatedCount > 0
3556
- ? ';其中 ' + truncatedCount + ' 条超过长度上限,' + [...cutFields].join('、') + '已被截断'
3541
+ if (stagedEntries.length > 0) {
3542
+ // Counts quote what actually LANDED: staged ⊆ accepted, and the gap is
3543
+ // the fallback duplicates already disclosed on their own line — an
3544
+ // accepted-based count would claim inserts that were skipped.
3545
+ const stagedCut = new Set()
3546
+ let stagedTruncated = 0
3547
+ for (const item of stagedEntries) {
3548
+ if (item.cut.length > 0) {
3549
+ stagedTruncated += 1
3550
+ for (const field of item.cut) stagedCut.add(field)
3551
+ }
3552
+ }
3553
+ const truncated = stagedTruncated > 0
3554
+ ? ';其中 ' + stagedTruncated + ' 条超过长度上限,' + [...stagedCut].join('、') + '已被截断'
3557
3555
  : ''
3558
- parts.push('新增指令 ' + accepted.length + ' 条,跳过 ' + skipped + ' 条' + truncated)
3556
+ parts.push('新增指令 ' + stagedEntries.length + ' 条,跳过 ' + skipped + ' 条' + truncated)
3559
3557
  } else if (skipped > 0) {
3560
3558
  parts.push('指令跳过 ' + skipped + ' 条(已存在或无效)')
3561
3559
  }
3562
- if (acceptedProfiles.length > 0) {
3563
- const truncated = truncatedProfileCount > 0
3564
- ? ';其中 ' + truncatedProfileCount + ' 个超过长度上限,' + [...cutProfileFields].join('、') + '已被截断'
3560
+ if (stagedProfiles.length > 0) {
3561
+ const stagedCut = new Set()
3562
+ let stagedTruncated = 0
3563
+ for (const item of stagedProfiles) {
3564
+ if (item.cut.length > 0) {
3565
+ stagedTruncated += 1
3566
+ for (const field of item.cut) stagedCut.add(field)
3567
+ }
3568
+ }
3569
+ const truncated = stagedTruncated > 0
3570
+ ? ';其中 ' + stagedTruncated + ' 个超过长度上限,' + [...stagedCut].join('、') + '已被截断'
3565
3571
  : ''
3566
- parts.push('新增用户形象 ' + acceptedProfiles.length + ' 个,跳过 ' + skippedProfiles + ' 个' + truncated)
3572
+ parts.push('新增用户形象 ' + stagedProfiles.length + ' 个,跳过 ' + skippedProfiles + ' 个' + truncated)
3567
3573
  } else if (skippedProfiles > 0) {
3568
3574
  parts.push('用户形象跳过 ' + skippedProfiles + ' 个(已存在或无效)')
3569
3575
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-instruction-memory",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",