dsh-selfnote 0.14.1 → 0.14.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/lib/profile.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * - 同一标签下多条时不擅自归纳,只取最新并标注「另有 N 条」
9
9
  */
10
10
 
11
+ import { countChars } from './store.js'
12
+
11
13
  const DEFAULT_MAX_CHARS = 80
12
14
  const DEFAULT_PIECE_MAX = 60
13
15
 
@@ -51,10 +53,10 @@ export function buildProfile(facts, options = {}) {
51
53
  const latest = bucket.slice().sort((a, b) => (stamp(a) < stamp(b) ? 1 : stamp(a) > stamp(b) ? -1 : 0))[0]
52
54
  const body = String(latest.text ?? '').trim()
53
55
  if (body === '') continue
54
- if (body.length > pieceMax) continue
56
+ if (countChars(body) > pieceMax) continue
55
57
  const piece = `${tag}:${body}`
56
58
  const candidate = text === '' ? piece : `${text};${piece}`
57
- if (candidate.length > maxChars) continue
59
+ if (countChars(candidate) > maxChars) continue
58
60
  text = candidate
59
61
  parts.push({ tag, factId: latest.id, text: body })
60
62
  if (bucket.length > 1) conflicts.push({ tag, count: bucket.length - 1 })
@@ -64,7 +66,7 @@ export function buildProfile(facts, options = {}) {
64
66
  text,
65
67
  parts,
66
68
  conflicts,
67
- chars: text.length,
69
+ chars: countChars(text),
68
70
  sources: parts.map((part) => part.factId),
69
71
  }
70
72
  }
package/lib/store.js CHANGED
@@ -15,6 +15,16 @@ import { join } from 'node:path'
15
15
  import { writeAtomic, writeJsonAtomic } from './atomic.js'
16
16
 
17
17
  /** 当前生效内容的上限(字符)。默认 1600 字,约 1.1k token。 */
18
+ export function countChars(text) {
19
+ return [...String(text ?? '')].length
20
+ }
21
+
22
+ /** 按码点截断:不把 emoji / 增补汉字的代理对切一半。 */
23
+ export function truncateChars(text, max) {
24
+ const room = Math.max(0, Math.floor(max))
25
+ return [...String(text ?? '')].slice(0, room).join('')
26
+ }
27
+
18
28
  export const DEFAULT_MAX_CHARS = 1600
19
29
  /** 条数上限,和字符上限共同生效。 */
20
30
  export const DEFAULT_MAX_ENTRIES = 40
@@ -94,7 +104,7 @@ export function today() {
94
104
 
95
105
  /** 正文归一:回车换行压成空格、去掉首尾空白、截到硬上限。 */
96
106
  function cleanEntryText(text) {
97
- return String(text ?? '').replace(/\s+/g, ' ').trim().slice(0, MAX_ENTRY_CHARS)
107
+ return truncateChars(String(text ?? '').replace(/\s+/g, ' ').trim(), MAX_ENTRY_CHARS)
98
108
  }
99
109
 
100
110
  /**
@@ -166,12 +176,12 @@ export function writeNotes(entries) {
166
176
  export function trim(entries, { maxChars = DEFAULT_MAX_CHARS, maxEntries = DEFAULT_MAX_ENTRIES } = {}) {
167
177
  const kept = [...entries]
168
178
  const overflow = []
169
- let total = renderNotes(kept).length
179
+ let total = countChars(renderNotes(kept))
170
180
  while (kept.length > maxEntries || (total > maxChars && kept.length > 1)) {
171
181
  const dropped = kept.shift()
172
182
  if (!dropped) break
173
183
  overflow.push(dropped)
174
- total = renderNotes(kept).length
184
+ total = countChars(renderNotes(kept))
175
185
  }
176
186
  return { kept, overflow }
177
187
  }
@@ -290,7 +300,7 @@ export function stats() {
290
300
  }
291
301
  return {
292
302
  entries: entries.length,
293
- chars: renderNotes(entries).length,
303
+ chars: countChars(renderNotes(entries)),
294
304
  // 三型各几条:设置页要能解释「为什么我这条没进上下文」
295
305
  tiers: countTiers(entries),
296
306
  pinned: entries.filter((entry) => entry.pin === true).length,
package/lib/transfer.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * 索引不导出:它记的是磁盘路径与 mtime,搬过去必然过期,导入后重建一次就行。
10
10
  */
11
11
 
12
+ import { normalizeTier } from './store.js'
13
+
12
14
  export const BUNDLE_VERSION = 2
13
15
  export const BUNDLE_APP = 'dsh-selfnote'
14
16
 
@@ -55,7 +57,7 @@ export function buildBundle({ notes = [], facts = [], habits = [], config = {},
55
57
  v: BUNDLE_VERSION,
56
58
  exportedAt: now.toISOString(),
57
59
  counts: { notes: notes.length, facts: facts.length, habits: habits.length },
58
- notes: notes.map((entry) => ({ date: entry.date ?? '', text: entry.text ?? '' })),
60
+ notes: notes.map((entry) => ({ date: entry.date ?? '', text: entry.text ?? '', tier: normalizeTier(entry.tier), pin: entry.pin === true })),
59
61
  facts: facts.map((fact) => ({ ...fact })),
60
62
  habits: habits.map((habit) => ({ ...habit })),
61
63
  // 人格跟着走(v2);纠偏候选不跟 —— 它是「这台机器上攒下的证据」,搬过去就失了上下文
@@ -65,6 +67,26 @@ export function buildBundle({ notes = [], facts = [], habits = [], config = {},
65
67
  }
66
68
 
67
69
  /** 导入前的体检:结构不对就直说,别合并一半。 */
70
+ export function mergeConfig(bundle, current = {}) {
71
+ const source = isObject(bundle?.config) ? bundle.config : {}
72
+ const base = isObject(current) ? current : {}
73
+ const config = {}
74
+ for (const field of CONFIG_FIELDS) if (base[field] !== undefined) config[field] = base[field]
75
+ const applied = []
76
+ const bools = new Set(['enabled', 'profileEnabled', 'personaEnabled'])
77
+ const nums = new Set(['maxChars', 'maxEntries', 'profileMaxChars', 'halfLifeDays', 'tempTtlDays'])
78
+ for (const field of CONFIG_FIELDS) {
79
+ if (!Object.hasOwn(source, field)) continue
80
+ const value = source[field]
81
+ const good = bools.has(field) ? typeof value === 'boolean'
82
+ : nums.has(field) ? typeof value === 'number' && Number.isFinite(value)
83
+ : typeof value === 'string'
84
+ if (!good) continue
85
+ config[field] = value
86
+ applied.push(field)
87
+ }
88
+ return { config, applied }
89
+ }
68
90
  export function validateBundle(bundle) {
69
91
  if (!isObject(bundle)) return '不是 JSON 对象'
70
92
  if (bundle.app !== BUNDLE_APP) return `不是本插件导出的文件(app=${String(bundle.app)})`
@@ -138,11 +160,8 @@ export function mergeBundle(bundle, current = {}, options = {}) {
138
160
  continue
139
161
  }
140
162
  const status = textOf(raw.status) || 'candidate'
141
- const fact = { ...raw, text }
142
- if (!keepStatus && status !== 'candidate') {
143
- fact.status = 'candidate'
144
- demoted.push(id)
145
- }
163
+ const fact = { ...raw, text, status: keepStatus ? status : 'candidate' }
164
+ if (!keepStatus && status !== 'candidate') demoted.push(id)
146
165
  if (fact.status === 'candidate') {
147
166
  delete fact.reviewedAt
148
167
  delete fact.retiredAt
@@ -164,7 +183,7 @@ export function mergeBundle(bundle, current = {}, options = {}) {
164
183
  skipped.notes += 1
165
184
  continue
166
185
  }
167
- notes.push({ date: textOf(raw.date) || '', text })
186
+ notes.push({ date: textOf(raw.date) || '', text, tier: normalizeTier(raw.tier), pin: raw.pin === true })
168
187
  seenNotes.add(text)
169
188
  added.notes += 1
170
189
  }
package/lib/warm.js CHANGED
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs'
12
12
  import { join } from 'node:path'
13
13
 
14
14
  import { memoryRoot, readFacts, tokenize } from './memory.js'
15
+ import { countChars } from './store.js'
15
16
  import { writeAtomic } from './atomic.js'
16
17
 
17
18
  const MAX_TEXT = 300
@@ -19,6 +20,16 @@ const MAX_POINTERS = 8
19
20
 
20
21
  export const warmPath = () => join(memoryRoot(), 'warm.jsonl')
21
22
 
23
+ let pendingBadRows = []
24
+ function collectBadRows(rows) {
25
+ for (const row of rows) if (!pendingBadRows.includes(row)) pendingBadRows.push(row)
26
+ }
27
+ function flushBadRows() {
28
+ if (pendingBadRows.length === 0) return
29
+ mkdirSync(memoryRoot(), { recursive: true })
30
+ writeAtomic(join(memoryRoot(), `warm.bad-${Date.now().toString(36)}-${process.pid}.jsonl`), pendingBadRows.join('\n') + '\n')
31
+ pendingBadRows = []
32
+ }
22
33
  export function readWarm() {
23
34
  const file = warmPath()
24
35
  if (!existsSync(file)) return []
@@ -28,8 +39,10 @@ export function readWarm() {
28
39
  try {
29
40
  const item = JSON.parse(line)
30
41
  if (item && typeof item === 'object') out.push(item)
42
+ else collectBadRows([line])
31
43
  } catch {
32
- // 坏行跳过:一条摘要读不出来,不该让整层都废掉
44
+ // 坏行跳过:隔离留存,别在下一次写回时无声抹掉
45
+ collectBadRows([line])
33
46
  }
34
47
  }
35
48
  return out
@@ -39,6 +52,7 @@ function writeWarm(items) {
39
52
  // 目录可能还不存在(第一次写温层)
40
53
  mkdirSync(memoryRoot(), { recursive: true })
41
54
  writeAtomic(warmPath(), items.map((item) => JSON.stringify(item)).join('\n') + (items.length > 0 ? '\n' : ''))
55
+ flushBadRows()
42
56
  }
43
57
 
44
58
  function validPointer(pointer) {
@@ -55,7 +69,7 @@ function validPointer(pointer) {
55
69
  export function writeSummary(input = {}) {
56
70
  const text = String(input.text ?? '').trim()
57
71
  if (text === '') return { ok: false, error: 'text 不能为空' }
58
- if (text.length > MAX_TEXT) return { ok: false, error: `一条摘要最多 ${MAX_TEXT} 字,拆成两条` }
72
+ if (countChars(text) > MAX_TEXT) return { ok: false, error: `一条摘要最多 ${MAX_TEXT} 字,拆成两条` }
59
73
 
60
74
  const raw = Array.isArray(input.pointers) ? input.pointers : []
61
75
  const pointers = raw
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfnote",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
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/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/notes-tier.mjs && node tests/persona.mjs && node tests/corrections.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/explain.mjs && node tests/skipped.mjs && node tests/injection-count.mjs && node tests/notes-tier.mjs && node tests/capacity-warning.mjs && node tests/atomic-retry.mjs && node tests/unicode-rulers.mjs && node tests/transfer-roundtrip.mjs && node tests/r3-regressions.mjs && node tests/persona.mjs && node tests/corrections.mjs && node tests/client-state.mjs && node tests/client.mjs",
31
31
  "test:accept": "node tests/acceptance-runtime.mjs"
32
32
  },
33
33
  "engines": {