dsh-selfnote 0.13.0 → 0.13.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/README.md +23 -0
- package/lib/index.js +71 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -252,6 +252,29 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
|
|
|
252
252
|
- **采纳 / 退役都要人点**:出现次数再多也不会自动采纳;退役的事实不会因为新候选出现就自己复活。
|
|
253
253
|
- **留痕**:每次改状态记下时间、从哪改的(`via: settings`)、前后状态(`history`),事后可查。
|
|
254
254
|
|
|
255
|
+
### 候选为空时,怎么知道它到底有没有在听
|
|
256
|
+
|
|
257
|
+
`GET /api/selfnote/capture` 是这块的诊断口(要带 `x-selfnote` 头,值见 `~/.dsh/selfnote/auth-token`):
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"since": "2026-11-04T07:31:02.114Z",
|
|
262
|
+
"events": 160,
|
|
263
|
+
"types": { "user/message": 12, "job/registered": 40, "turn/ended": 108 },
|
|
264
|
+
"reasons": { "written": 1, "no-candidate": 11, "not-user-authored": 148 },
|
|
265
|
+
"candidates": 1,
|
|
266
|
+
"written": 1,
|
|
267
|
+
"lastType": "turn/ended",
|
|
268
|
+
"trace": [{ "at": "…", "sid": "session-", "type": "user/message", "seq": 91, "reason": "no-candidate" }]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `types` 是事件类型直方图:`user/message` 在涨,说明**你的话真的进来了**;不涨才是链路断了。
|
|
273
|
+
- `reasons` 是每一条的最后落点:`written` 写出了候选、`no-candidate` 过了人审但没命中触发词、`not-user-authored` 不是人打的、`no-session` 没有会话 id、`error` 采集抛错。
|
|
274
|
+
- `trace` 保留最近 40 条,带时间、会话前缀、seq 和落点。
|
|
275
|
+
|
|
276
|
+
候选区空着有两种情况,这里能分开:**没在听**(`types` 里没有 `user/message`)和**在听但没话可采**(`no-candidate` 在涨)。这两个计数是 0.13.2 才补的 —— 上一版有两个计数器声明了从来没人加一,读出来永远 0,排查时反而把结论带偏。
|
|
277
|
+
|
|
255
278
|
## 注入预算
|
|
256
279
|
|
|
257
280
|
每回合五段,合计 800 字(自述占最大一份):
|
package/lib/index.js
CHANGED
|
@@ -81,16 +81,62 @@ export const name = 'selfnote'
|
|
|
81
81
|
/** 声明式依赖:systemPrompt 注入,tools 写入,webServer 提供设置页 API。 */
|
|
82
82
|
export const inject = ['systemPrompt', 'tools', 'webServer']
|
|
83
83
|
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* 自动采集的现场情况:只用于排查「事件到底有没有到、卡在哪一步」,不参与业务。
|
|
86
|
+
*
|
|
87
|
+
* 以前这里有两个 userAuthored / noSeq 计数器,声明了从来没人加一,读出来永远是 0 ——
|
|
88
|
+
* 排查时反而误导(看起来像「消息一条都没进来」)。现在换成两条真账:
|
|
89
|
+
* types(按事件类型计数)与 reasons(按落点计数),外加最近若干条轨迹。
|
|
90
|
+
*/
|
|
91
|
+
const CAPTURE_TRACE_MAX = 40
|
|
92
|
+
|
|
85
93
|
const captureDebug = {
|
|
94
|
+
since: null,
|
|
86
95
|
events: 0,
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
types: {},
|
|
97
|
+
reasons: {},
|
|
89
98
|
candidates: 0,
|
|
90
99
|
written: 0,
|
|
91
100
|
lastType: null,
|
|
92
101
|
lastKeys: null,
|
|
93
102
|
lastError: null,
|
|
103
|
+
trace: [],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function bumpCounter(bucket, key) {
|
|
107
|
+
if (typeof key !== 'string' || key === '') return
|
|
108
|
+
bucket[key] = (bucket[key] ?? 0) + 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 事件一进来先记账:类型直方图 + 最近几条。返回类型,供后面的落点记录复用。 */
|
|
112
|
+
function noteCaptureEvent(sessionId, event) {
|
|
113
|
+
captureDebug.events += 1
|
|
114
|
+
const type = typeof event?.type === 'string' && event.type !== '' ? event.type : 'unknown'
|
|
115
|
+
bumpCounter(captureDebug.types, type)
|
|
116
|
+
captureDebug.lastType = event?.type ?? null
|
|
117
|
+
captureDebug.lastKeys = event !== null && typeof event === 'object' ? Object.keys(event).slice(0, 12) : null
|
|
118
|
+
uploadTrace({
|
|
119
|
+
at: new Date().toISOString(),
|
|
120
|
+
sid: typeof sessionId === 'string' ? sessionId.slice(0, 8) : '',
|
|
121
|
+
type,
|
|
122
|
+
seq: Number.isSafeInteger(event?.seq) ? event.seq : null,
|
|
123
|
+
reason: null,
|
|
124
|
+
})
|
|
125
|
+
return type
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 这一轮采集最后落在哪:写进去了 / 没候选 / 不是人说的 / 没 seq …… */
|
|
129
|
+
function noteCaptureOutcome(reason) {
|
|
130
|
+
bumpCounter(captureDebug.reasons, reason)
|
|
131
|
+
const last = captureDebug.trace[captureDebug.trace.length - 1]
|
|
132
|
+
if (last !== undefined) last.reason = reason
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function uploadTrace(entry) {
|
|
136
|
+
captureDebug.trace.push(entry)
|
|
137
|
+
if (captureDebug.trace.length > CAPTURE_TRACE_MAX) {
|
|
138
|
+
captureDebug.trace.splice(0, captureDebug.trace.length - CAPTURE_TRACE_MAX)
|
|
139
|
+
}
|
|
94
140
|
}
|
|
95
141
|
|
|
96
142
|
const API_PREFIX = '/api/selfnote'
|
|
@@ -592,14 +638,23 @@ function trimSessionMap(map) {
|
|
|
592
638
|
while (map.size > MAX_SESSION_TRACKED) map.delete(map.keys().next().value)
|
|
593
639
|
}
|
|
594
640
|
|
|
641
|
+
/** 一个会话可能有两个 id(记录 id 与 header.id):两边都记,哪边先到都能命中。 */
|
|
642
|
+
function sessionKeysOf(session) {
|
|
643
|
+
const ids = [session?.id, session?.header?.id, session?.header?.sessionId]
|
|
644
|
+
return [...new Set(ids.filter((id) => typeof id === 'string' && id !== ''))]
|
|
645
|
+
}
|
|
646
|
+
|
|
595
647
|
/** 压缩事件到达时按会话打个标记:下一轮照这个会话用过的东西补一遍。 */
|
|
596
|
-
function noteCompaction(
|
|
648
|
+
function noteCompaction(session, event) {
|
|
597
649
|
const type = String(event?.type ?? '')
|
|
598
650
|
if (!COMPACTION_EVENTS.has(type)) return
|
|
599
651
|
// 压缩失败/中止时窗口里还有原文,补了是重复
|
|
600
652
|
const data = event?.data
|
|
601
653
|
if (data !== null && typeof data === 'object' && 'error' in data && data.error !== undefined) return
|
|
602
|
-
|
|
654
|
+
const keys = sessionKeysOf(session)
|
|
655
|
+
if (keys.length === 0) return
|
|
656
|
+
const ids = [...new Set(keys.flatMap((key) => lastInjectedBySession.get(key) ?? []))]
|
|
657
|
+
for (const key of keys) compactionPending.set(key, { at: Date.now(), ids })
|
|
603
658
|
trimSessionMap(compactionPending)
|
|
604
659
|
}
|
|
605
660
|
|
|
@@ -2200,14 +2255,16 @@ export function apply(ctx, config) {
|
|
|
2200
2255
|
/* ---- 自动采集(P1-B):只发现,不确认 ---- */
|
|
2201
2256
|
// session/event 是 fire-and-forget:这里绝不能把异常抛回宿主,也不能阻塞对话
|
|
2202
2257
|
if (typeof ctx?.on === 'function') {
|
|
2258
|
+
captureDebug.since = new Date().toISOString()
|
|
2203
2259
|
const offCapture = ctx.on('session/event', (session, event) => {
|
|
2204
|
-
|
|
2205
|
-
captureDebug.lastType = event?.type ?? null
|
|
2206
|
-
captureDebug.lastKeys = event !== null && typeof event === 'object' ? Object.keys(event).slice(0, 12) : null
|
|
2260
|
+
const type = noteCaptureEvent(session?.id, event)
|
|
2207
2261
|
const sessionId = session?.id
|
|
2208
|
-
if (typeof sessionId !== 'string' || sessionId === '')
|
|
2262
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
2263
|
+
noteCaptureOutcome('no-session')
|
|
2264
|
+
return
|
|
2265
|
+
}
|
|
2209
2266
|
// 压缩发生了就记一笔:下一轮把这个会话用过的记忆再贴一遍
|
|
2210
|
-
noteCompaction(
|
|
2267
|
+
noteCompaction(session, event)
|
|
2211
2268
|
// 同一会话按到达顺序串行,避免 seq=11 先完成把游标推到 11、把 seq=10 挤掉
|
|
2212
2269
|
enqueue(sessionId, () =>
|
|
2213
2270
|
handleEvent(event, {
|
|
@@ -2220,17 +2277,20 @@ export function apply(ctx, config) {
|
|
|
2220
2277
|
)
|
|
2221
2278
|
.then((result) => {
|
|
2222
2279
|
captureDebug.candidates += result?.plan?.created?.length ?? 0
|
|
2223
|
-
|
|
2280
|
+
const written = result?.handled === true
|
|
2281
|
+
if (written) {
|
|
2224
2282
|
captureDebug.written += 1
|
|
2225
2283
|
const plan = result.plan ?? {}
|
|
2226
2284
|
ctx.logger?.info?.(
|
|
2227
2285
|
`selfnote: 采集到 ${plan.created?.length ?? 0} 条候选(合并 ${plan.merged?.length ?? 0},冲突 ${plan.conflicts?.length ?? 0})`,
|
|
2228
2286
|
)
|
|
2229
2287
|
}
|
|
2288
|
+
noteCaptureOutcome(written ? 'written' : String(result?.reason ?? type + ':unknown'))
|
|
2230
2289
|
})
|
|
2231
2290
|
.catch((error) => {
|
|
2232
2291
|
// 采集坏掉不许影响对话
|
|
2233
2292
|
captureDebug.lastError = String(error?.message ?? error)
|
|
2293
|
+
noteCaptureOutcome('error')
|
|
2234
2294
|
ctx.logger?.warn?.('selfnote: 自动采集失败 ' + String(error?.message ?? error))
|
|
2235
2295
|
})
|
|
2236
2296
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfnote",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Self-narrative memory for DeepSeek Harness: the model writes notes to itself, which persist across sessions and are injected back. | 人格库:模型写给自己看的话,跨会话保留并每回合注入。",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"assets"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node tests/smoke.mjs && node tests/memory.mjs && node tests/roles.mjs && node tests/heat.mjs && node tests/fair.mjs && node tests/facts-tx.mjs && node tests/crash-write.mjs && node tests/capture.mjs && node tests/capture-p1b.mjs && node tests/index.mjs && node tests/incremental.mjs && node tests/profile.mjs && node tests/warm.mjs && node tests/transfer.mjs && node tests/api.mjs && node tests/client-state.mjs && node tests/client.mjs",
|
|
30
|
+
"test": "node tests/smoke.mjs && node tests/memory.mjs && node tests/roles.mjs && node tests/heat.mjs && node tests/fair.mjs && node tests/capture-debug.mjs && node tests/facts-tx.mjs && node tests/crash-write.mjs && node tests/capture.mjs && node tests/capture-p1b.mjs && node tests/index.mjs && node tests/incremental.mjs && node tests/profile.mjs && node tests/warm.mjs && node tests/transfer.mjs && node tests/api.mjs && node tests/client-state.mjs && node tests/client.mjs",
|
|
31
31
|
"test:accept": "node tests/acceptance-runtime.mjs"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|