dsh-session-guard 3.2.3 → 4.0.0

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/src/store.js CHANGED
@@ -1,77 +1,77 @@
1
- /**
2
- * dsh-session-guard — 每会话持久化状态存储(学 dsh-task-control 的独立存储模式,D4)。
3
- *
4
- * 暂停/锁定状态不写 session log(harness 持久化 reader 只认已知事件类型),
5
- * 改为插件自有 JSON 文件:`$DSH_HOME/.dsh/session-guard/<sessionId>.json`
6
- * (可用 DSH_SESSION_GUARD_STATE_DIR 覆盖根目录)。原子写:tmp + rename。
7
- */
8
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
9
- import { dirname, join } from 'node:path'
10
-
11
- /** 状态根目录。 */
12
- export function storeRoot() {
13
- return (
14
- process.env.DSH_SESSION_GUARD_STATE_DIR ||
15
- join(process.env.DSH_HOME || process.env.HOME || process.cwd(), '.dsh', 'session-guard')
16
- )
17
- }
18
-
19
- /** 会话 id → 安全文件名。 */
20
- function encodeSessionId(id) {
21
- return String(id).replace(/[^a-zA-Z0-9._-]/g, '_')
22
- }
23
-
24
- /** 每会话状态文件路径。 */
25
- export function stateFilePath(id) {
26
- return join(storeRoot(), `${encodeSessionId(id)}.json`)
27
- }
28
-
29
- /** 未锁定基线。 */
30
- export function idleState(id) {
31
- return { sessionId: String(id), queueLocked: false, lockReason: null, updatedAt: null }
32
- }
33
-
34
- /**
35
- * 创建存储(内存缓存 + 磁盘持久化)。
36
- */
37
- export function createStore() {
38
- const cache = new Map()
39
- return {
40
- /** 读一会话状态;无记录返回 null(调用方自行 fallback idleState)。 */
41
- get(id) {
42
- if (cache.has(id)) return cache.get(id)
43
- try {
44
- const f = stateFilePath(id)
45
- if (!existsSync(f)) return null
46
- const v = JSON.parse(readFileSync(f, 'utf8'))
47
- cache.set(id, v)
48
- return v
49
- } catch {
50
- return null
51
- }
52
- },
53
- /** 原子写。 */
54
- set(id, value) {
55
- cache.set(id, value)
56
- try {
57
- const f = stateFilePath(id)
58
- mkdirSync(dirname(f), { recursive: true })
59
- const tmp = `${f}.tmp`
60
- writeFileSync(tmp, JSON.stringify(value, null, 2))
61
- renameSync(tmp, f)
62
- } catch (e) {
63
- // 落盘失败仅影响重启恢复,内存态仍可用。
64
- console.error(`[session-guard] store write failed: ${String(e && e.message || e)}`)
65
- }
66
- },
67
- /** 清除(解锁时)。 */
68
- clear(id) {
69
- cache.delete(id)
70
- try {
71
- rmSync(stateFilePath(id), { force: true })
72
- } catch {
73
- /* ignore */
74
- }
75
- },
76
- }
77
- }
1
+ /**
2
+ * dsh-session-guard — 每会话持久化状态存储(学 dsh-task-control 的独立存储模式,D4)。
3
+ *
4
+ * 暂停/锁定状态不写 session log(harness 持久化 reader 只认已知事件类型),
5
+ * 改为插件自有 JSON 文件:`$DSH_HOME/.dsh/session-guard/<sessionId>.json`
6
+ * (可用 DSH_SESSION_GUARD_STATE_DIR 覆盖根目录)。原子写:tmp + rename。
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
9
+ import { dirname, join } from 'node:path'
10
+
11
+ /** 状态根目录。 */
12
+ export function storeRoot() {
13
+ return (
14
+ process.env.DSH_SESSION_GUARD_STATE_DIR ||
15
+ join(process.env.DSH_HOME || process.env.HOME || process.cwd(), '.dsh', 'session-guard')
16
+ )
17
+ }
18
+
19
+ /** 会话 id → 安全文件名。 */
20
+ function encodeSessionId(id) {
21
+ return String(id).replace(/[^a-zA-Z0-9._-]/g, '_')
22
+ }
23
+
24
+ /** 每会话状态文件路径。 */
25
+ export function stateFilePath(id) {
26
+ return join(storeRoot(), `${encodeSessionId(id)}.json`)
27
+ }
28
+
29
+ /** 未锁定基线。 */
30
+ export function idleState(id) {
31
+ return { sessionId: String(id), queueLocked: false, lockReason: null, updatedAt: null }
32
+ }
33
+
34
+ /**
35
+ * 创建存储(内存缓存 + 磁盘持久化)。
36
+ */
37
+ export function createStore() {
38
+ const cache = new Map()
39
+ return {
40
+ /** 读一会话状态;无记录返回 null(调用方自行 fallback idleState)。 */
41
+ get(id) {
42
+ if (cache.has(id)) return cache.get(id)
43
+ try {
44
+ const f = stateFilePath(id)
45
+ if (!existsSync(f)) return null
46
+ const v = JSON.parse(readFileSync(f, 'utf8'))
47
+ cache.set(id, v)
48
+ return v
49
+ } catch {
50
+ return null
51
+ }
52
+ },
53
+ /** 原子写。 */
54
+ set(id, value) {
55
+ cache.set(id, value)
56
+ try {
57
+ const f = stateFilePath(id)
58
+ mkdirSync(dirname(f), { recursive: true })
59
+ const tmp = `${f}.tmp`
60
+ writeFileSync(tmp, JSON.stringify(value, null, 2))
61
+ renameSync(tmp, f)
62
+ } catch (e) {
63
+ // 落盘失败仅影响重启恢复,内存态仍可用。
64
+ console.error(`[session-guard] store write failed: ${String(e && e.message || e)}`)
65
+ }
66
+ },
67
+ /** 清除(解锁时)。 */
68
+ clear(id) {
69
+ cache.delete(id)
70
+ try {
71
+ rmSync(stateFilePath(id), { force: true })
72
+ } catch {
73
+ /* ignore */
74
+ }
75
+ },
76
+ }
77
+ }
@@ -1,17 +1,19 @@
1
1
  /**
2
- * dsh-session-guard — `tool/result` 调用 id 的双形态读取(**零依赖**,可单测)。
2
+ * dsh-session-guard — `tool/result` 调用 id 的多形态读取(**零依赖**,可单测)。
3
3
  *
4
- * 为什么需要它:DSH 0.1.1-rc.2 与 0.1.2-rc.1 的 `session/event` 形状一致
5
- * (已逐行核对 `packages/session/session-persistence/src/coordinator.ts` 的
6
- * `tool/result` 分支,两版本都保留两种形态),但**同一条记录有两种历史形态**:
4
+ * 形状代际(2026-09-27 v4 适配订正;engines.dsh ≥0.1.7-rc.1):
7
5
  *
8
- * - 现行形态:`event.data.message.content[].toolCallId`(`tool-result` 块);
9
- * - 遗留形态:`event.data.message.source.callId`(旧记录经持久化协调器迁移后的
10
- * 形态,两版本的回放日志里都可能出现)。
6
+ * - **v4 native(现行,必读)**:`event.data.message` 为一等 `role:'tool'` 消息,
7
+ * `toolCallId` 在 message 顶层(`ToolResultMessage`,0.1.7-rc.2 dsh-llm
8
+ * message.d.ts 实证)。v4 解码 native-only,`type:'tool-result'` 块被宿主
9
+ * `assertBlock` 硬拒——**本文件的第一读取顺位**。
10
+ * - v3 wrapper(历史兼容):`event.data.message.content[].toolCallId`
11
+ * (`tool-result` 块)。未迁移的 v3 日志读侧仍可见。
12
+ * - 遗留形态:`event.data.message.source.callId`(旧记录经持久化协调器迁移)。
11
13
  *
12
- * 读取顺序:优先块上的 `toolCallId`(更精确),缺失才回退 `source.callId`。
13
- * **不要改成单读**——只读块会在旧记录上丢 id(in-flight 工具无法落地),
14
- * 只读 `source.callId` 会在新记录上丢 id(暂停点判定失效)。
14
+ * 读取顺序:native 顶层 `toolCallId` → 块上 `toolCallId` → `source.callId`。
15
+ * **不要改成单读**——只读一层会在其它代际的记录上丢 id(in-flight 工具无法落地
16
+ * / 暂停点判定失效)。
15
17
  *
16
18
  * 本文件刻意不导入任何 `@deepseek-ai/*` 包,因此可在没有 DSH 运行时依赖的
17
19
  * 环境下直接单测(见 `tests/tool-call-id.test.mjs`)。
@@ -20,10 +22,12 @@
20
22
  /**
21
23
  * 读一条 `tool/result` 记录(`event.data.message`)的调用 id。
22
24
  * @param {unknown} message - `event.data.message`(可能缺失或非对象)
23
- * @returns {string|undefined} 调用 id;两种形态都缺时返回 undefined
25
+ * @returns {string|undefined} 调用 id;各形态都缺时返回 undefined
24
26
  */
25
27
  export function readToolResultCallId(message) {
26
28
  if (message === null || typeof message !== 'object') return undefined
29
+ // v4 native:一等 tool-role 消息,id 在顶层。
30
+ if (typeof message.toolCallId === 'string') return message.toolCallId
27
31
  const content = Array.isArray(message.content) ? message.content : []
28
32
  for (const block of content) {
29
33
  if (block?.type === 'tool-result' && typeof block.toolCallId === 'string') return block.toolCallId
package/src/wiring.js CHANGED
@@ -197,7 +197,7 @@ export function createWiring({
197
197
  id: `session-guard-deferred-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
198
198
  role: 'user',
199
199
  content: [{ type: 'text', text }],
200
- source: { kind: 'plugin', plugin: 'session-guard', form: 'notice' },
200
+ source: { kind: 'plugin:session-guard', form: 'notice' },
201
201
  })
202
202
  return true
203
203
  } catch (e) {