dsh-instruction-memory 1.5.2 → 1.5.3

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,33 @@
7
7
  > 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
8
8
  > 这些历史 tag **不应回填**——npm 上的版本号已被占用。
9
9
 
10
+ ## [1.5.3] — 2026-11-09
11
+
12
+ 1.5.2 的发布后修正,两处都是当天发现:一处**判断依据用错**,一处**依赖了初始化顺序**。没有新功能,
13
+ 存储格式、注入文本与面板行为不变。
14
+
15
+ ### 修复
16
+
17
+ - **背压判断把健康但读得慢的订阅者当成死连接(1.5.2 引入)**:1.5.2 让 `send()` 返回 `res.write()`
18
+ 的结果,并在返回 `false` 时立刻 `cleanup()`。但 `write()` 的 `false` 只表示**这一次写入超过了
19
+ socket 缓冲的高水位**——一个健康、但此刻读得慢的客户端同样会拿到 `false`。于是一个大帧(极限存储
20
+ 约 3.4 MB)就可能把正常订阅者踢下线。现在返回值不再用作存活判据:心跳只负责让"写失败"这条路走通
21
+ ——对端真的消失时,写入会报错并触发该流早已注册的 `close`/`error` 清理;`pushSnapshot` 恢复
22
+ 1.5.1 的语义。
23
+ - **`diskRevFrom()` 的声明顺序依赖**:1.5.2 把它定义在 `writeToDisk()` 之后。`apply()` 是同步函数,
24
+ 通常情况下没有问题;但"缺主文件时从 `.bak` 恢复"这条路径会在 boot 序列里调用 `writeToDisk()`,
25
+ 那是一次**可能先于声明执行**的调用,而 `const` 会落在暂时性死区并抛 `ReferenceError`(保存因此
26
+ 失败、`storageError` 报"保存失败")。现在两个 helper 提到 `writeToDisk()` 之前,用顺序消除这个
27
+ 假设,而不是依赖"今天恰好先 await"。
28
+
29
+ ### 验收
30
+
31
+ - 全套 7 个套件与 `node check-metadata.mjs` 全绿;`contract-test.mjs` 连续独立运行 5 次均 ALL PASS。
32
+ - 1.5.2 的 CI 在 `windows-latest / Node 24` 上失败了 2 条 SSE 断言:
33
+ `sse: a writer that throws on broadcast is fully disposed (response ended)` 与
34
+ `sse: disposing the dead writer does not interrupt a healthy stream`。本版按上面的修正重新验证这两个
35
+ 断言所在的路径(死连接清理 + 健康流不受牵连)。
36
+
10
37
  ## [1.5.2] — 2026-11-09
11
38
 
12
39
  针对 2026-11-09 的代码审查(Host 侧与 Client 侧并行只读审查,逐条复核后落地)。本版**没有新增功能、
package/lib/index.js CHANGED
@@ -2238,7 +2238,13 @@ export function apply(ctx) {
2238
2238
  }
2239
2239
  for (const [send, entry] of Array.from(sseStreams)) {
2240
2240
  try {
2241
- if (send(SSE_PING_FRAME) === false) entry.cleanup()
2241
+ // The RETURN VALUE is deliberately not read as a liveness signal:
2242
+ // res.write() answers false when THIS write exceeded the socket's
2243
+ // high-water mark, which a healthy but momentarily slow reader
2244
+ // produces too — killing on it tore down live streams. A dead peer
2245
+ // is detected the reliable way instead: the write fails, which fires
2246
+ // the close/error handler this stream has already registered.
2247
+ send(SSE_PING_FRAME)
2242
2248
  } catch {
2243
2249
  entry.cleanup()
2244
2250
  }
@@ -2268,12 +2274,10 @@ export function apply(ctx) {
2268
2274
  for (const [send, entry] of Array.from(sseStreams)) {
2269
2275
  try {
2270
2276
  if (entry.lite === true) {
2271
- // A false return means the socket buffer is full — this subscriber
2272
- // stopped reading. Drop it now rather than queue frames in memory.
2273
- if (send(litePayload) === false) entry.cleanup()
2277
+ send(litePayload)
2274
2278
  } else {
2275
2279
  if (fullPayload === null) fullPayload = 'data: ' + JSON.stringify(snapshot()) + '\n\n'
2276
- if (send(fullPayload) === false) entry.cleanup()
2280
+ send(fullPayload)
2277
2281
  }
2278
2282
  } catch {
2279
2283
  // The response is dead: run its full teardown (end + unsubscribe),
@@ -2449,6 +2453,28 @@ export function apply(ctx) {
2449
2453
 
2450
2454
  const storePath = resolveStorePath()
2451
2455
 
2456
+ /**
2457
+ * The rev a PARSED store carries, or 0 when it carries none usable.
2458
+ *
2459
+ * Defined BEFORE writeToDisk on purpose: the missing-primary recovery path
2460
+ * calls writeToDisk from the boot sequence, and a `const` declared later in
2461
+ * apply() would still be in its temporal dead zone if that write ever ran
2462
+ * before apply() reached the declaration. Ordering it here removes the
2463
+ * question instead of relying on the await that happens to come first today.
2464
+ */
2465
+ const diskRevFrom = (parsed) => {
2466
+ const rev = parsed !== null && parsed !== undefined && typeof parsed === 'object' ? parsed.rev : null
2467
+ return typeof rev === 'number' && Number.isFinite(rev) && rev > 0 ? Math.round(rev) : 0
2468
+ }
2469
+
2470
+ const readDiskRev = async () => {
2471
+ try {
2472
+ return diskRevFrom(await readStoreFile(storePath))
2473
+ } catch {
2474
+ return 0
2475
+ }
2476
+ }
2477
+
2452
2478
  /**
2453
2479
  * The exact object written to memory.json — and the one an export carries.
2454
2480
  *
@@ -3132,25 +3158,6 @@ export function apply(ctx) {
3132
3158
  * then silently misses the second save; and (b) deciding whether a save made
3133
3159
  * while another process holds the lock would clobber a newer store.
3134
3160
  */
3135
- /**
3136
- * The rev a PARSED store carries, or 0 when it carries none usable. Split out
3137
- * so a caller that already read the file — writeToDisk does, once, inside the
3138
- * lock — can derive the same number without a second read. Called at request
3139
- * time, long after apply() finished defining it.
3140
- */
3141
- const diskRevFrom = (parsed) => {
3142
- const rev = parsed !== null && parsed !== undefined && typeof parsed === 'object' ? parsed.rev : null
3143
- return typeof rev === 'number' && Number.isFinite(rev) && rev > 0 ? Math.round(rev) : 0
3144
- }
3145
-
3146
- const readDiskRev = async () => {
3147
- try {
3148
- return diskRevFrom(await readStoreFile(storePath))
3149
- } catch {
3150
- return 0
3151
- }
3152
- }
3153
-
3154
3161
  /**
3155
3162
  * Load the store from disk. Most branches only read; two of them also write —
3156
3163
  * the missing-primary recovery promotes the backup to memory.json, and the
@@ -5364,9 +5371,7 @@ export function apply(ctx) {
5364
5371
  // restarted) instead of hammering the route.
5365
5372
  res.write('retry: 5000\n\n')
5366
5373
  const lite = isLiteStreamRequest(req)
5367
- // The write() result is the backpressure signal that pushSnapshot and the
5368
- // heartbeat act on: false means the peer stopped reading this stream.
5369
- const send = (payload) => res.write(payload)
5374
+ const send = (payload) => { res.write(payload) }
5370
5375
  let closed = false
5371
5376
  const cleanup = () => {
5372
5377
  closed = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-instruction-memory",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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",