@avantf/dsh-mem 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +97 -87
  3. package/lib/client.js +2351 -1170
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +1063 -639
  6. package/lib/interface-version.json +4 -0
  7. package/lib/types/client/index.d.ts.map +1 -1
  8. package/lib/types/client/index.js +21 -1
  9. package/lib/types/client/index.js.map +1 -1
  10. package/lib/types/data_home.d.ts +3 -0
  11. package/lib/types/data_home.d.ts.map +1 -0
  12. package/lib/types/data_home.js +26 -0
  13. package/lib/types/data_home.js.map +1 -0
  14. package/lib/types/envinit-bootstrap.d.ts +13 -3
  15. package/lib/types/envinit-bootstrap.js +14 -4
  16. package/lib/types/envinit.d.ts +11 -6
  17. package/lib/types/envinit.d.ts.map +1 -1
  18. package/lib/types/envinit.js +62 -155
  19. package/lib/types/envinit.js.map +1 -1
  20. package/lib/types/index.d.ts +33 -11
  21. package/lib/types/index.d.ts.map +1 -1
  22. package/lib/types/index.js +86 -39
  23. package/lib/types/index.js.map +1 -1
  24. package/lib/types/interface_gate.d.ts +51 -0
  25. package/lib/types/interface_gate.d.ts.map +1 -0
  26. package/lib/types/interface_gate.js +84 -0
  27. package/lib/types/interface_gate.js.map +1 -0
  28. package/lib/types/prompt.d.ts +74 -4
  29. package/lib/types/prompt.d.ts.map +1 -1
  30. package/lib/types/prompt.js +85 -2
  31. package/lib/types/prompt.js.map +1 -1
  32. package/lib/types/provision.d.ts +29 -24
  33. package/lib/types/provision.d.ts.map +1 -1
  34. package/lib/types/provision.js +3 -59
  35. package/lib/types/provision.js.map +1 -1
  36. package/lib/types/remote.d.ts +7 -3
  37. package/lib/types/remote.d.ts.map +1 -1
  38. package/lib/types/remote.js +14 -4
  39. package/lib/types/remote.js.map +1 -1
  40. package/package.json +17 -19
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { accessSync, chmodSync, constants, copyFileSync, createWriteStream, existsSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
3
- import z from "@deepseek-ai/schemastery";
4
- import { defineTool } from "@deepseek-ai/dsh-tools";
5
- import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
3
  import { basename, delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
7
- import { z as z$1 } from "zod";
4
+ import { z } from "zod";
8
5
  import { homedir } from "node:os";
6
+ import z$1 from "@deepseek-ai/schemastery";
7
+ import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { isSeq, parse, parseDocument } from "yaml";
10
10
  import { createHash } from "node:crypto";
11
11
  import { Readable } from "node:stream";
@@ -13,121 +13,16 @@ import { pipeline } from "node:stream/promises";
13
13
  import { execFileSync, spawn, spawnSync } from "node:child_process";
14
14
  import { createInflateRaw } from "node:zlib";
15
15
  import Database from "better-sqlite3";
16
- import { readFile } from "node:fs/promises";
17
16
  import { fileURLToPath, pathToFileURL } from "node:url";
18
- //#region lib/types/prompt.js
19
- /**
20
- * The two system-prompt sections this plugin contributes (DESIGN §10).
21
- *
22
- * They exist for something a tool description cannot carry: a description says what a call DOES,
23
- * never when to reach for it unprompted, and the default behaviour is to wait until asked. So each
24
- * says only WHEN its store is worth writing or reading. Neither says anything about how memory is
25
- * retained — that stays out of every model-facing string, held to the contract's
26
- * `RETENTION_VOCABULARY`, the same list the tool descriptions are held to.
27
- *
28
- * The knowledge one is the section whose ABSENCE was measured: a user had just ingested
29
- * 《Cgroup v2 技术综述》(which answers "cgroup v2 的内存保护" at score 1.0) and asked about it in a
30
- * session whose prompt named only the memory tools, so the model never looked. A tool description
31
- * cannot fix that by itself — `kb_query`'s says what the call does and even leads with 记忆 facts,
32
- * which reads as a memory tool — and the model cannot know the library is relevant to a question
33
- * without being told to check it. Hence the "先检索一次再作答" nudge, plus the explicit
34
- * "你看不到库里有什么" clause: the thing that made the miss invisible is that nothing in the
35
- * prompt acknowledged the store existed at all.
36
- *
37
- * `order` 3000/3010 are third-party slots: after the harness's per-tool guidance block (1000–2900)
38
- * and before its toolset-level guidance (5000). They are not repository-owned placements, so they
39
- * do not resolve through `getSectionOrder`.
40
- *
41
- * The sections say WHAT TO DO and never WHY. The reasoning behind each rule is measured and
42
- * lives here, in DESIGN §10 and in the CHANGELOG — not in the prompt, where it costs tokens on
43
- * every step and does not change the action.
44
- *
45
- * Kept in its own module — not in `index.ts` — so the text can be read and checked without the
46
- * plugin entry's `@deepseek-ai/*` runtime imports, which resolve only in a harness workspace.
47
- */
48
- const MEMORY_PROMPT_SECTION = {
49
- name: "avantf:memory-usage",
50
- order: 3e3,
51
- text: "记忆(`mem_remember`):把**重要且跨会话仍成立**的事实主动记下来——用户偏好、项目决策、环境约束、接口约定、踩过的坑。同一事实的修正用 `update`,一句自包含的陈述。不要记录临时聊天内容、一次性任务的中间状态、能直接从代码或文档读到的内容。"
52
- };
53
- /**
54
- * WHEN to consult the document knowledge base. See the module comment for why this is a separate
55
- * section rather than a clause inside the memory one: the two stores answer different questions,
56
- * and a session that needs the knowledge base may have no memory question at all.
57
- */
58
- const KNOWLEDGE_PROMPT_SECTION = {
59
- name: "avantf:knowledge-usage",
60
- order: 3010,
61
- text: "知识库(`kb_query` / `kb_add`)存放**用户提供的成篇资料**:文档、长说明、规范、综述等要按原文查阅的内容。回答涉及已入库资料的问题之前,先用 `kb_query` 检索;命中就按 `source_ref` 引用原文。用户给了值得长期留存的成篇内容就主动 `kb_add` 入库;一句话的事实属于记忆(`mem_remember`),不要入知识库。"
62
- };
63
- /**
64
- * HOW to change a document that is already in the library.
65
- *
66
- * A separate section because it is a different job from the one above, and because the first
67
- * section is already at 382 of its 400-character budget. It exists to prevent a measured failure:
68
- * asked to "update the knowledge base", a model called the old all-in-one tool with a NEW title and
69
- * silently created a sibling document (《… · 补遗》) while the original stayed behind — two
70
- * overlapping documents, and the reader left to reconcile them. Changing a document is a FILE EDIT,
71
- * so the section has to name the file tools and warn about the one that destroys the frontmatter.
72
- */
73
- const KB_EDIT_PROMPT_SECTION = {
74
- name: "avantf:kb-edit",
75
- order: 3020,
76
- text: "更新知识库时,要在原文上修改或追加,不要新建一个补充文档。用 `kb_list` 拿到受管 `.md` 的绝对路径,用文件工具修改。在用户明确要求时使用 `kb_remove` 删除。"
77
- };
78
- //#endregion
79
- //#region lib/types/hints.js
80
- /**
81
- * The plugin's CONDITIONAL hints: one line per store, contributed only when that store already
82
- * holds something relevant to what the user just sent (DESIGN §12).
83
- *
84
- * These are deliberately NOT part of the always-on usage sections. A usage section says WHEN a
85
- * store is worth consulting and therefore has to be paid for on every step of every session; this
86
- * says "there is something here, right now", which is only true sometimes. The two layers compose:
87
- * the section is the fallback for the paraphrase the lexical probe misses, and the hint is the
88
- * concrete nudge for the case it catches.
89
- *
90
- * ONE LINE PER STORE, never a merged sentence. The two stores answer different questions and
91
- * either can be the relevant one, so `RelevanceHit` is a closed four-valued answer and each store
92
- * gets its own contribution — a caller can then see, and later tune or drop, one without the other.
93
- *
94
- * WHY EACH LINE NAMES THE PLUGIN. This text is injected as runtime context, and the harness
95
- * materialises that as a USER-role snapshot in the conversation (`dsh-agent-loop`'s
96
- * `runtimeContext.project`). Without an author, a line like "记忆里有相关内容" reads as something
97
- * the user said. The `[avantf-mem 插件]` prefix is what keeps the provenance honest.
98
- */
99
- const MEMORY_HINT = "[avantf-mem 插件] 记忆里有与本次提问相关的事实;需要时用 `mem_recall` 检索。";
100
- const KNOWLEDGE_HINT = "[avantf-mem 插件] 知识库里有与本次提问相关的内容;需要时用 `kb_query` 检索。";
101
- /**
102
- * Map one answer to the two contributions, as text.
103
- *
104
- * An empty string is the "contribute nothing" value: `renderPrompt`/`renderContextSections` drop
105
- * empty text, so `none` (and the missing half of `memory`/`knowledge`) costs no tokens at all.
106
- */
107
- function hintLines(hit) {
108
- return {
109
- memory: hit === "memory" || hit === "both" ? MEMORY_HINT : "",
110
- knowledge: hit === "knowledge" || hit === "both" ? KNOWLEDGE_HINT : ""
111
- };
112
- }
113
- /**
114
- * The plain text of an inbox message, or `''` when it carries none.
115
- *
116
- * `dsh-agent-loop`'s own extractor returns `undefined` unless the message is exactly ONE text
117
- * block; a steered or multi-part message would then silently produce no hint. Concatenating the
118
- * text blocks keeps the probe working for those, and non-text blocks (images) are simply skipped —
119
- * the probe is lexical, so it has nothing to say about them anyway.
120
- */
121
- function messageText(message) {
122
- const content = message?.content;
123
- if (!Array.isArray(content)) return "";
124
- return content.map((block) => {
125
- const b = block;
126
- return b?.type === "text" && typeof b.text === "string" ? b.text : "";
127
- }).filter(Boolean).join("\n");
128
- }
129
- //#endregion
130
17
  //#region ../contract/lib/types.js
18
+ /** Shared domain types for avantf-mem. Single source of truth in @avantf/mem-contract. */
19
+ const CATEGORY_VALUES = [
20
+ "user_pref",
21
+ "user_env",
22
+ "project",
23
+ "tool",
24
+ "general"
25
+ ];
131
26
  /**
132
27
  * The fact statuses a caller may filter by, as the ONE list every surface reads: the tool schema's
133
28
  * `z.enum`, the derived MCP inputSchema and DSH parameter spec, and the CLI's `--status` flag (which
@@ -187,26 +82,26 @@ const DEGRADED_WEIGHTS = {
187
82
  * window); a positive value is clamped to it. Past the window transformers.js truncates
188
83
  * with no error, so the adapter bounds the text first and warns once (DESIGN §20).
189
84
  */
190
- const maxInputTokens = z$1.number().int().nonnegative().default(0);
191
- const semanticSchema = z$1.object({
192
- backend: z$1.string().default("local_bge"),
193
- local_model: z$1.string().default("Xenova/bge-small-zh-v1.5"),
194
- dim: z$1.number().int().positive().default(512),
85
+ const maxInputTokens = z.number().int().nonnegative().default(0);
86
+ const semanticSchema = z.object({
87
+ backend: z.string().default("local_bge"),
88
+ local_model: z.string().default("Xenova/bge-small-zh-v1.5"),
89
+ dim: z.number().int().positive().default(512),
195
90
  max_input_tokens: maxInputTokens,
196
- mirror: z$1.string().default("https://hf-mirror.com"),
197
- cache_dir: z$1.string().default("~/.avantf/models"),
198
- auto_download: z$1.boolean().default(true)
91
+ mirror: z.string().default("https://hf-mirror.com"),
92
+ cache_dir: z.string().default(""),
93
+ auto_download: z.boolean().default(true)
199
94
  });
200
- const rerankSchema = z$1.object({
201
- backend: z$1.string().default("none"),
202
- local_model: z$1.string().default("Xenova/bge-reranker-base"),
95
+ const rerankSchema = z.object({
96
+ backend: z.string().default("none"),
97
+ local_model: z.string().default("Xenova/bge-reranker-base"),
203
98
  max_input_tokens: maxInputTokens,
204
- mirror: z$1.string().default("https://hf-mirror.com"),
205
- cache_dir: z$1.string().default("~/.avantf/models"),
206
- auto_download: z$1.boolean().default(true)
99
+ mirror: z.string().default("https://hf-mirror.com"),
100
+ cache_dir: z.string().default(""),
101
+ auto_download: z.boolean().default(true)
207
102
  });
208
- const vectorStoreSchema = z$1.object({
209
- backend: z$1.string().default("auto"),
103
+ const vectorStoreSchema = z.object({
104
+ backend: z.string().default("auto"),
210
105
  /**
211
106
  * Vector counts at which `backend: auto` migrates to an ANN index.
212
107
  *
@@ -220,9 +115,9 @@ const vectorStoreSchema = z$1.object({
220
115
  * sub-millisecond. At 8000 recall falls to 0.89, so for a larger corpus raise
221
116
  * `hnswlib_ef_search` (or accept the loss) instead of moving this threshold.
222
117
  */
223
- auto_thresholds: z$1.object({
224
- hnswlib: z$1.number().int().positive().default(2e3),
225
- faiss: z$1.number().int().positive().default(1e5)
118
+ auto_thresholds: z.object({
119
+ hnswlib: z.number().int().positive().default(2e3),
120
+ faiss: z.number().int().positive().default(1e5)
226
121
  }).default({
227
122
  hnswlib: 2e3,
228
123
  faiss: 1e5
@@ -234,20 +129,20 @@ const vectorStoreSchema = z$1.object({
234
129
  * ~3 ms at n=2000). Recall falls as the corpus grows, so raise this before raising
235
130
  * `auto_thresholds.hnswlib`. The store uses `max(ef_search, 8 × k)`.
236
131
  */
237
- hnswlib_ef_search: z$1.number().int().min(16).max(2048).default(256)
132
+ hnswlib_ef_search: z.number().int().min(16).max(2048).default(256)
238
133
  });
239
- const retrieverSchema = z$1.object({
240
- weight_semantic: z$1.number().min(0).max(1).default(.55),
241
- weight_fts: z$1.number().min(0).max(1).default(.3),
242
- weight_jaccard: z$1.number().min(0).max(1).default(.15),
134
+ const retrieverSchema = z.object({
135
+ weight_semantic: z.number().min(0).max(1).default(.55),
136
+ weight_fts: z.number().min(0).max(1).default(.3),
137
+ weight_jaccard: z.number().min(0).max(1).default(.15),
243
138
  /**
244
139
  * Token budget for a retrieval RESULT (`0` = unlimited). `limit` bounds how many hits, never
245
140
  * how much text, so a broad query could otherwise pour tens of thousands of characters into a
246
141
  * model's context while a narrow one poured in a paragraph (DESIGN §20). Hits shortened by the
247
142
  * budget are marked `truncated`.
248
143
  */
249
- max_output_tokens: z$1.number().int().nonnegative().default(8e3),
250
- over_fetch_factor: z$1.number().int().positive().default(5),
144
+ max_output_tokens: z.number().int().nonnegative().default(8e3),
145
+ over_fetch_factor: z.number().int().positive().default(5),
251
146
  /**
252
147
  * Rows one non-semantic retrieval leg may hand to fusion (`0` = derived: `max(200, 4×overFetch)`).
253
148
  *
@@ -261,59 +156,59 @@ const retrieverSchema = z$1.object({
261
156
  * spend less memory than lose the tail: it is `retriever.leg_cap` that makes the cap a
262
157
  * measurable variable instead of a constant baked into the store.
263
158
  */
264
- leg_cap: z$1.number().int().nonnegative().default(0)
159
+ leg_cap: z.number().int().nonnegative().default(0)
265
160
  });
266
161
  /**
267
162
  * Trust & forgetting (TRUST_MODEL.md). The aging clock is the ACTIVE-DAY counter
268
163
  * (`avantf_stats.trust_clock`), not wall time: downtime counts as at most
269
164
  * `presence.gap_cap_days` so a shut-down system never "starves" its memories.
270
165
  */
271
- const presenceSchema = z$1.object({
272
- mode: z$1.enum(["process"]).default("process"),
166
+ const presenceSchema = z.object({
167
+ mode: z.enum(["process"]).default("process"),
273
168
  /** One presence advances the clock by at most this many days (downtime ⇒ 1 day). */
274
- gap_cap_days: z$1.number().min(0).default(1),
169
+ gap_cap_days: z.number().min(0).default(1),
275
170
  /** Long-lived surfaces (plugin / MCP) tick this often; 0 = startup pass only. */
276
- heartbeat_minutes: z$1.number().int().nonnegative().default(60)
171
+ heartbeat_minutes: z.number().int().nonnegative().default(60)
277
172
  });
278
- const trustSchema = z$1.object({
173
+ const trustSchema = z.object({
279
174
  /** false = no presence, no decay, no reinforcement, no pinning (TTL/idle/purge keep running). */
280
- enabled: z$1.boolean().default(true),
175
+ enabled: z.boolean().default(true),
281
176
  /** Initial trust of a new fact. */
282
- start: z$1.number().min(0).max(1).default(.5),
177
+ start: z.number().min(0).max(1).default(.5),
283
178
  /** Linear decay per active day: `0.5 / 90` ⇒ 90 active days from start to 0. */
284
- decay_per_day: z$1.number().min(0).max(1).default(.5 / 90),
179
+ decay_per_day: z.number().min(0).max(1).default(.5 / 90),
285
180
  /** Trust ≤ this ⇒ forgotten (archived 'forgot'). 0 = "reaching zero is forgetting". */
286
- forget_threshold: z$1.number().min(0).max(1).default(0),
181
+ forget_threshold: z.number().min(0).max(1).default(0),
287
182
  /** Reaching this via explicit feedback promotes to permanent (snapped to 1.0). */
288
- permanent_threshold: z$1.number().min(0).max(1).default(.9),
183
+ permanent_threshold: z.number().min(0).max(1).default(.9),
289
184
  /** A recall of something at/below this raises it straight back to this value. */
290
- recall_floor: z$1.number().min(0).max(1).default(.5),
185
+ recall_floor: z.number().min(0).max(1).default(.5),
291
186
  /** Per-recall gain (before marginal decay). */
292
- recall_delta: z$1.number().min(0).max(1).default(.03),
187
+ recall_delta: z.number().min(0).max(1).default(.03),
293
188
  /** Effective reinforcement events per fact per 24h (R10: zero-gain ones don't count). */
294
- recall_daily_cap: z$1.number().int().nonnegative().default(3),
189
+ recall_daily_cap: z.number().int().nonnegative().default(3),
295
190
  /** 1 = every in-window gain is equal; 0.5 = 0.03 / 0.015 / 0.0075 … */
296
- recall_marginal_decay: z$1.number().min(0).max(1).default(1),
191
+ recall_marginal_decay: z.number().min(0).max(1).default(1),
297
192
  /** Recall alone can never exceed this (and never pins — see D11). */
298
- recall_ceiling: z$1.number().min(0).max(1).default(.85),
193
+ recall_ceiling: z.number().min(0).max(1).default(.85),
299
194
  /** Explicit `helpful` / `unhelpful` step. */
300
- feedback_delta: z$1.number().min(0).max(1).default(.05),
195
+ feedback_delta: z.number().min(0).max(1).default(.05),
301
196
  /** 0 = unlimited; >0 also caps feedback reinforcement per fact per 24h. */
302
- feedback_daily_cap: z$1.number().int().nonnegative().default(0),
197
+ feedback_daily_cap: z.number().int().nonnegative().default(0),
303
198
  /** A revised fact inherits the settled trust (and pin) of the row it replaces. */
304
- inherit_trust_on_update: z$1.boolean().default(true),
199
+ inherit_trust_on_update: z.boolean().default(true),
305
200
  /** Pinned (permanent) facts are never physically purged. */
306
- purge_skips_pinned: z$1.boolean().default(true),
201
+ purge_skips_pinned: z.boolean().default(true),
307
202
  /** Calendar fallback: unused for this many days ⇒ archived 'idle'. */
308
- idle_calendar_days: z$1.number().int().positive().default(365),
203
+ idle_calendar_days: z.number().int().positive().default(365),
309
204
  /** Per-tick budget for the settle sweep (the other sweeps only touch critical rows). */
310
- tick_max_facts: z$1.number().int().positive().default(5e3),
205
+ tick_max_facts: z.number().int().positive().default(5e3),
311
206
  presence: presenceSchema.prefault({})
312
207
  });
313
- const lifecycleSchema = z$1.object({
208
+ const lifecycleSchema = z.object({
314
209
  /** Purge window, measured in ACTIVE days since `archived_clock`. */
315
- purge_after_archived_days: z$1.number().int().nonnegative().default(365),
316
- contradiction_threshold: z$1.number().min(0).max(1).default(.6)
210
+ purge_after_archived_days: z.number().int().nonnegative().default(365),
211
+ contradiction_threshold: z.number().min(0).max(1).default(.6)
317
212
  });
318
213
  /**
319
214
  * External tools and derived runtime state the plugin must PROVIDE, not merely use (DESIGN §13).
@@ -327,21 +222,21 @@ const lifecycleSchema = z$1.object({
327
222
  * The mirror list defaults to DOMESTIC proxies, with the official source always tried last — a
328
223
  * 35 MB download from GitHub is the difference between a usable first run and an abandoned one.
329
224
  */
330
- const toolsSchema = z$1.object({
225
+ const toolsSchema = z.object({
331
226
  /** Managed install root; `<dir>/<tool>/<version>/bin/<binary>` per artifact. */
332
- dir: z$1.string().default("~/.avantf/tools"),
227
+ dir: z.string().default(""),
333
228
  /** URL templates tried before the official source; `{url}` is the original URL, `{file}` its name. */
334
- mirror: z$1.array(z$1.string()).default([
229
+ mirror: z.array(z.string()).default([
335
230
  "https://ghfast.top/{url}",
336
231
  "https://ghproxy.net/{url}",
337
232
  "https://gh-proxy.com/{url}"
338
233
  ]),
339
234
  /** Download a missing artifact at startup. `false` = use only what is already on this machine. */
340
- auto_install: z$1.boolean().default(true)
235
+ auto_install: z.boolean().default(true)
341
236
  });
342
237
  /** The merged runtime config shared by memory and knowledge stores. */
343
- const ConfigSchema = z$1.object({
344
- dataHome: z$1.string().default("~/.avantf"),
238
+ const ConfigSchema = z.object({
239
+ dataHome: z.string().default("~/.avantf"),
345
240
  semantic: semanticSchema.prefault({}),
346
241
  rerank: rerankSchema.prefault({}),
347
242
  vectorStore: vectorStoreSchema.prefault({}),
@@ -356,16 +251,7 @@ const defaultConfig = ConfigSchema.parse({});
356
251
  * Memory store-specific config. Overrides on top of the common Config.
357
252
  * Note: `db.path` is the single authoritative path for the memory DB.
358
253
  */
359
- const MemoryConfigSchema = z$1.object({
360
- db: z$1.object({ path: z$1.string().default("") }).prefault({}),
361
- category_values: z$1.array(z$1.string()).default([
362
- "user_pref",
363
- "user_env",
364
- "project",
365
- "tool",
366
- "general"
367
- ])
368
- });
254
+ const MemoryConfigSchema = z.object({ db: z.object({ path: z.string().default("") }).prefault({}) });
369
255
  /**
370
256
  * Ingestion boundary (DESIGN §8 / D5). `kb_ingest`'s `source_uri` is RESOLVED —
371
257
  * a local path is read, an http(s) URL is fetched — and everything it reads lands
@@ -373,24 +259,24 @@ const MemoryConfigSchema = z$1.object({
373
259
  * read-any-file / probe-the-intranet primitive for a steered agent. Safe by
374
260
  * default, with one explicit opt-in per resource kind.
375
261
  */
376
- const ingestSchema = z$1.object({
262
+ const ingestSchema = z.object({
377
263
  /** Roots a local `source_uri`/`kb_import` path may be read from; empty ⇒ the process workspace (`cwd`). */
378
- local_roots: z$1.array(z$1.string()).default([]),
264
+ local_roots: z.array(z.string()).default([]),
379
265
  /** Lift the roots check entirely — read any local path. */
380
- allow_outside_workspace: z$1.boolean().default(false),
266
+ allow_outside_workspace: z.boolean().default(false),
381
267
  /** Allow `http(s)` fetches to loopback/private/link-local addresses. */
382
- allow_private_network: z$1.boolean().default(false)
268
+ allow_private_network: z.boolean().default(false)
383
269
  });
384
270
  /** Knowledge store-specific config. */
385
- const KnowledgeConfigSchema = z$1.object({
386
- db: z$1.object({ path: z$1.string().default("") }).prefault({}),
271
+ const KnowledgeConfigSchema = z.object({
272
+ db: z.object({ path: z.string().default("") }).prefault({}),
387
273
  /**
388
274
  * The managed document directory: one editable `.md` per document, holding the body the KB
389
275
  * indexed. Empty ⇒ `<dataHome>/knowledge/docs`. It holds a COPY, never the origin: a
390
276
  * `source_uri` is read once at ingest, so editing or deleting these files cannot touch
391
277
  * whatever the document came from.
392
278
  */
393
- docs: z$1.object({ dir: z$1.string().default("") }).prefault({}),
279
+ docs: z.object({ dir: z.string().default("") }).prefault({}),
394
280
  /**
395
281
  * Version control for the store's on-disk text, via the reusable `GitRepo`.
396
282
  *
@@ -399,16 +285,16 @@ const KnowledgeConfigSchema = z$1.object({
399
285
  * must not enter the history. Point `root` higher (e.g. at the whole store directory) to share
400
286
  * more, and `ignore` is what keeps the databases out; the defaults already cover them.
401
287
  */
402
- git: z$1.object({
403
- mode: z$1.enum(["auto", "off"]).default("auto"),
404
- root: z$1.string().default(""),
405
- ignore: z$1.array(z$1.string()).default([
288
+ git: z.object({
289
+ mode: z.enum(["auto", "off"]).default("auto"),
290
+ root: z.string().default(""),
291
+ ignore: z.array(z.string()).default([
406
292
  "*.db",
407
293
  "*.db-wal",
408
294
  "*.db-shm"
409
295
  ])
410
296
  }).prefault({}),
411
- open: z$1.object({ editor: z$1.string().default("") }).prefault({}),
297
+ open: z.object({ editor: z.string().default("") }).prefault({}),
412
298
  /**
413
299
  * The knowledge-domain allowlist: the `domain` names a write may use. The default is a small
414
300
  * general set; a deployment narrows or widens it here. An EXPLICIT EMPTY array means no
@@ -416,7 +302,7 @@ const KnowledgeConfigSchema = z$1.object({
416
302
  * allowlist is non-empty additionally accepts every domain already present in its `documents`
417
303
  * table, so renaming the taxonomy never strands existing documents.
418
304
  */
419
- domains: z$1.array(z$1.string()).default([
305
+ domains: z.array(z.string()).default([
420
306
  "design",
421
307
  "api",
422
308
  "ops",
@@ -431,13 +317,48 @@ const KnowledgeConfigSchema = z$1.object({
431
317
  * rejected — the embedder bounds them and warns once — but their tail would only reach the
432
318
  * FTS leg, so raise this only together with `semantic.max_input_tokens`.
433
319
  */
434
- chunk_size: z$1.number().int().positive().default(500),
320
+ chunk_size: z.number().int().positive().default(500),
435
321
  /** 10% of `chunk_size`: consecutive chunks share this much context (unchanged ratio). */
436
- chunk_overlap: z$1.number().int().nonnegative().default(50),
437
- source_priority: z$1.enum(["workspace", "none"]).default("workspace"),
322
+ chunk_overlap: z.number().int().nonnegative().default(50),
323
+ source_priority: z.enum(["workspace", "none"]).default("workspace"),
438
324
  ingest: ingestSchema.prefault({})
439
325
  });
440
326
  //#endregion
327
+ //#region ../contract/lib/family.js
328
+ /**
329
+ * The family root and the two managed directories under it — ONE definition of a convention that
330
+ * four packages would otherwise each spell out.
331
+ *
332
+ * `@avantf/dsh-plugin-base` (the family's environment framework) installs everything it manages under a
333
+ * single root: `$AVANTF_HOME` when set, else `~/.avantf/env`. `tools` holds external binaries
334
+ * (pandoc), `models` the downloaded embedding/rerank weights in the flat layout the transformers.js
335
+ * cache reads. The engine's built-in defaults point HERE, not at the pre-framework
336
+ * `~/.avantf/{tools,models}`: those legacy directories are no longer a fallback anywhere, so a
337
+ * machine that never creates them loses nothing, and no process resolves a path that only works
338
+ * because somebody left a compatibility symlink behind.
339
+ *
340
+ * `provision` mirrors {@link familyHome} (it is deliberately dependency-free, so it cannot import
341
+ * this module); `packages/core/test/family_paths.spec.ts` pins the two copies together so the
342
+ * convention cannot drift.
343
+ */
344
+ function familyHome$1(env = process.env) {
345
+ const configured = env["AVANTF_HOME"]?.trim() ?? "";
346
+ return configured !== "" ? expandTilde(configured) : join(homedir(), ".avantf", "env");
347
+ }
348
+ /** The managed external-binary root (`<family root>/tools`). */
349
+ function familyToolsDir$1(env = process.env) {
350
+ return join(familyHome$1(env), "tools");
351
+ }
352
+ /** The managed model-cache root (`<family root>/models`). */
353
+ function familyModelsDir(env = process.env) {
354
+ return join(familyHome$1(env), "models");
355
+ }
356
+ /** `~/x` → `<home>/x`, `~` → `<home>`; anything else is used as given. */
357
+ function expandTilde(path) {
358
+ if (path === "~") return homedir();
359
+ return path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
360
+ }
361
+ //#endregion
441
362
  //#region ../contract/lib/tools.js
442
363
  /**
443
364
  * The closed value sets these tools accept, exported so no other surface has to retype them.
@@ -447,6 +368,15 @@ const KnowledgeConfigSchema = z$1.object({
447
368
  * refusing the new value — with no test failing anywhere. One array, every consumer reading it.
448
369
  * (`FACT_STATUSES` lives in `types.ts`, beside the `FactStatus` type it is the single source of.)
449
370
  */
371
+ /**
372
+ * The built-in category vocabulary, spelled for the fields that DESCRIBE it.
373
+ *
374
+ * `category` is free-form (`FactCategory` is `… | string`) and stays that way — this is a suggestion
375
+ * a caller can see, not a closed set the engine enforces. It is written once, in `types.ts`, and read
376
+ * here so the constant has a real consumer: before this, nothing at runtime ever read it, which meant
377
+ * editing it changed nothing anywhere.
378
+ */
379
+ const CATEGORY_HINT = `内置分类:${CATEGORY_VALUES.join(" / ")};也可自定义。`;
450
380
  const CONTRADICTION_RESOLUTIONS = ["true_positive", "false_positive"];
451
381
  const QUERY_KINDS = [
452
382
  "all",
@@ -454,6 +384,37 @@ const QUERY_KINDS = [
454
384
  "doc_chunk"
455
385
  ];
456
386
  /**
387
+ * The longest retrieval query the contract accepts, in characters.
388
+ *
389
+ * There is no useful query beyond this, and there IS a cost: a CJK query is expanded into one trigram
390
+ * OR-phrase per character and evaluated by SQLite **synchronously on the host's event loop** (200 000
391
+ * characters measured at 119 seconds, of which the expansion was 27–52 ms — the rest was `MATCH`). The
392
+ * cap belongs HERE because every entry point derives from this union: the plugin's tools, the MCP
393
+ * inputSchema, the CLI's argv validation and the browser payload are all this same object.
394
+ */
395
+ const MAX_QUERY_CHARS = 2e3;
396
+ /** Why the cap exists, in the words the caller sees. */
397
+ const QUERY_TOO_LONG = `检索文本最长 ${String(MAX_QUERY_CHARS)} 字符:更长的输入会被展开成成百上千个 OR 短语并在宿主事件循环上同步执行。把范围收窄,或先用 kb_ingest 把整篇文档入库再检索。`;
398
+ /**
399
+ * Vocabulary that describes HOW memory is retained, banned from every model-facing string.
400
+ *
401
+ * The model cannot observe those clocks and has no action to take on them; the one thing such
402
+ * text enables is rewriting a fact to "refresh" it, which defeats the policy it just described.
403
+ * It binds every tool description and parameter description AND the DSH plugin's system-prompt
404
+ * section, because the prompt section is prompt text too and a second copy of the list is a copy
405
+ * that drifts.
406
+ */
407
+ const RETENTION_VOCABULARY = [
408
+ "衰减",
409
+ "消退",
410
+ "遗忘",
411
+ "强化",
412
+ "活跃使用日",
413
+ "信任度",
414
+ "永久记忆",
415
+ "长期有效的信息"
416
+ ];
417
+ /**
457
418
  * Admin actions that return a FACT VIEW. Their payload goes through
458
419
  * {@link modelFacingToolResult}, which drops the retention diagnostics; the other admin
459
420
  * actions (`trust_diagnose`, `vectors_diagnose`, …) are diagnostics the caller asked for and
@@ -470,179 +431,179 @@ function modelFacingToolResult(toolKey, action, value) {
470
431
  if (toolKey !== "admin") return value;
471
432
  return FACT_VIEW_ADMIN_ACTIONS.includes(action) ? withoutRetentionDiagnostics(value) : value;
472
433
  }
473
- const RememberUnion = z$1.discriminatedUnion("action", [
474
- z$1.object({
475
- action: z$1.literal("add"),
476
- content: z$1.string().min(1).describe("要写入或替换的事实内容:一句自包含的陈述。action=add 时必填。"),
477
- category: z$1.string().optional().describe("分类标签;可选,默认 general。"),
478
- ttl_days: z$1.number().int().nonnegative().optional().describe("有效期天数(0 或省略 = 不设有效期;正整数 = 自写入起该天数后自动归档)。update 未给则继承被改写事实的 TTL。")
434
+ const RememberUnion = z.discriminatedUnion("action", [
435
+ z.object({
436
+ action: z.literal("add"),
437
+ content: z.string().min(1).describe("要写入或替换的事实内容:一句自包含的陈述。action=add 时必填。"),
438
+ category: z.string().optional().describe(`分类标签;可选,默认 general。${CATEGORY_HINT}`),
439
+ ttl_days: z.number().int().nonnegative().optional().describe("有效期天数(0 或省略 = 不设有效期;正整数 = 自写入起该天数后自动归档)。update 未给则继承被改写事实的 TTL。")
479
440
  }),
480
- z$1.object({
481
- action: z$1.literal("update"),
482
- fact_id: z$1.number().int().positive().describe("要更新的事实 ID。action=update 时必填。"),
483
- content: z$1.string().min(1).describe("替换后的新事实内容。action=update 时必填。"),
484
- category: z$1.string().optional().describe("新的分类标签;可选,未给则继承被改写事实的分类。"),
485
- ttl_days: z$1.number().int().nonnegative().optional().describe("新的有效期天数(0 = 取消有效期);可选,未给则继承被改写事实的 TTL。")
441
+ z.object({
442
+ action: z.literal("update"),
443
+ fact_id: z.number().int().positive().describe("要更新的事实 ID。action=update 时必填。"),
444
+ content: z.string().min(1).describe("替换后的新事实内容。action=update 时必填。"),
445
+ category: z.string().optional().describe(`新的分类标签;可选,未给则继承被改写事实的分类。${CATEGORY_HINT}`),
446
+ ttl_days: z.number().int().nonnegative().optional().describe("新的有效期天数(0 = 取消有效期);可选,未给则继承被改写事实的 TTL。")
486
447
  }),
487
- z$1.object({
488
- action: z$1.literal("remove"),
489
- fact_id: z$1.number().int().positive().describe("要删除的事实 ID。action=remove 时必填。"),
490
- reason: z$1.string().optional().describe("删除原因;可选。")
448
+ z.object({
449
+ action: z.literal("remove"),
450
+ fact_id: z.number().int().positive().describe("要删除的事实 ID。action=remove 时必填。"),
451
+ reason: z.string().optional().describe("删除原因;可选。")
491
452
  }),
492
- z$1.object({
493
- action: z$1.literal("helpful"),
494
- fact_id: z$1.number().int().positive().describe("对回答有帮助的事实 ID。action=helpful 时必填。")
453
+ z.object({
454
+ action: z.literal("helpful"),
455
+ fact_id: z.number().int().positive().describe("对回答有帮助的事实 ID。action=helpful 时必填。")
495
456
  }),
496
- z$1.object({
497
- action: z$1.literal("unhelpful"),
498
- fact_id: z$1.number().int().positive().describe("对回答无帮助的事实 ID。action=unhelpful 时必填。")
457
+ z.object({
458
+ action: z.literal("unhelpful"),
459
+ fact_id: z.number().int().positive().describe("对回答无帮助的事实 ID。action=unhelpful 时必填。")
499
460
  })
500
461
  ]);
501
- const RecallUnion = z$1.discriminatedUnion("action", [
502
- z$1.object({
503
- action: z$1.literal("search"),
504
- query: z$1.string().min(1).describe("检索文本。action=search 时必填。"),
505
- category: z$1.string().optional().describe("限定分类;可选。"),
506
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。"),
507
- max_tokens: z$1.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。")
462
+ const RecallUnion = z.discriminatedUnion("action", [
463
+ z.object({
464
+ action: z.literal("search"),
465
+ query: z.string().min(1).max(MAX_QUERY_CHARS, QUERY_TOO_LONG).describe("检索文本。action=search 时必填。"),
466
+ category: z.string().optional().describe("限定分类;可选。"),
467
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。"),
468
+ max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。")
508
469
  }),
509
- z$1.object({
510
- action: z$1.literal("ask"),
511
- subj: z$1.string().optional().describe("三元组主语;action=ask 时与 pred/obj/query 至少给一个。"),
512
- pred: z$1.string().optional().describe("三元组谓语。"),
513
- obj: z$1.string().optional().describe("三元组宾语。"),
514
- query: z$1.string().optional().describe("自然语言问题;给了 query 就按整句检索。"),
515
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
470
+ z.object({
471
+ action: z.literal("ask"),
472
+ subj: z.string().optional().describe("三元组主语;action=ask 时与 pred/obj/query 至少给一个。"),
473
+ pred: z.string().optional().describe("三元组谓语。"),
474
+ obj: z.string().optional().describe("三元组宾语。"),
475
+ query: z.string().optional().describe("自然语言问题;给了 query 就按整句检索。"),
476
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
516
477
  }),
517
- z$1.object({
518
- action: z$1.literal("chain"),
519
- subj: z$1.string().min(1).describe("链式推理的起点实体。action=chain 时必填。"),
520
- pred: z$1.string().optional().describe("第一跳关系;可选。"),
521
- second_pred: z$1.string().optional().describe("第二跳关系;可选。"),
522
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
478
+ z.object({
479
+ action: z.literal("chain"),
480
+ subj: z.string().min(1).describe("链式推理的起点实体。action=chain 时必填。"),
481
+ pred: z.string().optional().describe("第一跳关系;可选。"),
482
+ second_pred: z.string().optional().describe("第二跳关系;可选。"),
483
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
523
484
  }),
524
- z$1.object({
525
- action: z$1.literal("probe"),
526
- entity: z$1.string().min(1).describe("要探查的实体名。action=probe 时必填。"),
527
- category: z$1.string().optional().describe("限定分类;可选。"),
528
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
485
+ z.object({
486
+ action: z.literal("probe"),
487
+ entity: z.string().min(1).describe("要探查的实体名。action=probe 时必填。"),
488
+ category: z.string().optional().describe("限定分类;可选。"),
489
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
529
490
  }),
530
- z$1.object({
531
- action: z$1.literal("reason"),
532
- entities: z$1.array(z$1.string().min(1)).min(1).describe("参与推理的实体名数组(≥1)。action=reason 时必填。"),
533
- category: z$1.string().optional().describe("限定分类;可选。"),
534
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
491
+ z.object({
492
+ action: z.literal("reason"),
493
+ entities: z.array(z.string().min(1)).min(1).describe("参与推理的实体名数组(≥1)。action=reason 时必填。"),
494
+ category: z.string().optional().describe("限定分类;可选。"),
495
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
535
496
  }),
536
- z$1.object({
537
- action: z$1.literal("related"),
538
- entity: z$1.string().min(1).describe("要查询相关事实的实体名。action=related 时必填。"),
539
- category: z$1.string().optional().describe("限定分类;可选。"),
540
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
497
+ z.object({
498
+ action: z.literal("related"),
499
+ entity: z.string().min(1).describe("要查询相关事实的实体名。action=related 时必填。"),
500
+ category: z.string().optional().describe("限定分类;可选。"),
501
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
541
502
  }),
542
- z$1.object({
543
- action: z$1.literal("contradict"),
544
- category: z$1.string().optional().describe("限定分类;可选。"),
545
- limit: z$1.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
503
+ z.object({
504
+ action: z.literal("contradict"),
505
+ category: z.string().optional().describe("限定分类;可选。"),
506
+ limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
546
507
  })
547
508
  ]);
548
- const AdminUnion = z$1.discriminatedUnion("action", [
549
- z$1.object({ action: z$1.literal("stats") }),
550
- z$1.object({
551
- action: z$1.literal("list"),
552
- category: z$1.string().optional().describe("限定分类;可选。"),
553
- status: z$1.enum(FACT_STATUSES).optional().describe("按状态过滤;可选,默认 active。"),
554
- limit: z$1.number().int().positive().max(500).optional().describe("返回条数上限(1-500);可选,默认 50。"),
555
- offset: z$1.number().int().nonnegative().optional().describe("分页偏移(从 0 开始);配合 limit 翻页;可选。")
509
+ const AdminUnion = z.discriminatedUnion("action", [
510
+ z.object({ action: z.literal("stats") }),
511
+ z.object({
512
+ action: z.literal("list"),
513
+ category: z.string().optional().describe("限定分类;可选。"),
514
+ status: z.enum(FACT_STATUSES).optional().describe("按状态过滤;可选,默认 active。"),
515
+ limit: z.number().int().positive().max(500).optional().describe("返回条数上限(1-500);可选,默认 50。"),
516
+ offset: z.number().int().nonnegative().optional().describe("分页偏移(从 0 开始);配合 limit 翻页;可选。")
556
517
  }),
557
- z$1.object({
558
- action: z$1.literal("detail"),
559
- fact_id: z$1.number().int().positive().describe("要查看的事实 ID。action=detail 时必填。")
518
+ z.object({
519
+ action: z.literal("detail"),
520
+ fact_id: z.number().int().positive().describe("要查看的事实 ID。action=detail 时必填。")
560
521
  }),
561
- z$1.object({
562
- action: z$1.literal("archive"),
563
- fact_id: z$1.number().int().positive().describe("要归档的事实 ID。action=archive 时必填。"),
564
- reason: z$1.string().optional().describe("归档原因;可选。")
522
+ z.object({
523
+ action: z.literal("archive"),
524
+ fact_id: z.number().int().positive().describe("要归档的事实 ID。action=archive 时必填。"),
525
+ reason: z.string().optional().describe("归档原因;可选。")
565
526
  }),
566
- z$1.object({
567
- action: z$1.literal("restore"),
568
- fact_id: z$1.number().int().positive().describe("要恢复的事实 ID。action=restore 时必填。")
527
+ z.object({
528
+ action: z.literal("restore"),
529
+ fact_id: z.number().int().positive().describe("要恢复的事实 ID。action=restore 时必填。")
569
530
  }),
570
- z$1.object({
571
- action: z$1.literal("pin"),
572
- fact_id: z$1.number().int().positive().describe("要标记为长期保留的事实 ID。action=pin 时必填。")
531
+ z.object({
532
+ action: z.literal("pin"),
533
+ fact_id: z.number().int().positive().describe("要标记为长期保留的事实 ID。action=pin 时必填。")
573
534
  }),
574
- z$1.object({
575
- action: z$1.literal("unpin"),
576
- fact_id: z$1.number().int().positive().describe("要取消长期保留标记的事实 ID。action=unpin 时必填。")
535
+ z.object({
536
+ action: z.literal("unpin"),
537
+ fact_id: z.number().int().positive().describe("要取消长期保留标记的事实 ID。action=unpin 时必填。")
577
538
  }),
578
- z$1.object({ action: z$1.literal("trust_diagnose") }),
579
- z$1.object({ action: z$1.literal("vectors_diagnose") }),
580
- z$1.object({
581
- action: z$1.literal("vectors_fix"),
582
- dry_run: z$1.boolean().optional().describe("true=只诊断不写入;可选,默认 false。")
539
+ z.object({ action: z.literal("trust_diagnose") }),
540
+ z.object({ action: z.literal("vectors_diagnose") }),
541
+ z.object({
542
+ action: z.literal("vectors_fix"),
543
+ dry_run: z.boolean().optional().describe("true=只诊断不写入;可选,默认 false。")
583
544
  }),
584
- z$1.object({ action: z$1.literal("contradict_check") }),
585
- z$1.object({
586
- action: z$1.literal("contradict_resolve"),
587
- contradiction_id: z$1.number().int().positive().describe("要裁决的矛盾 ID(来自 `mem_remember` 的 contradictions 或 `mem_recall` 的 contradict)。"),
588
- resolution: z$1.enum(CONTRADICTION_RESOLUTIONS).describe("true_positive=确实是矛盾(配 loser_fact_id 归档错的一方);false_positive=两条并不冲突,关闭该配对且两条都保留。"),
589
- loser_fact_id: z$1.number().int().positive().optional().describe("判错的一方(必须是该配对里的两条之一);给定时会被归档。仅 true_positive 可带。")
545
+ z.object({ action: z.literal("contradict_check") }),
546
+ z.object({
547
+ action: z.literal("contradict_resolve"),
548
+ contradiction_id: z.number().int().positive().describe("要裁决的矛盾 ID(来自 `mem_remember` 的 contradictions 或 `mem_recall` 的 contradict)。"),
549
+ resolution: z.enum(CONTRADICTION_RESOLUTIONS).describe("true_positive=确实是矛盾(配 loser_fact_id 归档错的一方);false_positive=两条并不冲突,关闭该配对且两条都保留。"),
550
+ loser_fact_id: z.number().int().positive().optional().describe("判错的一方(必须是该配对里的两条之一);给定时会被归档。仅 true_positive 可带。")
590
551
  }),
591
- z$1.object({ action: z$1.literal("maintenance") })
552
+ z.object({ action: z.literal("maintenance") })
592
553
  ]);
593
554
  /** The `source` a write gets when it omits one; a document's identity is `(domain, source, title)`. */
594
555
  const DEFAULT_KB_SOURCE = "default";
595
- const KbUnion = z$1.discriminatedUnion("action", [
596
- z$1.object({
597
- action: z$1.literal("ingest"),
598
- text: z$1.string().min(1).optional().describe("要入库的文本;与 source_uri 至少给一个。"),
599
- source_uri: z$1.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库;与 text 至少给一个。"),
600
- domain: z$1.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=ingest 时必填。"),
601
- source: z$1.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
602
- title: z$1.string().optional().describe("文档标题;可选。"),
603
- overwrite: z$1.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
556
+ const KbUnion = z.discriminatedUnion("action", [
557
+ z.object({
558
+ action: z.literal("ingest"),
559
+ text: z.string().min(1).optional().describe("要入库的文本;与 source_uri 至少给一个。"),
560
+ source_uri: z.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库;与 text 至少给一个。"),
561
+ domain: z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=ingest 时必填。"),
562
+ source: z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
563
+ title: z.string().optional().describe("文档标题;可选。"),
564
+ overwrite: z.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
604
565
  }),
605
- z$1.object({
606
- action: z$1.literal("import"),
607
- paths: z$1.array(z$1.string().min(1)).min(1).describe("要导入的文件路径数组(≥1)。action=import 时必填。"),
608
- domain: z$1.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=import 时必填。"),
609
- source: z$1.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
610
- overwrite: z$1.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
566
+ z.object({
567
+ action: z.literal("import"),
568
+ paths: z.array(z.string().min(1)).min(1).describe("要导入的文件路径数组(≥1)。action=import 时必填。"),
569
+ domain: z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=import 时必填。"),
570
+ source: z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
571
+ overwrite: z.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
611
572
  }),
612
- z$1.object({
613
- action: z$1.literal("list"),
614
- domain: z$1.string().optional().describe("限定知识域;可选。"),
615
- source: z$1.string().optional().describe("限定来源;可选。"),
616
- limit: z$1.number().int().positive().optional().describe("本页最多返回多少篇文档(按 updated_at 倒序);可选,缺省不限。"),
617
- offset: z$1.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
573
+ z.object({
574
+ action: z.literal("list"),
575
+ domain: z.string().optional().describe("限定知识域;可选。"),
576
+ source: z.string().optional().describe("限定来源;可选。"),
577
+ limit: z.number().int().positive().optional().describe("本页最多返回多少篇文档(按 updated_at 倒序);可选,缺省不限。"),
578
+ offset: z.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
618
579
  }),
619
- z$1.object({
620
- action: z$1.literal("detail"),
621
- doc_id: z$1.number().int().positive().describe("文档 ID。action=detail 时必填。")
580
+ z.object({
581
+ action: z.literal("detail"),
582
+ doc_id: z.number().int().positive().describe("文档 ID。action=detail 时必填。")
622
583
  }),
623
- z$1.object({
624
- action: z$1.literal("remove"),
625
- doc_id: z$1.number().int().positive().describe("要删除的文档 ID。action=remove 时必填。")
584
+ z.object({
585
+ action: z.literal("remove"),
586
+ doc_id: z.number().int().positive().describe("要删除的文档 ID。action=remove 时必填。")
626
587
  }),
627
- z$1.object({
628
- action: z$1.literal("reindex"),
629
- domain: z$1.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
630
- dry_run: z$1.boolean().optional().describe("true=只报告将重建多少(chunks/实体/向量),不写入;可选,默认 false。")
588
+ z.object({
589
+ action: z.literal("reindex"),
590
+ domain: z.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
591
+ dry_run: z.boolean().optional().describe("true=只报告将重建多少(chunks/实体/向量),不写入;可选,默认 false。")
631
592
  }),
632
- z$1.object({
633
- action: z$1.literal("sync"),
634
- doc_id: z$1.number().int().positive().optional().describe("只同步该文档;可选,缺省检查全部。"),
635
- adopt: z$1.boolean().optional().describe("true=认领:把该文档路径上**没有 frontmatter** 的文件重新摄入并补写 frontmatter(action=sync 且给了 doc_id 时才有意义)。整篇覆写会毁掉 frontmatter,此时 `sync` 会**自动**认领(守卫:文件完全没有 frontmatter、正文非空、指纹稳定、路径归属唯一);不满足守卫的会留在 `unclaimed` 里,用这个参数立即认领。"),
636
- dry_run: z$1.boolean().optional().describe("true=只报告哪些文档的受管文件被改过(stale/missing/orphans),不重新摄入;可选,默认 false。")
593
+ z.object({
594
+ action: z.literal("sync"),
595
+ doc_id: z.number().int().positive().optional().describe("只同步该文档;可选,缺省检查全部。"),
596
+ adopt: z.boolean().optional().describe("true=认领:把该文档路径上**没有 frontmatter** 的文件重新摄入并补写 frontmatter(action=sync 且给了 doc_id 时才有意义)。整篇覆写会毁掉 frontmatter,此时 `sync` 会**自动**认领(守卫:文件完全没有 frontmatter、正文非空、指纹稳定、路径归属唯一);不满足守卫的会留在 `unclaimed` 里,用这个参数立即认领。"),
597
+ dry_run: z.boolean().optional().describe("true=只报告哪些文档的受管文件被改过(stale/missing/orphans),不重新摄入;可选,默认 false。")
637
598
  })
638
599
  ]);
639
- const QueryUnion = z$1.object({
640
- query: z$1.string().min(1).describe("检索文本,必填。"),
641
- kind: z$1.enum(QUERY_KINDS).optional().describe("结果类型:all/fact/doc_chunk;可选,默认 all。"),
642
- max_tokens: z$1.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。"),
643
- domain: z$1.string().optional().describe("限定知识域;可选。"),
644
- source: z$1.string().optional().describe("限定来源;可选。"),
645
- limit: z$1.number().int().positive().max(50).default(10).describe("返回条数上限(1-50);可选,默认 10。")
600
+ const QueryUnion = z.object({
601
+ query: z.string().min(1).max(MAX_QUERY_CHARS, QUERY_TOO_LONG).describe("检索文本,必填。"),
602
+ kind: z.enum(QUERY_KINDS).optional().describe("结果类型:all/fact/doc_chunk;可选,默认 all。"),
603
+ max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。"),
604
+ domain: z.string().optional().describe("限定知识域;可选。"),
605
+ source: z.string().optional().describe("限定来源;可选。"),
606
+ limit: z.number().int().positive().max(50).default(10).describe("返回条数上限(1-50);可选,默认 10。")
646
607
  });
647
608
  const REMEMBER_TOOL = {
648
609
  key: "remember",
@@ -674,9 +635,9 @@ const QUERY_TOOL = {
674
635
  description: "在**用户已入库的文档切片**与记忆事实里做统一检索(跨库)。只检索、不生成答案。",
675
636
  input: QueryUnion
676
637
  };
677
- const KbDomainField = z$1.string().min(1).describe("知识域;取值来自配置 knowledge.domains。");
678
- const KbSourceField = z$1.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。");
679
- const KbTitleField = z$1.string().optional().describe("文档标题;可选,缺省取正文首个标题或首行。");
638
+ const KbDomainField = z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。");
639
+ const KbSourceField = z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。");
640
+ const KbTitleField = z.string().optional().describe("文档标题;可选,缺省取正文首个标题或首行。");
680
641
  const TOOL_SPECS = [
681
642
  REMEMBER_TOOL,
682
643
  RECALL_TOOL,
@@ -685,40 +646,40 @@ const TOOL_SPECS = [
685
646
  key: "kb_add",
686
647
  name: "kb_add",
687
648
  description: "新增一篇文档入库。**只新增、不修改**:同一 `(domain, source, title)` 已存在时会被**拒绝**。给 text 或 source_uri 是单篇(text 是粘贴文本,source_uri 是本地文件路径或 http(s) URL,可摄入文本、PDF、pandoc 能读的文档格式与 .xlsx);给 paths 是批量导入文件或目录(目录只收 .md/.markdown/.txt/.json/.jsonl/.yaml/.yml/.pdf/.docx/.docm/.odt/.epub/.html/.htm/.xhtml/.tex/.rst/.ipynb/.csv/.tsv/.org/.rtf/.xlsx,不跟随符号链接)。",
688
- input: z$1.object({
649
+ input: z.object({
689
650
  domain: KbDomainField,
690
651
  source: KbSourceField,
691
652
  title: KbTitleField,
692
- text: z$1.string().min(1).optional().describe("要入库的文本;与 source_uri、paths 三选一。"),
693
- source_uri: z$1.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库。"),
694
- paths: z$1.array(z$1.string().min(1)).min(1).optional().describe("批量导入的文件/目录路径(≥1);给了它就是批量导入。")
653
+ text: z.string().min(1).optional().describe("要入库的文本;与 source_uri、paths 三选一。"),
654
+ source_uri: z.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库。"),
655
+ paths: z.array(z.string().min(1)).min(1).optional().describe("批量导入的文件/目录路径(≥1);给了它就是批量导入。")
695
656
  })
696
657
  },
697
658
  {
698
659
  key: "kb_list",
699
660
  name: "kb_list",
700
661
  description: "列出知识库里的文档:doc_id、domain、source、title、**受管 .md 文件的绝对路径**与更新时间;给了 doc_id 时只取该篇。",
701
- input: z$1.object({
702
- doc_id: z$1.number().int().positive().optional().describe("给了 `doc_id` 时只取该篇;可选。"),
703
- domain: z$1.string().optional().describe("限定知识域;可选,仅在未给 doc_id 时有意义。"),
704
- source: z$1.string().optional().describe("限定来源;可选,仅在未给 doc_id 时有意义。"),
705
- limit: z$1.number().int().positive().optional().describe("本页最多返回多少篇(按 updated_at 倒序);可选,缺省 50。"),
706
- offset: z$1.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
662
+ input: z.object({
663
+ doc_id: z.number().int().positive().optional().describe("给了 `doc_id` 时只取该篇;可选。"),
664
+ domain: z.string().optional().describe("限定知识域;可选,仅在未给 doc_id 时有意义。"),
665
+ source: z.string().optional().describe("限定来源;可选,仅在未给 doc_id 时有意义。"),
666
+ limit: z.number().int().positive().optional().describe("本页最多返回多少篇(按 updated_at 倒序);可选,缺省 50。"),
667
+ offset: z.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
707
668
  })
708
669
  },
709
670
  {
710
671
  key: "kb_remove",
711
672
  name: "kb_remove",
712
673
  description: "删除一篇文档,连同它在 `knowledge.docs.dir` 下的受管 .md 副本。",
713
- input: z$1.object({ doc_id: z$1.number().int().positive().describe("要删除的文档 ID。") })
674
+ input: z.object({ doc_id: z.number().int().positive().describe("要删除的文档 ID。") })
714
675
  },
715
676
  {
716
677
  key: "kb_reindex",
717
678
  name: "kb_reindex",
718
679
  description: "重建知识库索引(分块/实体/向量)。可选限定 domain。",
719
- input: z$1.object({
720
- domain: z$1.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
721
- dry_run: z$1.boolean().optional().describe("true=只报告将重建多少,不写入;可选,默认 false。")
680
+ input: z.object({
681
+ domain: z.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
682
+ dry_run: z.boolean().optional().describe("true=只报告将重建多少,不写入;可选,默认 false。")
722
683
  })
723
684
  },
724
685
  QUERY_TOOL
@@ -899,8 +860,14 @@ function errorCode(error) {
899
860
  * `~/` expansion and boolean env parsing (previously duplicated in core paths,
900
861
  * both model adapters, and the config loader).
901
862
  */
902
- /** Expand a leading `~/` against the USER home directory. */
863
+ /** Expand `~/…` (and a BARE `~`) against the USER home directory.
864
+ *
865
+ * The bare form matters: `AVANTF_HOME=~` used to come back verbatim, i.e. as the RELATIVE path `~`,
866
+ * and everything built on it was written into the current working directory. The base kit's own
867
+ * `expandHome` always expanded it, so the two copies disagreed on exactly the input a shell makes
868
+ * easy to type. */
903
869
  function expandHome$1(p) {
870
+ if (p === "~") return homedir();
904
871
  return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
905
872
  }
906
873
  /** Parse a boolean env var: `0`/`false` → false, unset/empty → undefined, else true. */
@@ -937,21 +904,232 @@ function envAutoDownload() {
937
904
  return project;
938
905
  }
939
906
  //#endregion
907
+ //#region lib/types/prompt.js
908
+ /**
909
+ * The three system-prompt sections this plugin contributes (DESIGN §10).
910
+ *
911
+ * The TEXTS are user-editable: each section's body lives in its own `.md` under the shared family
912
+ * prompt directory `<data_home>/prompts` (`PROMPT_FILES` maps file → section), and the constants
913
+ * below are both the built-in default and what a missing/blank file is filled with. The identity —
914
+ * section name and order — stays in code, so editing a file cannot silently move a section in the
915
+ * prompt. The loader itself is generic and lives in the family base (`PromptFiles`, exported by
916
+ * `@avantf/dsh-plugin-base`), because "ensure the file exists, otherwise use the default" is the same
917
+ * flow for every section — and taking it from the base at RUNTIME means fixing it takes one base
918
+ * release, not a plugin rebuild. When the base is unavailable the built-in defaults below are used
919
+ * and nothing is written to disk.
920
+ *
921
+ * They exist for something a tool description cannot carry: a description says what a call DOES,
922
+ * never when to reach for it unprompted, and the default behaviour is to wait until asked. So each
923
+ * says only WHEN its store is worth writing or reading. Neither says anything about how memory is
924
+ * retained — that stays out of every model-facing string, held to the contract's
925
+ * `RETENTION_VOCABULARY`, the same list the tool descriptions are held to.
926
+ *
927
+ * The knowledge one is the section whose ABSENCE was measured: a user had just ingested
928
+ * 《Cgroup v2 技术综述》(which answers "cgroup v2 的内存保护" at score 1.0) and asked about it in a
929
+ * session whose prompt named only the memory tools, so the model never looked. A tool description
930
+ * cannot fix that by itself — `kb_query`'s says what the call does and even leads with 记忆 facts,
931
+ * which reads as a memory tool — and the model cannot know the library is relevant to a question
932
+ * without being told to check it. Hence the "先检索一次再作答" nudge, plus the explicit
933
+ * "你看不到库里有什么" clause: the thing that made the miss invisible is that nothing in the
934
+ * prompt acknowledged the store existed at all.
935
+ *
936
+ * `order` 3000/3010 are third-party slots: after the harness's per-tool guidance block (1000–2900)
937
+ * and before its toolset-level guidance (5000). They are not repository-owned placements, so they
938
+ * do not resolve through `getSectionOrder`.
939
+ *
940
+ * The sections say WHAT TO DO and never WHY. The reasoning behind each rule is measured and
941
+ * lives here, in DESIGN §10 and in the CHANGELOG — not in the prompt, where it costs tokens on
942
+ * every step and does not change the action.
943
+ *
944
+ * Kept in its own module — not in `index.ts` — so the text can be read and checked without the
945
+ * plugin entry's `@deepseek-ai/*` runtime imports, which resolve only in a harness workspace.
946
+ */
947
+ const MEMORY_PROMPT_SECTION = {
948
+ name: "avantf:memory-usage",
949
+ order: 3e3,
950
+ text: "记忆(`mem_remember`):把**重要且跨会话仍成立**的事实主动记下来——用户偏好、项目决策、环境约束、接口约定、踩过的坑。同一事实的修正用 `update`,一句自包含的陈述。不要记录临时聊天内容、一次性任务的中间状态、能直接从代码或文档读到的内容。"
951
+ };
952
+ /**
953
+ * WHEN to consult the document knowledge base. See the module comment for why this is a separate
954
+ * section rather than a clause inside the memory one: the two stores answer different questions,
955
+ * and a session that needs the knowledge base may have no memory question at all.
956
+ */
957
+ const KNOWLEDGE_PROMPT_SECTION = {
958
+ name: "avantf:knowledge-usage",
959
+ order: 3010,
960
+ text: "知识库(`kb_query` / `kb_add`)存放**用户提供的成篇资料**:文档、长说明、规范、综述等要按原文查阅的内容。回答涉及已入库资料的问题之前,先用 `kb_query` 检索;命中就按 `source_ref` 引用原文。用户给了值得长期留存的成篇内容就主动 `kb_add` 入库;一句话的事实属于记忆(`mem_remember`),不要入知识库。"
961
+ };
962
+ /**
963
+ * Where each section's text comes from: one `.md` per section, inside the SHARED family prompt
964
+ * directory (`<data_home>/prompts`, see {@link PromptFiles}).
965
+ *
966
+ * Every avantf plugin writes its prompts into that one directory and owns a prefix — `mem-*` here,
967
+ * `work-*` in the work engine — so a deployment can find and diff all of its model-facing text in
968
+ * one place without any plugin having to guess which files belong to it. This manifest is that
969
+ * ownership: a `.md` it does not list is ignored (never read, never written, never deleted).
970
+ *
971
+ * The FILE owns the text; the CODE keeps the identity. Mapping a file name to a section HERE is what
972
+ * makes that true: no edit to the directory can move a section to a different point in the prompt
973
+ * (order) or change the name the harness dedupes on — and no file another plugin owns can be
974
+ * mistaken for one of these.
975
+ *
976
+ * `fallback` is taken from the constant above rather than retyped, so "the text that used to be
977
+ * hardcoded" and "the text a missing file is filled with" cannot drift apart.
978
+ */
979
+ const PROMPT_FILES = [
980
+ {
981
+ file: "mem-memory-usage.md",
982
+ section: MEMORY_PROMPT_SECTION
983
+ },
984
+ {
985
+ file: "mem-knowledge-usage.md",
986
+ section: KNOWLEDGE_PROMPT_SECTION
987
+ },
988
+ {
989
+ file: "mem-kb-edit.md",
990
+ section: {
991
+ name: "avantf:kb-edit",
992
+ order: 3020,
993
+ text: "更新知识库时,要在原文上修改或追加,不要新建一个补充文档。用 `kb_list` 拿到受管 `.md` 的绝对路径,用文件工具修改。在用户明确要求时使用 `kb_remove` 删除。"
994
+ }
995
+ }
996
+ ];
997
+ /** The generic loader's input: which file, and the body to write/use when it is missing or blank. */
998
+ function promptFileSpecs() {
999
+ return PROMPT_FILES.map(({ file, section }) => ({
1000
+ file,
1001
+ fallback: section.text
1002
+ }));
1003
+ }
1004
+ /**
1005
+ * Zip the loaded texts back onto their sections, in manifest order.
1006
+ *
1007
+ * A section whose text is missing from `loaded` (the caller loaded a partial manifest) keeps its
1008
+ * built-in default rather than losing its prompt entirely.
1009
+ */
1010
+ function buildPromptSections(loaded) {
1011
+ const textByFile = new Map(loaded.map((entry) => [entry.file, entry.text]));
1012
+ return PROMPT_FILES.map(({ file, section }) => ({
1013
+ name: section.name,
1014
+ order: section.order,
1015
+ text: textByFile.get(file) ?? section.text
1016
+ }));
1017
+ }
1018
+ /** Explanatory phrasings the defaults are forbidden to contain; the spec guards the same shapes. */
1019
+ const WHY_WORDS = [
1020
+ "因为",
1021
+ "否则",
1022
+ "原因是",
1023
+ "之所以"
1024
+ ];
1025
+ /**
1026
+ * What is worth saying about text the USER wrote — as warnings, never as edits.
1027
+ *
1028
+ * The defaults are held to hard rules by `prompt_section.spec.ts`: under the budget (they are
1029
+ * rendered into every model step), no retention vocabulary, no explanatory "why". An edited file
1030
+ * cannot be held to them — it is the user's prompt — but all three are MEASURED regressions rather
1031
+ * than style, so a line in the log is cheap and a silently worse prompt is not. Nothing is
1032
+ * truncated and nothing is refused: the file is injected exactly as written.
1033
+ */
1034
+ function promptTextWarnings(sections) {
1035
+ const out = [];
1036
+ for (const section of sections) {
1037
+ const text = typeof section.text === "string" ? section.text : "";
1038
+ if (text.length >= 400) out.push(`${section.name} is ${String(text.length)} chars (every model step carries it; the built-in default is under ${String(400)})`);
1039
+ for (const word of RETENTION_VOCABULARY) if (text.includes(word)) out.push(`${section.name} describes retention ("${word}"), which invites rewriting a fact to refresh it`);
1040
+ for (const word of WHY_WORDS) if (text.includes(word)) out.push(`${section.name} explains why ("${word}"); the defaults state actions only`);
1041
+ }
1042
+ return out;
1043
+ }
1044
+ //#endregion
1045
+ //#region lib/types/hints.js
1046
+ /**
1047
+ * The plugin's CONDITIONAL hints: one line per store, contributed only when that store already
1048
+ * holds something relevant to what the user just sent (DESIGN §12).
1049
+ *
1050
+ * These are deliberately NOT part of the always-on usage sections. A usage section says WHEN a
1051
+ * store is worth consulting and therefore has to be paid for on every step of every session; this
1052
+ * says "there is something here, right now", which is only true sometimes. The two layers compose:
1053
+ * the section is the fallback for the paraphrase the lexical probe misses, and the hint is the
1054
+ * concrete nudge for the case it catches.
1055
+ *
1056
+ * ONE LINE PER STORE, never a merged sentence. The two stores answer different questions and
1057
+ * either can be the relevant one, so `RelevanceHit` is a closed four-valued answer and each store
1058
+ * gets its own contribution — a caller can then see, and later tune or drop, one without the other.
1059
+ *
1060
+ * WHY EACH LINE NAMES THE PLUGIN. This text is injected as runtime context, and the harness
1061
+ * materialises that as a USER-role snapshot in the conversation (`dsh-agent-loop`'s
1062
+ * `runtimeContext.project`). Without an author, a line like "记忆里有相关内容" reads as something
1063
+ * the user said. The `[avantf-mem 插件]` prefix is what keeps the provenance honest.
1064
+ */
1065
+ const MEMORY_HINT = "[avantf-mem 插件] 记忆里有与本次提问相关的事实;需要时用 `mem_recall` 检索。";
1066
+ const KNOWLEDGE_HINT = "[avantf-mem 插件] 知识库里有与本次提问相关的内容;需要时用 `kb_query` 检索。";
1067
+ /**
1068
+ * Map one answer to the two contributions, as text.
1069
+ *
1070
+ * An empty string is the "contribute nothing" value: `renderPrompt`/`renderContextSections` drop
1071
+ * empty text, so `none` (and the missing half of `memory`/`knowledge`) costs no tokens at all.
1072
+ */
1073
+ function hintLines(hit) {
1074
+ return {
1075
+ memory: hit === "memory" || hit === "both" ? MEMORY_HINT : "",
1076
+ knowledge: hit === "knowledge" || hit === "both" ? KNOWLEDGE_HINT : ""
1077
+ };
1078
+ }
1079
+ /**
1080
+ * The plain text of an inbox message, or `''` when it carries none.
1081
+ *
1082
+ * `dsh-agent-loop`'s own extractor returns `undefined` unless the message is exactly ONE text
1083
+ * block; a steered or multi-part message would then silently produce no hint. Concatenating the
1084
+ * text blocks keeps the probe working for those, and non-text blocks (images) are simply skipped —
1085
+ * the probe is lexical, so it has nothing to say about them anyway.
1086
+ */
1087
+ function messageText(message) {
1088
+ const content = message?.content;
1089
+ if (!Array.isArray(content)) return "";
1090
+ return content.map((block) => {
1091
+ const b = block;
1092
+ return b?.type === "text" && typeof b.text === "string" ? b.text : "";
1093
+ }).filter(Boolean).join("\n");
1094
+ }
1095
+ //#endregion
940
1096
  //#region ../core/lib/config/paths.js
941
1097
  /**
942
1098
  * Resolve the data home (default `~/.avantf`).
943
1099
  *
944
- * Precedence, per DESIGN §3: an EXPLICIT caller value (layer ⑤) → `AVANTF_HOME`
945
- * (layer ④) → the configured/default `dataHome`. `common.dataHome` cannot stand in
946
- * for layer ⑤: the schema gives it a default, so it always holds a value and would
947
- * make an explicit argument indistinguishable from "nothing was configured" —
948
- * which is exactly how the env var used to win over a caller's own `dataHome`.
1100
+ * Precedence, per DESIGN §3 and the family layering (root `AGENTS.md` 「边界与路径」: built-in →
1101
+ * profile → store → ENV → explicit): an EXPLICIT caller value (⑤) → `AVANTF_HOME` (④) → the
1102
+ * configured `common.dataHome` (②) → `~/.avantf`. The environment is a deployment override and
1103
+ * outranks the config file.
1104
+ *
1105
+ * `configured` cannot stand in for layer ⑤: the schema gives it a default, so it always holds a
1106
+ * value and would make an explicit argument indistinguishable from "nothing was configured" — which
1107
+ * is exactly how the env var used to win over a caller's own `dataHome`.
1108
+ *
1109
+ * The slots are NAMED for the same reason the base kit's copy is: the two implementations used to
1110
+ * disagree about their POSITIONAL order (`(explicit, env, common)` here, `(common, explicit)`…
1111
+ * whichever way a reader guessed), so "which slot does the profile's `dataHome` belong in?" was
1112
+ * answerable only by reading the call site. The base kit implements the same rule with the same named
1113
+ * shape, and `packages/plugin/test/family_pin.spec.ts` pins the two implementations together.
1114
+ */
1115
+ function resolveDataHome(input) {
1116
+ const layer5 = input.explicit?.trim() ?? "";
1117
+ const fromEnv = process.env["AVANTF_HOME"]?.trim() ?? "";
1118
+ const layer2 = input.common.dataHome?.trim() ?? "";
1119
+ return expandHome$1(layer5 !== "" ? layer5 : fromEnv !== "" ? fromEnv : layer2 !== "" ? layer2 : "~/.avantf");
1120
+ }
1121
+ /**
1122
+ * Every config file lives in ONE directory, beside the prompt files.
1123
+ *
1124
+ * The layering is unchanged (① built-in → ② common → ③ store → ④ env → ⑤ explicit); only the
1125
+ * locations moved, so all of a deployment's editable text — `configs/*.yaml` and `prompts/*.md` —
1126
+ * is in one place instead of scattered through `memory/` and `knowledge/` next to the databases.
949
1127
  */
950
- function resolveDataHome(common, explicit) {
951
- return expandHome$1(explicit || process.env["AVANTF_HOME"] || common.dataHome || "~/.avantf");
1128
+ function configsDir(home) {
1129
+ return join(home, "configs");
952
1130
  }
953
1131
  function commonConfigPath(home) {
954
- return join(home, "config.yaml");
1132
+ return join(configsDir(home), "common.yaml");
955
1133
  }
956
1134
  function memoryDir(home) {
957
1135
  return join(home, "memory");
@@ -960,10 +1138,10 @@ function knowledgeDir(home) {
960
1138
  return join(home, "knowledge");
961
1139
  }
962
1140
  function memoryConfigPath(home) {
963
- return join(memoryDir(home), "config.yaml");
1141
+ return join(configsDir(home), "memory.yaml");
964
1142
  }
965
1143
  function knowledgeConfigPath(home) {
966
- return join(knowledgeDir(home), "config.yaml");
1144
+ return join(configsDir(home), "knowledge.yaml");
967
1145
  }
968
1146
  function memoryDbPath(home, override) {
969
1147
  const fromEnv = process.env["AVANTF_MEM_DB"];
@@ -988,6 +1166,97 @@ function knowledgeDocsDir(home, override) {
988
1166
  function ensureDataLayout(home) {
989
1167
  mkdirSync(memoryDir(home), { recursive: true });
990
1168
  mkdirSync(knowledgeDir(home), { recursive: true });
1169
+ mkdirSync(configsDir(home), { recursive: true });
1170
+ }
1171
+ //#endregion
1172
+ //#region ../core/lib/config/config_files.js
1173
+ /**
1174
+ * The default `configs/common.yaml`, written when the file is missing or blank.
1175
+ *
1176
+ * It is **comments only**, on purpose: the built-in defaults are already in force, so an all-comment
1177
+ * file changes NOTHING about how the engine behaves — it only puts the knobs, their defaults and the
1178
+ * two managed keys' escape hatches where a reader will find them. Uncommenting a line is the edit;
1179
+ * deleting the file brings the defaults straight back.
1180
+ *
1181
+ * The rule mirrors the prompt files (the plugins' `src/prompt_files.ts`): missing or blank is an
1182
+ * unfinished edit, not an empty configuration, so it is filled in. A file with ANY content is the
1183
+ * user's and is never rewritten.
1184
+ *
1185
+ * Keys must match the schema in `@avantf/mem-contract` (`ConfigSchema`): an unknown key in an
1186
+ * UNCOMMENTED line is warned about by the loader, so a wrong name here would be a trap rather than a
1187
+ * hint. `config_files.spec.ts` pins the ones this template names.
1188
+ */
1189
+ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。这里全是注释:删掉/留空 = 用内建默认,取消注释即生效。
1190
+ # 分层(低→高):① 内建默认 → ② 本文件 → ③ 同目录的 memory.yaml / knowledge.yaml(缺失=不覆盖)
1191
+ # → ④ 环境变量 → ⑤ 插件/CLI 显式传参
1192
+ # 所有可编辑文本都在 ~/.avantf 下:configs/*.yaml(本目录)与 prompts/*.md(模型提示词)。
1193
+
1194
+ # 数据根不在这里配:它由 ④ AVANTF_HOME(或 CLI --data-home、插件 profile 的 dataHome)决定 ——
1195
+ # 本文件就在数据根里面,解析根的时候还没读到它,所以写在这里的 dataHome 对数据根无效(DESIGN §3)。
1196
+
1197
+ # semantic: # 嵌入后端(记忆与知识共用,跨库分数才可比)
1198
+ # backend: local_bge
1199
+ # local_model: Xenova/bge-small-zh-v1.5 # 必须是 ONNX 仓库
1200
+ # dim: 512
1201
+ # auto_download: true
1202
+ # ⚠️ cache_dir / mirror 由家族底座 @avantf/dsh-plugin-base 管理:写在这里会被忽略并告警。
1203
+ # 缓存目录用环境变量 AVANTF_MEM_MODEL_CACHE,镜像用 AVANTF_MEM_MODEL_MIRROR(或 HF_ENDPOINT)。
1204
+
1205
+ # rerank: # 重排的唯一开关;none = 不加载任何重排模型
1206
+ # backend: none
1207
+ # local_model: Xenova/bge-reranker-base
1208
+
1209
+ # vectorStore:
1210
+ # backend: auto # auto = 越过阈值自动从 local_numpy 迁到 hnswlib
1211
+ # auto_thresholds:
1212
+ # hnswlib: 2000
1213
+ # hnswlib_ef_search: 256 # ANN 的召回/速度旋钮,语料变大先调它
1214
+
1215
+ # retriever: # 三条检索腿在融合里的权重
1216
+ # weight_semantic: 0.55
1217
+ # weight_fts: 0.30
1218
+ # weight_jaccard: 0.15
1219
+
1220
+ # lifecycle:
1221
+ # purge_after_archived_days: 365 # 归档事实的物理清理窗口(活跃日)
1222
+ # contradiction_threshold: 0.6
1223
+
1224
+ # trust:
1225
+ # decay_per_day: 0.0055…
1226
+ # presence:
1227
+ # heartbeat_minutes: 60 # 常驻进程的心跳(活跃日时钟与生命周期扫描)
1228
+
1229
+ # tools: # 外部二进制(pandoc)的受管目录
1230
+ # dir: '' # 留空 = 家族根 tools(~/.avantf/env/tools);AVANTF_TOOLS_DIR 可覆盖
1231
+ # auto_install: true
1232
+ `;
1233
+ /**
1234
+ * Ensure the common config exists with content, and hand back the path either way.
1235
+ *
1236
+ * Never throws: an unwritable data home must not keep the engine (or the plugin mount) from working
1237
+ * — the built-in defaults are always available, and a missing file is the same thing to the loader
1238
+ * as an empty one.
1239
+ */
1240
+ function ensureCommonConfigFile(path, logger) {
1241
+ try {
1242
+ mkdirSync(dirname(path), { recursive: true });
1243
+ if (existsSync(path)) {
1244
+ if (readFileSync(path, "utf8").replace(/^\uFEFF/u, "").trim() !== "") return;
1245
+ writeAtomic(path, DEFAULT_COMMON_CONFIG);
1246
+ logger?.info(`config file was blank and has been filled with the default: ${path}`);
1247
+ return;
1248
+ }
1249
+ writeAtomic(path, DEFAULT_COMMON_CONFIG);
1250
+ logger?.info(`config file created with the default (all comments): ${path}`);
1251
+ } catch (error) {
1252
+ logger?.warn(`config file ${path} is unusable (${error instanceof Error ? error.message : String(error)}); using the built-in defaults`);
1253
+ }
1254
+ }
1255
+ /** Temp file + rename, the same shape the managed document copy and the store config use. */
1256
+ function writeAtomic(path, text) {
1257
+ const tmp = `${path}.tmp-${String(process.pid)}`;
1258
+ writeFileSync(tmp, text, "utf8");
1259
+ renameSync(tmp, path);
991
1260
  }
992
1261
  //#endregion
993
1262
  //#region ../core/lib/config/loader.js
@@ -1163,13 +1432,27 @@ function applyEnvLayer(common) {
1163
1432
  return next;
1164
1433
  }
1165
1434
  /**
1166
- * Load config with layering: ① built-in defaults → ② `~/.avantf/config.yaml`
1435
+ * Load config with layering: ① built-in defaults → ② `~/.avantf/configs/common.yaml`
1167
1436
  * (common) → ③ `~/.avantf/{memory,knowledge}/config.yaml` (store) → ④ env →
1168
1437
  * ⑤ explicit. Returns the resolved per-store config + data home.
1169
1438
  */
1170
1439
  function loadConfig(opts) {
1171
1440
  const logger = opts?.logger ?? defaultLogger;
1172
- let common = { ...defaultConfig };
1441
+ let common = {
1442
+ ...defaultConfig,
1443
+ tools: {
1444
+ ...defaultConfig.tools,
1445
+ dir: defaultConfig.tools.dir === "" ? familyToolsDir$1() : defaultConfig.tools.dir
1446
+ },
1447
+ semantic: {
1448
+ ...defaultConfig.semantic,
1449
+ cache_dir: defaultConfig.semantic.cache_dir === "" ? familyModelsDir() : defaultConfig.semantic.cache_dir
1450
+ },
1451
+ rerank: {
1452
+ ...defaultConfig.rerank,
1453
+ cache_dir: defaultConfig.rerank.cache_dir === "" ? familyModelsDir() : defaultConfig.rerank.cache_dir
1454
+ }
1455
+ };
1173
1456
  const managed = opts?.managedRoots;
1174
1457
  if (managed !== void 0) common = {
1175
1458
  ...common,
@@ -1190,8 +1473,12 @@ function loadConfig(opts) {
1190
1473
  ...common,
1191
1474
  dataHome: opts.dataHome
1192
1475
  });
1193
- const home = resolveDataHome(common, opts?.dataHome);
1476
+ const home = resolveDataHome({
1477
+ common,
1478
+ explicit: opts?.dataHome
1479
+ });
1194
1480
  ensureDataLayout(home);
1481
+ ensureCommonConfigFile(commonConfigPath(home), logger);
1195
1482
  const commonYaml = readYamlOrNull(commonConfigPath(home));
1196
1483
  if (commonYaml) {
1197
1484
  stripManagedModelKeys(commonYaml, commonConfigPath(home), logger);
@@ -1236,7 +1523,7 @@ function loadConfig(opts) {
1236
1523
  }
1237
1524
  /** The built-in `tools` section (layer ①); the config schema defaults to exactly this shape. */
1238
1525
  const DEFAULT_TOOLS_CONFIG = {
1239
- dir: "~/.avantf/tools",
1526
+ dir: "",
1240
1527
  mirror: [
1241
1528
  "https://ghfast.top/{url}",
1242
1529
  "https://ghproxy.net/{url}",
@@ -1251,8 +1538,24 @@ const DEFAULT_TOOLS_CONFIG = {
1251
1538
  * tool already here?" before any runtime has installed its settings — which is what keeps a startup
1252
1539
  * sweep from re-downloading a tool that is already on disk.
1253
1540
  */
1254
- function defaultToolsDir() {
1255
- return expandHome(DEFAULT_TOOLS_CONFIG.dir);
1541
+ function defaultToolsDir(env = process.env) {
1542
+ return familyToolsDir(env);
1543
+ }
1544
+ /**
1545
+ * The family root and its managed directories — `$AVANTF_HOME` when set, else `~/.avantf/env`.
1546
+ *
1547
+ * Mirrored from `@avantf/mem-contract`'s `familyHome()` / `familyToolsDir()`: this package is
1548
+ * deliberately dependency-free (node builtins only), so it cannot import them, and
1549
+ * `packages/core/test/family_paths.spec.ts` pins the two copies together so the convention cannot
1550
+ * drift. `@avantf/dsh-plugin-base` installs everything it manages under this one root.
1551
+ */
1552
+ function familyHome(env = process.env) {
1553
+ const configured = env["AVANTF_HOME"];
1554
+ return configured !== void 0 && configured.trim() !== "" ? expandHome(configured.trim()) : join(homedir(), ".avantf", "env");
1555
+ }
1556
+ /** The managed external-binary root (`<family root>/tools`). */
1557
+ function familyToolsDir(env = process.env) {
1558
+ return join(familyHome(env), "tools");
1256
1559
  }
1257
1560
  /**
1258
1561
  * The project-wide download kill switch (`AVANTF_MEM_AUTO_DOWNLOAD=0`), the same variable the model
@@ -1294,7 +1597,7 @@ function resolveToolsDir(config, env = process.env) {
1294
1597
  const fromEnv = env["AVANTF_TOOLS_DIR"];
1295
1598
  if (fromEnv !== void 0 && fromEnv !== "") return expandHome(fromEnv);
1296
1599
  if (config.dir !== "") return expandHome(config.dir);
1297
- return expandHome(DEFAULT_TOOLS_CONFIG.dir);
1600
+ return defaultToolsDir(env);
1298
1601
  }
1299
1602
  /**
1300
1603
  * Read the `tools` section out of an UNVALIDATED config object (the value comes from YAML, so every
@@ -1624,21 +1927,43 @@ async function extractArchive(pack, archive, destination, artifact) {
1624
1927
  * Atomically publish a fully-built directory as the artifact's version directory.
1625
1928
  *
1626
1929
  * `renameSync` inside one filesystem is atomic, so a reader (another process, or the next `ensure`)
1627
- * sees either the old state or the complete new one — never a half-written tool. A pre-existing
1628
- * target is REPLACED: a retry after a failed verification must not be blocked by the wreckage of the
1629
- * previous attempt.
1930
+ * sees either the old tree or the complete new one — never a half-written tool. A pre-existing target
1931
+ * is REPLACED, and it is moved ASIDE rather than deleted first: `rm -rf` on a real tool tree runs for
1932
+ * hundreds of milliseconds, and for that whole window the version directory did not exist, so a
1933
+ * concurrently started pandoc got ENOENT and retried the install. Two renames shorten that window to
1934
+ * microseconds, and if the second one fails the old tree is moved back — a failed publish leaves the
1935
+ * previous install in place instead of nothing.
1936
+ *
1937
+ * What this does NOT claim: there is no cross-process LOCK. Two processes can install the same
1938
+ * version at once and both pay for the download and the extract; the second publish wins (each
1939
+ * staging tree is complete, so the result is a complete `bin/<binary>` either way). A lock would have
1940
+ * to detect its own stale holders and would have to tell the waiter when the winner is done, which is
1941
+ * a coordination protocol rather than a rename — out of scope here, and recorded instead of implied.
1942
+ *
1943
+ * A crash between the two renames leaves the old tree under `<target>.old-<pid>-<rand>` beside the
1944
+ * version directory. Nothing enumerates that parent, so the residue is inert; the next install of the
1945
+ * same version replaces the target normally.
1630
1946
  */
1631
1947
  function publishAtomically(staging, target, artifact) {
1632
1948
  mkdirSync(dirname(target), { recursive: true });
1633
- rmSync(target, {
1634
- recursive: true,
1635
- force: true
1636
- });
1949
+ const moved = existsSync(target) ? `${target}.old-${String(process.pid)}-${Math.random().toString(36).slice(2, 8)}` : null;
1950
+ if (moved !== null) try {
1951
+ renameSync(target, moved);
1952
+ } catch (error) {
1953
+ throw new ProvisionError("extract", artifact, `替换旧版本失败(${target} → ${moved}):${describeError$1(error)}`);
1954
+ }
1637
1955
  try {
1638
1956
  renameSync(staging, target);
1639
1957
  } catch (error) {
1958
+ if (moved !== null) try {
1959
+ renameSync(moved, target);
1960
+ } catch {}
1640
1961
  throw new ProvisionError("extract", artifact, `原子落盘失败(${staging} → ${target}):${describeError$1(error)}`);
1641
1962
  }
1963
+ if (moved !== null) rmSync(moved, {
1964
+ recursive: true,
1965
+ force: true
1966
+ });
1642
1967
  }
1643
1968
  function makeScratch(toolsDir, id) {
1644
1969
  const base = `${toolsDir}/.tmp`;
@@ -2763,16 +3088,18 @@ async function parseQueryPattern(query) {
2763
3088
  //#endregion
2764
3089
  //#region ../core/lib/modelBootstrap.js
2765
3090
  /**
2766
- * This module does NOT import the family base `@avantf/dsh-compat` — and nothing else does either.
3091
+ * This module does NOT import the family base `@avantf/dsh-plugin-base` — and nothing else in the
3092
+ * engine does either.
2767
3093
  *
2768
- * `@avantf/mem` is inlined into the plugin bundle, and the base is a *devDependency of the plugin*
2769
- * (the source of the declared range only): the family framework `@avantf/dsh-envinit` fetches it
2770
- * into its managed root at startup and the plugin loads THAT copy by file URL
2771
- * (`plugin/src/envinit.ts`). A static import here would therefore put a top-level
2772
- * `import "@avantf/dsh-compat"` in the artifact that resolves nowhere — the artifact would throw
2773
- * `ERR_MODULE_NOT_FOUND` during module evaluation, before any of our code ran. The plugin computes
2774
- * the verdict itself (from the framework-provided base) and hands it over as PLAIN DATA; this
2775
- * module only reads it (see AGENTS.md, «The `@avantf/*` family dependency is provisioned»).
3094
+ * `@avantf/mem` is inlined into the plugin bundle, and the base is a **peer of the plugin** (plus a
3095
+ * `devDependency` that is only the source of the declared range): the plugin's inlined zero-dependency
3096
+ * bootstrap resolves the installed base at startup and loads it by file URL (`plugin/src/envinit.ts`).
3097
+ * A static import here would therefore put a top-level `import "@avantf/dsh-plugin-base"` in the
3098
+ * artifact that may resolve nowhere — the artifact would throw `ERR_MODULE_NOT_FOUND` during module
3099
+ * evaluation, before any of our code ran, which is exactly the failure the family forbids ("the base
3100
+ * missing must degrade the plugin, never prevent it from loading"). The plugin computes the verdict
3101
+ * itself (off the runtime-loaded base) and hands it over as PLAIN DATA; this module only reads it
3102
+ * (see AGENTS.md, «The three family hard constraints»).
2776
3103
  */
2777
3104
  const COMPAT_PREFIX = "compat:";
2778
3105
  /** Hard cap on the host-readiness wait (see {@link WarmOptions.waitForCapMs}). */
@@ -2945,7 +3272,7 @@ function runtimeOf(artifactEnv) {
2945
3272
  * It used to be a parallel `warmModelsAsync(...)` call beside the tools sweep, which meant two
2946
3273
  * startup mechanisms with two logging vocabularies and no single place that answered "is this host
2947
3274
  * fully provisioned?". The model has no pinned version and no managed directory of its own ON THIS
2948
- * LEGACY PATH (the transformers.js cache under the legacy `~/.avantf/models` is the layout), so it
3275
+ * (the flat `<repo>/<file>` layout the transformers.js cache reads), so it
2949
3276
  * declares neither; its `isPresent` is `false` because "is the model warm?" is not a question that
2950
3277
  * can be answered without attempting the warm-up, and the warm-up is cheap when the cache is
2951
3278
  * already there (measured ~1.2 s, in the background). The DSH plugin uses this sweep only when the
@@ -3521,7 +3848,7 @@ var LocalBgeBackend = class {
3521
3848
  this.pipeFactory = pipeFactory;
3522
3849
  this.env = {
3523
3850
  mirror: process.env["AVANTF_MEM_MODEL_MIRROR"] ?? process.env["HF_ENDPOINT"] ?? env?.mirror ?? "https://hf-mirror.com",
3524
- cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? env?.cacheDir ?? "~/.avantf/models",
3851
+ cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? blankToFamilyModels$1(env?.cacheDir),
3525
3852
  autoDownload: envAutoDownload() ?? env?.autoDownload ?? true
3526
3853
  };
3527
3854
  this.gate = new WarmGate(() => this.env.autoDownload);
@@ -3696,6 +4023,10 @@ function applyModelEnv(mod, env) {
3696
4023
  mod.env.allowRemoteModels = env.autoDownload;
3697
4024
  mod.env.allowLocalModels = true;
3698
4025
  }
4026
+ /** A blank `cache_dir` means "not configured": the family root's `models`, never the pre-framework path. */
4027
+ function blankToFamilyModels$1(configured) {
4028
+ return configured !== void 0 && configured.trim() !== "" ? configured : familyModelsDir();
4029
+ }
3699
4030
  //#endregion
3700
4031
  //#region ../retrieval-core/lib/adapters/local_reranker.js
3701
4032
  /** Rerank pairs are classified in batches to bound peak memory. */
@@ -3735,7 +4066,7 @@ var LocalReranker = class {
3735
4066
  this.pipeFactory = pipeFactory;
3736
4067
  this.env = {
3737
4068
  mirror: process.env["AVANTF_MEM_MODEL_MIRROR"] ?? process.env["HF_ENDPOINT"] ?? env?.mirror ?? "https://hf-mirror.com",
3738
- cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? env?.cacheDir ?? "~/.avantf/models",
4069
+ cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? blankToFamilyModels(env?.cacheDir),
3739
4070
  autoDownload: envAutoDownload() ?? env?.autoDownload ?? true
3740
4071
  };
3741
4072
  this.gate = new WarmGate(() => this.env.autoDownload);
@@ -3831,6 +4162,10 @@ async function createClassifier(model, env) {
3831
4162
  applyModelEnv(mod, env);
3832
4163
  return await mod.pipeline("text-classification", model);
3833
4164
  }
4165
+ /** A blank `cache_dir` means "not configured": the family root's `models`, never the pre-framework path. */
4166
+ function blankToFamilyModels(configured) {
4167
+ return configured !== void 0 && configured.trim() !== "" ? configured : familyModelsDir();
4168
+ }
3834
4169
  //#endregion
3835
4170
  //#region ../retrieval-core/lib/adapters/local_numpy.js
3836
4171
  /**
@@ -4764,7 +5099,7 @@ const PLAIN_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
4764
5099
  * Resolving the pandoc binary for the converter.
4765
5100
  *
4766
5101
  * The binary is NOT this package's business to install — `@avantf/mem-provision` owns that, the
4767
- * pinned version, the mirror list, and the `~/.avantf/tools` layout. What this module adds is the
5102
+ * pinned version, the mirror list, and the managed-tools layout. What this module adds is the
4768
5103
  * converter's view of it: one place that knows how to ask for the executable, where an explicit
4769
5104
  * override goes, and why a missing pandoc is a hard failure rather than a fallback.
4770
5105
  *
@@ -4796,7 +5131,7 @@ function setPandocProvisioning(settings) {
4796
5131
  /** The current settings, defaulting to the managed directory + the package's mirror defaults. */
4797
5132
  function pandocProvisioning() {
4798
5133
  return provisioning ?? {
4799
- toolsDir: join(homedir(), ".avantf", "tools"),
5134
+ toolsDir: defaultToolsDir(),
4800
5135
  mirror: [],
4801
5136
  autoInstall: true
4802
5137
  };
@@ -5712,7 +6047,7 @@ function buildFtsQuery(query, tokenizer = resolveFtsTokenizer()) {
5712
6047
  } else if (clean.length >= 2) parts.push(`"${clean}"`);
5713
6048
  }
5714
6049
  if (!parts.length) return null;
5715
- return parts.join(" OR ");
6050
+ return [...new Set(parts)].slice(0, 64).join(" OR ");
5716
6051
  }
5717
6052
  //#endregion
5718
6053
  //#region ../core/lib/db/conn.js
@@ -6793,12 +7128,20 @@ var FactsDao = class FactsDao {
6793
7128
  linkSupersede(factId, supersedesId) {
6794
7129
  this.db.prepare("UPDATE facts SET supersedes_id = ? WHERE fact_id = ? AND supersedes_id IS NULL").run(supersedesId, factId);
6795
7130
  }
6796
- /** Restore an archived fact, settling its trust and refreshing the idle clock (spec §2.6). */
7131
+ /**
7132
+ * Restore an archived fact, settling its trust and refreshing the idle clock (spec §2.6).
7133
+ *
7134
+ * `AND status = 'archived'` is load-bearing, like the guard on every sibling above: this UPDATE
7135
+ * clears the archive bookkeeping and refreshes `last_retrieved_at`, so ungated it would take an
7136
+ * ACTIVE row and (a) push its idle clock forward — silently extending the life of a fact the next
7137
+ * tick was about to re-archive — and (b) reset `trust_score` to the settled value it computed. It
7138
+ * returned `1` either way, so the caller could not tell that a row it never asked about had moved.
7139
+ */
6797
7140
  restore(factId, trust, clock) {
6798
7141
  return this.db.prepare(`UPDATE facts
6799
7142
  SET status = 'active', archived_at = NULL, archive_reason = NULL, archived_clock = NULL,
6800
7143
  trust_score = ?, settle_clock = ?, last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
6801
- WHERE fact_id = ?`).run(trust, clock, factId).changes;
7144
+ WHERE fact_id = ? AND status = 'archived'`).run(trust, clock, factId).changes;
6802
7145
  }
6803
7146
  bumpHelpful(factId, delta) {
6804
7147
  this.db.prepare("UPDATE facts SET helpful_count = MAX(0, helpful_count + ?) WHERE fact_id = ?").run(delta, factId);
@@ -8416,7 +8759,7 @@ var MemoryStore = class {
8416
8759
  if (rows.length === 0) return {
8417
8760
  logged: [],
8418
8761
  checked: 0,
8419
- pending: 0
8762
+ pending: this.facts.countPendingConflicts()
8420
8763
  };
8421
8764
  this.conflictDrainCursor = rows[rows.length - 1].fact_id;
8422
8765
  const checked = this.contradictions.checkMany(rows.map((r) => r.fact_id));
@@ -9397,6 +9740,20 @@ var ChunksDao = class {
9397
9740
  this.db.prepare("UPDATE doc_chunks SET semantic_vector = ?, embedding_model = ? WHERE chunk_id = ?").run(bytes, embeddingModel, chunkId);
9398
9741
  }
9399
9742
  /**
9743
+ * The doc's chunks WITH their stored vectors — the reuse index for a re-ingest.
9744
+ *
9745
+ * A re-ingest deletes and re-inserts every chunk (new ids), so without this the only way to keep a
9746
+ * vector is to re-encode the same text; `vectorReusable` already knows how to tell "unchanged", but
9747
+ * only `reindex` could use it. Read BEFORE the replace, inside its transaction.
9748
+ */
9749
+ vectorStateForDoc(docId) {
9750
+ return this.db.prepare(`SELECT dc.chunk_id AS chunk_id, dc.text AS text, dc.content_hash AS content_hash,
9751
+ dc.embedding_model AS embedding_model, dc.entities_version AS entities_version,
9752
+ (dc.semantic_vector IS NOT NULL) AS has_vector, dc.semantic_vector AS vec
9753
+ FROM doc_chunks dc
9754
+ WHERE dc.doc_id = ?`).all(docId);
9755
+ }
9756
+ /**
9400
9757
  * Write a batch of vectors in ONE transaction.
9401
9758
  *
9402
9759
  * `setVector` is autocommit: each chunk costs a commit (measured 14.2 µs/row against 2.2 µs in a
@@ -9507,8 +9864,11 @@ var ChunksDao = class {
9507
9864
  * Entity-overlap candidates, most-shared first and CAPPED (see the memory store's sibling).
9508
9865
  *
9509
9866
  * `limit` is applied per name-batch and the batches are unioned, so the result is bounded by
9510
- * `limit x ceil(names/batch)` — still corpus-independent, which is the property that matters:
9511
- * the caller only ever keeps `overFetch` entries after fusion.
9867
+ * `limit x ceil(names/batch)` — corpus-independent, which is what this query can promise. The
9868
+ * caller is expected to enforce the real `cap` on the union AFTER scoring it (it re-computes the
9869
+ * true Jaccard in JS): a union of per-batch tops is not a top-N, and a leg that returns more than
9870
+ * the cap makes `scores.size === legCap` — the "was this leg trimmed" signal — report the opposite
9871
+ * of the truth. (The memory store's sibling does not batch, so its single `LIMIT` is already exact.)
9512
9872
  */
9513
9873
  candidatesByEntityNames(names, domain, source, limit) {
9514
9874
  if (limit <= 0) return [];
@@ -9983,10 +10343,10 @@ async function documentText(bytes, label) {
9983
10343
  /**
9984
10344
  * Where these knobs live. The boundary belongs to the knowledge store (only it
9985
10345
  * ingests), so it sits in the per-store config — naming it in the errors matters,
9986
- * because the obvious place to look is the common `~/.avantf/config.yaml`, where
10346
+ * because the obvious place to look is the common `~/.avantf/configs/common.yaml`, where
9987
10347
  * the `knowledge.` prefix is a key the loader will reject as unknown.
9988
10348
  */
9989
- const KB_CONFIG_FILE = "~/.avantf/knowledge/config.yaml";
10349
+ const KB_CONFIG_FILE = "~/.avantf/configs/knowledge.yaml";
9990
10350
  /** Strip IPv6 brackets and the FQDN root dot — `localhost.` and `[::1]` are `localhost` and `::1`. */
9991
10351
  function normalizeHost(hostname) {
9992
10352
  return (hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname).toLowerCase().replace(/\.$/, "");
@@ -10230,6 +10590,11 @@ function describePaths(paths) {
10230
10590
  files: paths.length
10231
10591
  };
10232
10592
  }
10593
+ /**
10594
+ * The listing shape is the contract's {@link BrowseListing} (re-exported above for this package's
10595
+ * callers). The engine fills it in and the host Remote method declares that same interface as its
10596
+ * payload, so the shape has exactly one definition and one check.
10597
+ */
10233
10598
  function insideRoots(path, roots, allowAnywhere) {
10234
10599
  if (allowAnywhere) return true;
10235
10600
  return roots.some((root) => path === root || path.startsWith(root.endsWith(sep) ? root : root + sep));
@@ -10685,7 +11050,7 @@ var GitRepo = class {
10685
11050
  //#region ../core/lib/config/domains.js
10686
11051
  /**
10687
11052
  * Append one 知识域 to the STORE config's top-level `domains` allowlist
10688
- * (`~/.avantf/knowledge/config.yaml`, layer ③ of `loader.ts`) and return the new list.
11053
+ * (`~/.avantf/configs/knowledge.yaml`, layer ③ of `loader.ts`) and return the new list.
10689
11054
  *
10690
11055
  * This is the write half of the UI-only `kbAddDomain` path, so the user adds a domain once in the
10691
11056
  * 知识 tab instead of hand-editing YAML. It writes to the STORE config rather than the common one:
@@ -11150,6 +11515,7 @@ var KnowledgeStore = class {
11150
11515
  const result = this.db.transaction(() => {
11151
11516
  const docId = this.docs.upsert(domain, source, docTitle, sourceUri);
11152
11517
  const oldChunkIds = this.chunks.idsForDoc(docId);
11518
+ const reusable = this.reusableByHash(docId, this.vectorSpace());
11153
11519
  this.chunks.deleteForDoc(docId);
11154
11520
  this.docs.touch(docId, sourceUri);
11155
11521
  return {
@@ -11162,11 +11528,12 @@ var KnowledgeStore = class {
11162
11528
  sourceRef: `${domain}:${source}:${docId}:${idx}`,
11163
11529
  charStart: chunk.start,
11164
11530
  charEnd: chunk.end
11165
- })))
11531
+ }))),
11532
+ reusable
11166
11533
  };
11167
11534
  })();
11168
11535
  this.evictVectors(result.oldChunkIds);
11169
- const indexed = await this.indexChunks(result.newChunkIds);
11536
+ const indexed = await this.indexChunks(result.newChunkIds, result.reusable);
11170
11537
  return {
11171
11538
  doc_id: result.doc_id,
11172
11539
  chunks: chunks.length,
@@ -11246,22 +11613,88 @@ var KnowledgeStore = class {
11246
11613
  * Extract entities per chunk (persisted) and encode/persist chunk vectors.
11247
11614
  *
11248
11615
  * Returns how many chunks did NOT get a vector, so the caller can put it in the tool result. A
11249
- * missing model counts every chunk: the document is still searchable by text and entity, but not
11250
- * semantically, and "入库成功 N 段" must not imply otherwise.
11616
+ * missing model counts every chunk that had no reusable vector: the document is still searchable by
11617
+ * text and entity, but not semantically, and "入库成功 N 段" must not imply otherwise.
11251
11618
  */
11252
- async indexChunks(chunkIds) {
11619
+ async indexChunks(chunkIds, reusable) {
11253
11620
  if (!chunkIds.length) return { vectors_failed: 0 };
11254
11621
  const rows = this.chunks.texts(chunkIds);
11255
11622
  await this.replaceChunkEntities(rows);
11256
11623
  this.chunks.setEntitiesVersion(rows.map((r) => r.chunk_id), 1);
11624
+ const space = this.vectorSpace();
11625
+ const { remaining } = this.writeReusedVectors(rows, space, reusable);
11257
11626
  if (!this.semantic.isAvailable()) {
11258
11627
  this.semantic.ensureWarm?.();
11259
- return { vectors_failed: rows.length };
11628
+ return { vectors_failed: remaining.length };
11260
11629
  }
11261
- const { failed } = await this.encodeAndStore(rows, this.vectorSpace());
11630
+ const { failed } = await this.encodeAndStore(remaining, space);
11262
11631
  return { vectors_failed: failed };
11263
11632
  }
11264
11633
  /**
11634
+ * The doc's stored vectors that still APPLY, keyed by content hash (see {@link vectorReusable}).
11635
+ *
11636
+ * Called inside the ingest transaction, before the replace: two chunks with the same text are one
11637
+ * hash, and the first row wins — the reuse is only a cache lookup, so a duplicate entry cannot be
11638
+ * distinguished from a hit and need not be.
11639
+ */
11640
+ reusableByHash(docId, space) {
11641
+ const out = /* @__PURE__ */ new Map();
11642
+ for (const row of this.chunks.vectorStateForDoc(docId)) {
11643
+ if (row.vec === null || row.content_hash === null) continue;
11644
+ if (!this.vectorReusable(row, space)) continue;
11645
+ if (!out.has(row.content_hash)) out.set(row.content_hash, row.vec);
11646
+ }
11647
+ return out;
11648
+ }
11649
+ /**
11650
+ * Write back the vectors a re-ingest can reuse, and report which rows still need the model.
11651
+ *
11652
+ * A row is only reused when the lookup misses nothing: any row whose write fails, or whose text has
11653
+ * no reusable vector, comes back in `remaining` and is encoded as before — so the reuse path cannot
11654
+ * leave a chunk unindexed that the plain path would have indexed.
11655
+ */
11656
+ writeReusedVectors(rows, space, reusable) {
11657
+ if (reusable === void 0 || reusable.size === 0) return {
11658
+ reused: 0,
11659
+ remaining: [...rows]
11660
+ };
11661
+ const remaining = [];
11662
+ const writes = [];
11663
+ for (const r of rows) {
11664
+ const bytes = reusable.get(contentHash(r.text));
11665
+ if (bytes === void 0) remaining.push(r);
11666
+ else writes.push({
11667
+ chunk_id: r.chunk_id,
11668
+ bytes,
11669
+ embeddingModel: space,
11670
+ text: r.text
11671
+ });
11672
+ }
11673
+ if (writes.length === 0) return {
11674
+ reused: 0,
11675
+ remaining
11676
+ };
11677
+ try {
11678
+ this.chunks.setVectors(writes);
11679
+ } catch {
11680
+ return {
11681
+ reused: 0,
11682
+ remaining: [...remaining, ...writes.map((w) => ({
11683
+ chunk_id: w.chunk_id,
11684
+ text: w.text
11685
+ }))]
11686
+ };
11687
+ }
11688
+ for (const w of writes) {
11689
+ const vec = bytesToFloat32(w.bytes);
11690
+ if (vec !== null && vec.length === this.vstore.dim) this.vstore.add(w.chunk_id, vec);
11691
+ }
11692
+ return {
11693
+ reused: writes.length,
11694
+ remaining
11695
+ };
11696
+ }
11697
+ /**
11265
11698
  * Encode texts ONE AT A TIME, writing every `WRITE_BATCH` vectors in one transaction.
11266
11699
  *
11267
11700
  * The model call is deliberately NOT batched, and that is a MEASURED decision, not an omission:
@@ -11298,6 +11731,7 @@ var KnowledgeStore = class {
11298
11731
  encoded += writes.length;
11299
11732
  } catch {
11300
11733
  failed += writes.length;
11734
+ this.evictVectors(writes.map((w) => w.chunk_id));
11301
11735
  }
11302
11736
  }
11303
11737
  if (failed > 0) retrievalLogger().warn(`kb index: ${String(failed)} of ${String(rows.length)} chunk vector(s) were not written — run kb_reindex to retry (the text/entity/FTS indexes are complete; only the semantic vectors are affected)`);
@@ -11771,7 +12205,8 @@ var KnowledgeStore = class {
11771
12205
  const jaccard = [...qEntities].filter((e) => factSet.has(e)).length / union.size;
11772
12206
  if (jaccard > 0) out.set(id, jaccard);
11773
12207
  }
11774
- return out;
12208
+ if (out.size <= cap) return out;
12209
+ return new Map([...out].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, cap));
11775
12210
  }
11776
12211
  };
11777
12212
  const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.+?)\s*$/;
@@ -12187,7 +12622,13 @@ const resultSymbol = (method) => fieldSymbol(method, "result");
12187
12622
  *
12188
12623
  * Carries BOTH members on purpose: the host's validator moved from `codec.schema.parse` (0.1.5) to
12189
12624
  * `codec.create()` (0.1.6), and each generation checks only its own. One shape therefore serves
12190
- * either host — the same "one shape satisfies two revisions" trick the job plugin's `strict()` uses.
12625
+ * either host.
12626
+ *
12627
+ * Kept LOCAL — a two-line mirror of the base kit's `strictCodec`, not a runtime call — on purpose:
12628
+ * the Remote face is built at module load and must exist even when the base cannot be loaded at all,
12629
+ * so its construction may not depend on a runtime `import()` of the base. This is the "mirrors a
12630
+ * convention, so changing it takes a plugin release" side of the rule in the root AGENTS.md; the
12631
+ * shared, fixable-by-a-base-release code lives in `@avantf/dsh-plugin-base`'s kit.
12191
12632
  */
12192
12633
  function strict(schema, typeSymbol) {
12193
12634
  return {
@@ -12207,11 +12648,11 @@ function parameter(method, name, schema, acceptsUndefined) {
12207
12648
  ...acceptsUndefined === void 0 ? {} : { acceptsUndefined }
12208
12649
  };
12209
12650
  }
12210
- const text = z$1.string();
12211
- const count = z$1.number().int().optional();
12212
- const optionalText = z$1.string().optional();
12213
- const bool = z$1.boolean().optional();
12214
- const textArray = z$1.array(z$1.string()).optional();
12651
+ const text = z.string();
12652
+ const count = z.number().int().optional();
12653
+ const optionalText = z.string().optional();
12654
+ const bool = z.boolean().optional();
12655
+ const textArray = z.array(z.string()).optional();
12215
12656
  /** One direct Remote method carrying a single `args` object parameter. */
12216
12657
  function direct(method, args) {
12217
12658
  return {
@@ -12221,11 +12662,11 @@ function direct(method, args) {
12221
12662
  method,
12222
12663
  invocation: { kind: "direct" },
12223
12664
  parameters: [parameter(method, "args", args)],
12224
- result: strict(z$1.any(), resultSymbol(method))
12665
+ result: strict(z.any(), resultSymbol(method))
12225
12666
  };
12226
12667
  }
12227
12668
  const descriptors = [
12228
- direct("remember", z$1.object({
12669
+ direct("remember", z.object({
12229
12670
  action: optionalText.optional(),
12230
12671
  content: optionalText.optional(),
12231
12672
  fact_id: count.optional(),
@@ -12233,7 +12674,7 @@ const descriptors = [
12233
12674
  ttl_days: count.optional(),
12234
12675
  reason: optionalText.optional()
12235
12676
  })),
12236
- direct("recall", z$1.object({
12677
+ direct("recall", z.object({
12237
12678
  action: optionalText.optional(),
12238
12679
  query: optionalText.optional(),
12239
12680
  entity: optionalText.optional(),
@@ -12246,7 +12687,7 @@ const descriptors = [
12246
12687
  limit: count.optional(),
12247
12688
  max_tokens: count.optional()
12248
12689
  })),
12249
- direct("admin", z$1.object({
12690
+ direct("admin", z.object({
12250
12691
  action: optionalText.optional(),
12251
12692
  fact_id: count.optional(),
12252
12693
  category: optionalText.optional(),
@@ -12259,7 +12700,7 @@ const descriptors = [
12259
12700
  resolution: optionalText.optional(),
12260
12701
  loser_fact_id: count.optional()
12261
12702
  })),
12262
- direct("kb", z$1.object({
12703
+ direct("kb", z.object({
12263
12704
  action: optionalText.optional(),
12264
12705
  text: optionalText.optional(),
12265
12706
  source_uri: optionalText.optional(),
@@ -12274,7 +12715,7 @@ const descriptors = [
12274
12715
  overwrite: bool.optional(),
12275
12716
  adopt: bool.optional()
12276
12717
  })),
12277
- direct("query", z$1.object({
12718
+ direct("query", z.object({
12278
12719
  query: text,
12279
12720
  kind: optionalText.optional(),
12280
12721
  domain: optionalText.optional(),
@@ -12282,11 +12723,11 @@ const descriptors = [
12282
12723
  limit: count.optional(),
12283
12724
  max_tokens: count.optional()
12284
12725
  })),
12285
- direct("classifySource", z$1.object({ text: optionalText.optional() })),
12286
- direct("browseDir", z$1.object({ path: optionalText.optional() })),
12287
- direct("kbDomains", z$1.object({})),
12288
- direct("kbAddDomain", z$1.object({ domain: text })),
12289
- direct("openDoc", z$1.object({
12726
+ direct("classifySource", z.object({ text: optionalText.optional() })),
12727
+ direct("browseDir", z.object({ path: optionalText.optional() })),
12728
+ direct("kbDomains", z.object({})),
12729
+ direct("kbAddDomain", z.object({ domain: text })),
12730
+ direct("openDoc", z.object({
12290
12731
  doc_id: count,
12291
12732
  target: optionalText.optional()
12292
12733
  }))
@@ -12303,9 +12744,13 @@ function declared(name, schema) {
12303
12744
  * Host face: registered by the host half through `ctx.typert.register()`.
12304
12745
  *
12305
12746
  * The schema assertions name `TypertSchema['schema']` — the FIELD they are assigned to — rather
12306
- * than this package's own `z.ZodType`. The two are different zod copies: the plugin pins
12307
- * zod 4.4.3, the harness bundles 4.6.2, and `ZodType` gained members between them, so a cast to
12308
- * the local `z.ZodType` could never be assignable and the annotation said nothing. Through
12747
+ * than this package's own `z.ZodType`. There are two PHYSICALLY different zod copies in play —
12748
+ * the workspace's (root catalog `zod: 4.6.5`, the one the plugin compiles against) and the one
12749
+ * inside the installed harness build — and two copies of the same major still have incompatible
12750
+ * type identities (the single-`zod` rule in the root `AGENTS.md` is the same lesson), so a
12751
+ * cast to the local `z.ZodType` could never be assignable and the annotation said nothing. Never
12752
+ * write a version number here again: it moves with the catalog, and a stale pair sends the next
12753
+ * reader to reason from the wrong premise. Through
12309
12754
  * `TypertSchema['schema']` the assertion is at least against the type the field actually
12310
12755
  * requires, and a future zod alignment (which would make the cast unnecessary) shows up here.
12311
12756
  */
@@ -12341,40 +12786,35 @@ const hostContribution = {
12341
12786
  invocations: descriptors
12342
12787
  };
12343
12788
  //#endregion
12344
- //#region lib/types/provision.js
12789
+ //#region lib/types/data_home.js
12345
12790
  /**
12346
- * This plugin's compatibility spec, and the wiring that runs it.
12347
- *
12348
- * The rules, the probes, the report and the post-registration check all live in
12349
- * `@avantf/dsh-compat` (shared with the rest of the family). What stays here is only what is true of
12350
- * THIS plugin: the services and methods it calls, the dsh packages that identify the host, the wire
12351
- * schema names and tool names that must be visible afterwards, the events it listens to, and the
12352
- * Chinese report a user reads. See `DESIGN.md` §12.1.
12353
- *
12354
- * **Who obtains the base.** Not this module. The family framework `@avantf/dsh-envinit` provisions
12355
- * `@avantf/dsh-compat` as the `mem:compat` manifest item and hands back the directory it landed in
12356
- * (`src/envinit.ts`); this module is given the loaded module and derives everything from it. That is
12357
- * the whole point of the framework: acquisition (resolve → managed copy → verified download →
12358
- * atomic publish) is one shared mechanism, and the plugin only declares what it needs.
12791
+ * The plugin's data root, resolved through the FAMILY's layers.
12359
12792
  *
12360
- * The base therefore has NO import path of its own here — not even a dynamic one. A static `import`
12361
- * would throw `ERR_MODULE_NOT_FOUND` while the artifact was being evaluated (the base is not a
12362
- * runtime dependency of the published plugin any more), and a bare dynamic `import()` would bypass
12363
- * the framework's managed copy. The plugin loads the module the framework resolved, by file URL.
12793
+ * The profile's `config.dataHome` is CONFIGURATION, so it takes the CONFIGURED layer: ⑤ an explicit
12794
+ * caller value → ④ `$AVANTF_HOME` → ② this configured value → `~/.avantf`. Handing it straight to the
12795
+ * engine's explicit slot instead — which is what this plugin used to do — promotes ② above ④, and the
12796
+ * same profile then resolves to two different directories depending on which half of the family reads
12797
+ * it: the work plugin resolves the same knob through its configured layer, so with `$AVANTF_HOME` set
12798
+ * work read `<env>/prompts` while this plugin read `<config>/prompts`. That directory is SHARED (both
12799
+ * plugins' editable prompt files live in it), so "the file you edited is read by one plugin and not
12800
+ * the other" is a real failure, not a nuance. `AGENTS.md` 「边界与路径」 states the rule this
12801
+ * implements: the configured value travels in the config layer, never in the explicit one.
12364
12802
  *
12365
- * The plugin computes the gate's verdict here and hands it to the engine as plain data
12366
- * (`{ load, status, reason }`), so the inlined engine bundle never imports the base either.
12803
+ * The engine's own copy of the rule does the work (it is the one the CLI and MCP resolve with, and
12804
+ * `test/family_pin.spec.ts` pins it against the base kit's). This module exists so the plugin's USE of
12805
+ * it is testable without the DSH harness: the entry module imports `@deepseek-ai/*` at load, which
12806
+ * resolves only inside a harness workspace.
12367
12807
  *
12368
- * @module @avantf/dsh-mem/provision
12808
+ * @module data_home
12369
12809
  */
12370
- /** The base package id, named here too so the item declaration and the spec need no import. */
12371
- const COMPAT_PACKAGE = "@avantf/dsh-compat";
12372
- /**
12373
- * Fallback range for `@avantf/dsh-compat`, used only when `package.json` has lost the dependency
12374
- * entry. The manifest is the single source of truth (`compatRange()`); `scripts/pack-plugin.mjs`
12375
- * pins this constant to it so the two cannot drift.
12376
- */
12377
- const COMPAT_RANGE = "^0.1.0";
12810
+ /** Layer ② for the configured value; `$AVANTF_HOME` (④) and any explicit value (⑤) still outrank it. */
12811
+ function configDataHome(configured) {
12812
+ return resolveDataHome({ common: { dataHome: configured ?? "" } });
12813
+ }
12814
+ //#endregion
12815
+ //#region lib/types/provision.js
12816
+ /** The base package id, named here so the gate's messages need no import of it. */
12817
+ const COMPAT_PACKAGE = "@avantf/dsh-plugin-base";
12378
12818
  /**
12379
12819
  * The services this plugin calls, with the methods it calls on them.
12380
12820
  *
@@ -12438,27 +12878,6 @@ function warn(log, message) {
12438
12878
  } catch {}
12439
12879
  }
12440
12880
  /**
12441
- * The range of `@avantf/dsh-compat` this build is written against.
12442
- *
12443
- * Read from THIS plugin's manifest, which is the single source of truth — the framework does not
12444
- * pick it, and a hardcoded constant would silently keep provisioning an old range after the family
12445
- * publishes a new one. The constant is only a fallback for a manifest that lost the entry.
12446
- *
12447
- * It lives in `devDependencies`, deliberately. Nothing imports the base: the framework fetches it
12448
- * into its own managed home and this plugin loads it from there, so a RUNTIME dependency would
12449
- * install a second copy on every consumer that no code path ever loads. `devDependencies` still
12450
- * ships in the packed manifest (pnpm keeps it, rewriting `catalog:` like any other field), so the
12451
- * range travels with the artifact; it just is not installed.
12452
- */
12453
- async function compatRange(manifestUrl = new URL("../package.json", import.meta.url)) {
12454
- try {
12455
- const raw = JSON.parse(await readFile(manifestUrl, "utf8"));
12456
- const declared = raw.devDependencies?.["@avantf/dsh-compat"] ?? raw.dependencies?.["@avantf/dsh-compat"];
12457
- if (typeof declared === "string" && declared.trim() !== "") return declared;
12458
- } catch {}
12459
- return COMPAT_RANGE;
12460
- }
12461
- /**
12462
12881
  * Build the plugin-specific half of the gate from a loaded base.
12463
12882
  *
12464
12883
  * Called ONCE per process (the loaded runtime is cached by `src/envinit.ts`), which is what makes the
@@ -12556,7 +12975,8 @@ function registerCompatMegaphone(ctx, verdict, log, compat) {
12556
12975
  heading: "记忆插件未加载:与当前 dsh 的兼容性检查未通过,插件已拒绝加载(不注册服务、工具、prompt 段与真实 typert face)。",
12557
12976
  warningsLabel: "风险提示:",
12558
12977
  warnings: verdict.warnings,
12559
- fix: "修复:在本仓库重建(`pnpm build:dsh`),或把 `@deepseek-ai/dsh` 换回本构建声明的版本。"
12978
+ fix: "修复:在本仓库重建(`pnpm build:dsh`),或把 `@deepseek-ai/dsh` 换回本构建声明的版本。",
12979
+ logPointer: (prefix) => `完整诊断见宿主日志里 ${prefix} 开头的行。`
12560
12980
  })
12561
12981
  });
12562
12982
  }
@@ -12577,7 +12997,7 @@ function verifyRegisteredFaces(input) {
12577
12997
  toolNames: input.toolNames
12578
12998
  });
12579
12999
  }
12580
- const PACKAGE = "@avantf/dsh-envinit";
13000
+ const PACKAGE = "@avantf/dsh-plugin-base";
12581
13001
  function parseVersion(input) {
12582
13002
  const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(input.trim());
12583
13003
  if (match === null) return void 0;
@@ -12803,7 +13223,7 @@ async function ensureFramework(options) {
12803
13223
  warn(`supportedRange must be a string; got ${typeof options.supportedRange}`);
12804
13224
  return;
12805
13225
  }
12806
- const supported = options.supportedRange ?? "^0.1.3";
13226
+ const supported = options.supportedRange ?? ">=0.3.0 <1.0.0";
12807
13227
  let dir;
12808
13228
  try {
12809
13229
  dir = dirname(createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`));
@@ -12889,32 +13309,112 @@ function rangeOf(value) {
12889
13309
  return typeof range === "string" && range.trim() !== "" ? range : void 0;
12890
13310
  }
12891
13311
  //#endregion
13312
+ //#region lib/types/interface_gate.js
13313
+ /** Read one member without letting a hostile module's getter escape. */
13314
+ function member(module, name) {
13315
+ try {
13316
+ return module?.[name];
13317
+ } catch {
13318
+ return;
13319
+ }
13320
+ }
13321
+ function cannotTell(reason) {
13322
+ return {
13323
+ status: "cannot-tell",
13324
+ reason
13325
+ };
13326
+ }
13327
+ /**
13328
+ * Whether the base's shared capabilities may be used under `verdict`.
13329
+ *
13330
+ * The ONE mapping from the gate's verdict to the loader's decision, exported so it is asserted
13331
+ * directly and the loader and its test cannot drift: only `incompatible` withholds the base;
13332
+ * `cannot-tell` does not, because it is not a negative answer. A `false` here is still a DEGRADATION
13333
+ * (own prompt defaults, gate skipped, legacy provisioning), never a refused mount.
13334
+ */
13335
+ function baseIsUsable(verdict) {
13336
+ return verdict.status !== "incompatible";
13337
+ }
13338
+ /**
13339
+ * Where the build's bake record sits when no explicit URL is given: beside the built ENTRY
13340
+ * (`lib/index.js`), which is where `scripts/link-envinit.mjs` writes it. A per-file `tsc` emit of
13341
+ * this module lives one directory below that entry and looks one level up; the first readable record
13342
+ * wins. Under `vitest` (running from `src/`) neither exists, which is the correct "not baked".
13343
+ */
13344
+ const BAKED_CANDIDATES = ["./interface-version.json", "../interface-version.json"];
13345
+ /**
13346
+ * Read this artifact's baked requirement through the loaded base's own reader.
13347
+ * @param read - the base's `readInterfaceRequirement`, taken off the loaded module.
13348
+ * @param bakedUrl - test seam: read EXACTLY this record instead of walking {@link BAKED_CANDIDATES}.
13349
+ */
13350
+ function requirementOf(read, bakedUrl) {
13351
+ const candidates = bakedUrl === void 0 ? BAKED_CANDIDATES.map((relative) => new URL(relative, import.meta.url)) : [bakedUrl];
13352
+ for (const url of candidates) {
13353
+ let requirement;
13354
+ try {
13355
+ requirement = read(url);
13356
+ } catch {
13357
+ continue;
13358
+ }
13359
+ const version = requirement?.interfaceVersion;
13360
+ if (typeof version === "number" && Number.isInteger(version) && version > 0) return { interfaceVersion: version };
13361
+ }
13362
+ }
13363
+ /**
13364
+ * Ask the loaded base whether it implements the interface generation this artifact was built for.
13365
+ *
13366
+ * @param module - the loaded base module (only `checkInterface` / `readInterfaceRequirement` are
13367
+ * touched, each through a guarded read).
13368
+ * @param bakedUrl - test seam: the bake record to read; production leaves it undefined.
13369
+ * @returns a verdict that is never `incompatible` unless BOTH sides were readable and differ, and
13370
+ * never a throw.
13371
+ */
13372
+ function interfaceVerdict(module, bakedUrl) {
13373
+ const check = member(module, "checkInterface");
13374
+ const read = member(module, "readInterfaceRequirement");
13375
+ if (typeof check !== "function" || typeof read !== "function") return cannotTell("the loaded base has no interface gate (checkInterface / readInterfaceRequirement missing) — it predates the runtime interface contract, so which generation it implements cannot be told");
13376
+ const requirement = requirementOf(read, bakedUrl);
13377
+ if (requirement === void 0) return cannotTell("this build has no baked interface requirement (lib/interface-version.json is missing or malformed), so the runtime interface gate cannot run");
13378
+ let verdict;
13379
+ try {
13380
+ verdict = check(requirement.interfaceVersion, module);
13381
+ } catch {
13382
+ return cannotTell("the loaded base threw while judging the interface generation");
13383
+ }
13384
+ const status = verdict?.status;
13385
+ if (status !== "ok" && status !== "incompatible" && status !== "cannot-tell") return cannotTell("the loaded base returned no usable interface verdict");
13386
+ return verdict;
13387
+ }
13388
+ //#endregion
12892
13389
  //#region lib/types/envinit.js
12893
13390
  /**
12894
- * Environment initialisation for the memory plugin, through the family framework
12895
- * `@avantf/dsh-envinit`.
13391
+ * Environment initialisation for the memory plugin, through the family base
13392
+ * `@avantf/dsh-plugin-base`.
12896
13393
  *
12897
- * The shape is fixed by the framework's DESIGN §7.2 and is the same for every plugin:
13394
+ * The shape is fixed by the base's DESIGN §7.2 and is the same for every plugin:
12898
13395
  *
12899
- * bootstrap (inlined) → load the framework → register + declare → wait for the `blocking` set →
12900
- * dispatch the `background` set → the plugin's own pre-mount check.
13396
+ * bootstrap (inlined) → load the base → register + declare the remaining resources → dispatch the
13397
+ * `background` set → the plugin's own pre-mount check.
12901
13398
  *
12902
- * What this plugin needs, in three phases:
13399
+ * What this plugin needs, in two phases:
12903
13400
  *
12904
- * 1. **`mem:compat`** (`npm-package`, `startup: 'blocking'`) — the family compatibility gate
12905
- * `@avantf/dsh-compat`. It has to be there BEFORE anything is registered, because a proven
12906
- * incompatible host must refuse the mount with nothing half-registered. This replaces the
12907
- * bespoke self-heal the plugin used to run itself (`@avantf/mem-provision`'s
12908
- * `ensureCompatBase`), whose private root (`<dataHome>/dsh-compat/**`) the framework
12909
- * deliberately does not read.
13401
+ * 1. **The gate, straight from the base** — the family compatibility gate is part of
13402
+ * `@avantf/dsh-plugin-base` and arrived WITH the module the bootstrap loaded. There is no
13403
+ * `mem:compat` item, no npm download and no managed `<home>/compat/**` copy any more; the base's
13404
+ * `compat` module provides the rules, probes, report and post-registration check, and
13405
+ * `runtimeFromCompat(framework)` derives this plugin's spec from it. The gate has to be runnable
13406
+ * BEFORE anything is registered, because a proven incompatible host must refuse the mount with
13407
+ * nothing half-registered. This replaces the bespoke self-heal the plugin used to run itself
13408
+ * (`@avantf/mem-provision`'s `ensureCompatBase`), whose private root (`<dataHome>/dsh-compat/**`)
13409
+ * the base deliberately does not read.
12910
13410
  * 2. **`mem:pandoc`** (`binary-archive`, `startup: 'background'`) — the documented "expensive but
12911
13411
  * not startup-blocking" resource. It is declared AFTER the runtime exists, because its spec
12912
13412
  * comes from the resolved config (`tools.auto_install`, `tools.mirror`), and it is dispatched
12913
13413
  * without waiting: the caller continues its own initialisation and hooks the outcome through
12914
13414
  * `onSettled`.
12915
13415
  * 3. **`mem:model`** (`model-cache`, `startup: 'background'`) — the embedding model. It uses the
12916
- * framework's `flat` layout, whose files land at `<root>/<repo>/<file>` — exactly the shape
12917
- * `@huggingface/transformers` reads under its `cacheDir` — so the runtime loads the framework's
13416
+ * base's `flat` layout, whose files land at `<root>/<repo>/<file>` — exactly the shape
13417
+ * `@huggingface/transformers` reads under its `cacheDir` — so the runtime loads the base's
12918
13418
  * copy instead of fetching a second one. For the default repo the file list is the narrow set
12919
13419
  * the runtime actually opens (see {@link MODEL_FILES}); any other repo omits the list, because
12920
13420
  * a fixed six-file list made the item fail for a repo whose file set differs. The endpoint is
@@ -12925,24 +13425,23 @@ function rangeOf(value) {
12925
13425
  *
12926
13426
  * Two hard rules:
12927
13427
  *
12928
- * - **The framework is never statically imported.** Only `import type` and the inlined bootstrap
13428
+ * - **The base is never statically imported.** Only `import type` and the inlined bootstrap
12929
13429
  * are static; the value comes from `loadFramework()`. A top-level value import would throw before
12930
13430
  * the bootstrap ran — exactly the broken-tree case the bootstrap exists for.
12931
- * - **Nothing here refuses the mount.** "Cannot tell" is not "incompatible": a missing framework,
13431
+ * - **Nothing here refuses the mount.** "Cannot tell" is not "incompatible": a missing base,
12932
13432
  * an unavailable gate or a failed resource is a WARNING, and the plugin mounts with its tools,
12933
13433
  * service, prompt and Remote face intact.
12934
13434
  *
12935
13435
  * @module @avantf/dsh-mem/envinit
12936
13436
  */
12937
- /** The framework package, as declared in this plugin's `peerDependencies`. */
12938
- const FRAMEWORK_PACKAGE = "@avantf/dsh-envinit";
13437
+ /** The family base package, as declared in this plugin's `peerDependencies`. */
13438
+ const FRAMEWORK_PACKAGE = "@avantf/dsh-plugin-base";
12939
13439
  /** The plugin namespace every item is declared under (`<plugin>:<name>`, framework DESIGN §13.4). */
12940
13440
  const PLUGIN = "mem";
12941
13441
  /**
12942
13442
  * The item ids. `resolve()` / `onSettled` are keyed by them, so the DSH plugin compares against these
12943
13443
  * names rather than re-spelling literals.
12944
13444
  */
12945
- const COMPAT_ITEM = "mem:compat";
12946
13445
  const PANDOC_ITEM = "mem:pandoc";
12947
13446
  const MODEL_ITEM = "mem:model";
12948
13447
  /**
@@ -13040,70 +13539,6 @@ function provisionLogger(log) {
13040
13539
  };
13041
13540
  }
13042
13541
  /**
13043
- * The file URL to `import()` for a managed npm copy.
13044
- *
13045
- * The framework hands back the package DIRECTORY (`<version dir>/node_modules/<name>`), and a
13046
- * directory is not importable, so the entry is read from the copy's own manifest the way Node would:
13047
- * `exports["."]` first, then `main`.
13048
- */
13049
- async function entryUrlOf(packageDir) {
13050
- let manifest;
13051
- try {
13052
- manifest = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
13053
- } catch {
13054
- return;
13055
- }
13056
- const dot = manifest.exports?.["."];
13057
- const fromExports = typeof dot === "string" ? dot : dot?.default;
13058
- return pathToFileURL(join(packageDir, typeof fromExports === "string" ? fromExports : typeof manifest.main === "string" ? manifest.main : "./index.js")).href;
13059
- }
13060
- /**
13061
- * The resolved directory of a peer in THIS plugin's tree.
13062
- *
13063
- * The framework must reuse the host's own copy for identity-sensitive peers, and it cannot find them
13064
- * by itself: its resolver looks at `process.cwd()` and at its own module URL — neither is the
13065
- * plugin's tree. So the caller hands the directory over explicitly (`NpmPackageSpec.peers[].dir`);
13066
- * without it the item fails with `peer/unsatisfied`.
13067
- */
13068
- function peerDirOf(name) {
13069
- const require = createRequire(import.meta.url);
13070
- try {
13071
- return dirname(require.resolve(`${name}/package.json`));
13072
- } catch {
13073
- try {
13074
- let dir = dirname(require.resolve(name));
13075
- for (let depth = 0; depth < 6; depth += 1) {
13076
- if (dir.endsWith(`/${name}`)) return dir;
13077
- const parent = dirname(dir);
13078
- if (parent === dir) break;
13079
- dir = parent;
13080
- }
13081
- } catch {}
13082
- }
13083
- }
13084
- /** The `mem:compat` item: the family compatibility gate, waited for before anything registers. */
13085
- function compatItem(framework, range, zodDir) {
13086
- return {
13087
- id: COMPAT_ITEM,
13088
- kind: framework.NPM_PACKAGE_KIND,
13089
- spec: {
13090
- name: COMPAT_PACKAGE,
13091
- range,
13092
- peers: [{
13093
- name: "zod",
13094
- ...zodDir === void 0 ? {} : { dir: zodDir }
13095
- }]
13096
- },
13097
- target: { root: "runtime" },
13098
- onMissing: {
13099
- atStartup: "degrade",
13100
- atUse: "degrade"
13101
- },
13102
- startup: "blocking",
13103
- schemaVersion: framework.ITEM_SCHEMA_VERSION
13104
- };
13105
- }
13106
- /**
13107
13542
  * The `mem:pandoc` item: the pinned pandoc release as a `binary-archive`.
13108
13543
  *
13109
13544
  * The packs are `@avantf/mem-provision`'s (`PANDOC_PACKS`), the single source of the URLs and the
@@ -13229,41 +13664,25 @@ async function loadOnce(options) {
13229
13664
  warn(options.log, `${OWN_PREFIX} WARNING — ${FRAMEWORK_PACKAGE} could not be made available; the plugin falls back to its own provisioning and mounts anyway`);
13230
13665
  return;
13231
13666
  }
13232
- const provisioner = framework.createProvisioner({
13233
- home,
13234
- envinitRange: range,
13235
- logger: provisionLogger(options.log),
13236
- policy: {
13237
- autoDownload,
13238
- deadlineMs
13239
- }
13240
- });
13241
- provisioner.register(framework.npmPackageProvider());
13242
- provisioner.declare({
13243
- plugin: PLUGIN,
13244
- items: [compatItem(framework, await compatRange(), peerDirOf("zod"))]
13245
- });
13246
- await provisioner.ensure({
13247
- only: [COMPAT_ITEM],
13248
- deadlineMs,
13249
- onSettled: (entry) => {
13250
- if (entry.action === "failed") warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_ITEM} failed (${String(entry.code)}): ${entry.reason ?? ""}`);
13667
+ {
13668
+ const verdict = interfaceVerdict(framework);
13669
+ if (!baseIsUsable(verdict)) {
13670
+ warn(options.log, `${OWN_PREFIX} WARNING — interface: ${verdict.reason ?? "the loaded base implements another interface generation"}; the base's shared capabilities are NOT used (own prompt defaults, gate skipped, legacy provisioning) and the plugin mounts anyway`);
13671
+ return;
13251
13672
  }
13252
- });
13253
- const disposers = [provisioner];
13673
+ if (verdict.status === "cannot-tell") warn(options.log, `${OWN_PREFIX} WARNING — interface: ${verdict.reason ?? "the interface generation cannot be told"}; using the loaded base anyway ("cannot tell" is never "incompatible")`);
13674
+ }
13675
+ const disposers = [];
13254
13676
  let compat;
13255
- const state = provisioner.resolve(COMPAT_ITEM);
13256
- if (state.state !== "ready") warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_PACKAGE} is unavailable (${describeState(state)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
13257
- else try {
13258
- const module = options.compatModule ?? await importCompat(state.handle.dir);
13259
- if (module === void 0) warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_PACKAGE} has no readable entry at ${state.handle.dir}; the compatibility gate is SKIPPED`);
13260
- else compat = runtimeFromCompat(module);
13677
+ try {
13678
+ compat = runtimeFromCompat(options.compatModule ?? framework);
13261
13679
  } catch (error) {
13262
- warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_PACKAGE} could not be imported (${reasonOf(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
13680
+ warn(options.log, `${OWN_PREFIX} WARNING — the ${COMPAT_PACKAGE} compatibility gate could not be initialised (${reasonOf(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
13263
13681
  }
13264
13682
  let disposed = false;
13265
13683
  return {
13266
13684
  home,
13685
+ kit: framework,
13267
13686
  compat,
13268
13687
  roots: {
13269
13688
  tools: join(home, "tools"),
@@ -13316,19 +13735,6 @@ async function loadOnce(options) {
13316
13735
  }
13317
13736
  };
13318
13737
  }
13319
- /** One line describing why a non-ready item is not ready, safe to put in a WARNING. */
13320
- function describeState(state) {
13321
- if (state.state === "ready") return `ready (${state.handle.source})`;
13322
- if (state.state === "failed" || state.state === "skipped") return `${state.state}: ${state.code}${state.detail === void 0 ? "" : ` (${state.detail})`}`;
13323
- if (state.state === "pending") return "pending (still being fetched)";
13324
- return state.state;
13325
- }
13326
- /** Load the managed copy's entry by file URL; `undefined` when it has no readable entry. */
13327
- async function importCompat(packageDir) {
13328
- const url = await entryUrlOf(packageDir);
13329
- if (url === void 0) return void 0;
13330
- return await import(url);
13331
- }
13332
13738
  //#endregion
13333
13739
  //#region lib/types/open.js
13334
13740
  /**
@@ -13497,7 +13903,7 @@ function zodDef(schema) {
13497
13903
  function jsonSchemaOf(schema) {
13498
13904
  if (typeof schema?.safeParse !== "function") return void 0;
13499
13905
  try {
13500
- return z$1.toJSONSchema(schema, {
13906
+ return z.toJSONSchema(schema, {
13501
13907
  io: "input",
13502
13908
  unrepresentable: "any"
13503
13909
  });
@@ -13663,9 +14069,9 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
13663
14069
  done = true;
13664
14070
  };
13665
14071
  const name = "avantf-mem";
13666
- const Config = z.object({
13667
- mode: z.union([z.const("cordis"), z.const("mcp")]).default("cordis"),
13668
- dataHome: z.string()
14072
+ const Config = z$1.object({
14073
+ mode: z$1.union([z$1.const("cordis"), z$1.const("mcp")]).default("cordis"),
14074
+ dataHome: z$1.string()
13669
14075
  });
13670
14076
  /**
13671
14077
  * Facts whose entity/triple rows the heartbeat may rebuild per beat.
@@ -13864,6 +14270,16 @@ let AvantfMemGateway = (() => {
13864
14270
  super(ctx, "avantfMem");
13865
14271
  this.rt = rt;
13866
14272
  }
14273
+ /**
14274
+ * Run one already-validated tool call and wrap the answer in the contract envelope.
14275
+ *
14276
+ * `T` is the payload the tool key is DECLARED to answer, spelled at each `@Remote` below; this used
14277
+ * to be `unknown` everywhere, which made the host half the one seam in the type chain where nothing
14278
+ * said what crosses the wire. The single `as T` is unavoidable and is the honest part: the dispatch
14279
+ * table is keyed by `ToolSpec.key` (a `string`), so its result is `unknown` by construction. Naming
14280
+ * the type is what turns "the client assumed a shape" into "the engine's own type is what the host
14281
+ * claims to send".
14282
+ */
13867
14283
  async call(key, spec, payload) {
13868
14284
  const parsed = spec.input.safeParse(payload);
13869
14285
  if (!parsed.success) return validationError(spec.name, parsed.error.issues);
@@ -13879,6 +14295,11 @@ let AvantfMemGateway = (() => {
13879
14295
  };
13880
14296
  }
13881
14297
  }
14298
+ /**
14299
+ * Each payload is written as the ENGINE type that produces it (`StoreResult<…>`, or a contract
14300
+ * interface), never as a hand-copied shape: if a store method's result changes, the declaration
14301
+ * here stops compiling instead of quietly misdescribing the wire.
14302
+ */
13882
14303
  remember(args) {
13883
14304
  const payload = defined(args);
13884
14305
  return this.call("remember", REMEMBER_TOOL, {
@@ -13900,6 +14321,12 @@ let AvantfMemGateway = (() => {
13900
14321
  ...payload
13901
14322
  });
13902
14323
  }
14324
+ /**
14325
+ * The one method that keeps `unknown`, and deliberately: its payload is whichever of the seven
14326
+ * corpus actions the caller asked for (a page, one document, an ingest report, a conflict report, a
14327
+ * reindex plan …), so there is no single engine type to name. The engine's `KbDispatch` spells all
14328
+ * seven out; the client names its expectation at each call site (`callRemote<…>(…)`).
14329
+ */
13903
14330
  kb(args) {
13904
14331
  return this.call("kb", KB_TOOL, defined(args));
13905
14332
  }
@@ -14105,8 +14532,8 @@ function startManagedResources(env, rt, logger, waitFor) {
14105
14532
  if (entry.action === "failed" || entry.action === "skipped") {
14106
14533
  logger.warn(`envinit: ${entry.id} is ${entry.action} (${String(entry.code)}${entry.reason === void 0 ? "" : ` — ${entry.reason}`})`);
14107
14534
  if (entry.id === "mem:pandoc") {
14108
- const fallback = legacyToolsDir(rt, env);
14109
- logger.warn(`envinit: falling back to ${fallback} for pandoc (override with tools.dir or AVANTF_PANDOC)`);
14535
+ const fallback = resolveToolsDir(parseToolsConfig(rt.config.common.tools));
14536
+ logger.warn(`envinit: pandoc unavailable from the framework; the converter will use ${fallback} (override with tools.dir or AVANTF_PANDOC)`);
14110
14537
  setPandocProvisioning({
14111
14538
  toolsDir: fallback,
14112
14539
  mirror: rt.config.common.tools.mirror,
@@ -14120,21 +14547,6 @@ function startManagedResources(env, rt, logger, waitFor) {
14120
14547
  logger.info(`envinit: ${entry.id} ${entry.action} (${entry.source}${entry.version === void 0 ? "" : ` ${entry.version}`})`);
14121
14548
  });
14122
14549
  }
14123
- /**
14124
- * The tools directory to use when the framework did NOT deliver pandoc.
14125
- *
14126
- * Resolved through the provisioning layer's own `resolveToolsDir`, so the environment hatch
14127
- * (`AVANTF_TOOLS_DIR`) and the `~` expansion behave here exactly as they do everywhere else — the
14128
- * converter hands `settings.toolsDir` to `ensurePandoc` VERBATIM (`pandocBinary`), so whatever this
14129
- * returns is the final word for the process. `managedRoots` replaced the built-in default, so
14130
- * `config.common.tools.dir` equals the family root even when the operator never touched it; only a
14131
- * resolved value that is STILL the family root means "nobody chose one", and only then may the
14132
- * legacy default apply.
14133
- */
14134
- function legacyToolsDir(rt, env) {
14135
- const resolved = resolveToolsDir(parseToolsConfig(rt.config.common.tools));
14136
- return resolved === env.roots.tools ? defaultToolsDir() : resolved;
14137
- }
14138
14550
  async function apply(ctx, config) {
14139
14551
  const logger = hostLogger(ctx);
14140
14552
  const env = await loadEnvinit({ log: logger });
@@ -14144,7 +14556,7 @@ async function apply(ctx, config) {
14144
14556
  }
14145
14557
  const compat = env?.compat;
14146
14558
  let verdict;
14147
- if (compat === void 0) logger.warn("plugin mount: the dsh compatibility gate is ABSENT (@avantf/dsh-compat unavailable — see the `compat:` WARNING above); mounting the full plugin anyway");
14559
+ if (compat === void 0) logger.warn("plugin mount: the dsh compatibility gate is ABSENT (@avantf/dsh-plugin-base unavailable — see the WARNING above); mounting the full plugin anyway");
14148
14560
  else {
14149
14561
  const run = provision(ctx, logger, compat);
14150
14562
  if (!run.verdict.load) {
@@ -14156,11 +14568,12 @@ async function apply(ctx, config) {
14156
14568
  }
14157
14569
  const toolSpecs = TOOL_SPECS;
14158
14570
  if (config.mode === "mcp") logger.warn("config mode=mcp is not served in-process — mounting the cordis toolset; run @avantf/mem-mcp standalone for MCP");
14159
- logger.info(`plugin mount: mode=${config.mode ?? "cordis"} dataHome=${config.dataHome ?? "(unset → AVANTF_HOME, else ~/.avantf)"}`);
14571
+ const dataHome = configDataHome(config.dataHome);
14572
+ logger.info(`plugin mount: mode=${config.mode ?? "cordis"} dataHome=${dataHome} (configured ${config.dataHome ?? "unset"})`);
14160
14573
  let rt;
14161
14574
  try {
14162
14575
  rt = buildRuntime({
14163
- dataHome: config.dataHome,
14576
+ dataHome,
14164
14577
  logger,
14165
14578
  ...env === void 0 ? {} : { managedRoots: {
14166
14579
  tools: env.roots.tools,
@@ -14217,14 +14630,25 @@ async function apply(ctx, config) {
14217
14630
  logger.info(`trust heartbeat: every ${heartbeatMinutes}min (active-day clock + lifecycle sweep)`);
14218
14631
  return () => clearInterval(timer);
14219
14632
  });
14220
- for (const section of [
14221
- MEMORY_PROMPT_SECTION,
14222
- KNOWLEDGE_PROMPT_SECTION,
14223
- KB_EDIT_PROMPT_SECTION
14224
- ]) {
14633
+ const promptDir = join(rt.config.home, "prompts");
14634
+ const kit = env?.kit;
14635
+ const loadedPrompts = kit?.PromptFiles === void 0 ? promptFileSpecs().map((spec) => ({
14636
+ file: spec.file,
14637
+ path: join(promptDir, spec.file),
14638
+ text: spec.fallback,
14639
+ source: "default",
14640
+ wrote: false
14641
+ })) : new kit.PromptFiles({
14642
+ dir: promptDir,
14643
+ logger
14644
+ }).load(promptFileSpecs());
14645
+ const promptSections = buildPromptSections(loadedPrompts);
14646
+ for (const section of promptSections) {
14225
14647
  ctx.systemPrompt.section(section);
14226
14648
  logger.info(`prompt section registered: ${section.name} (order ${String(section.order)})`);
14227
14649
  }
14650
+ for (const warning of promptTextWarnings(promptSections)) logger.warn(`prompt text: ${warning}`);
14651
+ logger.info(`prompt files: ${promptDir} (${loadedPrompts.map((entry) => `${entry.file}:${entry.source}`).join(", ")})`);
14228
14652
  const hints = /* @__PURE__ */ new WeakMap();
14229
14653
  ctx.on("agent/inbox/inserted", (payload) => {
14230
14654
  const agent = payload.agent;