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/CHANGELOG.ja.md +21 -0
- package/CHANGELOG.ko.md +21 -0
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -21
- package/README.md +14 -5
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +16 -16
- package/lib/client.js.map +1 -1
- package/package.json +8 -8
- package/src/bridge.js +136 -136
- package/src/client/family-section.tsx +46 -46
- package/src/client/index.ts +135 -135
- package/src/client/pause-button-text.ts +60 -60
- package/src/client/pause-button.tsx +129 -129
- package/src/client/scope-face.ts +20 -20
- package/src/client/settings-card.tsx +391 -391
- package/src/client/styles.ts +30 -30
- package/src/detect.js +27 -27
- package/src/gate.js +93 -93
- package/src/pause-gate.js +30 -10
- package/src/pause-store.js +101 -101
- package/src/retry.js +1 -1
- package/src/scheduler.js +42 -42
- package/src/step-gate.js +399 -399
- package/src/store.js +77 -77
- package/src/tool-call-id.js +15 -11
- package/src/wiring.js +1 -1
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
|
+
}
|
package/src/tool-call-id.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-session-guard — `tool/result` 调用 id
|
|
2
|
+
* dsh-session-guard — `tool/result` 调用 id 的多形态读取(**零依赖**,可单测)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
* -
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
200
|
+
source: { kind: 'plugin:session-guard', form: 'notice' },
|
|
201
201
|
})
|
|
202
202
|
return true
|
|
203
203
|
} catch (e) {
|