dsh-memu-memory 1.1.3 → 1.2.0
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 +31 -2
- package/README.zh.md +30 -2
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +1 -1
- package/docs/emotion-model-spec.md +281 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +1 -1
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +33 -11
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +115 -0
- package/lib/cycle.js +40 -6
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +221 -19
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +335 -40
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +9 -1
- package/lib/state.js +29 -12
- package/lib/tools.js +49 -2
- package/lib/vector-profile.js +27 -0
- package/package.json +2 -1
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +212 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +320 -0
- package/tests/consolidation-fork.test.mjs +357 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +321 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +77 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +92 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +536 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +208 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +733 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +164 -0
- package/tests/windows-env.test.mjs +89 -0
package/lib/private.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import crypto from 'node:crypto'
|
|
3
|
-
import { getSessionBinding, getLibrary, getSettings, setSessionBinding, loadPrivateIndex, effectiveScope } from './state.js'
|
|
3
|
+
import { getSessionBinding, getLibrary, getSettings, setSessionBinding, deleteSessionBinding, loadPrivateIndex, effectiveScope } from './state.js'
|
|
4
4
|
import { domainOf, privateRemember, isMemoryEligible, visibleToSession } from './memory.js'
|
|
5
5
|
import { loadPromptForDomain, CONSOLIDATION_CONTRACTS } from './prompts.js'
|
|
6
6
|
import { summarizeRemote } from './remote-summary.js'
|
|
7
7
|
import { condenseEvents, readSessionSnapshot } from './curator.js'
|
|
8
8
|
import { record } from './activity-log.js'
|
|
9
|
+
import { ACTIVE_MEMORY_POLICY, assessWake, collectEvidence } from './active-memory.js'
|
|
10
|
+
import { entryFingerprint } from './consolidation-receipt.js'
|
|
9
11
|
|
|
10
12
|
// Short-term entries expire on a clock, so the idle pass is the last moment anyone
|
|
11
13
|
// can decide what they were worth. The list is generated in code rather than added
|
|
@@ -264,6 +266,43 @@ function turnEndFailure(event) {
|
|
|
264
266
|
|
|
265
267
|
const inFlight = new Map()
|
|
266
268
|
|
|
269
|
+
// 这里的 import 刻意写在语句之后:lib/binding-diagnostics.js 的 RUNTIME_STATE 把 `inFlight`
|
|
270
|
+
// 的 file:line 当成诊断契约(第 267 行),修改位置时同步更新该诊断映射,
|
|
271
|
+
// 而那是别人的文件、本包不改。ESM 的 import 声明是提升的,写在语句之后合法。
|
|
272
|
+
import { warn } from './paths.js'
|
|
273
|
+
import { captureSessionSnapshot } from './consolidation-snapshot.js'
|
|
274
|
+
import {
|
|
275
|
+
beginJob, completeJob, failJob, findOperation, noteEvidence, operationKey,
|
|
276
|
+
recoverStaleJobs, recordOperation, scopeGenerationOf,
|
|
277
|
+
} from './consolidation-ledger.js'
|
|
278
|
+
import { decideCompletion, receiptContractText } from './consolidation-receipt.js'
|
|
279
|
+
import { buildWorkerBrief, probeToolSurface } from './consolidation-worker.js'
|
|
280
|
+
|
|
281
|
+
// 账本异常必须可见;调用方只有拿到明确的持久租约才可启动 worker。
|
|
282
|
+
// 其余读写失败返回保守结果,不得据此推进已确认边界。
|
|
283
|
+
function safeLedger(action, fallback = null) {
|
|
284
|
+
try {
|
|
285
|
+
return action()
|
|
286
|
+
} catch (error) {
|
|
287
|
+
try { warn('consolidation ledger: ' + String(error?.message || error)) } catch {}
|
|
288
|
+
return fallback
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 交给结果日志的快照身份:有 hash、有边界、没有正文。
|
|
293
|
+
function snapshotMetaOf(snapshot) {
|
|
294
|
+
if (!snapshot) return null
|
|
295
|
+
return {
|
|
296
|
+
hash: snapshot.hash,
|
|
297
|
+
source: snapshot.source,
|
|
298
|
+
fromSeq: snapshot.fromSeq,
|
|
299
|
+
throughSeq: snapshot.throughSeq,
|
|
300
|
+
eventCount: snapshot.eventCount,
|
|
301
|
+
droppedEvents: snapshot.droppedEvents,
|
|
302
|
+
capturedAt: snapshot.capturedAt,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
267
306
|
// Every agent-backend consolidation runs here, live sessions included: the same
|
|
268
307
|
// main agent, on a seeded copy of the transcript, writing into the library the
|
|
269
308
|
// original used. In-place consolidation would append a housekeeping turn to the
|
|
@@ -284,6 +323,10 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
284
323
|
let handle = null
|
|
285
324
|
let offSession = null
|
|
286
325
|
let turnEnd = null
|
|
326
|
+
let jobId = null
|
|
327
|
+
let scopeChanged = null
|
|
328
|
+
const assistantTexts = []
|
|
329
|
+
let childId = null
|
|
287
330
|
// 每次整理都要在活动日志里留一条:谁触发(手动/空闲)、fork 有没有建、结果是完成、
|
|
288
331
|
// 超时还是失败。否则"点了没反应"只能靠翻会话文件才知道发生了什么。
|
|
289
332
|
const finish = (result) => {
|
|
@@ -295,49 +338,173 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
295
338
|
sessionId,
|
|
296
339
|
libraryId: binding.libraryId || null,
|
|
297
340
|
ok,
|
|
298
|
-
level: ok ?
|
|
341
|
+
level: ok ? 'info' : (result?.timedOut ? 'warn' : 'error'),
|
|
299
342
|
message: '整理' + (ok ? (skipped ? '跳过(没有值得记的内容)' : '完成') : (result?.timedOut ? '超时' : '失败'))
|
|
300
343
|
+ (force ? ' · 手动' : ' · 空闲')
|
|
301
344
|
+ (result?.childId ? ' · fork ' + result.childId.slice(-8) : '')
|
|
345
|
+
+ (result?.reasonCode ? ' · ' + result.reasonCode : '')
|
|
302
346
|
+ (result?.error ? ':' + String(result.error) : '')
|
|
303
|
-
+ (result?.count !== undefined ? ' · 写入 ' + result.count : '')
|
|
304
|
-
|
|
347
|
+
+ (result?.count !== undefined ? ' · 写入 ' + result.count : '')
|
|
348
|
+
+ (result?.wrote !== null && result?.wrote !== undefined ? ' · 索引 +' + result.wrote : '')
|
|
349
|
+
+ (result?.scanned !== undefined ? ' · 扫描 ' + result.scanned + ' 事件 / 新 ' + (result.fresh ?? '?') : ''),
|
|
350
|
+
detail: { force, jobId, ...result },
|
|
305
351
|
refs: [],
|
|
306
352
|
})
|
|
307
353
|
return result
|
|
308
354
|
}
|
|
309
355
|
try {
|
|
310
|
-
|
|
356
|
+
// 僵尸租约先回收:进程被杀之后 inflight 会留在账本里,不能让一条死记录永久挡住这个会话。
|
|
357
|
+
safeLedger(() => {
|
|
358
|
+
const recovered = recoverStaleJobs()
|
|
359
|
+
if (recovered.length > 0) {
|
|
360
|
+
record({ kind: 'check', scope: 'private', ok: true, message: '回收 ' + recovered.length + ' 个过期整理租约', detail: { recovered } })
|
|
361
|
+
}
|
|
362
|
+
return recovered
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// 读不到会话就什么都不用谈,所以先确认快照能力,再确认 fork 能力——
|
|
366
|
+
// 失败信息要说清缺的是哪一个(readSession/observeSession 还是 agents.create)。
|
|
367
|
+
const sessionQuery = (() => { try { return ctx.get?.('sessionQuery') || null } catch { return null } })()
|
|
368
|
+
if (!sessionQuery || (typeof sessionQuery.readSession !== 'function' && typeof sessionQuery.observeSession !== 'function')) {
|
|
369
|
+
return finish({
|
|
370
|
+
ok: false, sessionId, code: 'unavailable', reasonCode: 'no-session-query', settled: false,
|
|
371
|
+
error: 'host has no sessionQuery.readSession / observeSession; historical consolidation is unavailable',
|
|
372
|
+
})
|
|
373
|
+
}
|
|
311
374
|
const agents = ctx.get?.('agents')
|
|
312
|
-
if (
|
|
313
|
-
return finish({ ok: false, error: 'host has no
|
|
375
|
+
if (typeof agents?.create !== 'function') {
|
|
376
|
+
return finish({ ok: false, sessionId, code: 'unavailable', reasonCode: 'no-agents-service', settled: false, error: 'host has no agents.create; historical consolidation is unavailable' })
|
|
314
377
|
}
|
|
315
378
|
const buildForkSeed = await loadBuildForkSeed()
|
|
316
|
-
if (!buildForkSeed)
|
|
379
|
+
if (!buildForkSeed) {
|
|
380
|
+
return finish({ ok: false, sessionId, code: 'unavailable', reasonCode: 'no-fork-seed', settled: false, error: 'dsh-session buildForkSeed is unavailable' })
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 安全快照:cut 固定、身份可核验。未闭合的回合、没配对的工具调用、半截 step 在这里就被挡住,
|
|
384
|
+
// 而不是被当成内容喂给 worker(C 包第 1、2 条)。拿不到就 deferred,不收编。
|
|
385
|
+
const captured = await captureSessionSnapshot(ctx, sessionId, { signal: abort.signal })
|
|
386
|
+
if (!captured.ok) {
|
|
387
|
+
if (captured.code === 'deferred') {
|
|
388
|
+
return finish({ ok: true, sessionId, skipped: captured.reasonCode, settled: false, deferred: true, reasonCode: captured.reasonCode, detail: captured.error })
|
|
389
|
+
}
|
|
390
|
+
return finish({
|
|
391
|
+
ok: false, sessionId, code: captured.code, reasonCode: captured.reasonCode, settled: false,
|
|
392
|
+
retryable: captured.retryable === true, error: captured.error,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
const snapshot = captured.snapshot
|
|
396
|
+
const events = captured.events
|
|
397
|
+
const snapshotMeta = snapshotMetaOf(snapshot)
|
|
398
|
+
const generationAtStart = scopeGenerationOf(binding)
|
|
399
|
+
// 待检查边界与已确认边界分开记:证据到达就记一次,失败不回退。
|
|
400
|
+
safeLedger(() => noteEvidence({ sessionId, fromSeq: snapshot.fromSeq, throughSeq: snapshot.throughSeq }))
|
|
401
|
+
if (events.length === 0) return finish({ ok: false, sessionId, reasonCode: 'empty-snapshot', settled: false, error: 'this session has no content to consolidate' })
|
|
317
402
|
|
|
318
|
-
const childId = `memu-consolidate-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`
|
|
319
|
-
const snapshot = await sessionQuery.readSession(sessionId)
|
|
320
|
-
const events = Array.isArray(snapshot?.events) ? snapshot.events : []
|
|
321
|
-
const lastSeq = events[events.length - 1]?.seq
|
|
322
|
-
if (events.length === 0 || lastSeq === undefined) return finish({ ok: false, error: 'this session has no content to consolidate' })
|
|
323
403
|
// The scheduler asks "is this conversation idle", which is not the same as
|
|
324
404
|
// "did anything happen that deserves a memory". Answer the second question
|
|
325
405
|
// before paying for a fork; a manual consolidation passes force and skips it.
|
|
326
406
|
if (!force) {
|
|
327
|
-
const
|
|
328
|
-
|
|
407
|
+
const fresh = unconsolidatedEvents(binding, events)
|
|
408
|
+
const value = assessConsolidationValue(fresh)
|
|
409
|
+
// 这一批里没有真人回合、或只有一句「好的继续」:这一段确实没什么可整理的,
|
|
410
|
+
// 是终态而不是 deferral——否则空闲扫描会为同一句寒暄一直重扫。
|
|
411
|
+
// 没有真人回合、或只有一句「谢谢」:这一段确实没有可整理的证据,是终态而不是 deferral。
|
|
412
|
+
// 写成 deferred 会让空闲扫描为同一句寒暄每轮重扫(tests/cycle.test.mjs 的寒暄用例钉住这一点)。
|
|
413
|
+
if (!value.worth) return finish({ ok: true, sessionId, skipped: value.reason, settled: false, deferred: true, reasonCode: value.reason, assessment: value, snapshot: snapshotMeta })
|
|
414
|
+
// 加权判定:短空闲要求分数达标,长空闲(饥饿阈值)无条件放行;
|
|
415
|
+
// 阈值与权重集中在 lib/active-memory.js,可根据回放标定。
|
|
416
|
+
const settings = getSettings()
|
|
417
|
+
const idleMinutes = binding.lastTurnAt ? (Date.now() - (Date.parse(binding.lastTurnAt) || Date.now())) / 60_000 : Infinity
|
|
418
|
+
const starved = idleMinutes >= Number(settings.activeMemoryStarvationMinutes ?? ACTIVE_MEMORY_POLICY.starvationMinutes)
|
|
419
|
+
const evidence = collectEvidence(fresh)
|
|
420
|
+
const wake = assessWake({
|
|
421
|
+
evidence,
|
|
422
|
+
starved,
|
|
423
|
+
transcriptEvents: events.length,
|
|
424
|
+
// 「待整理区间有多大」是守卫唯一该看的规模信号:守卫要躲的是为一句轻话重种
|
|
425
|
+
// 整段转录,而不是把一个从没整理过的大会话永远挡在门外。
|
|
426
|
+
pendingEvents: fresh.length,
|
|
427
|
+
policy: { ...ACTIVE_MEMORY_POLICY, minScore: Number(settings.activeMemoryMinScore ?? ACTIVE_MEMORY_POLICY.minScore) },
|
|
428
|
+
})
|
|
429
|
+
record({
|
|
430
|
+
kind: 'check',
|
|
431
|
+
scope: 'private',
|
|
432
|
+
sessionId,
|
|
433
|
+
libraryId: binding.libraryId || null,
|
|
434
|
+
ok: wake.wake,
|
|
435
|
+
message: (wake.wake ? '主动检查触发' : '主动检查跳过')
|
|
436
|
+
+ ' · score ' + wake.score
|
|
437
|
+
+ ' · ' + wake.reason
|
|
438
|
+
+ ' · 空闲 ' + (Number.isFinite(idleMinutes) ? Math.round(idleMinutes) + ' 分钟' : '未知')
|
|
439
|
+
+ ' · 会话 ' + events.length + ' 事件'
|
|
440
|
+
+ ' · 快照 ' + snapshot.hash,
|
|
441
|
+
detail: { trigger: 'auto', reason: wake.reason, score: wake.score, parts: wake.parts, starved, transcriptEvents: events.length, snapshot: snapshotMeta },
|
|
442
|
+
})
|
|
443
|
+
if (!wake.wake) return finish({ ok: true, sessionId, skipped: 'low-score', settled: false, assessment: value, score: wake.score, parts: wake.parts, snapshot: snapshotMeta })
|
|
329
444
|
}
|
|
330
|
-
|
|
445
|
+
|
|
446
|
+
// 幂等:同一份固定快照已经结算过就不再 fork 一次(失败的部分写入靠操作键判同再试,
|
|
447
|
+
// 而不是靠"同一个主题就跳过")。
|
|
448
|
+
const opKey = operationKey({ sessionId, libraryId: binding.libraryId, snapshotHash: snapshot.hash, scopeGeneration: generationAtStart })
|
|
449
|
+
const previous = safeLedger(() => findOperation(sessionId, opKey))
|
|
450
|
+
if (previous && (previous.outcome === 'completed' || previous.outcome === 'skipped')) {
|
|
451
|
+
return finish({ ok: true, sessionId, skipped: 'already-consolidated', settled: true, snapshot: snapshotMeta, credential: previous })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 工具面必须可验证:父会话这一轮真实发给模型的工具表里若没有记忆写工具,
|
|
455
|
+
// fork 同样不会有——那就不 fork,明确 unavailable(C 包第 3 条)。
|
|
456
|
+
const probe = probeToolSurface({ ctx, sessionId, events })
|
|
457
|
+
if (!probe.ok) {
|
|
458
|
+
return finish({
|
|
459
|
+
ok: false, sessionId, code: 'unavailable', reasonCode: probe.reasonCode, settled: false, retryable: false,
|
|
460
|
+
toolSurface: probe.tools || null, ptc: probe.ptcObserved === true,
|
|
461
|
+
error: 'this conversation exposes no memory write tool to the model (' + String(probe.source || 'unknown') + '), so a consolidation worker could not write anything; consolidation is unavailable until the session has memu_private_remember / memu_private_update',
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
jobId = 'cons-' + Date.now().toString(36) + '-' + crypto.randomBytes(3).toString('hex')
|
|
466
|
+
const begun = safeLedger(() => beginJob({ sessionId, jobId, snapshot, binding }))
|
|
467
|
+
if (!begun) {
|
|
468
|
+
return finish({
|
|
469
|
+
ok: false, sessionId, settled: false, retryable: true, reasonCode: 'ledger-unavailable',
|
|
470
|
+
error: 'the consolidation lease could not be persisted; no worker was created',
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
if (begun.ok !== true) {
|
|
474
|
+
return finish({
|
|
475
|
+
ok: false, sessionId, running: true, settled: false, reasonCode: 'lease-held',
|
|
476
|
+
error: 'this session already holds a consolidation lease (' + String(begun.jobId) + ' until ' + new Date(begun.leaseUntil).toISOString() + '); the previous run is still unaccounted for',
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const header = captured.header || {}
|
|
481
|
+
// 每次整理都留"看了多少、新的多少、真正写了几条",否则"0 记忆"无法定位是
|
|
482
|
+
// 没触发、被门挡住还是提取空转。
|
|
483
|
+
const freshCount = unconsolidatedEvents(binding, events).length
|
|
484
|
+
let beforeIds = null
|
|
485
|
+
let beforeEntries = null
|
|
486
|
+
try {
|
|
487
|
+
const beforeIndex = loadPrivateIndex(binding.libraryId)
|
|
488
|
+
beforeIds = new Set(Object.keys(beforeIndex.entries))
|
|
489
|
+
beforeEntries = Object.fromEntries(Object.entries(beforeIndex.entries).map(([id, entry]) => [id, entryFingerprint(entry)]))
|
|
490
|
+
} catch { beforeIds = null; beforeEntries = null }
|
|
331
491
|
|
|
332
492
|
const agentOptions = inheritedAgentOptions(ctx, sessionId, events)
|
|
333
|
-
if (!agentOptions)
|
|
493
|
+
if (!agentOptions) {
|
|
494
|
+
safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'no-parent-route', retryable: true, error: 'cannot inherit the parent model/provider' }))
|
|
495
|
+
return finish({ ok: false, sessionId, reasonCode: 'no-parent-route', settled: false, error: 'cannot inherit the parent model/provider; consolidation fork was not created' })
|
|
496
|
+
}
|
|
334
497
|
|
|
498
|
+
childId = 'memu-consolidate-' + Date.now().toString(36) + '-' + crypto.randomBytes(3).toString('hex')
|
|
335
499
|
// `inheritedEventCount` is a TOP-LEVEL CreateAgentOptions field, NOT session
|
|
336
500
|
// metadata: SessionStore.prepare() rebuilds the header from a fixed meta
|
|
337
501
|
// whitelist (version, id, createdAt, cwd, parentSession, isSeeded, origin,
|
|
338
502
|
// delegationDepth, agentPreset), so a count placed in `meta` is silently
|
|
339
503
|
// dropped and Session's constructor then refuses the seeded session with
|
|
340
504
|
// "seeded session requires an inherited event count". Every fork died there.
|
|
505
|
+
//
|
|
506
|
+
// 种子只种到快照的 throughSeq:worker 看到的世界就是它要对账的那一段,
|
|
507
|
+
// 快照之后新发生的事件留给下一批。
|
|
341
508
|
handle = await agents.create({
|
|
342
509
|
sessionId: childId,
|
|
343
510
|
meta: {
|
|
@@ -349,9 +516,8 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
349
516
|
origin: 'subagent',
|
|
350
517
|
agentPreset: header.agentPreset,
|
|
351
518
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
seed: buildForkSeed(events, lastSeq),
|
|
519
|
+
inheritedEventCount: snapshot.throughSeq + 1,
|
|
520
|
+
seed: buildForkSeed(events, snapshot.throughSeq),
|
|
355
521
|
agentOptions,
|
|
356
522
|
signal: abort.signal,
|
|
357
523
|
})
|
|
@@ -364,21 +530,52 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
364
530
|
sessionId,
|
|
365
531
|
libraryId: binding.libraryId || null,
|
|
366
532
|
ok: true,
|
|
367
|
-
message: '整理已开始 · ' + (force ? '手动' : '空闲') + ' · fork ' + childId.slice(-8) + ' · ' + events.length + ' 条事件',
|
|
368
|
-
detail: { childId, force, events: events.length, phase: 'started' },
|
|
533
|
+
message: '整理已开始 · ' + (force ? '手动' : '空闲') + ' · fork ' + childId.slice(-8) + ' · ' + events.length + ' 条事件 · 快照 ' + snapshot.hash,
|
|
534
|
+
detail: { childId, jobId, force, events: events.length, phase: 'started', snapshot: snapshotMeta },
|
|
369
535
|
})
|
|
370
536
|
|
|
371
|
-
// turn/end
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
// success to the idle sweep. The listener is public and session-scoped; we
|
|
375
|
-
// register before followup so the event cannot race the wait.
|
|
537
|
+
// turn/end 是这一轮的唯一权威结果;whenIdle 只说明 driver 停了(失败的回合也停)。
|
|
538
|
+
// 顺带从同一个公共事件流里收三样东西:worker 的正文(回执在里面)、
|
|
539
|
+
// 它的工具动作(用来在写入途中重验父授权),以及父会话自己是否动过。
|
|
376
540
|
const childSession = handle?.agent?.session
|
|
541
|
+
const parentAgent = (() => { try { return agents.get?.(sessionId) || null } catch { return null } })()
|
|
542
|
+
const parentLive = parentAgent?.session || null
|
|
543
|
+
const isParentSession = (session) => session === parentLive || (session && parentLive === null && session?.id === sessionId)
|
|
544
|
+
const revalidate = (source) => {
|
|
545
|
+
if (scopeChanged) return
|
|
546
|
+
const now = scopeGenerationOf(getSessionBinding(sessionId))
|
|
547
|
+
if (!generationAtStart || now === generationAtStart) return
|
|
548
|
+
scopeChanged = { source, expected: generationAtStart, actual: now }
|
|
549
|
+
record({
|
|
550
|
+
kind: 'consolidate', scope: 'private', sessionId, libraryId: binding.libraryId || null, ok: false, level: 'warn',
|
|
551
|
+
message: '整理作废:父会话授权变化(' + source + ')',
|
|
552
|
+
detail: { jobId, childId, ...scopeChanged },
|
|
553
|
+
})
|
|
554
|
+
try { abort.abort() } catch {}
|
|
555
|
+
}
|
|
377
556
|
if (typeof ctx.on === 'function' && childSession) {
|
|
378
557
|
try {
|
|
379
558
|
offSession = ctx.on('session/event', (session, event) => {
|
|
380
|
-
if (session
|
|
381
|
-
|
|
559
|
+
if (session === childSession) {
|
|
560
|
+
if (event?.type === 'turn/end') { turnEnd = event; return }
|
|
561
|
+
if (event?.type === 'assistant/message') {
|
|
562
|
+
const blocks = event?.data?.message?.content
|
|
563
|
+
if (Array.isArray(blocks)) {
|
|
564
|
+
for (const block of blocks) {
|
|
565
|
+
if (block?.type === 'text' && typeof block.text === 'string' && block.text.trim()) assistantTexts.push(block.text)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
// 工具动作是写入的落点:每出现一次就重验一次父授权,变了立刻停掉这条 fork。
|
|
571
|
+
if (event?.type === 'tool/call' || event?.type === 'tool/result' || event?.type === 'tool/ptc-dispatch' || event?.type === 'tool/ptc-dispatch-start') {
|
|
572
|
+
revalidate('worker-write')
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
// 用户又说话了:父会话的库/域/上下文可能已经被切换,重新比一次指纹。
|
|
578
|
+
if (isParentSession(session) && (event?.type === 'turn/start' || event?.type === 'user/message')) revalidate('parent-activity')
|
|
382
579
|
})
|
|
383
580
|
} catch {}
|
|
384
581
|
}
|
|
@@ -387,6 +584,15 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
387
584
|
// freshly minted session id.
|
|
388
585
|
setSessionBinding(childId, {
|
|
389
586
|
libraryId: binding.libraryId,
|
|
587
|
+
consolidationFork: true,
|
|
588
|
+
// 整理发生在几小时后,父会话此刻的心境不是这段转录里那件事的情绪,所以 fork
|
|
589
|
+
// 不继承 mood。显式写 null 而不是留空:缺字段和「这一刻是中性」在写入时都走
|
|
590
|
+
// 同一条兜底,但只有 null 能把「后台 worker 没有当下心境」说清楚。
|
|
591
|
+
mood: null,
|
|
592
|
+
parentSessionId: sessionId,
|
|
593
|
+
parentScopeGeneration: generationAtStart,
|
|
594
|
+
snapshotHash: snapshot.hash,
|
|
595
|
+
jobId,
|
|
390
596
|
scopeKey: binding.scopeKey,
|
|
391
597
|
memoryOwnerSessionId: binding.memoryOwnerSessionId || sessionId,
|
|
392
598
|
memoryContext: binding.memoryContext || {},
|
|
@@ -396,7 +602,18 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
396
602
|
domainOverride: binding.domainOverride || null,
|
|
397
603
|
})
|
|
398
604
|
|
|
399
|
-
|
|
605
|
+
// 写入前最后一次重验:这次的授权还成立吗?不成立就别把 prompt 发出去。
|
|
606
|
+
revalidate('pre-dispatch')
|
|
607
|
+
if (scopeChanged) {
|
|
608
|
+
safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'parent-scope-changed', retryable: false, error: 'the parent conversation changed scope before dispatch' }))
|
|
609
|
+
return finish({
|
|
610
|
+
ok: false, sessionId, childId, settled: false, retryable: false, reasonCode: 'parent-scope-changed',
|
|
611
|
+
error: 'the parent conversation switched library, scope or memory context before the worker was dispatched; nothing was credited',
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const brief = buildWorkerBrief({ snapshot, probe, contract: receiptContractText({ jobId, snapshotHash: snapshot.hash }), reason: force ? 'manual' : 'auto', jobId })
|
|
616
|
+
handle.agent.followup(noticeMessage([buildPrivateConsolidationPrompt(sessionId), '', brief].join('\n'), '整理这场对话的私有记忆'))
|
|
400
617
|
// followup only queues. whenIdle says the driver stopped; turn/end says
|
|
401
618
|
// whether that turn actually succeeded, so both are checked below.
|
|
402
619
|
const idle = typeof handle.agent.whenIdle === 'function' ? handle.agent.whenIdle() : Promise.resolve()
|
|
@@ -406,40 +623,118 @@ export async function requestHistoricalConsolidation(ctx, sessionId, { maxWaitMs
|
|
|
406
623
|
const wait = new Promise((resolve) => { timer = setTimeout(() => resolve('timeout'), maxWaitMs) })
|
|
407
624
|
outcome = await Promise.race([idle.then(() => 'done'), wait])
|
|
408
625
|
} finally { clearTimeout(timer) }
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
626
|
+
|
|
627
|
+
let wrote = null
|
|
628
|
+
let decision = null
|
|
629
|
+
let completion = null
|
|
413
630
|
if (outcome === 'done') {
|
|
414
631
|
const failure = turnEndFailure(turnEnd)
|
|
415
|
-
if (failure)
|
|
416
|
-
|
|
632
|
+
if (failure) {
|
|
633
|
+
safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'fork-turn-failed', error: failure }))
|
|
634
|
+
return finish({ ok: false, error: 'consolidation fork failed: ' + failure, sessionId, childId, settled: false, timedOut: false })
|
|
635
|
+
}
|
|
636
|
+
// 父授权重验:写入可能已经落了一部分,但凭证只有在授权还成立时才算数。
|
|
637
|
+
const generationNow = scopeGenerationOf(getSessionBinding(sessionId))
|
|
638
|
+
if (scopeChanged || (generationAtStart && generationNow !== generationAtStart)) {
|
|
639
|
+
safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'parent-scope-changed', retryable: false, error: 'the parent conversation changed scope during consolidation' }))
|
|
640
|
+
return finish({
|
|
641
|
+
ok: false, sessionId, childId, settled: false, retryable: false, reasonCode: 'parent-scope-changed',
|
|
642
|
+
error: 'the parent conversation switched library, scope or memory context while the worker was running; the result was not credited (writes that already landed stay in the library, the range stays pending)',
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
// 兼容字段 lastSleepSeq 只能跟着凭证走:没有对账过的结果就不算整理过,
|
|
646
|
+
// 否则旧游标会把「失败」记成「已整理」,正是 C 包要消灭的那类谎。
|
|
647
|
+
if (beforeIds) {
|
|
648
|
+
try {
|
|
649
|
+
const afterIds = Object.keys(loadPrivateIndex(binding.libraryId).entries)
|
|
650
|
+
wrote = afterIds.filter((id) => !beforeIds.has(id)).length
|
|
651
|
+
} catch { wrote = null }
|
|
652
|
+
}
|
|
653
|
+
let index = null
|
|
654
|
+
try { index = loadPrivateIndex(binding.libraryId) } catch { index = null }
|
|
655
|
+
// 完成不是"没报错":worker 的回执 + 库里的真实变化,两边对上才结算。
|
|
656
|
+
const workerText = assistantTexts.join('\n')
|
|
657
|
+
decision = decideCompletion({ text: workerText, wrote, index, beforeIds, beforeEntries, expectedJobId: jobId, expectedSnapshotHash: snapshot.hash })
|
|
658
|
+
|
|
659
|
+
if (!decision.advance) {
|
|
660
|
+
safeLedger(() => failJob({
|
|
661
|
+
sessionId, jobId, reasonCode: decision.reasonCode || 'not-settled',
|
|
662
|
+
retryable: decision.retryable !== false, error: decision.error || null,
|
|
663
|
+
}))
|
|
664
|
+
return finish({
|
|
665
|
+
ok: false, sessionId, childId, settled: false, retryable: decision.retryable !== false,
|
|
666
|
+
status: decision.status, outcome: decision.outcome, reasonCode: decision.reasonCode,
|
|
667
|
+
counts: decision.counted || null, snapshot: snapshotMeta,
|
|
668
|
+
error: decision.error || ('consolidation result was not verified: ' + String(decision.reasonCode || 'unknown')),
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
completion = safeLedger(() => completeJob({
|
|
672
|
+
sessionId, jobId, snapshot,
|
|
673
|
+
covered: decision.covered,
|
|
674
|
+
status: decision.status,
|
|
675
|
+
outcome: decision.outcome,
|
|
676
|
+
reasonCode: decision.reasonCode,
|
|
677
|
+
retryable: decision.retryable === true,
|
|
678
|
+
counts: decision.counted,
|
|
679
|
+
receipt: decision.receipt,
|
|
680
|
+
}))
|
|
681
|
+
if (!completion?.advanced) {
|
|
682
|
+
return finish({ ok: false, sessionId, childId, settled: false, retryable: true,
|
|
683
|
+
reasonCode: 'ledger-not-advanced', snapshot: snapshotMeta,
|
|
684
|
+
error: 'the consolidation ledger could not verify the covered snapshot range' })
|
|
685
|
+
}
|
|
686
|
+
setSessionBinding(sessionId, { lastSleepSeq: snapshot.throughSeq })
|
|
687
|
+
safeLedger(() => recordOperation({ sessionId, key: opKey, outcome: decision.status, ids: decision.createdIds || [] }))
|
|
417
688
|
}
|
|
418
689
|
// A timeout is not success: the fork may still be mid-write. It has to come
|
|
419
690
|
// back with a message, not just `timedOut` — the settings button renders
|
|
420
691
|
// `result.error` and an empty string there is indistinguishable from a click
|
|
421
692
|
// that did nothing at all (observed: two clicks, no feedback, no memories).
|
|
422
693
|
if (outcome === 'timeout') {
|
|
694
|
+
safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'timeout', error: 'timed out before a receipt' }))
|
|
423
695
|
return finish({
|
|
424
696
|
ok: false,
|
|
425
697
|
sessionId,
|
|
426
698
|
childId,
|
|
427
699
|
settled: false,
|
|
428
700
|
timedOut: true,
|
|
429
|
-
|
|
701
|
+
reasonCode: 'timeout',
|
|
702
|
+
error: 'consolidation timed out after ' + Math.round(maxWaitMs / 1000) + 's; the fork was stopped (memories written before the timeout are kept). Retry, or consolidate a shorter conversation.',
|
|
430
703
|
})
|
|
431
704
|
}
|
|
432
|
-
return finish({
|
|
705
|
+
return finish({
|
|
706
|
+
ok: outcome === 'done',
|
|
707
|
+
sessionId,
|
|
708
|
+
childId,
|
|
709
|
+
settled: outcome === 'done',
|
|
710
|
+
timedOut: false,
|
|
711
|
+
status: decision?.status || null,
|
|
712
|
+
outcome: decision?.outcome || null,
|
|
713
|
+
reasonCode: decision?.reasonCode || null,
|
|
714
|
+
counts: decision?.counted || null,
|
|
715
|
+
createdIds: decision?.createdIds || [],
|
|
716
|
+
updatedIds: decision?.updatedIds || [],
|
|
717
|
+
heldIds: decision?.heldIds || [],
|
|
718
|
+
receipt: decision?.receipt
|
|
719
|
+
? { status: decision.receipt.status, outcome: decision.receipt.outcome, reasonCode: decision.receipt.reasonCode }
|
|
720
|
+
: null,
|
|
721
|
+
credential: completion?.checked ? { checked: completion.checked, pending: completion.pending } : null,
|
|
722
|
+
snapshot: snapshotMeta,
|
|
723
|
+
scanned: events.length,
|
|
724
|
+
fresh: freshCount,
|
|
725
|
+
wrote,
|
|
726
|
+
})
|
|
433
727
|
} catch (error) {
|
|
434
|
-
|
|
728
|
+
if (jobId) safeLedger(() => failJob({ sessionId, jobId, reasonCode: 'exception', error: String(error?.message || error) }))
|
|
729
|
+
return finish({ ok: false, sessionId, settled: false, reasonCode: 'exception', error: String(error?.message || error) })
|
|
435
730
|
} finally {
|
|
436
731
|
inFlight.delete(sessionId)
|
|
437
732
|
abort.abort()
|
|
438
733
|
try { offSession?.() } catch {}
|
|
439
734
|
try { await handle?.dispose?.() } catch {}
|
|
735
|
+
try { if (childId) deleteSessionBinding(childId) } catch {}
|
|
440
736
|
}
|
|
441
737
|
}
|
|
442
|
-
|
|
443
738
|
export function isConsolidating(sessionId) {
|
|
444
739
|
return inFlight.has(sessionId)
|
|
445
740
|
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- open_loop:只标外部阻塞(等对方决定、等发布、等一个跑不完的验证),不标「还没想清楚」。
|
|
29
29
|
- confidence:没完全确认就写低一点。
|
|
30
30
|
- 工程事实用 owner=about_project、mood_valence=0、arousal=0.35、peak=false;工作偏好才用 owner=about_user,不把排障焦虑当作事实准确性。
|
|
31
|
+
- 情绪按事件所在回合还原:emotion 描述的是那条记忆里的那次事件,不是整理时的心情;工程事实不写角色情绪,owner=about_project 固定中性。dominance 只在写人物侧条目时填(自己在推动为正、被推着走为负),工程事实省略。同一轮的多条条目各填各的,不把一次争吵的情绪串到所有条目。
|
|
31
32
|
|
|
32
33
|
永久记忆只留给「永远不能违反的约束」;普通决定不要 pin。
|
|
33
34
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
留成 `MM-DD` 会让它变成每年都响的周年。
|
|
23
23
|
- open_loop:真实未完的承诺或明确阻塞才标 true;普通犹豫、沉默与未知不自动形成待办。
|
|
24
24
|
- confidence:依据不足就标明未知并写低一点,不能用低置信度许可编造。
|
|
25
|
+
- 情绪按事件所在回合还原:用那一轮对话里的情绪填 emotion,不要用整理时的心情。同一轮的多条条目各填各的,不把一次争吵的情绪串到所有条目。工程事实(owner=about_project)固定中性。dominance 跟着 emotion 一起填:自己在推动为正、被推着走为负,没把握就省略。
|
|
25
26
|
|
|
26
27
|
永久记忆只留明确永久要求、有依据的核心约束,角色身份或侧事件理由仅适用于实际叙事;不要为了显得深刻而 pin。具体能否写永久、如何更新,以随后输出协议为准。
|
|
27
28
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- open_loop:仅明确的外部阻塞、未完成承诺或影响后续的已铺垫悬念;普通犹豫或未知不自动标 true。
|
|
27
27
|
- confidence:不确定就低一点。
|
|
28
28
|
- 工程事实用 owner=about_project、mood_valence=0、arousal=0.35、peak=false;人物经历的感受与事实置信分开,不让聊天情绪污染技术结论。
|
|
29
|
+
- 情绪按事件所在回合还原:人物侧填那一轮对话里的情绪(emotion/arousal/dominance),技术侧不写角色情绪;同一轮拆开的多条条目各填各的,一次争吵的情绪不串到所有条目。工程事实(owner=about_project)固定中性。dominance 只跟人物侧的 emotion 一起填,技术侧省略。
|
|
29
30
|
|
|
30
31
|
永久记忆只留给"忘了就不再是我"或绝不能违反的约束;普通决定不要 pin。
|
|
31
32
|
不要写全局记忆;全局记忆只由 curator 写。
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
- open_loop:只标真实未完的承诺或已铺垫且影响后续的悬念;普通沉默和未说出口的念头不自动标 true,兑现或明确取消后才关闭。
|
|
31
31
|
- confidence:不确定就低一点。
|
|
32
32
|
- mood_valence/arousal 是感受,confidence 是证据,不把情绪强度当准确性。owner 区分自己的体验、共同经历与对方明确说出的事;不为好召回而夸大 peak。
|
|
33
|
+
- 情绪按事件所在回合还原:emotion 填的是那一瞬间的感受,取那一轮对话里的情绪,不是整理时的心情。同一轮的多条条目各写各的,一场争吵的情绪不串到所有条目。工程事实(owner=about_project)固定中性。dominance 跟着 emotion 一起填:那一刻是自己推动的(正)还是被推着走的(负),没把握就省略,不要用 0 凑数。
|
|
33
34
|
|
|
34
35
|
永久记忆只在对方明确要求、随机侧事件、或极少数「忘了就不再是我」时使用;不要为了显得深刻而 pin。
|
|
35
36
|
不要写全局记忆;全局记忆只由 curator 写。
|
package/lib/prompts.js
CHANGED
|
@@ -19,7 +19,7 @@ export const PROMPT_DEFS = [
|
|
|
19
19
|
key: 'system-guide',
|
|
20
20
|
file: 'system-guide.md',
|
|
21
21
|
title: '记忆使用指南',
|
|
22
|
-
description: '
|
|
22
|
+
description: '以插件消息投放的常驻记忆规则:什么时候检索、什么时候写入、永久记忆与 unpin 的边界;按 domain 选变体,规则变化会在下一回合首步重新投放。',
|
|
23
23
|
variables: ['mustRememberCap', 'unpinBudget', 'shortTermTtlHours', 'anchorTokens', 'turnTokens'],
|
|
24
24
|
scope: 'private',
|
|
25
25
|
domainLayered: true,
|
|
@@ -28,7 +28,7 @@ export const PROMPT_DEFS = [
|
|
|
28
28
|
key: 'private-policy',
|
|
29
29
|
file: 'private-policy.md',
|
|
30
30
|
title: '私有记忆策略',
|
|
31
|
-
description: '
|
|
31
|
+
description: '字段规约模板;当前版本不再自动投放主会话,字段契约由工具 schema 与运行校验提供。保留供查阅和覆盖记录。',
|
|
32
32
|
variables: ['mustRememberCap', 'unpinBudget', 'shortTermTtlHours', 'anchorTokens', 'turnTokens'],
|
|
33
33
|
scope: 'private',
|
|
34
34
|
domainLayered: true,
|
|
@@ -84,8 +84,8 @@ export const CONSOLIDATION_CONTRACTS = {
|
|
|
84
84
|
local: '使用当前可用的记忆工具,先按主题检索去重。兼容细节用 memu_private_update 合并;真正新增或矛盾待复核的内容用 memu_private_remember,一条一件,不自行裁定冲突。不能查询或更新时如实保留限制,不宣称已核对或已修改。没有值得留的就直接结束,不要勉强写。',
|
|
85
85
|
remote: [
|
|
86
86
|
'只输出一个 JSON 对象,不要任何解释或 Markdown 代码块:',
|
|
87
|
-
'{"memories":[{"title":"≤30字","content":"完整正文","layer":"long_term|short_term","tags":["可选"],"open_loop":false,"confidence":0.0,"source":"inferred|user_said|imagined|told","visibility":"
|
|
88
|
-
'你没有记忆工具,也没有输入以外的索引:只返回最多 3 条候选,不声称已检索、合并、更新、裁决或完成写入。不要输出工具调用或 must_remember;矛盾候选在 content 中保留来源、时间与不确定性,交给后续复核。领域表达服从上文,字段服从本 JSON
|
|
87
|
+
'{"memories":[{"title":"≤30字","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
|
+
'你没有记忆工具,也没有输入以外的索引:只返回最多 3 条候选,不声称已检索、合并、更新、裁决或完成写入。不要输出工具调用或 must_remember;矛盾候选在 content 中保留来源、时间与不确定性,交给后续复核。领域表达服从上文,字段服从本 JSON 协议。emotion 只接受四轴 {"joy","anger","sorrow","fear"} 的份额对象,没有明确依据就用 null;dominance 是 -1..1 的掌控感。未确定的感受/日期用 null,不为填字段编造。',
|
|
89
89
|
'没有值得留的就输出 {"memories":[]}。',
|
|
90
90
|
].join('\n'),
|
|
91
91
|
}
|