dsh-memu-memory 1.1.2 → 1.1.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/AGENTS.md +8 -6
- package/README.md +21 -5
- package/README.zh.md +21 -5
- package/docs/contributing.md +5 -3
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +32 -5
- package/docs/troubleshooting.md +36 -7
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +630 -0
- package/lib/client.js +697 -49
- package/lib/curator.js +81 -37
- package/lib/cycle.js +3 -0
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +36 -0
- package/lib/inject.js +131 -30
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +249 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +385 -45
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/paths.js +24 -0
- package/lib/private.js +139 -13
- package/lib/rpc.js +45 -4
- package/lib/state.js +109 -161
- package/lib/tools.js +9 -5
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +941 -0
- package/package.json +3 -2
- package/scripts/acceptance-local.mjs +5 -4
- package/scripts/chain-matrix.mjs +527 -0
- package/scripts/check-all.mjs +19 -0
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- package/scripts/repetition-eval.mjs +702 -0
package/lib/state.js
CHANGED
|
@@ -5,12 +5,20 @@ import {
|
|
|
5
5
|
dataDir, librariesDir, indexDir, backupsDir,
|
|
6
6
|
ensureDir, readJson, readFailure, readFailuresUnder, writeJson, withFileLock, assertJsonShape, warn, stamp, shortHash, libraryIdForScope, isSafeId,
|
|
7
7
|
} from './paths.js'
|
|
8
|
+
import {
|
|
9
|
+
activeLeases, createBackup, deleteBackupEntry, listBackupEntries,
|
|
10
|
+
pruneBackups, restoreFromBackup, withBackupLock,
|
|
11
|
+
} from './backup.js'
|
|
8
12
|
import { migrateIndexVisibility } from './visibility.js'
|
|
13
|
+
import { memuDbFor, memuGlobalDbFor } from './vector-profile.js'
|
|
9
14
|
|
|
10
15
|
const STATE_FILE = () => path.join(dataDir(), 'state.json')
|
|
11
16
|
const GLOBAL_DB = () => path.join(dataDir(), 'global.sqlite3')
|
|
12
17
|
const GLOBAL_INDEX = () => path.join(indexDir(), 'global.json')
|
|
13
18
|
|
|
19
|
+
// Observable outcome of the last backup action; see listBackupEntries().
|
|
20
|
+
let lastBackupReport = null
|
|
21
|
+
|
|
14
22
|
export const DEFAULT_SETTINGS = {
|
|
15
23
|
sidebarSettings: false,
|
|
16
24
|
visionCaptionEnabled: true,
|
|
@@ -24,12 +32,17 @@ export const DEFAULT_SETTINGS = {
|
|
|
24
32
|
// "too little memory", it is noise: a per-turn block sized for paragraph-long
|
|
25
33
|
// prompts drowns the conversation it was meant to help. So the per-turn budgets
|
|
26
34
|
// are small, and the windows that stop repetition are measured in many turns.
|
|
27
|
-
privateTopK:
|
|
28
|
-
globalTopK:
|
|
29
|
-
anchorTokens:
|
|
30
|
-
//
|
|
31
|
-
// so the injected block has to stay clearly smaller than the message it
|
|
32
|
-
|
|
35
|
+
privateTopK: 5,
|
|
36
|
+
globalTopK: 5,
|
|
37
|
+
anchorTokens: 800,
|
|
38
|
+
// 800 tokens for one recall block. A dense chat calls recall on almost every
|
|
39
|
+
// message, so the injected block has to stay clearly smaller than the message it
|
|
40
|
+
// serves. This is the recall budget, not the whole plugin-message ceiling.
|
|
41
|
+
turnTokens: 800,
|
|
42
|
+
// Whole plugin-message ceiling: anchor + recall + optional cue/texture. It must
|
|
43
|
+
// be at least the sum of the sub-budgets the user wants to allow; the default leaves
|
|
44
|
+
// room for the default anchor and recall blocks.
|
|
45
|
+
injectionTotalTokens: 2000,
|
|
33
46
|
relevanceThreshold: 0.4,
|
|
34
47
|
// 16 turns is a couple of minutes of group traffic. The old 8 expired inside a
|
|
35
48
|
// single burst, so the same entry came back every minute or two.
|
|
@@ -90,6 +103,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
90
103
|
// agent writes and summarizes into the JSON index, with no MemU engine and no
|
|
91
104
|
// embedding API. 'api' hands the work to the configured remote MemU service.
|
|
92
105
|
memoryBackend: 'agent',
|
|
106
|
+
vectorSource: 'local',
|
|
93
107
|
habitsInjection: true,
|
|
94
108
|
// A positive age marks cleanup candidates; deletion always needs a user action.
|
|
95
109
|
purgeArchivedAfterDays: 0,
|
|
@@ -110,10 +124,14 @@ function defaultState() {
|
|
|
110
124
|
function mergeState(raw) {
|
|
111
125
|
const base = defaultState()
|
|
112
126
|
if (!raw || typeof raw !== 'object') return base
|
|
127
|
+
const rawSettings = raw.settings && typeof raw.settings === 'object' ? raw.settings : {}
|
|
128
|
+
const legacyVectorSource = rawSettings.vectorSource === 'api' || rawSettings.vectorSource === 'local'
|
|
129
|
+
? rawSettings.vectorSource
|
|
130
|
+
: (rawSettings.memoryMode === 'cloud' ? 'api' : 'local')
|
|
113
131
|
return {
|
|
114
132
|
...base,
|
|
115
133
|
...raw,
|
|
116
|
-
settings: { ...base.settings, ...
|
|
134
|
+
settings: { ...base.settings, ...rawSettings, vectorSource: legacyVectorSource },
|
|
117
135
|
libraries: raw.libraries && typeof raw.libraries === 'object' ? raw.libraries : {},
|
|
118
136
|
sessions: raw.sessions && typeof raw.sessions === 'object' ? raw.sessions : {},
|
|
119
137
|
workspacePolicies: raw.workspacePolicies && typeof raw.workspacePolicies === 'object' ? raw.workspacePolicies : {},
|
|
@@ -306,16 +324,27 @@ export function getMemoryBackend() {
|
|
|
306
324
|
return loadState().settings?.memoryBackend === 'api' ? 'api' : 'agent'
|
|
307
325
|
}
|
|
308
326
|
|
|
327
|
+
// Single route authority for automatic memory reads and writes.
|
|
328
|
+
export function effectiveVectorRoute(settings = loadState().settings) {
|
|
329
|
+
if (settings?.memoryBackend !== 'api') return 'lexical'
|
|
330
|
+
return settings?.vectorSource === 'api' ? 'remote-api' : 'managed-local'
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function vectorBackendEnabled(settings = loadState().settings) {
|
|
334
|
+
return effectiveVectorRoute(settings) !== 'lexical'
|
|
335
|
+
}
|
|
336
|
+
|
|
309
337
|
export function updateSettings(patch) {
|
|
310
338
|
const clean = {}
|
|
311
339
|
const ranges = {
|
|
312
340
|
privateTopK: [1, 20], globalTopK: [1, 20], anchorTokens: [160, 8000], turnTokens: [80, 8000],
|
|
341
|
+
injectionTotalTokens: [160, 16000],
|
|
313
342
|
relevanceThreshold: [0, 1], dedupeTurns: [0, 200], globalEveryNTurns: [0, 200], minUserChars: [0, 1000],
|
|
314
343
|
mustRememberCap: [1, 100], randomCueChance: [0, 1], shortTermTtlHours: [0, 8760],
|
|
315
344
|
unpinBudgetPerSession: [1, 20], unpinCooldownMinutes: [0, 1440], sleepAfterIdleMinutes: [1, 1440],
|
|
316
345
|
autoCurateEveryHours: [0, 8760], autoCurateSessionLimit: [1, 10], purgeArchivedAfterDays: [0, 36500],
|
|
317
346
|
}
|
|
318
|
-
const enums = { autoInject: ['smart', 'anchor-only', 'per-turn', 'off'], memoryMode: ['local', 'cloud'], memoryBackend: ['agent', 'api'], newSessionDefault: ['off', 'auto', 'global', 'both'] }
|
|
347
|
+
const enums = { autoInject: ['smart', 'anchor-only', 'per-turn', 'off'], memoryMode: ['local', 'cloud'], memoryBackend: ['agent', 'api'], vectorSource: ['local', 'api'], newSessionDefault: ['off', 'auto', 'global', 'both'] }
|
|
319
348
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
320
349
|
if (!Object.hasOwn(DEFAULT_SETTINGS, key)) continue
|
|
321
350
|
if (enums[key]) {
|
|
@@ -906,12 +935,12 @@ export function markAnniversaryShown(sessionId, memoryId, dayKey) {
|
|
|
906
935
|
|
|
907
936
|
export function memoryDbPath(libraryId) {
|
|
908
937
|
ensureDir(librariesDir())
|
|
909
|
-
return path.join(librariesDir(), `${libraryId}.sqlite3`)
|
|
938
|
+
return effectiveVectorRoute() === 'managed-local' ? memuDbFor(libraryId) : path.join(librariesDir(), `${libraryId}.sqlite3`)
|
|
910
939
|
}
|
|
911
940
|
|
|
912
941
|
export function globalDbPath() {
|
|
913
942
|
ensureDir(dataDir())
|
|
914
|
-
return GLOBAL_DB()
|
|
943
|
+
return effectiveVectorRoute() === 'managed-local' ? memuGlobalDbFor() : GLOBAL_DB()
|
|
915
944
|
}
|
|
916
945
|
|
|
917
946
|
// Index files are parsed on every folder in the sidebar, and the parse (full
|
|
@@ -1144,175 +1173,94 @@ export function newMemoryId(prefix = 'mem') {
|
|
|
1144
1173
|
return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`
|
|
1145
1174
|
}
|
|
1146
1175
|
|
|
1147
|
-
|
|
1176
|
+
/**
|
|
1177
|
+
* Creates a managed backup of the local memory stores (plugin state, indexes,
|
|
1178
|
+
* prompts, attachments, the vector journal and every MemU database), then keeps
|
|
1179
|
+
* the newest BACKUP_KEEP complete ones. Retention, staging, checksums, atomic
|
|
1180
|
+
* publish and the shared lock all live in lib/backup.js so there is exactly one
|
|
1181
|
+
* place that decides what a backup is and which backup may be deleted.
|
|
1182
|
+
*
|
|
1183
|
+
* Returns the published directory: callers (engine switch, library delete,
|
|
1184
|
+
* scripts) treat the return value as a path.
|
|
1185
|
+
*/
|
|
1186
|
+
export function backupDatabases(options = {}) {
|
|
1148
1187
|
// Pending bookkeeping changes live in memory for up to 50ms; a backup taken
|
|
1149
1188
|
// before a flush would silently capture the previous state.
|
|
1150
|
-
|
|
1151
|
-
const dest = path.join(backupsDir(), stamp().replace(/[:.]/g, '-'))
|
|
1152
|
-
ensureDir(dest)
|
|
1153
|
-
ensureDir(path.join(dest, 'libraries'))
|
|
1154
|
-
ensureDir(path.join(dest, 'index'))
|
|
1155
|
-
// The prompt manifest copy below silently no-opped without this directory —
|
|
1156
|
-
// an empty catch swallowed the ENOENT and backups shipped without it.
|
|
1157
|
-
ensureDir(path.join(dest, 'prompts'))
|
|
1158
|
-
const copy = (from, to) => {
|
|
1159
|
-
let info
|
|
1160
|
-
try { info = fs.lstatSync(from) } catch (error) { if (error.code === 'ENOENT') return; throw error }
|
|
1161
|
-
if (!info.isFile()) throw new Error(`backup refuses non-file: ${path.basename(from)}`)
|
|
1162
|
-
fs.copyFileSync(from, to)
|
|
1163
|
-
}
|
|
1164
|
-
copy(STATE_FILE(), path.join(dest, 'state.json'))
|
|
1165
|
-
copy(GLOBAL_DB(), path.join(dest, 'global.sqlite3'))
|
|
1166
|
-
copy(GLOBAL_INDEX(), path.join(dest, 'index', 'global.json'))
|
|
1167
|
-
copy(path.join(dataDir(), 'prompts', 'manifest.json'), path.join(dest, 'prompts', 'manifest.json'))
|
|
1189
|
+
let report
|
|
1168
1190
|
try {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
copyTree(path.join(dataDir(), 'attachments'), path.join(dest, 'attachments'))
|
|
1191
|
-
copyTree(path.join(dataDir(), 'prompts'), path.join(dest, 'prompts'))
|
|
1192
|
-
|
|
1193
|
-
// A backup whose integrity nobody can check is only a hope. Hash every file so
|
|
1194
|
-
// a restore can refuse a truncated or half-written copy.
|
|
1195
|
-
const files = {}
|
|
1196
|
-
const walk = (dir, prefix = '') => {
|
|
1197
|
-
let names = []
|
|
1198
|
-
try { names = fs.readdirSync(dir) } catch { return }
|
|
1199
|
-
for (const name of names) {
|
|
1200
|
-
const full = path.join(dir, name)
|
|
1201
|
-
let stat = null
|
|
1202
|
-
try { stat = fs.statSync(full) } catch { continue }
|
|
1203
|
-
if (stat.isDirectory()) { walk(full, `${prefix}${name}/`); continue }
|
|
1204
|
-
let sha256 = null
|
|
1205
|
-
try { sha256 = crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex') } catch {}
|
|
1206
|
-
files[`${prefix}${name}`] = { bytes: stat.size, sha256 }
|
|
1191
|
+
report = createBackup({ ...options, flushState })
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
// A refusal is the safety property working, not something to swallow: a
|
|
1194
|
+
// database the engine could not snapshot aborts the backup, and the reason
|
|
1195
|
+
// has to stay visible on the same channel the panel already reads.
|
|
1196
|
+
lastBackupReport = {
|
|
1197
|
+
at: stamp(),
|
|
1198
|
+
action: 'create',
|
|
1199
|
+
status: 'failed',
|
|
1200
|
+
code: error?.code || null,
|
|
1201
|
+
error: String(error?.message || error),
|
|
1202
|
+
name: null,
|
|
1203
|
+
dir: null,
|
|
1204
|
+
files: 0,
|
|
1205
|
+
bytes: 0,
|
|
1206
|
+
snapshots: [],
|
|
1207
|
+
pruned: [],
|
|
1208
|
+
pruneFailed: [],
|
|
1209
|
+
pendingCleanup: 0,
|
|
1210
|
+
notice: null,
|
|
1207
1211
|
}
|
|
1212
|
+
throw error
|
|
1208
1213
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
writeJson(path.join(dest, 'manifest.json'), {
|
|
1212
|
-
version: 1,
|
|
1213
|
-
createdAt: stamp(),
|
|
1214
|
-
engine: current ? { version: current.version, schemaVersion: current.schemaVersion || null } : null,
|
|
1215
|
-
files,
|
|
1216
|
-
})
|
|
1217
|
-
return dest
|
|
1214
|
+
lastBackupReport = { at: stamp(), action: 'create', ...report }
|
|
1215
|
+
return report.dir
|
|
1218
1216
|
}
|
|
1219
1217
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
} catch {
|
|
1224
|
-
return null
|
|
1225
|
-
}
|
|
1218
|
+
/** What the last backup/restore/prune did, so the outcome stays observable. */
|
|
1219
|
+
export function lastBackupReportView() {
|
|
1220
|
+
return lastBackupReport ? { ...lastBackupReport } : null
|
|
1226
1221
|
}
|
|
1227
1222
|
|
|
1223
|
+
/** Newest first, including entries that failed to complete (reported, never deleted). */
|
|
1228
1224
|
export function listBackups() {
|
|
1229
|
-
|
|
1230
|
-
try { names = fs.readdirSync(backupsDir()) } catch { return [] }
|
|
1231
|
-
return names
|
|
1232
|
-
.map((name) => {
|
|
1233
|
-
const dir = path.join(backupsDir(), name)
|
|
1234
|
-
let stat = null
|
|
1235
|
-
try { stat = fs.statSync(dir) } catch { return null }
|
|
1236
|
-
if (!stat.isDirectory()) return null
|
|
1237
|
-
const manifest = readJson(path.join(dir, 'manifest.json'), null)
|
|
1238
|
-
const fileCount = manifest?.files ? Object.keys(manifest.files).length : null
|
|
1239
|
-
const bytes = manifest?.files
|
|
1240
|
-
? Object.values(manifest.files).reduce((sum, f) => sum + Number(f.bytes || 0), 0)
|
|
1241
|
-
: null
|
|
1242
|
-
return {
|
|
1243
|
-
name,
|
|
1244
|
-
dir,
|
|
1245
|
-
createdAt: manifest?.createdAt || stat.mtime.toISOString(),
|
|
1246
|
-
engine: manifest?.engine || null,
|
|
1247
|
-
files: fileCount,
|
|
1248
|
-
bytes,
|
|
1249
|
-
hasManifest: Boolean(manifest),
|
|
1250
|
-
}
|
|
1251
|
-
})
|
|
1252
|
-
.filter(Boolean)
|
|
1253
|
-
.sort((a, b) => String(b.name).localeCompare(String(a.name)))
|
|
1225
|
+
return listBackupEntries()
|
|
1254
1226
|
}
|
|
1255
1227
|
|
|
1256
1228
|
/** Removes one backup directory. The name is validated, never used as a path. */
|
|
1257
1229
|
export function deleteBackup(name) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
fs.rmSync(dir, { recursive: true, force: true })
|
|
1262
|
-
return { deleted: name }
|
|
1230
|
+
const result = deleteBackupEntry(name)
|
|
1231
|
+
lastBackupReport = { at: stamp(), action: 'delete', ...result }
|
|
1232
|
+
return { deleted: result.deleted, status: result.status }
|
|
1263
1233
|
}
|
|
1264
1234
|
|
|
1265
1235
|
/**
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1236
|
+
* Applies the retention rule on demand. Creation already prunes, so this exists
|
|
1237
|
+
* for the paths that must reclaim the space a protected restore target kept
|
|
1238
|
+
* alive, and for callers that want the report without making a backup.
|
|
1269
1239
|
*/
|
|
1270
|
-
export function
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
copy(path.join(dir, 'index', 'global.json'), GLOBAL_INDEX())
|
|
1295
|
-
copy(path.join(dir, 'prompts', 'manifest.json'), path.join(dataDir(), 'prompts', 'manifest.json'))
|
|
1296
|
-
for (const [sub, target] of [['index', indexDir()], ['libraries', librariesDir()]]) {
|
|
1297
|
-
let names = []
|
|
1298
|
-
try { names = fs.readdirSync(path.join(dir, sub)) } catch { continue }
|
|
1299
|
-
for (const file of names) copy(path.join(dir, sub, file), path.join(target, file))
|
|
1300
|
-
}
|
|
1301
|
-
const restoreTree = (from, target) => {
|
|
1302
|
-
let items
|
|
1303
|
-
try { items = fs.readdirSync(from, { withFileTypes: true }) } catch (error) { if (error.code === 'ENOENT') return; throw error }
|
|
1304
|
-
for (const item of items) {
|
|
1305
|
-
if (item.isSymbolicLink()) throw new Error('restore refuses symbolic links')
|
|
1306
|
-
if (item.isDirectory()) restoreTree(path.join(from, item.name), path.join(target, item.name))
|
|
1307
|
-
else copy(path.join(from, item.name), path.join(target, item.name))
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
restoreTree(path.join(dir, 'attachments'), path.join(dataDir(), 'attachments'))
|
|
1311
|
-
restoreTree(path.join(dir, 'prompts'), path.join(dataDir(), 'prompts'))
|
|
1312
|
-
// The restored files are the truth now; anything cached describes the state
|
|
1313
|
-
// that was just replaced.
|
|
1314
|
-
clearCaches()
|
|
1315
|
-
return { restored: name, safety }
|
|
1240
|
+
export function pruneOldBackups(options = {}) {
|
|
1241
|
+
const result = pruneBackups(options)
|
|
1242
|
+
lastBackupReport = { at: stamp(), action: 'prune', ...result }
|
|
1243
|
+
return result
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** Names of backups that a running restore or migration is reading right now. */
|
|
1247
|
+
export function leasedBackups() {
|
|
1248
|
+
return Object.keys(activeLeases())
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Puts a backup back. The target is leased and its whole inventory is verified
|
|
1253
|
+
* before anything is written; the live root is saved as a safety backup next;
|
|
1254
|
+
* only then are files replaced. Failure leaves a recovery path: the safety
|
|
1255
|
+
* backup holds the state that was replaced and retention may not delete it.
|
|
1256
|
+
* A backup that predates the vector journal also retires the stale live
|
|
1257
|
+
* vector-active pointer, so recall falls back to text instead of querying
|
|
1258
|
+
* databases that no longer match the restored index.
|
|
1259
|
+
*/
|
|
1260
|
+
export function restoreBackup(name, { beforeRestore = null, keep = undefined } = {}) {
|
|
1261
|
+
const result = restoreFromBackup(name, { beforeRestore, flushState, clearCaches, keep })
|
|
1262
|
+
lastBackupReport = { at: stamp(), action: 'restore', ...result }
|
|
1263
|
+
return result
|
|
1316
1264
|
}
|
|
1317
1265
|
|
|
1318
1266
|
/** Drops the in-memory mirrors so the next read comes from disk. */
|
package/lib/tools.js
CHANGED
|
@@ -190,7 +190,7 @@ export function registerTools(ctx) {
|
|
|
190
190
|
mood_valence: { type: 'number', description: 'How this moment felt, -1 (bad) to 1 (good). Omit to inherit the current conversation mood.' },
|
|
191
191
|
arousal: { type: 'number', description: 'How strongly it hit, 0-1.' },
|
|
192
192
|
peak: { type: 'boolean', description: 'A high point or low point of the arc — peaks and endings stay easier to recall.' },
|
|
193
|
-
salient_date: { type: 'string', description: 'MM-DD for something that comes back every year (a birthday, an anniversary of an event); it resurfaces on that day each year. YYYY-MM-DD for a one-off dated commitment or appointment: it is raised the day before and on the day itself, and never again after it has passed. Use the annual form only when the day really does recur. A relative day (今天/明天/后天/today/tomorrow) must be resolved to the absolute date it points at: sending a bare MM-DD turns one appointment into a yearly reminder.' },
|
|
193
|
+
salient_date: { type: 'string', description: 'MM-DD for something that comes back every year (a birthday, an anniversary of an event); it resurfaces on that day each year. YYYY-MM-DD for a one-off dated commitment or appointment: it is raised the day before and on the day itself, and never again after it has passed. Use the annual form only when the day really does recur. A relative day (今天/明天/后天/today/tomorrow) must be resolved to the absolute date it points at: sending a bare MM-DD turns one appointment into a yearly reminder. If the content itself names a date, pass it here too: when this field is empty the plugin only falls back to a single unambiguous YYYY-MM-DD found in the text.' },
|
|
194
194
|
track: { type: 'string', description: 'memory (default) or habit for an aggregated expression/sticker habit; do not write one habit per occurrence.' },
|
|
195
195
|
attachments: { type: 'array', items: { type: 'object', properties: { sha256: { type: 'string', required: true } }, additionalProperties: true }, description: 'At most 8 sha256 references received in this conversation. Never supply paths or bytes. Ordinary image memories default to short_term if layer is omitted; choose long_term explicitly only when important.' },
|
|
196
196
|
context: { type: 'object', properties: {
|
|
@@ -252,11 +252,14 @@ export function registerTools(ctx) {
|
|
|
252
252
|
},
|
|
253
253
|
async (args, exec) => {
|
|
254
254
|
const result = await privateRecall({ exec, query: args.query, topK: args.topK })
|
|
255
|
+
// Surface a degraded semantic leg on the explicit tool path too; silently
|
|
256
|
+
// returning fewer hits would make a failed backend look like an empty library.
|
|
257
|
+
const warning = (result.errors || []).length ? `\n注意:本次召回已降级:${(result.errors || []).join('; ')}` : ''
|
|
255
258
|
// Read-only: never materialize a binding/library from a recall header.
|
|
256
259
|
const binding = peekBinding(exec)
|
|
257
260
|
const header = `私有记忆(library=${binding?.libraryId || '?'}, domain=${domainOf(binding)})`
|
|
258
|
-
if (!result.files?.length) return `${header}\n
|
|
259
|
-
return [header, ...result.files.map((f, i) => formatPrivateItem(f, i + 1, { query: args.query }))].join('\n\n')
|
|
261
|
+
if (!result.files?.length) return `${header}\n没有命中。${warning}`
|
|
262
|
+
return [header, ...result.files.map((f, i) => formatPrivateItem(f, i + 1, { query: args.query }))].join('\n\n') + warning
|
|
260
263
|
},
|
|
261
264
|
{ isConcurrencySafe: () => true },
|
|
262
265
|
))
|
|
@@ -270,14 +273,15 @@ export function registerTools(ctx) {
|
|
|
270
273
|
// instead of ensureSession() silently creating a placeholder library.
|
|
271
274
|
if (!peekBinding(exec)?.libraryId) return '当前对话没有绑定记忆库。'
|
|
272
275
|
const result = await privateList({ exec })
|
|
273
|
-
|
|
276
|
+
const warning = result.warning ? `\n注意:${result.warning}` : ''
|
|
277
|
+
if (!result.files?.length) return `私有记忆库 ${result.libraryId} 为空。${warning}`
|
|
274
278
|
const lines = [`私有记忆库 ${result.libraryId}(${result.files.length} 条)`]
|
|
275
279
|
for (const file of result.files) {
|
|
276
280
|
const e = file.entry || {}
|
|
277
281
|
const tag = salientTag(e)
|
|
278
282
|
lines.push(`- [${file.name}] ${e.title || file.description || '(untitled)'} · ${e.layer || 'long_term'} · ${e.status || 'active'}${e.marginalized ? ' · marginalized' : ''}${tag ? ` · ${tag}` : ''}`)
|
|
279
283
|
}
|
|
280
|
-
return lines.join('\n')
|
|
284
|
+
return lines.join('\n') + warning
|
|
281
285
|
},
|
|
282
286
|
{ isConcurrencySafe: () => true },
|
|
283
287
|
))
|