solopup-memory 0.21.0-beta.2 → 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 +7 -0
- package/README.en.md +5 -3
- package/README.md +9 -3
- package/RELEASE.md +10 -0
- package/SECURITY.md +4 -0
- package/index.mjs +35 -17
- package/package.json +5 -3
- package/privacy.mjs +25 -0
- package/test-native-write-validation.mjs +6 -0
- package/test-privacy.mjs +65 -0
- package/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.21.0-beta.2 — explicit memory workflow
|
|
4
11
|
|
|
5
12
|
- Move model-facing memory instructions into a separate protocol context, available with an empty index and reporting scoped write-tool availability.
|
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.2 is
|
|
|
14
14
|
## Install in DSH
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
dsh plugin --profile desktop add solopup-memory
|
|
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
|
|
|
@@ -60,3 +60,5 @@ node accept.mjs --profile desktop
|
|
|
60
60
|
MIT License. Copyright © 2026 oakfilm.
|
|
61
61
|
|
|
62
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. 从本会话已接收的用户消息提出候选,并绑定原话引用。
|
|
@@ -42,10 +48,10 @@ Markdown 索引和主题正文便于阅读;来源、候选、历史与调用
|
|
|
42
48
|
使用 DeepSeek Harness 官方插件管理器安装。代码为可直接执行的 ESM,无安装时构建脚本,也不需要 npm 账户。以下以 desktop profile 为例。
|
|
43
49
|
|
|
44
50
|
```sh
|
|
45
|
-
dsh plugin --profile desktop add solopup-memory
|
|
51
|
+
dsh plugin --profile desktop add github:OakcoderX/solopup-memory#v0.21.0-beta.3 --ignore-scripts
|
|
46
52
|
```
|
|
47
53
|
|
|
48
|
-
npm
|
|
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`。
|
|
49
55
|
|
|
50
56
|
已安装旧名 `dsh-memory` 时,先完成完整备份、关闭使用该库的宿主,用官方管理器移除旧包,再安装新包;不要同时启用两个记忆插件。新名字继续使用原记忆库和控制格式。
|
|
51
57
|
|
package/RELEASE.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
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
|
+
|
|
1
11
|
# 0.21.0-beta.2 — explicit memory workflow update
|
|
2
12
|
|
|
3
13
|
Public beta, not stable semantic acceptance. The beta.1 evidence in the historical section below is not automatically a beta.2 result.
|
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/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ 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
7
|
import {memoryProtocol} from './memory-protocol.mjs'
|
|
8
|
+
import {privateMemory,publicMemoryRecords,publicMemoryIndex,PRIVATE_MEMORY_NOTICE} from './privacy.mjs'
|
|
8
9
|
import {nativeOwnerEvidence,validateNativeWrite} from './native-write-validation.mjs'
|
|
9
10
|
import {toolProposal} from './source-record.mjs'
|
|
10
11
|
import {revisionMode,prepareValueRevision,inheritedRoots} from './field-lineage.mjs'
|
|
@@ -291,8 +292,9 @@ function renderScopeLocal(scope, dir) {
|
|
|
291
292
|
let flagged = 0
|
|
292
293
|
const kept = []
|
|
293
294
|
const policy = withdrawalPolicy(dir)
|
|
294
|
-
const typed=activeMeanings(candidateSnapshot(dir,scope).records),assessmentRecords=effectiveSourceRecords(dir,scope)
|
|
295
|
+
const typed=activeMeanings(candidateSnapshot(dir,scope).records),assessmentRecords=publicMemoryRecords(effectiveSourceRecords(dir,scope))
|
|
295
296
|
for (const line of filterWithdrawn(index.text, policy, true).split('\n')) {
|
|
297
|
+
if (privateMemory(meaningForLine(typed,line),line)) continue
|
|
296
298
|
if (line.trim().startsWith('- [')) {
|
|
297
299
|
if (isExpired(line, now)) {
|
|
298
300
|
hidden += 1
|
|
@@ -1048,16 +1050,19 @@ function writeMemoryLocal({ type, dir, line, detail, summary, expires, supersede
|
|
|
1048
1050
|
evidence: provenance?.evidence,
|
|
1049
1051
|
basis: provenance?.basis,
|
|
1050
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))))
|
|
1051
1056
|
return {
|
|
1052
1057
|
path: indexFile,
|
|
1053
1058
|
indexLine: cleanLine,
|
|
1054
1059
|
duplicate: false,
|
|
1055
1060
|
lineCount: next.split(/\r?\n/).length,
|
|
1056
1061
|
...(detailPath === undefined ? {} : { detailPath }),
|
|
1057
|
-
...(supersededLine === undefined ? {} : { superseded: supersededLine }),
|
|
1062
|
+
...(supersededLine === undefined||hiddenLine(supersededLine) ? {} : { superseded: supersededLine }),
|
|
1058
1063
|
id: newId,
|
|
1059
|
-
related,
|
|
1060
|
-
...(
|
|
1064
|
+
related:related.filter(edge=>!hiddenLine(edge.line)),
|
|
1065
|
+
...(visibleWarnings.length === 0 ? {} : { warning: visibleWarnings.join(';') }),
|
|
1061
1066
|
}
|
|
1062
1067
|
}
|
|
1063
1068
|
return lockToken === REVIEW_LOCK ? execute() : withLock(dir, execute)
|
|
@@ -1245,7 +1250,7 @@ function effectiveSourceRecords(dir,scope,indexText,includeUntyped=false){
|
|
|
1245
1250
|
}
|
|
1246
1251
|
|
|
1247
1252
|
/** Current index-only legacy entries are visible as unclassified data, never inherited sources. */
|
|
1248
|
-
function validationCurrentRecords(dir,scope,records){
|
|
1253
|
+
function validationCurrentRecords(dir,scope,records,privateTarget){
|
|
1249
1254
|
const policy=withdrawalPolicy(dir),index=filterWithdrawn(readTextSafe(join(dir,'MEMORY.md'))??'',policy,true)
|
|
1250
1255
|
const known=new Set(records.map(r=>r.indexLine??indexLine(r.type,r.summary,r.expires,r.detail!==undefined))),rows=[...records]
|
|
1251
1256
|
for(const line of new Set(index.split(/\r?\n/).map(line=>line.trim()))){
|
|
@@ -1253,7 +1258,7 @@ function validationCurrentRecords(dir,scope,records){
|
|
|
1253
1258
|
const type=/^- \[(user|feedback|project|reference)\] /.exec(line)?.[1];if(!type)continue
|
|
1254
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]})
|
|
1255
1260
|
}
|
|
1256
|
-
return rows
|
|
1261
|
+
return rows.filter(row=>!privateMemory(row)||typeof privateTarget==='string'&&privateTarget.length>0&&row.indexLine?.includes(privateTarget))
|
|
1257
1262
|
}
|
|
1258
1263
|
|
|
1259
1264
|
function publishIndexChange(dir,before,after,reason){
|
|
@@ -1269,6 +1274,7 @@ function searchHistoricalStores(stores,needle,options,context,task,limit,mode){
|
|
|
1269
1274
|
const policy=withdrawalPolicy(dir),index=filterWithdrawn(readTextSafe(join(dir,'MEMORY.md'))??'',policy,true)
|
|
1270
1275
|
const result=historyRows(dir,{scope,index,revoked:policy.revoked,atSequence:options.atSequence,asOf:options.asOf,expiredAt:isExpired});coverage.push(result.coverage)
|
|
1271
1276
|
for(const {row,line,history}of result.rows){
|
|
1277
|
+
if(options.includePrivate!==true&&privateMemory(row.source,row.line))continue
|
|
1272
1278
|
const interpretation=row.source.meaning?meaningView(row.source,context,task):undefined
|
|
1273
1279
|
if(interpretation?.applicability==='not-applicable')continue
|
|
1274
1280
|
if(interpretation)interpretation.advice={action:'continue',reason:'historical-record-is-not-a-current-instruction',authority:'none'}
|
|
@@ -1298,6 +1304,7 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1298
1304
|
if (needle.length > 200) throw new Error('memory_search: query 太长(上限 200 字)')
|
|
1299
1305
|
const limit = Math.max(1, Math.min(20, options.limit ?? 8))
|
|
1300
1306
|
const mode = options.mode ?? 'ranked'
|
|
1307
|
+
if(options.includePrivate!==undefined&&typeof options.includePrivate!=='boolean')throw Error('memory-search-invalid-private-option')
|
|
1301
1308
|
if (mode !== 'exact' && mode !== 'ranked') throw new Error('memory_search: mode 必须是 exact 或 ranked')
|
|
1302
1309
|
const context=normalizeContext(options.context),task=normalizeTask(options.task)
|
|
1303
1310
|
if(options.view!==undefined&&!['current','history'].includes(options.view))throw new Error('memory-search-invalid-view')
|
|
@@ -1306,7 +1313,7 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1306
1313
|
|
|
1307
1314
|
const files = stores.flatMap(({ dir, scope }) => {
|
|
1308
1315
|
const policy = withdrawalPolicy(dir)
|
|
1309
|
-
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))
|
|
1310
1317
|
return ['MEMORY.md', ...TYPES.map(type => `${type}.md`)].map(name => ({
|
|
1311
1318
|
name, path: join(dir, name), policy, scope, typed, owned, assessmentRecords, key: scope === undefined ? name : `${scope}/${name}`,
|
|
1312
1319
|
}))
|
|
@@ -1319,12 +1326,16 @@ function searchScopedStoresLocal(stores, query, options = {}) {
|
|
|
1319
1326
|
for (const file of files) {
|
|
1320
1327
|
const text = readTextSafe(file.path)
|
|
1321
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
|
|
1322
1332
|
let lineNumber = 0
|
|
1323
1333
|
const filtered=filterWithdrawn(text,file.policy,file.name==='MEMORY.md'),details=file.name==='MEMORY.md'?undefined:detailedMeanings(file.owned,filtered)
|
|
1324
1334
|
for (const raw of filtered.split('\n')) {
|
|
1325
1335
|
lineNumber += 1
|
|
1326
1336
|
if(file.name==='MEMORY.md'&&isExpired(raw))continue
|
|
1327
1337
|
const record=file.name==='MEMORY.md'?meaningForLine(file.typed,raw):details.get(lineNumber)
|
|
1338
|
+
if(options.includePrivate!==true&&privateMemory(record,raw))continue
|
|
1328
1339
|
if(record?.expires&&isExpired(`placeholder (exp:${record.expires})`))continue
|
|
1329
1340
|
const interpretation=record?.meaning?meaningView(record,context,task,file.assessmentRecords):undefined
|
|
1330
1341
|
if(interpretation?.applicability==='not-applicable')continue
|
|
@@ -1599,6 +1610,8 @@ function readLog(dir) {
|
|
|
1599
1610
|
* @param query - `{ id }` 或 `{ match }`(索引行子串,必须唯一)。
|
|
1600
1611
|
*/
|
|
1601
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)
|
|
1602
1615
|
assertPublicationClear(dir)
|
|
1603
1616
|
const policy = withdrawalPolicy(dir)
|
|
1604
1617
|
const events = readLog(dir).filter((e) => !policy.memoryIds.includes(e.id) && !policy.lineHashes.includes(normalizedLineHash(String(e.line ?? ''))))
|
|
@@ -1607,7 +1620,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1607
1620
|
const live = new Map()
|
|
1608
1621
|
for (const line of current.split('\n')) {
|
|
1609
1622
|
const trimmed = line.trim()
|
|
1610
|
-
if (trimmed.startsWith('- [')) live.set(entryId(trimmed), trimmed)
|
|
1623
|
+
if (trimmed.startsWith('- [')&&!hidden(trimmed)) live.set(entryId(trimmed), trimmed)
|
|
1611
1624
|
}
|
|
1612
1625
|
|
|
1613
1626
|
let key = query.id
|
|
@@ -1628,8 +1641,9 @@ function lookupLinksLocal(dir, query) {
|
|
|
1628
1641
|
const inserted = events.filter((event) => event.op === 'insert' && event.id === key)
|
|
1629
1642
|
const line = live.get(key) ?? inserted.at(-1)?.line ?? events.find((event) => event.id === key)?.line
|
|
1630
1643
|
if (line === undefined) return { found: false, related: [], backlinks: [] }
|
|
1644
|
+
if(hidden(line))return{found:false,related:[],backlinks:[]}
|
|
1631
1645
|
|
|
1632
|
-
const related = inserted.at(-1)?.related ?? []
|
|
1646
|
+
const related = (inserted.at(-1)?.related ?? []).filter(item=>!hidden(item.line))
|
|
1633
1647
|
|
|
1634
1648
|
// 入边要顺着取代链找:指向被取代旧条目的边,等同指向新条目。
|
|
1635
1649
|
// 否则「改了一条记忆」就等于把它从图上拔掉。
|
|
@@ -1640,6 +1654,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1640
1654
|
const backlinks = []
|
|
1641
1655
|
for (const event of events) {
|
|
1642
1656
|
if (event.op !== 'insert' || lineage.has(event.id)) continue
|
|
1657
|
+
if(hidden(event.line))continue
|
|
1643
1658
|
if (!(event.related ?? []).some((item) => lineage.has(item.id))) continue
|
|
1644
1659
|
if (backlinks.some((item) => item.id === event.id)) continue
|
|
1645
1660
|
backlinks.push({ id: event.id, line: event.line })
|
|
@@ -1661,7 +1676,7 @@ function lookupLinksLocal(dir, query) {
|
|
|
1661
1676
|
backlinks: backlinks.slice(0, RELATION_LIMIT),
|
|
1662
1677
|
backlinksTotal: backlinks.length,
|
|
1663
1678
|
...(replaced?.supersededById === undefined ? {} : { supersededById: replaced.supersededById }),
|
|
1664
|
-
...(supersedesLine === undefined ? {} : { supersedesLine }),
|
|
1679
|
+
...(supersedesLine === undefined||hidden(supersedesLine) ? {} : { supersedesLine }),
|
|
1665
1680
|
}
|
|
1666
1681
|
}
|
|
1667
1682
|
|
|
@@ -1777,7 +1792,7 @@ async function admitNativeWrite(ctx,sourceInputs,config,args,exec,target){
|
|
|
1777
1792
|
const revision=readOwned(()=>{
|
|
1778
1793
|
const value=storeRevision(target.dir)
|
|
1779
1794
|
const sourceRecords=effectiveSourceRecords(target.dir,target.scope,undefined,true)
|
|
1780
|
-
currentRecords=validationCurrentRecords(target.dir,target.scope,sourceRecords)
|
|
1795
|
+
currentRecords=validationCurrentRecords(target.dir,target.scope,sourceRecords,target.supersedes)
|
|
1781
1796
|
if(target.supersedes!==undefined){
|
|
1782
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')
|
|
1783
1798
|
const compiled=prepareValueRevision(target,sourceRecords,hits[0].trim())
|
|
@@ -1946,6 +1961,7 @@ function defineSearchTool(ctx) {
|
|
|
1946
1961
|
name: 'memory_search',
|
|
1947
1962
|
description:
|
|
1948
1963
|
'在记忆索引与主题文件里检索。默认 ranked 支持中英多词问题与有限英文词形归并;exact 用于精确子串定位;' +
|
|
1964
|
+
'默认隐藏已识别的个人敏感内容。用户询问相关资料时才设includePrivate=true;返回内容会进入当前会话日志和模型请求,不能声称只在本地读取。' +
|
|
1949
1965
|
'默认同时查当前项目和全局偏好,不查其他项目;可指定单库。view=history显式查询带来源的旧值、取代与移出记录,返回历史身份及覆盖范围,不能当当前指令。',
|
|
1950
1966
|
parameters: {
|
|
1951
1967
|
type: 'object',
|
|
@@ -1953,6 +1969,7 @@ function defineSearchTool(ctx) {
|
|
|
1953
1969
|
required: ['query'],
|
|
1954
1970
|
properties: {
|
|
1955
1971
|
query: { type: 'string', description: '检索词,尽量具体(≤200 字);也可给返回的完整64位recordId精确定位来源条目,不能用8位索引id' },
|
|
1972
|
+
includePrivate:{type:'boolean',description:'仅在用户询问相关个人资料时明确设true,允许读取被自动降载的个人内容。默认false;不是文件访问权限或完整隐私识别器。'},
|
|
1956
1973
|
mode: { type: 'string', enum: ['exact', 'ranked'], description: 'ranked 默认多词排序;exact 精确定位。不是语义向量检索,不支持完整同义词或英文词形分析' },
|
|
1957
1974
|
view:{type:'string',enum:['current','history'],description:'current默认只查当前视图;history查受管历史档案(已撤回来源仍不可见)。旧日志不会被伪造成完整历史。'},
|
|
1958
1975
|
atSequence:{type:'integer',description:'仅history:按选定单库的记录序号查询当时有效的条目,必须指定scope=project或global;不是跨库统一版本。'},
|
|
@@ -2000,10 +2017,10 @@ function defineSearchTool(ctx) {
|
|
|
2000
2017
|
return searchScopedStores([
|
|
2001
2018
|
{ dir: resolveStoreDir(ctx, exec, 'memory_search', 'project'), scope: 'project' },
|
|
2002
2019
|
{ dir: resolveStoreDir(ctx, exec, 'memory_search', 'global'), scope: 'global' },
|
|
2003
|
-
], 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 })
|
|
2004
2021
|
}
|
|
2005
2022
|
const dir = resolveStoreDir(ctx, exec, 'memory_search', args.scope)
|
|
2006
|
-
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 })
|
|
2007
2024
|
},
|
|
2008
2025
|
}
|
|
2009
2026
|
}
|
|
@@ -2255,7 +2272,7 @@ function defineForgetTool(ctx) {
|
|
|
2255
2272
|
text:
|
|
2256
2273
|
value.removed === 0
|
|
2257
2274
|
? `没有匹配到任何索引行:${value.path}`
|
|
2258
|
-
:
|
|
2275
|
+
: `已停止使用 ${value.removed} 条索引记忆(${value.path})。原话、主题正文、会话日志、备份及已发送内容未由此清除;这不是历史数据擦除。`,
|
|
2259
2276
|
},
|
|
2260
2277
|
],
|
|
2261
2278
|
},
|
|
@@ -2430,13 +2447,14 @@ function setupAutoCapture(ctx, config) {
|
|
|
2430
2447
|
const captureDirs=[storeRoot(),projectDir].filter(Boolean)
|
|
2431
2448
|
const memorySnapshot=managedReadMany(captureDirs,()=>{
|
|
2432
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)}})
|
|
2433
|
-
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=>{
|
|
2434
2452
|
const line=record.indexLine??indexLine(record.type,record.summary,record.expires,record.detail!==undefined)
|
|
2435
2453
|
if(!store.indexLines.includes(line))return[]
|
|
2436
2454
|
const topic=filterWithdrawn(readTextSafe(join(store.dir,record.type+'.md'))??'',store.policy)
|
|
2437
2455
|
return[captureProjection(record,store.scope,`[${store.scope}] ${line}`,topic)]
|
|
2438
2456
|
})).slice(-60)
|
|
2439
|
-
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}`))}
|
|
2440
2458
|
})
|
|
2441
2459
|
const revisions=memorySnapshot.revisions
|
|
2442
2460
|
// 按类型分库:跨项目的偏好/纠正进全局库,项目决定进项目库。
|
|
@@ -2803,7 +2821,7 @@ export function apply(ctx, config = {}) {
|
|
|
2803
2821
|
|
|
2804
2822
|
ctx.systemPrompt.context({
|
|
2805
2823
|
name: 'memory:protocol', order: CONTEXT_ORDER - 1, required: true,
|
|
2806
|
-
interpolate: false, text: context => memoryProtocol(ctx, context),
|
|
2824
|
+
interpolate: false, text: context => memoryProtocol(ctx, context)+'\n'+PRIVATE_MEMORY_NOTICE,
|
|
2807
2825
|
})
|
|
2808
2826
|
|
|
2809
2827
|
ctx.systemPrompt.context({
|
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",
|
|
@@ -147,7 +147,9 @@
|
|
|
147
147
|
"RELEASE.md",
|
|
148
148
|
"memory-protocol.mjs",
|
|
149
149
|
"capture-source-aliases.mjs",
|
|
150
|
-
"test-explicit-memory.mjs"
|
|
150
|
+
"test-explicit-memory.mjs",
|
|
151
|
+
"privacy.mjs",
|
|
152
|
+
"test-privacy.mjs"
|
|
151
153
|
],
|
|
152
154
|
"engines": {
|
|
153
155
|
"node": ">=24.9",
|
|
@@ -160,7 +162,7 @@
|
|
|
160
162
|
},
|
|
161
163
|
"license": "MIT",
|
|
162
164
|
"scripts": {
|
|
163
|
-
"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",
|
|
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",
|
|
164
166
|
"eval": "node eval/run-eval.mjs",
|
|
165
167
|
"gate": "node eval/run-eval.mjs --check",
|
|
166
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。读取结果仍会进入当前会话及模型请求;这不是加密或文件访问隔离。'
|
|
@@ -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/version.mjs
CHANGED