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/README.md +24 -17
- package/lib/atomic.js +38 -1
- package/lib/capture-run.js +7 -4
- package/lib/capture.js +2 -1
- package/lib/client.js +63 -38
- package/lib/corrections.js +15 -1
- package/lib/explain.js +77 -0
- package/lib/index-store.js +4 -4
- package/lib/index.js +125 -50
- package/lib/memory.js +10 -8
- package/lib/persona-tx.js +5 -14
- package/lib/persona.js +7 -1
- package/lib/profile.js +5 -3
- package/lib/store.js +14 -4
- package/lib/transfer.js +26 -7
- package/lib/warm.js +16 -2
- package/package.json +2 -2
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
|
|
56
|
+
if (countChars(body) > pieceMax) continue
|
|
55
57
|
const piece = `${tag}:${body}`
|
|
56
58
|
const candidate = text === '' ? piece : `${text};${piece}`
|
|
57
|
-
if (candidate
|
|
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
|
|
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()
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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.
|
|
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": {
|