dsh-memu-memory 1.1.2 → 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 +8 -6
- package/README.md +50 -5
- package/README.zh.md +49 -5
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +6 -4
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +40 -11
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -366
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +635 -0
- package/lib/client.js +728 -58
- 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/curator.js +81 -37
- package/lib/cycle.js +43 -6
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +36 -0
- package/lib/inject.js +232 -103
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +267 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +599 -57
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/mood.js +268 -39
- package/lib/paths.js +24 -0
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +450 -29
- 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 +54 -5
- package/lib/state.js +137 -172
- package/lib/tools.js +58 -7
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +968 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +7 -5
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +537 -0
- package/scripts/check-all.mjs +29 -1
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- package/scripts/repetition-eval.mjs +702 -0
- 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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主动记忆判定:一次闲时检查值不值得花 token。
|
|
3
|
+
*
|
|
4
|
+
* 三条输入都是"结构性"的,不做关键词判定:
|
|
5
|
+
* H 真人回合数(只算 source.kind === 'user' 的消息)
|
|
6
|
+
* C 新增证据量(真人文字 + 助手结论 + 去重后的工具结果,按字符估算)
|
|
7
|
+
* U 有效工具工作单元(去重的成功工具结果;轮询、空输出、重复错误只算一次)
|
|
8
|
+
*
|
|
9
|
+
* 触发规则(可被 settings 覆盖):
|
|
10
|
+
* - 会话空闲 < activeMemoryStarvationMinutes:分数必须 >= minScore;
|
|
11
|
+
* - 空闲 >= 饥饿阈值:小会话无条件检查(保证不会永远轮不到);
|
|
12
|
+
* - 大 transcript(>= largeTranscriptEvents)不吃饥饿豁免:真机回放里
|
|
13
|
+
* 5913 条事件的会话,一个"继续"回合只有 0.14~0.43 分,却会把整段
|
|
14
|
+
* 26MB 转录重新种进 fork;这种薄回合必须等分数达标;
|
|
15
|
+
* - 近期刚整理过 / 刚注入过记忆:直接抑制,避免"记忆后立刻再 fork 一次"。
|
|
16
|
+
*
|
|
17
|
+
* 纯函数,无 IO,便于回放标定与单测。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const ACTIVE_MEMORY_VERSION = 1
|
|
21
|
+
|
|
22
|
+
export const ACTIVE_MEMORY_POLICY = Object.freeze({
|
|
23
|
+
version: ACTIVE_MEMORY_VERSION,
|
|
24
|
+
minScore: 0.8,
|
|
25
|
+
starvationMinutes: 15,
|
|
26
|
+
consolidatedCooldownMinutes: 60,
|
|
27
|
+
recallCooldownMinutes: 15,
|
|
28
|
+
humanTurnsCap: 3,
|
|
29
|
+
evidenceCharsCap: 4000,
|
|
30
|
+
toolUnitsCap: 20,
|
|
31
|
+
partCap: 2,
|
|
32
|
+
// 超过这个规模,fork 的种子成本随整段转录增长,饥饿不再单独构成理由。
|
|
33
|
+
largeTranscriptEvents: 2000,
|
|
34
|
+
// 大 transcript 的守卫只该推迟一次检查,不该无限推迟。待整理区间一旦超过这个规模,
|
|
35
|
+
// 无论分数多低都要整理一次:游标不前移,那段内容就一直躺在 pending 里等着,
|
|
36
|
+
// 「省 token」和「永远不记」之间的差别是延迟上界,不是覆盖上界。
|
|
37
|
+
// 600 事件大约相当于一次中等长度的会话;20k 事件的首轮整理本来就贵,但那是内容
|
|
38
|
+
// 本身的价格,不是规则造成的浪费。
|
|
39
|
+
maxPendingEvents: 600,
|
|
40
|
+
weights: Object.freeze({ human: 0.35, content: 0.45, tool: 0.2 }),
|
|
41
|
+
// 单个工具结果计入证据量的上限;整段工具日志不能挤掉真人说的话。
|
|
42
|
+
toolResultCharsCap: 2000,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function textOfContent(content) {
|
|
46
|
+
if (typeof content === 'string') return content.trim()
|
|
47
|
+
if (!Array.isArray(content)) return ''
|
|
48
|
+
return content
|
|
49
|
+
.filter(block => block && block.type === 'text' && typeof block.text === 'string')
|
|
50
|
+
.map(block => block.text)
|
|
51
|
+
.join(' ')
|
|
52
|
+
.trim()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从一个事件窗口里收集证据。窗口应当只包含"上次检查之后"的事件;
|
|
57
|
+
* 调用方负责裁剪,这里不猜测边界。
|
|
58
|
+
*/
|
|
59
|
+
export function collectEvidence(events, policy = ACTIVE_MEMORY_POLICY) {
|
|
60
|
+
const evidence = {
|
|
61
|
+
humanTurns: 0,
|
|
62
|
+
humanChars: 0,
|
|
63
|
+
assistantChars: 0,
|
|
64
|
+
toolChars: 0,
|
|
65
|
+
toolUnits: 0,
|
|
66
|
+
toolCalls: 0,
|
|
67
|
+
lastHumanText: '',
|
|
68
|
+
}
|
|
69
|
+
const seenUnits = new Set()
|
|
70
|
+
for (const event of events || []) {
|
|
71
|
+
const type = event?.type
|
|
72
|
+
if (type === 'user/message') {
|
|
73
|
+
if (event.data?.source?.kind !== 'user') continue
|
|
74
|
+
const text = textOfContent(event.data?.content)
|
|
75
|
+
if (!text) continue
|
|
76
|
+
evidence.humanTurns += 1
|
|
77
|
+
evidence.humanChars += text.length
|
|
78
|
+
evidence.lastHumanText = text
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (type === 'assistant/message') {
|
|
82
|
+
const blocks = event.data?.message?.content
|
|
83
|
+
if (!Array.isArray(blocks)) continue
|
|
84
|
+
for (const block of blocks) {
|
|
85
|
+
if (block?.type === 'tool-call') evidence.toolCalls += 1
|
|
86
|
+
else if (block?.type === 'text' && typeof block.text === 'string') evidence.assistantChars += block.text.trim().length
|
|
87
|
+
}
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (type === 'tool/call') {
|
|
91
|
+
evidence.toolCalls += 1
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (type === 'tool/result') {
|
|
95
|
+
const message = event.data?.message || {}
|
|
96
|
+
if (message.isError === true) continue
|
|
97
|
+
const text = textOfContent(message.content)
|
|
98
|
+
if (!text) continue
|
|
99
|
+
// 同一份结果重复出现(轮询、重读、重试)只算一次工作单元。
|
|
100
|
+
const key = text.slice(0, 160) + '|' + text.length
|
|
101
|
+
if (seenUnits.has(key)) continue
|
|
102
|
+
seenUnits.add(key)
|
|
103
|
+
evidence.toolUnits += 1
|
|
104
|
+
evidence.toolChars += Math.min(text.length, policy.toolResultCharsCap)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return evidence
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 可解释评分:每个分项封顶,避免"只刷工具次数"把分数拉满。 */
|
|
111
|
+
export function scoreEvidence(evidence, policy = ACTIVE_MEMORY_POLICY) {
|
|
112
|
+
const human = Number(evidence?.humanTurns) || 0
|
|
113
|
+
const contentChars = (Number(evidence?.humanChars) || 0) + (Number(evidence?.assistantChars) || 0) + (Number(evidence?.toolChars) || 0)
|
|
114
|
+
const units = Number(evidence?.toolUnits) || 0
|
|
115
|
+
const h = Math.min(human / policy.humanTurnsCap, policy.partCap)
|
|
116
|
+
const c = Math.min(contentChars / policy.evidenceCharsCap, policy.partCap)
|
|
117
|
+
const u = Math.min(units / policy.toolUnitsCap, policy.partCap)
|
|
118
|
+
const score = policy.weights.human * h + policy.weights.content * c + policy.weights.tool * u
|
|
119
|
+
return {
|
|
120
|
+
score: Math.round(score * 1000) / 1000,
|
|
121
|
+
parts: { human: h, content: c, tool: u, humanTurns: human, contentChars, toolUnits: units },
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 是否唤醒检查。真人回合是硬门槛(纯工具轮询不构成检查理由),
|
|
127
|
+
* 其余交给分数;`starved` 由调用方按空闲时长传入。
|
|
128
|
+
* `transcriptEvents` 是整段会话的事件数,用来决定饥饿豁免是否还有成本意义。
|
|
129
|
+
*/
|
|
130
|
+
export function assessWake({ evidence, starved = false, transcriptEvents = 0, pendingEvents = 0, policy = ACTIVE_MEMORY_POLICY } = {}) {
|
|
131
|
+
const scored = scoreEvidence(evidence, policy)
|
|
132
|
+
if ((Number(evidence?.humanTurns) || 0) === 0) {
|
|
133
|
+
return { wake: false, reason: 'no-human-turn', ...scored }
|
|
134
|
+
}
|
|
135
|
+
// 积压优先于守卫:守卫的理由是「为一句轻话重种整段转录不划算」,但当整段转录本身
|
|
136
|
+
// 还没整理过时,那不是重种,是欠着。没有这条,一个大 transcript 只要每轮都是轻话,
|
|
137
|
+
// 就永远等不到检查——延迟没有上界,长会话的早期决定再也进不了记忆。
|
|
138
|
+
if (Number(pendingEvents) >= Number(policy.maxPendingEvents || Infinity)) {
|
|
139
|
+
return { wake: true, reason: 'backlog', ...scored }
|
|
140
|
+
}
|
|
141
|
+
const large = (Number(transcriptEvents) || 0) >= policy.largeTranscriptEvents
|
|
142
|
+
if (starved && !large) return { wake: true, reason: 'starved', ...scored }
|
|
143
|
+
if (scored.score < policy.minScore) {
|
|
144
|
+
return { wake: false, reason: large && starved ? 'large-transcript-thin' : 'low-score', ...scored }
|
|
145
|
+
}
|
|
146
|
+
return { wake: true, reason: 'score', ...scored }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 抑制原因:近期整理过(避免记忆后马上再 fork)或近期注入过记忆。
|
|
151
|
+
* 返回 null 表示不抑制。
|
|
152
|
+
*/
|
|
153
|
+
export function suppressionReason({
|
|
154
|
+
now = Date.now(),
|
|
155
|
+
lastSleepAt = null,
|
|
156
|
+
lastInjectedAt = null,
|
|
157
|
+
consolidatedCooldownMinutes = ACTIVE_MEMORY_POLICY.consolidatedCooldownMinutes,
|
|
158
|
+
recallCooldownMinutes = ACTIVE_MEMORY_POLICY.recallCooldownMinutes,
|
|
159
|
+
} = {}) {
|
|
160
|
+
const sleep = Date.parse(lastSleepAt || '') || 0
|
|
161
|
+
if (sleep && now - sleep < consolidatedCooldownMinutes * 60_000) return 'recent-consolidation'
|
|
162
|
+
const recalled = Date.parse(lastInjectedAt || '') || 0
|
|
163
|
+
if (recalled && now - recalled < recallCooldownMinutes * 60_000) return 'recent-recall'
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { logsDir, ensureDir, redactSecrets, warn } from './paths.js'
|
|
5
|
+
|
|
6
|
+
// 记忆链路的活动日志:召回、写入、整理、向量。用途是回答"刚才那一下到底走了哪条路、
|
|
7
|
+
// 召回/写入了什么、为什么没写成",而不是给开发者看堆栈。
|
|
8
|
+
//
|
|
9
|
+
// 三条硬约束:
|
|
10
|
+
// 1. 永不抛错。日志坏了不能把召回/写入一起弄坏(fail-open),失败只 warn 一次。
|
|
11
|
+
// 2. 永不无限增长。内存里保留最近 MAX_ENTRIES 条,文件超过 MAX_FILE_BYTES 时重写成
|
|
12
|
+
// 最近一半;这是本机诊断用的环形日志,不是审计材料。
|
|
13
|
+
// 3. 出盘前脱敏。message 与 detail 都过 redactSecrets;detail 只留诊断需要的字段,
|
|
14
|
+
// 正文最多 200 字符(召回查询、记忆标题),足够定位问题又不把整段对话抄进日志。
|
|
15
|
+
|
|
16
|
+
export const ACTIVITY_KINDS = Object.freeze(['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'system', 'check'])
|
|
17
|
+
const MAX_ENTRIES = 2000
|
|
18
|
+
const MAX_FILE_BYTES = 4 * 1024 * 1024
|
|
19
|
+
const MAX_MESSAGE_CHARS = 300
|
|
20
|
+
const MAX_DETAIL_CHARS = 4000
|
|
21
|
+
const MAX_QUERY_CHARS = 200
|
|
22
|
+
|
|
23
|
+
let cache = null
|
|
24
|
+
let loadWarned = false
|
|
25
|
+
let writeWarned = false
|
|
26
|
+
|
|
27
|
+
function activityFile() {
|
|
28
|
+
return path.join(logsDir(), 'activity.jsonl')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clamp(value, max) {
|
|
32
|
+
const text = String(value ?? '')
|
|
33
|
+
return text.length > max ? text.slice(0, max - 1) + '…' : text
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeDetail(value) {
|
|
37
|
+
if (value === undefined || value === null) return null
|
|
38
|
+
let text
|
|
39
|
+
try { text = JSON.stringify(value) } catch { text = JSON.stringify({ note: 'detail is not serializable' }) }
|
|
40
|
+
text = redactSecrets(text)
|
|
41
|
+
if (text.length > MAX_DETAIL_CHARS) text = text.slice(0, MAX_DETAIL_CHARS - 1) + '…'
|
|
42
|
+
try { return JSON.parse(text) } catch { return { text: text.slice(0, MAX_DETAIL_CHARS - 20) } }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalize(event = {}) {
|
|
46
|
+
const kind = ACTIVITY_KINDS.includes(event.kind) ? event.kind : 'system'
|
|
47
|
+
const level = ['info', 'warn', 'error'].includes(event.level) ? event.level : (event.ok === false ? 'warn' : 'info')
|
|
48
|
+
const at = typeof event.at === 'string' && !Number.isNaN(Date.parse(event.at)) ? event.at : new Date().toISOString()
|
|
49
|
+
const refs = Array.isArray(event.refs) ? event.refs.filter(item => typeof item === 'string' && item).slice(0, 20) : []
|
|
50
|
+
return {
|
|
51
|
+
id: 'act-' + Date.now().toString(36) + '-' + crypto.randomBytes(3).toString('hex'),
|
|
52
|
+
at,
|
|
53
|
+
kind,
|
|
54
|
+
level,
|
|
55
|
+
scope: event.scope === 'private' || event.scope === 'global' ? event.scope : null,
|
|
56
|
+
sessionId: typeof event.sessionId === 'string' ? event.sessionId : null,
|
|
57
|
+
libraryId: typeof event.libraryId === 'string' ? event.libraryId : null,
|
|
58
|
+
message: redactSecrets(clamp(event.message, MAX_MESSAGE_CHARS)),
|
|
59
|
+
detail: safeDetail(event.detail),
|
|
60
|
+
refs,
|
|
61
|
+
ok: event.ok !== false,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseLines(text) {
|
|
66
|
+
const out = []
|
|
67
|
+
for (const line of text.split('\n')) {
|
|
68
|
+
if (!line.trim()) continue
|
|
69
|
+
try {
|
|
70
|
+
const value = JSON.parse(line)
|
|
71
|
+
if (value && typeof value === 'object' && typeof value.at === 'string') out.push(value)
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function load() {
|
|
78
|
+
if (cache) return cache
|
|
79
|
+
cache = []
|
|
80
|
+
try {
|
|
81
|
+
const file = activityFile()
|
|
82
|
+
if (fs.existsSync(file)) cache = parseLines(fs.readFileSync(file, 'utf8')).slice(-MAX_ENTRIES)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (!loadWarned) {
|
|
85
|
+
loadWarned = true
|
|
86
|
+
try { warn('activity log could not be read: ' + (error?.message || error) + ' (recall and writes continue; the log starts empty)') } catch {}
|
|
87
|
+
}
|
|
88
|
+
cache = []
|
|
89
|
+
}
|
|
90
|
+
return cache
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rewrite(entries) {
|
|
94
|
+
const file = activityFile()
|
|
95
|
+
ensureDir(path.dirname(file))
|
|
96
|
+
const temp = file + '.tmp-' + process.pid
|
|
97
|
+
fs.writeFileSync(temp, entries.map(entry => JSON.stringify(entry)).join('\n') + (entries.length ? '\n' : ''), 'utf8')
|
|
98
|
+
fs.renameSync(temp, file)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 记录一条活动。永不抛错;返回写入的条目(失败时返回 null)。 */
|
|
102
|
+
export function record(event) {
|
|
103
|
+
try {
|
|
104
|
+
const entry = normalize(event)
|
|
105
|
+
const entries = load()
|
|
106
|
+
entries.push(entry)
|
|
107
|
+
if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
|
|
108
|
+
try {
|
|
109
|
+
const file = activityFile()
|
|
110
|
+
ensureDir(path.dirname(file))
|
|
111
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8')
|
|
112
|
+
let size = 0
|
|
113
|
+
try { size = fs.statSync(file).size } catch {}
|
|
114
|
+
if (size > MAX_FILE_BYTES) rewrite(entries.slice(-Math.floor(MAX_ENTRIES / 2)))
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (!writeWarned) {
|
|
117
|
+
writeWarned = true
|
|
118
|
+
try { warn('activity log could not be written: ' + (error?.message || error) + ' (memory operations are unaffected)') } catch {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return entry
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 最近的活动,最新在前。`q` 只做字面包含匹配(大小写不敏感),不解释正则:
|
|
129
|
+
* 这是给人看的筛选框,不是查询语言。
|
|
130
|
+
*/
|
|
131
|
+
export function listEvents({ limit = 200, kind = null, level = null, scope = null, sessionId = null, since = null, q = null } = {}) {
|
|
132
|
+
const wanted = ACTIVITY_KINDS.includes(kind) ? kind : null
|
|
133
|
+
const wantedLevel = ['info', 'warn', 'error'].includes(level) ? level : null
|
|
134
|
+
const sinceTime = typeof since === 'string' && !Number.isNaN(Date.parse(since)) ? Date.parse(since) : null
|
|
135
|
+
const needle = typeof q === 'string' && q.trim() ? q.trim().toLowerCase() : null
|
|
136
|
+
const count = Math.max(1, Math.min(MAX_ENTRIES, Number(limit) || 200))
|
|
137
|
+
const rows = []
|
|
138
|
+
const entries = load()
|
|
139
|
+
for (let index = entries.length - 1; index >= 0 && rows.length < count; index -= 1) {
|
|
140
|
+
const entry = entries[index]
|
|
141
|
+
if (wanted && entry.kind !== wanted) continue
|
|
142
|
+
if (wantedLevel && entry.level !== wantedLevel) continue
|
|
143
|
+
if (scope && entry.scope !== scope) continue
|
|
144
|
+
if (sessionId && entry.sessionId !== sessionId) continue
|
|
145
|
+
if (sinceTime && (Date.parse(entry.at) || 0) < sinceTime) continue
|
|
146
|
+
if (needle) {
|
|
147
|
+
const haystack = (entry.message + ' ' + JSON.stringify(entry.detail || {}) + ' ' + (entry.refs || []).join(' ')).toLowerCase()
|
|
148
|
+
if (!haystack.includes(needle)) continue
|
|
149
|
+
}
|
|
150
|
+
rows.push(entry)
|
|
151
|
+
}
|
|
152
|
+
return rows
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function logStats() {
|
|
156
|
+
const entries = load()
|
|
157
|
+
const counts = {}
|
|
158
|
+
for (const entry of entries) counts[entry.kind] = (counts[entry.kind] || 0) + 1
|
|
159
|
+
let bytes = 0
|
|
160
|
+
try { bytes = fs.statSync(activityFile()).size } catch {}
|
|
161
|
+
return { total: entries.length, bytes, counts, file: activityFile(), capped: MAX_ENTRIES, maxBytes: MAX_FILE_BYTES }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 清空日志(显式操作;不是自动裁剪)。 */
|
|
165
|
+
export function clearEvents() {
|
|
166
|
+
cache = []
|
|
167
|
+
try { rewrite([]) } catch (error) {
|
|
168
|
+
try { warn('activity log could not be cleared: ' + (error?.message || error)) } catch {}
|
|
169
|
+
}
|
|
170
|
+
return { ok: true }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 导出为可直接贴进 issue 的 JSONL 文本。 */
|
|
174
|
+
export function exportEvents(options = {}) {
|
|
175
|
+
return listEvents({ limit: Math.min(MAX_ENTRIES, Number(options.limit) || MAX_ENTRIES), ...options })
|
|
176
|
+
.map(entry => JSON.stringify(entry))
|
|
177
|
+
.join('\n')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 测试用:丢掉进程内缓存,下一次调用重新读盘。 */
|
|
181
|
+
export function resetActivityLogCache() {
|
|
182
|
+
cache = null
|
|
183
|
+
loadWarned = false
|
|
184
|
+
writeWarned = false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 一个自动裁剪的便捷入口:`query` 是召回/整理里最常要看的东西,统一截断。
|
|
188
|
+
export function clipQuery(value) {
|
|
189
|
+
return clamp(redactSecrets(String(value ?? '')), MAX_QUERY_CHARS)
|
|
190
|
+
}
|