dsh-memu-memory 1.2.0 → 1.2.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/AGENTS.md +1 -1
- package/README.md +19 -8
- package/README.zh.md +12 -6
- package/docs/contributing.md +16 -3
- package/docs/env-and-commands.md +8 -0
- package/docs/host-api.md +3 -1
- package/docs/memory-semantics.md +21 -2
- package/docs/troubleshooting.md +42 -3
- package/lib/activity-log.js +22 -5
- package/lib/backup.js +24 -2
- package/lib/binding-diagnostics.js +3 -3
- package/lib/client.js +1610 -117
- package/lib/consolidation-worker.js +68 -17
- package/lib/curator-evidence.js +131 -0
- package/lib/curator-policy.js +81 -0
- package/lib/curator.js +1299 -83
- package/lib/cycle.js +3 -0
- package/lib/embedding-download.js +39 -4
- package/lib/engine.js +6 -3
- package/lib/global-graph.js +282 -0
- package/lib/graph.js +190 -14
- package/lib/inbound.js +19 -0
- package/lib/index.js +33 -1
- package/lib/inject.js +277 -17
- package/lib/lexicon.js +1115 -0
- package/lib/local-vector-service.js +8 -6
- package/lib/managed-runtime.js +33 -2
- package/lib/memory.js +585 -76
- package/lib/memu.js +2 -2
- package/lib/migrate.js +83 -2
- package/lib/private-links.js +587 -0
- package/lib/private.js +128 -7
- package/lib/prompts/global-curator.md +76 -19
- package/lib/prompts/private-consolidation.coding.md +4 -1
- package/lib/prompts/private-consolidation.md +3 -1
- package/lib/prompts/private-consolidation.mixed.md +3 -1
- package/lib/prompts/private-consolidation.rp.md +3 -1
- package/lib/prompts.js +1 -1
- package/lib/rpc.js +622 -13
- package/lib/state.js +4 -1
- package/lib/text-retrieval.js +661 -0
- package/lib/text-similarity.js +124 -0
- package/lib/tools.js +230 -16
- package/lib/vector-migration.js +28 -3
- package/lib/vector-profile.js +24 -0
- package/lib/webui/standalone.css +17 -0
- package/lib/webui/standalone.js +2 -0
- package/package.json +3 -2
- package/scripts/acceptance-local.mjs +8 -5
- package/scripts/curator-pair-calibrate.mjs +259 -0
- package/scripts/lexicon-calibrate.mjs +230 -0
- package/scripts/lexicon-dedupe.mjs +318 -0
- package/scripts/replay.mjs +573 -0
- package/tests/absorb-review.test.mjs +353 -0
- package/tests/appendix-b.test.mjs +6 -2
- package/tests/appendix-b2.test.mjs +2 -2
- package/tests/client-export.test.mjs +520 -0
- package/tests/client-render.test.mjs +19 -3
- package/tests/consolidation-credential.test.mjs +16 -5
- package/tests/consolidation-feed.test.mjs +96 -0
- package/tests/consolidation-fork.test.mjs +144 -10
- package/tests/consolidation-snapshot.test.mjs +82 -4
- package/tests/curator-absorb.test.mjs +443 -0
- package/tests/curator-calibrate.test.mjs +173 -0
- package/tests/curator-clusters.test.mjs +281 -0
- package/tests/curator-commit.test.mjs +324 -0
- package/tests/curator-evidence.test.mjs +102 -0
- package/tests/curator-link.test.mjs +316 -0
- package/tests/curator-pairing.test.mjs +252 -0
- package/tests/curator-probe.test.mjs +15 -6
- package/tests/curator-review-mode.test.mjs +314 -0
- package/tests/disclosure.test.mjs +2 -1
- package/tests/fixtures/curator-pairs/labeled.v1.jsonl +90 -0
- package/tests/fixtures/lexicon-stub-hooks.mjs +15 -0
- package/tests/fixtures/lexicon-stub.mjs +284 -0
- package/tests/graph-cascade.test.mjs +223 -0
- package/tests/graph.test.mjs +113 -0
- package/tests/help-page.test.mjs +415 -0
- package/tests/hybrid-recall.test.mjs +32 -2
- package/tests/inbound.test.mjs +6 -3
- package/tests/inject-guide.test.mjs +28 -0
- package/tests/inject.test.mjs +157 -5
- package/tests/lexicon-calibrate.test.mjs +48 -0
- package/tests/lexicon-dedupe.test.mjs +171 -0
- package/tests/lexicon-frequency.test.mjs +179 -0
- package/tests/lexicon-inject.test.mjs +410 -0
- package/tests/lexicon-perf.test.mjs +436 -0
- package/tests/lexicon-ui.test.mjs +547 -0
- package/tests/lexicon.test.mjs +922 -0
- package/tests/managed-memory.test.mjs +55 -0
- package/tests/managed-runtime.test.mjs +32 -0
- package/tests/private-links.test.mjs +445 -0
- package/tests/prompts.test.mjs +35 -0
- package/tests/recall-dedup.test.mjs +446 -0
- package/tests/sidebar-beat.test.mjs +5 -1
- package/tests/tool-schema.test.mjs +1 -1
- package/tests/tools.test.mjs +112 -3
- package/tests/vector-migration.test.mjs +58 -1
- package/tests/webui-auth.test.mjs +348 -2
- package/tests/webui.test.mjs +191 -2
package/lib/private.js
CHANGED
|
@@ -181,8 +181,14 @@ export async function requestRemoteConsolidation(ctx, sessionId, { signal, timeo
|
|
|
181
181
|
signal,
|
|
182
182
|
timeoutMs,
|
|
183
183
|
})
|
|
184
|
+
// 写入计数必须说真话:`memories` 是模型给的清单,逐条 privateRemember 可能被
|
|
185
|
+
// 事实级去重拒绝(deduped,不落库)或因矛盾暂扣(pending_review,仅入索引)。
|
|
186
|
+
// count 只数真正落库的;被拒/暂扣分列进 detail,否则"写入 N 条"就是误报。
|
|
187
|
+
let written = 0
|
|
188
|
+
let deduped = 0
|
|
189
|
+
let held = 0
|
|
184
190
|
for (const memory of memories) {
|
|
185
|
-
await privateRemember({
|
|
191
|
+
const outcome = await privateRemember({
|
|
186
192
|
...memory,
|
|
187
193
|
exec: { agent: { session: { header: { id: sessionId } } } },
|
|
188
194
|
content: memory.content,
|
|
@@ -192,12 +198,17 @@ export async function requestRemoteConsolidation(ctx, sessionId, { signal, timeo
|
|
|
192
198
|
confidence: memory.confidence,
|
|
193
199
|
source: memory.source || 'inferred',
|
|
194
200
|
})
|
|
201
|
+
if (outcome?.deduped === true) deduped += 1
|
|
202
|
+
else if (outcome?.entry?.status === 'pending_review') held += 1
|
|
203
|
+
else written += 1
|
|
195
204
|
}
|
|
196
205
|
setSessionBinding(sessionId, { lastSleepSeq: events[events.length - 1]?.seq })
|
|
197
|
-
const result = { ok: true, sessionId, backend: 'api', count:
|
|
206
|
+
const result = { ok: true, sessionId, backend: 'api', count: written, deduped, held }
|
|
198
207
|
record({
|
|
199
208
|
kind: 'consolidate', scope: 'private', sessionId, libraryId: binding.libraryId || null, ok: true,
|
|
200
|
-
message: `整理完成(远程)· 写入 ${
|
|
209
|
+
message: `整理完成(远程)· 写入 ${written}`
|
|
210
|
+
+ (deduped ? ` · 去重拦下 ${deduped}` : '')
|
|
211
|
+
+ (held ? ` · 待复核 ${held}` : ''),
|
|
201
212
|
detail: { ...result, forced: force },
|
|
202
213
|
})
|
|
203
214
|
return result
|
|
@@ -264,19 +275,91 @@ function turnEndFailure(event) {
|
|
|
264
275
|
return 'the fork turn ended ' + (reason.kind || 'without a reason') + (message ? ': ' + message : '')
|
|
265
276
|
}
|
|
266
277
|
|
|
278
|
+
// PTC 兼容(2026-10-31 现场 worker-tools-ptc-only):PTC 会话的 header 工具表只有
|
|
279
|
+
// run_code,其余工具走 run_code 程序里的 SDK 绑定(宿主 ptc.ts 用
|
|
280
|
+
// tools.schemas(agent, toolSet) 枚举"调用 agent 的可见面")。preset 组合没列出本插件
|
|
281
|
+
// 时,fork 的注册表里根本没有 memu_*,worker 既不能原生调用也没有 SDK 绑定,回执只能
|
|
282
|
+
// 是 tools-unavailable。修法与 preset-scope.js 的注入挂载同构:把 worker 读写面挂进 fork
|
|
283
|
+
// 自己的 ctx scope —— 注册表按 scope 链取可见面,所以只对这个 worker 可见,不扩散到
|
|
284
|
+
// 其它会话;挂载点必须在 followup 之前(宿主在 step 开始时 resolve 一次 admitted set,
|
|
285
|
+
// 之后注册的工具不进本步)。失败不阻断整理:worker 会按任务书如实回执 tools-unavailable。
|
|
286
|
+
async function attachWorkerTools(agents, childId, parentId) {
|
|
287
|
+
const wanted = [...WORKER_WRITE_TOOLS, ...WORKER_READ_TOOLS]
|
|
288
|
+
const ctxOf = (record) => {
|
|
289
|
+
try { return record?.ctx || record?.agent?.ctx || null } catch { return null }
|
|
290
|
+
}
|
|
291
|
+
let parentHasCtx = null
|
|
292
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
293
|
+
let record = null
|
|
294
|
+
try { record = agents.get?.(childId) || null } catch { record = null }
|
|
295
|
+
if (record) {
|
|
296
|
+
const actx = ctxOf(record)
|
|
297
|
+
// 有记录但没有 ctx:这版宿主不暴露 agent context(preset-scope.js 的同款限制),
|
|
298
|
+
// 重试不会变出来。ctx 在但没有 tools 服务:组合里就没有。两者都直接放弃。
|
|
299
|
+
if (!actx || !(actx.tools && typeof actx.tools.register === 'function')) break
|
|
300
|
+
try {
|
|
301
|
+
const dispose = registerTools(actx, { only: wanted })
|
|
302
|
+
// tools.js 在 disposer 上附挂实际注册成功的名单(编译 + register 都过的那部分)。
|
|
303
|
+
// 读它,不再把 wanted 全量当成"已挂载"——部分挂载必须结构化可见。
|
|
304
|
+
const attached = Array.isArray(dispose?.attached) ? [...dispose.attached] : null
|
|
305
|
+
return {
|
|
306
|
+
dispose,
|
|
307
|
+
attached,
|
|
308
|
+
requested: wanted,
|
|
309
|
+
// 名单缺失 = 无法证明挂全:partial 照实标 true,attached 落 null(与失败同口径)。
|
|
310
|
+
partial: attached ? attached.length < wanted.length : true,
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
warn('consolidation worker tool attach failed: ' + String(error?.message || error))
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 子记录还没出现:只在这个宿主确实会暴露 ctx(父记录带 ctx)时才值得短退避等它。
|
|
318
|
+
if (parentHasCtx === null) parentHasCtx = Boolean(ctxOf(agents.get?.(parentId)))
|
|
319
|
+
if (!parentHasCtx) break
|
|
320
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
321
|
+
}
|
|
322
|
+
warn('consolidation worker has no tools service on its own context; the worker may be unable to write memories')
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 活动日志"整理已开始"条目的挂载元数据口径(workerTools* 三件):
|
|
327
|
+
// workerToolsAttached 实际注册成功的工具名数组;null = 挂载没有完成或无法确认
|
|
328
|
+
// (fork ctx 无 tools 服务、register 抛错、宿主没回名单),
|
|
329
|
+
// 与"挂全了"严格区分——这正是旧字段把 wanted 全量误报成
|
|
330
|
+
// 已挂载的教训。
|
|
331
|
+
// workerToolsRequested 请求挂载的完整 wanted 名单;与 attached 全等才是全量挂载。
|
|
332
|
+
// workerToolsPartial true = 未证明 worker 拿到完整 wanted 集(子集、空集、或挂载
|
|
333
|
+
// 整体失败)。刻意把"一件都没挂上"也标 true:若失败标 false,
|
|
334
|
+
// false 会被一眼读成"全量挂载",元数据又不准了。
|
|
335
|
+
function workerToolsMeta(workerTools) {
|
|
336
|
+
const wanted = [...WORKER_WRITE_TOOLS, ...WORKER_READ_TOOLS]
|
|
337
|
+
if (!workerTools) {
|
|
338
|
+
return { workerToolsAttached: null, workerToolsRequested: wanted, workerToolsPartial: true }
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
workerToolsAttached: Array.isArray(workerTools.attached) ? workerTools.attached : null,
|
|
342
|
+
workerToolsRequested: Array.isArray(workerTools.requested) ? workerTools.requested : wanted,
|
|
343
|
+
workerToolsPartial: workerTools.partial === true,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
267
347
|
const inFlight = new Map()
|
|
268
348
|
|
|
269
349
|
// 这里的 import 刻意写在语句之后:lib/binding-diagnostics.js 的 RUNTIME_STATE 把 `inFlight`
|
|
270
|
-
// 的 file:line
|
|
350
|
+
// 的 file:line 当成诊断契约(当前 lib/private.js:336),修改位置时同步更新该诊断映射,
|
|
271
351
|
// 而那是别人的文件、本包不改。ESM 的 import 声明是提升的,写在语句之后合法。
|
|
272
352
|
import { warn } from './paths.js'
|
|
273
353
|
import { captureSessionSnapshot } from './consolidation-snapshot.js'
|
|
274
354
|
import {
|
|
275
355
|
beginJob, completeJob, failJob, findOperation, noteEvidence, operationKey,
|
|
276
|
-
recoverStaleJobs, recordOperation, scopeGenerationOf,
|
|
356
|
+
recoverStaleJobs, recordOperation, scopeGenerationOf, clearSessionState,
|
|
277
357
|
} from './consolidation-ledger.js'
|
|
278
358
|
import { decideCompletion, receiptContractText } from './consolidation-receipt.js'
|
|
279
|
-
import { buildWorkerBrief, probeToolSurface } from './consolidation-worker.js'
|
|
359
|
+
import { buildWorkerBrief, probeToolSurface, WORKER_READ_TOOLS, WORKER_WRITE_TOOLS } from './consolidation-worker.js'
|
|
360
|
+
import { isGoneSessionError } from './cycle.js'
|
|
361
|
+
import { autoLinkPrivateMemories } from './private-links.js'
|
|
362
|
+
import { registerTools } from './tools.js'
|
|
280
363
|
|
|
281
364
|
// 账本异常必须可见;调用方只有拿到明确的持久租约才可启动 worker。
|
|
282
365
|
// 其余读写失败返回保守结果,不得据此推进已确认边界。
|
|
@@ -325,6 +408,7 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
325
408
|
let turnEnd = null
|
|
326
409
|
let jobId = null
|
|
327
410
|
let scopeChanged = null
|
|
411
|
+
let workerTools = null
|
|
328
412
|
const assistantTexts = []
|
|
329
413
|
let childId = null
|
|
330
414
|
// 每次整理都要在活动日志里留一条:谁触发(手动/空闲)、fork 有没有建、结果是完成、
|
|
@@ -387,6 +471,19 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
387
471
|
if (captured.code === 'deferred') {
|
|
388
472
|
return finish({ ok: true, sessionId, skipped: captured.reasonCode, settled: false, deferred: true, reasonCode: captured.reasonCode, detail: captured.error })
|
|
389
473
|
}
|
|
474
|
+
// 会话已被删除(host 回答 not found)是正常生命周期,不是整理故障:标记
|
|
475
|
+
// sessionGone(空闲扫描从此跳过)、记 info 级 check 而不是 error——删除的
|
|
476
|
+
// 对话不该在整理提醒里挂一条失败(2026-10-31 用户报告)。
|
|
477
|
+
if (isGoneSessionError(captured.error)) {
|
|
478
|
+
setSessionBinding(sessionId, { sessionGone: true, sessionGoneAt: new Date().toISOString(), sleepRetryAt: null, sleepAttempts: 0, lastSleepError: null })
|
|
479
|
+
try { clearSessionState(sessionId) } catch {}
|
|
480
|
+
record({
|
|
481
|
+
kind: 'check', scope: 'private', sessionId, libraryId: binding.libraryId || null, ok: true,
|
|
482
|
+
message: '对话已删除,不再整理(session not found)',
|
|
483
|
+
detail: { reasonCode: 'session-gone' },
|
|
484
|
+
})
|
|
485
|
+
return { ok: false, sessionId, code: 'session-gone', reasonCode: 'session-gone', settled: false, retryable: false, error: captured.error }
|
|
486
|
+
}
|
|
390
487
|
return finish({
|
|
391
488
|
ok: false, sessionId, code: captured.code, reasonCode: captured.reasonCode, settled: false,
|
|
392
489
|
retryable: captured.retryable === true, error: captured.error,
|
|
@@ -522,6 +619,13 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
522
619
|
signal: abort.signal,
|
|
523
620
|
})
|
|
524
621
|
|
|
622
|
+
// header 无法证明 worker 有写工具时(PTC 会话名单只有 run_code / 旧会话没有名单),
|
|
623
|
+
// 把 worker 读写面挂进 fork 自己的 scope,保证 SDK 面(PTC)与原生面(普通会话)都真能写入。
|
|
624
|
+
// 标准 preset 的 fork 名单里已有 memu_*(probe.hasWrite === true),不重复挂。
|
|
625
|
+
if (probe.hasWrite !== true) {
|
|
626
|
+
workerTools = await attachWorkerTools(agents, childId, sessionId)
|
|
627
|
+
}
|
|
628
|
+
|
|
525
629
|
// 先记"已开始"再等结果:一个大会话的 fork 可能跑好几分钟,没有这条记录,
|
|
526
630
|
// 侧边栏和日志页在等待期间只会显示上一次的结果,用户会以为按钮没生效。
|
|
527
631
|
record({
|
|
@@ -531,7 +635,9 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
531
635
|
libraryId: binding.libraryId || null,
|
|
532
636
|
ok: true,
|
|
533
637
|
message: '整理已开始 · ' + (force ? '手动' : '空闲') + ' · fork ' + childId.slice(-8) + ' · ' + events.length + ' 条事件 · 快照 ' + snapshot.hash,
|
|
534
|
-
detail: { childId, jobId, force, events: events.length, phase: 'started', snapshot: snapshotMeta
|
|
638
|
+
detail: { childId, jobId, force, events: events.length, phase: 'started', snapshot: snapshotMeta,
|
|
639
|
+
probe: { source: probe.source, hasWrite: probe.hasWrite, ptc: probe.ptcObserved === true },
|
|
640
|
+
...workerToolsMeta(workerTools) },
|
|
535
641
|
})
|
|
536
642
|
|
|
537
643
|
// turn/end 是这一轮的唯一权威结果;whenIdle 只说明 driver 停了(失败的回合也停)。
|
|
@@ -685,6 +791,20 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
685
791
|
}
|
|
686
792
|
setSessionBinding(sessionId, { lastSleepSeq: snapshot.throughSeq })
|
|
687
793
|
safeLedger(() => recordOperation({ sessionId, key: opKey, outcome: decision.status, ids: decision.createdIds || [] }))
|
|
794
|
+
// 私有自动关联(§5.6 兜底):只在整理真的对账并推进边界之后跑,纯算法,
|
|
795
|
+
// 0 token。worker 若已调用 memu_private_link_suggest,这一遍是幂等的(已有的
|
|
796
|
+
// relation/person 边会跳过)。link 只是图元数据:失败只记 warn,绝不能让一次
|
|
797
|
+
// 已经成功的整理变成失败;timeout / unsettled / ledger-not-advanced 三条路径
|
|
798
|
+
// 走不到这里,所以它们不会触发自动关联。
|
|
799
|
+
const linkIds = [...(decision.createdIds || []), ...(decision.updatedIds || [])]
|
|
800
|
+
if (linkIds.length) {
|
|
801
|
+
try {
|
|
802
|
+
await autoLinkPrivateMemories({ libraryId: binding.libraryId, sessionId, ids: linkIds })
|
|
803
|
+
} catch (error) {
|
|
804
|
+
// autoLinkPrivateMemories 自己已捕获操作性失败;这里只兜"意料之外"的抛错。
|
|
805
|
+
try { warn('private auto-link after consolidation skipped: ' + String(error?.message || error)) } catch {}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
688
808
|
}
|
|
689
809
|
// A timeout is not success: the fork may still be mid-write. It has to come
|
|
690
810
|
// back with a message, not just `timedOut` — the settings button renders
|
|
@@ -731,6 +851,7 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
731
851
|
inFlight.delete(sessionId)
|
|
732
852
|
abort.abort()
|
|
733
853
|
try { offSession?.() } catch {}
|
|
854
|
+
try { workerTools?.dispose?.() } catch {}
|
|
734
855
|
try { await handle?.dispose?.() } catch {}
|
|
735
856
|
try { if (childId) deleteSessionBinding(childId) } catch {}
|
|
736
857
|
}
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
输入:
|
|
8
8
|
1. 选中的会话:id、标题、时间、cwd、agent preset、消息与工具调用摘要。
|
|
9
|
-
2.
|
|
10
|
-
3.
|
|
9
|
+
2. 现有全局记忆索引 existing_global_index:按与本次会话的相关性排序、带预算截断(index_truncated=true 时说明有省略),可能为空。
|
|
10
|
+
3. 候选簇 candidate_clusters:代码预先算好的相似分组(最多 12 簇,每簇 ≤6 条),含 pair_scores(0-1 相似分)、medoid_id(最居中一条)、suggested_op(absorb/link 提示)。
|
|
11
|
+
4. 可选:用户对全局记忆的显式要求。
|
|
12
|
+
|
|
13
|
+
候选簇与索引的使用规则:
|
|
14
|
+
- 候选操作优先用簇内 id;簇外 id 不要输出。索引可能不完整,提交层会重新做重复/冲突复核;不要因为“索引里没看见”就断定某条不存在。
|
|
15
|
+
- suggested_op 只是提示,不是命令;你必须按下面的规则自行判断。
|
|
16
|
+
- pair_scores ≥0.82 大概率是同一事实的改写;0.55-0.82 是同主题不同事实。
|
|
11
17
|
|
|
12
18
|
第一步:给每个会话分类
|
|
13
19
|
rp / coding / mixed / other。依据 preset、标题、cwd、内容,不要只看关键词。
|
|
@@ -39,37 +45,88 @@ identity、preferences、working_style、tooling、domain_profile、do_not_assum
|
|
|
39
45
|
- 两个会话互相矛盾、又判断不了哪个对(例如同一件事两种说法)→ op=conflict,
|
|
40
46
|
写 target_id 指向被质疑的那条(没有就留空)。conflict 不会直接写进全局记忆,
|
|
41
47
|
它进入人工复核队列,由人决定接受还是丢弃。
|
|
42
|
-
- 你不太确定(confidence < 0.5)或者证据只来自一次口误 → needs_review=true,
|
|
43
|
-
并在 review_reason 写清为什么不确定。
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
第五步:为每个候选选操作(不要为了产出而造关系)
|
|
50
|
+
- 同一事实的轻微改写(pair ≥0.82)→ merge/supersede,不要 absorb。
|
|
51
|
+
- 同一主题、不同事实、放在一起看才有价值 → op=link:
|
|
52
|
+
* kind=related:对称的“互相关”;kind=refines:本条细化对方;kind=supports:本条为对方提供佐证。
|
|
53
|
+
* note 8-500 字,且必须原样包含至少一句 evidence 里的 quote(可回放的转录原话)。
|
|
54
|
+
* 跨域只能 related;rp 和 coding 之间永远不 link。
|
|
55
|
+
- 同一主题、多条碎片(共 2-6 条)需要合成一条集合记忆 → op=absorb:
|
|
56
|
+
* target_id 选最完整/最居中的那条(通常是 medoid);source_ids 1-5 条;target+source 总数 2-6。
|
|
57
|
+
* content 必须保留每条来源的独有事实;禁止只写摘要丢掉条件、版本、例外。
|
|
58
|
+
* 每条 source 至少一条 evidence 带 source_id 指向它;总 evidence ≤20。
|
|
59
|
+
* 不要跨 track absorb;不要跨 domain 强合并;RP 剧情事实、关系进度、临时情绪不写全局。
|
|
60
|
+
- 同一主题但说法相反 → op=conflict;不要自己选赢家。
|
|
61
|
+
- 证据不足 / 一次口误 → needs_review=true 并写清 review_reason;没有 quote 不要编。
|
|
62
|
+
- 拿不准 → op=noop;没有合适候选不要为了产出而创建。
|
|
63
|
+
|
|
64
|
+
第六步:写条目
|
|
46
65
|
中性第三人称,简洁,可执行。不要用角色口吻,不要写“我觉得”。
|
|
47
66
|
每条带 evidence(session_id + 一句原文/事件)和 confidence(0-1)。
|
|
48
67
|
引用只能来自输入中确实存在的说话人与片段;摘要就写成摘要,不伪装逐字引语,不编造 session_id。群聊先核对是谁说的,不把其他成员或角色的喜好归给当前用户;凭据与私密信息不能出现在 evidence 里。
|
|
49
68
|
不要写“用户聊了很多项目的事”这种总结;
|
|
50
69
|
要写“用户在 coding 任务中偏好先给结论,再给细节”。
|
|
51
70
|
|
|
71
|
+
标题即目录(锚点只展示标题,正文不注入):
|
|
72
|
+
- 每条 create / merge / absorb 都必须写 8–40 字的标题。标题要说出具体事实——项目、工具、人物、事件或结论的关键词,
|
|
73
|
+
不要「关于…的记录」「一些偏好」「工作相关」这类空标签。
|
|
74
|
+
- absorb 与集合记忆的标题要覆盖所有来源的关键词,不能只概括其中一条;被合并掉的细节全部留在 content 里。
|
|
75
|
+
- 标题是给以后检索用的目录项,不是文采展示;拿不准就把结论写进标题。
|
|
76
|
+
|
|
77
|
+
上限(代码会截断,超出部分直接丢弃):
|
|
78
|
+
- memories ≤80;
|
|
79
|
+
- link 与 absorb 合计 ≤20;其中 absorb ≤8;
|
|
80
|
+
- 每条 link note 8-500 字;
|
|
81
|
+
- 每条 evidence 的 quote 必须来自输入会话原文。
|
|
82
|
+
|
|
52
83
|
输出 JSON:
|
|
53
84
|
{
|
|
54
85
|
"sessions": [{"id":"...","domain":"rp|coding|mixed|other","reason":"..."}],
|
|
55
|
-
"memories": [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
"memories": [
|
|
87
|
+
{
|
|
88
|
+
"op":"create|merge|supersede|noop|conflict",
|
|
89
|
+
"target_id":"...",
|
|
90
|
+
"track":"memory|skill",
|
|
91
|
+
"domain":"universal|coding|rp|project:<name>|tooling|personal|general",
|
|
92
|
+
"applies_to":["..."],
|
|
93
|
+
"title":"...",
|
|
94
|
+
"content":"...",
|
|
95
|
+
"confidence":0.0,
|
|
96
|
+
"needs_review":false,
|
|
97
|
+
"review_reason":"",
|
|
98
|
+
"evidence":[{"session_id":"...","quote":"..."}],
|
|
99
|
+
"reason":"..."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"op":"link",
|
|
103
|
+
"from_id":"gmem-...",
|
|
104
|
+
"to_id":"gmem-...",
|
|
105
|
+
"kind":"related|refines|supports",
|
|
106
|
+
"note":"8-500字,原样包含至少一句 evidence quote",
|
|
107
|
+
"confidence":0.7,
|
|
108
|
+
"evidence":[{"session_id":"...","quote":"..."}],
|
|
109
|
+
"reason":"..."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"op":"absorb",
|
|
113
|
+
"target_id":"gmem-...",
|
|
114
|
+
"source_ids":["gmem-..."],
|
|
115
|
+
"title":"...",
|
|
116
|
+
"content":"...",
|
|
117
|
+
"confidence":0.9,
|
|
118
|
+
"evidence":[{"session_id":"...","quote":"...","source_id":"gmem-..."}],
|
|
119
|
+
"reason":"..."
|
|
120
|
+
}
|
|
121
|
+
],
|
|
69
122
|
"domain_summary": {"rp":"...","coding":"...","mixed":"..."},
|
|
70
123
|
"skipped": [{"session_id":"...","reason":"..."}]
|
|
71
124
|
}
|
|
72
125
|
|
|
126
|
+
边界:提示词负责语义判断,代码负责能不能落库——提交层会重新做候选检索、
|
|
127
|
+
证据落地、覆盖率与上限复核;absorb 的 content 只是候选摘要,覆盖率不足时
|
|
128
|
+
会回退抽取或转人工复核。证据、domain、track、applies_to 的校验以代码为准。
|
|
129
|
+
|
|
73
130
|
输入数据:
|
|
74
131
|
|
|
75
132
|
{{input}}
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
最多新增 3 条。宁可少写一条,也不要写一条未来会被自己当成噪音的。
|
|
18
18
|
|
|
19
19
|
每条的形状:
|
|
20
|
-
- title
|
|
20
|
+
- title:8–40 字,结论不是主题——读标题就该知道答案,并且要写具体事实(模块、符号、命令、错误签名、结论的关键词)。
|
|
21
|
+
锚点/召回只显示标题、不显示正文,所以标题就是目录项;不要「关于……的记录」「一些改动」这类空标签,细节放进 content。
|
|
22
|
+
合并/吸收后的标题要覆盖来源的关键词。
|
|
21
23
|
- content:通常 150–350 字,按「结论 / 作用范围与定位 / 条件与原因 / 验证状态」排;优先文件路径与符号名,行号仅作辅助,附明确出现过的版本、命令或测试范围。复杂逻辑可稍长,不为字数损坏语义。
|
|
22
24
|
- 代码块:仅在必要时保留带语言标记的最小片段,保证缩进、标识符、参数和运算符准确;不编造代码、路径、提交号或验证结果。
|
|
23
25
|
- 未完成工作:可写 short_term 接续卡,列明下一步、缺少的验证与真实阻塞;未验证方案不能升级为长期事实。
|
|
@@ -31,4 +33,5 @@
|
|
|
31
33
|
- 情绪按事件所在回合还原:emotion 描述的是那条记忆里的那次事件,不是整理时的心情;工程事实不写角色情绪,owner=about_project 固定中性。dominance 只在写人物侧条目时填(自己在推动为正、被推着走为负),工程事实省略。同一轮的多条条目各填各的,不把一次争吵的情绪串到所有条目。
|
|
32
34
|
|
|
33
35
|
永久记忆只留给「永远不能违反的约束」;普通决定不要 pin。
|
|
36
|
+
可抽词典词:只抽稳定、可复用、有证据的工程术语——项目名、内部代号、缩写、工具/模块别名、团队黑话;不抽 sensitive trigger 和一次性词。
|
|
34
37
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
- 只与实际提供或成功检索到的旧记忆去重:兼容可合并,矛盾保留来源、时间与条件供复核,不抹旧正文或自行宣布取代。最多新增 3 条,没有值得留的就不写。
|
|
15
15
|
|
|
16
16
|
字段:
|
|
17
|
-
- title
|
|
17
|
+
- title:8–40 字,写具体事实(人名、物名、事件、结论的关键词)。锚点与召回只显示标题、不显示正文,所以标题就是目录项;
|
|
18
|
+
不要「一些事」「那段对话」「关于……的记录」这类空标签,细节全部放进 content。合并/吸收后的标题要覆盖来源的关键词。
|
|
18
19
|
- content:自包含的具体内容、对象/条件及后续意义,通常 80–220 字,不硬凑长度。
|
|
19
20
|
- tags:可选,便于以后检索。
|
|
20
21
|
- salient_date:只在确有日期时填,且要落成绝对日期。每年复发的那一天(生日、周年)用 `MM-DD`;
|
|
@@ -25,4 +26,5 @@
|
|
|
25
26
|
- 情绪按事件所在回合还原:用那一轮对话里的情绪填 emotion,不要用整理时的心情。同一轮的多条条目各填各的,不把一次争吵的情绪串到所有条目。工程事实(owner=about_project)固定中性。dominance 跟着 emotion 一起填:自己在推动为正、被推着走为负,没把握就省略。
|
|
26
27
|
|
|
27
28
|
永久记忆只留明确永久要求、有依据的核心约束,角色身份或侧事件理由仅适用于实际叙事;不要为了显得深刻而 pin。具体能否写永久、如何更新,以随后输出协议为准。
|
|
29
|
+
可抽词典词:只抽稳定、可复用、有证据的黑话、梗、术语;不抽 sensitive trigger 和一次性词。
|
|
28
30
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
- 最多新增 3 条,优先会影响之后决定或相处的事项,没有值得留下的就不写。
|
|
18
18
|
|
|
19
19
|
字段:
|
|
20
|
-
- title
|
|
20
|
+
- title:8–40 字,写具体事实(项目/模块/人物/事件/结论的关键词)。锚点/召回只显示标题、不显示正文,所以标题就是目录项;
|
|
21
|
+
不要「一些事」「关于……的记录」这类空标签,细节放进 content。合并/吸收后的标题要覆盖来源的关键词。
|
|
21
22
|
- content:技术侧通常 150–350 字,人物侧通常 60–180 字,按信息需要调整,不硬凑长度。
|
|
22
23
|
- tags:可选。
|
|
23
24
|
- salient_date:只在确有日期时填,且要落成绝对日期。每年复发的那一天(生日、周年)用 `MM-DD`;
|
|
@@ -29,4 +30,5 @@
|
|
|
29
30
|
- 情绪按事件所在回合还原:人物侧填那一轮对话里的情绪(emotion/arousal/dominance),技术侧不写角色情绪;同一轮拆开的多条条目各填各的,一次争吵的情绪不串到所有条目。工程事实(owner=about_project)固定中性。dominance 只跟人物侧的 emotion 一起填,技术侧省略。
|
|
30
31
|
|
|
31
32
|
永久记忆只留给"忘了就不再是我"或绝不能违反的约束;普通决定不要 pin。
|
|
33
|
+
可抽词典词:技术侧项目名、缩写、内部代号、工具别名;人物侧角色自造词、昵称、梗、口癖。只抽稳定、可复用、有证据的词,不抽 sensitive trigger 和一次性词。
|
|
32
34
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
- 群聊分清谁对谁说了什么,私下告知不变成全员知情;OOC 写作约定不变成剧情经历,waiting/sleep 日志不变成冷落或剧情时间。一次客套不推导长期偏好,不按轮数增加好感。
|
|
22
22
|
|
|
23
23
|
字段:
|
|
24
|
-
- title
|
|
24
|
+
- title:8–40 字,写清这件事是什么——具体的人名、称谓、物件、事件或结论关键词。锚点/召回只显示标题、不显示正文,
|
|
25
|
+
所以标题就是目录项;不要「关于……的回忆」「一些对话」这类空标签,细节留在 content。合并/吸收后的标题要覆盖来源的关键词。
|
|
25
26
|
- content:一个瞬间 + 它对之后的影响,通常 60–180 字,不硬凑长度;最多引一句有出处的原话。能让下次称呼、选择或分寸自然接续,比抒情更重要。
|
|
26
27
|
- tags:可选。
|
|
27
28
|
- salient_date:只在确有日期时填,且要落成绝对日期。每年复发的那一天(生日、周年)用 `MM-DD`;
|
|
@@ -33,4 +34,5 @@
|
|
|
33
34
|
- 情绪按事件所在回合还原:emotion 填的是那一瞬间的感受,取那一轮对话里的情绪,不是整理时的心情。同一轮的多条条目各写各的,一场争吵的情绪不串到所有条目。工程事实(owner=about_project)固定中性。dominance 跟着 emotion 一起填:那一刻是自己推动的(正)还是被推着走的(负),没把握就省略,不要用 0 凑数。
|
|
34
35
|
|
|
35
36
|
永久记忆只在对方明确要求、随机侧事件、或极少数「忘了就不再是我」时使用;不要为了显得深刻而 pin。
|
|
37
|
+
可抽词典词:只抽稳定、可复用、有证据的角色自造词、昵称、梗、口癖、称呼;不抽 sensitive trigger 和一次性词。
|
|
36
38
|
不要写全局记忆;全局记忆只由 curator 写。
|
package/lib/prompts.js
CHANGED
|
@@ -84,7 +84,7 @@ export const CONSOLIDATION_CONTRACTS = {
|
|
|
84
84
|
local: '使用当前可用的记忆工具,先按主题检索去重。兼容细节用 memu_private_update 合并;真正新增或矛盾待复核的内容用 memu_private_remember,一条一件,不自行裁定冲突。不能查询或更新时如实保留限制,不宣称已核对或已修改。没有值得留的就直接结束,不要勉强写。',
|
|
85
85
|
remote: [
|
|
86
86
|
'只输出一个 JSON 对象,不要任何解释或 Markdown 代码块:',
|
|
87
|
-
'{"memories":[{"title":"
|
|
87
|
+
'{"memories":[{"title":"8-40字,具体事实关键词","content":"完整正文","layer":"long_term|short_term","tags":["可选"],"open_loop":false,"confidence":0.0,"source":"inferred|user_said|imagined|told","visibility":"shared|secret","disclosure":"can_tell|only_if_asked|never_say","owner":"me|us|about_user|about_project","emotion":null,"dominance":0,"mood_valence":0.0,"arousal":0.35,"peak":false,"salient_date":null}]}',
|
|
88
88
|
'你没有记忆工具,也没有输入以外的索引:只返回最多 3 条候选,不声称已检索、合并、更新、裁决或完成写入。不要输出工具调用或 must_remember;矛盾候选在 content 中保留来源、时间与不确定性,交给后续复核。领域表达服从上文,字段服从本 JSON 协议。emotion 只接受四轴 {"joy","anger","sorrow","fear"} 的份额对象,没有明确依据就用 null;dominance 是 -1..1 的掌控感。未确定的感受/日期用 null,不为填字段编造。',
|
|
89
89
|
'没有值得留的就输出 {"memories":[]}。',
|
|
90
90
|
].join('\n'),
|