dsh-memu-memory 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +8 -6
  2. package/README.md +50 -5
  3. package/README.zh.md +49 -5
  4. package/docs/active-memory-ab.md +385 -0
  5. package/docs/binding-lifecycle.md +99 -0
  6. package/docs/contributing.md +6 -4
  7. package/docs/emotion-model-spec.md +281 -0
  8. package/docs/env-and-commands.md +13 -0
  9. package/docs/host-api.md +35 -3
  10. package/docs/memory-semantics.md +40 -11
  11. package/docs/recall-benchmark.md +238 -0
  12. package/docs/troubleshooting.md +395 -366
  13. package/lib/active-memory.js +165 -0
  14. package/lib/activity-log.js +190 -0
  15. package/lib/backup.js +1103 -0
  16. package/lib/binding-diagnostics.js +635 -0
  17. package/lib/client.js +728 -58
  18. package/lib/consolidation-ledger.js +347 -0
  19. package/lib/consolidation-receipt.js +284 -0
  20. package/lib/consolidation-snapshot.js +274 -0
  21. package/lib/consolidation-worker.js +115 -0
  22. package/lib/curator.js +81 -37
  23. package/lib/cycle.js +43 -6
  24. package/lib/embedding-catalog.js +763 -0
  25. package/lib/embedding-download.js +407 -0
  26. package/lib/embedding-runtime.js +710 -0
  27. package/lib/engine.js +2 -2
  28. package/lib/index.js +36 -0
  29. package/lib/inject.js +232 -103
  30. package/lib/local-embedding-entry.js +145 -0
  31. package/lib/local-embedding-manager.js +267 -0
  32. package/lib/local-embedding-protocol.js +173 -0
  33. package/lib/local-embedding-worker.js +652 -0
  34. package/lib/local-vector-service.js +283 -0
  35. package/lib/memory.js +599 -57
  36. package/lib/memu-config.js +10 -22
  37. package/lib/memu.js +53 -19
  38. package/lib/mood.js +268 -39
  39. package/lib/paths.js +24 -0
  40. package/lib/preset-scope.js +1 -1
  41. package/lib/private.js +450 -29
  42. package/lib/prompts/private-consolidation.coding.md +1 -0
  43. package/lib/prompts/private-consolidation.md +1 -0
  44. package/lib/prompts/private-consolidation.mixed.md +1 -0
  45. package/lib/prompts/private-consolidation.rp.md +1 -0
  46. package/lib/prompts.js +4 -4
  47. package/lib/remote-summary.js +51 -25
  48. package/lib/rpc.js +54 -5
  49. package/lib/state.js +137 -172
  50. package/lib/tools.js +58 -7
  51. package/lib/vector-migration.js +878 -0
  52. package/lib/vector-profile.js +968 -0
  53. package/package.json +4 -2
  54. package/scripts/acceptance-local.mjs +7 -5
  55. package/scripts/active-memory-ab-live.mjs +655 -0
  56. package/scripts/active-memory-ab.mjs +1302 -0
  57. package/scripts/chain-matrix.mjs +537 -0
  58. package/scripts/check-all.mjs +29 -1
  59. package/scripts/check-doc-drift.mjs +353 -0
  60. package/scripts/diagnose-bindings.mjs +176 -0
  61. package/scripts/local-runtime-install.mjs +142 -0
  62. package/scripts/local-runtime-verify.mjs +56 -0
  63. package/scripts/local-vector-verify.mjs +93 -0
  64. package/scripts/recall-bench.mjs +813 -0
  65. package/scripts/repetition-ab-scenarios.mjs +93 -0
  66. package/scripts/repetition-detector-snapshot.mjs +314 -0
  67. package/scripts/repetition-eval-live.mjs +360 -0
  68. package/scripts/repetition-eval.mjs +702 -0
  69. package/tests/activation.test.mjs +89 -0
  70. package/tests/active-memory-ab.test.mjs +745 -0
  71. package/tests/active-memory.test.mjs +129 -0
  72. package/tests/activity-log.test.mjs +126 -0
  73. package/tests/activity-wiring.test.mjs +103 -0
  74. package/tests/appendix-b.test.mjs +212 -0
  75. package/tests/appendix-b2.test.mjs +166 -0
  76. package/tests/audit-regression.test.mjs +272 -0
  77. package/tests/audit-safety.test.mjs +227 -0
  78. package/tests/backend-compensation.test.mjs +272 -0
  79. package/tests/backup.test.mjs +793 -0
  80. package/tests/binding-diagnostics.test.mjs +720 -0
  81. package/tests/browser.test.mjs +366 -0
  82. package/tests/client-render.test.mjs +892 -0
  83. package/tests/coalesce.test.mjs +113 -0
  84. package/tests/config.test.mjs +159 -0
  85. package/tests/consolidation-credential.test.mjs +320 -0
  86. package/tests/consolidation-fork.test.mjs +357 -0
  87. package/tests/consolidation-ledger.test.mjs +298 -0
  88. package/tests/consolidation-snapshot.test.mjs +321 -0
  89. package/tests/consolidation-value.test.mjs +89 -0
  90. package/tests/context-scope.test.mjs +40 -0
  91. package/tests/cue.test.mjs +44 -0
  92. package/tests/curator-probe.test.mjs +433 -0
  93. package/tests/curator.test.mjs +150 -0
  94. package/tests/cycle.test.mjs +411 -0
  95. package/tests/disclosure.test.mjs +231 -0
  96. package/tests/doc-drift.test.mjs +222 -0
  97. package/tests/embedding-catalog.test.mjs +266 -0
  98. package/tests/embedding-download.test.mjs +391 -0
  99. package/tests/embedding-runtime.test.mjs +389 -0
  100. package/tests/emotion-chain.test.mjs +84 -0
  101. package/tests/emotion-contract.test.mjs +445 -0
  102. package/tests/engine.test.mjs +35 -0
  103. package/tests/fixtures/active-memory-ab/README.md +56 -0
  104. package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
  105. package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
  106. package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
  107. package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
  108. package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
  109. package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
  110. package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
  111. package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
  112. package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
  113. package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
  114. package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
  115. package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
  116. package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
  117. package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
  118. package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
  119. package/tests/fixtures/active-memory-ab/manifest.json +199 -0
  120. package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
  121. package/tests/fixtures/recall-bench/02-exact.json +153 -0
  122. package/tests/fixtures/recall-bench/03-negation.json +173 -0
  123. package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
  124. package/tests/fixtures/recall-bench/05-conflict.json +194 -0
  125. package/tests/fixtures/recall-bench/06-boundary.json +273 -0
  126. package/tests/fixtures/recall-bench/07-secret.json +191 -0
  127. package/tests/fixtures/recall-bench/08-coding.json +164 -0
  128. package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
  129. package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
  130. package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
  131. package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
  132. package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
  133. package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
  134. package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
  135. package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
  136. package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
  137. package/tests/fixtures/repetition-eval/02-en.json +97 -0
  138. package/tests/fixtures/repetition-eval/03-code.json +91 -0
  139. package/tests/fixtures/repetition-eval/04-names.json +116 -0
  140. package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
  141. package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
  142. package/tests/graph-view.test.mjs +187 -0
  143. package/tests/graph.test.mjs +77 -0
  144. package/tests/hook-order.test.mjs +72 -0
  145. package/tests/humanlike.test.mjs +232 -0
  146. package/tests/hybrid-recall.test.mjs +92 -0
  147. package/tests/inbound.test.mjs +393 -0
  148. package/tests/inject-guide.test.mjs +364 -0
  149. package/tests/inject.test.mjs +222 -0
  150. package/tests/injection-budget-settings.test.mjs +35 -0
  151. package/tests/injection-budget.test.mjs +64 -0
  152. package/tests/injection-chain.test.mjs +191 -0
  153. package/tests/loader-register.mjs +3 -0
  154. package/tests/loader.mjs +12 -0
  155. package/tests/local-embedding-entry.test.mjs +95 -0
  156. package/tests/local-embedding-manager.test.mjs +476 -0
  157. package/tests/local-embedding-protocol.test.mjs +95 -0
  158. package/tests/local-embedding-worker.test.mjs +346 -0
  159. package/tests/managed-memory.test.mjs +536 -0
  160. package/tests/managed-runtime.test.mjs +40 -0
  161. package/tests/media.test.mjs +132 -0
  162. package/tests/migration-payload.test.mjs +42 -0
  163. package/tests/mood.test.mjs +109 -0
  164. package/tests/multiprocess.test.mjs +29 -0
  165. package/tests/new-session-gate.test.mjs +361 -0
  166. package/tests/package-shipping.test.mjs +84 -0
  167. package/tests/preset-scope.test.mjs +212 -0
  168. package/tests/prompts.test.mjs +250 -0
  169. package/tests/react-snapshot.test.mjs +23 -0
  170. package/tests/recall-bench.test.mjs +306 -0
  171. package/tests/remote.test.mjs +140 -0
  172. package/tests/rename.test.mjs +30 -0
  173. package/tests/repetition-detector-snapshot.test.mjs +231 -0
  174. package/tests/repetition-eval-live.test.mjs +177 -0
  175. package/tests/repetition-eval.test.mjs +373 -0
  176. package/tests/salient-date.test.mjs +142 -0
  177. package/tests/settings-beat.test.mjs +221 -0
  178. package/tests/short-term.test.mjs +166 -0
  179. package/tests/sidebar-beat.test.mjs +208 -0
  180. package/tests/sidebar-refresh.test.mjs +123 -0
  181. package/tests/sidebar.test.mjs +70 -0
  182. package/tests/snippet.test.mjs +47 -0
  183. package/tests/source-kind.test.mjs +52 -0
  184. package/tests/state.test.mjs +150 -0
  185. package/tests/stats.test.mjs +112 -0
  186. package/tests/storage.test.mjs +125 -0
  187. package/tests/stubs/dsh-llm.js +5 -0
  188. package/tests/stubs/dsh-session.js +7 -0
  189. package/tests/stubs/dsh-tools.js +101 -0
  190. package/tests/tool-schema.test.mjs +73 -0
  191. package/tests/tools.test.mjs +184 -0
  192. package/tests/ui-defaults.test.mjs +57 -0
  193. package/tests/update-fields.test.mjs +138 -0
  194. package/tests/vector-migration.test.mjs +733 -0
  195. package/tests/vector-profile.test.mjs +425 -0
  196. package/tests/visibility-migration.test.mjs +152 -0
  197. package/tests/webui-auth.test.mjs +454 -0
  198. package/tests/webui.test.mjs +164 -0
  199. package/tests/windows-env.test.mjs +89 -0
@@ -0,0 +1,64 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+
7
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-injection-budget-'))
8
+ process.env.DSH_MEMU_HOME = home
9
+
10
+ const state = await import('../lib/state.js')
11
+ const inject = await import('../lib/inject.js')
12
+ const budget = await import('../lib/budget.js')
13
+
14
+ test.after(() => {
15
+ try { fs.rmSync(home, { recursive: true, force: true }) } catch {}
16
+ })
17
+
18
+ function pluginMessages(messages) {
19
+ return (messages || []).filter((message) => message?.role === 'user' && message?.source?.kind === 'plugin:' + inject.PLUGIN)
20
+ }
21
+
22
+ function pluginText(messages) {
23
+ return pluginMessages(messages).map((message) => String(message?.content?.[0]?.text || '')).join('\n')
24
+ }
25
+
26
+ function seedRecallMemory(libraryId, layer = 'long_term') {
27
+ const index = state.loadPrivateIndex(libraryId)
28
+ const createdAt = new Date().toISOString()
29
+ index.entries['mem-budget-recall'] = {
30
+ track: 'memory',
31
+ layer,
32
+ title: 'deploy script',
33
+ content: 'The deploy script lives in tools/deploy.ps1 and needs the staging token before the production rollout. ' +
34
+ 'Run tools/deploy.ps1 with the staging token first, then promote it to production after the smoke test passes. ' +
35
+ 'The deploy script logs to tools/deploy.log and refuses to run without the staging token.',
36
+ status: 'active',
37
+ confidence: 0.9,
38
+ createdAt,
39
+ updatedAt: createdAt,
40
+ ...(layer === 'short_term' ? { expiresAt: new Date(Date.now() + 72 * 3600 * 1000).toISOString() } : {}),
41
+ }
42
+ state.savePrivateIndex(libraryId, index)
43
+ }
44
+
45
+ function makeTurn(sessionId, { cwd, agentPreset, text }) {
46
+ const user = { id: sessionId + '-u1', role: 'user', source: { kind: 'user' }, content: [{ type: 'text', text }] }
47
+ const agent = { session: { header: { id: sessionId, cwd, agentPreset, origin: 'user' } } }
48
+ return { agent, decision: { kind: 'enter', messages: [user] } }
49
+ }
50
+
51
+ test('turnTokens stays the recall budget while injectionTotalTokens gates the whole message', async () => {
52
+ state.updateSettings({ turnTokens: 640, injectionTotalTokens: 2000, randomCueChance: 0, habitsInjection: false })
53
+ const cwd = path.join(home, 'project-budget-total')
54
+ const binding = state.ensureSession('budget-total', { cwd, agentPreset: 'coding' })
55
+ seedRecallMemory(binding.libraryId, 'short_term')
56
+ state.setSessionBinding('budget-total', { injectedIds: [inject.ANCHOR_SENTINEL] })
57
+ const turn = makeTurn('budget-total', { cwd, agentPreset: 'coding', text: 'where does the deploy script live' })
58
+ const out = await inject.injectForStep({ agent: turn.agent, decision: turn.decision, turn: 1, step: 1, signal: { aborted: false } })
59
+ const text = pluginText(out.messages)
60
+ const used = budget.estTokens(text)
61
+ assert.ok(text.includes('[mem-budget-recall]'), text)
62
+ assert.ok(used <= 2000, 'used ' + used)
63
+ assert.ok(used <= 640 + 200, 'the recall block is capped by turnTokens, not the total: ' + used)
64
+ })
@@ -0,0 +1,191 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-memory-chain-'))
9
+ process.env.DSH_MEMU_HOME = home
10
+
11
+ const state = await import('../lib/state.js')
12
+ const memory = await import('../lib/memory.js')
13
+ const inject = await import('../lib/inject.js')
14
+
15
+ test.after(() => {
16
+ try { state.flushState() } catch {}
17
+ try { fs.rmSync(home, { recursive: true, force: true }) } catch {}
18
+ })
19
+
20
+ const local = (y, m, d) => new Date(y, m - 1, d, 12, 0, 0)
21
+
22
+ // ---------------------------------------------------------------- A: no library
23
+ // The anniversary block used to throw on a binding without a library, and because
24
+ // it sits inside the pre-step it took ordinary recall down with it: the session
25
+ // recorded "injection failed: invalid library id" and remembered nothing.
26
+
27
+ test('an anniversary block on a binding with no library is empty, not fatal', () => {
28
+ assert.doesNotThrow(() => inject.buildAnniversary('chain-none', { useLibrary: true, libraryId: null }))
29
+ const empty = inject.buildAnniversary('chain-none', { useLibrary: true, libraryId: null })
30
+ assert.equal(empty.text, '')
31
+ assert.deepEqual(empty.ids, [])
32
+ assert.equal(inject.buildAnniversary('chain-none', null).text, '')
33
+ })
34
+
35
+ test('a step with no library still injects and records no recall error', async () => {
36
+ const cwd = path.join(home, 'project-no-library')
37
+ const binding = state.ensureSession('chain-no-lib', { cwd, agentPreset: 'coding', createLibrary: false })
38
+ assert.equal(binding.libraryId, null, 'createLibrary:false must not leave a library behind')
39
+
40
+ const user = {
41
+ id: 'u1',
42
+ role: 'user',
43
+ source: { kind: 'user' },
44
+ content: [{ type: 'text', text: 'What is the plan for today?' }],
45
+ }
46
+ const decision = { kind: 'enter', messages: [user] }
47
+ const agent = { session: { header: { id: 'chain-no-lib', cwd, agentPreset: 'coding', origin: 'user' } } }
48
+ const out = await inject.injectForStep({ agent, decision, turn: 1, step: 1, signal: { aborted: false } })
49
+ assert.ok(Array.isArray(out.messages))
50
+ assert.equal(state.getSessionBinding('chain-no-lib').lastRecallError, undefined,
51
+ 'a missing library is not a recall failure')
52
+ })
53
+
54
+ // ------------------------------------------------------- B: folder library reuse
55
+ // A folder that already owns a library must hand it to every new conversation.
56
+ // Only creating one needs `createLibrary`; re-using one never did.
57
+
58
+ test('a new conversation adopts the library its folder already has', () => {
59
+ const cwd = path.join(home, 'project-adopt')
60
+ const first = state.ensureSession('chain-first', { cwd, agentPreset: 'coding' })
61
+ assert.ok(first.libraryId, 'the first conversation creates the folder library')
62
+ const count = Object.keys(state.loadState().libraries).length
63
+
64
+ const second = state.ensureSession('chain-second', { cwd, agentPreset: 'coding', createLibrary: false })
65
+ assert.equal(second.libraryId, first.libraryId, 'createLibrary:false still reads the folder library')
66
+ assert.equal(Object.keys(state.loadState().libraries).length, count, 'adoption must not create a second library')
67
+ })
68
+
69
+ test('adoption is scoped to the folder, not to the caller', () => {
70
+ const withLibrary = path.join(home, 'project-scoped-a')
71
+ const withoutLibrary = path.join(home, 'project-scoped-b')
72
+ const owned = state.ensureSession('chain-scoped-a', { cwd: withLibrary })
73
+ const empty = state.ensureSession('chain-scoped-b', { cwd: withoutLibrary, createLibrary: false })
74
+ assert.equal(empty.libraryId, null, 'a folder with no library stays unbound')
75
+ assert.notEqual(empty.libraryId, owned.libraryId)
76
+ })
77
+
78
+ test('an archived library is not silently revived by a new conversation', () => {
79
+ const cwd = path.join(home, 'project-archived')
80
+ const owned = state.ensureSession('chain-archived-a', { cwd })
81
+ state.archiveLibrary(owned.libraryId)
82
+ const later = state.ensureSession('chain-archived-b', { cwd, createLibrary: false })
83
+ assert.equal(later.libraryId, null, 'reviving a library is a deliberate act, not a side effect')
84
+ })
85
+
86
+ // --------------------------------------------- C: 明天 is one day, not every year
87
+ // The extractor resolved 明天 to 10-29 and handed back MM-DD, so the appointment
88
+ // became an anniversary that resurfaced every 29 October.
89
+
90
+ test('a relative day with a matching month-day becomes one specific date', () => {
91
+ assert.equal(memory.reconcileSalientDate('10-29', '明天要去考驾照', local(2026, 10, 28)), '2026-10-29')
92
+ assert.equal(memory.reconcileSalientDate('11-01', '后天加班', local(2026, 10, 30)), '2026-11-01')
93
+ assert.equal(memory.reconcileSalientDate('10-30', 'today the exam happens', local(2026, 10, 30)), '2026-10-30')
94
+ assert.equal(memory.reconcileSalientDate('01-01', 'tomorrow is the new year', local(2026, 12, 31)), '2027-01-01')
95
+ })
96
+
97
+ test('an explicit date in the text settles the year without guessing', () => {
98
+ assert.equal(memory.reconcileSalientDate('10-29', '明天(2026-10-29)要去考驾照', local(2020, 1, 1)), '2026-10-29')
99
+ })
100
+
101
+ test('a real anniversary is left alone', () => {
102
+ assert.equal(memory.reconcileSalientDate('10-28', '我的生日是10月28日', local(2026, 10, 29)), '10-28')
103
+ assert.equal(memory.reconcileSalientDate('10-28', '他的生日', local(2026, 10, 29)), '10-28')
104
+ // A relative word whose month-day does not match it came from somewhere else.
105
+ assert.equal(memory.reconcileSalientDate('10-28', '明天要去考驾照', local(2026, 10, 29)), '10-28')
106
+ assert.equal(memory.reconcileSalientDate('2026-10-29', '明天要去考驾照', local(2026, 10, 28)), '2026-10-29')
107
+ assert.equal(memory.reconcileSalientDate(null, '明天要去考驾照', local(2026, 10, 28)), null)
108
+ })
109
+
110
+ test('the write path stores the dated form, so injection announces a due date', async () => {
111
+ const cwd = path.join(home, 'project-write')
112
+ const binding = state.ensureSession('chain-write', { cwd, agentPreset: 'coding' })
113
+ const result = await memory.privateRemember({
114
+ exec: { agent: { session: { header: { id: 'chain-write', cwd } } } },
115
+ libraryId: binding.libraryId,
116
+ title: '考驾照',
117
+ content: '明天(2099-10-29)要去考驾照',
118
+ layer: 'long_term',
119
+ source: 'user_said',
120
+ salientDate: '10-29',
121
+ })
122
+ const index = state.loadPrivateIndex(binding.libraryId)
123
+ const entries = Object.values(index.entries)
124
+ const entry = result?.id ? index.entries[result.id] : entries[entries.length - 1]
125
+ assert.ok(entry, 'the memory was written')
126
+ assert.equal(entry.salientDate, '2099-10-29', 'a resolved appointment is dated, not annual')
127
+ assert.equal(memory.isDatedSalientDate(entry.salientDate), true)
128
+ assert.equal(memory.salientDateFires(entry.salientDate, local(2099, 10, 28)), true)
129
+ assert.equal(memory.salientDateFires(entry.salientDate, local(2100, 10, 29)), false, 'and it does not recur')
130
+ })
131
+
132
+ // ------------------------------------------------- C on the other write paths
133
+ // The write side settles a relative day, but three other paths can file a date:
134
+ // re-dating an existing entry, importing entries (consolidation included), and the
135
+ // consolidation prompts themselves never describing the field at all.
136
+
137
+ test('re-dating an entry settles the year the same way a fresh write does', async () => {
138
+ const cwd = path.join(home, 'project-update')
139
+ const binding = state.ensureSession('chain-update', { cwd, agentPreset: 'coding' })
140
+ const exec = { agent: { session: { header: { id: 'chain-update', cwd } } } }
141
+ const written = await memory.privateRemember({
142
+ exec,
143
+ libraryId: binding.libraryId,
144
+ title: '考驾照',
145
+ content: '明天(2098-10-29)要去考驾照',
146
+ layer: 'long_term',
147
+ source: 'user_said',
148
+ })
149
+ const redated = await memory.privateUpdate({ exec, libraryId: binding.libraryId, id: written.id, salientDate: '10-29' })
150
+ assert.equal(redated.entry.salientDate, '2098-10-29', 'a repair must not re-file the year-less form')
151
+
152
+ const birthday = await memory.privateRemember({
153
+ exec,
154
+ libraryId: binding.libraryId,
155
+ title: '生日',
156
+ content: '她的生日是10月28日',
157
+ layer: 'long_term',
158
+ source: 'user_said',
159
+ })
160
+ const kept = await memory.privateUpdate({ exec, libraryId: binding.libraryId, id: birthday.id, salientDate: '10-28' })
161
+ assert.equal(kept.entry.salientDate, '10-28', 'a real anniversary keeps its annual form when re-dated')
162
+ })
163
+
164
+ test('an imported entry is settled on the way in', async () => {
165
+ const cwd = path.join(home, 'project-import')
166
+ const binding = state.ensureSession('chain-import', { cwd, agentPreset: 'coding' })
167
+ await memory.importMemories({
168
+ libraryId: binding.libraryId,
169
+ memories: [{ title: '考驾照', content: '明天(2098-10-29)要去考驾照', salientDate: '10-29', layer: 'long_term' }],
170
+ })
171
+ const entries = Object.values(state.loadPrivateIndex(binding.libraryId).entries)
172
+ const entry = entries.find((e) => (e.title || '').includes('考驾照'))
173
+ assert.ok(entry, 'the imported memory exists')
174
+ assert.equal(entry.salientDate, '2098-10-29', 'import must not carry an unsettled MM-DD in')
175
+ })
176
+
177
+ test('every consolidation prompt tells the model how to write the field', () => {
178
+ const dir = fileURLToPath(new URL('../lib/prompts/', import.meta.url))
179
+ for (const name of ['private-consolidation.md', 'private-consolidation.rp.md', 'private-consolidation.coding.md', 'private-consolidation.mixed.md']) {
180
+ const body = fs.readFileSync(path.join(dir, name), 'utf8')
181
+ assert.ok(body.includes('salient_date'), name + ' must describe salient_date')
182
+ assert.ok(body.includes('MM-DD') && body.includes('YYYY-MM-DD'), name + ' must state both forms')
183
+ assert.ok(/明天/.test(body), name + ' must say a relative day has to be resolved')
184
+ }
185
+ })
186
+
187
+ test('the remember tool tells the model to resolve a relative day', () => {
188
+ const source = fs.readFileSync(fileURLToPath(new URL('../lib/tools.js', import.meta.url)), 'utf8')
189
+ assert.ok(source.includes('A relative day'), 'the tool description must mention relative days')
190
+ assert.ok(source.includes('turns one appointment into a yearly reminder'), 'and why it matters')
191
+ })
@@ -0,0 +1,3 @@
1
+ import { register } from 'node:module'
2
+
3
+ register(new URL('./loader.mjs', import.meta.url))
@@ -0,0 +1,12 @@
1
+ const stubs = new URL('./stubs/', import.meta.url)
2
+ const map = {
3
+ '@deepseek-ai/dsh-tools': new URL('dsh-tools.js', stubs),
4
+ '@deepseek-ai/dsh-llm': new URL('dsh-llm.js', stubs),
5
+ '@deepseek-ai/dsh-session': new URL('dsh-session.js', stubs),
6
+ }
7
+
8
+ export async function resolve(specifier, context, next) {
9
+ const hit = map[specifier]
10
+ if (hit) return { url: hit.href, shortCircuit: true }
11
+ return next(specifier, context)
12
+ }
@@ -0,0 +1,95 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { execFile } from 'node:child_process'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { ENTRY_ENV, parseEmbeddingEntryConfig, runEmbeddingEntry } from '../lib/local-embedding-entry.js'
9
+
10
+ const ENTRY = fileURLToPath(new URL('../lib/local-embedding-entry.js', import.meta.url))
11
+ const TOKEN = 'entry-worker-token-0123456789abcdef'
12
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-memory-entry-'))
13
+ test.after(() => fs.rmSync(workDir, { recursive: true, force: true }))
14
+
15
+ const manifest = { id: 'bge-small-zh-v1.5', revision: 'r1', protocol: 'mean-l2-512', verified: true, dimension: 512 }
16
+ const manifestFile = path.join(workDir, 'manifest.json')
17
+ fs.writeFileSync(manifestFile, JSON.stringify(manifest))
18
+
19
+ const env = (overrides = {}) => ({
20
+ [ENTRY_ENV.modelDir]: workDir,
21
+ [ENTRY_ENV.manifest]: manifestFile,
22
+ [ENTRY_ENV.token]: TOKEN,
23
+ ...overrides,
24
+ })
25
+
26
+ const runChild = (extraEnv) => new Promise((resolve) => {
27
+ execFile(process.execPath, [ENTRY], { env: { ...process.env, ...extraEnv }, timeout: 30_000 }, (error, stdout, stderr) => {
28
+ resolve({ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, stdout, stderr })
29
+ })
30
+ })
31
+
32
+ test('entry config is read from the process environment, never from argv', () => {
33
+ const config = parseEmbeddingEntryConfig(env())
34
+ assert.equal(config.modelDir, workDir)
35
+ assert.deepEqual(config.manifest, manifest)
36
+ assert.equal(config.token, TOKEN)
37
+ assert.equal(config.host, '127.0.0.1')
38
+ assert.equal(config.port, 0)
39
+
40
+ assert.deepEqual(parseEmbeddingEntryConfig(env({ [ENTRY_ENV.manifest]: JSON.stringify(manifest) })).manifest, manifest)
41
+ assert.equal(parseEmbeddingEntryConfig(env({ [ENTRY_ENV.port]: '45999', [ENTRY_ENV.maxBatch]: '8' })).port, 45999)
42
+ assert.throws(() => parseEmbeddingEntryConfig(env({ [ENTRY_ENV.token]: '' })), (error) => error.code === 'LOCAL_VECTOR_TOKEN_INVALID')
43
+ assert.throws(() => parseEmbeddingEntryConfig(env({ [ENTRY_ENV.modelDir]: '' })), (error) => error.code === 'LOCAL_VECTOR_MODEL_DIR_MISSING')
44
+ assert.throws(() => parseEmbeddingEntryConfig(env({ [ENTRY_ENV.manifest]: path.join(workDir, 'missing.json') })), (error) => error.code === 'LOCAL_VECTOR_MANIFEST_INVALID')
45
+ assert.throws(() => parseEmbeddingEntryConfig(env({ [ENTRY_ENV.manifest]: '{ broken' })), (error) => error.code === 'LOCAL_VECTOR_MANIFEST_INVALID')
46
+ assert.throws(() => parseEmbeddingEntryConfig(env({ [ENTRY_ENV.port]: 'abc' })), (error) => error.code === 'LOCAL_VECTOR_BAD_CONFIG')
47
+ })
48
+
49
+ test('a ready line carries the port and model but never the token', async () => {
50
+ const seen = []
51
+ const stdout = { write: (chunk) => { seen.push(String(chunk)) } }
52
+ const result = await runEmbeddingEntry({
53
+ env: env(),
54
+ stdout,
55
+ startWorker: async (options) => {
56
+ seen.push('options:' + JSON.stringify(Object.keys(options).sort()))
57
+ return { baseUrl: 'http://127.0.0.1:41000/v1', url: 'http://127.0.0.1:41000/v1/embeddings', model: 'bge-small-zh-v1.5@r1+mean-l2-512', dimension: 512, close: async () => {}, lastError: null }
58
+ },
59
+ })
60
+ const readyLine = seen.filter((line) => line.trim().startsWith('{')).join('').trim()
61
+ const ready = JSON.parse(readyLine)
62
+ assert.equal(ready.type, 'ready')
63
+ assert.equal(ready.baseUrl, 'http://127.0.0.1:41000/v1')
64
+ assert.equal(ready.url, 'http://127.0.0.1:41000/v1/embeddings')
65
+ assert.equal(ready.model, 'bge-small-zh-v1.5@r1+mean-l2-512')
66
+ assert.equal(ready.dimension, 512)
67
+ assert.ok(!readyLine.includes(TOKEN))
68
+ assert.equal(result.ready.type, 'ready')
69
+ assert.ok(seen.some((line) => line.startsWith('options:') && line.includes('token')))
70
+ })
71
+
72
+ test('the entry refuses a missing model directory before it opens a port', async () => {
73
+ const child = await runChild({ ...env(), [ENTRY_ENV.modelDir]: '' })
74
+ assert.equal(child.code, 1)
75
+ assert.equal(child.stdout.trim(), '')
76
+ const error = JSON.parse(child.stderr.trim().split('\n').filter((line) => line.trim().startsWith('{')).pop())
77
+ assert.equal(error.type, 'error')
78
+ assert.equal(error.code, 'LOCAL_VECTOR_MODEL_DIR_MISSING')
79
+ assert.ok(!child.stderr.includes(TOKEN))
80
+ })
81
+
82
+ test('the entry refuses a pending manifest and a missing runtime without downloading anything', async () => {
83
+ const pendingFile = path.join(workDir, 'pending.json')
84
+ fs.writeFileSync(pendingFile, JSON.stringify({ ...manifest, verified: false, status: 'pending' }))
85
+ const pending = await runChild({ ...env(), [ENTRY_ENV.manifest]: pendingFile })
86
+ assert.equal(pending.code, 1)
87
+ assert.equal(JSON.parse(pending.stderr.trim().split('\n').filter((line) => line.trim().startsWith('{')).pop()).code, 'LOCAL_VECTOR_MANIFEST_UNVERIFIED')
88
+
89
+ fs.writeFileSync(path.join(workDir, 'model.onnx'), 'not a graph')
90
+ const runtime = await runChild(env())
91
+ assert.equal(runtime.code, 1)
92
+ assert.equal(runtime.stdout.trim(), '')
93
+ const error = JSON.parse(runtime.stderr.trim().split('\n').filter((line) => line.trim().startsWith('{')).pop())
94
+ assert.ok(['LOCAL_VECTOR_RUNTIME_UNAVAILABLE', 'LOCAL_VECTOR_MODEL_FILE_MISSING', 'LOCAL_VECTOR_SMOKE_FAILED'].includes(error.code))
95
+ })