@modusensus/dsh-mneme 0.7.0 → 0.7.2
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/README.md +415 -149
- package/{dsh-mneme/src → lib}/api.js +6 -3
- package/{dsh-mneme/lib → lib}/client.js +55 -8
- package/{dsh-mneme/src → lib}/config.js +5 -0
- package/{dsh-mneme/src → lib}/dream.js +7 -2
- package/{dsh-mneme/lib → lib}/index.js +7 -6
- package/{dsh-mneme/lib → lib}/inject.js +31 -2
- package/{dsh-mneme/src → lib}/service.js +37 -3
- package/{dsh-mneme/src → lib}/settings.js +15 -6
- package/{dsh-mneme/src → lib}/store.js +5 -0
- package/package.json +40 -18
- package/{dsh-mneme/lib → src}/api.js +6 -3
- package/{dsh-mneme/lib → src}/config.js +5 -0
- package/{dsh-mneme/lib → src}/dream.js +7 -2
- package/{dsh-mneme/src → src}/index.js +7 -6
- package/{dsh-mneme/src → src}/inject.js +31 -2
- package/{dsh-mneme/lib → src}/service.js +37 -3
- package/{dsh-mneme/lib → src}/settings.js +15 -6
- package/{dsh-mneme/lib → src}/store.js +5 -0
- package/{dsh-mneme/test → test}/api.test.js +10 -3
- package/{dsh-mneme/test → test}/client.test.js +37 -0
- package/{dsh-mneme/test → test}/config.test.js +7 -0
- package/{dsh-mneme/test → test}/inject.test.js +28 -0
- package/{dsh-mneme/test → test}/settings.test.js +6 -2
- package/{dsh-mneme/test → test}/tag.test.js +114 -0
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -89
- package/CONTRIBUTING.md +0 -245
- package/SECURITY.md +0 -544
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -286
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -482
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -80
- package/v0.7-plan-2026-08-21.md +0 -75
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/decisions.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/sleep.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/tag-extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/heat.js +0 -0
- /package/{dsh-mneme/lib → lib}/hot-memory.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/mirror.js +0 -0
- /package/{dsh-mneme/lib → lib}/parser/tag.js +0 -0
- /package/{dsh-mneme/lib → lib}/parser/wiki-link.js +0 -0
- /package/{dsh-mneme/lib → lib}/quality-filter.js +0 -0
- /package/{dsh-mneme/lib → lib}/reranker.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/tag-boost.js +0 -0
- /package/{dsh-mneme/lib → lib}/summarize.js +0 -0
- /package/{dsh-mneme/lib → lib}/tools.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/sync-lib.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/dream/decisions.js +0 -0
- /package/{dsh-mneme/src → src}/dream/sleep.js +0 -0
- /package/{dsh-mneme/src → src}/dream/tag-extractor.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/heat.js +0 -0
- /package/{dsh-mneme/src → src}/hot-memory.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/mirror.js +0 -0
- /package/{dsh-mneme/src → src}/parser/tag.js +0 -0
- /package/{dsh-mneme/src → src}/parser/wiki-link.js +0 -0
- /package/{dsh-mneme/src → src}/quality-filter.js +0 -0
- /package/{dsh-mneme/src → src}/reranker.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/search/tag-boost.js +0 -0
- /package/{dsh-mneme/src → src}/summarize.js +0 -0
- /package/{dsh-mneme/src → src}/tools.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/boundary-v0625.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/directory.test.js +0 -0
- /package/{dsh-mneme/test → test}/dream.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/graph-api.test.js +0 -0
- /package/{dsh-mneme/test → test}/heat.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/hot-memory.test.js +0 -0
- /package/{dsh-mneme/test → test}/llm-audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/normalize-decisions.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/provenance.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-layer.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-runs.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/reranker.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/service-search.test.js +0 -0
- /package/{dsh-mneme/test → test}/service.test.js +0 -0
- /package/{dsh-mneme/test → test}/sleep-heat.test.js +0 -0
- /package/{dsh-mneme/test → test}/sleep.test.js +0 -0
- /package/{dsh-mneme/test → test}/store.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/summarize.test.js +0 -0
- /package/{dsh-mneme/test → test}/tag-boost.test.js +0 -0
- /package/{dsh-mneme/test → test}/tools.test.js +0 -0
- /package/{dsh-mneme/test → test}/updated-at-semantics.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
- /package/{dsh-mneme/test → test}/wiki-link.test.js +0 -0
|
@@ -243,11 +243,14 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
243
243
|
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
settings.setAutoTagConfig(patch);
|
|
247
|
+
// Respond with the effective (settings-over-config merged) toggles
|
|
248
|
+
// so the panel always sees booleans that actually gate runtime
|
|
249
|
+
// behaviour (issue #31).
|
|
250
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
248
251
|
return;
|
|
249
252
|
}
|
|
250
|
-
sendJson(res, 200, { config:
|
|
253
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
251
254
|
} catch {
|
|
252
255
|
sendJson(res, 500, { error: "internal" });
|
|
253
256
|
}
|
|
@@ -122,6 +122,8 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
"memory.directory.empty": "暂无记忆条目",
|
|
123
123
|
"directory.editToggle": "编辑模式(显示删除)",
|
|
124
124
|
"directory.deleteConfirm": "确定删除这条记忆吗?",
|
|
125
|
+
"directory.deleteConfirmShort": "确认?",
|
|
126
|
+
"directory.deleteError": "删除失败",
|
|
125
127
|
"memory.card.open": "在主区记忆库中查看全文",
|
|
126
128
|
"memory.time.now": "刚刚",
|
|
127
129
|
"memory.time.seconds": "{n}秒前",
|
|
@@ -229,6 +231,8 @@ window.__ModuleLoader__.load({
|
|
|
229
231
|
"memory.directory.empty": "No memories yet",
|
|
230
232
|
"directory.editToggle": "Edit mode",
|
|
231
233
|
"directory.deleteConfirm": "Delete this memory?",
|
|
234
|
+
"directory.deleteConfirmShort": "Confirm?",
|
|
235
|
+
"directory.deleteError": "Delete failed",
|
|
232
236
|
"memory.card.open": "Open full text in the Memory tab",
|
|
233
237
|
"memory.time.now": "just now",
|
|
234
238
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -432,6 +436,9 @@ window.__ModuleLoader__.load({
|
|
|
432
436
|
".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
|
|
433
437
|
".mneme-dir-del:hover{filter:brightness(1.25)}",
|
|
434
438
|
".mneme-edit-on .mneme-dir-del{opacity:1}",
|
|
439
|
+
".mneme-dir-del-confirm{font-weight:700;background:rgba(229,72,77,.12);border-radius:4px}",
|
|
440
|
+
".mneme-dir-item.mneme-dir-error{background:rgba(229,72,77,.14)}",
|
|
441
|
+
".mneme-dir-del-err{flex:none;font-size:12px;line-height:16px;color:#e5484d}",
|
|
435
442
|
".mneme-dir-ititle{background:none;border:none;padding:0;font:inherit;color:inherit;cursor:pointer;flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
436
443
|
".mneme-dir-folder{margin-bottom:4px}",
|
|
437
444
|
".mneme-dir-folderhead{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
|
|
@@ -1184,6 +1191,9 @@ window.__ModuleLoader__.load({
|
|
|
1184
1191
|
const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
|
|
1185
1192
|
const [status, setStatus] = useState("loading"); // loading | ready | error
|
|
1186
1193
|
const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
|
|
1194
|
+
const [confirmingId, setConfirmingId] = useState(null); // row waiting for the 2nd confirm click
|
|
1195
|
+
const [delErrorId, setDelErrorId] = useState(null); // row whose delete just failed
|
|
1196
|
+
const confirmTimer = useRef(null);
|
|
1187
1197
|
|
|
1188
1198
|
useEffect(() => {
|
|
1189
1199
|
let cancelled = false;
|
|
@@ -1215,9 +1225,24 @@ window.__ModuleLoader__.load({
|
|
|
1215
1225
|
}).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
|
|
1216
1226
|
};
|
|
1217
1227
|
|
|
1228
|
+
const resetConfirm = () => {
|
|
1229
|
+
clearTimeout(confirmTimer.current);
|
|
1230
|
+
setConfirmingId(null);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1218
1233
|
const handleDelete = (memId, e) => {
|
|
1219
1234
|
e?.stopPropagation?.();
|
|
1220
|
-
|
|
1235
|
+
// 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
|
|
1236
|
+
// window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
|
|
1237
|
+
// 所以改为面板内联两步确认,不依赖任何原生对话框。
|
|
1238
|
+
if (confirmingId !== memId) {
|
|
1239
|
+
setConfirmingId(memId);
|
|
1240
|
+
clearTimeout(confirmTimer.current);
|
|
1241
|
+
confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
clearTimeout(confirmTimer.current);
|
|
1245
|
+
setConfirmingId(null);
|
|
1221
1246
|
apiFetch("/api/dsh-mneme/memories", {
|
|
1222
1247
|
method: "DELETE",
|
|
1223
1248
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1225,26 +1250,48 @@ window.__ModuleLoader__.load({
|
|
|
1225
1250
|
})
|
|
1226
1251
|
.then((res) => (res.ok ? res.json() : null))
|
|
1227
1252
|
.then((d) => {
|
|
1228
|
-
if (!d || d.deleted !== true)
|
|
1253
|
+
if (!d || d.deleted !== true) {
|
|
1254
|
+
// 服务端未确认删除也给出可见提示,不静默吞掉
|
|
1255
|
+
setDelErrorId(memId);
|
|
1256
|
+
setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1229
1259
|
setDir((prev) => ({
|
|
1230
1260
|
groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
|
|
1231
1261
|
untagged: prev.untagged.filter((m) => m.id !== memId)
|
|
1232
1262
|
}));
|
|
1233
1263
|
})
|
|
1234
|
-
.catch(() => {
|
|
1264
|
+
.catch(() => {
|
|
1265
|
+
// 网络失败同样给出可见提示
|
|
1266
|
+
setDelErrorId(memId);
|
|
1267
|
+
setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
|
|
1268
|
+
});
|
|
1235
1269
|
};
|
|
1236
1270
|
|
|
1237
1271
|
const renderFolder = (name, memories, extraKey, emptyLabel) => {
|
|
1238
1272
|
const folderKey = extraKey || name;
|
|
1239
1273
|
const open = !collapsed[folderKey];
|
|
1240
|
-
const rows = memories.map((m) =>
|
|
1241
|
-
|
|
1274
|
+
const rows = memories.map((m) => {
|
|
1275
|
+
const confirming = confirmingId === m.id;
|
|
1276
|
+
const err = delErrorId === m.id;
|
|
1277
|
+
return h("div", {
|
|
1278
|
+
key: m.id,
|
|
1279
|
+
className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
|
|
1280
|
+
// 点击行内其它地方取消「确认?」态
|
|
1281
|
+
onClick: resetConfirm
|
|
1282
|
+
},
|
|
1242
1283
|
h("span", { className: "mneme-tree-line" }),
|
|
1243
1284
|
h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
|
|
1244
1285
|
h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
|
|
1245
|
-
editMode && h("button", {
|
|
1246
|
-
|
|
1247
|
-
|
|
1286
|
+
editMode && h("button", {
|
|
1287
|
+
type: "button",
|
|
1288
|
+
className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
|
|
1289
|
+
"aria-label": "delete",
|
|
1290
|
+
onClick: (e) => handleDelete(m.id, e)
|
|
1291
|
+
}, confirming ? t("directory.deleteConfirmShort") : "✕"),
|
|
1292
|
+
err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
|
|
1293
|
+
);
|
|
1294
|
+
});
|
|
1248
1295
|
return h("div", {
|
|
1249
1296
|
key: folderKey,
|
|
1250
1297
|
className: "mneme-dir-folder",
|
|
@@ -17,6 +17,11 @@ export const Config = z.object({
|
|
|
17
17
|
summarizeModel: z.string().default(""),
|
|
18
18
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
19
19
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
20
|
+
// 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
|
|
21
|
+
// 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
|
|
22
|
+
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
|
+
// 关闭时行为与之前完全一致。
|
|
24
|
+
injectTimePrefix: z.boolean().default(false),
|
|
20
25
|
autoDream: z.boolean().default(true),
|
|
21
26
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
22
27
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -559,7 +559,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
559
559
|
if (inFlight) await inFlight.catch(() => {});
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
async function runDream(ctx, service, config) {
|
|
562
|
+
async function runDream(ctx, service, config, settings) {
|
|
563
563
|
const logger = ctx.logger;
|
|
564
564
|
let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
565
565
|
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
@@ -838,7 +838,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
838
838
|
// and always fail-safe — a tag failure must never change the consolidation
|
|
839
839
|
// outcome reported below (it is logged and counted, nothing more).
|
|
840
840
|
let autoTagged = 0;
|
|
841
|
-
|
|
841
|
+
// Effective opt-in (issue #31): the Web panel's settings toggle
|
|
842
|
+
// (user_settings) overrides the zod plugin config when explicitly stored;
|
|
843
|
+
// unset → fall back to config.autoTagEnabled. settings is null-safe (absent
|
|
844
|
+
// in tests / direct calls) so behaviour is unchanged without it.
|
|
845
|
+
const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
|
|
846
|
+
if (autoTagEnabled === true) {
|
|
842
847
|
try {
|
|
843
848
|
const tagResult = await runAutoTag({ ctx, service, config, route });
|
|
844
849
|
autoTagged = tagResult?.tagged ?? 0;
|
|
@@ -60,7 +60,12 @@ export const apply = (ctx, config) => {
|
|
|
60
60
|
store.purgeRecallRunsOlderThan(recallRetentionDays);
|
|
61
61
|
} catch { /* non-fatal */ }
|
|
62
62
|
const mirror = createMirror(memoryDir);
|
|
63
|
-
|
|
63
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
64
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
65
|
+
// Created before the service so the tag-config resolver (issue #31) can merge
|
|
66
|
+
// the Web panel's persisted toggles over the plugin config at runtime.
|
|
67
|
+
const settings = createSettings(store.db);
|
|
68
|
+
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
|
|
64
69
|
|
|
65
70
|
// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
|
|
66
71
|
// a safe re-render at boot so a stale mirror converges without needing a
|
|
@@ -85,10 +90,6 @@ export const apply = (ctx, config) => {
|
|
|
85
90
|
} catch { /* non-fatal: recall recording is bookkeeping */ }
|
|
86
91
|
});
|
|
87
92
|
|
|
88
|
-
// User-configurable settings (profile, rules) and custom commands share the
|
|
89
|
-
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
90
|
-
const settings = createSettings(store.db);
|
|
91
|
-
|
|
92
93
|
// Semantic pipeline: a local/ollama embedder when configured, otherwise the
|
|
93
94
|
// legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
|
|
94
95
|
// the store's embedding column and tracks the active model fingerprint. A
|
|
@@ -251,7 +252,7 @@ export const apply = (ctx, config) => {
|
|
|
251
252
|
delayMs: cfg.dreamDelayMs,
|
|
252
253
|
logger: ctx.logger,
|
|
253
254
|
semantic: { embedder, vectorIndex },
|
|
254
|
-
onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
|
|
255
|
+
onRun: () => (dream ? dream.runDream(ctx, service, cfg, settings) : Promise.resolve({ ok: true, skipped: true }))
|
|
255
256
|
});
|
|
256
257
|
service.setDreamHook(() => dream.maybeSchedule(service));
|
|
257
258
|
}
|
|
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
|
+
// current date/time is injected once per conversation — at the first prompt
|
|
80
|
+
// assembly of a new session — so the model can sense what time/day it is.
|
|
81
|
+
// A per-session latch means later assemblies in the same session never
|
|
82
|
+
// re-inject; a bare render ctx (no session id) falls back to once per
|
|
83
|
+
// injector lifetime.
|
|
84
|
+
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
85
|
+
let timePrefixSession = null;
|
|
86
|
+
|
|
87
|
+
function renderTimePrefix(ctx) {
|
|
88
|
+
if (config.injectTimePrefix !== true) return "";
|
|
89
|
+
const sessionId = ctx?.agent?.session?.id;
|
|
90
|
+
if (sessionId === undefined) {
|
|
91
|
+
if (timePrefixSession !== null) return "";
|
|
92
|
+
timePrefixSession = true;
|
|
93
|
+
} else {
|
|
94
|
+
if (sessionId === timePrefixSession) return "";
|
|
95
|
+
timePrefixSession = sessionId;
|
|
96
|
+
}
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
99
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
100
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
101
|
+
return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
79
105
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
80
106
|
// and an entry that would exceed it collapses to its title only, so a long
|
|
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
188
214
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
189
215
|
const hotText = renderHotContext(ctx);
|
|
190
216
|
const body = render(candidates);
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
|
|
218
|
+
// Time prefix (issue #34): leads the injection block once per session.
|
|
219
|
+
const timePrefix = renderTimePrefix(ctx);
|
|
220
|
+
if (!timePrefix) return out;
|
|
221
|
+
return out ? `${timePrefix}\n\n${out}` : timePrefix;
|
|
193
222
|
}
|
|
194
223
|
}),
|
|
195
224
|
ctx.systemPrompt.context({
|
|
@@ -65,7 +65,7 @@ export function computeRetrievalMetrics(actualIds, expectedIds) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function createService({ store, mirror, config, onWrite, logger }) {
|
|
68
|
+
export function createService({ store, mirror, config, onWrite, logger, settings }) {
|
|
69
69
|
// Optional dream scheduler hook, installed via setDreamHook after creation
|
|
70
70
|
// (the scheduler holds a reference back to the service, so it cannot be
|
|
71
71
|
// passed in the constructor). Fired on the same write events as onWrite.
|
|
@@ -91,6 +91,20 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
91
91
|
let vectorIndex = null;
|
|
92
92
|
let reranker = null;
|
|
93
93
|
|
|
94
|
+
// Effective tag toggles (issue #31): the settings layer (Web panel switches,
|
|
95
|
+
// persisted in user_settings) overrides the plugin config when explicitly
|
|
96
|
+
// stored; unset keys fall back to the zod config default. This is the single
|
|
97
|
+
// runtime source of truth for setMemoryTags' manual gate and the Web panel's
|
|
98
|
+
// getTagConfig display — the panel toggle and the dream autoTag gate were
|
|
99
|
+
// previously writing/reading two disconnected stores.
|
|
100
|
+
const tagConfig = () => {
|
|
101
|
+
const stored = settings?.getAutoTagConfig?.() ?? {};
|
|
102
|
+
return {
|
|
103
|
+
autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : (config.autoTagEnabled ?? false),
|
|
104
|
+
manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : (config.manualTagEnabled ?? true)
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
94
108
|
// Optional recall recorder, installed via setRecallRecorder after creation.
|
|
95
109
|
// When searchMemories is called with recordRecall=true it receives the
|
|
96
110
|
// actual merged recall scene (candidates + scores + source + threshold) so
|
|
@@ -933,6 +947,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
933
947
|
});
|
|
934
948
|
// Bug7: a degraded/archived result is applied on top of the merged row.
|
|
935
949
|
const result = applyQualityDisposition(merged, quality, qf);
|
|
950
|
+
// Bridge tool-passed tags (plus any quality signal tags) into the
|
|
951
|
+
// entity_attrs tag store so directory / tag: recall / tagBoost see them
|
|
952
|
+
// (issue #31). Must run before afterSync so the mirror re-render reads
|
|
953
|
+
// the bridged row. Unconditional on the array (empty included): a merge
|
|
954
|
+
// that clears tags must also invalidate any stale entity_attrs row from
|
|
955
|
+
// an earlier save, or the directory keeps showing the old tag.
|
|
956
|
+
if (Array.isArray(result.tags)) store.setMemoryTags(result.id, result.tags);
|
|
936
957
|
afterSync("write");
|
|
937
958
|
notifyWrite();
|
|
938
959
|
scheduleEmbed(result);
|
|
@@ -952,6 +973,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
952
973
|
...(quality ? { quality_score: quality.score } : {})
|
|
953
974
|
});
|
|
954
975
|
const result = applyQualityDisposition(created, quality, qf);
|
|
976
|
+
// Bridge tool-passed tags (plus any quality signal tags) into the
|
|
977
|
+
// entity_attrs tag store so directory / tag: recall / tagBoost see them
|
|
978
|
+
// (issue #31). Must run before afterSync so the mirror re-render reads
|
|
979
|
+
// the bridged row.
|
|
980
|
+
if (Array.isArray(result.tags) && result.tags.length) store.setMemoryTags(result.id, result.tags);
|
|
955
981
|
afterSync("write");
|
|
956
982
|
notifyWrite();
|
|
957
983
|
scheduleEmbed(result);
|
|
@@ -1598,6 +1624,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1598
1624
|
update: (id, p, ctx = {}) => {
|
|
1599
1625
|
const old = store.getById(id);
|
|
1600
1626
|
const updated = store.update(id, p);
|
|
1627
|
+
// Bridge tag edits made through update (memory_update tool / panel) into
|
|
1628
|
+
// the entity_attrs tag store too (issue #31) — same convergence as
|
|
1629
|
+
// saveWithDedupe. An explicit empty array clears the live tag row, moving
|
|
1630
|
+
// the memory back to `untagged`.
|
|
1631
|
+
if (p?.tags !== undefined) store.setMemoryTags(updated.id, updated.tags);
|
|
1601
1632
|
// Record a user correction when any meaningful field changed and the
|
|
1602
1633
|
// reflection failure tracker is enabled. expected = what it became,
|
|
1603
1634
|
// actual = what it was before; query (when provided) captures the
|
|
@@ -1757,7 +1788,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1757
1788
|
// tag pass (gated by autoTagEnabled, wrapped in a transaction by the
|
|
1758
1789
|
// extractor so the mirror re-renders exactly once).
|
|
1759
1790
|
setMemoryTags: (memoryId, tags) => {
|
|
1760
|
-
if (
|
|
1791
|
+
if (tagConfig().manualTagEnabled === false) {
|
|
1761
1792
|
return { ok: false, error: "manualTagEnabled is off" };
|
|
1762
1793
|
}
|
|
1763
1794
|
const stored = store.setMemoryTags(memoryId, tags);
|
|
@@ -1768,7 +1799,10 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1768
1799
|
getMemoryTags: (memoryId) => store.getMemoryTags(memoryId),
|
|
1769
1800
|
// Read-only gate flag so the Web panel can hide tag editing when the
|
|
1770
1801
|
// manual path is disabled (default: manual tagging is on).
|
|
1771
|
-
manualTagEnabled: () =>
|
|
1802
|
+
manualTagEnabled: () => tagConfig().manualTagEnabled !== false,
|
|
1803
|
+
// Effective tag toggle pair (settings-over-config merge) for the panel
|
|
1804
|
+
// (/api/dsh-mneme/config GET/PUT).
|
|
1805
|
+
getTagConfig: tagConfig,
|
|
1772
1806
|
applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
|
|
1773
1807
|
saveAttr: (r) => store.saveAttr(r),
|
|
1774
1808
|
createEntity: (r) => store.createEntity(r),
|
|
@@ -148,9 +148,13 @@ export function createSettings(db) {
|
|
|
148
148
|
if (j && typeof j === "object") stored = j;
|
|
149
149
|
} catch { /* fall through to defaults */ }
|
|
150
150
|
}
|
|
151
|
+
// null = never explicitly stored → callers fall back to the plugin
|
|
152
|
+
// config default, so the settings layer can't override a toggle the
|
|
153
|
+
// user never touched (issue #31). This also unifies the manualTagEnabled
|
|
154
|
+
// default (settings no longer invents false where plugin config says true).
|
|
151
155
|
return {
|
|
152
|
-
autoTagEnabled: stored.autoTagEnabled ===
|
|
153
|
-
manualTagEnabled: stored.manualTagEnabled ===
|
|
156
|
+
autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : null,
|
|
157
|
+
manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : null
|
|
154
158
|
};
|
|
155
159
|
},
|
|
156
160
|
setAutoTagConfig(partial) {
|
|
@@ -161,10 +165,15 @@ export function createSettings(db) {
|
|
|
161
165
|
return j && typeof j === "object" ? j : {};
|
|
162
166
|
} catch { return {}; }
|
|
163
167
|
})();
|
|
164
|
-
const cfg = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
const cfg = {};
|
|
169
|
+
// Only persist keys the caller explicitly set (or already stored). A
|
|
170
|
+
// partial update must never invent `false` for a key the user left
|
|
171
|
+
// alone — that would silently disable manual tagging on a toggle of
|
|
172
|
+
// autoTag alone (issue #31).
|
|
173
|
+
if (partial.autoTagEnabled !== undefined) cfg.autoTagEnabled = partial.autoTagEnabled === true;
|
|
174
|
+
else if (cur.autoTagEnabled !== undefined) cfg.autoTagEnabled = cur.autoTagEnabled === true;
|
|
175
|
+
if (partial.manualTagEnabled !== undefined) cfg.manualTagEnabled = partial.manualTagEnabled === true;
|
|
176
|
+
else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
|
|
168
177
|
setSetting("autoTag", JSON.stringify(cfg));
|
|
169
178
|
return cfg;
|
|
170
179
|
}
|
|
@@ -1743,6 +1743,11 @@ export function createStore(path) {
|
|
|
1743
1743
|
VALUES (?, ?, 'tags', ?, ?, ?, NULL, 1.0, 'manual')`
|
|
1744
1744
|
).run(id, memoryId, JSON.stringify(arr), memoryId, now);
|
|
1745
1745
|
}
|
|
1746
|
+
// Keep memories.tags (the display/search column) as a mirror of the
|
|
1747
|
+
// entity_attrs source of truth, so API rows and keyword search never
|
|
1748
|
+
// drift from the directory/tag store (issue #31).
|
|
1749
|
+
db.prepare("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?")
|
|
1750
|
+
.run(JSON.stringify(arr), now, memoryId);
|
|
1746
1751
|
db.exec("RELEASE set_memory_tags");
|
|
1747
1752
|
} catch (e) {
|
|
1748
1753
|
db.exec("ROLLBACK TO set_memory_tags");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,16 +12,23 @@
|
|
|
12
12
|
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "
|
|
15
|
+
"main": "lib/index.js",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"default": "./
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./client": {
|
|
21
|
-
"default": "./
|
|
21
|
+
"default": "./lib/client.js"
|
|
22
22
|
},
|
|
23
|
-
"./package.json": "./
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"src",
|
|
28
|
+
"scripts",
|
|
29
|
+
"test",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
25
32
|
"dsh": {
|
|
26
33
|
"client": {
|
|
27
34
|
"inject": [
|
|
@@ -33,20 +40,9 @@
|
|
|
33
40
|
"platform": "web"
|
|
34
41
|
},
|
|
35
42
|
"bundle": {
|
|
36
|
-
"patch": "./
|
|
37
|
-
},
|
|
38
|
-
"marketplace": {
|
|
39
|
-
"profiles": [
|
|
40
|
-
"web"
|
|
41
|
-
],
|
|
42
|
-
"requiresBuildApproval": false,
|
|
43
|
-
"requiresRestart": true,
|
|
44
|
-
"manualSteps": false
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
45
44
|
}
|
|
46
45
|
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"prepublishOnly": "node -e \"const fs=require('fs');const v=JSON.parse(fs.readFileSync('./dsh-mneme/package.json','utf8')).version;const p=JSON.parse(fs.readFileSync('./package.json','utf8'));p.version=v;fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\\n')\""
|
|
49
|
-
},
|
|
50
46
|
"peerDependencies": {
|
|
51
47
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
48
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -54,5 +50,31 @@
|
|
|
54
50
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
55
51
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
56
52
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
+
"c8": "^12.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"sync": "node scripts/sync-lib.js",
|
|
65
|
+
"prepack": "npm run sync",
|
|
66
|
+
"test": "node --test test/*.test.js",
|
|
67
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
+
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
+
"stress": "node scripts/stress-dsh.js"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@huggingface/transformers": "^4.2.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"adm-zip": "0.6.0"
|
|
76
|
+
},
|
|
77
|
+
"c8": {
|
|
78
|
+
"reporter": ["text", "lcov"]
|
|
57
79
|
}
|
|
58
80
|
}
|
|
@@ -243,11 +243,14 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
243
243
|
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
settings.setAutoTagConfig(patch);
|
|
247
|
+
// Respond with the effective (settings-over-config merged) toggles
|
|
248
|
+
// so the panel always sees booleans that actually gate runtime
|
|
249
|
+
// behaviour (issue #31).
|
|
250
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
248
251
|
return;
|
|
249
252
|
}
|
|
250
|
-
sendJson(res, 200, { config:
|
|
253
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
251
254
|
} catch {
|
|
252
255
|
sendJson(res, 500, { error: "internal" });
|
|
253
256
|
}
|
|
@@ -17,6 +17,11 @@ export const Config = z.object({
|
|
|
17
17
|
summarizeModel: z.string().default(""),
|
|
18
18
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
19
19
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
20
|
+
// 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
|
|
21
|
+
// 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
|
|
22
|
+
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
|
+
// 关闭时行为与之前完全一致。
|
|
24
|
+
injectTimePrefix: z.boolean().default(false),
|
|
20
25
|
autoDream: z.boolean().default(true),
|
|
21
26
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
22
27
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -559,7 +559,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
559
559
|
if (inFlight) await inFlight.catch(() => {});
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
async function runDream(ctx, service, config) {
|
|
562
|
+
async function runDream(ctx, service, config, settings) {
|
|
563
563
|
const logger = ctx.logger;
|
|
564
564
|
let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
565
565
|
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
@@ -838,7 +838,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
838
838
|
// and always fail-safe — a tag failure must never change the consolidation
|
|
839
839
|
// outcome reported below (it is logged and counted, nothing more).
|
|
840
840
|
let autoTagged = 0;
|
|
841
|
-
|
|
841
|
+
// Effective opt-in (issue #31): the Web panel's settings toggle
|
|
842
|
+
// (user_settings) overrides the zod plugin config when explicitly stored;
|
|
843
|
+
// unset → fall back to config.autoTagEnabled. settings is null-safe (absent
|
|
844
|
+
// in tests / direct calls) so behaviour is unchanged without it.
|
|
845
|
+
const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
|
|
846
|
+
if (autoTagEnabled === true) {
|
|
842
847
|
try {
|
|
843
848
|
const tagResult = await runAutoTag({ ctx, service, config, route });
|
|
844
849
|
autoTagged = tagResult?.tagged ?? 0;
|
|
@@ -60,7 +60,12 @@ export const apply = (ctx, config) => {
|
|
|
60
60
|
store.purgeRecallRunsOlderThan(recallRetentionDays);
|
|
61
61
|
} catch { /* non-fatal */ }
|
|
62
62
|
const mirror = createMirror(memoryDir);
|
|
63
|
-
|
|
63
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
64
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
65
|
+
// Created before the service so the tag-config resolver (issue #31) can merge
|
|
66
|
+
// the Web panel's persisted toggles over the plugin config at runtime.
|
|
67
|
+
const settings = createSettings(store.db);
|
|
68
|
+
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
|
|
64
69
|
|
|
65
70
|
// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
|
|
66
71
|
// a safe re-render at boot so a stale mirror converges without needing a
|
|
@@ -85,10 +90,6 @@ export const apply = (ctx, config) => {
|
|
|
85
90
|
} catch { /* non-fatal: recall recording is bookkeeping */ }
|
|
86
91
|
});
|
|
87
92
|
|
|
88
|
-
// User-configurable settings (profile, rules) and custom commands share the
|
|
89
|
-
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
90
|
-
const settings = createSettings(store.db);
|
|
91
|
-
|
|
92
93
|
// Semantic pipeline: a local/ollama embedder when configured, otherwise the
|
|
93
94
|
// legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
|
|
94
95
|
// the store's embedding column and tracks the active model fingerprint. A
|
|
@@ -251,7 +252,7 @@ export const apply = (ctx, config) => {
|
|
|
251
252
|
delayMs: cfg.dreamDelayMs,
|
|
252
253
|
logger: ctx.logger,
|
|
253
254
|
semantic: { embedder, vectorIndex },
|
|
254
|
-
onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
|
|
255
|
+
onRun: () => (dream ? dream.runDream(ctx, service, cfg, settings) : Promise.resolve({ ok: true, skipped: true }))
|
|
255
256
|
});
|
|
256
257
|
service.setDreamHook(() => dream.maybeSchedule(service));
|
|
257
258
|
}
|
|
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
|
+
// current date/time is injected once per conversation — at the first prompt
|
|
80
|
+
// assembly of a new session — so the model can sense what time/day it is.
|
|
81
|
+
// A per-session latch means later assemblies in the same session never
|
|
82
|
+
// re-inject; a bare render ctx (no session id) falls back to once per
|
|
83
|
+
// injector lifetime.
|
|
84
|
+
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
85
|
+
let timePrefixSession = null;
|
|
86
|
+
|
|
87
|
+
function renderTimePrefix(ctx) {
|
|
88
|
+
if (config.injectTimePrefix !== true) return "";
|
|
89
|
+
const sessionId = ctx?.agent?.session?.id;
|
|
90
|
+
if (sessionId === undefined) {
|
|
91
|
+
if (timePrefixSession !== null) return "";
|
|
92
|
+
timePrefixSession = true;
|
|
93
|
+
} else {
|
|
94
|
+
if (sessionId === timePrefixSession) return "";
|
|
95
|
+
timePrefixSession = sessionId;
|
|
96
|
+
}
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
99
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
100
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
101
|
+
return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
79
105
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
80
106
|
// and an entry that would exceed it collapses to its title only, so a long
|
|
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
188
214
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
189
215
|
const hotText = renderHotContext(ctx);
|
|
190
216
|
const body = render(candidates);
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
|
|
218
|
+
// Time prefix (issue #34): leads the injection block once per session.
|
|
219
|
+
const timePrefix = renderTimePrefix(ctx);
|
|
220
|
+
if (!timePrefix) return out;
|
|
221
|
+
return out ? `${timePrefix}\n\n${out}` : timePrefix;
|
|
193
222
|
}
|
|
194
223
|
}),
|
|
195
224
|
ctx.systemPrompt.context({
|