solopup-memory 0.21.0-beta.1 → 0.21.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.en.md +7 -3
- package/README.md +13 -3
- package/RELEASE.md +28 -1
- package/SECURITY.md +4 -0
- package/capture-llm.mjs +8 -2
- package/capture-source-aliases.mjs +20 -0
- package/index.mjs +40 -16
- package/memory-protocol.mjs +21 -0
- package/package.json +8 -3
- package/privacy.mjs +25 -0
- package/test-explicit-memory.mjs +43 -0
- package/test-native-write-validation.mjs +6 -0
- package/test-privacy.mjs +65 -0
- package/test-restore.mjs +1 -1
- package/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0-beta.3 - private-memory exposure reduction
|
|
4
|
+
|
|
5
|
+
- Exclude recognized personal profiles, birth details, health, intimate and private contact information from automatic index projection and unrelated capture/review context, including private source quotations.
|
|
6
|
+
- Hide these records from default current/history memory searches; explicit includePrivate=true enables on-demand retrieval and its normal log/model exposure.
|
|
7
|
+
- Preserve stored records, source identities and append-only session logs. Forget results now distinguish index withdrawal from historical erasure.
|
|
8
|
+
- This is a conservative content classifier, not exhaustive PII detection, encryption, a file-access boundary, or remote deletion. Raw Markdown, source ledgers and existing archives still require separate handling.
|
|
9
|
+
|
|
10
|
+
## 0.21.0-beta.2 — explicit memory workflow
|
|
11
|
+
|
|
12
|
+
- Move model-facing memory instructions into a separate protocol context, available with an empty index and reporting scoped write-tool availability.
|
|
13
|
+
- Direct explicit durable-memory requests to the real tool in the same turn; distinguish recorded/duplicate, pending and rejected results. Personal statements remain claims; guesses are not promoted.
|
|
14
|
+
- Bind short automatic-capture source labels back to original message identities before the unchanged role and quotation checks.
|
|
15
|
+
- Preserve source validation, managed withdrawal, storage schemas and historical beta.1 evidence. This remains a public beta.
|
|
16
|
+
|
|
3
17
|
## 0.21.0-beta.1 — SoloPup Memory public beta
|
|
4
18
|
|
|
5
19
|
- Publish the dev.45-derived plugin as SoloPup Memory (`solopup-memory`), with localized DSH metadata, executable ESM and a Git-compatible bundle.
|
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Remember what matters. Keep up when you change your mind.** · [中文](README.md)
|
|
4
4
|
|
|
5
|
-
Source-linked, layered **text memory** for DeepSeek Harness. **0.21.0-beta.
|
|
5
|
+
Source-linked, layered **text memory** for DeepSeek Harness. **0.21.0-beta.3 is a public beta, not a stable release.** See [RELEASE.md](RELEASE.md) for tested scope and known gaps.
|
|
6
6
|
|
|
7
7
|
- Separate global preferences from project decisions.
|
|
8
8
|
- Keep original owner-message citations, conditions, exceptions and optional expiry.
|
|
@@ -14,10 +14,10 @@ Source-linked, layered **text memory** for DeepSeek Harness. **0.21.0-beta.1 is
|
|
|
14
14
|
## Install in DSH
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
dsh plugin --profile desktop add github:OakcoderX/solopup-memory#v0.21.0-beta.
|
|
17
|
+
dsh plugin --profile desktop add github:OakcoderX/solopup-memory#v0.21.0-beta.3 --ignore-scripts
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
This pins the privacy update; the release page supplies a full commit SHA for an immutable Git pin. npm availability may follow the GitHub release: check the registry and release notes before installing `solopup-memory@0.21.0-beta.3`. The npm beta tag may still point to an earlier release. This package ships executable ESM and has no dependency installation or build scripts. Alternatively download its release tarball and install the local file with `dsh plugin --profile desktop add /absolute/path/to/solopup-memory-0.21.0-beta.3.tgz --offline --ignore-scripts`.
|
|
21
21
|
|
|
22
22
|
Restart the profile and use `memory_status`. Existing legacy stores may require explicit local review. Do not delete control files to bypass that review. If upgrading from `dsh-memory`, create a full private backup, stop the hosts using that data, remove the old package, then install SoloPup Memory. Do not run both packages together. The data location and legacy format identifiers are intentionally retained.
|
|
23
23
|
|
|
@@ -58,3 +58,7 @@ node accept.mjs --profile desktop
|
|
|
58
58
|
`verify` additionally requires a locally installed DSH and runs isolated load and mechanical gates. `accept` reads the selected profile and the live version marker; it does not certify natural-dialogue quality. See [SECURITY.md](SECURITY.md), [COMPATIBILITY.md](COMPATIBILITY.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
59
59
|
|
|
60
60
|
MIT License. Copyright © 2026 oakfilm.
|
|
61
|
+
|
|
62
|
+
Explicit requests to remember durable information are directed to the native `memory_write` tool in the same turn. A separate protocol context is available even when the memory store is empty. Personal self-reports remain claims; assistant guesses are not confirmed facts. Automatic capture uses short, code-bound source labels before the original identity and quote checks. This remains a public beta, and instructions do not guarantee model compliance.
|
|
63
|
+
|
|
64
|
+
Recognized private content is excluded from automatic memory projection and unrelated saved-memory context in capture/admission review. Current and historical search hide it by default; includePrivate=true enables explicit on-demand retrieval. These are conservative content rules, not exhaustive classification or a filesystem security boundary. Raw Markdown and provenance remain local plaintext. Explicit retrieval still enters session history and model requests. Existing session archives, exports, backups and already-sent data are not erased.
|
package/README.md
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
**记住重要的事,跟上你的改变。** · [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
当前公开版本为 **0.21.0-beta.
|
|
5
|
+
当前公开版本为 **0.21.0-beta.3**,是文字记忆测试版,尚未通过稳定版验收。已验证范围和已知问题见 [RELEASE.md](RELEASE.md)。
|
|
6
6
|
|
|
7
7
|
把用户明确表达的长期偏好、项目决定、可查证线索和暂定信念保存下来,在后续对话中按项目与适用条件找回。首次公开版本只处理文字。
|
|
8
8
|
|
|
9
|
+
## 个人资料与按需读取
|
|
10
|
+
|
|
11
|
+
已识别的生日、性别、健康、亲密关系和私人联系方式默认不进入自动索引投影,也不作为无关后台整理或写入审查的旧记忆发送。普通 `memory_search` 默认隐藏这些内容;用户询问相关资料时,才明确使用 `includePrivate: true`。主题文件含有未分段的敏感内容时,普通搜索会保守跳过整个主题文件,安全的索引摘要仍可检索。
|
|
12
|
+
|
|
13
|
+
这是内容规则提供的降载保护,不是完整的隐私分类器。未识别的改写仍可能漏过;文件工具仍能读原始文件,按需读取结果也会进入当前会话日志和模型请求。此版本没有把原始 `MEMORY.md` 改成无明文的指针库,没有擦除既有日志、缓存、备份或已发送数据。`memory_forget` 表示停止使用索引条目,不表示彻底删除个人信息。
|
|
14
|
+
|
|
9
15
|
## 工作过程
|
|
10
16
|
|
|
11
17
|
1. 从本会话已接收的用户消息提出候选,并绑定原话引用。
|
|
@@ -13,6 +19,8 @@
|
|
|
13
19
|
3. 通过检查后才发布到有效记忆。明确拒绝的不写入;无法判断的新候选可以留在不自动注入的待审区。未通过的替换请求不降格成普通追加。
|
|
14
20
|
4. 后续对话自动得到受预算限制的索引;检索同时查当前项目与全局库,结果标明归属。其他项目不会因此自动变成当前项目的记忆。
|
|
15
21
|
|
|
22
|
+
用户明确要求记住持久信息时,插件指令要求本轮使用正式 `memory_write`,等待实际结果再确认;不等待后台整理。本人陈述的个人资料按未独立核实的 `claim` 保存,助手猜测不能冒充本人确认。模型仍可能不遵循指令,因此需要实际调用与新会话验收。
|
|
23
|
+
|
|
16
24
|
默认每四轮尝试一次后台整理。主动 `memory_write` 也经过来源审查。`pending-review`、`rejected`、`not-applied` 都不表示已经记住。
|
|
17
25
|
|
|
18
26
|
## 记忆的层次
|
|
@@ -40,10 +48,10 @@ Markdown 索引和主题正文便于阅读;来源、候选、历史与调用
|
|
|
40
48
|
使用 DeepSeek Harness 官方插件管理器安装。代码为可直接执行的 ESM,无安装时构建脚本,也不需要 npm 账户。以下以 desktop profile 为例。
|
|
41
49
|
|
|
42
50
|
```sh
|
|
43
|
-
dsh plugin --profile desktop add github:OakcoderX/solopup-memory#v0.21.0-beta.
|
|
51
|
+
dsh plugin --profile desktop add github:OakcoderX/solopup-memory#v0.21.0-beta.3 --ignore-scripts
|
|
44
52
|
```
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
以上固定本次修复版本;需要不可变 Git 来源时,用发行页的完整提交 SHA 替换标签。npm 发布可能晚于 GitHub,请先核对注册表和发行说明,再使用 `dsh plugin --profile desktop add solopup-memory@0.21.0-beta.3 --ignore-scripts`;旧的 `beta` 标签可能仍指向先前版本。也可下载发行页的 `.tgz`,使用 `dsh plugin --profile desktop add /absolute/path/to/solopup-memory-0.21.0-beta.3.tgz --offline --ignore-scripts`。
|
|
47
55
|
|
|
48
56
|
已安装旧名 `dsh-memory` 时,先完成完整备份、关闭使用该库的宿主,用官方管理器移除旧包,再安装新包;不要同时启用两个记忆插件。新名字继续使用原记忆库和控制格式。
|
|
49
57
|
|
|
@@ -100,3 +108,5 @@ node accept.mjs --profile desktop
|
|
|
100
108
|
详细安全边界见 [SECURITY.md](SECURITY.md),宿主兼容范围见 [COMPATIBILITY.md](COMPATIBILITY.md)。
|
|
101
109
|
|
|
102
110
|
用户单条消息的完整来源窗口目前上限为 2000 字符。超长消息不从前半段建立记忆;自动整理显示来源不完整并暂不执行,主动写入也拒绝使用仍在当前核对窗口内的截断来源。可在新的短消息会话中完整重述需长期保留的约定。这个修复不会自动纠正旧版已经保存的解释。
|
|
111
|
+
|
|
112
|
+
市场源请求失败时,按 [DSH 官方代理手册](https://deepseek-harness.github.io/deepseek-harness/guide/network-proxy) 配置 DSH 自身的代理环境;DSH 不自动读取 macOS 系统代理。
|
package/RELEASE.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
# 0.21.0-beta.3 - private-memory exposure reduction
|
|
2
|
+
|
|
3
|
+
Public beta. Conservative known-category filtering, not full privacy isolation or stable semantic acceptance.
|
|
4
|
+
|
|
5
|
+
Recognized private summaries, details and quoted evidence are excluded from automatic memory projection and unrelated saved-memory context for capture and admission review. Default current/history searches hide this content; includePrivate=true allows on-demand retrieval. Existing records and original session logs are preserved. File tools can still read raw data, classifier coverage is incomplete, and on-demand output still enters logs and model requests. Raw-index pointer migration and physical history cleanup are not part of this release.
|
|
6
|
+
|
|
7
|
+
Prior beta.2 evidence below is historical and not automatically a beta.3 result.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 0.21.0-beta.2 — explicit memory workflow update
|
|
12
|
+
|
|
13
|
+
Public beta, not stable semantic acceptance. The beta.1 evidence in the historical section below is not automatically a beta.2 result.
|
|
14
|
+
|
|
15
|
+
- Add a separate, scoped protocol context directing explicit remember requests to the real memory tool in the same turn, with truthful success/pending/failure reporting. It is available even with an empty index. Model compliance is still checked through actual calls, not assumed from instructions.
|
|
16
|
+
- Keep personal self-reports as claims and assistant guesses separate; do not use shell imports or fabricated provenance to bypass native validation.
|
|
17
|
+
- Shorten automatic-capture message labels with a request-local code binding. Original role, quote, scope and source withdrawal checks still apply.
|
|
18
|
+
- No data schema change; existing records and provenance are preserved. Existing semantic and platform gaps remain open.
|
|
19
|
+
|
|
20
|
+
Verified in the daily macOS DSH host using the actual DeepSeek account and Standard preset: an ordinary remember request invoked the native memory tool and was recorded in its first turn; a fresh conversation without inherited history recovered the project fact. A separate fresh conversation recovered an existing global personal fact. One real extraction preserved the original user source through the short labels and omitted an assistant-invented place. Synthetic test sources were withdrawn and original Markdown remained unchanged.
|
|
21
|
+
|
|
22
|
+
The first test attempt exhausted its own call cap before the global-personal query could answer; that attempt is retained as incomplete, with a separately identified successful follow-up. These bounded cases do not establish general semantic accuracy or stable-release readiness.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Historical beta.1 release evidence
|
|
27
|
+
|
|
1
28
|
# 0.21.0-beta.1 — public beta / 公开测试版
|
|
2
29
|
|
|
3
30
|
SoloPup Memory is the public product name of the former local `dsh-memory` plugin. This release derives from the privately tested dev.45 implementation. It is **not stable** and does not claim superiority over other memory systems.
|
|
@@ -23,4 +50,4 @@ Stable release still requires prospective semantic acceptance, handling/document
|
|
|
23
50
|
|
|
24
51
|
- [Package and install a plugin](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md): the package declares `dsh.bundle.patch`; direct Git and archive installation are supported.
|
|
25
52
|
- [Plugin markets](https://deepseek-harness.github.io/deepseek-harness/en/guide/plugin-market): publish a format-1 HTTPS index with a Git version pinned to a full commit; users add the source themselves.
|
|
26
|
-
- The official [DSH site](https://www.deepseek.com/en/harness/) links its community plugin entry to the GitHub `dsh-plugin` topic. The repository uses that topic. No central listing approval
|
|
53
|
+
- The official [DSH site](https://www.deepseek.com/en/harness/) links its community plugin entry to the GitHub `dsh-plugin` topic. The repository uses that topic. No central listing approval is claimed. The same tested beta archive was subsequently published to npm as `solopup-memory@0.21.0-beta.1` with the `beta` tag; the market index now installs this exact npm version.
|
package/SECURITY.md
CHANGED
|
@@ -71,3 +71,7 @@
|
|
|
71
71
|
## 报告问题
|
|
72
72
|
|
|
73
73
|
私密报告:在本机开 issue 或直接告知维护者,不要在公开渠道贴出可利用细节。
|
|
74
|
+
|
|
75
|
+
## beta.3 privacy boundary
|
|
76
|
+
|
|
77
|
+
Recognized private records, including their quoted evidence, are omitted from automatic projection and unrelated saved-memory contexts. The classifier is heuristic and incomplete; explicit [private] or [private:category] text is conservatively treated as on-demand. Searches default to excluding recognized private content; includePrivate=true is a disclosure control, not a user authentication boundary. Other tools can read the raw files. Private facts explicitly present in the current conversation or prior ordinary messages remain in that conversation. Existing append-only archives, caches, exports, backups and provider-side copies are not purged. Never claim that hiding an index or memory_forget erases those copies. Physical raw-index migration and reviewed local erasure are separate work.
|
package/capture-llm.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {createHash} from 'node:crypto'
|
|
|
15
15
|
import {TEXT_MEMORY_POLICY} from './memory-policy.mjs'
|
|
16
16
|
import {VALIDATION_CONTRACT} from './capture-validation-contract.mjs'
|
|
17
17
|
import {CaptureSubmission,CAPTURE_SUBMISSION_TOOL} from './capture-submission.mjs'
|
|
18
|
+
import {captureSourceAliases} from './capture-source-aliases.mjs'
|
|
18
19
|
import {parseCaptureBatch,CAPTURE_RESPONSE_BYTES} from './capture-diagnostics.mjs'
|
|
19
20
|
import { escapeFraming, findSecrets, normalizeText, redactSecrets } from './security.mjs'
|
|
20
21
|
|
|
@@ -52,6 +53,7 @@ const SYSTEM = [
|
|
|
52
53
|
'- 新观测明确支持或反驳已有主张时,使用meaning.kind=episode或claim并给relations:[{target:完整recordId,stance:supports或contradicts}]。只能引用下方同scope现行目标清单,目标不明确则省略,不猜ID;不把旧摘要复述当新实验,不自动将信念取代为已证事实,明确更正个人决定仍用supersedes。关系是陈述映射,不是独立查证。',
|
|
53
54
|
'- 竞争解释用 meaning.alternatives,可能推翻判断的观测用 meaning.falsifiedBy(各最多3条,仅有依据时给)。这些是待检验内容,不是已经查证的反证。不输出 verified、confidence、授权等字段;',
|
|
54
55
|
'- 每条evidence最少1条、最多3条不同用户消息的引用,每条含真实messageId和8到500字的连续原话quote。合并同一信念时选择支撑主张、竞争解释和反证条件的最多3个直接来源,不为重复说明增加第4个引用;超过3条会整条拒绝,不会自动截掉证据。不得引用助手消息作用户证据。',
|
|
56
|
+
'- 对话中的id是本次请求绑定的短来源标签(如source1);messageId只逐字复制对应用户消息的id,不猜会话编号、UUID或事件序号。代码会还原真实来源并继续核对角色和原话。',
|
|
55
57
|
'- relations.target仅能逐字复制现行目标清单提供的64位十六进制recordId,不是sessionId、messageId或8位索引id。目标清单为空时不得填target;不要为本批尚未发布的新主张伪造身份。',
|
|
56
58
|
'- explicit 表示用户明确陈述了本人长期身份、署名或固定工作关系,或明确表达了持久偏好、纠正、决定、价值、本人持有的暂定信念,或明确要求长期保留的未核实主张;不表示外部命题已被证明。用户明确说“我长期持有但可推翻的假设”应记录为explicit且meaning.kind=belief,仍是暂定信念。模型从行为猜测偏好、补猜用户未说的原因仍必须标inferred,仅进候选;',
|
|
57
59
|
'- 每条一句话,不超过 120 字,写清「做什么/不做什么」,不要写「用户希望」这类空话;',
|
|
@@ -218,7 +220,10 @@ export function selectCaptureControls(controls, modelInfo) {
|
|
|
218
220
|
*/
|
|
219
221
|
export async function extractWithLlm(options) {
|
|
220
222
|
if(Array.isArray(options.messages)&&options.messages.some(m=>m?.role==='user'&&m.sourceComplete===false))throw Error('capture-input-source-incomplete')
|
|
221
|
-
return runCaptureModel(options,{system:SYSTEM,tool:CAPTURE_SUBMISSION_TOOL,prepare:(messages,indexLines,maxEntries,maxInputBytes,targets,records)=>
|
|
223
|
+
return runCaptureModel(options,{system:SYSTEM,tool:CAPTURE_SUBMISSION_TOOL,prepare:(messages,indexLines,maxEntries,maxInputBytes,targets,records)=>{
|
|
224
|
+
const sources=captureSourceAliases(messages)
|
|
225
|
+
return {text:buildUserPrompt(sources.messages,indexLines,maxEntries,maxInputBytes,targets,records),restore:sources.restore}
|
|
226
|
+
},submission:()=>new CaptureSubmission(),finalize:(batch,prepared)=>prepared.restore(batch)})
|
|
222
227
|
}
|
|
223
228
|
export async function runCaptureModel(options,task) {
|
|
224
229
|
const { llm, sessionId, messages, indexLines, maxEntries = 4, maxTokens = 800, maxInputBytes, timeoutMs = 25_000, route, signal, relationTargets=[],structuredRecords=[] } = options
|
|
@@ -276,7 +281,8 @@ export async function runCaptureModel(options,task) {
|
|
|
276
281
|
}
|
|
277
282
|
controller.signal.throwIfAborted()
|
|
278
283
|
if (!submission.sawTool&&text.trim().length === 0) throw new Error('capture-empty-response')
|
|
279
|
-
const
|
|
284
|
+
const parsed=submission.batch(text,maxEntries)
|
|
285
|
+
const batch=task.finalize?task.finalize(parsed,prepared):parsed
|
|
280
286
|
return options.includeDiagnostics===true?batch:batch.candidates
|
|
281
287
|
}
|
|
282
288
|
try {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Short request-local labels, bound by code to the original message identity. */
|
|
2
|
+
export function captureSourceAliases(messages = []) {
|
|
3
|
+
const originals = new Set(messages.map(message => message?.id).filter(id => typeof id === 'string'))
|
|
4
|
+
const ids = new Map()
|
|
5
|
+
const mapped = messages.map((message, index) => {
|
|
6
|
+
if (!message || typeof message !== 'object' || typeof message.id !== 'string') return message
|
|
7
|
+
let alias = `source${index + 1}`
|
|
8
|
+
while (originals.has(alias) || ids.has(alias)) alias = '_' + alias
|
|
9
|
+
ids.set(alias, message.id)
|
|
10
|
+
return { ...message, id: alias }
|
|
11
|
+
})
|
|
12
|
+
return {
|
|
13
|
+
messages: mapped,
|
|
14
|
+
restore(batch) {
|
|
15
|
+
return { ...batch, candidates: batch.candidates.map(candidate => ({ ...candidate,
|
|
16
|
+
evidence: candidate.evidence?.map(ref => ({ ...ref, messageId: ids.get(ref.messageId) ?? ref.messageId })),
|
|
17
|
+
})) }
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
}
|
package/index.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import {assertRestorationInactive} from './restoration-lock.mjs'
|
|
|
4
4
|
export {initializeManagedMemory,managedMemoryStatus} from './managed-access.mjs'
|
|
5
5
|
import {createObservationTracker,observationRoots,OBSERVATION_REFERENCE_SCHEMA,OBSERVATION_LIST_SCHEMA} from './tool-observations.mjs'
|
|
6
6
|
import {createToolInputTracker} from './tool-inputs.mjs'
|
|
7
|
+
import {memoryProtocol} from './memory-protocol.mjs'
|
|
8
|
+
import {privateMemory,publicMemoryRecords,publicMemoryIndex,PRIVATE_MEMORY_NOTICE} from './privacy.mjs'
|
|
7
9
|
import {nativeOwnerEvidence,validateNativeWrite} from './native-write-validation.mjs'
|
|
8
10
|
import {toolProposal} from './source-record.mjs'
|
|
9
11
|
import {revisionMode,prepareValueRevision,inheritedRoots} from './field-lineage.mjs'
|
|
@@ -290,8 +292,9 @@ function renderScopeLocal(scope, dir) {
|
|
|
290
292
|
let flagged = 0
|
|
291
293
|
const kept = []
|
|
292
294
|
const policy = withdrawalPolicy(dir)
|
|
293
|
-
const typed=activeMeanings(candidateSnapshot(dir,scope).records),assessmentRecords=effectiveSourceRecords(dir,scope)
|
|
295
|
+
const typed=activeMeanings(candidateSnapshot(dir,scope).records),assessmentRecords=publicMemoryRecords(effectiveSourceRecords(dir,scope))
|
|
294
296
|
for (const line of filterWithdrawn(index.text, policy, true).split('\n')) {
|
|
297
|
+
if (privateMemory(meaningForLine(typed,line),line)) continue
|
|
295
298
|
if (line.trim().startsWith('- [')) {
|
|
296
299
|
if (isExpired(line, now)) {
|
|
297
300
|
hidden += 1
|
|
@@ -1047,16 +1050,19 @@ function writeMemoryLocal({ type, dir, line, detail, summary, expires, supersede
|
|
|
1047
1050
|
evidence: provenance?.evidence,
|
|
1048
1051
|
basis: provenance?.basis,
|
|
1049
1052
|
})
|
|
1053
|
+
const privateLines=new Set(liveSourceRecords(dir,scope).filter(privateMemory).map(record=>record.indexLine).filter(Boolean))
|
|
1054
|
+
const hiddenLine=line=>privateLines.has(line)||privateMemory(line)
|
|
1055
|
+
const visibleWarnings=warnings.filter(w=>!privateMemory(w)&&![...privateLines].some(line=>w.includes(summaryOf(line))))
|
|
1050
1056
|
return {
|
|
1051
1057
|
path: indexFile,
|
|
1052
1058
|
indexLine: cleanLine,
|
|
1053
1059
|
duplicate: false,
|
|
1054
1060
|
lineCount: next.split(/\r?\n/).length,
|
|
1055
1061
|
...(detailPath === undefined ? {} : { detailPath }),
|
|
1056
|
-
...(supersededLine === undefined ? {} : { superseded: supersededLine }),
|
|
1062
|
+
...(supersededLine === undefined||hiddenLine(supersededLine) ? {} : { superseded: supersededLine }),
|
|
1057
1063
|
id: newId,
|
|
1058
|
-
related,
|
|
1059
|
-
...(
|
|
1064
|
+
related:related.filter(edge=>!hiddenLine(edge.line)),
|
|
1065
|
+
...(visibleWarnings.length === 0 ? {} : { warning: visibleWarnings.join(';') }),
|
|
1060
1066
|
}
|
|
1061
1067
|
}
|
|
1062
1068
|
return lockToken === REVIEW_LOCK ? execute() : withLock(dir, execute)
|
|
@@ -1244,7 +1250,7 @@ function effectiveSourceRecords(dir,scope,indexText,includeUntyped=false){
|
|
|
1244
1250
|
}
|
|
1245
1251
|
|
|
1246
1252
|
/** Current index-only legacy entries are visible as unclassified data, never inherited sources. */
|
|
1247
|
-
function validationCurrentRecords(dir,scope,records){
|
|
1253
|
+
function validationCurrentRecords(dir,scope,records,privateTarget){
|
|
1248
1254
|
const policy=withdrawalPolicy(dir),index=filterWithdrawn(readTextSafe(join(dir,'MEMORY.md'))??'',policy,true)
|
|
1249
1255
|
const known=new Set(records.map(r=>r.indexLine??indexLine(r.type,r.summary,r.expires,r.detail!==undefined))),rows=[...records]
|
|
1250
1256
|
for(const line of new Set(index.split(/\r?\n/).map(line=>line.trim()))){
|
|
@@ -1252,7 +1258,7 @@ function validationCurrentRecords(dir,scope,records){
|
|
|
1252
1258
|
const type=/^- \[(user|feedback|project|reference)\] /.exec(line)?.[1];if(!type)continue
|
|
1253
1259
|
rows.push({id:createHash('sha256').update('memory-unclassified-current-index:'+scope+':'+line).digest('hex'),status:'recorded',scope,type,summary:summaryOf(line),indexLine:line,legacyIndexOnly:true,expires:/ \(exp:(\d{4}-\d{2}-\d{2})\)$/.exec(line)?.[1]})
|
|
1254
1260
|
}
|
|
1255
|
-
return rows
|
|
1261
|
+
return rows.filter(row=>!privateMemory(row)||typeof privateTarget==='string'&&privateTarget.length>0&&row.indexLine?.includes(privateTarget))
|
|
1256
1262
|
}
|
|
1257
1263
|
|
|
1258
1264
|
function publishIndexChange(dir,before,after,reason){
|
|
@@ -1268,6 +1274,7 @@ function searchHistoricalStores(stores,needle,options,context,task,limit,mode){
|
|
|
1268
1274
|
const policy=withdrawalPolicy(dir),index=filterWithdrawn(readTextSafe(join(dir,'MEMORY.md'))??'',policy,true)
|
|
1269
1275
|
const result=historyRows(dir,{scope,index,revoked:policy.revoked,atSequence:options.atSequence,asOf:options.asOf,expiredAt:isExpired});coverage.push(result.coverage)
|
|
1270
1276
|
for(const {row,line,history}of result.rows){
|
|
1277
|
+
if(options.includePrivate!==true&&privateMemory(row.source,row.line))continue
|
|
1271
1278
|
const interpretation=row.source.meaning?meaningView(row.source,context,task):undefined
|
|
1272
1279
|
if(interpretation?.applicability==='not-applicable')continue
|
|
1273
1280
|
if(interpretation)interpretation.advice={action:'continue',reason:'historical-record-is-not-a-current-instruction',authority:'none'}
|
|
@@ -1297,6 +1304,7 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1297
1304
|
if (needle.length > 200) throw new Error('memory_search: query 太长(上限 200 字)')
|
|
1298
1305
|
const limit = Math.max(1, Math.min(20, options.limit ?? 8))
|
|
1299
1306
|
const mode = options.mode ?? 'ranked'
|
|
1307
|
+
if(options.includePrivate!==undefined&&typeof options.includePrivate!=='boolean')throw Error('memory-search-invalid-private-option')
|
|
1300
1308
|
if (mode !== 'exact' && mode !== 'ranked') throw new Error('memory_search: mode 必须是 exact 或 ranked')
|
|
1301
1309
|
const context=normalizeContext(options.context),task=normalizeTask(options.task)
|
|
1302
1310
|
if(options.view!==undefined&&!['current','history'].includes(options.view))throw new Error('memory-search-invalid-view')
|
|
@@ -1305,7 +1313,7 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1305
1313
|
|
|
1306
1314
|
const files = stores.flatMap(({ dir, scope }) => {
|
|
1307
1315
|
const policy = withdrawalPolicy(dir)
|
|
1308
|
-
const owned=liveSourceRecords(dir,scope).filter(record=>['recorded','adopted'].includes(record.status)),typed=activeMeanings(owned),assessmentRecords=effectiveSourceRecords(dir,scope)
|
|
1316
|
+
const owned=liveSourceRecords(dir,scope).filter(record=>['recorded','adopted'].includes(record.status)),typed=activeMeanings(owned),assessmentRecords=options.includePrivate===true?effectiveSourceRecords(dir,scope):publicMemoryRecords(effectiveSourceRecords(dir,scope))
|
|
1309
1317
|
return ['MEMORY.md', ...TYPES.map(type => `${type}.md`)].map(name => ({
|
|
1310
1318
|
name, path: join(dir, name), policy, scope, typed, owned, assessmentRecords, key: scope === undefined ? name : `${scope}/${name}`,
|
|
1311
1319
|
}))
|
|
@@ -1318,12 +1326,16 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1318
1326
|
for (const file of files) {
|
|
1319
1327
|
const text = readTextSafe(file.path)
|
|
1320
1328
|
if (text === undefined) continue
|
|
1329
|
+
// Legacy topic paragraphs may span lines without record markers. Never
|
|
1330
|
+
// return the unlabelled continuation of a recognized private paragraph.
|
|
1331
|
+
if(file.name!=='MEMORY.md'&&options.includePrivate!==true&&privateMemory(text))continue
|
|
1321
1332
|
let lineNumber = 0
|
|
1322
1333
|
const filtered=filterWithdrawn(text,file.policy,file.name==='MEMORY.md'),details=file.name==='MEMORY.md'?undefined:detailedMeanings(file.owned,filtered)
|
|
1323
1334
|
for (const raw of filtered.split('\n')) {
|
|
1324
1335
|
lineNumber += 1
|
|
1325
1336
|
if(file.name==='MEMORY.md'&&isExpired(raw))continue
|
|
1326
1337
|
const record=file.name==='MEMORY.md'?meaningForLine(file.typed,raw):details.get(lineNumber)
|
|
1338
|
+
if(options.includePrivate!==true&&privateMemory(record,raw))continue
|
|
1327
1339
|
if(record?.expires&&isExpired(`placeholder (exp:${record.expires})`))continue
|
|
1328
1340
|
const interpretation=record?.meaning?meaningView(record,context,task,file.assessmentRecords):undefined
|
|
1329
1341
|
if(interpretation?.applicability==='not-applicable')continue
|
|
@@ -1598,6 +1610,8 @@ function readLog(dir) {
|
|
|
1598
1610
|
* @param query - `{ id }` 或 `{ match }`(索引行子串,必须唯一)。
|
|
1599
1611
|
*/
|
|
1600
1612
|
function lookupLinksLocal(dir, query) {
|
|
1613
|
+
const hiddenLines=new Set(liveSourceRecords(dir).filter(privateMemory).map(record=>record.indexLine).filter(Boolean))
|
|
1614
|
+
const hidden=line=>hiddenLines.has(line)||privateMemory(line)
|
|
1601
1615
|
assertPublicationClear(dir)
|
|
1602
1616
|
const policy = withdrawalPolicy(dir)
|
|
1603
1617
|
const events = readLog(dir).filter((e) => !policy.memoryIds.includes(e.id) && !policy.lineHashes.includes(normalizedLineHash(String(e.line ?? ''))))
|
|
@@ -1606,7 +1620,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1606
1620
|
const live = new Map()
|
|
1607
1621
|
for (const line of current.split('\n')) {
|
|
1608
1622
|
const trimmed = line.trim()
|
|
1609
|
-
if (trimmed.startsWith('- [')) live.set(entryId(trimmed), trimmed)
|
|
1623
|
+
if (trimmed.startsWith('- [')&&!hidden(trimmed)) live.set(entryId(trimmed), trimmed)
|
|
1610
1624
|
}
|
|
1611
1625
|
|
|
1612
1626
|
let key = query.id
|
|
@@ -1627,8 +1641,9 @@ function lookupLinksLocal(dir, query) {
|
|
|
1627
1641
|
const inserted = events.filter((event) => event.op === 'insert' && event.id === key)
|
|
1628
1642
|
const line = live.get(key) ?? inserted.at(-1)?.line ?? events.find((event) => event.id === key)?.line
|
|
1629
1643
|
if (line === undefined) return { found: false, related: [], backlinks: [] }
|
|
1644
|
+
if(hidden(line))return{found:false,related:[],backlinks:[]}
|
|
1630
1645
|
|
|
1631
|
-
const related = inserted.at(-1)?.related ?? []
|
|
1646
|
+
const related = (inserted.at(-1)?.related ?? []).filter(item=>!hidden(item.line))
|
|
1632
1647
|
|
|
1633
1648
|
// 入边要顺着取代链找:指向被取代旧条目的边,等同指向新条目。
|
|
1634
1649
|
// 否则「改了一条记忆」就等于把它从图上拔掉。
|
|
@@ -1639,6 +1654,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1639
1654
|
const backlinks = []
|
|
1640
1655
|
for (const event of events) {
|
|
1641
1656
|
if (event.op !== 'insert' || lineage.has(event.id)) continue
|
|
1657
|
+
if(hidden(event.line))continue
|
|
1642
1658
|
if (!(event.related ?? []).some((item) => lineage.has(item.id))) continue
|
|
1643
1659
|
if (backlinks.some((item) => item.id === event.id)) continue
|
|
1644
1660
|
backlinks.push({ id: event.id, line: event.line })
|
|
@@ -1660,7 +1676,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1660
1676
|
backlinks: backlinks.slice(0, RELATION_LIMIT),
|
|
1661
1677
|
backlinksTotal: backlinks.length,
|
|
1662
1678
|
...(replaced?.supersededById === undefined ? {} : { supersededById: replaced.supersededById }),
|
|
1663
|
-
...(supersedesLine === undefined ? {} : { supersedesLine }),
|
|
1679
|
+
...(supersedesLine === undefined||hidden(supersedesLine) ? {} : { supersedesLine }),
|
|
1664
1680
|
}
|
|
1665
1681
|
}
|
|
1666
1682
|
|
|
@@ -1776,7 +1792,7 @@ async function admitNativeWrite(ctx,sourceInputs,config,args,exec,target){
|
|
|
1776
1792
|
const revision=readOwned(()=>{
|
|
1777
1793
|
const value=storeRevision(target.dir)
|
|
1778
1794
|
const sourceRecords=effectiveSourceRecords(target.dir,target.scope,undefined,true)
|
|
1779
|
-
currentRecords=validationCurrentRecords(target.dir,target.scope,sourceRecords)
|
|
1795
|
+
currentRecords=validationCurrentRecords(target.dir,target.scope,sourceRecords,target.supersedes)
|
|
1780
1796
|
if(target.supersedes!==undefined){
|
|
1781
1797
|
const hits=(readTextSafe(join(target.dir,'MEMORY.md'))??'').split(/\r?\n/).filter(line=>line.trim().startsWith('- [')&&line.includes(target.supersedes));if(hits.length!==1)throw Error('memory-native-supersedes-not-unique')
|
|
1782
1798
|
const compiled=prepareValueRevision(target,sourceRecords,hits[0].trim())
|
|
@@ -1945,6 +1961,7 @@ function defineSearchTool(ctx) {
|
|
|
1945
1961
|
name: 'memory_search',
|
|
1946
1962
|
description:
|
|
1947
1963
|
'在记忆索引与主题文件里检索。默认 ranked 支持中英多词问题与有限英文词形归并;exact 用于精确子串定位;' +
|
|
1964
|
+
'默认隐藏已识别的个人敏感内容。用户询问相关资料时才设includePrivate=true;返回内容会进入当前会话日志和模型请求,不能声称只在本地读取。' +
|
|
1948
1965
|
'默认同时查当前项目和全局偏好,不查其他项目;可指定单库。view=history显式查询带来源的旧值、取代与移出记录,返回历史身份及覆盖范围,不能当当前指令。',
|
|
1949
1966
|
parameters: {
|
|
1950
1967
|
type: 'object',
|
|
@@ -1952,6 +1969,7 @@ function defineSearchTool(ctx) {
|
|
|
1952
1969
|
required: ['query'],
|
|
1953
1970
|
properties: {
|
|
1954
1971
|
query: { type: 'string', description: '检索词,尽量具体(≤200 字);也可给返回的完整64位recordId精确定位来源条目,不能用8位索引id' },
|
|
1972
|
+
includePrivate:{type:'boolean',description:'仅在用户询问相关个人资料时明确设true,允许读取被自动降载的个人内容。默认false;不是文件访问权限或完整隐私识别器。'},
|
|
1955
1973
|
mode: { type: 'string', enum: ['exact', 'ranked'], description: 'ranked 默认多词排序;exact 精确定位。不是语义向量检索,不支持完整同义词或英文词形分析' },
|
|
1956
1974
|
view:{type:'string',enum:['current','history'],description:'current默认只查当前视图;history查受管历史档案(已撤回来源仍不可见)。旧日志不会被伪造成完整历史。'},
|
|
1957
1975
|
atSequence:{type:'integer',description:'仅history:按选定单库的记录序号查询当时有效的条目,必须指定scope=project或global;不是跨库统一版本。'},
|
|
@@ -1999,10 +2017,10 @@ function defineSearchTool(ctx) {
|
|
|
1999
2017
|
return searchScopedStores([
|
|
2000
2018
|
{ dir: resolveStoreDir(ctx, exec, 'memory_search', 'project'), scope: 'project' },
|
|
2001
2019
|
{ dir: resolveStoreDir(ctx, exec, 'memory_search', 'global'), scope: 'global' },
|
|
2002
|
-
], args.query, { limit: args.limit, mode: args.mode, context:args.context,task:args.task,view:args.view,atSequence:args.atSequence,asOf:args.asOf })
|
|
2020
|
+
], args.query, { includePrivate:args.includePrivate,limit: args.limit, mode: args.mode, context:args.context,task:args.task,view:args.view,atSequence:args.atSequence,asOf:args.asOf })
|
|
2003
2021
|
}
|
|
2004
2022
|
const dir = resolveStoreDir(ctx, exec, 'memory_search', args.scope)
|
|
2005
|
-
return searchStore(dir, args.query, { limit: args.limit, mode: args.mode, context:args.context,task:args.task,view:args.view,atSequence:args.atSequence,asOf:args.asOf })
|
|
2023
|
+
return searchStore(dir, args.query, { includePrivate:args.includePrivate,limit: args.limit, mode: args.mode, context:args.context,task:args.task,view:args.view,atSequence:args.atSequence,asOf:args.asOf })
|
|
2006
2024
|
},
|
|
2007
2025
|
}
|
|
2008
2026
|
}
|
|
@@ -2254,7 +2272,7 @@ function defineForgetTool(ctx) {
|
|
|
2254
2272
|
text:
|
|
2255
2273
|
value.removed === 0
|
|
2256
2274
|
? `没有匹配到任何索引行:${value.path}`
|
|
2257
|
-
:
|
|
2275
|
+
: `已停止使用 ${value.removed} 条索引记忆(${value.path})。原话、主题正文、会话日志、备份及已发送内容未由此清除;这不是历史数据擦除。`,
|
|
2258
2276
|
},
|
|
2259
2277
|
],
|
|
2260
2278
|
},
|
|
@@ -2429,13 +2447,14 @@ function setupAutoCapture(ctx, config) {
|
|
|
2429
2447
|
const captureDirs=[storeRoot(),projectDir].filter(Boolean)
|
|
2430
2448
|
const memorySnapshot=managedReadMany(captureDirs,()=>{
|
|
2431
2449
|
const stores=captureDirs.map(dir=>{const scope=dir===storeRoot()?'global':'project',policy=withdrawalPolicy(dir);return{dir,scope,policy,records:effectiveSourceRecords(dir,scope,undefined,true),indexLines:filterWithdrawn(readTextSafe(join(dir,'MEMORY.md'))??'',policy,true).split('\n').map(line=>line.trim()).filter(line=>line.startsWith('- [')).slice(-60)}})
|
|
2432
|
-
const
|
|
2450
|
+
for(const store of stores)store.indexLines=publicMemoryIndex(store.indexLines,store.records)
|
|
2451
|
+
const projections=stores.flatMap(store=>publicMemoryRecords(store.records).flatMap(record=>{
|
|
2433
2452
|
const line=record.indexLine??indexLine(record.type,record.summary,record.expires,record.detail!==undefined)
|
|
2434
2453
|
if(!store.indexLines.includes(line))return[]
|
|
2435
2454
|
const topic=filterWithdrawn(readTextSafe(join(store.dir,record.type+'.md'))??'',store.policy)
|
|
2436
2455
|
return[captureProjection(record,store.scope,`[${store.scope}] ${line}`,topic)]
|
|
2437
2456
|
})).slice(-60)
|
|
2438
|
-
return{validationRecords:stores.flatMap(store=>validationCurrentRecords(store.dir,store.scope,store.records)),sourceRecords:stores.flatMap(store=>store.records),structuredRecords:projections,relationTargets:stores.flatMap(store=>store.records.filter(record=>['claim','belief'].includes(record.meaning?.kind)&&!record.meaning.relations?.length).map(record=>({scope:store.scope,recordId:record.id,kind:record.meaning.kind,summary:record.summary}))).slice(-60),revisions:new Map(captureDirs.map(dir=>[dir,{value:storeRevision(dir)}])),indexLines:stores.flatMap(store=>store.indexLines.map(line=>`[${store.scope}] ${line}`))}
|
|
2457
|
+
return{validationRecords:stores.flatMap(store=>validationCurrentRecords(store.dir,store.scope,store.records)),sourceRecords:stores.flatMap(store=>store.records),structuredRecords:projections,relationTargets:stores.flatMap(store=>publicMemoryRecords(store.records).filter(record=>['claim','belief'].includes(record.meaning?.kind)&&!record.meaning.relations?.length).map(record=>({scope:store.scope,recordId:record.id,kind:record.meaning.kind,summary:record.summary}))).slice(-60),revisions:new Map(captureDirs.map(dir=>[dir,{value:storeRevision(dir)}])),indexLines:stores.flatMap(store=>store.indexLines.map(line=>`[${store.scope}] ${line}`))}
|
|
2439
2458
|
})
|
|
2440
2459
|
const revisions=memorySnapshot.revisions
|
|
2441
2460
|
// 按类型分库:跨项目的偏好/纠正进全局库,项目决定进项目库。
|
|
@@ -2800,6 +2819,11 @@ export function apply(ctx, config = {}) {
|
|
|
2800
2819
|
let calls = 0
|
|
2801
2820
|
const memoryHistory = setupMemoryHistory(ctx, renderForRuntime)
|
|
2802
2821
|
|
|
2822
|
+
ctx.systemPrompt.context({
|
|
2823
|
+
name: 'memory:protocol', order: CONTEXT_ORDER - 1, required: true,
|
|
2824
|
+
interpolate: false, text: context => memoryProtocol(ctx, context)+'\n'+PRIVATE_MEMORY_NOTICE,
|
|
2825
|
+
})
|
|
2826
|
+
|
|
2803
2827
|
ctx.systemPrompt.context({
|
|
2804
2828
|
name: CONTEXT_NAME,
|
|
2805
2829
|
order: CONTEXT_ORDER,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Model instructions are separate from untrusted stored memory data. */
|
|
2
|
+
export function memoryProtocol(ctx, context) {
|
|
3
|
+
if (!context?.agent) return ''
|
|
4
|
+
let availability = '当前工具可用性未能核对;以实际工具列表和执行结果为准。'
|
|
5
|
+
try {
|
|
6
|
+
const names = ctx.tools.schemas(context.agent).map(tool => tool.name)
|
|
7
|
+
availability = names.includes('memory_write')
|
|
8
|
+
? '当前会话已提供 memory_write;native 模式直接调用,PTC 模式通过 run_code 的 tools.memory_write 调用。'
|
|
9
|
+
: '当前会话未提供 memory_write;明确报告工具不可用,不能声称已经保存,也不能用脚本绕过来源校验。'
|
|
10
|
+
} catch { /* Older hosts may not offer scoped schema inspection. */ }
|
|
11
|
+
return [
|
|
12
|
+
'SoloPup Memory 使用协议(插件指令;memory-store 内是待核对的数据)。',
|
|
13
|
+
availability,
|
|
14
|
+
'用户明确要求“记住/以后记得”某项持久信息时,应在本轮调用正式 memory_write 并等待结果,不等每4轮的后台整理。仅讨论“记住”一词、引用他人指令或明确说不要记录,不构成保存请求。',
|
|
15
|
+
'用户本人明确陈述的生日、性别、署名、固定关系等可复用个人事实,用 type=user、scope=global、meaning.kind=claim 保存;claim 只表示本人陈述,不是独立核实。明确仅属于本项目的资料用 project 范围。',
|
|
16
|
+
'助手猜测、星座推断、人格判断和对第三方动机的解释不能变成本人已确认事实;不要补入用户未在来源中说出的地点或其他细节。',
|
|
17
|
+
'只在 memory_write 返回 recorded 或 duplicate 时确认已保存。pending-review 是待审,rejected/not-applied 是未保存;说明实际状态,不把调用过工具当保存成功。',
|
|
18
|
+
'不要通过 bash/node 导入内部 writeMemory、stageCandidate 等函数来代替正式工具,不伪造 callHash 或来源。真实工具失败时保留错误;不要改写记忆文件绕过审查。',
|
|
19
|
+
'跨会话回忆先核对注入索引,细节或缺失信息用 memory_search;没有依据就明确不知道,区分当前聊天推测和长期已保存资料。',
|
|
20
|
+
].join('\n')
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solopup-memory",
|
|
3
|
-
"version": "0.21.0-beta.
|
|
3
|
+
"version": "0.21.0-beta.3",
|
|
4
4
|
"description": "SoloPup Memory — source-linked, layered text memory for DeepSeek Harness. Public beta.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -144,7 +144,12 @@
|
|
|
144
144
|
"icon.svg",
|
|
145
145
|
"locale/",
|
|
146
146
|
"README.en.md",
|
|
147
|
-
"RELEASE.md"
|
|
147
|
+
"RELEASE.md",
|
|
148
|
+
"memory-protocol.mjs",
|
|
149
|
+
"capture-source-aliases.mjs",
|
|
150
|
+
"test-explicit-memory.mjs",
|
|
151
|
+
"privacy.mjs",
|
|
152
|
+
"test-privacy.mjs"
|
|
148
153
|
],
|
|
149
154
|
"engines": {
|
|
150
155
|
"node": ">=24.9",
|
|
@@ -157,7 +162,7 @@
|
|
|
157
162
|
},
|
|
158
163
|
"license": "MIT",
|
|
159
164
|
"scripts": {
|
|
160
|
-
"test": "node --test --test-concurrency=4 test.mjs test-security.mjs test-adversarial.mjs test-concurrency.mjs test-supersede.mjs test-history.mjs test-smell.mjs test-links.mjs test-packaging.mjs test-patch.mjs test-compat.mjs test-conflict.mjs test-usability.mjs test-compaction-compat.mjs test-schema.mjs test-capture.mjs test-paired-analysis.mjs test-retrieval.mjs test-candidates.mjs test-publication.mjs test-withdrawal.mjs test-capture-jobs.mjs test-context-history.mjs test-capture-inputs.mjs test-inline-summary.mjs test-withdrawal-archives.mjs test-plugin-contract.mjs test-project-stores.mjs test-service-readiness.mjs test-source-key.mjs test-tool-inputs.mjs test-tool-write-lifecycle.mjs test-tool-write-guard.mjs test-source-retirement.mjs test-managed-access.mjs test-managed-activation.mjs test-runtime-startup.mjs test-capture-revision.mjs test-meaning.mjs test-episodes.mjs test-belief-revision.mjs test-tool-observations.mjs test-capture-diagnostics.mjs test-snapshots.mjs test-restore.mjs test-status-both.mjs test-text-admission.mjs test-capture-submission.mjs test-capture-projections.mjs test-capture-validation.mjs test-capture-validation-job.mjs test-capture-validation-flow.mjs test-capture-validation-lineage.mjs test-native-write-validation.mjs test-native-service-lifecycle.mjs test-field-lineage.mjs test-capture-field-lineage.mjs test-validation-transition-context.mjs test-control-schema.mjs test-revision-admission.mjs test-rule-admission.mjs test-validation-source-refs.mjs test-source-completeness.mjs test-legacy-source-identity.mjs",
|
|
165
|
+
"test": "node --test --test-concurrency=4 test.mjs test-security.mjs test-adversarial.mjs test-concurrency.mjs test-supersede.mjs test-history.mjs test-smell.mjs test-links.mjs test-packaging.mjs test-patch.mjs test-compat.mjs test-conflict.mjs test-usability.mjs test-compaction-compat.mjs test-schema.mjs test-capture.mjs test-paired-analysis.mjs test-retrieval.mjs test-candidates.mjs test-publication.mjs test-withdrawal.mjs test-capture-jobs.mjs test-context-history.mjs test-capture-inputs.mjs test-inline-summary.mjs test-withdrawal-archives.mjs test-plugin-contract.mjs test-project-stores.mjs test-service-readiness.mjs test-source-key.mjs test-tool-inputs.mjs test-tool-write-lifecycle.mjs test-tool-write-guard.mjs test-source-retirement.mjs test-managed-access.mjs test-managed-activation.mjs test-runtime-startup.mjs test-capture-revision.mjs test-meaning.mjs test-episodes.mjs test-belief-revision.mjs test-tool-observations.mjs test-capture-diagnostics.mjs test-snapshots.mjs test-restore.mjs test-status-both.mjs test-text-admission.mjs test-capture-submission.mjs test-capture-projections.mjs test-capture-validation.mjs test-capture-validation-job.mjs test-capture-validation-flow.mjs test-capture-validation-lineage.mjs test-native-write-validation.mjs test-native-service-lifecycle.mjs test-field-lineage.mjs test-capture-field-lineage.mjs test-validation-transition-context.mjs test-control-schema.mjs test-revision-admission.mjs test-rule-admission.mjs test-validation-source-refs.mjs test-source-completeness.mjs test-legacy-source-identity.mjs test-explicit-memory.mjs test-privacy.mjs",
|
|
161
166
|
"eval": "node eval/run-eval.mjs",
|
|
162
167
|
"gate": "node eval/run-eval.mjs --check",
|
|
163
168
|
"verify": "node scripts/verify.mjs",
|
package/privacy.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Conservative exposure reduction, not a confidentiality boundary or exhaustive PII detector. */
|
|
2
|
+
import {normalizeText} from './security.mjs'
|
|
3
|
+
const patterns = [
|
|
4
|
+
['exact-dob', /生日|出生(?:日期|年月|于|在)?|\bbirth(?:day|date)\b|\bdate\s+of\s+birth\b|\bdob\b|\bborn\b/i],
|
|
5
|
+
['personal-profile', /个人档案|性别|男性|女性|\bpersonal\s+profile\b|\bgender\b/i],
|
|
6
|
+
['health', /病史|医疗诊断|疾病诊断|医生诊断|诊断(?:为|出)|用药|过敏|怀孕|月经|健康状况|医疗资料|\bmedical\b|\bclinical\s+diagnosis\b|\bdiagnosed\s+with\b|\bmedication\b/i],
|
|
7
|
+
['intimate', /亲密关系|性取向|性生活|性行为|伴侣|男朋友|女朋友|前任|恋爱|情人|\bsexual\b|\bintimate\b|\bromantic\b/i],
|
|
8
|
+
['personal-contact', /身份证|护照号码|家庭住址|私人电话|银行卡号|\bpassport\s+number\b|\bhome\s+address\b/i],
|
|
9
|
+
['marked-private', /\[private(?::[a-z-]+)?\]|保密资料|私密档案|敏感个人/i],
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export function memoryPrivacy(value, line = '') {
|
|
13
|
+
const content = typeof value === 'string' ? value : value && typeof value === 'object'
|
|
14
|
+
? [value.summary, value.detail, value.indexLine, JSON.stringify(value.meaning ?? {}),
|
|
15
|
+
...(value.evidence ?? []).map(e => e.quote), JSON.stringify(value.fieldLineage ?? {})].filter(Boolean).join('\n') : ''
|
|
16
|
+
const text = normalizeText(content + '\n' + line)
|
|
17
|
+
const categories = patterns.filter(([, pattern]) => pattern.test(text)).map(([category]) => category)
|
|
18
|
+
return {exposure: categories.length ? 'on-demand' : 'automatic', categories}
|
|
19
|
+
}
|
|
20
|
+
export const privateMemory = (value, line = '') => memoryPrivacy(value, line).exposure === 'on-demand'
|
|
21
|
+
export function publicMemoryRecords(records) { return records.filter(record => !privateMemory(record)) }
|
|
22
|
+
export function publicMemoryIndex(lines, records) {
|
|
23
|
+
return lines.filter(line => !privateMemory(records.find(record => record.indexLine === line || line.endsWith(' ' + record.indexLine)), line))
|
|
24
|
+
}
|
|
25
|
+
export const PRIVATE_MEMORY_NOTICE = '个人资料可能按需保存在本地;未自动提供其内容。仅在用户询问相关资料时使用 memory_search 并明确 includePrivate=true。读取结果仍会进入当前会话及模型请求;这不是加密或文件访问隔离。'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {memoryProtocol} from './memory-protocol.mjs'
|
|
4
|
+
import {captureSourceAliases} from './capture-source-aliases.mjs'
|
|
5
|
+
import {extractWithLlm} from './capture-llm.mjs'
|
|
6
|
+
import {assessCandidate} from './capture-evidence.mjs'
|
|
7
|
+
|
|
8
|
+
test('protocol reports scoped availability and remains available with no memory data', () => {
|
|
9
|
+
const agent={session:{id:'owner'}}
|
|
10
|
+
const text=memoryProtocol({tools:{schemas:scope=>{assert.equal(scope,agent);return [{name:'memory_write'}]}}},{agent})
|
|
11
|
+
assert.match(text,/本轮调用正式 memory_write/)
|
|
12
|
+
assert.match(text,/claim 只表示本人陈述/)
|
|
13
|
+
assert.match(text,/pending-review 是待审/)
|
|
14
|
+
assert.match(text,/不伪造 callHash/)
|
|
15
|
+
assert.match(memoryProtocol({tools:{schemas:()=>[]}},{agent}),/当前会话未提供/)
|
|
16
|
+
assert.equal(memoryProtocol({},{}),'')
|
|
17
|
+
assert.match(memoryProtocol({}, {agent}),/未能核对/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('source labels are request-local and cannot collide with actual identifiers', () => {
|
|
21
|
+
const messages=[{id:'source1',role:'user',text:'原话不变'},{id:'long-session:22',role:'assistant',text:'助手猜测'}]
|
|
22
|
+
const mapped=captureSourceAliases(messages)
|
|
23
|
+
assert.equal(mapped.messages[0].id,'_source1')
|
|
24
|
+
const batch=mapped.restore({candidates:[{evidence:[{messageId:'_source1',quote:'原话不变'},{messageId:'source2',quote:'助手猜测'},{messageId:'unknown',quote:'未知'}]}],parserRejected:0})
|
|
25
|
+
assert.deepEqual(batch.candidates[0].evidence.map(e=>e.messageId),['source1','long-session:22','unknown'])
|
|
26
|
+
assert.equal(messages[0].id,'source1')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('real extraction boundary binds short ids back before owner quote validation',async()=>{
|
|
30
|
+
const quote='记住:验收角色沈沐出生于1990年5月12日,男性。'
|
|
31
|
+
const messages=[{id:'session-synthetic:7177',role:'user',text:quote,hash:'a'.repeat(64)},
|
|
32
|
+
{id:'session-synthetic:7179',role:'assistant',text:'我猜他出生于长沙。',hash:'b'.repeat(64)}]
|
|
33
|
+
const llm={async prepareCall(config){return{config,async*stream(request){
|
|
34
|
+
assert.match(request.messages[0].content[0].text,/"id":"source1"/)
|
|
35
|
+
assert.doesNotMatch(request.messages[0].content[0].text,/session-synthetic:7177/)
|
|
36
|
+
yield {type:'text-delta',text:JSON.stringify([{type:'user',scope:'global',basis:'explicit',meaning:{kind:'claim'},summary:quote,evidence:[{messageId:'source1',quote}]}])}
|
|
37
|
+
}}}}
|
|
38
|
+
const candidates=await extractWithLlm({llm,messages,indexLines:[]})
|
|
39
|
+
assert.equal(candidates[0].evidence[0].messageId,messages[0].id)
|
|
40
|
+
assert.equal(assessCandidate(candidates[0],messages,true).status,'eligible')
|
|
41
|
+
for(const ref of [{messageId:messages[1].id,quote:messages[1].text},{messageId:'unknown',quote},{messageId:messages[0].id,quote:'他出生于长沙'}])
|
|
42
|
+
assert.equal(assessCandidate({...candidates[0],evidence:[ref]},messages,true).reason,'invalid-user-evidence')
|
|
43
|
+
})
|
|
@@ -40,6 +40,12 @@ test('native approval publishes the unchanged typed candidate; exact replay is a
|
|
|
40
40
|
assert.equal(result.status,'recorded');assert.equal(f.records()[0].summary,proposal.summary);assert.equal(f.requests.length,1)
|
|
41
41
|
assert.equal((await f.tool.execute(proposal,exec)).status,'duplicate');assert.equal(f.requests.length,1)
|
|
42
42
|
}))
|
|
43
|
+
test('unrelated native admission does not disclose saved private records to its reviewer',()=>box({duringStream:(_f,input)=>{
|
|
44
|
+
assert.doesNotMatch(JSON.stringify(input.currentMemory),/1993|4月16|个人档案/)
|
|
45
|
+
}},async f=>{
|
|
46
|
+
writeFixtureMemory({dir:f.root,type:'user',scope:'global',summary:'个人档案:出生于1993年4月16日,女性',meaning:{kind:'claim'}})
|
|
47
|
+
f.owner();const result=await f.tool.execute(proposal,f.call());assert.equal(result.status,'recorded')
|
|
48
|
+
}))
|
|
43
49
|
test('definite rejection leaves no current or pending memory and does not render success',()=>box({state:'unsupported'},async f=>{
|
|
44
50
|
f.owner();const result=await f.tool.execute(proposal,f.call());assert.equal(result.status,'rejected');assert.equal(f.records().length,0);assert.doesNotMatch(f.rendered(result),/已写入/);assert.equal(searchStore(f.root,'长期反馈').hits.length,0)
|
|
45
51
|
}))
|
package/test-privacy.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync} from 'node:fs'
|
|
4
|
+
import {join} from 'node:path'
|
|
5
|
+
import {tmpdir} from 'node:os'
|
|
6
|
+
import {apply,searchStore,lookupLinks} from './index.mjs'
|
|
7
|
+
import {writeFixtureMemory} from './fixture-host.mjs'
|
|
8
|
+
import {memoryPrivacy,privateMemory,publicMemoryRecords,publicMemoryIndex} from './privacy.mjs'
|
|
9
|
+
|
|
10
|
+
test('actual background extractor receives no unrelated saved private source or summary',async()=>{
|
|
11
|
+
const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-private-background-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
|
|
12
|
+
const cwd=join(home,'project'),handlers=[],effects=[];mkdirSync(cwd);let requestText,finished=false
|
|
13
|
+
const llm={async prepareCall(config){return{config,async*stream(request){requestText=JSON.stringify(request);yield{type:'text-delta',text:'[]'};yield{type:'finish',reason:{kind:'stop'}};finished=true}}}}
|
|
14
|
+
try{
|
|
15
|
+
apply({llm,systemPrompt:{context(){}},workingDirectory:{get:()=>cwd},tools:{register(){}},on:(name,fn)=>handlers.push([name,fn]),effect:fn=>effects.push(fn())},
|
|
16
|
+
{autoCapture:{everyTurns:1,provider:'fixture',model:'fixture'}})
|
|
17
|
+
writeFixtureMemory({dir:join(home,'memory'),type:'user',scope:'global',summary:'个人档案:出生于1993年4月16日,女性',meaning:{kind:'claim'}})
|
|
18
|
+
const session={id:'synthetic-unrelated-work'},emit=e=>{for(const [n,f]of handlers)if(n==='session/event')f(session,e)}
|
|
19
|
+
emit({type:'user/message',seq:1,data:{source:{kind:'user'},content:[{type:'text',text:'本项目以后交付字幕时固定先检查断句。'}]}})
|
|
20
|
+
emit({type:'step/end',seq:2})
|
|
21
|
+
for(let i=0;i<100&&!finished;i++)await new Promise(r=>setTimeout(r,5))
|
|
22
|
+
assert.equal(finished,true)
|
|
23
|
+
assert.doesNotMatch(requestText,/1993|4月16|个人档案:/)
|
|
24
|
+
}finally{for(const f of effects.reverse())if(typeof f==='function')f();if(old===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=old;rmSync(home,{recursive:true,force:true})}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('known personal categories default to on-demand, including evidence-only private data',()=>{
|
|
28
|
+
for(const text of ['生日是1993年4月16日','个人档案:女性','病史与用药资料','亲密关系与伴侣','家庭住址','[private:personal] opaque','birthday is April 16'])assert.equal(privateMemory(text),true)
|
|
29
|
+
assert.equal(privateMemory({summary:'工作反馈先给结论',evidence:[{quote:'我的生日是1993年4月16日,我希望先给结论。'}]}),true)
|
|
30
|
+
assert.equal(memoryPrivacy('字幕交付先检查中英断句').exposure,'automatic')
|
|
31
|
+
assert.equal(privateMemory('工具失败的原生记录仅用于诊断'),false)
|
|
32
|
+
})
|
|
33
|
+
test('background projections and pointers omit private evidence without changing originals',()=>{
|
|
34
|
+
const secret={indexLine:'- [user] 个人档案:出生于1993年4月16日 — user.md',summary:'个人档案:出生于1993年4月16日',evidence:[]}
|
|
35
|
+
const mixed={indexLine:'- [user] 回答先给结论 — user.md',summary:'回答先给结论',evidence:[{quote:'我的生日是1993年4月16日,回答先给结论'}]}
|
|
36
|
+
const safe={indexLine:'- [feedback] 字幕保留断句 — feedback.md',summary:'字幕保留断句',evidence:[]}
|
|
37
|
+
const records=[secret,mixed,safe],before=JSON.stringify(records)
|
|
38
|
+
assert.deepEqual(publicMemoryRecords(records),[safe])
|
|
39
|
+
assert.deepEqual(publicMemoryIndex(records.map(r=>r.indexLine),records),[safe.indexLine])
|
|
40
|
+
assert.equal(JSON.stringify(records),before)
|
|
41
|
+
})
|
|
42
|
+
test('real plugin render and both current/history search hide private records by default',()=>{
|
|
43
|
+
const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-privacy-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
|
|
44
|
+
const cwd=join(home,'project'),providers=[],tools=[];mkdirSync(cwd)
|
|
45
|
+
try{
|
|
46
|
+
apply({systemPrompt:{context:p=>providers.push(p)},workingDirectory:{get:()=>cwd},tools:{register:t=>tools.push(t),schemas:()=>[{name:'memory_write'}]},on(){},effect(){}},{autoCapture:{enabled:false}})
|
|
47
|
+
const dir=join(home,'memory'),secret='固定个人档案:出生于1993年4月16日,女性。'
|
|
48
|
+
const privateWrite=writeFixtureMemory({dir,type:'user',scope:'global',summary:secret,detail:'私人资料分行\n1993-04-16\n女性',meaning:{kind:'claim'}})
|
|
49
|
+
writeFixtureMemory({dir,type:'feedback',scope:'global',summary:'字幕验收先看断句',meaning:{kind:'preference'}})
|
|
50
|
+
const before=readFileSync(join(dir,'MEMORY.md'),'utf8')
|
|
51
|
+
const rendered=providers.find(p=>p.name==='memory:index').text({agent:{session:{}}})
|
|
52
|
+
assert.doesNotMatch(rendered,/1993|女性|固定个人档案/)
|
|
53
|
+
assert.match(rendered,/字幕验收先看断句/)
|
|
54
|
+
assert.match(providers.find(p=>p.name==='memory:protocol').text({agent:{session:{}}}),/includePrivate=true/)
|
|
55
|
+
assert.equal(searchStore(dir,'1993',{mode:'exact'}).hits.length,0)
|
|
56
|
+
assert.ok(searchStore(dir,'1993',{mode:'exact',includePrivate:true}).hits.length>0)
|
|
57
|
+
assert.equal(lookupLinks(dir,{id:privateWrite.id}).found,false)
|
|
58
|
+
assert.equal(lookupLinks(dir,{match:'个人档案'}).found,false)
|
|
59
|
+
assert.equal(searchStore(dir,'1993',{mode:'exact',view:'history',scope:'global'}).hits.length,0)
|
|
60
|
+
assert.ok(searchStore(dir,'1993',{mode:'exact',view:'history',scope:'global',includePrivate:true}).hits.length>0)
|
|
61
|
+
assert.throws(()=>searchStore(dir,'1993',{includePrivate:'true'}),/private-option/)
|
|
62
|
+
assert.equal(readFileSync(join(dir,'MEMORY.md'),'utf8'),before)
|
|
63
|
+
assert.ok(tools.find(t=>t.name==='memory_search').parameters.properties.includePrivate)
|
|
64
|
+
}finally{if(old===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=old;rmSync(home,{recursive:true,force:true})}
|
|
65
|
+
})
|
package/test-restore.mjs
CHANGED
|
@@ -94,7 +94,7 @@ test('a live second worker cannot resume or steal the in-progress journal',()=>b
|
|
|
94
94
|
test('a host mounted during the missing-directory phase writes no new root and sees only a restore notice',()=>box(async f=>{
|
|
95
95
|
seed(f.data,'private7360');createMemorySnapshot(f.data,f.snapshot);assert.throws(()=>restore(f,{fault:e=>{if(e.phase==='old-moved')throw Error('stop')}}),/stop/);assert.equal(existsSync(f.data),false)
|
|
96
96
|
const tools=[],providers=[],effects=[];let calls=0;const ctx={systemPrompt:{context:p=>providers.push(p)},tools:{register:t=>tools.push(t)},workingDirectory:{get:()=>f.cwd},llm:{listProviders:()=>[],prepareCall(){calls++;throw Error('must not call')}},on(){},effect(fn){effects.push(fn())}}
|
|
97
|
-
apply(ctx,{autoCapture:{everyTurns:1}});const status=await tools.find(t=>t.name==='memory_status').execute({scope:'global'});assert.equal(status.sourceControl.code,'managed-memory-restore-in-progress');const text=providers
|
|
97
|
+
apply(ctx,{autoCapture:{everyTurns:1}});const status=await tools.find(t=>t.name==='memory_status').execute({scope:'global'});assert.equal(status.sourceControl.code,'managed-memory-restore-in-progress');const text=providers.find(p=>p.name==='memory:index').text({agent:{session:{id:'during-restore'}}});assert.match(text,/恢复尚未完成/);assert.doesNotMatch(text,/private7360/);assert.equal(existsSync(f.data),false);assert.equal(calls,0);for(const fn of effects.reverse())fn?.();resumeMemoryRestore(f.data,{apply:true})
|
|
98
98
|
}))
|
|
99
99
|
test('CLI performs a reviewed restore in a fresh process; incomplete arguments cannot mutate the target',()=>box(f=>{
|
|
100
100
|
seed(f.data,'before7471');createMemorySnapshot(f.data,f.snapshot);seed(f.data,'after7582');const cli=join(here,'scripts/backup.mjs'),run=args=>spawnSync(process.execPath,[cli,...args],{encoding:'utf8'}),base=['restore','--root',f.data,'--snapshot',f.snapshot]
|
package/version.mjs
CHANGED