dsh-plugin-xujie-memory 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCEPTANCE.md +37 -0
- package/BUILD-PROVENANCE.json +53 -0
- package/CHANGELOG.md +40 -1
- package/LICENSE +21 -21
- package/README.md +67 -17
- package/README_EN.md +54 -8
- package/cordis.patch.yml +49 -49
- package/lib/client.js +17219 -0
- package/lib/controller.js +307 -0
- package/lib/index.js +200 -14
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +15104 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +15093 -0
- package/lib/types.d.ts +13 -0
- package/lib/types.js +1 -0
- package/package.json +49 -5
- package/python/specs/characters/aloof.json +14 -14
- package/python/specs/characters/gentle.json +14 -14
- package/python/specs/characters/tsundere.json +14 -14
- package/python/specs/debt.v1.json +31 -31
- package/python/specs/nickname.v1.json +16 -16
- package/python/specs/preference.v1.json +16 -16
- package/python/xujie_memory/__init__.py +123 -123
- package/python/xujie_memory/admission.py +211 -211
- package/python/xujie_memory/audit.py +377 -377
- package/python/xujie_memory/benchmark.py +281 -281
- package/python/xujie_memory/character.py +199 -199
- package/python/xujie_memory/console.py +41 -41
- package/python/xujie_memory/consolidate.py +356 -356
- package/python/xujie_memory/due.py +121 -121
- package/python/xujie_memory/emotion.py +573 -573
- package/python/xujie_memory/engine.py +1333 -1312
- package/python/xujie_memory/errors.py +98 -98
- package/python/xujie_memory/importance.py +104 -104
- package/python/xujie_memory/judge.py +350 -350
- package/python/xujie_memory/management.py +203 -0
- package/python/xujie_memory/memory.py +71 -10
- package/python/xujie_memory/orchestrator.py +690 -666
- package/python/xujie_memory/phrasing.py +292 -292
- package/python/xujie_memory/privacy.py +105 -0
- package/python/xujie_memory/schema.sql +492 -480
- package/python/xujie_memory/server.py +76 -0
- package/python/xujie_memory/specs.py +170 -170
- package/python/xujie_memory/summary.py +238 -238
- package/scripts/build.mjs +19 -0
- package/scripts/prepack.mjs +9 -0
- package/scripts/test-all.mjs +9 -0
- package/scripts/test-node.mjs +10 -0
- package/src/client/MemoryPanel.tsx +536 -0
- package/src/client/NeuralField.tsx +88 -0
- package/src/client/css.d.ts +4 -0
- package/src/client/icons.tsx +28 -0
- package/src/client/index.tsx +59 -0
- package/src/client/memory-adapter.ts +67 -0
- package/src/client/model.ts +79 -0
- package/src/client/neural/engine.ts +999 -0
- package/src/client/neural/morphology.ts +219 -0
- package/src/client/panel.css +337 -0
- package/src/client/types.ts +6 -0
- package/src/controller.ts +88 -0
- package/src/generated/typert.host.js +449 -0
- package/src/generated/typert.remote-client.js +438 -0
- package/src/index.ts +2 -0
- package/src/types.ts +13 -0
- package/test/cordis-fixture.mjs +5 -0
- package/test/management_regression.py +187 -0
- package/test/node-regression.entry.mjs +52 -0
- package/vendor/DSH-LICENSE.txt +21 -0
- package/vendor/THIRD-PARTY.md +6 -0
- package/vendor/dsh-protocol/index.ts +484 -0
- package/vendor/dsh-protocol/json-value.ts +53 -0
- package/vendor/dsh-protocol/owned-value.ts +39 -0
- package/vendor/dsh-protocol/principal.ts +17 -0
- package/vendor/dsh-protocol/remote-access.ts +38 -0
- package/vendor/dsh-protocol/remote-error.ts +49 -0
- package/vendor/dsh-protocol/remote-info.ts +161 -0
- package/vendor/dsh-protocol/types.ts +939 -0
- package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
- package/vendor/licenses/zod-MIT.txt +21 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
import threading
|
|
5
|
+
import unittest
|
|
6
|
+
import urllib.request
|
|
7
|
+
import urllib.error
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
sys.path.insert(0, str(ROOT / 'python'))
|
|
12
|
+
from xujie_memory.server import MemoryService, serve
|
|
13
|
+
from xujie_memory.management import Management, ManagementError
|
|
14
|
+
from xujie_memory.summary import SummaryStore
|
|
15
|
+
from xujie_memory.profile import ProfileStore
|
|
16
|
+
|
|
17
|
+
class ManagementRegression(unittest.TestCase):
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
20
|
+
self.db = Path(self.tmp.name) / 'memory.db'
|
|
21
|
+
self.svc = MemoryService(self.db, ROOT / 'python/specs')
|
|
22
|
+
self.m = Management(self.svc)
|
|
23
|
+
self.svc.turn('one', '我不吃辣', user='owner', context={'peer_id':'assistant'})
|
|
24
|
+
self.scope = self.svc.scope_of('one', 'owner')
|
|
25
|
+
self.tid = self.svc.engine._conn().execute('SELECT trace_id FROM traces WHERE scope_id=?', (self.scope,)).fetchone()[0]
|
|
26
|
+
self.hash = self.m.detail(self.scope,self.tid)['trace']['content_hash']
|
|
27
|
+
def tearDown(self):
|
|
28
|
+
self.svc.close()
|
|
29
|
+
self.tmp.cleanup()
|
|
30
|
+
def test_catalog_and_graph_use_real_records(self):
|
|
31
|
+
self.assertEqual(self.m.catalog()['scopes'][0]['scope_id'],self.scope)
|
|
32
|
+
snap = self.m.snapshot(self.scope)
|
|
33
|
+
self.assertTrue(any(n['id']==self.tid for n in snap['nodes']))
|
|
34
|
+
self.assertTrue(any(n['layer']=='working' for n in snap['nodes']))
|
|
35
|
+
self.assertTrue(any(n['layer']=='semantic' for n in snap['nodes']))
|
|
36
|
+
self.assertTrue(snap['cards'])
|
|
37
|
+
self.assertTrue(all(e['kind'] in ('evidence','working_projection') for e in snap['edges']))
|
|
38
|
+
def test_scope_isolation(self):
|
|
39
|
+
other = self.svc.scope_of('other','different-user')
|
|
40
|
+
for method,args in [(self.m.detail,(other,self.tid)),(self.m.edit,(other,self.tid,'我喜欢咖啡',self.hash)),(self.m.forget,(other,self.tid,self.hash))]:
|
|
41
|
+
with self.assertRaisesRegex(ManagementError,'trace_not_found'): method(*args)
|
|
42
|
+
self.assertEqual(self.m.search(other,'吃辣')['hits'],[])
|
|
43
|
+
def test_cross_session_user_scope(self):
|
|
44
|
+
self.assertEqual(self.scope,self.svc.scope_of('two','owner'))
|
|
45
|
+
def test_management_search_finds_old_records_after_2000_new_traces(self):
|
|
46
|
+
engine = self.svc.engine
|
|
47
|
+
with engine._tx() as conn:
|
|
48
|
+
conn.execute("UPDATE traces SET recorded_at='2000-01-01T00:00:00Z' WHERE trace_id=?", (self.tid,))
|
|
49
|
+
for i in range(2000):
|
|
50
|
+
engine.record_trace(self.scope, f'stress:{i}', 'owner', 'user_statement', f'测试记忆编号{i:04d}')
|
|
51
|
+
self.assertEqual(self.m.snapshot(self.scope)['stats']['traces'], 2001)
|
|
52
|
+
self.assertTrue(self.m.snapshot(self.scope)['truncated'])
|
|
53
|
+
self.assertEqual(self.m.search(self.scope, '我不吃辣')['hits'][0]['trace_id'], self.tid)
|
|
54
|
+
self.assertFalse(any(hit['trace_id'] == self.tid for hit in self.m.memory.search(self.scope, '我不吃辣')))
|
|
55
|
+
self.m.forget(self.scope, self.tid, self.hash)
|
|
56
|
+
self.assertFalse(any(hit['trace_id'] == self.tid for hit in self.m.search(self.scope, '我不吃辣')['hits']))
|
|
57
|
+
def test_edit_replaces_and_reindexes_persistently(self):
|
|
58
|
+
result = self.m.edit(self.scope,self.tid,'我喜欢咖啡',self.hash)
|
|
59
|
+
new = result['trace']['trace_id']
|
|
60
|
+
self.assertNotEqual(new,self.tid)
|
|
61
|
+
self.assertEqual(self.svc.engine.get_trace(self.tid)['content'],'')
|
|
62
|
+
self.assertEqual(self.m.search(self.scope,'咖啡')['hits'][0]['trace_id'],new)
|
|
63
|
+
self.assertFalse(self.m.search(self.scope,'吃辣')['hits'])
|
|
64
|
+
card = ProfileStore(self.svc.engine).card(self.scope,'owner')
|
|
65
|
+
self.assertTrue(any('咖啡' in t['value'] for t in card['tags']))
|
|
66
|
+
self.assertFalse(any('辣' in t['value'] for t in card['tags']))
|
|
67
|
+
self.svc.close()
|
|
68
|
+
self.svc = MemoryService(self.db, ROOT/'python/specs')
|
|
69
|
+
self.m = Management(self.svc)
|
|
70
|
+
self.assertEqual(self.m.detail(self.scope,new)['trace']['content'],'我喜欢咖啡')
|
|
71
|
+
def test_stale_edit_and_delete_are_rejected(self):
|
|
72
|
+
for method,args in [(self.m.edit,(self.scope,self.tid,'我喜欢咖啡','stale')),(self.m.forget,(self.scope,self.tid,'stale'))]:
|
|
73
|
+
with self.assertRaisesRegex(ManagementError,'version_conflict'): method(*args)
|
|
74
|
+
self.assertEqual(self.m.detail(self.scope,self.tid)['trace']['content'],'我不吃辣')
|
|
75
|
+
def test_edit_is_atomic_on_encoder_failure(self):
|
|
76
|
+
with patch.object(self.svc.orchestrator.ingest.encoder,'encode',side_effect=RuntimeError('synthetic failure')):
|
|
77
|
+
with self.assertRaisesRegex(RuntimeError,'synthetic failure'):
|
|
78
|
+
self.m.edit(self.scope,self.tid,'我喜欢咖啡',self.hash)
|
|
79
|
+
self.assertEqual(self.m.detail(self.scope,self.tid)['trace']['content'],'我不吃辣')
|
|
80
|
+
self.assertTrue(self.m.search(self.scope,'吃辣')['hits'])
|
|
81
|
+
self.assertEqual(self.svc.engine._conn().execute('SELECT COUNT(*) FROM memory_edits').fetchone()[0],0)
|
|
82
|
+
def test_edit_preserves_anchor(self):
|
|
83
|
+
self.m.memory.mark(self.scope,self.tid,5)
|
|
84
|
+
result = self.m.edit(self.scope,self.tid,'我喜欢咖啡',self.hash)
|
|
85
|
+
self.assertTrue(result['trace']['anchor'])
|
|
86
|
+
self.assertEqual(result['trace']['importance'],5)
|
|
87
|
+
def test_edit_preserves_original_source_and_event_time(self):
|
|
88
|
+
original_time = "2025-03-12T08:30:00+00:00"
|
|
89
|
+
self.svc.engine._conn().execute(
|
|
90
|
+
"UPDATE traces SET source_time=?,source_time_precision='second' WHERE trace_id=?",
|
|
91
|
+
(original_time,self.tid))
|
|
92
|
+
result = self.m.edit(self.scope,self.tid,'我喜欢咖啡',self.hash)
|
|
93
|
+
replacement = self.svc.engine.get_trace(result['trace']['trace_id'])
|
|
94
|
+
self.assertEqual(replacement['source_time'],original_time)
|
|
95
|
+
self.assertEqual(replacement['source_time_precision'],'second')
|
|
96
|
+
rows = self.svc.engine._conn().execute(
|
|
97
|
+
'SELECT e.effective_at FROM events e JOIN event_evidence v ON v.event_id=e.event_id '
|
|
98
|
+
'WHERE v.trace_id=? AND e.void=0',(replacement['trace_id'],)).fetchall()
|
|
99
|
+
self.assertTrue(rows)
|
|
100
|
+
self.assertTrue(all(row['effective_at']==original_time for row in rows))
|
|
101
|
+
def test_summary_and_anchor_projections_keep_real_evidence(self):
|
|
102
|
+
self.m.memory.mark(self.scope,self.tid,5)
|
|
103
|
+
summary = SummaryStore(self.svc.engine).create(self.scope,[self.tid])
|
|
104
|
+
snap = self.m.snapshot(self.scope)
|
|
105
|
+
self.assertTrue(any(n['kind']=='summary' and self.tid in n.get('evidence',[]) for n in snap['nodes']))
|
|
106
|
+
self.assertTrue(any(n['kind']=='anchor' and n['trace_id']==self.tid for n in snap['nodes']))
|
|
107
|
+
self.assertTrue(any(e['kind']=='summary_input' and e['source']==self.tid for e in snap['edges']))
|
|
108
|
+
self.assertTrue(any(e['kind']=='anchor_projection' and e['source']==self.tid for e in snap['edges']))
|
|
109
|
+
def test_delete_purges_summary_and_person_card(self):
|
|
110
|
+
summary = SummaryStore(self.svc.engine).create(self.scope,[self.tid])
|
|
111
|
+
self.m.forget(self.scope,self.tid,self.hash)
|
|
112
|
+
saved = SummaryStore(self.svc.engine).get(summary.summary_id)
|
|
113
|
+
self.assertEqual(saved['text'],'')
|
|
114
|
+
self.assertEqual(saved['lifecycle_state'],'archived')
|
|
115
|
+
self.assertFalse(self.m.search(self.scope,'吃辣')['hits'])
|
|
116
|
+
self.assertFalse(any('辣' in t['value'] for t in ProfileStore(self.svc.engine).card(self.scope,'owner')['tags']))
|
|
117
|
+
self.assertEqual(self.svc.engine._conn().execute('SELECT COUNT(*) FROM traces_fts WHERE trace_id=?',(self.tid,)).fetchone()[0],0)
|
|
118
|
+
self.svc.close()
|
|
119
|
+
self.svc = MemoryService(self.db, ROOT/'python/specs')
|
|
120
|
+
self.assertTrue(self.svc.engine.get_trace(self.tid)['erased_at'])
|
|
121
|
+
def test_upgrade_repairs_old_erased_summary_and_keeps_private_backup(self):
|
|
122
|
+
summary = SummaryStore(self.svc.engine).create(self.scope,[self.tid])
|
|
123
|
+
self.m.forget(self.scope,self.tid,self.hash)
|
|
124
|
+
# Reproduce the pre-0.7 residue; the old package is not required by tests.
|
|
125
|
+
c = self.svc.engine._conn()
|
|
126
|
+
c.execute("UPDATE summaries SET text='我不吃辣',lifecycle_state='active' WHERE summary_id=?",(summary.summary_id,))
|
|
127
|
+
c.execute('DROP TABLE memory_edits')
|
|
128
|
+
c.execute('DROP TABLE memory_maintenance')
|
|
129
|
+
self.svc.close()
|
|
130
|
+
self.svc = MemoryService(self.db, ROOT/'python/specs')
|
|
131
|
+
self.m = Management(self.svc)
|
|
132
|
+
saved = SummaryStore(self.svc.engine).get(summary.summary_id)
|
|
133
|
+
self.assertEqual(saved['text'],'')
|
|
134
|
+
self.assertEqual(saved['lifecycle_state'],'archived')
|
|
135
|
+
self.assertTrue(Path(str(self.db)+'.pre-0.7.0.bak').is_file())
|
|
136
|
+
self.assertFalse(self.m.search(self.scope,'吃辣')['hits'])
|
|
137
|
+
def test_session_scope_remains_separate(self):
|
|
138
|
+
other = MemoryService(Path(self.tmp.name)/'session.db',ROOT/'python/specs',scope_mode='session')
|
|
139
|
+
try:
|
|
140
|
+
self.assertNotEqual(other.scope_of('s1','owner'),other.scope_of('s2','owner'))
|
|
141
|
+
finally:
|
|
142
|
+
other.close()
|
|
143
|
+
def test_unchanged_edit_is_noop(self):
|
|
144
|
+
self.assertFalse(self.m.edit(self.scope,self.tid,'我不吃辣',self.hash)['changed'])
|
|
145
|
+
def test_invalid_text_rejected(self):
|
|
146
|
+
for s in (' ', '字'*2001):
|
|
147
|
+
with self.assertRaisesRegex(ManagementError,'invalid_content_length'):
|
|
148
|
+
self.m.edit(self.scope,self.tid,s,self.hash)
|
|
149
|
+
def test_concurrent_edit_has_exactly_one_winner(self):
|
|
150
|
+
outcomes = []
|
|
151
|
+
def edit(content):
|
|
152
|
+
try: outcomes.append(self.m.edit(self.scope,self.tid,content,self.hash)['ok'])
|
|
153
|
+
except ManagementError: outcomes.append(False)
|
|
154
|
+
ts = [threading.Thread(target=edit,args=(s,)) for s in ('我喜欢咖啡','我喜欢茶')]
|
|
155
|
+
for t in ts:t.start()
|
|
156
|
+
for t in ts:t.join()
|
|
157
|
+
self.assertEqual(sorted(outcomes),[False,True])
|
|
158
|
+
|
|
159
|
+
class HttpRegression(unittest.TestCase):
|
|
160
|
+
def test_http_auth_read_write_conflict_and_no_cors(self):
|
|
161
|
+
with tempfile.TemporaryDirectory() as d:
|
|
162
|
+
h = serve(Path(d)/'m.db',ROOT/'python/specs',port=0,token='test-token-only')
|
|
163
|
+
thread = threading.Thread(target=h.serve_forever,daemon=True);thread.start()
|
|
164
|
+
url = f'http://127.0.0.1:{h.server_port}'
|
|
165
|
+
def post(path,body,token='test-token-only'):
|
|
166
|
+
req=urllib.request.Request(url+path,data=json.dumps(body).encode(),headers={'Content-Type':'application/json','X-Xujie-Token':token})
|
|
167
|
+
try:
|
|
168
|
+
with urllib.request.urlopen(req) as r:return r.status,json.load(r),r.headers
|
|
169
|
+
except urllib.error.HTTPError as r:return r.code,json.load(r),r.headers
|
|
170
|
+
try:
|
|
171
|
+
self.assertEqual(post('/manage/catalog',{},'wrong')[0],401)
|
|
172
|
+
status,_,headers=post('/manage/catalog',{})
|
|
173
|
+
self.assertEqual(status,200);self.assertNotIn('Access-Control-Allow-Origin',headers)
|
|
174
|
+
self.assertEqual(post('/turn',{'session':'s','text':'我不吃辣'})[0],200)
|
|
175
|
+
scope=post('/manage/catalog',{})[1]['scopes'][0]['scope_id']
|
|
176
|
+
snap=post('/manage/snapshot',{'scope_id':scope})[1]
|
|
177
|
+
trace=next(n for n in snap['nodes'] if n['kind']=='trace')
|
|
178
|
+
args={'scope_id':scope,'trace_id':trace['trace_id'],'content':'我喜欢咖啡','expected_hash':'stale'}
|
|
179
|
+
self.assertEqual(post('/manage/edit',args)[0],409)
|
|
180
|
+
args['expected_hash']=trace['content_hash']
|
|
181
|
+
status,edited,_=post('/manage/edit',args);self.assertEqual(status,200)
|
|
182
|
+
new=edited['trace']
|
|
183
|
+
self.assertEqual(post('/manage/forget',{'scope_id':scope,'trace_id':new['trace_id'],'expected_hash':new['content_hash']})[0],200)
|
|
184
|
+
finally:
|
|
185
|
+
h.shutdown();h.server_close();h.service.close();thread.join()
|
|
186
|
+
|
|
187
|
+
if __name__=='__main__':unittest.main(verbosity=2)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs'
|
|
3
|
+
import {tmpdir} from 'node:os'
|
|
4
|
+
import {join} from 'node:path'
|
|
5
|
+
import {spawn} from 'node:child_process'
|
|
6
|
+
import {once} from 'node:events'
|
|
7
|
+
import {apply} from '../lib/index.js'
|
|
8
|
+
import {TYPERT_REMOTE} from '../lib/typert.remote-client.js'
|
|
9
|
+
const dir=mkdtempSync(join(tmpdir(),'xujie-node-'))
|
|
10
|
+
const token='isolated-test-token'
|
|
11
|
+
writeFileSync(join(dir,'token'),token,{mode:0o600})
|
|
12
|
+
const python=process.env.PYTHON ?? 'python3'
|
|
13
|
+
const kernel=spawn(python,['-u','-c',`from xujie_memory.server import serve\nfrom pathlib import Path\nh=serve(Path(${JSON.stringify(join(dir,'memory.db'))}),Path(${JSON.stringify(join(process.cwd(),'python/specs'))}),port=0,token=${JSON.stringify(token)})\nprint(h.server_port,flush=True)\nh.serve_forever()`],{env:{...process.env,PYTHONPATH:join(process.cwd(),'python')},stdio:['ignore','pipe','pipe']})
|
|
14
|
+
let checks=0
|
|
15
|
+
const check=(label,fn)=>{fn();checks++;console.log('PASS',label)}
|
|
16
|
+
const cleanups=[]
|
|
17
|
+
try {
|
|
18
|
+
let text='';const port=await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(new Error('kernel startup timeout')),10000);kernel.stdout.on('data',chunk=>{text+=chunk;if(text.includes('\n')){clearTimeout(timeout);resolve(Number(text.trim().split('\n')[0]))}});kernel.once('error',reject);kernel.once('exit',code=>{if(!text)reject(new Error('kernel early exit '+code))})})
|
|
19
|
+
const endpoint=`http://127.0.0.1:${port}`
|
|
20
|
+
const handlers=new Map(),tools=new Map();let section
|
|
21
|
+
const ctx={_services:{},invocation:{peer:{principal:{kind:'owner'}}},logger:{info(){},warn(){},debug(){}},tools:{register(t){tools.set(t.name,t);return()=>tools.delete(t.name)}},effect(fn){const dispose=fn();if(typeof dispose==='function')cleanups.push(dispose);return dispose},inject(deps,fn){fn(ctx)},get(key){if(key==='systemPrompt')return{section(s){section=s}};return ctx._services[key]},on(key,fn){handlers.set(key,fn);return()=>handlers.delete(key)}}
|
|
22
|
+
apply(ctx,{autoStart:false,endpoint,tokenFile:join(dir,'token')})
|
|
23
|
+
check('four memory tools are registered',()=>assert.deepEqual([...tools.keys()].sort(),['memory_edit','memory_forget','memory_search','memory_status']))
|
|
24
|
+
const alice={id:'alice-one',userId:'alice'},bob={id:'bob-one',userId:'bob'}
|
|
25
|
+
const event=text=>({type:'user/message',data:{source:{kind:'user'},content:[{type:'text',text}]}})
|
|
26
|
+
handlers.get('session/event')(alice,event('我不吃辣'))
|
|
27
|
+
const stats=await tools.get('memory_status').execute({}, {session:alice})
|
|
28
|
+
check('automatic turn writes through real Python HTTP kernel',()=>assert.equal(stats.traces,1))
|
|
29
|
+
check('prompt contains stored memory',()=>assert.ok(section.text({agent:{session:alice}}).includes('辣')))
|
|
30
|
+
check('new user cannot inherit another user fallback',()=>assert.equal(section.text({agent:{session:bob}}),''))
|
|
31
|
+
check('assembly without session does not expose fallback',()=>assert.equal(section.text({}),''))
|
|
32
|
+
const controller=ctx._services.xujieMemory
|
|
33
|
+
const catalog=await controller.catalog(),scope=catalog.scopes[0].scope_id
|
|
34
|
+
const snapshot=await controller.snapshot(scope)
|
|
35
|
+
check('generated snapshot codec accepts real projected DTO',()=>assert.ok(TYPERT_REMOTE.methods['xujieMemory/snapshot'].result.create().safeParse(snapshot).success))
|
|
36
|
+
const node=snapshot.nodes.find(n=>n.kind==='trace')
|
|
37
|
+
const saved=await controller.edit(scope,node.trace_id,'我喜欢咖啡',node.content_hash)
|
|
38
|
+
check('generated edit codec accepts actual result',()=>assert.ok(TYPERT_REMOTE.methods['xujieMemory/edit'].result.create().safeParse(saved).success))
|
|
39
|
+
check('UI mutation invalidates prompt cache',()=>assert.equal(section.text({agent:{session:alice}}),''))
|
|
40
|
+
const hits=await controller.search(scope,'咖啡')
|
|
41
|
+
check('search returns persisted replacement evidence',()=>assert.equal(hits[0].trace_id,saved.trace.trace_id))
|
|
42
|
+
check('generated search codec accepts actual result',()=>assert.ok(TYPERT_REMOTE.methods['xujieMemory/search'].result.create().safeParse(hits).success))
|
|
43
|
+
ctx.invocation={peer:{principal:{kind:'guest'}}}
|
|
44
|
+
await assert.rejects(()=>controller.catalog(),e=>e.code==='xujie-memory/owner-required');checks++;console.log('PASS guest owner-only boundary')
|
|
45
|
+
ctx.invocation={peer:{principal:{kind:'owner'}}}
|
|
46
|
+
const modelEdit=await tools.get('memory_edit').execute({trace_id:hits[0].trace_id,content:'我喜欢绿茶',expected_hash:hits[0].content_hash},{session:alice})
|
|
47
|
+
check('model correction tool writes the actual memory DB',()=>assert.equal(modelEdit.trace.content,'我喜欢绿茶'))
|
|
48
|
+
await controller.forget(scope,modelEdit.trace.trace_id,modelEdit.trace.content_hash)
|
|
49
|
+
const remaining=await controller.search(scope,'绿茶')
|
|
50
|
+
check('GUI forget removes raw text from live DB search',()=>assert.deepEqual(remaining,[]))
|
|
51
|
+
} catch(error){console.error(error);process.exitCode=1}
|
|
52
|
+
finally{for(const dispose of cleanups.reverse())try{dispose()}catch{};kernel.kill();await Promise.race([once(kernel,'exit'),new Promise(r=>setTimeout(r,2000))]);rmSync(dir,{recursive:true,force:true});console.log('Completed checks:',checks)}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bundled source and runtime notices
|
|
2
|
+
|
|
3
|
+
- `dsh-protocol/`: copies of DeepSeek Harness's MIT-licensed Typert protocol helpers at the exact rc.5 source commit recorded in BUILD-PROVENANCE.json. See DSH-LICENSE.txt.
|
|
4
|
+
- Compiled strict contract descriptors bundle Zod and source-map helper code. Their MIT notices are retained under licenses/.
|
|
5
|
+
- React/JSX runtime and Cordis are shared host externals, not bundled copies.
|
|
6
|
+
- The bundled Windows Python runtime and its existing license files are unchanged from the supplied 0.6.0 distribution.
|