dsh-selfnote 0.13.1 → 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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/lib/index.js +59 -8
  3. 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
- userAuthored: 0,
88
- noSeq: 0,
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'
@@ -2209,12 +2255,14 @@ export function apply(ctx, config) {
2209
2255
  /* ---- 自动采集(P1-B):只发现,不确认 ---- */
2210
2256
  // session/event 是 fire-and-forget:这里绝不能把异常抛回宿主,也不能阻塞对话
2211
2257
  if (typeof ctx?.on === 'function') {
2258
+ captureDebug.since = new Date().toISOString()
2212
2259
  const offCapture = ctx.on('session/event', (session, event) => {
2213
- captureDebug.events += 1
2214
- captureDebug.lastType = event?.type ?? null
2215
- captureDebug.lastKeys = event !== null && typeof event === 'object' ? Object.keys(event).slice(0, 12) : null
2260
+ const type = noteCaptureEvent(session?.id, event)
2216
2261
  const sessionId = session?.id
2217
- if (typeof sessionId !== 'string' || sessionId === '') return
2262
+ if (typeof sessionId !== 'string' || sessionId === '') {
2263
+ noteCaptureOutcome('no-session')
2264
+ return
2265
+ }
2218
2266
  // 压缩发生了就记一笔:下一轮把这个会话用过的记忆再贴一遍
2219
2267
  noteCompaction(session, event)
2220
2268
  // 同一会话按到达顺序串行,避免 seq=11 先完成把游标推到 11、把 seq=10 挤掉
@@ -2229,17 +2277,20 @@ export function apply(ctx, config) {
2229
2277
  )
2230
2278
  .then((result) => {
2231
2279
  captureDebug.candidates += result?.plan?.created?.length ?? 0
2232
- if (result?.handled === true) {
2280
+ const written = result?.handled === true
2281
+ if (written) {
2233
2282
  captureDebug.written += 1
2234
2283
  const plan = result.plan ?? {}
2235
2284
  ctx.logger?.info?.(
2236
2285
  `selfnote: 采集到 ${plan.created?.length ?? 0} 条候选(合并 ${plan.merged?.length ?? 0},冲突 ${plan.conflicts?.length ?? 0})`,
2237
2286
  )
2238
2287
  }
2288
+ noteCaptureOutcome(written ? 'written' : String(result?.reason ?? type + ':unknown'))
2239
2289
  })
2240
2290
  .catch((error) => {
2241
2291
  // 采集坏掉不许影响对话
2242
2292
  captureDebug.lastError = String(error?.message ?? error)
2293
+ noteCaptureOutcome('error')
2243
2294
  ctx.logger?.warn?.('selfnote: 自动采集失败 ' + String(error?.message ?? error))
2244
2295
  })
2245
2296
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfnote",
3
- "version": "0.13.1",
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": {