@modusensus/dsh-mneme 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/bin/dsh-mneme-choro.mjs +254 -0
- package/docs/CONFIGURATION.md +10 -1
- package/lib/api-standalone.js +17 -21
- package/lib/api.js +414 -12
- package/lib/bootstrap-http.js +105 -0
- package/lib/bootstrap.js +5 -1
- package/lib/choro-command.js +104 -0
- package/lib/client.js +1637 -110
- package/lib/config-keys.js +144 -0
- package/lib/config.js +60 -1
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/index.js +87 -5
- package/lib/inject.js +18 -4
- package/lib/lang.js +82 -0
- package/lib/profile-facts.js +356 -0
- package/lib/quality-filter.js +48 -12
- package/lib/service.js +112 -4
- package/lib/settings.js +13 -1
- package/lib/store.js +978 -27
- package/lib/tools.js +244 -1
- package/lib/user-settings.js +63 -7
- package/lib/wiki/inject.js +164 -0
- package/lib/wiki/partition.js +200 -0
- package/lib/wiki/plan.js +214 -0
- package/lib/wiki/queries.js +360 -0
- package/lib/wiki/render.js +187 -0
- package/lib/wiki/scan.js +371 -0
- package/lib/wiki/t0.js +463 -0
- package/lib/wiki/t1.js +496 -0
- package/lib/write-admission.js +5 -3
- package/package.json +4 -2
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +17 -21
- package/src/api.js +414 -12
- package/src/bootstrap-http.js +105 -0
- package/src/bootstrap.js +5 -1
- package/src/choro-command.js +104 -0
- package/src/config-keys.js +144 -0
- package/src/config.js +60 -1
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/index.js +87 -5
- package/src/inject.js +18 -4
- package/src/lang.js +82 -0
- package/src/profile-facts.js +356 -0
- package/src/quality-filter.js +48 -12
- package/src/service.js +112 -4
- package/src/settings.js +13 -1
- package/src/store.js +978 -27
- package/src/tools.js +244 -1
- package/src/user-settings.js +63 -7
- package/src/wiki/inject.js +164 -0
- package/src/wiki/partition.js +200 -0
- package/src/wiki/plan.js +214 -0
- package/src/wiki/queries.js +360 -0
- package/src/wiki/render.js +187 -0
- package/src/wiki/scan.js +371 -0
- package/src/wiki/t0.js +463 -0
- package/src/wiki/t1.js +496 -0
- package/src/write-admission.js +5 -3
- package/test/api-routes-gaps.test.js +131 -1
- package/test/api.test.js +9 -1
- package/test/bootstrap.test.js +8 -0
- package/test/choro-command.test.js +218 -0
- package/test/client.test.js +802 -21
- package/test/config-keys-routes.test.js +233 -0
- package/test/config.test.js +79 -0
- package/test/contrast.test.js +14 -7
- package/test/document.test.js +32 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/inject.test.js +51 -0
- package/test/llm-audit.test.js +29 -1
- package/test/persona-settings.test.js +31 -0
- package/test/profile-facts.test.js +492 -0
- package/test/quality-backfill.test.js +290 -0
- package/test/quality-filter.test.js +113 -4
- package/test/service.test.js +1 -1
- package/test/session-gate.test.js +1 -1
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/wiki-api.test.js +273 -0
- package/test/wiki-inject.test.js +265 -0
- package/test/wiki-partition.test.js +112 -0
- package/test/wiki-plan.test.js +80 -0
- package/test/wiki-queries.test.js +270 -0
- package/test/wiki-scan.test.js +208 -0
- package/test/wiki-store.test.js +190 -0
- package/test/wiki-t0.test.js +380 -0
- package/test/wiki-t1.test.js +235 -0
- package/test/wiki-tools.test.js +176 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// dsh-mneme/src/choro-command.js
|
|
2
|
+
// /choro:仓库地志的只读回执(M3,spec §10)。
|
|
3
|
+
//
|
|
4
|
+
// 与 pipeline-command.js 的区别:那边持有状态(开窗 / 结账),这边**什么都不写**。
|
|
5
|
+
// spec §2 D6 定的是「只手动触发,过期只提示」——重建、富化、切档位全都不在这条
|
|
6
|
+
// 命令里,它只回答三个问题:当前仓库登记过地志吗?基线还一致吗?提纲长什么样?
|
|
7
|
+
import { gitHead } from "./wiki/t0.js";
|
|
8
|
+
import { renderChoroBlock } from "./wiki/inject.js";
|
|
9
|
+
import { STR } from "./lang.js";
|
|
10
|
+
|
|
11
|
+
export const CHORO_COMMAND = "choro";
|
|
12
|
+
|
|
13
|
+
/** 短 sha:回执里只用于人眼比对,给全 40 位纯属噪音。 */
|
|
14
|
+
function shortSha(sha) {
|
|
15
|
+
const text = String(sha ?? "").trim();
|
|
16
|
+
return text.length > 7 ? text.slice(0, 7) : text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 生成时间只留日期(与注入块同口径:分钟级精度在这块文本里没有信息量)。 */
|
|
20
|
+
function dateOnly(iso) {
|
|
21
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(String(iso ?? ""));
|
|
22
|
+
return m ? m[1] : String(iso ?? "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 过期判定:地志落库时的 git 基线 vs 当前 HEAD。
|
|
27
|
+
*
|
|
28
|
+
* **只在这里做**——注入块是每轮都要渲染的热路径,fork 一次 git 在 Windows 上
|
|
29
|
+
* 几十毫秒起步(P29)。这里是用户主动敲的,付得起一次 rev-parse。
|
|
30
|
+
* 三种结局分开报:一致 / 不一致 / 读不到 git——不能把「读不到」说成「没过期」。
|
|
31
|
+
*/
|
|
32
|
+
function staleness(repo) {
|
|
33
|
+
const baseline = String(repo.git_baseline ?? "").trim();
|
|
34
|
+
const head = gitHead(repo.root_path);
|
|
35
|
+
if (!head) return { state: "no-git", baseline, head: null };
|
|
36
|
+
if (baseline && baseline === head) return { state: "fresh", baseline, head };
|
|
37
|
+
return { state: "stale", baseline, head };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 处理函数。返回 CommandResult 形状 { kind, text }——**只作 UI 回执,不进模型
|
|
42
|
+
* 上下文**(同 pipeline-command.js 的契约注释);要模型看提纲,走开局注入那条路。
|
|
43
|
+
*
|
|
44
|
+
* @param {object} opts.choro createChoroQueries() 的产物
|
|
45
|
+
* @param {object} opts.config 合并后的配置
|
|
46
|
+
* @param {() => string} [opts.language]
|
|
47
|
+
*/
|
|
48
|
+
export function createChoroHandler({ choro, config, language = () => "zh" } = {}) {
|
|
49
|
+
return (invocation) => {
|
|
50
|
+
const lang = language() === "en" ? "en" : "zh";
|
|
51
|
+
const S = STR.choroCommandUsage[lang];
|
|
52
|
+
try {
|
|
53
|
+
// 闸只认 chorographyEnabled,不认注入档位:resolveInjectTier 在轻量档下也返回
|
|
54
|
+
// "off",用它当闸会让 lightMode 的用户敲 /choro 时听到「没开,去设置里打开」
|
|
55
|
+
// ——而设置里明明是开着的(档位只管注入,不管这条命令能不能看提纲)。
|
|
56
|
+
if (!config || config.chorographyEnabled !== true) return { kind: "error", text: S.off };
|
|
57
|
+
const raw = invocation?.agent?.session?.header?.cwd ?? null;
|
|
58
|
+
const cwd = typeof raw === "string" ? raw.trim() : "";
|
|
59
|
+
if (!cwd) return { kind: "error", text: S.noCwd };
|
|
60
|
+
const repo = choro ? choro.repoByCwd(cwd) : null;
|
|
61
|
+
if (!repo) return { kind: "error", text: S.noRepo(cwd) };
|
|
62
|
+
const outline = choro.outline(repo.id);
|
|
63
|
+
if (!outline || outline.modules.length === 0) {
|
|
64
|
+
return { kind: "success", text: S.title(repo.name, 0, dateOnly(repo.updated_at)) + "\n" + S.empty };
|
|
65
|
+
}
|
|
66
|
+
const lines = [S.title(repo.name, outline.modules.length, dateOnly(outline.generatedAt))];
|
|
67
|
+
// 命令里的清单不设 light 的 6 行上限:人主动敲了命令就是要看全表。
|
|
68
|
+
// 但 full 档的字符上限仍由 renderChoroBlock 自己兜——回执是 UI 里一个文本框,
|
|
69
|
+
// 贴爆它同样是失败(上限不在这里判,两处各判一次迟早对不上)。
|
|
70
|
+
const block = renderChoroBlock(outline, "full", lang);
|
|
71
|
+
if (block) lines.push(block.split("\n").slice(1).join("\n"));
|
|
72
|
+
// 截断语要说「还剩几个」,不是「总共几个」:总表格行数减去真列出来的行数才是
|
|
73
|
+
// 读者没看到的量。传总数会让人以为「另有 38 个」而实际只漏了 3 个。
|
|
74
|
+
const listed = block ? block.split("\n").filter((line) => line.startsWith("- ")).length : 0;
|
|
75
|
+
const rest = Math.max(0, outline.modules.length - listed);
|
|
76
|
+
if (rest > 0) lines.push(S.truncated(rest));
|
|
77
|
+
const head = staleness(repo);
|
|
78
|
+
if (head.state === "fresh") lines.push(S.fresh(shortSha(head.head)));
|
|
79
|
+
else if (head.state === "stale") lines.push(S.stale(shortSha(head.baseline), shortSha(head.head)));
|
|
80
|
+
else lines.push(S.noGit);
|
|
81
|
+
lines.push(S.hint);
|
|
82
|
+
return { kind: "success", text: lines.filter(Boolean).join("\n") };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// 回执层永不抛进宿主(同 pipeline-command.js):把原因写进文本就够了。
|
|
85
|
+
return { kind: "error", text: String(error?.message ?? error) };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 注册。返回 disposer 或 null(宿主没有命令注册表时静默返回 null,不抛)。
|
|
92
|
+
* 形状与 registerMoRunCommand 一致——两条内置命令只有「做什么」不同。
|
|
93
|
+
*/
|
|
94
|
+
export function registerChoroCommand({ ctx, choro, config, enabled, language = () => "zh" }) {
|
|
95
|
+
if (enabled !== true) return null;
|
|
96
|
+
if (typeof ctx?.commands?.register !== "function") return null;
|
|
97
|
+
const lang = language() === "en" ? "en" : "zh";
|
|
98
|
+
return ctx.commands.register({
|
|
99
|
+
name: CHORO_COMMAND,
|
|
100
|
+
description: STR.choroCommandDescription[lang],
|
|
101
|
+
input: { hint: STR.choroCommandInputHint[lang] },
|
|
102
|
+
handler: createChoroHandler({ choro, config, language })
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// 配置键覆盖面的「唯一一份」描述(issue #386 第 7 条)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独一个模块:面板上能改的键(settings.js 白名单)与 schema 里实际存在的
|
|
4
|
+
// 键(config.js 的 Config)是两张表,它们从来没被摆在同一个地方对过。于是
|
|
5
|
+
// 「用户能理解、但面板改不到」的键只能靠读源码 + 手数得到答案——issue #386 列了
|
|
6
|
+
// 七组、维护者另外复述了一遍,每次都要重新数。
|
|
7
|
+
//
|
|
8
|
+
// 这里只做「算」:把 schema 声明的叶子键与白名单取交/差,回三份清单。它不读
|
|
9
|
+
// 数据库、不碰 cfg 的实际值、**不返回任何键的值**——apiToken 这类敏感键的名字可以
|
|
10
|
+
// 出现在「仅配置文件」清单里(那正是这条 issue 想让用户看见的东西),值一个字节
|
|
11
|
+
// 都不出。判定口径住在 flagKeysFromSpec,api.js 的 featureSnapshot 用的也是它;
|
|
12
|
+
// 谁要是另抄一份白名单,test/config-keys-routes.test.js 的静态锁直接转红。
|
|
13
|
+
|
|
14
|
+
import { Config } from "./config.js";
|
|
15
|
+
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 白名单里的全部键(面板 PUT 能写、effective 会算的那些)。
|
|
19
|
+
*
|
|
20
|
+
* 顺序与 api.js 的 featureSnapshot 逐字对应:booleans → ints → numbers → strings
|
|
21
|
+
* → urls → enums。api.js 直接引这一份,不再自己拼一遍。
|
|
22
|
+
*/
|
|
23
|
+
export function flagKeysFromSpec() {
|
|
24
|
+
return [
|
|
25
|
+
...FEATURE_FLAG_SPEC.booleans,
|
|
26
|
+
...Object.keys(FEATURE_FLAG_SPEC.ints),
|
|
27
|
+
...Object.keys(FEATURE_FLAG_SPEC.numbers),
|
|
28
|
+
...FEATURE_FLAG_SPEC.strings,
|
|
29
|
+
...FEATURE_FLAG_SPEC.urls,
|
|
30
|
+
...Object.keys(FEATURE_FLAG_SPEC.enums)
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把「已解析的配置对象」摊平成叶子键清单。
|
|
36
|
+
*
|
|
37
|
+
* 这份清单的含义是**「有默认值因此会出现在 effective 里的键」**,不是「schema 声明
|
|
38
|
+
* 了哪些键」——两者不等。`Config({})` 解析后 `apiToken` 根本不存在(`z.string()`
|
|
39
|
+
* 没有 `.default()`),若拿它当 schema 清单,最该被用户看见的那个键反而消失了
|
|
40
|
+
* (第一版就是这么错的,被 `config-keys returns key names but never the apiToken
|
|
41
|
+
* value` 那条用例抓住)。
|
|
42
|
+
*
|
|
43
|
+
* 数组当叶子(`sessionGateSessions` 是 `z.array`,面板没有编辑器,整条替换不算
|
|
44
|
+
* 「改一个值」)。
|
|
45
|
+
*/
|
|
46
|
+
export function defaultedLeafKeys(instance = Config({})) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const walk = (node, prefix) => {
|
|
49
|
+
for (const key of Object.keys(node)) {
|
|
50
|
+
const value = node[key];
|
|
51
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
52
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) walk(value, path);
|
|
53
|
+
else out.push(path);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(instance, "");
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* schema **声明**的全部叶子键(含没有默认值的那些,比如 apiToken)。
|
|
62
|
+
*
|
|
63
|
+
* 直接走 schema 的 ref 图而不是解析结果:`Config.dict` 里每一项都是
|
|
64
|
+
* `{ uid, refs }`,真实节点在 `refs[uid]`。三种形状要分开处理:
|
|
65
|
+
* - `object`:有 `dict`,递归进去;
|
|
66
|
+
* - `dict`:键是开放的(`heatTypeDecay` 就是),只能靠 `meta.default` 的对象键
|
|
67
|
+
* 把已知的那几个名字还原出来;没有默认值就整条当一个叶子;
|
|
68
|
+
* - 其它:叶子。
|
|
69
|
+
*
|
|
70
|
+
* 探针实测:声明叶子 196 个、解析结果 191 个、白名单 113 个。两边差的那五个是
|
|
71
|
+
* `dreamProvider` / `dreamModel` / `dreamReasoningEffort` / `apiToken` /
|
|
72
|
+
* `sleepReasoningEffort`——都只声明、没给默认值。
|
|
73
|
+
*/
|
|
74
|
+
export function schemaLeafKeys(schema = Config) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const resolve = (node) => (node && node.refs && node.uid in node.refs ? node.refs[node.uid] : node);
|
|
77
|
+
const walk = (node, prefix) => {
|
|
78
|
+
const real = resolve(node);
|
|
79
|
+
if (!real) return;
|
|
80
|
+
if (real.type === "object" && real.dict) {
|
|
81
|
+
for (const key of Object.keys(real.dict)) walk(real.dict[key], prefix ? `${prefix}.${key}` : key);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (real.type === "dict") {
|
|
85
|
+
const def = real.meta && real.meta.default;
|
|
86
|
+
if (def && typeof def === "object" && !Array.isArray(def)) {
|
|
87
|
+
for (const key of Object.keys(def)) out.push(`${prefix}.${key}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out.push(prefix);
|
|
92
|
+
};
|
|
93
|
+
if (schema && schema.dict) {
|
|
94
|
+
for (const key of Object.keys(schema.dict)) walk(schema.dict[key], key);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 三份清单 + 计数。#386 第 7 条要的答案全在这里。
|
|
101
|
+
*
|
|
102
|
+
* - `panelEditable`:schema 声明了、且白名单也登记了的键(面板能改)。
|
|
103
|
+
* - `fileOnly`:schema 声明了、面板改不到的键——issue 正文点名的那七组都在里面,
|
|
104
|
+
* 连同 `apiToken`、`memoryDir` 这类「知道它存在就够了」的键。
|
|
105
|
+
* - `undefaulted`:白名单登记了、schema 也有,**但没有 `.default()`**,所以
|
|
106
|
+
* `effective` 里根本不会出现它。**这是本轮读码发现的一处真实不对称**:
|
|
107
|
+
* `dreamProvider` / `dreamModel` 在 settings.js 的 STRINGS 表里,config.js 那边
|
|
108
|
+
* 只有 `z.string().description(...)` 而没有 `.default()`,于是面板有输入框、保存
|
|
109
|
+
* 也能存,但 `featureSnapshot()` 的 `configFlagValue` 取到 undefined 就不写进
|
|
110
|
+
* effective(api.js 的 `if (value !== undefined)`)。列出来而不擅自补默认值:
|
|
111
|
+
* 补默认值会改变「未配置时到底用哪个模型」的既有行为,那是另一个决定。
|
|
112
|
+
*
|
|
113
|
+
* 三份互不相交,并集 = 白名单 ∪(声明里不在白名单的那批)——探针实测白名单
|
|
114
|
+
* 113 个全部都在声明里,所以并集就是声明的 196 个,一个不漏也不重复。
|
|
115
|
+
*/
|
|
116
|
+
export function describeConfigKeys() {
|
|
117
|
+
const declared = schemaLeafKeys();
|
|
118
|
+
const declaredSet = new Set(declared);
|
|
119
|
+
const defaulted = new Set(defaultedLeafKeys());
|
|
120
|
+
const flags = flagKeysFromSpec();
|
|
121
|
+
const flagSet = new Set(flags);
|
|
122
|
+
|
|
123
|
+
const panelEditable = [];
|
|
124
|
+
const undefaulted = [];
|
|
125
|
+
for (const key of flags) {
|
|
126
|
+
if (!declaredSet.has(key)) continue;
|
|
127
|
+
if (defaulted.has(key)) panelEditable.push(key);
|
|
128
|
+
else undefaulted.push(key);
|
|
129
|
+
}
|
|
130
|
+
const fileOnly = declared.filter((key) => !flagSet.has(key));
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
counts: {
|
|
134
|
+
schemaLeaf: declared.length,
|
|
135
|
+
flag: flags.length,
|
|
136
|
+
panelEditable: panelEditable.length,
|
|
137
|
+
fileOnly: fileOnly.length,
|
|
138
|
+
undefaulted: undefaulted.length
|
|
139
|
+
},
|
|
140
|
+
panelEditable,
|
|
141
|
+
fileOnly,
|
|
142
|
+
undefaulted
|
|
143
|
+
};
|
|
144
|
+
}
|
package/src/config.js
CHANGED
|
@@ -130,6 +130,15 @@ export const Config = z.object({
|
|
|
130
130
|
// 关闭时行为与之前完全一致。
|
|
131
131
|
codingRetrospect: z.boolean().default(false),
|
|
132
132
|
// 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
|
|
133
|
+
// 后一组是「工程协作动作」词:合并/开 PR/看 CI/发版/验收/冲突/分支/
|
|
134
|
+
// issue 这类话在原词表里一个都不命中,于是被判成非编码任务,编码记忆
|
|
135
|
+
// 整类被 codingGate 滤掉——生产日志里这类 query 占非编码调用的 17.1~17.8%。
|
|
136
|
+
// 只加中文:isCodingTask 是纯子串匹配,英文短词(pr/ci/commit)会命中
|
|
137
|
+
// prompt / GitHub CLI 路径这类文本,无词边界时误命中率达 57.9%。
|
|
138
|
+
// 「评审」与「审评」并列保留,不是笔误:她真实说「等审评」「看CI和审评」
|
|
139
|
+
// (非编码池内审评 9 条 / 717 次,评审 5 条 / 421 次,审评反而更多),
|
|
140
|
+
// 同义不同形,子串匹配必须两套都放,删掉任一个都会漏掉一类真实说法。
|
|
141
|
+
// 刻意不加的:事务(唯一命中是政治学「公共事务」简答)、依赖包(依赖已在表内)。
|
|
133
142
|
codingKeywords: z.array(z.string()).default([
|
|
134
143
|
"代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
|
|
135
144
|
"接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
|
|
@@ -138,7 +147,12 @@ export const Config = z.object({
|
|
|
138
147
|
"测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
|
|
139
148
|
"命令行", "shell", "配置", "配置文件", "yaml", "json",
|
|
140
149
|
"插件", "开发", "编译", "构建", "部署", "git", "commit",
|
|
141
|
-
"review", "前端", "后端", "页面", "组件", "dsh", "memos"
|
|
150
|
+
"review", "前端", "后端", "页面", "组件", "dsh", "memos",
|
|
151
|
+
"提交", "合并", "分支", "冲突", "验收", "审查", "评审", "审评", "发版",
|
|
152
|
+
"仓库", "回滚", "推送", "拉取", "变基", "上线", "打包", "迁移",
|
|
153
|
+
"字段", "鉴权", "超时", "并发", "闪退", "白屏", "端口", "进程",
|
|
154
|
+
"守护", "定时任务", "索引", "缓存", "死锁", "内存泄漏", "日志",
|
|
155
|
+
"版本号", "环境变量"
|
|
142
156
|
]),
|
|
143
157
|
// 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
|
|
144
158
|
// 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
|
|
@@ -696,6 +710,10 @@ export const Config = z.object({
|
|
|
696
710
|
// injection sort re-ranks by importance * quality_score/100 (degraded)
|
|
697
711
|
// score < 30 → archived + tagged low_quality (still explicitly searchable)
|
|
698
712
|
// Meta-memory markers, near-duplicates and repetitive filler lose points.
|
|
713
|
+
// "Repetitive" is trigram repetition > 0.7 (this change). The shipped v0.7.12
|
|
714
|
+
// measure was unique-chars/total, which decays with length: it flagged 94% of
|
|
715
|
+
// 800-2000 char memories and 100% of longer ones, i.e. long Chinese prose was
|
|
716
|
+
// punished for being long. Threshold rationale lives in quality-filter.js.
|
|
699
717
|
memoryQualityFilter: z.object({
|
|
700
718
|
enabled: z.boolean().default(true),
|
|
701
719
|
archiveThreshold: z.natural().min(1).max(100).default(30),
|
|
@@ -888,6 +906,30 @@ export const Config = z.object({
|
|
|
888
906
|
// 「用户开关 > 轻量预设 > bundle」,同 continuityRescueEnabled / distillRecords.enabled)。
|
|
889
907
|
organizeEnabled: z.boolean().default(false),
|
|
890
908
|
|
|
909
|
+
// chorography(仓库地志,spec: docs/specs/2026-11-04-chorography-design.md)的
|
|
910
|
+
// 暴露闸(opt-in,默认关):管模型侧三个只读工具(choro_search / read_page /
|
|
911
|
+
// module_map)、面板浏览页,以及 M3 的开局注入。工具描述常驻模型上下文 = 新增
|
|
912
|
+
// 成本,按仓库纪律 opt-in;默认关=工具不存在、面板页空态、注入块不渲染,行为与
|
|
913
|
+
// 0.10.1 逐字节一致。
|
|
914
|
+
//
|
|
915
|
+
// 本键仍**不进 LIGHT_MODE_OFF**,但理由收窄成一句:它是「是否启用这个子系统」,
|
|
916
|
+
// 不是「每轮付多少」。轻量档要付的是下面那条 chorographyInject(把档位压成
|
|
917
|
+
// off)——两层分开,用户才能在轻量档里保留工具、只关掉每轮那块文本。
|
|
918
|
+
chorographyEnabled: z.boolean().default(false),
|
|
919
|
+
|
|
920
|
+
// 开局注入档位(M3,spec §7):off / light / full,默认 light。
|
|
921
|
+
// light ≤8 行(模块名 + 文件数 + 页状态 + 页路径,按「还有 N 个」明说截断);
|
|
922
|
+
// full 列全部模块;off 不注入但工具照旧可用。
|
|
923
|
+
// 为什么是枚举而不是布尔:spec 原文就是三档,「开了但只给 6 行」与「全给」是两种
|
|
924
|
+
// 用法,布尔键表达不了;跟随 chorographyEnabled 隐式开又会丢掉「留工具、去文本」
|
|
925
|
+
// 这个组合。
|
|
926
|
+
// 为什么写作 z.union([z.const(...)]) 而不是 z.enum:与 summarizeDedupeMode 同款,
|
|
927
|
+
// 全仓枚举键统一这一种写法(z.enum 的报错文案与既有键不一致)。
|
|
928
|
+
// 轻量档**不进 LIGHT_MODE_OFF**:那张表对平铺键一律赋 false,枚举键进去会得到与
|
|
929
|
+
// schema 不符的 false,而 false !== "off" 时注入照样渲染——静默失效比不关更糟。
|
|
930
|
+
// 轻量档的压制改在消费侧(src/wiki/inject.js 的 resolveInjectTier 判 lightMode)。
|
|
931
|
+
chorographyInject: z.union([z.const("off"), z.const("light"), z.const("full")]).default("light"),
|
|
932
|
+
|
|
891
933
|
// --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
|
|
892
934
|
// 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
|
|
893
935
|
// 逐字节不变。开启后**两条写入路径都自动盖章**(*_scope_source='auto'):
|
|
@@ -994,6 +1036,19 @@ export const Config = z.object({
|
|
|
994
1036
|
// preInjectGate.enabled 的定性);是预设不是强制,用户显式勾选仍然赢。
|
|
995
1037
|
profileExtractEnabled: z.boolean().default(false),
|
|
996
1038
|
|
|
1039
|
+
// 「Agent 眼中的你」自动事实(2026-11-06,对标 ChatGPT 记忆 / DS Bot 每 bot 的
|
|
1040
|
+
// MEMORY.md):dream 巩固轮成功后按会话身份分卡增量维护一份逐条事实清单,
|
|
1041
|
+
// 注入在手填画像之后(src/user-settings.js 的渲染顺序),面板可手动触发与逐条
|
|
1042
|
+
// 修正(POST /api/dsh-mneme/profile-facts/*)。与 profileExtractEnabled 的三层
|
|
1043
|
+
// 理由同源:① 自动轮是 dream 级别的 LLM 调用 + 读上百行记忆,不该在没人要求时
|
|
1044
|
+
// 挂在面上;② 产物每帧注入,编造的代价不对称(维护 prompt 的三段硬约束与
|
|
1045
|
+
// profileExtract 同源);③ 它**不写** user_settings——手填画像神圣(#497 口径
|
|
1046
|
+
// 不变),事实住独立的 profile_facts 表,可被人在面板上逐条修正。
|
|
1047
|
+
// 进 LIGHT_MODE_OFF:自动维护轮与低资源档互斥(同 organizeEnabled 的定性);
|
|
1048
|
+
// 是预设不是强制,用户显式勾选仍然赢。注入段本身极轻(几十行文本),不额外
|
|
1049
|
+
// 占用轻量档预算的考量见键注释末句。
|
|
1050
|
+
profileFactsEnabled: z.boolean().default(false),
|
|
1051
|
+
|
|
997
1052
|
// #539 逐会话无痕:总开关默认关,保持旧用户行为;子代理选择器默认开,但总开关
|
|
998
1053
|
// 未开时不生效。手动名单是精确的 session id 匹配。
|
|
999
1054
|
sessionGateEnabled: z.boolean().default(false),
|
|
@@ -1035,6 +1090,10 @@ const LIGHT_MODE_OFF = [
|
|
|
1035
1090
|
// 要求 agent 拿住上一轮的 run_id 再回来,弱模型漏这一环就是一次被拒的调用;
|
|
1036
1091
|
// 预设关、不剥夺显式开启的权利(合并顺序同 continuityRescueEnabled)。
|
|
1037
1092
|
"organizeEnabled",
|
|
1093
|
+
// 轻量档不开画像事实自动维护轮(2026-11-06):dream 后的逐卡 LLM 调用 + 读上百行
|
|
1094
|
+
// 记忆,与低资源档互斥(同 organizeEnabled 的定性)。注入段本身是几十行常驻文本,
|
|
1095
|
+
// 关掉自动轮不省注入成本、省的是维护调用——这正是该进这张表的路径。
|
|
1096
|
+
"profileFactsEnabled",
|
|
1038
1097
|
// 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
|
|
1039
1098
|
"heatEnabled",
|
|
1040
1099
|
// 轻量模式不开注入前判定(#380:LLM-per-turn 附加路径,每帧注入多一次池级
|
package/src/document.js
CHANGED
|
@@ -276,8 +276,9 @@ export function createDocumentRegistrar({ store, config, embedQuery, pushContent
|
|
|
276
276
|
}
|
|
277
277
|
// #275 拍板 5(#230 验收口径的补丁):升格吸收的 evidence 行随之退出活跃面——
|
|
278
278
|
// 一次吸收 20 条,库里就是「21 行不是 1 行」。四条口径:①与本行同一事务(要么
|
|
279
|
-
// 都成、要么都不成);②pinned
|
|
280
|
-
//
|
|
279
|
+
// 都成、要么都不成);②pinned 永不自动归档——升格不能绕过它,而 pinned 有**两个**
|
|
280
|
+
// 判据:显式钉住的行(pinned_memories,#593 起为唯一正源)与 #249 的逐字保真池
|
|
281
|
+
// 类型(constraint / preference);③自有生命周期的类型(document / summary,见
|
|
281
282
|
// ABSORB_EXEMPT_TYPES)不吸收,吸收的是原子条;④只翻标志位、内容与审计全留
|
|
282
283
|
// (可恢复),opt-out 走 archiveEvidence。
|
|
283
284
|
// pinnedTypes 拿不到就不做这一步:宁可少做,也不能把保真池当普通行收走。
|
|
@@ -294,7 +295,12 @@ export function createDocumentRegistrar({ store, config, embedQuery, pushContent
|
|
|
294
295
|
const row = store.getById(id);
|
|
295
296
|
if (!row || row.archived || row.forgotten) continue;
|
|
296
297
|
if (isImportedRow(row)) { evidenceSkipped += 1; continue; }
|
|
297
|
-
|
|
298
|
+
// #593:显式 pin(pinned_memories)与类型保真池是**两个**判据。上一版只问
|
|
299
|
+
// pinnedTypes.has(row.type),等于把「被钉住的 pattern / decision」当普通行
|
|
300
|
+
// 收走——用户钉过的东西在文档升格时被静默归档,正是「钉着 ≠ 会注入」的
|
|
301
|
+
// 反面:这里连「钉着」都没保住。store.isPinned 缺席时退回类型口径(旧行为)。
|
|
302
|
+
const explicitlyPinned = typeof store.isPinned === "function" && store.isPinned(row.id);
|
|
303
|
+
if (explicitlyPinned || pinnedTypes.has(row.type) || ABSORB_EXEMPT_TYPES.has(row.type)) continue;
|
|
298
304
|
store.setArchived(id, true);
|
|
299
305
|
evidenceArchived += 1;
|
|
300
306
|
}
|
package/src/dream/clustering.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Cosine similarity of two equal-length numeric vectors. Returns 0 for empty
|
|
2
2
|
// or mismatched-length inputs, and for zero-norm vectors (numerically stable).
|
|
3
|
+
// Accepts typed arrays (Float32Array) as well as plain arrays: store.js caches
|
|
4
|
+
// vectors as Float32Array and `Array.isArray` is false for typed arrays — guarding
|
|
5
|
+
// on it alone would silently score every cached vector as 0.
|
|
6
|
+
const isVec = (v) => Array.isArray(v) || (ArrayBuffer.isView(v) && !(v instanceof DataView));
|
|
7
|
+
|
|
3
8
|
export function cosineSimilarity(a, b) {
|
|
4
|
-
if (!
|
|
9
|
+
if (!isVec(a) || !isVec(b) || a.length === 0 || a.length !== b.length) return 0;
|
|
5
10
|
let dot = 0, na = 0, nb = 0;
|
|
6
11
|
for (let i = 0; i < a.length; i++) {
|
|
7
12
|
dot += a[i] * b[i];
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,15 @@ import { createApi } from "./api.js";
|
|
|
21
21
|
import { createStandaloneApi } from "./api-standalone.js";
|
|
22
22
|
// #275 存储生命周期第一批:无损回收(手动入口,不挂启动路径)。
|
|
23
23
|
import { createMaintenance } from "./maintenance.js";
|
|
24
|
+
import { createChoroQueries } from "./wiki/queries.js";
|
|
25
|
+
// chorography M3:开局注入块与 /choro 只读回执。两者都只吃 queries 读侧,不写任何东西。
|
|
26
|
+
import { createChoroInject } from "./wiki/inject.js";
|
|
27
|
+
import { registerChoroCommand } from "./choro-command.js";
|
|
28
|
+
// 收尾档:面板重建按钮走 runChoroT0(P34;T1 不上 HTTP)。
|
|
29
|
+
import { runChoroT0 } from "./wiki/t0.js";
|
|
24
30
|
import { createSettings } from "./settings.js";
|
|
31
|
+
// 「Agent 眼中的你」自动事实(2026-11-06):逐卡事实清单 + dream 后自动维护轮。
|
|
32
|
+
import { createProfileFacts } from "./profile-facts.js";
|
|
25
33
|
import { createCommandManager } from "./commands.js";
|
|
26
34
|
import { createExecutionRecorder } from "./pipeline-runner.js";
|
|
27
35
|
import { registerMoRunCommand } from "./pipeline-command.js";
|
|
@@ -152,7 +160,10 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, serv
|
|
|
152
160
|
return undefined;
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
|
-
writeAudit
|
|
163
|
+
// 第三参必须显式传进去:writeAudit 的形参默认值 [] 是给"没有 id 可归因"
|
|
164
|
+
// 留的,省略第三参等于宣称"这条成功与任何记忆无关"——实测活库 6256 条
|
|
165
|
+
// success 行 100% 空数组,正是这个省略造成的。#405 只修了 error 半边。
|
|
166
|
+
writeAudit("success", null, relatedMemoryIds);
|
|
156
167
|
return text;
|
|
157
168
|
})().catch((err) => {
|
|
158
169
|
logger?.warn?.(`[dsh-mneme] entity extraction llm stream failed: ${String(err)}`);
|
|
@@ -430,6 +441,13 @@ export const apply = (ctx, config) => {
|
|
|
430
441
|
}
|
|
431
442
|
}
|
|
432
443
|
|
|
444
|
+
// 「Agent 眼中的你」自动事实(2026-11-06,opt-in:profileFactsEnabled):dream 巩固
|
|
445
|
+
// 轮成功后的逐卡维护 + 面板手动轮(api.js)+ 注入事实段(factsProvider 传注入器与
|
|
446
|
+
// 预览路由)。装配与开关无关——闸在消费侧(注入渲染 / 自动轮判空 / 路由 409),
|
|
447
|
+
// 与 profileExtractor 同款:503 只该指装配缺失,不该被读成「用户没开」。
|
|
448
|
+
// 位置必须在 dream 调度器之前:onRun 的 fire-and-forget 收尾要引用它。
|
|
449
|
+
const profileFacts = createProfileFacts({ ctx, service, store, config: cfg, logger: ctx.logger });
|
|
450
|
+
|
|
433
451
|
// Dream scheduler: automatic consolidation + summary runs, triggered by
|
|
434
452
|
// store growth. Writes through the service fire the dream hook, which asks
|
|
435
453
|
// the scheduler to (re)schedule a run once absolute and since-last-run
|
|
@@ -479,7 +497,26 @@ export const apply = (ctx, config) => {
|
|
|
479
497
|
metadata: JSON.stringify({ count, chars })
|
|
480
498
|
});
|
|
481
499
|
},
|
|
482
|
-
onRun: () =>
|
|
500
|
+
onRun: () => {
|
|
501
|
+
const run = dream
|
|
502
|
+
? dream.runDream(ctx, service, cfg)
|
|
503
|
+
: Promise.resolve({ ok: true, skipped: true });
|
|
504
|
+
// 画像事实自动维护:挂在巩固轮之后 fire-and-forget。刻意不并进 onRun 的
|
|
505
|
+
// promise——profileFacts 的失败不能被计入 dream 的失败退避
|
|
506
|
+
// (autoDreamFailureBackoff 的口径是巩固轮自身);逐卡经 service.enqueue
|
|
507
|
+
// 串行,不与 sleep/下一轮巩固抢模型。单卡失败只 warn,账本已有 failed 回执。
|
|
508
|
+
run.then(async (rep) => {
|
|
509
|
+
if (cfg.profileFactsEnabled !== true || (rep && rep.ok === false)) return;
|
|
510
|
+
for (const key of profileFacts.autoPassCandidates()) {
|
|
511
|
+
try {
|
|
512
|
+
await service.enqueue(() => profileFacts.runFor(key, { trigger: "auto" }));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
ctx.logger?.warn?.(`[dsh-mneme] profile-facts auto pass failed for card "${key || "default"}": ${error?.message ?? error}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}).catch(() => { /* dream 自身失败走调度器退避,这里不做第二遍日志 */ });
|
|
518
|
+
return run;
|
|
519
|
+
}
|
|
483
520
|
});
|
|
484
521
|
service.setDreamHook(() => dream.maybeSchedule(service));
|
|
485
522
|
}
|
|
@@ -560,8 +597,26 @@ export const apply = (ctx, config) => {
|
|
|
560
597
|
// a torn-down context.
|
|
561
598
|
disposers.push(() => semantic.dispose()); // #118 重试计时器 + boot 回填计时器(搬入 semantic.js 后由它自持)
|
|
562
599
|
|
|
600
|
+
// chorography M3:地志提纲折进 memory context(不新开 context——宿主侧同 order 无
|
|
601
|
+
// 兜底排序,多一块就多一份拼装抖动;spec §7 也要求「合并成一次拼装」)。档位由
|
|
602
|
+
// chorographyInject 决定,off / 未匹配到仓库 / 渲染失败一律返回空串,宿主会过滤
|
|
603
|
+
// 掉空文本,整块消失且不占位。
|
|
604
|
+
//
|
|
605
|
+
// 预算是 0:注入块只在 memory context 里多几行,本身不做检索、不 fork git——
|
|
606
|
+
// 真过期判定留给 /choro(P29)。所以这里不需要额外的「热路径」闸,chorographyEnabled
|
|
607
|
+
// 与 chorographyInject 两个开关在 resolveInjectTier 里已经判完。
|
|
608
|
+
const choroInject = createChoroInject({
|
|
609
|
+
choro: createChoroQueries({ store }),
|
|
610
|
+
config: cfg,
|
|
611
|
+
logger: ctx.logger,
|
|
612
|
+
language: () => langOf(cfg)
|
|
613
|
+
});
|
|
614
|
+
disposers.push(() => choroInject.dispose());
|
|
615
|
+
|
|
563
616
|
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
564
|
-
if (cfg.autoInject)
|
|
617
|
+
if (cfg.autoInject) {
|
|
618
|
+
disposers.push(createInjector(promptCtx, service, settings, cfg, preInjectGate, sessionGate, choroInject, profileFacts.factsProvider));
|
|
619
|
+
}
|
|
565
620
|
});
|
|
566
621
|
|
|
567
622
|
// #249 N3:压缩边缘双落点。触发靠宿主自己落的压缩事件(订阅 + pre-step 追加),
|
|
@@ -572,9 +627,27 @@ export const apply = (ctx, config) => {
|
|
|
572
627
|
}
|
|
573
628
|
|
|
574
629
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
575
|
-
|
|
630
|
+
// choro(M1):只读查询对象,chorographyEnabled 关着时工具压根不注册(闸在
|
|
631
|
+
// createTools 的 hiddenTools,与 organizeEnabled 同款反向语义)。
|
|
632
|
+
disposers.push(createTools(toolsCtx, service, cfg, embedder, sessionGate, createChoroQueries({ store })));
|
|
576
633
|
});
|
|
577
634
|
|
|
635
|
+
// chorography M3:/choro 只读回执。与 /mo-run 同形(ctx.commands.register),
|
|
636
|
+
// 但这里**不持有任何状态**、不写盘、不重建——spec §2 D6「只手动触发,过期只提示」
|
|
637
|
+
// 决定了这条命令只能看,不能改。位置必须在 disposers 声明之后(同 actionNudge 的
|
|
638
|
+
// TDZ 注释),否则插件加载时会直接抛 ReferenceError。
|
|
639
|
+
if (cfg.chorographyEnabled === true) {
|
|
640
|
+
const disposeChoro = registerChoroCommand({
|
|
641
|
+
ctx,
|
|
642
|
+
choro: createChoroQueries({ store }),
|
|
643
|
+
config: cfg,
|
|
644
|
+
enabled: true,
|
|
645
|
+
language: () => langOf(cfg)
|
|
646
|
+
});
|
|
647
|
+
if (disposeChoro) disposers.push(disposeChoro);
|
|
648
|
+
else ctx.logger?.info?.("[dsh-mneme] chorographyEnabled=true 但宿主没有命令注册表,/choro 未注册");
|
|
649
|
+
}
|
|
650
|
+
|
|
578
651
|
const summarizer = createSummarizer(ctx, service, cfg, {}, sessionGate);
|
|
579
652
|
disposers.push(summarizer.dispose);
|
|
580
653
|
|
|
@@ -599,11 +672,20 @@ export const apply = (ctx, config) => {
|
|
|
599
672
|
// 本模块一次都不会被调用)。这样「没启用」与「没装配」在 HTTP 面上是两个可区分的
|
|
600
673
|
// 答案(503 unavailable 只该出现在旧宿主/装配缺失上,不该被读成「用户没开」)。
|
|
601
674
|
const extractProfileDraft = createProfileExtractor({ ctx, service, config: cfg, logger: ctx.logger });
|
|
675
|
+
// chorography 读侧(M1):与 service 刻意分开——所有权模型相反(spec §3)。
|
|
676
|
+
// 闸在路由/工具两层(关着 409 / 不注册),queries 本体装配与开关无关。
|
|
677
|
+
// 收尾档:choro 对象加两件 host 侧能力——embedder(向量惰性物化,P37)与
|
|
678
|
+
// runT0Apply(面板重建按钮的写路径,P34;T1 不上 HTTP,CLI 仍是它的入口)。
|
|
679
|
+
const choro = {
|
|
680
|
+
...createChoroQueries({ store }),
|
|
681
|
+
embedder,
|
|
682
|
+
runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }),
|
|
683
|
+
};
|
|
602
684
|
const api = createApi(ctx, service, settings, commands ?? {
|
|
603
685
|
add: () => { throw new Error("commands unavailable"); },
|
|
604
686
|
remove: () => false,
|
|
605
687
|
list: () => []
|
|
606
|
-
}, embedder, { vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft);
|
|
688
|
+
}, embedder, { vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft, choro, profileFacts);
|
|
607
689
|
disposers.push(api.dispose);
|
|
608
690
|
}
|
|
609
691
|
|
package/src/inject.js
CHANGED
|
@@ -93,7 +93,7 @@ function extractRounds(ctx, maxRounds) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function createInjector(ctx, service, settings, config, gate = null, sessionGate = null) {
|
|
96
|
+
export function createInjector(ctx, service, settings, config, gate = null, sessionGate = null, choroInject = null, factsInject = null) {
|
|
97
97
|
const language = langOf(config);
|
|
98
98
|
const baseMaxItems = config.maxInjectedItems ?? 5;
|
|
99
99
|
const threshold = config.importanceThreshold ?? 3;
|
|
@@ -331,7 +331,9 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
331
331
|
// 一致(老用户零变化)。personaStrictAddressing 打开时,「有身份但没建卡」与
|
|
332
332
|
// 「卡被显式关掉」都不注入人格——default 只兜「这一帧解析不到身份」的会话。
|
|
333
333
|
function renderUserSettings(ctx) {
|
|
334
|
-
|
|
334
|
+
// factsInject 是 profile-facts 域的读口(index.js 接线,面板预览路由传同一份):
|
|
335
|
+
// 开关与事实有无的判定都在 renderUserSettingsBlock 内部,「关」等于零读数。
|
|
336
|
+
return renderUserSettingsBlock({ settings, config, agentId: sessionAgentId(ctx), factsProvider: factsInject }).text;
|
|
335
337
|
}
|
|
336
338
|
|
|
337
339
|
// #249 能力说明的系统提示段。order 150 = 插件指引段的既有惯例
|
|
@@ -405,9 +407,19 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
405
407
|
// #333(issue #34 恢复):时间前缀只在会话首轮出现(per-session 闩锁),
|
|
406
408
|
// 排在整个记忆块的最前——模型先知道「今天几号」,再看热上下文与长期记忆。
|
|
407
409
|
const timePrefix = renderTimePrefix(ctx);
|
|
408
|
-
//
|
|
410
|
+
// M3 仓库地志(chorography):只注标题级提纲,档位 off/light/full。
|
|
411
|
+
// cwd 用 scope.workspace_scope——那是全仓唯一的 cwd 口径(registry 反查,
|
|
412
|
+
// header.cwd 兜底),另读一份就是两张皮且重犯 #487 的 Proxy 陷阱(P27)。
|
|
413
|
+
// 取不到 cwd / 仓库没登记 / 渲染失败一律空串:本块在热路径上,不能抛。
|
|
414
|
+
const choroText = choroInject && typeof choroInject.text === "function"
|
|
415
|
+
? choroInject.text(scope?.workspace_scope ?? null)
|
|
416
|
+
: "";
|
|
417
|
+
// 拼接次序:时间 → 地志 → 热上下文 → 长期记忆(四者都可能为空,逐级判空)。
|
|
418
|
+
// 地志排在热上下文之前:它是「这个仓库由哪些模块组成」的稳定背景,
|
|
419
|
+
// 而热上下文是「刚才聊到哪」,越靠近正文越晚——稳定的在前,易变的在后。
|
|
409
420
|
const withHot = !hotText ? body : body ? `${hotText}\n\n${body}` : hotText;
|
|
410
|
-
const
|
|
421
|
+
const withChoro = !choroText ? withHot : withHot ? `${choroText}\n\n${withHot}` : choroText;
|
|
422
|
+
const finalBody = !timePrefix ? withChoro : withChoro ? `${timePrefix}\n\n${withChoro}` : timePrefix;
|
|
411
423
|
// Issue #179:旁路缓存一帧——面板「注入预览」卡据此展示构成与体积。
|
|
412
424
|
// chars 为条目内容的截断后近似值(与 render 同一函数计长),totalChars
|
|
413
425
|
// 是本次实际返回块的精确长度。maxItems 反映自适应收缩后的生效值。
|
|
@@ -424,6 +436,8 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
424
436
|
// 「注入预览」卡据此展示判定是否生效与标记数。
|
|
425
437
|
gate: gateFrame ? { state: gateFrame.state, nFlagged: gateFrame.flaggedIds ? gateFrame.flaggedIds.size : null, judged: gateFrame.judged ?? null } : null,
|
|
426
438
|
hotChars: hotText.length,
|
|
439
|
+
// M3:地志块实际占的字符数(0 = 本帧没注入),面板据此区分「关」与「没匹配到仓库」。
|
|
440
|
+
choroChars: choroText.length,
|
|
427
441
|
entries: candidates.map((m) => ({
|
|
428
442
|
id: m.id,
|
|
429
443
|
type: m.type,
|