@modusensus/dsh-mneme 0.7.11 → 0.7.13
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.en.md +69 -0
- package/README.md +227 -21
- package/bin/cli.mjs +603 -0
- package/lib/api-standalone.js +264 -0
- package/lib/api.js +91 -2
- package/lib/client.js +243 -1
- package/lib/config.js +80 -0
- package/lib/index.js +43 -12
- package/lib/quality-filter.js +4 -1
- package/lib/service.js +38 -5
- package/lib/settings.js +40 -0
- package/lib/store.js +4 -1
- package/lib/summarize.js +197 -59
- package/lib/tools.js +3 -3
- package/package.json +7 -1
- package/src/api-standalone.js +264 -0
- package/src/api.js +91 -2
- package/src/config.js +80 -0
- package/src/index.js +43 -12
- package/src/quality-filter.js +4 -1
- package/src/service.js +38 -5
- package/src/settings.js +40 -0
- package/src/store.js +4 -1
- package/src/summarize.js +197 -59
- package/src/tools.js +3 -3
- package/test/api.test.js +44 -0
- package/test/helpers/peer-worker.mjs +10 -0
- package/test/peer-blockers.test.js +4 -9
- package/test/settings.test.js +24 -0
- package/test/standalone-api.test.js +326 -0
- package/test/summarize.test.js +116 -0
package/lib/config.js
CHANGED
|
@@ -9,8 +9,39 @@ export const Config = z.object({
|
|
|
9
9
|
// active provider/model (same as before).
|
|
10
10
|
summarizeProvider: z.string().default(""),
|
|
11
11
|
summarizeModel: z.string().default(""),
|
|
12
|
+
// 蒸馏转录上限(字符)。借鉴 Codex「保留原始、替代压缩摘要」的思路:
|
|
13
|
+
// 蒸馏把完整对话上下文交给 LLM 提炼,不硬裁到 8000 字就截断语义;默认
|
|
14
|
+
// 24000 字符(约覆盖一整轮中等对话),需要更完整可调大。
|
|
15
|
+
distillMaxChars: z.natural().min(1000).max(200000).default(24000),
|
|
16
|
+
// 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
|
|
17
|
+
// 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
|
|
18
|
+
// distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
|
|
19
|
+
// distillRateLimitRetries 次,全程对用户透明,不把 429 错误码抛给用户。
|
|
20
|
+
distillRateLimitIntervalMs: z.natural().min(0).max(60000).default(1000),
|
|
21
|
+
distillRateLimitRetries: z.natural().min(0).max(10).default(3),
|
|
22
|
+
distillRateLimitBaseDelayMs: z.natural().min(100).max(60000).default(1000),
|
|
12
23
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
13
24
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
25
|
+
// 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
|
|
26
|
+
// 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
|
|
27
|
+
// constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
|
|
28
|
+
// → 工具调用与结果 → 代码执行),不再只看用户消息,便于提炼踩坑根因。
|
|
29
|
+
// 关闭时行为与之前完全一致。
|
|
30
|
+
codingRetrospect: z.boolean().default(false),
|
|
31
|
+
// 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
|
|
32
|
+
codingKeywords: z.array(z.string()).default([
|
|
33
|
+
"代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
|
|
34
|
+
"接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
|
|
35
|
+
"python", "javascript", "typescript", "node", "js", "ts",
|
|
36
|
+
"sql", "sqlite", "数据库", "算法", "重构", "优化", "性能",
|
|
37
|
+
"测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
|
|
38
|
+
"命令行", "shell", "配置", "配置文件", "yaml", "json",
|
|
39
|
+
"插件", "开发", "编译", "构建", "部署", "git", "commit",
|
|
40
|
+
"review", "前端", "后端", "页面", "组件", "dsh", "memos"
|
|
41
|
+
]),
|
|
42
|
+
// 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
|
|
43
|
+
// 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
|
|
44
|
+
codingBoostFactor: z.number().min(1).max(5).default(2),
|
|
14
45
|
autoDream: z.boolean().default(true),
|
|
15
46
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
16
47
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -249,4 +280,53 @@ export const Config = z.object({
|
|
|
249
280
|
// audits to recall_runs and NEVER touches recall_evals, regardless of this
|
|
250
281
|
// flag (production isolation is unconditional).
|
|
251
282
|
evalPersistTestResults: z.boolean().default(false),
|
|
283
|
+
|
|
284
|
+
// --- standalone external API (v0.7.12) ------------------------------------
|
|
285
|
+
// A plain node:http server for ecosystem integrations that cannot reach the
|
|
286
|
+
// DSH-internal webServer. Disabled by default; when enabled the Bearer token
|
|
287
|
+
// is persisted in the settings kv ("external_api"), auto-generated on first
|
|
288
|
+
// boot. Bind host: keep the loopback default — moving it to a non-loopback
|
|
289
|
+
// address exposes the whole memory store to the network and is the
|
|
290
|
+
// operator's responsibility.
|
|
291
|
+
externalApiEnabled: z.boolean().default(false),
|
|
292
|
+
externalApiPort: z.natural().default(8790),
|
|
293
|
+
externalApiHost: z.string().default("127.0.0.1"),
|
|
294
|
+
|
|
295
|
+
// --- light mode preset (v0.7.12) -------------------------------------------
|
|
296
|
+
// One switch for low-resource setups: turns off every background/semantic
|
|
297
|
+
// heavy path (dream consolidation, entity extraction, vector pipeline,
|
|
298
|
+
// reranker, BM25, semantic dedup / selective inject, sleep mode) while
|
|
299
|
+
// keeping the core loop (autoInject, autoSummarize, hot memory, quality
|
|
300
|
+
// filter, keyword search). Applied by applyLightModePreset before the config
|
|
301
|
+
// reaches any service; a persisted panel_mode="light" (settings kv) counts
|
|
302
|
+
// as lightMode=true too and wins over the bundle config.
|
|
303
|
+
lightMode: z.boolean().default(false),
|
|
252
304
|
});
|
|
305
|
+
|
|
306
|
+
// Fields forced to false by the light-mode preset. Everything not listed here
|
|
307
|
+
// (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
|
|
308
|
+
// thresholds/delays, ...) is left untouched — those are the core loop.
|
|
309
|
+
const LIGHT_MODE_OFF = [
|
|
310
|
+
"entityExtractionEnabled",
|
|
311
|
+
"autoDream",
|
|
312
|
+
"sleepModeEnabled",
|
|
313
|
+
"rerankEnabled",
|
|
314
|
+
"autoReindexOnBoot",
|
|
315
|
+
"hybridInject",
|
|
316
|
+
"searchSemanticDedup",
|
|
317
|
+
"selectiveInjectEnabled",
|
|
318
|
+
"bm25SearchEnabled"
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Apply the light-mode preset to a resolved config object (pure function,
|
|
323
|
+
* exported for tests). When cfg.lightMode is not exactly true the config is
|
|
324
|
+
* returned unchanged; otherwise a shallow copy carries false for every heavy
|
|
325
|
+
* feature. Idempotent and side-effect free.
|
|
326
|
+
*/
|
|
327
|
+
export function applyLightModePreset(cfg) {
|
|
328
|
+
if (cfg?.lightMode !== true) return cfg;
|
|
329
|
+
const preset = { ...cfg, lightMode: true };
|
|
330
|
+
for (const key of LIGHT_MODE_OFF) preset[key] = false;
|
|
331
|
+
return preset;
|
|
332
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -7,13 +7,14 @@ import { createSummarizer } from "./summarize.js";
|
|
|
7
7
|
import { createDreamScheduler } from "./dream.js";
|
|
8
8
|
import { createSleepScheduler, runSleep } from "./dream/sleep.js";
|
|
9
9
|
import { createApi } from "./api.js";
|
|
10
|
+
import { createStandaloneApi } from "./api-standalone.js";
|
|
10
11
|
import { createSettings } from "./settings.js";
|
|
11
12
|
import { createCommandManager } from "./commands.js";
|
|
12
13
|
import { createEmbedder } from "./embedding.js";
|
|
13
14
|
import { createEmbedderByProvider } from "./local-embedder.js";
|
|
14
15
|
import { LocalReranker } from "./reranker.js";
|
|
15
16
|
import { createVectorIndex } from "./vector-index.js";
|
|
16
|
-
import { Config } from "./config.js";
|
|
17
|
+
import { Config, applyLightModePreset } from "./config.js";
|
|
17
18
|
import { extractEntities } from "./entities/extractor.js";
|
|
18
19
|
import { mkdirSync } from "node:fs";
|
|
19
20
|
import { join } from "node:path";
|
|
@@ -29,12 +30,12 @@ export { Config };
|
|
|
29
30
|
// has no prototype, is called normally, and its returned disposer is collected
|
|
30
31
|
// and run by the fiber on unload.
|
|
31
32
|
export const apply = (ctx, config) => {
|
|
32
|
-
const
|
|
33
|
+
const rawCfg = Config(config);
|
|
33
34
|
|
|
34
35
|
// Resolve memoryDir: expand leading "~"
|
|
35
|
-
const memoryDir =
|
|
36
|
-
? join(homedir(),
|
|
37
|
-
:
|
|
36
|
+
const memoryDir = rawCfg.memoryDir.startsWith("~")
|
|
37
|
+
? join(homedir(), rawCfg.memoryDir.slice(1))
|
|
38
|
+
: rawCfg.memoryDir;
|
|
38
39
|
mkdirSync(memoryDir, { recursive: true });
|
|
39
40
|
|
|
40
41
|
const store = createStore(join(memoryDir, "memory.db"));
|
|
@@ -47,11 +48,26 @@ export const apply = (ctx, config) => {
|
|
|
47
48
|
// default 90). Best-effort like the failure prune — the audit trail is
|
|
48
49
|
// bookkeeping and a failed purge must never block plugin boot.
|
|
49
50
|
try {
|
|
50
|
-
if (
|
|
51
|
-
const retentionMs = Number.isInteger(
|
|
51
|
+
if (rawCfg.llmAudit?.enabled !== false) {
|
|
52
|
+
const retentionMs = Number.isInteger(rawCfg.llmAudit?.retentionDays) ? rawCfg.llmAudit.retentionDays : 90;
|
|
52
53
|
store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
|
|
53
54
|
}
|
|
54
55
|
} catch { /* non-fatal */ }
|
|
56
|
+
|
|
57
|
+
// User-configurable settings (profile, rules, panel mode, standalone API
|
|
58
|
+
// token) share the same SQLite file in dedicated tables, isolated from
|
|
59
|
+
// memories. Created before the config is finalized: the persisted
|
|
60
|
+
// panel_mode participates in light-mode resolution below.
|
|
61
|
+
const settings = createSettings(store.db);
|
|
62
|
+
|
|
63
|
+
// Light mode (v0.7.12): the bundle config flag OR a persisted panel_mode of
|
|
64
|
+
// "light" (the panel switch wins over the bundle config so it survives
|
|
65
|
+
// config redeploys). applyLightModePreset turns every heavy background /
|
|
66
|
+
// semantic feature off and keeps the core loop (autoInject, autoSummarize,
|
|
67
|
+
// hot memory, quality filter).
|
|
68
|
+
const lightMode = rawCfg.lightMode === true || settings.getPanelMode() === "light";
|
|
69
|
+
const cfg = applyLightModePreset({ ...rawCfg, lightMode });
|
|
70
|
+
|
|
55
71
|
const mirror = createMirror(memoryDir);
|
|
56
72
|
const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
|
|
57
73
|
|
|
@@ -78,10 +94,6 @@ export const apply = (ctx, config) => {
|
|
|
78
94
|
} catch { /* non-fatal: recall recording is bookkeeping */ }
|
|
79
95
|
});
|
|
80
96
|
|
|
81
|
-
// User-configurable settings (profile, rules) and custom commands share the
|
|
82
|
-
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
83
|
-
const settings = createSettings(store.db);
|
|
84
|
-
|
|
85
97
|
// Semantic pipeline: a local/ollama embedder when configured, otherwise the
|
|
86
98
|
// legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
|
|
87
99
|
// the store's embedding column and tracks the active model fingerprint. A
|
|
@@ -107,7 +119,13 @@ export const apply = (ctx, config) => {
|
|
|
107
119
|
|
|
108
120
|
let embedder = null;
|
|
109
121
|
let reranker = null;
|
|
110
|
-
if (
|
|
122
|
+
if (lightMode) {
|
|
123
|
+
// Light mode: the whole vector pipeline stays off — no embedder (nothing
|
|
124
|
+
// pulls in ONNX/transformers), no reranker, no boot backfill (the preset
|
|
125
|
+
// also cleared autoReindexOnBoot). Recall degrades to keyword search and
|
|
126
|
+
// human mirror edits still merge on boot.
|
|
127
|
+
applyHumanEdits();
|
|
128
|
+
} else if (cfg.embedProvider === "openai") {
|
|
111
129
|
// vectorIndex is passed so the legacy OpenAI embedder records the producing
|
|
112
130
|
// model fingerprint after each successful embed (Bug3).
|
|
113
131
|
embedder = createEmbedder({ store, settings, logger: ctx.logger, vectorIndex });
|
|
@@ -326,6 +344,19 @@ export const apply = (ctx, config) => {
|
|
|
326
344
|
disposers.push(api.dispose);
|
|
327
345
|
}
|
|
328
346
|
|
|
347
|
+
// Standalone external API (v0.7.12): plain node:http server for ecosystem
|
|
348
|
+
// integrations outside the DSH host. Persisted external_api settings win
|
|
349
|
+
// over the bundle config (enabled/port); the Bearer token lives in the same
|
|
350
|
+
// kv and is auto-generated on first boot by createStandaloneApi. Binding a
|
|
351
|
+
// non-loopback host is the operator's documented responsibility.
|
|
352
|
+
if ((settings.getExternalApi?.()?.enabled ?? cfg.externalApiEnabled) === true) {
|
|
353
|
+
const standalone = createStandaloneApi({ service, store, config: cfg, logger: ctx.logger, settings });
|
|
354
|
+
disposers.push(() => standalone.server.close());
|
|
355
|
+
standalone.ready.catch((error) => {
|
|
356
|
+
ctx.logger?.warn?.(`[dsh-mneme] standalone API failed to start: ${String(error)}`);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
329
360
|
// Async disposer: cordis awaits the returned promise on unload (runDisposable),
|
|
330
361
|
// so an in-flight dream run is allowed to finish before the SQLite store is
|
|
331
362
|
// closed — dream.dispose() resolves only after its current run settles.
|
package/lib/quality-filter.js
CHANGED
|
@@ -34,7 +34,10 @@ const TYPE_LABELS = {
|
|
|
34
34
|
decision: ["decision", "决策", "决定"],
|
|
35
35
|
history: ["history", "历史", "事件"],
|
|
36
36
|
summary: ["summary", "总结", "摘要", "总览"],
|
|
37
|
-
pattern: ["pattern", "模式", "规律"]
|
|
37
|
+
pattern: ["pattern", "模式", "规律"],
|
|
38
|
+
rejected_solution: ["rejected_solution", "被否决", "废弃方案"],
|
|
39
|
+
pitfall: ["pitfall", "踩坑"],
|
|
40
|
+
constraint: ["constraint", "约束"]
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
/** Normalized bigram-overlap similarity in [0,1]; 0 for tiny/empty inputs. */
|
package/lib/service.js
CHANGED
|
@@ -4,7 +4,21 @@ import { evaluateMemoryQuality } from "./quality-filter.js";
|
|
|
4
4
|
import { createBM25Index } from "./search/bm25.js";
|
|
5
5
|
import { adaptiveThreshold } from "./search/adaptive.js";
|
|
6
6
|
|
|
7
|
-
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"]);
|
|
7
|
+
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]);
|
|
8
|
+
|
|
9
|
+
// 编码记忆类型(codingRetrospect):rejected_solution / pitfall / constraint
|
|
10
|
+
// 只在编码任务时注入(防噪声污染其他业务),且编码场景下按 codingBoostFactor
|
|
11
|
+
// 加权排序提前。
|
|
12
|
+
const CODING_MEMORY_TYPES = new Set(["rejected_solution", "pitfall", "constraint"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 判断一段文本是否编码类任务(关键词匹配,codingRetrospect 读取侧门控)。
|
|
16
|
+
* 纯函数,无副作用,便于单测。
|
|
17
|
+
*/
|
|
18
|
+
export function isCodingTask(text, keywords = []) {
|
|
19
|
+
const t = String(text ?? "").toLowerCase();
|
|
20
|
+
return keywords.some((kw) => t.includes(String(kw).toLowerCase()));
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
// Epistemic trust weights (v0.4.5): when config.trustEpistemicWeighting is on,
|
|
10
24
|
// each recall candidate's existing score is multiplied by the weight of its
|
|
@@ -863,17 +877,35 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
863
877
|
*/
|
|
864
878
|
function injectCandidates({ query = "", maxItems = 5, threshold = 3, queryVector } = {}) {
|
|
865
879
|
const q = String(query ?? "").trim();
|
|
880
|
+
// codingRetrospect 读取侧门控:编码记忆(rejected_solution / pitfall /
|
|
881
|
+
// constraint)只在编码任务时注入,防噪声污染其他业务;编码任务时按
|
|
882
|
+
// codingBoostFactor 加权,让编码记忆在编码场景更靠前。
|
|
883
|
+
const isCoding = isCodingTask(q, config.codingKeywords ?? []);
|
|
884
|
+
const codingGate = (m) => isCoding || !CODING_MEMORY_TYPES.has(m.type);
|
|
866
885
|
// Bug7: quality-weighted importance in the rule-based tier. Unassessed rows
|
|
867
886
|
// (quality_score null) count as 100 (weight 1), so legacy stores keep their
|
|
868
887
|
// exact summary>preference>importance ordering.
|
|
869
888
|
const qualityWeight = (m) => (m.quality_score != null ? m.quality_score / 100 : 1);
|
|
870
889
|
const items = store.list({ limit: 200, includeForgotten: false })
|
|
871
890
|
.filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
891
|
+
codingGate(m) &&
|
|
872
892
|
(m.type === "summary" || m.type === "preference" || m.importance >= threshold))
|
|
873
893
|
.sort((a, b) => {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
894
|
+
// 编码记忆在编码任务时优先于普通 decision(与 preference 同级),
|
|
895
|
+
// importance 乘 codingBoostFactor 加权(封顶 5,保持 importance 语义)。
|
|
896
|
+
const priority = (m) => {
|
|
897
|
+
if (m.type === "summary") return 0;
|
|
898
|
+
if (m.type === "preference") return 1;
|
|
899
|
+
if (isCoding && CODING_MEMORY_TYPES.has(m.type)) return 1;
|
|
900
|
+
return 2;
|
|
901
|
+
};
|
|
902
|
+
const effImportance = (m) =>
|
|
903
|
+
(isCoding && CODING_MEMORY_TYPES.has(m.type))
|
|
904
|
+
? Math.min(5, m.importance * (config.codingBoostFactor ?? 2))
|
|
905
|
+
: m.importance;
|
|
906
|
+
const pa = priority(a);
|
|
907
|
+
const pb = priority(b);
|
|
908
|
+
return pa - pb || (effImportance(b) * qualityWeight(b)) - (effImportance(a) * qualityWeight(a));
|
|
877
909
|
});
|
|
878
910
|
let candidates = items;
|
|
879
911
|
if (config.hybridInject !== false && q) {
|
|
@@ -888,6 +920,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
888
920
|
const hits = vectorIndex.search(queryVector, { limit: maxItems * 2, threshold: 0 });
|
|
889
921
|
for (const m of hits) {
|
|
890
922
|
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
923
|
+
codingGate(m) &&
|
|
891
924
|
(m.type === "summary" || m.type === "preference" || m.importance >= threshold)) {
|
|
892
925
|
semanticItems.push(m);
|
|
893
926
|
}
|
|
@@ -896,7 +929,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
896
929
|
}
|
|
897
930
|
if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) {
|
|
898
931
|
for (const m of lastSemanticRecall.items) {
|
|
899
|
-
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten) semanticItems.push(m);
|
|
932
|
+
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten && codingGate(m)) semanticItems.push(m);
|
|
900
933
|
}
|
|
901
934
|
}
|
|
902
935
|
if (semanticItems.length) {
|
package/lib/settings.js
CHANGED
|
@@ -137,6 +137,46 @@ export function createSettings(db) {
|
|
|
137
137
|
};
|
|
138
138
|
setSetting("vector", JSON.stringify(cfg));
|
|
139
139
|
return cfg;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Standalone external API settings (kv "external_api"): {enabled, port,
|
|
144
|
+
* token}. The Bearer token is auto-generated on first boot and persisted
|
|
145
|
+
* here. Partial writes preserve the keys they don't mention.
|
|
146
|
+
*/
|
|
147
|
+
getExternalApi() {
|
|
148
|
+
const raw = getSetting("external_api");
|
|
149
|
+
if (!raw) return undefined;
|
|
150
|
+
try {
|
|
151
|
+
const cfg = JSON.parse(raw);
|
|
152
|
+
return typeof cfg === "object" && cfg !== null ? cfg : undefined;
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
setExternalApi(patch = {}) {
|
|
158
|
+
const prev = this.getExternalApi() ?? {};
|
|
159
|
+
const port = Number(patch.port ?? prev.port);
|
|
160
|
+
const host = typeof patch.host === "string" && patch.host.trim() ? patch.host.trim() : (prev.host ?? "127.0.0.1");
|
|
161
|
+
const cfg = {
|
|
162
|
+
enabled: patch.enabled !== undefined ? patch.enabled === true : prev.enabled === true,
|
|
163
|
+
port: Number.isInteger(port) && port > 0 ? port : 8790,
|
|
164
|
+
host,
|
|
165
|
+
token: String(patch.token ?? prev.token ?? "")
|
|
166
|
+
};
|
|
167
|
+
setSetting("external_api", JSON.stringify(cfg));
|
|
168
|
+
return cfg;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Web panel mode (kv "panel_mode"): "light" (low-resource preset) or
|
|
173
|
+
* "standard" (full feature set). Unset reads as "standard".
|
|
174
|
+
*/
|
|
175
|
+
getPanelMode() {
|
|
176
|
+
return getSetting("panel_mode") === "light" ? "light" : "standard";
|
|
177
|
+
},
|
|
178
|
+
setPanelMode(mode) {
|
|
179
|
+
setSetting("panel_mode", mode === "light" ? "light" : "standard");
|
|
140
180
|
}
|
|
141
181
|
};
|
|
142
182
|
}
|
package/lib/store.js
CHANGED
|
@@ -239,7 +239,10 @@ CREATE TABLE IF NOT EXISTS mirror_state (
|
|
|
239
239
|
);
|
|
240
240
|
`;
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
// Exported for API-layer type validation (standalone API POST /memories and
|
|
243
|
+
// the /status byType breakdown); the set itself stays the single source of
|
|
244
|
+
// truth for what store.save accepts.
|
|
245
|
+
export const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern", "rejected_solution", "pitfall", "constraint"]);
|
|
243
246
|
|
|
244
247
|
// Epistemic status: what kind of evidence a memory rests on. Defaults to
|
|
245
248
|
// 'subjective' so legacy rows (and rows without any signal) stay compatible.
|